using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace QuanTAlib;
///
/// SIMD-accelerated error computation helpers for error indicators.
/// Provides shared methods for computing absolute errors, squared errors,
/// and percentage errors with automatic SIMD/scalar fallback.
///
public static class ErrorHelpers
{
///
/// Default stack allocation threshold for temporary buffers.
/// 256 doubles = 2KB, safe margin for nested calls on 1MB thread stack.
/// Beyond this threshold, ArrayPool is used instead of stackalloc.
///
public const int StackAllocThreshold = 256;
///
/// Default resync interval for running sums to correct floating-point drift.
///
public const int DefaultResyncInterval = 1000;
private const string SpanLengthMismatchMessage = "All spans must have the same length";
///
/// Computes signed errors: actual - predicted (preserves sign for bias detection)
/// Uses AVX2 SIMD when available for clean data, with scalar fallback for NaN handling.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ComputeSignedErrors(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
// Try SIMD path - NaN detection is integrated into the SIMD loop
if (Avx2.IsSupported && len >= Vector256.Count)
{
int processedCount = ComputeSignedErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
if (processedCount == len)
{
return; // All processed via SIMD
}
// Continue with scalar for remaining elements (NaN was detected)
ComputeSignedErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted);
return;
}
// Scalar fallback with NaN handling
ComputeSignedErrorsScalar(actual, predicted, output, lastValidActual, lastValidPredicted);
}
///
/// Computes absolute errors: |actual - predicted|
/// Uses AVX2 SIMD when available with integrated NaN detection, with scalar fallback for NaN handling.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ComputeAbsoluteErrors(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
// Try SIMD path - NaN detection is integrated into the SIMD loop (avoids double-pass)
if (Avx2.IsSupported && len >= Vector256.Count)
{
int processedCount = ComputeAbsoluteErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
if (processedCount == len)
{
return; // All processed via SIMD
}
// Continue with scalar for remaining elements (NaN was detected)
ComputeAbsoluteErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted);
return;
}
// Scalar fallback with NaN handling
ComputeAbsoluteErrorsScalar(actual, predicted, output, lastValidActual, lastValidPredicted);
}
///
/// Computes squared errors: (actual - predicted)²
/// Uses AVX2 SIMD when available with integrated NaN detection, with scalar fallback for NaN handling.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ComputeSquaredErrors(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
// Try SIMD path - NaN detection is integrated into the SIMD loop (avoids double-pass)
if (Avx2.IsSupported && len >= Vector256.Count)
{
int processedCount = ComputeSquaredErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
if (processedCount == len)
{
return; // All processed via SIMD
}
// Continue with scalar for remaining elements (NaN was detected)
ComputeSquaredErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted);
return;
}
// Scalar fallback with NaN handling
ComputeSquaredErrorsScalar(actual, predicted, output, lastValidActual, lastValidPredicted);
}
///
/// Computes weighted errors: weight * |actual - predicted|
/// Used by WRMSE and other weighted error indicators.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ComputeWeightedErrors(
ReadOnlySpan actual,
ReadOnlySpan predicted,
ReadOnlySpan weights,
Span output)
{
if (actual.Length != predicted.Length || actual.Length != weights.Length || actual.Length != output.Length)
{
throw new ArgumentException("All spans must have the same length", nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
double lastValidWeight = FindFirstValidValue(weights);
double currentValidActual = lastValidActual;
double currentValidPredicted = lastValidPredicted;
double currentValidWeight = lastValidWeight;
for (int i = 0; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
double wgt = weights[i];
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
if (double.IsFinite(wgt))
{
currentValidWeight = wgt;
}
else
{
wgt = currentValidWeight;
}
double diff = act - pred;
output[i] = wgt * diff * diff;
}
}
///
/// Computes percentage errors: |actual - predicted| / |actual| * 100
/// Uses scalar path with NaN handling (percentage errors require division guards).
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ComputePercentageErrors(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output,
double epsilon = 1e-10)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
double currentValidActual = lastValidActual;
double currentValidPredicted = lastValidPredicted;
for (int i = 0; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
act = double.IsFinite(act) ? (currentValidActual = act) : currentValidActual;
pred = double.IsFinite(pred) ? (currentValidPredicted = pred) : currentValidPredicted;
#pragma warning restore S1121
double absActual = Math.Abs(act);
output[i] = absActual < epsilon
? Math.Abs(act - pred)
: Math.Abs(act - pred) / absActual * 100.0;
}
}
///
/// Computes symmetric percentage errors: |actual - predicted| / ((|actual| + |predicted|) / 2) * 100
/// Used by SMAPE indicator.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ComputeSymmetricPercentageErrors(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output,
double epsilon = 1e-10)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
double currentValidActual = lastValidActual;
double currentValidPredicted = lastValidPredicted;
for (int i = 0; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double denominator = (Math.Abs(act) + Math.Abs(pred)) / 2.0;
output[i] = denominator < epsilon
? 0.0 // Both values near zero
: Math.Abs(act - pred) / denominator * 100.0;
}
}
///
/// Computes log-cosh errors: log(cosh(actual - predicted))
/// Smoother alternative to squared errors.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ComputeLogCoshErrors(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
double currentValidActual = lastValidActual;
double currentValidPredicted = lastValidPredicted;
for (int i = 0; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
// log(cosh(x)) ≈ |x| - log(2) for large |x|, numerically stable
output[i] = LogCosh(diff);
}
}
///
/// Computes Pseudo-Huber errors: δ² * (√(1 + (error/δ)²) - 1)
/// Smooth approximation to Huber loss, also known as Charbonnier loss.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ComputePseudoHuberErrors(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output,
double delta = 1.0)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
double deltaSquared = delta * delta;
double currentValidActual = lastValidActual;
double currentValidPredicted = lastValidPredicted;
for (int i = 0; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
double ratio = diff / delta;
// δ² * (√(1 + (error/δ)²) - 1)
output[i] = deltaSquared * (Math.Sqrt(1.0 + ratio * ratio) - 1.0);
}
}
///
/// Computes Tukey's Biweight (Bisquare) errors:
/// ρ(x) = (c²/6) * (1 - (1 - (x/c)²)³) for |x| ≤ c
/// ρ(x) = c²/6 for |x| > c
/// Redescending M-estimator that completely rejects outliers.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ComputeTukeyBiweightErrors(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output,
double c = 4.685)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
double cSquaredOver6 = (c * c) / 6.0;
double currentValidActual = lastValidActual;
double currentValidPredicted = lastValidPredicted;
for (int i = 0; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
double absDiff = Math.Abs(diff);
if (absDiff > c)
{
output[i] = cSquaredOver6;
}
else
{
double ratio = diff / c;
double ratioSq = ratio * ratio;
double oneMinusRatioSq = 1.0 - ratioSq;
double cubed = oneMinusRatioSq * oneMinusRatioSq * oneMinusRatioSq;
output[i] = cSquaredOver6 * (1.0 - cubed);
}
}
}
///
/// Computes Huber errors: 0.5*x² for |x| ≤ δ, δ*(|x| - 0.5*δ) otherwise
/// Robust to outliers.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ComputeHuberErrors(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output,
double delta = 1.0)
{
if (actual.Length != predicted.Length || actual.Length != output.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(output));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
double halfDelta = 0.5 * delta;
double currentValidActual = lastValidActual;
double currentValidPredicted = lastValidPredicted;
for (int i = 0; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
double absDiff = Math.Abs(diff);
output[i] = absDiff <= delta
? 0.5 * diff * diff
: delta * (absDiff - halfDelta);
}
}
///
/// Applies rolling window mean to pre-computed errors.
/// Uses O(1) running sum with periodic resync for floating-point drift correction.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ApplyRollingMean(
ReadOnlySpan errors,
Span output,
int period,
int resyncInterval = 1000)
{
if (errors.Length != output.Length)
{
throw new ArgumentException("Spans must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = errors.Length;
if (len == 0)
{
return;
}
double[]? rented = null;
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
Span buffer = period <= StackAllocThreshold
? stackalloc double[period]
: (rented = ArrayPool.Shared.Rent(period)).AsSpan(0, period);
#pragma warning restore S1121
try
{
double sum = 0;
int bufferIndex = 0;
// Warmup phase
int warmupEnd = Math.Min(period, len);
for (int i = 0; i < warmupEnd; i++)
{
sum += errors[i];
buffer[i] = errors[i];
output[i] = sum / (i + 1);
}
// Main loop with O(1) update
int tickCount = 0;
for (int i = warmupEnd; i < len; i++)
{
double error = errors[i];
sum = sum - buffer[bufferIndex] + error;
buffer[bufferIndex] = error;
bufferIndex++;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
output[i] = sum / period;
tickCount++;
if (tickCount >= resyncInterval)
{
tickCount = 0;
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
recalcSum += buffer[k];
}
sum = recalcSum;
}
}
}
finally
{
if (rented is not null)
{
ArrayPool.Shared.Return(rented, clearArray: false);
}
}
}
///
/// Applies rolling window mean with square root of result (for RMSE-style indicators).
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ApplyRollingMeanSqrt(
ReadOnlySpan squaredErrors,
Span output,
int period,
int resyncInterval = 1000)
{
if (squaredErrors.Length != output.Length)
{
throw new ArgumentException("Spans must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = squaredErrors.Length;
if (len == 0)
{
return;
}
double[]? rented = null;
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
Span buffer = period <= StackAllocThreshold
? stackalloc double[period]
: (rented = ArrayPool.Shared.Rent(period)).AsSpan(0, period);
#pragma warning restore S1121
try
{
double sum = 0;
int bufferIndex = 0;
// Warmup phase
int warmupEnd = Math.Min(period, len);
for (int i = 0; i < warmupEnd; i++)
{
sum += squaredErrors[i];
buffer[i] = squaredErrors[i];
output[i] = Math.Sqrt(sum / (i + 1));
}
// Main loop with O(1) update
int tickCount = 0;
for (int i = warmupEnd; i < len; i++)
{
double sqError = squaredErrors[i];
sum = sum - buffer[bufferIndex] + sqError;
buffer[bufferIndex] = sqError;
bufferIndex++;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
output[i] = Math.Sqrt(sum / period);
tickCount++;
if (tickCount >= resyncInterval)
{
tickCount = 0;
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
recalcSum += buffer[k];
}
sum = recalcSum;
}
}
}
finally
{
if (rented is not null)
{
ArrayPool.Shared.Return(rented, clearArray: false);
}
}
}
///
/// Applies rolling window weighted mean with square root of result (for WRMSE-style indicators).
/// Computes sqrt(sum(weighted_errors) / sum(weights)) over a rolling window.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ApplyRollingWeightedMeanSqrt(
ReadOnlySpan weightedSquaredErrors,
ReadOnlySpan weights,
Span output,
int period,
int resyncInterval = 1000)
{
if (weightedSquaredErrors.Length != output.Length || weightedSquaredErrors.Length != weights.Length)
{
throw new ArgumentException("Spans must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = weightedSquaredErrors.Length;
if (len == 0)
{
return;
}
double[]? rentedErrors = null;
double[]? rentedWeights = null;
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
Span errorBuffer = period <= StackAllocThreshold
? stackalloc double[period]
: (rentedErrors = ArrayPool.Shared.Rent(period)).AsSpan(0, period);
Span weightBuffer = period <= StackAllocThreshold
? stackalloc double[period]
: (rentedWeights = ArrayPool.Shared.Rent(period)).AsSpan(0, period);
#pragma warning restore S1121
try
{
double sumErrors = 0;
double sumWeights = 0;
int bufferIndex = 0;
// Warmup phase
int warmupEnd = Math.Min(period, len);
for (int i = 0; i < warmupEnd; i++)
{
sumErrors += weightedSquaredErrors[i];
sumWeights += weights[i];
errorBuffer[i] = weightedSquaredErrors[i];
weightBuffer[i] = weights[i];
output[i] = sumWeights > 1e-10 ? Math.Sqrt(sumErrors / sumWeights) : 0.0;
}
// Main loop with O(1) update
int tickCount = 0;
for (int i = warmupEnd; i < len; i++)
{
double wse = weightedSquaredErrors[i];
double wgt = weights[i];
sumErrors = sumErrors - errorBuffer[bufferIndex] + wse;
sumWeights = sumWeights - weightBuffer[bufferIndex] + wgt;
errorBuffer[bufferIndex] = wse;
weightBuffer[bufferIndex] = wgt;
bufferIndex++;
if (bufferIndex >= period)
{
bufferIndex = 0;
}
output[i] = sumWeights > 1e-10 ? Math.Sqrt(sumErrors / sumWeights) : 0.0;
tickCount++;
if (tickCount >= resyncInterval)
{
tickCount = 0;
double recalcSumErrors = 0;
double recalcSumWeights = 0;
for (int k = 0; k < period; k++)
{
recalcSumErrors += errorBuffer[k];
recalcSumWeights += weightBuffer[k];
}
sumErrors = recalcSumErrors;
sumWeights = recalcSumWeights;
}
}
}
finally
{
if (rentedErrors is not null)
{
ArrayPool.Shared.Return(rentedErrors, clearArray: false);
}
if (rentedWeights is not null)
{
ArrayPool.Shared.Return(rentedWeights, clearArray: false);
}
}
}
///
/// Sanitizes input spans by replacing NaN/Infinity values with the last valid value.
/// Writes sanitized values to output spans for use in batch calculations.
///
/// Input actual values
/// Input predicted values
/// Output sanitized actual values
/// Output sanitized predicted values
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void SanitizeInputs(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span actualOut,
Span predictedOut)
{
if (actual.Length != predicted.Length || actual.Length != actualOut.Length || actual.Length != predictedOut.Length)
{
throw new ArgumentException(SpanLengthMismatchMessage, nameof(predictedOut));
}
int len = actual.Length;
if (len == 0)
{
return;
}
double lastValidActual = FindFirstValidValue(actual);
double lastValidPredicted = FindFirstValidValue(predicted);
for (int i = 0; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act))
{
lastValidActual = act;
}
else
{
act = lastValidActual;
}
if (double.IsFinite(pred))
{
lastValidPredicted = pred;
}
else
{
pred = lastValidPredicted;
}
actualOut[i] = act;
predictedOut[i] = pred;
}
}
///
/// Finds the first finite value in a span, or returns 0.0 if none found.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double FindFirstValidValue(ReadOnlySpan span)
{
for (int i = 0; i < span.Length; i++)
{
if (double.IsFinite(span[i]))
{
return span[i];
}
}
return 0.0;
}
#region Private Helpers
///
/// SIMD path with integrated NaN detection. Returns the number of elements processed.
/// If NaN is detected, returns the index where NaN was found so caller can continue with scalar.
/// Updates lastValidActual/lastValidPredicted to track last seen finite values for scalar continuation.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int ComputeSignedErrorsSimdWithNaNDetection(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output,
ref double lastValidActual,
ref double lastValidPredicted)
{
int len = actual.Length;
int vectorSize = Vector256.Count;
int vectorEnd = len - (len % vectorSize);
int i = 0;
for (; i < vectorEnd; i += vectorSize)
{
Vector256 actVec = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(actual.Slice(i)));
Vector256 predVec = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(predicted.Slice(i)));
// Check for NaN/Inf: x == x is false for NaN
Vector256 actCmp = Avx.Compare(actVec, actVec, FloatComparisonMode.OrderedNonSignaling);
Vector256 predCmp = Avx.Compare(predVec, predVec, FloatComparisonMode.OrderedNonSignaling);
Vector256 combined = Avx.And(actCmp, predCmp);
int mask = Avx.MoveMask(combined);
if (mask != 0b1111)
{
// NaN detected - update lastValid from previously processed elements before returning
if (i > 0)
{
lastValidActual = actual[i - 1];
lastValidPredicted = predicted[i - 1];
}
return i;
}
// No NaN - compute error
Vector256 errorVec = Avx.Subtract(actVec, predVec);
errorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
}
// Update lastValid from end of SIMD-processed section
if (i > 0)
{
lastValidActual = actual[i - 1];
lastValidPredicted = predicted[i - 1];
}
// Handle scalar remainder
for (; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
if (!double.IsFinite(act) || !double.IsFinite(pred))
{
// Return current position - caller will handle with scalar fallback
return i;
}
lastValidActual = act;
lastValidPredicted = pred;
output[i] = act - pred;
}
return len;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ComputeSignedErrorsScalar(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output,
double lastValidActual,
double lastValidPredicted)
{
int len = actual.Length;
double currentValidActual = lastValidActual;
double currentValidPredicted = lastValidPredicted;
for (int i = 0; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
output[i] = act - pred;
}
}
///
/// SIMD path with integrated NaN detection for absolute errors. Returns the number of elements processed.
/// If NaN is detected, returns the index where NaN was found so caller can continue with scalar.
/// Updates lastValidActual/lastValidPredicted to track last seen finite values for scalar continuation.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int ComputeAbsoluteErrorsSimdWithNaNDetection(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output,
ref double lastValidActual,
ref double lastValidPredicted)
{
int len = actual.Length;
int vectorSize = Vector256.Count;
int vectorEnd = len - (len % vectorSize);
// Mask for absolute value (clear sign bit)
Vector256 absMask = Vector256.Create(~(1L << 63)).AsDouble();
int i = 0;
for (; i < vectorEnd; i += vectorSize)
{
Vector256 actVec = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(actual.Slice(i)));
Vector256 predVec = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(predicted.Slice(i)));
// Check for NaN/Inf: x == x is false for NaN
Vector256 actCmp = Avx.Compare(actVec, actVec, FloatComparisonMode.OrderedNonSignaling);
Vector256 predCmp = Avx.Compare(predVec, predVec, FloatComparisonMode.OrderedNonSignaling);
Vector256 combined = Avx.And(actCmp, predCmp);
int mask = Avx.MoveMask(combined);
if (mask != 0b1111)
{
// NaN detected - update lastValid from previously processed elements before returning
if (i > 0)
{
lastValidActual = actual[i - 1];
lastValidPredicted = predicted[i - 1];
}
return i;
}
// No NaN - compute absolute error: |actual - predicted|
Vector256 errorVec = Avx.Subtract(actVec, predVec);
Vector256 absErrorVec = Avx.And(errorVec, absMask);
absErrorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
}
// Update lastValid from end of SIMD-processed section
if (i > 0)
{
lastValidActual = actual[i - 1];
lastValidPredicted = predicted[i - 1];
}
// Handle scalar remainder
for (; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
if (!double.IsFinite(act) || !double.IsFinite(pred))
{
// Return current position - caller will handle with scalar fallback
return i;
}
lastValidActual = act;
lastValidPredicted = pred;
output[i] = Math.Abs(act - pred);
}
return len;
}
///
/// SIMD path with integrated NaN detection for squared errors. Returns the number of elements processed.
/// If NaN is detected, returns the index where NaN was found so caller can continue with scalar.
/// Updates lastValidActual/lastValidPredicted to track last seen finite values for scalar continuation.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int ComputeSquaredErrorsSimdWithNaNDetection(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output,
ref double lastValidActual,
ref double lastValidPredicted)
{
int len = actual.Length;
int vectorSize = Vector256.Count;
int vectorEnd = len - (len % vectorSize);
int i = 0;
for (; i < vectorEnd; i += vectorSize)
{
Vector256 actVec = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(actual.Slice(i)));
Vector256 predVec = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(predicted.Slice(i)));
// Check for NaN/Inf: x == x is false for NaN
Vector256 actCmp = Avx.Compare(actVec, actVec, FloatComparisonMode.OrderedNonSignaling);
Vector256 predCmp = Avx.Compare(predVec, predVec, FloatComparisonMode.OrderedNonSignaling);
Vector256 combined = Avx.And(actCmp, predCmp);
int mask = Avx.MoveMask(combined);
if (mask != 0b1111)
{
// NaN detected - update lastValid from previously processed elements before returning
if (i > 0)
{
lastValidActual = actual[i - 1];
lastValidPredicted = predicted[i - 1];
}
return i;
}
// No NaN - compute squared error: (actual - predicted)²
Vector256 errorVec = Avx.Subtract(actVec, predVec);
Vector256 sqErrorVec = Avx.Multiply(errorVec, errorVec);
sqErrorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
}
// Update lastValid from end of SIMD-processed section
if (i > 0)
{
lastValidActual = actual[i - 1];
lastValidPredicted = predicted[i - 1];
}
// Handle scalar remainder
for (; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
if (!double.IsFinite(act) || !double.IsFinite(pred))
{
// Return current position - caller will handle with scalar fallback
return i;
}
lastValidActual = act;
lastValidPredicted = pred;
double diff = act - pred;
output[i] = diff * diff;
}
return len;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ComputeAbsoluteErrorsScalar(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output,
double lastValidActual,
double lastValidPredicted)
{
int len = actual.Length;
double currentValidActual = lastValidActual;
double currentValidPredicted = lastValidPredicted;
for (int i = 0; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
output[i] = Math.Abs(act - pred);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ComputeSquaredErrorsScalar(
ReadOnlySpan actual,
ReadOnlySpan predicted,
Span output,
double lastValidActual,
double lastValidPredicted)
{
int len = actual.Length;
double currentValidActual = lastValidActual;
double currentValidPredicted = lastValidPredicted;
for (int i = 0; i < len; i++)
{
double act = actual[i];
double pred = predicted[i];
if (double.IsFinite(act))
{
currentValidActual = act;
}
else
{
act = currentValidActual;
}
if (double.IsFinite(pred))
{
currentValidPredicted = pred;
}
else
{
pred = currentValidPredicted;
}
double diff = act - pred;
output[i] = diff * diff;
}
}
///
/// Numerically stable log(cosh(x)) computation.
/// For large |x|, uses approximation: |x| - log(2)
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double LogCosh(double x)
{
// For |x| > 20, cosh(x) ≈ exp(|x|)/2, so log(cosh(x)) ≈ |x| - log(2)
double absX = Math.Abs(x);
if (absX > 20.0)
{
return absX - 0.6931471805599453; // log(2)
}
return Math.Log(Math.Cosh(x));
}
#endregion
}