mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-29 02:07:42 +00:00
060649192f
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
1328 lines
41 KiB
C#
1328 lines
41 KiB
C#
using Xunit;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
public class ErrorHelpersTests
|
|
{
|
|
private const double Tolerance = 1e-10;
|
|
|
|
// ── Constants ───────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void StackAllocThreshold_Is256()
|
|
{
|
|
Assert.Equal(256, ErrorHelpers.StackAllocThreshold);
|
|
}
|
|
|
|
[Fact]
|
|
public void DefaultResyncInterval_Is1000()
|
|
{
|
|
Assert.Equal(1000, ErrorHelpers.DefaultResyncInterval);
|
|
}
|
|
|
|
// ── FindFirstValidValue ─────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void FindFirstValidValue_AllFinite_ReturnsFirst()
|
|
{
|
|
double[] data = [10.0, 20.0, 30.0];
|
|
Assert.Equal(10.0, ErrorHelpers.FindFirstValidValue(data));
|
|
}
|
|
|
|
[Fact]
|
|
public void FindFirstValidValue_LeadingNaN_SkipsToFirstFinite()
|
|
{
|
|
double[] data = [double.NaN, double.NaN, 42.0, 50.0];
|
|
Assert.Equal(42.0, ErrorHelpers.FindFirstValidValue(data));
|
|
}
|
|
|
|
[Fact]
|
|
public void FindFirstValidValue_AllNaN_ReturnsZero()
|
|
{
|
|
double[] data = [double.NaN, double.NaN, double.NaN];
|
|
Assert.Equal(0.0, ErrorHelpers.FindFirstValidValue(data));
|
|
}
|
|
|
|
[Fact]
|
|
public void FindFirstValidValue_EmptySpan_ReturnsZero()
|
|
{
|
|
Assert.Equal(0.0, ErrorHelpers.FindFirstValidValue(ReadOnlySpan<double>.Empty));
|
|
}
|
|
|
|
[Fact]
|
|
public void FindFirstValidValue_InfinitySkipped_ReturnsFirstFinite()
|
|
{
|
|
double[] data = [double.PositiveInfinity, double.NegativeInfinity, 7.0];
|
|
Assert.Equal(7.0, ErrorHelpers.FindFirstValidValue(data));
|
|
}
|
|
|
|
// ── ComputeSignedErrors ─────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void SignedErrors_BasicComputation()
|
|
{
|
|
double[] actual = [10.0, 20.0, 30.0];
|
|
double[] predicted = [8.0, 25.0, 29.0];
|
|
double[] output = new double[3];
|
|
|
|
ErrorHelpers.ComputeSignedErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(2.0, output[0], Tolerance);
|
|
Assert.Equal(-5.0, output[1], Tolerance);
|
|
Assert.Equal(1.0, output[2], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void SignedErrors_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ComputeSignedErrors(
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty);
|
|
Assert.True(true); // No exception = pass
|
|
}
|
|
|
|
[Fact]
|
|
public void SignedErrors_LengthMismatch_Throws()
|
|
{
|
|
double[] a = [1.0, 2.0];
|
|
double[] b = [1.0];
|
|
double[] o = [0.0, 0.0];
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ComputeSignedErrors(a, b, o));
|
|
}
|
|
|
|
[Fact]
|
|
public void SignedErrors_WithNaN_SubstitutesLastValid()
|
|
{
|
|
double[] actual = [10.0, double.NaN, 30.0];
|
|
double[] predicted = [5.0, 15.0, 25.0];
|
|
double[] output = new double[3];
|
|
|
|
ErrorHelpers.ComputeSignedErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(5.0, output[0], Tolerance); // 10 - 5
|
|
Assert.Equal(-5.0, output[1], Tolerance); // 10 (last valid) - 15
|
|
Assert.Equal(5.0, output[2], Tolerance); // 30 - 25
|
|
}
|
|
|
|
[Fact]
|
|
public void SignedErrors_LargeCleanArray_ProducesCorrectResults()
|
|
{
|
|
// ≥ 8 elements to exercise SIMD path (Vector256<double>.Count = 4)
|
|
double[] actual = [1, 2, 3, 4, 5, 6, 7, 8];
|
|
double[] predicted = [0, 1, 2, 3, 4, 5, 6, 7];
|
|
double[] output = new double[8];
|
|
|
|
ErrorHelpers.ComputeSignedErrors(actual, predicted, output);
|
|
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
Assert.Equal(1.0, output[i], Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SignedErrors_LargeArrayWithNaN_FallsBackCorrectly()
|
|
{
|
|
double[] actual = [1, 2, 3, double.NaN, 5, 6, 7, 8];
|
|
double[] predicted = [0, 0, 0, 0, 0, 0, 0, 0];
|
|
double[] output = new double[8];
|
|
|
|
ErrorHelpers.ComputeSignedErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(1.0, output[0], Tolerance);
|
|
Assert.Equal(2.0, output[1], Tolerance);
|
|
Assert.Equal(3.0, output[2], Tolerance);
|
|
Assert.Equal(3.0, output[3], Tolerance); // NaN → last valid (3)
|
|
Assert.Equal(5.0, output[4], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void SignedErrors_SingleElement()
|
|
{
|
|
double[] actual = [7.0];
|
|
double[] predicted = [3.0];
|
|
double[] output = new double[1];
|
|
|
|
ErrorHelpers.ComputeSignedErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(4.0, output[0], Tolerance);
|
|
}
|
|
|
|
// ── ComputeAbsoluteErrors ───────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void AbsoluteErrors_BasicComputation()
|
|
{
|
|
double[] actual = [10.0, 20.0, 30.0];
|
|
double[] predicted = [12.0, 15.0, 35.0];
|
|
double[] output = new double[3];
|
|
|
|
ErrorHelpers.ComputeAbsoluteErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(2.0, output[0], Tolerance);
|
|
Assert.Equal(5.0, output[1], Tolerance);
|
|
Assert.Equal(5.0, output[2], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void AbsoluteErrors_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ComputeAbsoluteErrors(
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void AbsoluteErrors_LengthMismatch_Throws()
|
|
{
|
|
double[] a = [1.0];
|
|
double[] b = [1.0, 2.0];
|
|
double[] o = [0.0];
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ComputeAbsoluteErrors(a, b, o));
|
|
}
|
|
|
|
[Fact]
|
|
public void AbsoluteErrors_AlwaysNonNegative()
|
|
{
|
|
double[] actual = [5.0, -3.0, 10.0, 0.0];
|
|
double[] predicted = [8.0, 2.0, 10.0, -5.0];
|
|
double[] output = new double[4];
|
|
|
|
ErrorHelpers.ComputeAbsoluteErrors(actual, predicted, output);
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
Assert.True(output[i] >= 0.0, $"AbsoluteError at {i} was {output[i]}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AbsoluteErrors_WithNaN_SubstitutesLastValid()
|
|
{
|
|
double[] actual = [10.0, double.NaN, 30.0];
|
|
double[] predicted = [5.0, 15.0, 25.0];
|
|
double[] output = new double[3];
|
|
|
|
ErrorHelpers.ComputeAbsoluteErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(5.0, output[0], Tolerance); // |10 - 5|
|
|
Assert.Equal(5.0, output[1], Tolerance); // |10 - 15|
|
|
Assert.Equal(5.0, output[2], Tolerance); // |30 - 25|
|
|
}
|
|
|
|
[Fact]
|
|
public void AbsoluteErrors_LargeCleanArray_SimdPath()
|
|
{
|
|
double[] actual = [10, 20, 30, 40, 50, 60, 70, 80];
|
|
double[] predicted = [12, 18, 33, 37, 55, 58, 73, 77];
|
|
double[] output = new double[8];
|
|
|
|
ErrorHelpers.ComputeAbsoluteErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(2.0, output[0], Tolerance);
|
|
Assert.Equal(2.0, output[1], Tolerance);
|
|
Assert.Equal(3.0, output[2], Tolerance);
|
|
Assert.Equal(3.0, output[3], Tolerance);
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
Assert.True(output[i] >= 0.0);
|
|
}
|
|
}
|
|
|
|
// ── ComputeSquaredErrors ────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void SquaredErrors_BasicComputation()
|
|
{
|
|
double[] actual = [10.0, 20.0, 30.0];
|
|
double[] predicted = [8.0, 25.0, 27.0];
|
|
double[] output = new double[3];
|
|
|
|
ErrorHelpers.ComputeSquaredErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(4.0, output[0], Tolerance); // (10-8)² = 4
|
|
Assert.Equal(25.0, output[1], Tolerance); // (20-25)² = 25
|
|
Assert.Equal(9.0, output[2], Tolerance); // (30-27)² = 9
|
|
}
|
|
|
|
[Fact]
|
|
public void SquaredErrors_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ComputeSquaredErrors(
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void SquaredErrors_LengthMismatch_Throws()
|
|
{
|
|
double[] a = [1.0, 2.0, 3.0];
|
|
double[] b = [1.0, 2.0];
|
|
double[] o = [0.0, 0.0, 0.0];
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ComputeSquaredErrors(a, b, o));
|
|
}
|
|
|
|
[Fact]
|
|
public void SquaredErrors_AlwaysNonNegative()
|
|
{
|
|
double[] actual = [-5.0, 0.0, 3.0, -1.0];
|
|
double[] predicted = [2.0, -3.0, 7.0, -1.0];
|
|
double[] output = new double[4];
|
|
|
|
ErrorHelpers.ComputeSquaredErrors(actual, predicted, output);
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
Assert.True(output[i] >= 0.0);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SquaredErrors_WithNaN_SubstitutesLastValid()
|
|
{
|
|
double[] actual = [10.0, double.NaN, 30.0];
|
|
double[] predicted = [7.0, 20.0, 27.0];
|
|
double[] output = new double[3];
|
|
|
|
ErrorHelpers.ComputeSquaredErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(9.0, output[0], Tolerance); // (10-7)² = 9
|
|
Assert.Equal(100.0, output[1], Tolerance); // (10-20)² = 100, NaN actual → 10
|
|
Assert.Equal(9.0, output[2], Tolerance); // (30-27)² = 9
|
|
}
|
|
|
|
[Fact]
|
|
public void SquaredErrors_LargeCleanArray_SimdPath()
|
|
{
|
|
double[] actual = [1, 2, 3, 4, 5, 6, 7, 8];
|
|
double[] predicted = [2, 3, 4, 5, 6, 7, 8, 9];
|
|
double[] output = new double[8];
|
|
|
|
ErrorHelpers.ComputeSquaredErrors(actual, predicted, output);
|
|
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
Assert.Equal(1.0, output[i], Tolerance); // Each diff is -1, squared = 1
|
|
}
|
|
}
|
|
|
|
// ── ComputeWeightedErrors ───────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void WeightedErrors_BasicComputation()
|
|
{
|
|
double[] actual = [10.0, 20.0, 30.0];
|
|
double[] predicted = [8.0, 18.0, 28.0];
|
|
double[] weights = [1.0, 2.0, 3.0];
|
|
double[] output = new double[3];
|
|
|
|
ErrorHelpers.ComputeWeightedErrors(actual, predicted, weights, output);
|
|
|
|
// weight * (act - pred)²
|
|
Assert.Equal(1.0 * 4.0, output[0], Tolerance); // 1 * (10-8)² = 4
|
|
Assert.Equal(2.0 * 4.0, output[1], Tolerance); // 2 * (20-18)² = 8
|
|
Assert.Equal(3.0 * 4.0, output[2], Tolerance); // 3 * (30-28)² = 12
|
|
}
|
|
|
|
[Fact]
|
|
public void WeightedErrors_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ComputeWeightedErrors(
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void WeightedErrors_LengthMismatch_Throws()
|
|
{
|
|
double[] a = [1.0];
|
|
double[] b = [1.0];
|
|
double[] w = [1.0, 2.0]; // mismatched
|
|
double[] o = [0.0];
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ComputeWeightedErrors(a, b, w, o));
|
|
}
|
|
|
|
[Fact]
|
|
public void WeightedErrors_WithNaN_SubstitutesLastValid()
|
|
{
|
|
double[] actual = [10.0, double.NaN];
|
|
double[] predicted = [8.0, 6.0];
|
|
double[] weights = [1.0, double.NaN];
|
|
double[] output = new double[2];
|
|
|
|
ErrorHelpers.ComputeWeightedErrors(actual, predicted, weights, output);
|
|
|
|
// [0]: 1.0 * (10-8)² = 4.0
|
|
Assert.Equal(4.0, output[0], Tolerance);
|
|
// [1]: NaN act→10, NaN wgt→1.0: 1.0 * (10-6)² = 16.0
|
|
Assert.Equal(16.0, output[1], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void WeightedErrors_ZeroWeight_ProducesZero()
|
|
{
|
|
double[] actual = [100.0];
|
|
double[] predicted = [0.0];
|
|
double[] weights = [0.0];
|
|
double[] output = new double[1];
|
|
|
|
ErrorHelpers.ComputeWeightedErrors(actual, predicted, weights, output);
|
|
|
|
Assert.Equal(0.0, output[0], Tolerance);
|
|
}
|
|
|
|
// ── ComputePercentageErrors ─────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void PercentageErrors_BasicComputation()
|
|
{
|
|
double[] actual = [100.0, 200.0];
|
|
double[] predicted = [90.0, 210.0];
|
|
double[] output = new double[2];
|
|
|
|
ErrorHelpers.ComputePercentageErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(10.0, output[0], Tolerance); // |100-90|/|100|*100 = 10%
|
|
Assert.Equal(5.0, output[1], Tolerance); // |200-210|/|200|*100 = 5%
|
|
}
|
|
|
|
[Fact]
|
|
public void PercentageErrors_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ComputePercentageErrors(
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void PercentageErrors_LengthMismatch_Throws()
|
|
{
|
|
double[] a = [1.0, 2.0];
|
|
double[] b = [1.0];
|
|
double[] o = [0.0, 0.0];
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ComputePercentageErrors(a, b, o));
|
|
}
|
|
|
|
[Fact]
|
|
public void PercentageErrors_NearZeroActual_UsesAbsoluteError()
|
|
{
|
|
// When |actual| < epsilon, falls back to |actual - predicted|
|
|
double[] actual = [1e-15];
|
|
double[] predicted = [5.0];
|
|
double[] output = new double[1];
|
|
|
|
ErrorHelpers.ComputePercentageErrors(actual, predicted, output);
|
|
|
|
// absActual ~ 0 < epsilon (1e-10), so output = |act - pred| = 5.0
|
|
Assert.Equal(5.0, output[0], 1e-5);
|
|
}
|
|
|
|
[Fact]
|
|
public void PercentageErrors_WithNaN_SubstitutesLastValid()
|
|
{
|
|
double[] actual = [100.0, double.NaN];
|
|
double[] predicted = [90.0, 80.0];
|
|
double[] output = new double[2];
|
|
|
|
ErrorHelpers.ComputePercentageErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(10.0, output[0], Tolerance); // |100-90|/100*100
|
|
Assert.Equal(20.0, output[1], Tolerance); // NaN→100: |100-80|/100*100
|
|
}
|
|
|
|
// ── ComputeSymmetricPercentageErrors ────────────────────────────────
|
|
|
|
[Fact]
|
|
public void SymmetricPercentageErrors_BasicComputation()
|
|
{
|
|
double[] actual = [100.0];
|
|
double[] predicted = [80.0];
|
|
double[] output = new double[1];
|
|
|
|
ErrorHelpers.ComputeSymmetricPercentageErrors(actual, predicted, output);
|
|
|
|
// |100-80| / ((|100|+|80|)/2) * 100 = 20 / 90 * 100 ≈ 22.222
|
|
double expected = 20.0 / 90.0 * 100.0;
|
|
Assert.Equal(expected, output[0], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void SymmetricPercentageErrors_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ComputeSymmetricPercentageErrors(
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void SymmetricPercentageErrors_LengthMismatch_Throws()
|
|
{
|
|
double[] a = [1.0];
|
|
double[] b = [1.0, 2.0];
|
|
double[] o = [0.0];
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ComputeSymmetricPercentageErrors(a, b, o));
|
|
}
|
|
|
|
[Fact]
|
|
public void SymmetricPercentageErrors_BothNearZero_ReturnsZero()
|
|
{
|
|
double[] actual = [1e-15];
|
|
double[] predicted = [1e-15];
|
|
double[] output = new double[1];
|
|
|
|
ErrorHelpers.ComputeSymmetricPercentageErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(0.0, output[0], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void SymmetricPercentageErrors_WithNaN_SubstitutesLastValid()
|
|
{
|
|
double[] actual = [100.0, double.NaN];
|
|
double[] predicted = [80.0, 90.0];
|
|
double[] output = new double[2];
|
|
|
|
ErrorHelpers.ComputeSymmetricPercentageErrors(actual, predicted, output);
|
|
|
|
// [1]: NaN→100: |100-90| / ((100+90)/2) * 100 = 10/95*100
|
|
double expected1 = 10.0 / 95.0 * 100.0;
|
|
Assert.Equal(expected1, output[1], Tolerance);
|
|
}
|
|
|
|
// ── ComputeLogCoshErrors ────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void LogCoshErrors_ZeroError_ReturnsZero()
|
|
{
|
|
double[] actual = [5.0];
|
|
double[] predicted = [5.0];
|
|
double[] output = new double[1];
|
|
|
|
ErrorHelpers.ComputeLogCoshErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(0.0, output[0], Tolerance); // log(cosh(0)) = 0
|
|
}
|
|
|
|
[Fact]
|
|
public void LogCoshErrors_SmallError_UsesExactFormula()
|
|
{
|
|
double[] actual = [10.0];
|
|
double[] predicted = [7.0];
|
|
double[] output = new double[1];
|
|
|
|
ErrorHelpers.ComputeLogCoshErrors(actual, predicted, output);
|
|
|
|
double expected = Math.Log(Math.Cosh(3.0));
|
|
Assert.Equal(expected, output[0], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void LogCoshErrors_LargeError_UsesApproximation()
|
|
{
|
|
// |x| > 20 triggers approximation: |x| - log(2)
|
|
double[] actual = [100.0];
|
|
double[] predicted = [50.0];
|
|
double[] output = new double[1];
|
|
|
|
ErrorHelpers.ComputeLogCoshErrors(actual, predicted, output);
|
|
|
|
double expected = 50.0 - Math.Log(2.0);
|
|
Assert.Equal(expected, output[0], 1e-6);
|
|
}
|
|
|
|
[Fact]
|
|
public void LogCoshErrors_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ComputeLogCoshErrors(
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void LogCoshErrors_LengthMismatch_Throws()
|
|
{
|
|
double[] a = [1.0, 2.0];
|
|
double[] b = [1.0];
|
|
double[] o = [0.0, 0.0];
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ComputeLogCoshErrors(a, b, o));
|
|
}
|
|
|
|
[Fact]
|
|
public void LogCoshErrors_AlwaysNonNegative()
|
|
{
|
|
double[] actual = [5.0, -3.0, 10.0];
|
|
double[] predicted = [8.0, -1.0, 10.0];
|
|
double[] output = new double[3];
|
|
|
|
ErrorHelpers.ComputeLogCoshErrors(actual, predicted, output);
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
Assert.True(output[i] >= 0.0, $"LogCosh at {i} was {output[i]}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LogCoshErrors_WithNaN_SubstitutesLastValid()
|
|
{
|
|
double[] actual = [10.0, double.NaN];
|
|
double[] predicted = [7.0, 7.0];
|
|
double[] output = new double[2];
|
|
|
|
ErrorHelpers.ComputeLogCoshErrors(actual, predicted, output);
|
|
|
|
double expected = Math.Log(Math.Cosh(3.0));
|
|
Assert.Equal(expected, output[0], Tolerance);
|
|
Assert.Equal(expected, output[1], Tolerance); // NaN→10, same result
|
|
}
|
|
|
|
// ── ComputePseudoHuberErrors ────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void PseudoHuberErrors_ZeroError_ReturnsZero()
|
|
{
|
|
double[] actual = [5.0];
|
|
double[] predicted = [5.0];
|
|
double[] output = new double[1];
|
|
|
|
ErrorHelpers.ComputePseudoHuberErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(0.0, output[0], Tolerance); // δ²(√(1+0)-1) = 0
|
|
}
|
|
|
|
[Fact]
|
|
public void PseudoHuberErrors_BasicComputation()
|
|
{
|
|
double[] actual = [10.0];
|
|
double[] predicted = [8.0];
|
|
double[] output = new double[1];
|
|
double delta = 1.0;
|
|
|
|
ErrorHelpers.ComputePseudoHuberErrors(actual, predicted, output, delta);
|
|
|
|
// δ²(√(1+(2/1)²)-1) = 1*(√5-1) ≈ 1.2360679...
|
|
double expected = Math.Sqrt(1.0 + 4.0) - 1.0;
|
|
Assert.Equal(expected, output[0], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void PseudoHuberErrors_CustomDelta()
|
|
{
|
|
double[] actual = [10.0];
|
|
double[] predicted = [8.0];
|
|
double[] output = new double[1];
|
|
double delta = 2.0;
|
|
|
|
ErrorHelpers.ComputePseudoHuberErrors(actual, predicted, output, delta);
|
|
|
|
// δ²(√(1+(2/2)²)-1) = 4*(√2-1) ≈ 1.6568...
|
|
double expected = 4.0 * (Math.Sqrt(2.0) - 1.0);
|
|
Assert.Equal(expected, output[0], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void PseudoHuberErrors_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ComputePseudoHuberErrors(
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void PseudoHuberErrors_LengthMismatch_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ComputePseudoHuberErrors([1.0], [1.0, 2.0], new double[1]));
|
|
}
|
|
|
|
[Fact]
|
|
public void PseudoHuberErrors_WithNaN_SubstitutesLastValid()
|
|
{
|
|
double[] actual = [10.0, double.NaN];
|
|
double[] predicted = [8.0, 8.0];
|
|
double[] output = new double[2];
|
|
|
|
ErrorHelpers.ComputePseudoHuberErrors(actual, predicted, output);
|
|
|
|
// Both should compute same result since NaN→10
|
|
Assert.Equal(output[0], output[1], Tolerance);
|
|
}
|
|
|
|
// ── ComputeTukeyBiweightErrors ──────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void TukeyBiweightErrors_SmallError_InlierFormula()
|
|
{
|
|
double[] actual = [10.0];
|
|
double[] predicted = [9.0];
|
|
double[] output = new double[1];
|
|
double c = 4.685;
|
|
|
|
ErrorHelpers.ComputeTukeyBiweightErrors(actual, predicted, output, c);
|
|
|
|
// diff = 1.0, |diff| ≤ c
|
|
double ratio = 1.0 / c;
|
|
double ratioSq = ratio * ratio;
|
|
double oneMinusRatioSq = 1.0 - ratioSq;
|
|
double cubed = oneMinusRatioSq * oneMinusRatioSq * oneMinusRatioSq;
|
|
double expected = (c * c / 6.0) * (1.0 - cubed);
|
|
Assert.Equal(expected, output[0], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void TukeyBiweightErrors_LargeError_OutlierRejection()
|
|
{
|
|
double[] actual = [100.0];
|
|
double[] predicted = [0.0];
|
|
double[] output = new double[1];
|
|
double c = 4.685;
|
|
|
|
ErrorHelpers.ComputeTukeyBiweightErrors(actual, predicted, output, c);
|
|
|
|
// |diff| = 100 > c, so output = c²/6
|
|
double expected = c * c / 6.0;
|
|
Assert.Equal(expected, output[0], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void TukeyBiweightErrors_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ComputeTukeyBiweightErrors(
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void TukeyBiweightErrors_LengthMismatch_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ComputeTukeyBiweightErrors([1.0, 2.0], [1.0], new double[2]));
|
|
}
|
|
|
|
[Fact]
|
|
public void TukeyBiweightErrors_CustomC()
|
|
{
|
|
double[] actual = [10.0];
|
|
double[] predicted = [8.0];
|
|
double[] output = new double[1];
|
|
double c = 2.0; // Small c so diff=2 is right at boundary
|
|
|
|
ErrorHelpers.ComputeTukeyBiweightErrors(actual, predicted, output, c);
|
|
|
|
// |diff| = 2.0 = c, so ratio = 1, ratioSq = 1, 1-ratioSq = 0, cubed = 0
|
|
// output = c²/6 * (1-0) = c²/6
|
|
double expected = c * c / 6.0;
|
|
Assert.Equal(expected, output[0], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void TukeyBiweightErrors_WithNaN_SubstitutesLastValid()
|
|
{
|
|
double[] actual = [10.0, double.NaN];
|
|
double[] predicted = [9.0, 9.0];
|
|
double[] output = new double[2];
|
|
|
|
ErrorHelpers.ComputeTukeyBiweightErrors(actual, predicted, output);
|
|
|
|
// Both should compute same result since NaN→10
|
|
Assert.Equal(output[0], output[1], Tolerance);
|
|
}
|
|
|
|
// ── ComputeHuberErrors ──────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void HuberErrors_SmallError_QuadraticRegion()
|
|
{
|
|
double[] actual = [10.0];
|
|
double[] predicted = [9.5];
|
|
double[] output = new double[1];
|
|
double delta = 1.0;
|
|
|
|
ErrorHelpers.ComputeHuberErrors(actual, predicted, output, delta);
|
|
|
|
// |diff| = 0.5 ≤ delta → 0.5 * diff² = 0.5 * 0.25 = 0.125
|
|
Assert.Equal(0.125, output[0], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void HuberErrors_LargeError_LinearRegion()
|
|
{
|
|
double[] actual = [10.0];
|
|
double[] predicted = [5.0];
|
|
double[] output = new double[1];
|
|
double delta = 1.0;
|
|
|
|
ErrorHelpers.ComputeHuberErrors(actual, predicted, output, delta);
|
|
|
|
// |diff| = 5 > delta → delta * (|diff| - 0.5*delta) = 1*(5-0.5) = 4.5
|
|
Assert.Equal(4.5, output[0], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void HuberErrors_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ComputeHuberErrors(
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void HuberErrors_LengthMismatch_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ComputeHuberErrors([1.0], [1.0, 2.0], new double[1]));
|
|
}
|
|
|
|
[Fact]
|
|
public void HuberErrors_CustomDelta()
|
|
{
|
|
double[] actual = [10.0];
|
|
double[] predicted = [7.0];
|
|
double[] output = new double[1];
|
|
double delta = 2.0;
|
|
|
|
ErrorHelpers.ComputeHuberErrors(actual, predicted, output, delta);
|
|
|
|
// |diff| = 3 > delta=2 → 2*(3-1) = 4.0
|
|
Assert.Equal(4.0, output[0], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void HuberErrors_ExactlyAtDelta_UsesQuadratic()
|
|
{
|
|
double[] actual = [10.0];
|
|
double[] predicted = [9.0];
|
|
double[] output = new double[1];
|
|
double delta = 1.0;
|
|
|
|
ErrorHelpers.ComputeHuberErrors(actual, predicted, output, delta);
|
|
|
|
// |diff| = 1.0 = delta → 0.5 * 1² = 0.5
|
|
Assert.Equal(0.5, output[0], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void HuberErrors_WithNaN_SubstitutesLastValid()
|
|
{
|
|
double[] actual = [10.0, double.NaN];
|
|
double[] predicted = [9.5, 9.5];
|
|
double[] output = new double[2];
|
|
|
|
ErrorHelpers.ComputeHuberErrors(actual, predicted, output);
|
|
|
|
Assert.Equal(output[0], output[1], Tolerance);
|
|
}
|
|
|
|
// ── ApplyRollingMean ────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void RollingMean_BasicComputation()
|
|
{
|
|
double[] errors = [2.0, 4.0, 6.0, 8.0, 10.0];
|
|
double[] output = new double[5];
|
|
|
|
ErrorHelpers.ApplyRollingMean(errors, output, period: 3);
|
|
|
|
// Warmup: output[0]=2/1=2, output[1]=(2+4)/2=3, output[2]=(2+4+6)/3=4
|
|
Assert.Equal(2.0, output[0], Tolerance);
|
|
Assert.Equal(3.0, output[1], Tolerance);
|
|
Assert.Equal(4.0, output[2], Tolerance);
|
|
// Main: output[3]=(4+6+8)/3=6, output[4]=(6+8+10)/3=8
|
|
Assert.Equal(6.0, output[3], Tolerance);
|
|
Assert.Equal(8.0, output[4], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingMean_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ApplyRollingMean(
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty,
|
|
period: 3);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingMean_LengthMismatch_Throws()
|
|
{
|
|
double[] a = [1.0, 2.0];
|
|
double[] b = [0.0];
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ApplyRollingMean(a, b, period: 2));
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingMean_PeriodZero_Throws()
|
|
{
|
|
double[] a = [1.0];
|
|
double[] b = [0.0];
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ApplyRollingMean(a, b, period: 0));
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingMean_NegativePeriod_Throws()
|
|
{
|
|
double[] a = [1.0];
|
|
double[] b = [0.0];
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ApplyRollingMean(a, b, period: -1));
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingMean_PeriodGreaterThanLength_WarmupOnly()
|
|
{
|
|
double[] errors = [2.0, 4.0, 6.0];
|
|
double[] output = new double[3];
|
|
|
|
// Period 10 > length 3 → all in warmup phase
|
|
ErrorHelpers.ApplyRollingMean(errors, output, period: 10);
|
|
|
|
Assert.Equal(2.0, output[0], Tolerance); // 2/1
|
|
Assert.Equal(3.0, output[1], Tolerance); // (2+4)/2
|
|
Assert.Equal(4.0, output[2], Tolerance); // (2+4+6)/3
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingMean_ResyncCorrectsDrift()
|
|
{
|
|
// Use a short resync interval to trigger the resync path
|
|
int period = 3;
|
|
int len = 10;
|
|
double[] errors = new double[len];
|
|
double[] output = new double[len];
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
errors[i] = 1.0; // Constant 1.0
|
|
}
|
|
|
|
ErrorHelpers.ApplyRollingMean(errors, output, period, resyncInterval: 3);
|
|
|
|
// After warmup, all values should be 1.0 (mean of three 1.0s)
|
|
for (int i = period - 1; i < len; i++)
|
|
{
|
|
Assert.Equal(1.0, output[i], 1e-8);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingMean_LargePeriod_UsesArrayPool()
|
|
{
|
|
// Period > 256 triggers ArrayPool path
|
|
int period = 300;
|
|
int len = period + 10;
|
|
double[] errors = new double[len];
|
|
double[] output = new double[len];
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
errors[i] = 2.0;
|
|
}
|
|
|
|
ErrorHelpers.ApplyRollingMean(errors, output, period);
|
|
|
|
// After warmup, should be 2.0 (mean of constant 2.0)
|
|
Assert.Equal(2.0, output[len - 1], 1e-8);
|
|
}
|
|
|
|
// ── ApplyRollingMeanSqrt ────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void RollingMeanSqrt_BasicComputation()
|
|
{
|
|
double[] squaredErrors = [4.0, 9.0, 16.0];
|
|
double[] output = new double[3];
|
|
|
|
ErrorHelpers.ApplyRollingMeanSqrt(squaredErrors, output, period: 2);
|
|
|
|
// Warmup: output[0] = √(4/1) = 2
|
|
Assert.Equal(2.0, output[0], Tolerance);
|
|
// output[1] = √((4+9)/2) = √6.5
|
|
Assert.Equal(Math.Sqrt(6.5), output[1], Tolerance);
|
|
// Main: output[2] = √((9+16)/2) = √12.5
|
|
Assert.Equal(Math.Sqrt(12.5), output[2], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingMeanSqrt_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ApplyRollingMeanSqrt(
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty,
|
|
period: 3);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingMeanSqrt_LengthMismatch_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ApplyRollingMeanSqrt([1.0, 2.0], new double[1], period: 2));
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingMeanSqrt_PeriodZero_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ApplyRollingMeanSqrt([1.0], new double[1], period: 0));
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingMeanSqrt_LargePeriod_UsesArrayPool()
|
|
{
|
|
int period = 300;
|
|
int len = period + 5;
|
|
double[] errors = new double[len];
|
|
double[] output = new double[len];
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
errors[i] = 9.0;
|
|
}
|
|
|
|
ErrorHelpers.ApplyRollingMeanSqrt(errors, output, period);
|
|
|
|
// √(9) = 3
|
|
Assert.Equal(3.0, output[len - 1], 1e-8);
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingMeanSqrt_ResyncCorrectsDrift()
|
|
{
|
|
int period = 3;
|
|
int len = 10;
|
|
double[] errors = new double[len];
|
|
double[] output = new double[len];
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
errors[i] = 4.0;
|
|
}
|
|
|
|
ErrorHelpers.ApplyRollingMeanSqrt(errors, output, period, resyncInterval: 3);
|
|
|
|
for (int i = period - 1; i < len; i++)
|
|
{
|
|
Assert.Equal(2.0, output[i], 1e-8); // √(4) = 2
|
|
}
|
|
}
|
|
|
|
// ── ApplyRollingWeightedMeanSqrt ────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void RollingWeightedMeanSqrt_BasicComputation()
|
|
{
|
|
double[] wse = [4.0, 8.0, 12.0];
|
|
double[] weights = [1.0, 2.0, 3.0];
|
|
double[] output = new double[3];
|
|
|
|
ErrorHelpers.ApplyRollingWeightedMeanSqrt(wse, weights, output, period: 2);
|
|
|
|
// Warmup[0]: √(4/1) = 2
|
|
Assert.Equal(2.0, output[0], Tolerance);
|
|
// Warmup[1]: √((4+8)/(1+2)) = √(12/3) = √4 = 2
|
|
Assert.Equal(2.0, output[1], Tolerance);
|
|
// Main[2]: √((8+12)/(2+3)) = √(20/5) = √4 = 2
|
|
Assert.Equal(2.0, output[2], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingWeightedMeanSqrt_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.ApplyRollingWeightedMeanSqrt(
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty,
|
|
period: 3);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingWeightedMeanSqrt_LengthMismatch_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ApplyRollingWeightedMeanSqrt(
|
|
[1.0, 2.0], [1.0], new double[2], period: 2));
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingWeightedMeanSqrt_PeriodZero_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.ApplyRollingWeightedMeanSqrt(
|
|
[1.0], [1.0], new double[1], period: 0));
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingWeightedMeanSqrt_ZeroWeights_ReturnsZero()
|
|
{
|
|
double[] wse = [10.0, 20.0, 30.0];
|
|
double[] weights = [0.0, 0.0, 0.0];
|
|
double[] output = new double[3];
|
|
|
|
ErrorHelpers.ApplyRollingWeightedMeanSqrt(wse, weights, output, period: 2);
|
|
|
|
// sumWeights ≤ 1e-10 → returns 0.0
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
Assert.Equal(0.0, output[i], Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingWeightedMeanSqrt_LargePeriod_UsesArrayPool()
|
|
{
|
|
int period = 300;
|
|
int len = period + 5;
|
|
double[] wse = new double[len];
|
|
double[] weights = new double[len];
|
|
double[] output = new double[len];
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
wse[i] = 9.0;
|
|
weights[i] = 1.0;
|
|
}
|
|
|
|
ErrorHelpers.ApplyRollingWeightedMeanSqrt(wse, weights, output, period);
|
|
|
|
// √(9*period / period) = √9 = 3
|
|
Assert.Equal(3.0, output[len - 1], 1e-8);
|
|
}
|
|
|
|
[Fact]
|
|
public void RollingWeightedMeanSqrt_ResyncCorrectsDrift()
|
|
{
|
|
int period = 2;
|
|
int len = 10;
|
|
double[] wse = new double[len];
|
|
double[] weights = new double[len];
|
|
double[] output = new double[len];
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
wse[i] = 16.0;
|
|
weights[i] = 1.0;
|
|
}
|
|
|
|
ErrorHelpers.ApplyRollingWeightedMeanSqrt(wse, weights, output, period, resyncInterval: 3);
|
|
|
|
for (int i = period - 1; i < len; i++)
|
|
{
|
|
Assert.Equal(4.0, output[i], 1e-8); // √(16) = 4
|
|
}
|
|
}
|
|
|
|
// ── SanitizeInputs ──────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void SanitizeInputs_CleanData_CopiesAsIs()
|
|
{
|
|
double[] actual = [1.0, 2.0, 3.0];
|
|
double[] predicted = [4.0, 5.0, 6.0];
|
|
double[] actualOut = new double[3];
|
|
double[] predictedOut = new double[3];
|
|
|
|
ErrorHelpers.SanitizeInputs(actual, predicted, actualOut, predictedOut);
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
Assert.Equal(actual[i], actualOut[i], Tolerance);
|
|
Assert.Equal(predicted[i], predictedOut[i], Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SanitizeInputs_WithNaN_ReplacesWithLastValid()
|
|
{
|
|
double[] actual = [10.0, double.NaN, 30.0];
|
|
double[] predicted = [5.0, double.NaN, 15.0];
|
|
double[] actualOut = new double[3];
|
|
double[] predictedOut = new double[3];
|
|
|
|
ErrorHelpers.SanitizeInputs(actual, predicted, actualOut, predictedOut);
|
|
|
|
Assert.Equal(10.0, actualOut[0], Tolerance);
|
|
Assert.Equal(10.0, actualOut[1], Tolerance); // NaN → 10
|
|
Assert.Equal(30.0, actualOut[2], Tolerance);
|
|
Assert.Equal(5.0, predictedOut[0], Tolerance);
|
|
Assert.Equal(5.0, predictedOut[1], Tolerance); // NaN → 5
|
|
Assert.Equal(15.0, predictedOut[2], Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void SanitizeInputs_EmptySpan_NoOp()
|
|
{
|
|
ErrorHelpers.SanitizeInputs(
|
|
ReadOnlySpan<double>.Empty,
|
|
ReadOnlySpan<double>.Empty,
|
|
Span<double>.Empty,
|
|
Span<double>.Empty);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public void SanitizeInputs_LengthMismatch_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ErrorHelpers.SanitizeInputs([1.0], [1.0, 2.0], new double[1], new double[1]));
|
|
}
|
|
|
|
[Fact]
|
|
public void SanitizeInputs_AllNaN_UsesZero()
|
|
{
|
|
double[] actual = [double.NaN, double.NaN];
|
|
double[] predicted = [double.NaN, double.NaN];
|
|
double[] actualOut = new double[2];
|
|
double[] predictedOut = new double[2];
|
|
|
|
ErrorHelpers.SanitizeInputs(actual, predicted, actualOut, predictedOut);
|
|
|
|
// FindFirstValidValue returns 0.0 when all NaN
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
Assert.Equal(0.0, actualOut[i], Tolerance);
|
|
Assert.Equal(0.0, predictedOut[i], Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SanitizeInputs_InfinityReplacedWithLastValid()
|
|
{
|
|
double[] actual = [10.0, double.PositiveInfinity, 30.0];
|
|
double[] predicted = [5.0, double.NegativeInfinity, 15.0];
|
|
double[] actualOut = new double[3];
|
|
double[] predictedOut = new double[3];
|
|
|
|
ErrorHelpers.SanitizeInputs(actual, predicted, actualOut, predictedOut);
|
|
|
|
Assert.Equal(10.0, actualOut[1], Tolerance); // Inf → 10
|
|
Assert.Equal(5.0, predictedOut[1], Tolerance); // -Inf → 5
|
|
}
|
|
|
|
// ── Cross-method consistency ────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void SignedErrors_AbsoluteErrors_Consistency()
|
|
{
|
|
// |signed| should equal absolute
|
|
double[] actual = [10.0, -5.0, 20.0, 0.0, -3.0];
|
|
double[] predicted = [7.0, -2.0, 25.0, -1.0, 3.0];
|
|
double[] signedOut = new double[5];
|
|
double[] absOut = new double[5];
|
|
|
|
ErrorHelpers.ComputeSignedErrors(actual, predicted, signedOut);
|
|
ErrorHelpers.ComputeAbsoluteErrors(actual, predicted, absOut);
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
Assert.Equal(Math.Abs(signedOut[i]), absOut[i], Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SquaredErrors_EqualsSignedErrorsSquared()
|
|
{
|
|
double[] actual = [10.0, 5.0, -3.0];
|
|
double[] predicted = [8.0, 7.0, -1.0];
|
|
double[] signedOut = new double[3];
|
|
double[] sqOut = new double[3];
|
|
|
|
ErrorHelpers.ComputeSignedErrors(actual, predicted, signedOut);
|
|
ErrorHelpers.ComputeSquaredErrors(actual, predicted, sqOut);
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
Assert.Equal(signedOut[i] * signedOut[i], sqOut[i], Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void PerfectPrediction_AllErrorsZero()
|
|
{
|
|
double[] actual = [1.0, 2.0, 3.0, 4.0];
|
|
double[] predicted = [1.0, 2.0, 3.0, 4.0];
|
|
|
|
double[] signed = new double[4];
|
|
double[] abs = new double[4];
|
|
double[] sq = new double[4];
|
|
double[] logcosh = new double[4];
|
|
|
|
ErrorHelpers.ComputeSignedErrors(actual, predicted, signed);
|
|
ErrorHelpers.ComputeAbsoluteErrors(actual, predicted, abs);
|
|
ErrorHelpers.ComputeSquaredErrors(actual, predicted, sq);
|
|
ErrorHelpers.ComputeLogCoshErrors(actual, predicted, logcosh);
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
Assert.Equal(0.0, signed[i], Tolerance);
|
|
Assert.Equal(0.0, abs[i], Tolerance);
|
|
Assert.Equal(0.0, sq[i], Tolerance);
|
|
Assert.Equal(0.0, logcosh[i], Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void HuberErrors_ApproachesQuadratic_ForSmallErrors()
|
|
{
|
|
// For very small errors, Huber ≈ 0.5 * error²
|
|
double[] actual = [10.0];
|
|
double[] predicted = [10.001];
|
|
double[] huberOut = new double[1];
|
|
double[] sqOut = new double[1];
|
|
|
|
ErrorHelpers.ComputeHuberErrors(actual, predicted, huberOut, delta: 1.0);
|
|
ErrorHelpers.ComputeSquaredErrors(actual, predicted, sqOut);
|
|
|
|
// Huber should be 0.5 * squared for small |diff|
|
|
Assert.Equal(0.5 * sqOut[0], huberOut[0], 1e-8);
|
|
}
|
|
|
|
[Fact]
|
|
public void SymmetricPercentageErrors_Symmetric()
|
|
{
|
|
// SMAPE should give same result regardless of which is actual/predicted
|
|
double[] a = [100.0];
|
|
double[] b = [80.0];
|
|
double[] out1 = new double[1];
|
|
double[] out2 = new double[1];
|
|
|
|
ErrorHelpers.ComputeSymmetricPercentageErrors(a, b, out1);
|
|
ErrorHelpers.ComputeSymmetricPercentageErrors(b, a, out2);
|
|
|
|
Assert.Equal(out1[0], out2[0], Tolerance);
|
|
}
|
|
}
|