From 699ac9f879179e58b82bcc04bee5b6f712435717 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Fri, 1 May 2026 23:28:10 +0300 Subject: [PATCH] feat: Add intelligent carrier selection algorithm with machine learning optimization and performance tracking - Add SelectionAlgorithm with scoring, weighting, and ML-based carrier selection - Add SelectionCriteria struct with region, profile type, urgency, and cost/performance weights - Add CarrierScore with performance, reliability, cost, region, and capability scores - Add calculateWeightedScore combining all scores with ML-optimized weights - Add getWeights with urgency-based adjustments --- .../internal/smdp/selection_algorithm.go | 183 ++++++++++ .../internal/smdp/selection_learning.go | 331 ++++++++++++++++++ .../internal/smdp/selection_main.go | 94 +++++ .../internal/smdp/selection_scoring.go | 168 +++++++++ .../internal/smdp/selection_test_advanced.go | 166 +++++++++ .../internal/smdp/selection_test_benchmark.go | 95 +++++ .../internal/smdp/selection_test_core.go | 204 +++++++++++ .../internal/smdp/selection_test_ml.go | 308 ++++++++++++++++ .../internal/smdp/selection_test_scoring.go | 199 +++++++++++ 9 files changed, 1748 insertions(+) create mode 100644 apps/carrier-connector/internal/smdp/selection_algorithm.go create mode 100644 apps/carrier-connector/internal/smdp/selection_learning.go create mode 100644 apps/carrier-connector/internal/smdp/selection_main.go create mode 100644 apps/carrier-connector/internal/smdp/selection_scoring.go create mode 100644 apps/carrier-connector/internal/smdp/selection_test_advanced.go create mode 100644 apps/carrier-connector/internal/smdp/selection_test_benchmark.go create mode 100644 apps/carrier-connector/internal/smdp/selection_test_core.go create mode 100644 apps/carrier-connector/internal/smdp/selection_test_ml.go create mode 100644 apps/carrier-connector/internal/smdp/selection_test_scoring.go diff --git a/apps/carrier-connector/internal/smdp/selection_algorithm.go b/apps/carrier-connector/internal/smdp/selection_algorithm.go new file mode 100644 index 0000000..2d6aa58 --- /dev/null +++ b/apps/carrier-connector/internal/smdp/selection_algorithm.go @@ -0,0 +1,183 @@ +package smdp + +import ( + "math" + "time" + + "github.com/sirupsen/logrus" +) + +// SelectionCriteria defines parameters for carrier selection +type SelectionCriteria struct { + Region string `json:"region"` + ProfileType string `json:"profile_type"` + Urgency string `json:"urgency"` // "low", "medium", "high" + CostSensitivity float64 `json:"cost_sensitivity"` // 0.0-1.0 + PerformanceWeight float64 `json:"performance_weight"` // 0.0-1.0 + ReliabilityWeight float64 `json:"reliability_weight"` // 0.0-1.0 +} + +// CarrierScore represents the scored evaluation of a carrier +type CarrierScore struct { + CarrierID string `json:"carrier_id"` + Carrier *Carrier `json:"carrier"` + TotalScore float64 `json:"total_score"` + PerformanceScore float64 `json:"performance_score"` + ReliabilityScore float64 `json:"reliability_score"` + CostScore float64 `json:"cost_score"` + RegionScore float64 `json:"region_score"` + CapabilityScore float64 `json:"capability_score"` + SelectedAt time.Time `json:"selected_at"` + Reason string `json:"reason"` +} + +// SelectionAlgorithm implements intelligent carrier selection +type SelectionAlgorithm struct { + logger *logrus.Logger + history map[string][]CarrierScore // Selection history per carrier + maxHistory int + learningRate float64 + mlModel *LearningModel // Machine learning model +} + +// NewSelectionAlgorithm creates a new selection algorithm instance +func NewSelectionAlgorithm() *SelectionAlgorithm { + return &SelectionAlgorithm{ + logger: logrus.New(), + history: make(map[string][]CarrierScore), + maxHistory: 100, + learningRate: 0.1, + mlModel: NewLearningModel(), + } +} + +// calculateWeightedScore combines all scores with weights +func (sa *SelectionAlgorithm) calculateWeightedScore(score *CarrierScore, criteria *SelectionCriteria) float64 { + weights := sa.getWeights(criteria) + + total := score.PerformanceScore*weights.performance + + score.ReliabilityScore*weights.reliability + + score.CostScore*weights.cost + + score.RegionScore*weights.region + + score.CapabilityScore*weights.capability + + return math.Min(100.0, total) +} + +// getWeights returns selection weights based on criteria and ML optimization +func (sa *SelectionAlgorithm) getWeights(criteria *SelectionCriteria) struct { + performance float64 + reliability float64 + cost float64 + region float64 + capability float64 +} { + // Get ML-optimized weights as base + mlWeights := sa.mlModel.GetOptimizedWeights() + + weights := struct { + performance float64 + reliability float64 + cost float64 + region float64 + capability float64 + }{ + performance: mlWeights.Performance, + reliability: mlWeights.Reliability, + cost: mlWeights.Cost, + region: mlWeights.Region, + capability: mlWeights.Capability, + } + + // Adjust based on urgency (override ML weights for specific business rules) + switch criteria.Urgency { + case "high": + weights.performance = math.Max(weights.performance, 0.4) + weights.reliability = math.Max(weights.reliability, 0.4) + weights.cost = math.Min(weights.cost, 0.1) + weights.region = math.Min(weights.region, 0.05) + weights.capability = math.Min(weights.capability, 0.05) + case "low": + weights.cost = math.Max(weights.cost, 0.4) + weights.performance = math.Min(weights.performance, 0.2) + weights.reliability = math.Min(weights.reliability, 0.2) + } + + // Apply user-defined weights (business preferences override ML) + if criteria.PerformanceWeight > 0 { + weights.performance = criteria.PerformanceWeight + } + if criteria.ReliabilityWeight > 0 { + weights.reliability = criteria.ReliabilityWeight + } + if criteria.CostSensitivity > 0 { + weights.cost = criteria.CostSensitivity + } + + // Normalize weights + total := weights.performance + weights.reliability + weights.cost + weights.region + weights.capability + if total > 0 { + weights.performance /= total + weights.reliability /= total + weights.cost /= total + weights.region /= total + weights.capability /= total + } + + return weights +} + +// recordSelection records the selection for learning +func (sa *SelectionAlgorithm) recordSelection(score *CarrierScore) { + history := sa.history[score.CarrierID] + history = append(history, *score) + + // Limit history size + if len(history) > sa.maxHistory { + history = history[1:] + } + + sa.history[score.CarrierID] = history +} + +// GetSelectionHistory returns selection history for a carrier +func (sa *SelectionAlgorithm) GetSelectionHistory(carrierID string) []CarrierScore { + return sa.history[carrierID] +} + +// UpdateLearning updates the algorithm based on feedback using ML model +func (sa *SelectionAlgorithm) UpdateLearning(carrierID string, actualPerformance float64) { + history := sa.history[carrierID] + if len(history) == 0 { + return + } + + // Get the most recent selection to get expected score + lastSelection := history[len(history)-1] + expectedScore := lastSelection.TotalScore + + // Update ML model with performance feedback + sa.mlModel.UpdateLearning(carrierID, actualPerformance, expectedScore) + + // Legacy learning rate adjustment for backward compatibility + if actualPerformance < 50.0 { + sa.learningRate *= 0.95 // Reduce learning rate for poor performance + } else { + sa.learningRate *= 1.05 // Increase learning rate for good performance + } +} + +// GetLearningStats returns statistics about the ML learning model +func (sa *SelectionAlgorithm) GetLearningStats() map[string]any { + return sa.mlModel.GetLearningStats() +} + +// PredictPerformance predicts expected performance for a carrier +func (sa *SelectionAlgorithm) PredictPerformance(carrierID string, criteria *SelectionCriteria) float64 { + return sa.mlModel.PredictPerformance(carrierID, criteria) +} + +// GetCarrierPerformance returns detailed performance metrics for a carrier +func (sa *SelectionAlgorithm) GetCarrierPerformance(carrierID string) *PerformanceMetrics { + return sa.mlModel.GetCarrierPerformance(carrierID) +} diff --git a/apps/carrier-connector/internal/smdp/selection_learning.go b/apps/carrier-connector/internal/smdp/selection_learning.go new file mode 100644 index 0000000..e74bbc4 --- /dev/null +++ b/apps/carrier-connector/internal/smdp/selection_learning.go @@ -0,0 +1,331 @@ +package smdp + +import ( + "math" + "time" +) + +// LearningModel represents the machine learning model for carrier selection +type LearningModel struct { + // Performance tracking + carrierPerformance map[string]*PerformanceMetrics + learningRate float64 + adaptationThreshold float64 + + // Weight optimization + baseWeights WeightVector + optimizedWeights WeightVector + + // Historical data + performanceHistory map[string][]float64 + selectionHistory map[string][]SelectionRecord + + // Model parameters + momentum float64 + regularization float64 + decayRate float64 +} + +// PerformanceMetrics tracks carrier performance over time +type PerformanceMetrics struct { + SuccessRate float64 + AverageResponseTime float64 + Reliability float64 + CostEfficiency float64 + LastUpdated time.Time + SampleCount int +} + +// WeightVector represents the selection weights +type WeightVector struct { + Performance float64 + Reliability float64 + Cost float64 + Region float64 + Capability float64 +} + +// SelectionRecord tracks individual selection outcomes +type SelectionRecord struct { + CarrierID string + SelectedAt time.Time + ExpectedScore float64 + ActualScore float64 + Performance float64 + ResponseTime time.Duration + Success bool + SelectionReason string +} + +// NewLearningModel creates a new machine learning model +func NewLearningModel() *LearningModel { + return &LearningModel{ + carrierPerformance: make(map[string]*PerformanceMetrics), + performanceHistory: make(map[string][]float64), + selectionHistory: make(map[string][]SelectionRecord), + learningRate: 0.01, + adaptationThreshold: 0.05, + momentum: 0.9, + regularization: 0.001, + decayRate: 0.995, + baseWeights: WeightVector{ + Performance: 0.3, + Reliability: 0.3, + Cost: 0.2, + Region: 0.1, + Capability: 0.1, + }, + optimizedWeights: WeightVector{ + Performance: 0.3, + Reliability: 0.3, + Cost: 0.2, + Region: 0.1, + Capability: 0.1, + }, + } +} + +// UpdateLearning updates the model with performance feedback using ML techniques +func (lm *LearningModel) UpdateLearning(carrierID string, actualPerformance float64, expectedScore float64) { + // Record the performance outcome + record := SelectionRecord{ + CarrierID: carrierID, + SelectedAt: time.Now(), + ExpectedScore: expectedScore, + ActualScore: actualPerformance, + Performance: actualPerformance, + Success: actualPerformance >= 70.0, // 70% threshold for success + } + + // Add to selection history + history := lm.selectionHistory[carrierID] + history = append(history, record) + + // Keep only last 100 records per carrier + if len(history) > 100 { + history = history[1:] + } + lm.selectionHistory[carrierID] = history + + // Update carrier performance metrics + lm.updatePerformanceMetrics(carrierID, record) + + // Calculate performance prediction error + predictionError := actualPerformance - expectedScore + + // If error is significant, trigger weight optimization + if math.Abs(predictionError) > lm.adaptationThreshold { + lm.optimizeWeights(carrierID, predictionError) + } + + // Apply learning rate decay + lm.learningRate *= lm.decayRate + if lm.learningRate < 0.001 { + lm.learningRate = 0.001 // Minimum learning rate + } +} + +// updatePerformanceMetrics updates the performance metrics for a carrier +func (lm *LearningModel) updatePerformanceMetrics(carrierID string, record SelectionRecord) { + metrics, exists := lm.carrierPerformance[carrierID] + if !exists { + metrics = &PerformanceMetrics{ + LastUpdated: time.Now(), + } + lm.carrierPerformance[carrierID] = metrics + } + + // Update metrics with exponential moving average + alpha := 0.1 // Smoothing factor + + if metrics.SampleCount == 0 { + // First sample + metrics.SuccessRate = 1.0 + if !record.Success { + metrics.SuccessRate = 0.0 + } + metrics.AverageResponseTime = float64(record.ResponseTime.Milliseconds()) + metrics.Reliability = record.Performance / 100.0 + } else { + // Exponential moving average update + if record.Success { + metrics.SuccessRate = alpha*1.0 + (1-alpha)*metrics.SuccessRate + } else { + metrics.SuccessRate = alpha*0.0 + (1-alpha)*metrics.SuccessRate + } + + responseTimeMs := float64(record.ResponseTime.Milliseconds()) + metrics.AverageResponseTime = alpha*responseTimeMs + (1-alpha)*metrics.AverageResponseTime + metrics.Reliability = alpha*(record.Performance/100.0) + (1-alpha)*metrics.Reliability + } + + metrics.CostEfficiency = 1.0 - (float64(record.ResponseTime.Milliseconds()) / 1000.0) // Simple cost proxy + metrics.LastUpdated = time.Now() + metrics.SampleCount++ +} + +// optimizeWeights uses gradient descent to optimize selection weights +func (lm *LearningModel) optimizeWeights(carrierID string, error float64) { + // Calculate gradients based on performance error + gradients := lm.calculateGradients(carrierID, error) + + // Apply gradient descent with momentum + learningRate := lm.learningRate + + // Update optimized weights + lm.optimizedWeights.Performance -= learningRate * gradients.Performance + lm.optimizedWeights.Reliability -= learningRate * gradients.Reliability + lm.optimizedWeights.Cost -= learningRate * gradients.Cost + lm.optimizedWeights.Region -= learningRate * gradients.Region + lm.optimizedWeights.Capability -= learningRate * gradients.Capability + + // Apply regularization to prevent overfitting + lm.applyRegularization() + + // Ensure weights sum to 1 and are non-negative + lm.normalizeWeights() +} + +// calculateGradients calculates the gradient for each weight based on error +func (lm *LearningModel) calculateGradients(carrierID string, error float64) WeightVector { + metrics := lm.carrierPerformance[carrierID] + if metrics == nil { + return WeightVector{} + } + + // Calculate gradients based on how each factor contributed to the error + gradients := WeightVector{} + + // Performance gradient: higher error suggests need to adjust performance weight + gradients.Performance = error * (metrics.Reliability - 0.5) * 2.0 + + // Reliability gradient: adjust based on success rate + gradients.Reliability = error * (metrics.SuccessRate - 0.5) * 2.0 + + // Cost gradient: adjust based on cost efficiency + gradients.Cost = error * (metrics.CostEfficiency - 0.5) * 2.0 + + // Region and capability gradients (simplified) + gradients.Region = error * 0.1 + gradients.Capability = error * 0.1 + + return gradients +} + +// applyRegularization applies L2 regularization to prevent overfitting +func (lm *LearningModel) applyRegularization() { + regFactor := lm.regularization + + lm.optimizedWeights.Performance *= (1.0 - regFactor) + lm.optimizedWeights.Reliability *= (1.0 - regFactor) + lm.optimizedWeights.Cost *= (1.0 - regFactor) + lm.optimizedWeights.Region *= (1.0 - regFactor) + lm.optimizedWeights.Capability *= (1.0 - regFactor) +} + +// normalizeWeights ensures weights sum to 1 and are non-negative +func (lm *LearningModel) normalizeWeights() { + // Ensure non-negative + if lm.optimizedWeights.Performance < 0 { + lm.optimizedWeights.Performance = 0 + } + if lm.optimizedWeights.Reliability < 0 { + lm.optimizedWeights.Reliability = 0 + } + if lm.optimizedWeights.Cost < 0 { + lm.optimizedWeights.Cost = 0 + } + if lm.optimizedWeights.Region < 0 { + lm.optimizedWeights.Region = 0 + } + if lm.optimizedWeights.Capability < 0 { + lm.optimizedWeights.Capability = 0 + } + + // Normalize to sum to 1 + total := lm.optimizedWeights.Performance + lm.optimizedWeights.Reliability + + lm.optimizedWeights.Cost + lm.optimizedWeights.Region + lm.optimizedWeights.Capability + + if total > 0 { + lm.optimizedWeights.Performance /= total + lm.optimizedWeights.Reliability /= total + lm.optimizedWeights.Cost /= total + lm.optimizedWeights.Region /= total + lm.optimizedWeights.Capability /= total + } else { + // Fallback to base weights if all weights become zero + lm.optimizedWeights = lm.baseWeights + } +} + +// GetOptimizedWeights returns the current optimized weights for selection +func (lm *LearningModel) GetOptimizedWeights() WeightVector { + return lm.optimizedWeights +} + +// GetCarrierPerformance returns performance metrics for a carrier +func (lm *LearningModel) GetCarrierPerformance(carrierID string) *PerformanceMetrics { + return lm.carrierPerformance[carrierID] +} + +// PredictPerformance predicts the expected performance for a carrier +func (lm *LearningModel) PredictPerformance(carrierID string, criteria *SelectionCriteria) float64 { + metrics := lm.carrierPerformance[carrierID] + if metrics == nil { + return 75.0 // Default prediction for new carriers + } + + // Use weighted combination of performance metrics + weights := lm.optimizedWeights + + prediction := metrics.SuccessRate*weights.Reliability*100 + + (1.0-metrics.AverageResponseTime/1000.0)*weights.Performance*100 + + metrics.CostEfficiency*weights.Cost*100 + + 0.5*weights.Region*100 + // Simplified region prediction + 0.5*weights.Capability*100 // Simplified capability prediction + + return math.Min(100.0, math.Max(0.0, prediction)) +} + +// GetLearningStats returns statistics about the learning model +func (lm *LearningModel) GetLearningStats() map[string]any { + stats := make(map[string]any) + + stats["learning_rate"] = lm.learningRate + stats["total_carriers_tracked"] = len(lm.carrierPerformance) + stats["optimized_weights"] = lm.optimizedWeights + stats["base_weights"] = lm.baseWeights + + // Calculate overall performance improvement + totalImprovement := 0.0 + sampleCount := 0 + + for _, history := range lm.selectionHistory { + if len(history) > 10 { // Only consider carriers with sufficient data + firstHalf := history[:len(history)/2] + secondHalf := history[len(history)/2:] + + firstAvg := 0.0 + for _, record := range firstHalf { + firstAvg += record.ActualScore + } + firstAvg /= float64(len(firstHalf)) + + secondAvg := 0.0 + for _, record := range secondHalf { + secondAvg += record.ActualScore + } + secondAvg /= float64(len(secondHalf)) + + improvement := secondAvg - firstAvg + totalImprovement += improvement + sampleCount++ + } + } + + if sampleCount > 0 { + stats["average_performance_improvement"] = totalImprovement / float64(sampleCount) + } + + return stats +} diff --git a/apps/carrier-connector/internal/smdp/selection_main.go b/apps/carrier-connector/internal/smdp/selection_main.go new file mode 100644 index 0000000..252d829 --- /dev/null +++ b/apps/carrier-connector/internal/smdp/selection_main.go @@ -0,0 +1,94 @@ +package smdp + +import ( + "context" + "fmt" + "sort" + "time" + + "github.com/sirupsen/logrus" +) + +// SelectOptimalCarrier selects the best carrier based on multiple criteria +func (sa *SelectionAlgorithm) SelectOptimalCarrier( + ctx context.Context, + carriers []*Carrier, + criteria *SelectionCriteria, +) (*CarrierScore, error) { + if len(carriers) == 0 { + return nil, fmt.Errorf("no carriers available for selection") + } + + sa.logger.WithFields(logrus.Fields{ + "region": criteria.Region, + "profile_type": criteria.ProfileType, + "urgency": criteria.Urgency, + "carrier_count": len(carriers), + }).Info("Starting optimal carrier selection") + + // Score all carriers + scores := make([]*CarrierScore, 0, len(carriers)) + for _, carrier := range carriers { + if !carrier.IsActive || carrier.HealthStatus != CarrierStatusHealthy { + continue + } + + score := sa.scoreCarrier(carrier, criteria) + scores = append(scores, score) + } + + if len(scores) == 0 { + return nil, fmt.Errorf("no healthy carriers available") + } + + // Sort by total score (descending) + sort.Slice(scores, func(i, j int) bool { + return scores[i].TotalScore > scores[j].TotalScore + }) + + // Select the best carrier + bestScore := scores[0] + sa.recordSelection(bestScore) + + sa.logger.WithFields(logrus.Fields{ + "selected_carrier": bestScore.CarrierID, + "total_score": bestScore.TotalScore, + "performance": bestScore.PerformanceScore, + "reliability": bestScore.ReliabilityScore, + "cost": bestScore.CostScore, + }).Info("Optimal carrier selected") + + return bestScore, nil +} + +// scoreCarrier calculates a comprehensive score for a carrier +func (sa *SelectionAlgorithm) scoreCarrier(carrier *Carrier, criteria *SelectionCriteria) *CarrierScore { + score := &CarrierScore{ + Carrier: carrier, + CarrierID: carrier.ID, + SelectedAt: time.Now(), + } + + // Performance score (0-100) + score.PerformanceScore = sa.calculatePerformanceScore(carrier) + + // Reliability score (0-100) + score.ReliabilityScore = sa.calculateReliabilityScore(carrier) + + // Cost score (0-100, higher is better/cheaper) + score.CostScore = sa.calculateCostScore(carrier, criteria) + + // Region compatibility score (0-100) + score.RegionScore = sa.calculateRegionScore(carrier, criteria.Region) + + // Capability score (0-100) + score.CapabilityScore = sa.calculateCapabilityScore(carrier, criteria.ProfileType) + + // Calculate weighted total score + score.TotalScore = sa.calculateWeightedScore(score, criteria) + + // Generate selection reason + score.Reason = sa.generateReason(score, criteria) + + return score +} diff --git a/apps/carrier-connector/internal/smdp/selection_scoring.go b/apps/carrier-connector/internal/smdp/selection_scoring.go new file mode 100644 index 0000000..d11e365 --- /dev/null +++ b/apps/carrier-connector/internal/smdp/selection_scoring.go @@ -0,0 +1,168 @@ +package smdp + +import ( + "fmt" + "math" + "slices" +) + +// calculatePerformanceScore evaluates carrier performance metrics +func (sa *SelectionAlgorithm) calculatePerformanceScore(carrier *Carrier) float64 { + metrics := carrier.Metrics + if metrics.TotalRequests == 0 { + return 50.0 // Neutral score for new carriers + } + + // Success rate (40% weight) + successRate := float64(metrics.SuccessfulRequests) / float64(metrics.TotalRequests) + successScore := successRate * 40.0 + + // Response time score (30% weight) - lower is better + responseScore := 30.0 + if metrics.AverageResponseTime > 0 { + // Normalize: 1s = 0 points, 100ms = 30 points + responseMs := metrics.AverageResponseTime.Milliseconds() + if responseMs <= 100 { + responseScore = 30.0 + } else if responseMs >= 1000 { + responseScore = 0.0 + } else { + responseScore = 30.0 * (1.0 - float64(responseMs-100)/900.0) + } + } + + // Request rate score (30% weight) - moderate rate is optimal + requestScore := 15.0 // Base score + if metrics.RequestRate > 0 && metrics.RequestRate <= 100 { + requestScore = 30.0 + } else if metrics.RequestRate > 100 { + // Penalize very high rates (potential overload) + requestScore = 30.0 * math.Max(0, 1.0-metrics.RequestRate/1000.0) + } + + return successScore + responseScore + requestScore +} + +// calculateReliabilityScore evaluates carrier reliability +func (sa *SelectionAlgorithm) calculateReliabilityScore(carrier *Carrier) float64 { + // Health status (40% weight) + healthScore := 0.0 + switch carrier.HealthStatus { + case CarrierStatusHealthy: + healthScore = 40.0 + case CarrierStatusDegraded: + healthScore = 20.0 + case CarrierStatusUnhealthy: + healthScore = 0.0 + default: + healthScore = 10.0 + } + + // Uptime based on recent performance (30% weight) + uptimeScore := 30.0 + if carrier.Metrics.TotalRequests > 0 { + successRate := float64(carrier.Metrics.SuccessfulRequests) / float64(carrier.Metrics.TotalRequests) + uptimeScore = successRate * 30.0 + } + + // Priority score (30% weight) - higher priority = more reliable + priorityScore := float64(carrier.Priority) / 100.0 * 30.0 + if priorityScore > 30.0 { + priorityScore = 30.0 + } + + return healthScore + uptimeScore + priorityScore +} + +// calculateCostScore evaluates cost effectiveness +func (sa *SelectionAlgorithm) calculateCostScore(carrier *Carrier, criteria *SelectionCriteria) float64 { + // This would integrate with actual pricing data + // For now, use priority as a proxy (higher priority = potentially more expensive) + costScore := 100.0 - float64(carrier.Priority) + if costScore < 0 { + costScore = 0 + } + + // Apply cost sensitivity + if criteria.CostSensitivity > 0.5 { + costScore *= 1.5 // Boost cost score for cost-sensitive requests + } + + return costScore +} + +// calculateRegionScore evaluates regional compatibility +func (sa *SelectionAlgorithm) calculateRegionScore(carrier *Carrier, region string) float64 { + if region == "" { + return 50.0 // Neutral score if no region specified + } + + // Check if carrier supports the region + if carrier.CountryCode == region { + return 100.0 + } + + // Check regional compatibility through MCC + // This would require a region-to-MCC mapping + // For now, return neutral score + return 50.0 +} + +// calculateCapabilityScore evaluates carrier capabilities +func (sa *SelectionAlgorithm) calculateCapabilityScore(carrier *Carrier, profileType string) float64 { + capabilities := carrier.Capabilities + score := 50.0 // Base score + + // Check if profile type is supported + if slices.Contains(capabilities.SupportedProfileTypes, profileType) { + score += 30.0 + } + + // Check for advanced features + hasBulkDownload := false + hasRemoteProvisioning := false + for _, feature := range capabilities.Features { + if feature == "bulk_download" { + hasBulkDownload = true + } + if feature == "remote_provisioning" { + hasRemoteProvisioning = true + } + } + + if hasBulkDownload { + score += 10.0 + } + if hasRemoteProvisioning { + score += 10.0 + } + + return math.Min(100.0, score) +} + +// generateReason creates a human-readable selection reason +func (sa *SelectionAlgorithm) generateReason(score *CarrierScore, criteria *SelectionCriteria) string { + reasons := []string{} + + if score.PerformanceScore > 80 { + reasons = append(reasons, "excellent performance") + } + if score.ReliabilityScore > 80 { + reasons = append(reasons, "high reliability") + } + if score.CostScore > 80 { + reasons = append(reasons, "cost-effective") + } + if score.RegionScore > 80 { + reasons = append(reasons, "region-compatible") + } + if score.CapabilityScore > 80 { + reasons = append(reasons, "full capability support") + } + + if len(reasons) == 0 { + return "best available option" + } + + return fmt.Sprintf("selected for %s", reasons[0]) +} diff --git a/apps/carrier-connector/internal/smdp/selection_test_advanced.go b/apps/carrier-connector/internal/smdp/selection_test_advanced.go new file mode 100644 index 0000000..268aa96 --- /dev/null +++ b/apps/carrier-connector/internal/smdp/selection_test_advanced.go @@ -0,0 +1,166 @@ +package smdp + +import ( + "context" + "testing" +) + +// TestEdgeCases tests edge cases and boundary conditions +func TestEdgeCases(t *testing.T) { + selector := NewSelectionAlgorithm() + + // Test empty carrier list + t.Run("EmptyCarrierList", func(t *testing.T) { + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + _, err := selector.SelectOptimalCarrier(context.Background(), []*Carrier{}, criteria) + if err == nil { + t.Error("Expected error for empty carrier list") + } + }) + + // Test all unhealthy carriers + t.Run("AllUnhealthyCarriers", func(t *testing.T) { + unhealthyCarriers := []*Carrier{ + createUnhealthyCarrier("broken1", "Broken 1", "US"), + createUnhealthyCarrier("broken2", "Broken 2", "US"), + } + + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + _, err := selector.SelectOptimalCarrier(context.Background(), unhealthyCarriers, criteria) + if err == nil { + t.Error("Expected error when all carriers are unhealthy") + } + }) + + // Test inactive carriers + t.Run("InactiveCarriers", func(t *testing.T) { + inactiveCarriers := []*Carrier{ + createInactiveCarrier("inactive1", "Inactive 1", "US"), + createInactiveCarrier("inactive2", "Inactive 2", "US"), + } + + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + _, err := selector.SelectOptimalCarrier(context.Background(), inactiveCarriers, criteria) + if err == nil { + t.Error("Expected error when all carriers are inactive") + } + }) +} + +// TestMultipleSelections tests behavior with multiple selections +func TestMultipleSelections(t *testing.T) { + selector := NewSelectionAlgorithm() + + carriers := []*Carrier{ + createTestCarrier("carrier1", "Carrier 1", "US", 90, 95, 100, 5.0), + createTestCarrier("carrier2", "Carrier 2", "US", 85, 98, 120, 4.0), + createTestCarrier("carrier3", "Carrier 3", "US", 80, 97, 110, 6.0), + } + + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + // Perform multiple selections + selections := make(map[string]int) + for i := range 20 { + score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria) + if err != nil { + t.Fatalf("Selection %d failed: %v", i, err) + } + selections[score.CarrierID]++ + } + + // Check that we have some distribution + totalSelections := 0 + for _, count := range selections { + totalSelections += count + } + if totalSelections != 20 { + t.Errorf("Expected 20 total selections, got %d", totalSelections) + } + + t.Logf("Selection distribution: %+v", selections) +} + +// TestLearningFeedback tests the learning feedback mechanism +func TestLearningFeedback(t *testing.T) { + selector := NewSelectionAlgorithm() + + carriers := []*Carrier{ + createTestCarrier("learning-carrier", "Learning Carrier", "US", 80, 90, 150, 5.0), + } + + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + // Initial selection + _, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria) + if err != nil { + t.Fatalf("Initial selection failed: %v", err) + } + + initialLearningRate := selector.learningRate + + // Update with poor performance + selector.UpdateLearning("learning-carrier", 30.0) + learningRateAfterPoor := selector.learningRate + + // Update with good performance + selector.UpdateLearning("learning-carrier", 90.0) + learningRateAfterGood := selector.learningRate + + // Verify learning rate changes + if learningRateAfterPoor >= initialLearningRate { + t.Error("Learning rate should decrease after poor performance") + } + + if learningRateAfterGood <= learningRateAfterPoor { + t.Error("Learning rate should increase after good performance") + } + + t.Logf("Learning rate progression: %.4f -> %.4f -> %.4f", + initialLearningRate, learningRateAfterPoor, learningRateAfterGood) +} + +// Helper function to create inactive carrier +func createInactiveCarrier(id, name, country string) *Carrier { + carrier := createTestCarrier(id, name, country, 50, 50, 500, 1.0) + carrier.IsActive = false + return carrier +} diff --git a/apps/carrier-connector/internal/smdp/selection_test_benchmark.go b/apps/carrier-connector/internal/smdp/selection_test_benchmark.go new file mode 100644 index 0000000..2655d5f --- /dev/null +++ b/apps/carrier-connector/internal/smdp/selection_test_benchmark.go @@ -0,0 +1,95 @@ +package smdp + +import ( + "context" + "testing" +) + +// BenchmarkSelectionAlgorithm benchmarks the selection algorithm performance +func BenchmarkSelectionAlgorithm(b *testing.B) { + selector := NewSelectionAlgorithm() + + carriers := make([]*Carrier, 100) + for i := range 100 { + carriers[i] = createTestCarrier( + "carrier-"+string(rune(i)), + "Carrier "+string(rune(i)), + "US", + 50+i%50, + 80+i%20, + 100+int64(i*10), + 1.0+float64(i)*0.1, + ) + } + + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria) + if err != nil { + b.Fatalf("Selection failed: %v", err) + } + } +} + +// BenchmarkScoring benchmarks individual scoring functions +func BenchmarkScoring(b *testing.B) { + selector := NewSelectionAlgorithm() + carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0) + criteria := &SelectionCriteria{CostSensitivity: 0.5} + + b.Run("PerformanceScore", func(b *testing.B) { + for i := 0; i < b.N; i++ { + selector.calculatePerformanceScore(carrier) + } + }) + + b.Run("ReliabilityScore", func(b *testing.B) { + for i := 0; i < b.N; i++ { + selector.calculateReliabilityScore(carrier) + } + }) + + b.Run("CostScore", func(b *testing.B) { + for i := 0; i < b.N; i++ { + selector.calculateCostScore(carrier, criteria) + } + }) + + b.Run("RegionScore", func(b *testing.B) { + for i := 0; i < b.N; i++ { + selector.calculateRegionScore(carrier, "US") + } + }) + + b.Run("CapabilityScore", func(b *testing.B) { + for i := 0; i < b.N; i++ { + selector.calculateCapabilityScore(carrier, "operational") + } + }) +} + +// BenchmarkWeightCalculation benchmarks weight calculation +func BenchmarkWeightCalculation(b *testing.B) { + selector := NewSelectionAlgorithm() + + criteria := &SelectionCriteria{ + Urgency: "medium", + PerformanceWeight: 0.4, + ReliabilityWeight: 0.3, + CostSensitivity: 0.3, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + selector.getWeights(criteria) + } +} diff --git a/apps/carrier-connector/internal/smdp/selection_test_core.go b/apps/carrier-connector/internal/smdp/selection_test_core.go new file mode 100644 index 0000000..64e7005 --- /dev/null +++ b/apps/carrier-connector/internal/smdp/selection_test_core.go @@ -0,0 +1,204 @@ +package smdp + +import ( + "context" + "testing" + "time" + + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/config" +) + +// TestSelectionAlgorithm tests the intelligent carrier selection algorithm +func TestSelectionAlgorithm(t *testing.T) { + // Create selection algorithm + selector := NewSelectionAlgorithm() + + // Create test carriers with different characteristics + carriers := []*Carrier{ + createTestCarrier("att-us", "AT&T US", "US", 90, 98, 150, 10.5), + createTestCarrier("verizon-us", "Verizon US", "US", 85, 99, 120, 8.2), + createTestCarrier("tmobile-de", "T-Mobile DE", "DE", 75, 95, 200, 6.8), + createTestCarrier("orange-fr", "Orange FR", "FR", 70, 95, 180, 4.5), + } + + // Test case 1: High priority US request + t.Run("HighPriorityUSRequest", func(t *testing.T) { + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "high", + CostSensitivity: 0.2, + PerformanceWeight: 0.6, + ReliabilityWeight: 0.6, + } + + score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria) + if err != nil { + t.Fatalf("Failed to select optimal carrier: %v", err) + } + + // Should select a US carrier with high performance + if score.CarrierID != "att-us" && score.CarrierID != "verizon-us" { + t.Errorf("Expected US carrier, got %s", score.CarrierID) + } + + if score.TotalScore < 80 { + t.Errorf("Expected high score for high priority request, got %.2f", score.TotalScore) + } + + t.Logf("Selected carrier: %s with score %.2f", score.CarrierID, score.TotalScore) + }) + + // Test case 2: Cost-optimized European request + t.Run("CostOptimizedEURequest", func(t *testing.T) { + criteria := &SelectionCriteria{ + Region: "DE", + ProfileType: "operational", + Urgency: "low", + CostSensitivity: 0.8, + PerformanceWeight: 0.2, + ReliabilityWeight: 0.3, + } + + score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria) + if err != nil { + t.Fatalf("Failed to select optimal carrier: %v", err) + } + + // Should prefer European carrier for better region compatibility + if score.CarrierID != "tmobile-de" && score.CarrierID != "orange-fr" { + t.Logf("Note: Selected non-EU carrier %s for DE region", score.CarrierID) + } + + t.Logf("Selected carrier: %s with score %.2f", score.CarrierID, score.TotalScore) + }) + + // Test case 3: Balanced global request + t.Run("BalancedGlobalRequest", func(t *testing.T) { + criteria := &SelectionCriteria{ + Region: "", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria) + if err != nil { + t.Fatalf("Failed to select optimal carrier: %v", err) + } + + // Should select the best overall performer + if score.TotalScore < 70 { + t.Errorf("Expected reasonable score for balanced request, got %.2f", score.TotalScore) + } + + t.Logf("Selected carrier: %s with score %.2f", score.CarrierID, score.TotalScore) + }) + + // Test case 4: No healthy carriers available + t.Run("NoHealthyCarriers", func(t *testing.T) { + unhealthyCarriers := []*Carrier{ + createUnhealthyCarrier("broken-carrier", "Broken Carrier", "US"), + } + + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + _, err := selector.SelectOptimalCarrier(context.Background(), unhealthyCarriers, criteria) + if err == nil { + t.Error("Expected error when no healthy carriers available") + } + + t.Logf("Correctly returned error: %v", err) + }) +} + +// TestSelectionLearning tests the learning capabilities +func TestSelectionLearning(t *testing.T) { + selector := NewSelectionAlgorithm() + + carriers := []*Carrier{ + createTestCarrier("test-carrier", "Test Carrier", "US", 80, 90, 150, 5.0), + } + + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + // Perform selection + score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria) + if err != nil { + t.Fatalf("Failed to select optimal carrier: %v", err) + } + + // Check selection history + history := selector.GetSelectionHistory("test-carrier") + if len(history) != 1 { + t.Errorf("Expected 1 selection in history, got %d", len(history)) + } + + // Update learning with performance feedback + selector.UpdateLearning("test-carrier", 85.0) + + // Perform another selection to see if learning affects results + score2, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria) + if err != nil { + t.Fatalf("Failed to select optimal carrier on second attempt: %v", err) + } + + t.Logf("First selection score: %.2f, Second selection score: %.2f", + score.TotalScore, score2.TotalScore) +} + +// Helper functions + +func createTestCarrier(id, name, country string, priority, successRate int, responseTime int64, requestRate float64) *Carrier { + return &Carrier{ + ID: id, + Name: name, + CountryCode: country, + MCC: "310", + MNC: "410", + IsActive: true, + Priority: priority, + HealthStatus: CarrierStatusHealthy, + LastHealthCheck: time.Now(), + ES2Config: &config.ES2Config{ + BaseURL: "https://example.com", + APIKey: "test-key", + InsecureSkipVerify: false, + FunctionalityRequesterID: "telecom-platform", + }, + Capabilities: &CarrierCapabilities{ + SupportedProfileTypes: []string{"operational", "testing"}, + Features: []string{"bulk_download"}, + MaxConcurrentRequests: 100, + }, + Metrics: &CarrierMetrics{ + TotalRequests: uint64(1000 * requestRate), + SuccessfulRequests: uint64(float64(1000*requestRate) * float64(successRate) / 100), + FailedRequests: uint64(float64(1000*requestRate) * float64(100-successRate) / 100), + AverageResponseTime: time.Duration(responseTime) * time.Millisecond, + RequestRate: requestRate, + }, + } +} + +func createUnhealthyCarrier(id, name, country string) *Carrier { + carrier := createTestCarrier(id, name, country, 50, 50, 500, 1.0) + carrier.HealthStatus = CarrierStatusUnhealthy + return carrier +} diff --git a/apps/carrier-connector/internal/smdp/selection_test_ml.go b/apps/carrier-connector/internal/smdp/selection_test_ml.go new file mode 100644 index 0000000..ed3575f --- /dev/null +++ b/apps/carrier-connector/internal/smdp/selection_test_ml.go @@ -0,0 +1,308 @@ +package smdp + +import ( + "context" + "testing" +) + +// TestMachineLearningModel tests the ML learning capabilities +func TestMachineLearningModel(t *testing.T) { + mlModel := NewLearningModel() + + // Test initial state + t.Run("InitialState", func(t *testing.T) { + weights := mlModel.GetOptimizedWeights() + + // Should start with base weights + if weights.Performance < 0.29 || weights.Performance > 0.31 { + t.Errorf("Expected performance weight around 0.3, got %.3f", weights.Performance) + } + + stats := mlModel.GetLearningStats() + if stats["learning_rate"].(float64) <= 0 { + t.Error("Learning rate should be positive") + } + }) + + // Test learning updates + t.Run("LearningUpdates", func(t *testing.T) { + carrierID := "test-ml-carrier" + + // Simulate multiple selections with feedback + for i := range 10 { + expectedScore := float64(70 + i*2) // Improving expected scores + actualScore := float64(65 + i*3) // Actual performance + + mlModel.UpdateLearning(carrierID, actualScore, expectedScore) + } + + // Check that weights have been optimized + optimizedWeights := mlModel.GetOptimizedWeights() + originalWeights := WeightVector{Performance: 0.3, Reliability: 0.3, Cost: 0.2, Region: 0.1, Capability: 0.1} + + // Weights should have changed from original + if optimizedWeights.Performance == originalWeights.Performance && + optimizedWeights.Reliability == originalWeights.Reliability { + t.Error("Expected weights to change after learning") + } + + // Weights should still sum to 1 + total := optimizedWeights.Performance + optimizedWeights.Reliability + + optimizedWeights.Cost + optimizedWeights.Region + optimizedWeights.Capability + if total < 0.99 || total > 1.01 { + t.Errorf("Weights should sum to 1.0, got %.3f", total) + } + }) + + // Test performance metrics tracking + t.Run("PerformanceMetrics", func(t *testing.T) { + carrierID := "metrics-carrier" + + // Add some performance data + mlModel.UpdateLearning(carrierID, 85.0, 80.0) + mlModel.UpdateLearning(carrierID, 90.0, 85.0) + mlModel.UpdateLearning(carrierID, 78.0, 82.0) + + metrics := mlModel.GetCarrierPerformance(carrierID) + if metrics == nil { + t.Fatal("Expected performance metrics for carrier") + } + + if metrics.SampleCount != 3 { + t.Errorf("Expected 3 samples, got %d", metrics.SampleCount) + } + + if metrics.SuccessRate < 0 || metrics.SuccessRate > 1 { + t.Errorf("Success rate should be between 0 and 1, got %.3f", metrics.SuccessRate) + } + }) + + // Test performance prediction + t.Run("PerformancePrediction", func(t *testing.T) { + carrierID := "prediction-carrier" + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + // Add training data + for i := range 5 { + mlModel.UpdateLearning(carrierID, 80.0+float64(i*2), 75.0+float64(i)) + } + + prediction := mlModel.PredictPerformance(carrierID, criteria) + if prediction < 0 || prediction > 100 { + t.Errorf("Prediction should be between 0 and 100, got %.2f", prediction) + } + + // Prediction for unknown carrier should return default + unknownPrediction := mlModel.PredictPerformance("unknown-carrier", criteria) + if unknownPrediction != 75.0 { + t.Errorf("Expected default prediction for unknown carrier, got %.2f", unknownPrediction) + } + }) +} + +// TestMLIntegration tests the integration of ML with SelectionAlgorithm +func TestMLIntegration(t *testing.T) { + selector := NewSelectionAlgorithm() + + // Create test carriers + carriers := []*Carrier{ + createTestCarrier("ml-test-1", "ML Test 1", "US", 80, 85, 150, 5.0), + createTestCarrier("ml-test-2", "ML Test 2", "US", 75, 90, 120, 4.0), + } + + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + // Test initial selection + t.Run("InitialSelection", func(t *testing.T) { + score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria) + if err != nil { + t.Fatalf("Selection failed: %v", err) + } + + // Provide learning feedback + selector.UpdateLearning(score.CarrierID, 85.0) + + // Check learning stats + stats := selector.GetLearningStats() + if stats["total_carriers_tracked"].(int) == 0 { + t.Error("Expected carriers to be tracked in ML model") + } + }) + + // Test learning over multiple iterations + t.Run("IterativeLearning", func(t *testing.T) { + weightsBefore := selector.mlModel.GetOptimizedWeights() + + // Perform multiple selections with feedback + for i := range 5 { + score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria) + if err != nil { + t.Fatalf("Selection %d failed: %v", i, err) + } + + // Simulate performance feedback (varying performance) + actualPerformance := 70.0 + float64(i*3) + selector.UpdateLearning(score.CarrierID, actualPerformance) + } + + weightsAfter := selector.mlModel.GetOptimizedWeights() + + // Weights should have been optimized + if weightsAfter.Performance == weightsBefore.Performance && + weightsAfter.Reliability == weightsBefore.Reliability { + t.Error("Expected weights to change after learning iterations") + } + + // Check performance prediction + prediction := selector.PredictPerformance("ml-test-1", criteria) + if prediction < 0 || prediction > 100 { + t.Errorf("Prediction should be between 0 and 100, got %.2f", prediction) + } + }) + + // Test carrier performance metrics + t.Run("CarrierPerformanceMetrics", func(t *testing.T) { + // Get performance metrics for a carrier + metrics := selector.GetCarrierPerformance("ml-test-1") + if metrics == nil { + t.Error("Expected performance metrics for carrier") + } + + if metrics.SampleCount == 0 { + t.Error("Expected sample count > 0 after learning") + } + }) +} + +// TestMLWeightOptimization tests the weight optimization logic +func TestMLWeightOptimization(t *testing.T) { + mlModel := NewLearningModel() + + t.Run("GradientDescent", func(t *testing.T) { + carrierID := "gradient-test" + + // Simulate consistent poor performance to trigger weight optimization + for range 10 { + expectedScore := 80.0 + actualScore := 50.0 // Consistently poor performance + + mlModel.UpdateLearning(carrierID, actualScore, expectedScore) + } + + weights := mlModel.GetOptimizedWeights() + + // After consistent poor performance, weights should be adjusted + // This is a simplified test - in practice, the optimization is more complex + total := weights.Performance + weights.Reliability + weights.Cost + weights.Region + weights.Capability + if total < 0.99 || total > 1.01 { + t.Errorf("Weights should still sum to 1.0 after optimization, got %.3f", total) + } + + // All weights should be non-negative + if weights.Performance < 0 || weights.Reliability < 0 || weights.Cost < 0 || + weights.Region < 0 || weights.Capability < 0 { + t.Error("All weights should be non-negative") + } + }) + + t.Run("Regularization", func(t *testing.T) { + // Test that regularization prevents extreme weight values + carrierID := "regularization-test" + + // Simulate extreme performance variations + for i := range 20 { + expectedScore := 80.0 + actualScore := 20.0 + float64(i*3) // Wide range of performance + + mlModel.UpdateLearning(carrierID, actualScore, expectedScore) + } + + weights := mlModel.GetOptimizedWeights() + + // Weights should be reasonable (not too extreme) + for _, weight := range []float64{ + weights.Performance, weights.Reliability, weights.Cost, + weights.Region, weights.Capability} { + if weight < 0.05 || weight > 0.7 { + t.Errorf("Weight %.3f seems too extreme (should be between 0.05 and 0.7)", weight) + } + } + }) +} + +// TestMLLearningRate tests the learning rate adaptation +func TestMLLearningRate(t *testing.T) { + mlModel := NewLearningModel() + + initialLearningRate := mlModel.GetLearningStats()["learning_rate"].(float64) + + // Add some learning data + for range 10 { + mlModel.UpdateLearning("rate-test", 75.0, 70.0) + } + + finalLearningRate := mlModel.GetLearningStats()["learning_rate"].(float64) + + // Learning rate should have decayed + if finalLearningRate >= initialLearningRate { + t.Error("Learning rate should decay over time") + } + + // Learning rate should not go below minimum + if finalLearningRate < 0.001 { + t.Error("Learning rate should not go below minimum threshold") + } +} + +// BenchmarkMLModel benchmarks the ML model performance +func BenchmarkMLModel(b *testing.B) { + mlModel := NewLearningModel() + carrierID := "benchmark-carrier" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + mlModel.UpdateLearning(carrierID, 75.0, 70.0) + } +} + +// BenchmarkSelectionWithML benchmarks selection with ML integration +func BenchmarkSelectionWithML(b *testing.B) { + selector := NewSelectionAlgorithm() + + carriers := []*Carrier{ + createTestCarrier("benchmark-1", "Benchmark 1", "US", 80, 85, 150, 5.0), + createTestCarrier("benchmark-2", "Benchmark 2", "US", 75, 90, 120, 4.0), + } + + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria) + if err != nil { + b.Fatalf("Selection failed: %v", err) + } + selector.UpdateLearning("benchmark-1", 75.0) + } +} diff --git a/apps/carrier-connector/internal/smdp/selection_test_scoring.go b/apps/carrier-connector/internal/smdp/selection_test_scoring.go new file mode 100644 index 0000000..3a674e9 --- /dev/null +++ b/apps/carrier-connector/internal/smdp/selection_test_scoring.go @@ -0,0 +1,199 @@ +package smdp + +import ( + "context" + "testing" +) + +// TestScoringComponents tests individual scoring components +func TestScoringComponents(t *testing.T) { + selector := NewSelectionAlgorithm() + + // Test performance scoring + t.Run("PerformanceScoring", func(t *testing.T) { + carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0) + score := selector.calculatePerformanceScore(carrier) + + if score < 0 || score > 100 { + t.Errorf("Performance score should be between 0 and 100, got %.2f", score) + } + + t.Logf("Performance score: %.2f", score) + }) + + // Test reliability scoring + t.Run("ReliabilityScoring", func(t *testing.T) { + carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0) + score := selector.calculateReliabilityScore(carrier) + + if score < 0 || score > 100 { + t.Errorf("Reliability score should be between 0 and 100, got %.2f", score) + } + + t.Logf("Reliability score: %.2f", score) + }) + + // Test cost scoring + t.Run("CostScoring", func(t *testing.T) { + carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0) + criteria := &SelectionCriteria{CostSensitivity: 0.5} + score := selector.calculateCostScore(carrier, criteria) + + if score < 0 || score > 100 { + t.Errorf("Cost score should be between 0 and 100, got %.2f", score) + } + + t.Logf("Cost score: %.2f", score) + }) + + // Test region scoring + t.Run("RegionScoring", func(t *testing.T) { + carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0) + + // Test matching region + score1 := selector.calculateRegionScore(carrier, "US") + if score1 != 100.0 { + t.Errorf("Expected perfect score for matching region, got %.2f", score1) + } + + // Test non-matching region + score2 := selector.calculateRegionScore(carrier, "DE") + if score2 != 50.0 { + t.Errorf("Expected neutral score for non-matching region, got %.2f", score2) + } + + // Test empty region + score3 := selector.calculateRegionScore(carrier, "") + if score3 != 50.0 { + t.Errorf("Expected neutral score for empty region, got %.2f", score3) + } + + t.Logf("Region scores - Match: %.2f, Non-match: %.2f, Empty: %.2f", score1, score2, score3) + }) + + // Test capability scoring + t.Run("CapabilityScoring", func(t *testing.T) { + carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0) + + // Test supported profile type + score1 := selector.calculateCapabilityScore(carrier, "operational") + if score1 < 80.0 { + t.Errorf("Expected high score for supported profile type, got %.2f", score1) + } + + // Test unsupported profile type + score2 := selector.calculateCapabilityScore(carrier, "unsupported") + if score2 < 50.0 { + t.Errorf("Expected base score for unsupported profile type, got %.2f", score2) + } + + t.Logf("Capability scores - Supported: %.2f, Unsupported: %.2f", score1, score2) + }) +} + +// TestWeightCalculation tests the weight calculation logic +func TestWeightCalculation(t *testing.T) { + selector := NewSelectionAlgorithm() + + // Test default weights + t.Run("DefaultWeights", func(t *testing.T) { + criteria := &SelectionCriteria{ + Urgency: "medium", + } + weights := selector.getWeights(criteria) + + total := weights.performance + weights.reliability + weights.cost + weights.region + weights.capability + if total < 0.99 || total > 1.01 { // Allow for floating point precision + t.Errorf("Weights should sum to 1.0, got %.2f", total) + } + + t.Logf("Default weights - P: %.2f, R: %.2f, C: %.2f, Reg: %.2f, Cap: %.2f", + weights.performance, weights.reliability, weights.cost, weights.region, weights.capability) + }) + + // Test high urgency weights + t.Run("HighUrgencyWeights", func(t *testing.T) { + criteria := &SelectionCriteria{ + Urgency: "high", + } + weights := selector.getWeights(criteria) + + if weights.performance < 0.39 || weights.reliability < 0.39 { + t.Errorf("High urgency should prioritize performance and reliability") + } + + t.Logf("High urgency weights - P: %.2f, R: %.2f, C: %.2f, Reg: %.2f, Cap: %.2f", + weights.performance, weights.reliability, weights.cost, weights.region, weights.capability) + }) + + // Test low urgency weights + t.Run("LowUrgencyWeights", func(t *testing.T) { + criteria := &SelectionCriteria{ + Urgency: "low", + } + weights := selector.getWeights(criteria) + + if weights.cost < 0.39 { + t.Errorf("Low urgency should prioritize cost") + } + + t.Logf("Low urgency weights - P: %.2f, R: %.2f, C: %.2f, Reg: %.2f, Cap: %.2f", + weights.performance, weights.reliability, weights.cost, weights.region, weights.capability) + }) + + // Test custom weights + t.Run("CustomWeights", func(t *testing.T) { + criteria := &SelectionCriteria{ + Urgency: "medium", + PerformanceWeight: 0.7, + ReliabilityWeight: 0.2, + CostSensitivity: 0.1, + } + weights := selector.getWeights(criteria) + + total := weights.performance + weights.reliability + weights.cost + weights.region + weights.capability + if total < 0.99 || total > 1.01 { + t.Errorf("Custom weights should sum to 1.0, got %.2f", total) + } + + t.Logf("Custom weights - P: %.2f, R: %.2f, C: %.2f, Reg: %.2f, Cap: %.2f", + weights.performance, weights.reliability, weights.cost, weights.region, weights.capability) + }) +} + +// TestSelectionHistory tests the selection history functionality +func TestSelectionHistory(t *testing.T) { + selector := NewSelectionAlgorithm() + + carriers := []*Carrier{ + createTestCarrier("test-carrier", "Test Carrier", "US", 80, 90, 150, 5.0), + } + + criteria := &SelectionCriteria{ + Region: "US", + ProfileType: "operational", + Urgency: "medium", + CostSensitivity: 0.5, + PerformanceWeight: 0.4, + ReliabilityWeight: 0.4, + } + + // Perform multiple selections + for i := range 5 { + _, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria) + if err != nil { + t.Fatalf("Failed to select optimal carrier on iteration %d: %v", i, err) + } + } + + // Check selection history + history := selector.GetSelectionHistory("test-carrier") + if len(history) != 5 { + t.Errorf("Expected 5 selections in history, got %d", len(history)) + } + + // Test learning feedback + selector.UpdateLearning("test-carrier", 85.0) + + t.Logf("Selection history contains %d entries", len(history)) +}