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 }