diff --git a/tests/AiDotNet.Tests/UnitTests/FitnessCalculators/ContrastiveLossFitnessCalculatorTests.cs b/tests/AiDotNet.Tests/UnitTests/FitnessCalculators/ContrastiveLossFitnessCalculatorTests.cs new file mode 100644 index 000000000..a3f57fce3 --- /dev/null +++ b/tests/AiDotNet.Tests/UnitTests/FitnessCalculators/ContrastiveLossFitnessCalculatorTests.cs @@ -0,0 +1,422 @@ +using System; +using AiDotNet.FitnessCalculators; +using AiDotNet.Models; +using AiDotNet.LinearAlgebra; +using Xunit; + +namespace AiDotNetTests.UnitTests.FitnessCalculators +{ + /// + /// Unit tests for ContrastiveLossFitnessCalculator, which evaluates model performance for similarity learning tasks. + /// + public class ContrastiveLossFitnessCalculatorTests + { + [Fact] + public void Constructor_WithDefaultParameters_UsesDefaultMargin() + { + // Arrange & Act + var calculator = new ContrastiveLossFitnessCalculator, Vector>(); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); // Contrastive loss: lower is better + } + + [Fact] + public void Constructor_WithCustomMargin_UsesSpecifiedMargin() + { + // Arrange & Act + var calculator = new ContrastiveLossFitnessCalculator, Vector>( + margin: 2.0, dataSetType: DataSetType.Validation); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); + } + + [Fact] + public void Constructor_WithTrainingDataSetType_UsesTraining() + { + // Arrange & Act + var calculator = new ContrastiveLossFitnessCalculator, Vector>( + dataSetType: DataSetType.Training); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); + } + + [Fact] + public void CalculateFitnessScore_WithIdenticalSimilarPairs_ReturnsZero() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // First half and second half are identical, actual values are same (similarity = 1) + Predicted = new Vector(new double[] { 1.0, 2.0, 1.0, 2.0 }), + Actual = new Vector(new double[] { 1.0, 1.0, 1.0, 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // For similar pairs (label=1) with identical embeddings: loss = distance² = 0 + Assert.Equal(0.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithDissimilarPairsBeyondMargin_ReturnsZero() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(margin: 1.0); + var dataSet = new DataSetStats, Vector> + { + // Pairs that are different (label=0) and far apart (beyond margin) + Predicted = new Vector(new double[] { 0.0, 5.0 }), + Actual = new Vector(new double[] { 1.0, 2.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // For dissimilar pairs beyond margin: loss = max(0, margin - distance)² = 0 + // Distance between [0] and [5] is 5, which is > margin (1.0) + Assert.Equal(0.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithSimilarPairsAtDistance_ReturnsCorrectValue() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // Similar pairs (label=1) with some distance + Predicted = new Vector(new double[] { 1.0, 4.0 }), + Actual = new Vector(new double[] { 1.0, 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Distance between [1.0] and [4.0] = 3.0 + // For similar pairs: loss = distance² = 9.0 + Assert.Equal(9.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithDissimilarPairsWithinMargin_ReturnsCorrectValue() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(margin: 2.0); + var dataSet = new DataSetStats, Vector> + { + // Dissimilar pairs (label=0) within margin + Predicted = new Vector(new double[] { 1.0, 1.5 }), + Actual = new Vector(new double[] { 2.0, 3.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Distance = 0.5, margin = 2.0 + // For dissimilar pairs: loss = max(0, 2.0 - 0.5)² = 1.5² = 2.25 + Assert.Equal(2.25, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithMultiplePairs_ReturnsAverageLoss() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(margin: 1.0); + var dataSet = new DataSetStats, Vector> + { + // Two pairs: first similar, second dissimilar + Predicted = new Vector(new double[] { 1.0, 2.0, 3.0, 4.0 }), + Actual = new Vector(new double[] { 1.0, 2.0, 5.0, 6.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Pair 1: [1.0, 2.0] vs [3.0, 4.0], actuals same (1.0, 2.0) so similar + // Distance = sqrt((1-3)² + (2-4)²) = sqrt(8) ≈ 2.828 + // Loss for pair 1 = 2.828² ≈ 8.0 + + // Pair 2: [3.0, 4.0] vs [5.0, 6.0], actuals different so dissimilar + // Distance = sqrt((3-5)² + (4-6)²) = sqrt(8) ≈ 2.828 + // Loss for pair 2 = max(0, 1.0 - 2.828)² = 0 + + // Average loss ≈ 4.0 + Assert.True(result >= 3.9 && result <= 4.1); + } + + [Fact] + public void CalculateFitnessScore_WithFloatType_WorksCorrectly() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(margin: 1.0f); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new float[] { 0.0f, 3.0f }), + Actual = new Vector(new float[] { 1.0f, 1.0f }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Similar pair, distance = 3.0, loss = 9.0 + Assert.Equal(9.0f, result, 5); + } + + [Fact] + public void CalculateFitnessScore_WithNullDataSet_ThrowsArgumentNullException() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(); + + // Act & Assert + Assert.Throws(() => + calculator.CalculateFitnessScore((DataSetStats, Vector>)null)); + } + + [Fact] + public void CalculateFitnessScore_WithModelEvaluationData_UsesValidationSet() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>( + dataSetType: DataSetType.Validation); + var evaluationData = new ModelEvaluationData, Vector> + { + ValidationSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 1.0 }), + Actual = new Vector(new double[] { 1.0, 1.0 }) + } + }; + + // Act + var result = calculator.CalculateFitnessScore(evaluationData); + + // Assert + Assert.Equal(0.0, result, 10); // Identical pairs + } + + [Fact] + public void CalculateFitnessScore_WithModelEvaluationDataAndTestSet_UsesTestSet() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>( + dataSetType: DataSetType.Testing); + var evaluationData = new ModelEvaluationData, Vector> + { + TestSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.0, 5.0 }), + Actual = new Vector(new double[] { 1.0, 1.0 }) + } + }; + + // Act + var result = calculator.CalculateFitnessScore(evaluationData); + + // Assert + Assert.Equal(25.0, result, 10); // Distance = 5.0, loss = 25.0 + } + + [Fact] + public void IsHigherScoreBetter_ReturnsFalse() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); + } + + [Fact] + public void IsBetterFitness_WithLowerScore_ReturnsTrue() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(); + double newScore = 0.5; + double currentBestScore = 1.0; + + // Act + var result = calculator.IsBetterFitness(newScore, currentBestScore); + + // Assert + Assert.True(result); // Lower score is better for loss functions + } + + [Fact] + public void IsBetterFitness_WithHigherScore_ReturnsFalse() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(); + double newScore = 1.5; + double currentBestScore = 0.8; + + // Act + var result = calculator.IsBetterFitness(newScore, currentBestScore); + + // Assert + Assert.False(result); // Higher score is worse for loss functions + } + + [Fact] + public void CalculateFitnessScore_FaceRecognitionScenario_ReturnsCorrectValue() + { + // Arrange - Simulating face verification scenario + var calculator = new ContrastiveLossFitnessCalculator, Vector>(margin: 1.0); + var dataSet = new DataSetStats, Vector> + { + // Same person faces should be close, different person faces should be far + Predicted = new Vector(new double[] { 0.1, 0.2, 0.9, 0.8 }), + Actual = new Vector(new double[] { 1.0, 1.0, 2.0, 3.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Pair 1: similar (actuals both 1.0), embeddings [0.1, 0.2] vs [0.9, 0.8] + // Distance ≈ sqrt((0.8)² + (0.6)²) = 1.0, loss = 1.0 + + // Pair 2: dissimilar (actuals 2.0 vs 3.0), embeddings close + // Distance = 0.1, loss = max(0, 1.0 - 0.1)² = 0.81 + + // Average loss ≈ 0.905 + Assert.True(result >= 0.85 && result <= 0.95); + } + + [Fact] + public void CalculateFitnessScore_WithLargeMargin_AllowsMoreSeparation() + { + // Arrange + var smallMarginCalc = new ContrastiveLossFitnessCalculator, Vector>(margin: 0.5); + var largeMarginCalc = new ContrastiveLossFitnessCalculator, Vector>(margin: 2.0); + var dataSet = new DataSetStats, Vector> + { + // Dissimilar pairs at medium distance + Predicted = new Vector(new double[] { 0.0, 1.0 }), + Actual = new Vector(new double[] { 1.0, 2.0 }) + }; + + // Act + var smallMarginResult = smallMarginCalc.CalculateFitnessScore(dataSet); + var largeMarginResult = largeMarginCalc.CalculateFitnessScore(dataSet); + + // Assert + // Distance = 1.0 + // Small margin (0.5): loss = max(0, 0.5 - 1.0)² = 0 + // Large margin (2.0): loss = max(0, 2.0 - 1.0)² = 1.0 + Assert.Equal(0.0, smallMarginResult, 10); + Assert.Equal(1.0, largeMarginResult, 10); + } + + [Fact] + public void CalculateFitnessScore_WithEvenNumberOfElements_SplitsCorrectly() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // 6 elements: splits into two groups of 3 + Predicted = new Vector(new double[] { 1.0, 2.0, 3.0, 1.0, 2.0, 3.0 }), + Actual = new Vector(new double[] { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // All pairs are similar (same actuals) and identical + Assert.Equal(0.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithZeroDistance_SimilarPairs_ReturnsZero() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.5, 0.5, 0.5, 0.5 }), + Actual = new Vector(new double[] { 1.0, 1.0, 1.0, 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Zero distance for similar pairs: loss = 0 + Assert.Equal(0.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithMaximumSeparation_DissimilarPairs_ReturnsZero() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(margin: 1.0); + var dataSet = new DataSetStats, Vector> + { + // Dissimilar pairs far apart + Predicted = new Vector(new double[] { 0.0, 10.0 }), + Actual = new Vector(new double[] { 1.0, 2.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Distance = 10.0 >> margin (1.0), loss = 0 + Assert.Equal(0.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_SignatureVerificationScenario_ReturnsCorrectValue() + { + // Arrange - Simulating signature verification + var calculator = new ContrastiveLossFitnessCalculator, Vector>(margin: 1.5); + var dataSet = new DataSetStats, Vector> + { + // Genuine signatures vs forgeries + Predicted = new Vector(new double[] { 0.8, 0.9, 1.0, 0.1, 0.2, 5.0 }), + Actual = new Vector(new double[] { 1.0, 1.0, 1.0, 2.0, 3.0, 4.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Should handle mixed scenarios of genuine and forged signatures + Assert.True(result >= 0.0); + } + + [Fact] + public void CalculateFitnessScore_WithVerySmallMargin_PenalizesDissimilarPairsMore() + { + // Arrange + var calculator = new ContrastiveLossFitnessCalculator, Vector>(margin: 0.1); + var dataSet = new DataSetStats, Vector> + { + // Dissimilar pairs at small distance + Predicted = new Vector(new double[] { 0.0, 0.05 }), + Actual = new Vector(new double[] { 1.0, 2.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Distance = 0.05, margin = 0.1 + // Loss = max(0, 0.1 - 0.05)² = 0.0025 + Assert.Equal(0.0025, result, 10); + } + } +} diff --git a/tests/AiDotNet.Tests/UnitTests/FitnessCalculators/CosineSimilarityLossFitnessCalculatorTests.cs b/tests/AiDotNet.Tests/UnitTests/FitnessCalculators/CosineSimilarityLossFitnessCalculatorTests.cs new file mode 100644 index 000000000..016cf8a4b --- /dev/null +++ b/tests/AiDotNet.Tests/UnitTests/FitnessCalculators/CosineSimilarityLossFitnessCalculatorTests.cs @@ -0,0 +1,475 @@ +using System; +using AiDotNet.FitnessCalculators; +using AiDotNet.Models; +using AiDotNet.LinearAlgebra; +using Xunit; + +namespace AiDotNetTests.UnitTests.FitnessCalculators +{ + /// + /// Unit tests for CosineSimilarityLossFitnessCalculator, which evaluates model performance based on vector direction similarity. + /// + public class CosineSimilarityLossFitnessCalculatorTests + { + [Fact] + public void Constructor_WithDefaultDataSetType_UsesValidation() + { + // Arrange & Act + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); // Cosine similarity loss: lower is better + } + + [Fact] + public void Constructor_WithTrainingDataSetType_UsesTraining() + { + // Arrange & Act + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(DataSetType.Training); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); + } + + [Fact] + public void CalculateFitnessScore_WithIdenticalVectors_ReturnsZero() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 2.0, 3.0 }), + Actual = new Vector(new double[] { 1.0, 2.0, 3.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Perfect alignment: cosine similarity = 1.0, loss = 1 - 1.0 = 0 + Assert.Equal(0.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithOppositeVectors_ReturnsTwo() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 2.0, 3.0 }), + Actual = new Vector(new double[] { -1.0, -2.0, -3.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Opposite directions: cosine similarity = -1.0, loss = 1 - (-1.0) = 2.0 + Assert.Equal(2.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithPerpendicularVectors_ReturnsOne() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 0.0 }), + Actual = new Vector(new double[] { 0.0, 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Perpendicular vectors: cosine similarity = 0, loss = 1 - 0 = 1.0 + Assert.Equal(1.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithSameDirectionDifferentMagnitude_ReturnsZero() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 2.0, 3.0 }), + Actual = new Vector(new double[] { 2.0, 4.0, 6.0 }) // Same direction, 2x magnitude + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Same direction regardless of magnitude: cosine similarity = 1.0, loss = 0 + Assert.Equal(0.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithPartialAlignment_ReturnsCorrectValue() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 0.0, 0.0 }), + Actual = new Vector(new double[] { 1.0, 1.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Dot product = 1.0 + // Norm predicted = 1.0, Norm actual = sqrt(2) ≈ 1.414 + // Cosine similarity = 1.0 / (1.0 * 1.414) ≈ 0.707 + // Loss = 1 - 0.707 ≈ 0.293 + Assert.Equal(0.29289321881345248, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithAllZeros_ReturnsOne() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.0, 0.0, 0.0 }), + Actual = new Vector(new double[] { 0.0, 0.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // All zeros case: handled by epsilon to prevent division by zero + // Should return close to 1.0 (no meaningful similarity) + Assert.True(result >= 0.999); + } + + [Fact] + public void CalculateFitnessScore_WithSingleElement_ReturnsCorrectValue() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.5 }), + Actual = new Vector(new double[] { 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Dot product = 0.5, norms = 0.5 and 1.0 + // Cosine similarity = 0.5 / (0.5 * 1.0) = 1.0 + // Loss = 1 - 1.0 = 0 + Assert.Equal(0.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithFloatType_WorksCorrectly() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new float[] { 1.0f, 2.0f, 3.0f }), + Actual = new Vector(new float[] { 1.0f, 2.0f, 3.0f }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + Assert.Equal(0.0f, result, 5); + } + + [Fact] + public void CalculateFitnessScore_WithNullDataSet_ThrowsArgumentNullException() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + + // Act & Assert + Assert.Throws(() => + calculator.CalculateFitnessScore((DataSetStats, Vector>)null)); + } + + [Fact] + public void CalculateFitnessScore_WithModelEvaluationData_UsesValidationSet() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(DataSetType.Validation); + var evaluationData = new ModelEvaluationData, Vector> + { + ValidationSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 2.0 }), + Actual = new Vector(new double[] { 1.0, 2.0 }) + } + }; + + // Act + var result = calculator.CalculateFitnessScore(evaluationData); + + // Assert + Assert.Equal(0.0, result, 10); // Perfect alignment + } + + [Fact] + public void CalculateFitnessScore_WithModelEvaluationDataAndTestSet_UsesTestSet() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(DataSetType.Testing); + var evaluationData = new ModelEvaluationData, Vector> + { + TestSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 0.0 }), + Actual = new Vector(new double[] { 0.0, 1.0 }) + } + }; + + // Act + var result = calculator.CalculateFitnessScore(evaluationData); + + // Assert + Assert.Equal(1.0, result, 10); // Perpendicular vectors + } + + [Fact] + public void IsHigherScoreBetter_ReturnsFalse() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); + } + + [Fact] + public void IsBetterFitness_WithLowerScore_ReturnsTrue() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + double newScore = 0.2; + double currentBestScore = 0.5; + + // Act + var result = calculator.IsBetterFitness(newScore, currentBestScore); + + // Assert + Assert.True(result); // Lower score is better for loss functions + } + + [Fact] + public void IsBetterFitness_WithHigherScore_ReturnsFalse() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + double newScore = 0.8; + double currentBestScore = 0.3; + + // Act + var result = calculator.IsBetterFitness(newScore, currentBestScore); + + // Assert + Assert.False(result); // Higher score is worse for loss functions + } + + [Fact] + public void CalculateFitnessScore_DocumentSimilarityScenario_ReturnsCorrectValue() + { + // Arrange - Simulating document vector comparison + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // Term frequency vectors for similar documents + Predicted = new Vector(new double[] { 0.5, 0.8, 0.1, 0.0 }), + Actual = new Vector(new double[] { 0.6, 0.7, 0.2, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Dot product = 0.5*0.6 + 0.8*0.7 + 0.1*0.2 = 0.3 + 0.56 + 0.02 = 0.88 + // Norm predicted = sqrt(0.25 + 0.64 + 0.01) ≈ 0.9487 + // Norm actual = sqrt(0.36 + 0.49 + 0.04) ≈ 0.9434 + // Cosine similarity = 0.88 / (0.9487 * 0.9434) ≈ 0.984 + // Loss = 1 - 0.984 ≈ 0.016 + Assert.Equal(0.016242195912488764, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithNegativeValues_HandlesCorrectly() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { -1.0, 2.0, -3.0 }), + Actual = new Vector(new double[] { 1.0, -2.0, 3.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Dot product = -1 + (-4) + (-9) = -14 + // Norms are both sqrt(14) + // Cosine similarity = -14 / 14 = -1.0 (opposite directions) + // Loss = 1 - (-1.0) = 2.0 + Assert.Equal(2.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithSmallAngles_ReturnsSmallLoss() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // Nearly aligned vectors + Predicted = new Vector(new double[] { 1.0, 0.1 }), + Actual = new Vector(new double[] { 1.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Small angle should result in high similarity and low loss + Assert.True(result < 0.01); + } + + [Fact] + public void CalculateFitnessScore_WithLargeAngles_ReturnsLargeLoss() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // Nearly opposite vectors + Predicted = new Vector(new double[] { 1.0, 0.1 }), + Actual = new Vector(new double[] { -1.0, -0.1 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Large angle (nearly 180°) should result in low similarity and high loss (close to 2.0) + Assert.True(result > 1.9); + } + + [Fact] + public void CalculateFitnessScore_RecommendationSystemScenario_ReturnsCorrectValue() + { + // Arrange - Simulating user preference vectors + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // User preferences for different categories + Predicted = new Vector(new double[] { 0.9, 0.1, 0.5, 0.3 }), + Actual = new Vector(new double[] { 0.8, 0.2, 0.6, 0.2 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Should show high similarity (low loss) for similar preferences + Assert.True(result < 0.1); // Less than 10% loss + } + + [Fact] + public void CalculateFitnessScore_ImageRetrievalScenario_ReturnsCorrectValue() + { + // Arrange - Simulating image feature vectors + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // Feature vectors for similar images + Predicted = new Vector(new double[] { 0.7, 0.5, 0.3, 0.8, 0.2 }), + Actual = new Vector(new double[] { 0.6, 0.6, 0.4, 0.7, 0.1 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Dot product = 0.42 + 0.30 + 0.12 + 0.56 + 0.02 = 1.42 + // Should handle feature vector comparison correctly + Assert.True(result >= 0.0 && result <= 2.0); + } + + [Fact] + public void CalculateFitnessScore_WithVerySmallValues_HandlesCorrectly() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.001, 0.002, 0.001 }), + Actual = new Vector(new double[] { 0.001, 0.001, 0.002 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Should handle small values without numerical instability + Assert.True(result >= 0.0 && result <= 2.0); + } + + [Fact] + public void CalculateFitnessScore_MagnitudeInvariance_VerifiesProperty() + { + // Arrange + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet1 = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 2.0, 3.0 }), + Actual = new Vector(new double[] { 2.0, 4.0, 6.0 }) + }; + var dataSet2 = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 10.0, 20.0, 30.0 }), + Actual = new Vector(new double[] { 20.0, 40.0, 60.0 }) + }; + + // Act + var result1 = calculator.CalculateFitnessScore(dataSet1); + var result2 = calculator.CalculateFitnessScore(dataSet2); + + // Assert + // Cosine similarity is magnitude-invariant - both should give same result + Assert.Equal(result1, result2, 10); + } + + [Fact] + public void CalculateFitnessScore_TextEmbeddingScenario_ReturnsCorrectValue() + { + // Arrange - Simulating word/sentence embeddings + var calculator = new CosineSimilarityLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // Embeddings for semantically similar text + Predicted = new Vector(new double[] { 0.8, 0.6, -0.2, 0.9, -0.1 }), + Actual = new Vector(new double[] { 0.7, 0.7, -0.1, 0.8, -0.2 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Should indicate high similarity for semantically similar embeddings + Assert.True(result < 0.15); // Less than 15% loss + } + } +} diff --git a/tests/AiDotNet.Tests/UnitTests/FitnessCalculators/DiceLossFitnessCalculatorTests.cs b/tests/AiDotNet.Tests/UnitTests/FitnessCalculators/DiceLossFitnessCalculatorTests.cs new file mode 100644 index 000000000..28d988495 --- /dev/null +++ b/tests/AiDotNet.Tests/UnitTests/FitnessCalculators/DiceLossFitnessCalculatorTests.cs @@ -0,0 +1,337 @@ +using System; +using AiDotNet.FitnessCalculators; +using AiDotNet.Models; +using AiDotNet.LinearAlgebra; +using Xunit; + +namespace AiDotNetTests.UnitTests.FitnessCalculators +{ + /// + /// Unit tests for DiceLossFitnessCalculator, which evaluates model performance for segmentation tasks. + /// + public class DiceLossFitnessCalculatorTests + { + [Fact] + public void Constructor_WithDefaultDataSetType_UsesValidation() + { + // Arrange & Act + var calculator = new DiceLossFitnessCalculator, Vector>(); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); // Dice loss: lower is better + } + + [Fact] + public void Constructor_WithTrainingDataSetType_UsesTraining() + { + // Arrange & Act + var calculator = new DiceLossFitnessCalculator, Vector>(DataSetType.Training); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); + } + + [Fact] + public void CalculateFitnessScore_WithPerfectPredictions_ReturnsZero() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 1.0, 1.0, 0.0, 0.0 }), + Actual = new Vector(new double[] { 1.0, 1.0, 1.0, 0.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Perfect overlap: Dice = (2 * 3) / (3 + 3) = 1.0, Loss = 1 - 1.0 = 0 + Assert.Equal(0.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithNoOverlap_ReturnsOne() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 1.0, 0.0, 0.0 }), + Actual = new Vector(new double[] { 0.0, 0.0, 1.0, 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // No overlap: intersection = 0, Dice = 0, Loss = 1 - 0 = 1 + Assert.Equal(1.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithPartialOverlap_ReturnsCorrectValue() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 1.0, 1.0, 0.0 }), + Actual = new Vector(new double[] { 1.0, 1.0, 0.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = 1*1 + 1*1 + 1*0 + 0*0 = 2 + // Sum predicted = 3, Sum actual = 2 + // Dice = (2 * 2) / (3 + 2) = 4/5 = 0.8 + // Loss = 1 - 0.8 = 0.2 + Assert.Equal(0.2, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithProbabilisticPredictions_ReturnsCorrectValue() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.8, 0.6, 0.2, 0.1 }), + Actual = new Vector(new double[] { 1.0, 1.0, 0.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = 0.8*1 + 0.6*1 + 0.2*0 + 0.1*0 = 1.4 + // Sum predicted = 1.7, Sum actual = 2.0 + // Dice = (2 * 1.4) / (1.7 + 2.0) = 2.8 / 3.7 ≈ 0.7568 + // Loss = 1 - 0.7568 ≈ 0.2432 + Assert.Equal(0.24324324324324326, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithAllZeros_ReturnsOne() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.0, 0.0, 0.0 }), + Actual = new Vector(new double[] { 0.0, 0.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // All zeros case: handled by epsilon to prevent division by zero + // Should return close to 1.0 (worst case) + Assert.True(result >= 0.999); + } + + [Fact] + public void CalculateFitnessScore_WithSingleElement_ReturnsCorrectValue() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.5 }), + Actual = new Vector(new double[] { 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = 0.5*1 = 0.5 + // Sum predicted = 0.5, Sum actual = 1.0 + // Dice = (2 * 0.5) / (0.5 + 1.0) = 1.0 / 1.5 ≈ 0.6667 + // Loss = 1 - 0.6667 ≈ 0.3333 + Assert.Equal(0.33333333333333331, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithFloatType_WorksCorrectly() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new float[] { 1.0f, 1.0f, 1.0f, 0.0f }), + Actual = new Vector(new float[] { 1.0f, 1.0f, 0.0f, 0.0f }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = 2, Sum predicted = 3, Sum actual = 2 + // Dice = (2 * 2) / (3 + 2) = 0.8, Loss = 0.2 + Assert.Equal(0.2f, result, 5); + } + + [Fact] + public void CalculateFitnessScore_WithImbalancedData_HandlesCorrectly() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // Simulating rare positive class (medical imaging scenario) + Predicted = new Vector(new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9, 0.8 }), + Actual = new Vector(new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = 0.9 + 0.8 = 1.7 + // Sum predicted = 1.7, Sum actual = 2.0 + // Dice = (2 * 1.7) / (1.7 + 2.0) = 3.4 / 3.7 ≈ 0.9189 + // Loss = 1 - 0.9189 ≈ 0.0811 + Assert.Equal(0.08108108108108109, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithNullDataSet_ThrowsArgumentNullException() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + + // Act & Assert + Assert.Throws(() => + calculator.CalculateFitnessScore((DataSetStats, Vector>)null)); + } + + [Fact] + public void CalculateFitnessScore_WithModelEvaluationData_UsesValidationSet() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(DataSetType.Validation); + var evaluationData = new ModelEvaluationData, Vector> + { + ValidationSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 1.0, 0.0 }), + Actual = new Vector(new double[] { 1.0, 1.0, 0.0 }) + } + }; + + // Act + var result = calculator.CalculateFitnessScore(evaluationData); + + // Assert + Assert.Equal(0.0, result, 10); // Perfect predictions + } + + [Fact] + public void CalculateFitnessScore_WithModelEvaluationDataAndTestSet_UsesTestSet() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(DataSetType.Testing); + var evaluationData = new ModelEvaluationData, Vector> + { + TestSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 0.0 }), + Actual = new Vector(new double[] { 0.0, 1.0 }) + } + }; + + // Act + var result = calculator.CalculateFitnessScore(evaluationData); + + // Assert + Assert.Equal(1.0, result, 10); // No overlap + } + + [Fact] + public void IsHigherScoreBetter_ReturnsFalse() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); + } + + [Fact] + public void IsBetterFitness_WithLowerScore_ReturnsTrue() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + double newScore = 0.2; + double currentBestScore = 0.5; + + // Act + var result = calculator.IsBetterFitness(newScore, currentBestScore); + + // Assert + Assert.True(result); // Lower score is better for loss functions + } + + [Fact] + public void IsBetterFitness_WithHigherScore_ReturnsFalse() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + double newScore = 0.8; + double currentBestScore = 0.3; + + // Act + var result = calculator.IsBetterFitness(newScore, currentBestScore); + + // Assert + Assert.False(result); // Higher score is worse for loss functions + } + + [Fact] + public void CalculateFitnessScore_MedicalImagingScenario_ReturnsCorrectValue() + { + // Arrange - Simulating tumor segmentation scenario + var calculator = new DiceLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // Model predicts tumor pixels with confidence scores + Predicted = new Vector(new double[] { 0.95, 0.88, 0.75, 0.10, 0.05 }), + // Ground truth: first 3 pixels are tumor, last 2 are not + Actual = new Vector(new double[] { 1.0, 1.0, 1.0, 0.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = 0.95 + 0.88 + 0.75 = 2.58 + // Sum predicted = 2.73, Sum actual = 3.0 + // Dice = (2 * 2.58) / (2.73 + 3.0) = 5.16 / 5.73 ≈ 0.9005 + // Loss = 1 - 0.9005 ≈ 0.0995 + Assert.Equal(0.09947643979057592, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithVerySmallValues_HandlesCorrectly() + { + // Arrange + var calculator = new DiceLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.001, 0.002, 0.001 }), + Actual = new Vector(new double[] { 0.001, 0.001, 0.002 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Should handle small values without numerical instability + Assert.True(result >= 0.0 && result <= 1.0); + } + } +} diff --git a/tests/AiDotNet.Tests/UnitTests/FitnessCalculators/JaccardLossFitnessCalculatorTests.cs b/tests/AiDotNet.Tests/UnitTests/FitnessCalculators/JaccardLossFitnessCalculatorTests.cs new file mode 100644 index 000000000..88ab67f6c --- /dev/null +++ b/tests/AiDotNet.Tests/UnitTests/FitnessCalculators/JaccardLossFitnessCalculatorTests.cs @@ -0,0 +1,395 @@ +using System; +using AiDotNet.FitnessCalculators; +using AiDotNet.Models; +using AiDotNet.LinearAlgebra; +using Xunit; + +namespace AiDotNetTests.UnitTests.FitnessCalculators +{ + /// + /// Unit tests for JaccardLossFitnessCalculator, which evaluates model performance using Intersection over Union. + /// + public class JaccardLossFitnessCalculatorTests + { + [Fact] + public void Constructor_WithDefaultDataSetType_UsesValidation() + { + // Arrange & Act + var calculator = new JaccardLossFitnessCalculator, Vector>(); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); // Jaccard loss: lower is better + } + + [Fact] + public void Constructor_WithTrainingDataSetType_UsesTraining() + { + // Arrange & Act + var calculator = new JaccardLossFitnessCalculator, Vector>(DataSetType.Training); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); + } + + [Fact] + public void CalculateFitnessScore_WithPerfectPredictions_ReturnsZero() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 1.0, 1.0, 0.0, 0.0 }), + Actual = new Vector(new double[] { 1.0, 1.0, 1.0, 0.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Perfect overlap: Intersection = 3, Union = 3, IoU = 1.0, Loss = 0 + Assert.Equal(0.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithNoOverlap_ReturnsOne() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 1.0, 0.0, 0.0 }), + Actual = new Vector(new double[] { 0.0, 0.0, 1.0, 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // No overlap: Intersection = 0, Union = 4, IoU = 0, Loss = 1 + Assert.Equal(1.0, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithPartialOverlap_ReturnsCorrectValue() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 1.0, 1.0, 0.0 }), + Actual = new Vector(new double[] { 1.0, 1.0, 0.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = min(1,1) + min(1,1) + min(1,0) + min(0,0) = 2 + // Union = max(1,1) + max(1,1) + max(1,0) + max(0,0) = 3 + // IoU = 2/3 ≈ 0.6667, Loss = 1 - 0.6667 ≈ 0.3333 + Assert.Equal(0.33333333333333331, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithProbabilisticPredictions_ReturnsCorrectValue() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.8, 0.6, 0.2, 0.1 }), + Actual = new Vector(new double[] { 1.0, 1.0, 0.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = min(0.8,1.0) + min(0.6,1.0) + min(0.2,0) + min(0.1,0) = 0.8 + 0.6 = 1.4 + // Union = max(0.8,1.0) + max(0.6,1.0) + max(0.2,0) + max(0.1,0) = 1.0 + 1.0 + 0.2 + 0.1 = 2.3 + // IoU = 1.4 / 2.3 ≈ 0.6087, Loss = 1 - 0.6087 ≈ 0.3913 + Assert.Equal(0.39130434782608703, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithAllZeros_ReturnsOne() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.0, 0.0, 0.0 }), + Actual = new Vector(new double[] { 0.0, 0.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // All zeros: Union = 0 + epsilon, Intersection = 0 + // IoU ≈ 0, Loss ≈ 1.0 + Assert.True(result >= 0.999); + } + + [Fact] + public void CalculateFitnessScore_WithSingleElement_ReturnsCorrectValue() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.5 }), + Actual = new Vector(new double[] { 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = min(0.5, 1.0) = 0.5 + // Union = max(0.5, 1.0) = 1.0 + // IoU = 0.5 / 1.0 = 0.5, Loss = 1 - 0.5 = 0.5 + Assert.Equal(0.5, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithFloatType_WorksCorrectly() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new float[] { 1.0f, 1.0f, 1.0f, 0.0f }), + Actual = new Vector(new float[] { 1.0f, 1.0f, 0.0f, 0.0f }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = 2, Union = 3, IoU = 2/3, Loss = 1/3 + Assert.Equal(0.33333333f, result, 5); + } + + [Fact] + public void CalculateFitnessScore_WithImbalancedData_HandlesCorrectly() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // Simulating object detection scenario with small object + Predicted = new Vector(new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9, 0.8 }), + Actual = new Vector(new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = 0.9 + 0.8 = 1.7 + // Union = 1.0 + 1.0 = 2.0 + // IoU = 1.7 / 2.0 = 0.85, Loss = 0.15 + Assert.Equal(0.15, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithNullDataSet_ThrowsArgumentNullException() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + + // Act & Assert + Assert.Throws(() => + calculator.CalculateFitnessScore((DataSetStats, Vector>)null)); + } + + [Fact] + public void CalculateFitnessScore_WithModelEvaluationData_UsesValidationSet() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(DataSetType.Validation); + var evaluationData = new ModelEvaluationData, Vector> + { + ValidationSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 1.0, 0.0 }), + Actual = new Vector(new double[] { 1.0, 1.0, 0.0 }) + } + }; + + // Act + var result = calculator.CalculateFitnessScore(evaluationData); + + // Assert + Assert.Equal(0.0, result, 10); // Perfect predictions + } + + [Fact] + public void CalculateFitnessScore_WithModelEvaluationDataAndTestSet_UsesTestSet() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(DataSetType.Testing); + var evaluationData = new ModelEvaluationData, Vector> + { + TestSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 0.0 }), + Actual = new Vector(new double[] { 0.0, 1.0 }) + } + }; + + // Act + var result = calculator.CalculateFitnessScore(evaluationData); + + // Assert + Assert.Equal(1.0, result, 10); // No overlap + } + + [Fact] + public void IsHigherScoreBetter_ReturnsFalse() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + + // Assert + Assert.False(calculator.IsHigherScoreBetter); + } + + [Fact] + public void IsBetterFitness_WithLowerScore_ReturnsTrue() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + double newScore = 0.2; + double currentBestScore = 0.5; + + // Act + var result = calculator.IsBetterFitness(newScore, currentBestScore); + + // Assert + Assert.True(result); // Lower score is better for loss functions + } + + [Fact] + public void IsBetterFitness_WithHigherScore_ReturnsFalse() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + double newScore = 0.8; + double currentBestScore = 0.3; + + // Act + var result = calculator.IsBetterFitness(newScore, currentBestScore); + + // Assert + Assert.False(result); // Higher score is worse for loss functions + } + + [Fact] + public void CalculateFitnessScore_ObjectDetectionScenario_ReturnsCorrectValue() + { + // Arrange - Simulating bounding box IoU scenario + var calculator = new JaccardLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + // Predicted box overlaps partially with ground truth box + Predicted = new Vector(new double[] { 0.8, 0.9, 0.7, 0.1, 0.2 }), + Actual = new Vector(new double[] { 1.0, 1.0, 1.0, 0.0, 0.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = 0.8 + 0.9 + 0.7 = 2.4 + // Union = 1.0 + 1.0 + 1.0 + 0.1 + 0.2 = 3.3 + // IoU = 2.4 / 3.3 ≈ 0.7273, Loss = 1 - 0.7273 ≈ 0.2727 + Assert.Equal(0.27272727272727271, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithMixedValues_ReturnsCorrectValue() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.3, 0.7, 0.5 }), + Actual = new Vector(new double[] { 0.6, 0.4, 0.8 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Intersection = min(0.3,0.6) + min(0.7,0.4) + min(0.5,0.8) = 0.3 + 0.4 + 0.5 = 1.2 + // Union = max(0.3,0.6) + max(0.7,0.4) + max(0.5,0.8) = 0.6 + 0.7 + 0.8 = 2.1 + // IoU = 1.2 / 2.1 ≈ 0.5714, Loss = 1 - 0.5714 ≈ 0.4286 + Assert.Equal(0.42857142857142855, result, 10); + } + + [Fact] + public void CalculateFitnessScore_WithVerySmallValues_HandlesCorrectly() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.001, 0.002, 0.001 }), + Actual = new Vector(new double[] { 0.001, 0.001, 0.002 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // Should handle small values without numerical instability + Assert.True(result >= 0.0 && result <= 1.0); + } + + [Fact] + public void CalculateFitnessScore_WithHighOverlap_ReturnsLowLoss() + { + // Arrange + var calculator = new JaccardLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 0.9, 0.95, 0.92, 0.88 }), + Actual = new Vector(new double[] { 1.0, 1.0, 1.0, 1.0 }) + }; + + // Act + var result = calculator.CalculateFitnessScore(dataSet); + + // Assert + // High overlap should result in low loss + Assert.True(result < 0.15); // Loss should be less than 15% + } + + [Fact] + public void CalculateFitnessScore_CompareWithDiceMetric_ShowsDifference() + { + // Arrange + var jaccardCalculator = new JaccardLossFitnessCalculator, Vector>(); + var diceCalculator = new DiceLossFitnessCalculator, Vector>(); + var dataSet = new DataSetStats, Vector> + { + Predicted = new Vector(new double[] { 1.0, 1.0, 1.0, 0.0 }), + Actual = new Vector(new double[] { 1.0, 1.0, 0.0, 0.0 }) + }; + + // Act + var jaccardResult = jaccardCalculator.CalculateFitnessScore(dataSet); + var diceResult = diceCalculator.CalculateFitnessScore(dataSet); + + // Assert + // Jaccard and Dice should give different values for the same data + // Jaccard: Intersection=2, Union=3, IoU=2/3, Loss=1/3 ≈ 0.333 + // Dice: Intersection=2, Sum=5, Dice=4/5, Loss=1/5 = 0.2 + Assert.Equal(0.33333333333333331, jaccardResult, 10); + Assert.Equal(0.2, diceResult, 10); + Assert.NotEqual(jaccardResult, diceResult); + } + } +}