feat: implement last-value substitution for NaN/Infinity in Ema and EmaVector, enhance documentation and tests

This commit is contained in:
Miha Kralj
2025-11-29 13:19:04 -08:00
parent acac3e610c
commit 6cdebb984d
11 changed files with 506 additions and 29 deletions
+6
View File
@@ -399,3 +399,9 @@ FodyWeavers.xsd
# Cline Memory Bank - exclude from git
memory-bank/
# macOS resource forks and metadata
._*
.DS_Store
.AppleDouble
.LSOverride
Binary file not shown.
+1 -1
View File
@@ -44,7 +44,7 @@
</PropertyGroup>
<PropertyGroup>
<NoWarn>S1944,S2053,S2222,S2259,S2583,S2589,S3329,S3655,S3900,S3949,S3966,S4158,S4347,S5773,S6781</NoWarn>
<NoWarn>S1944,S2053,S2222,S2259,S2583,S2589,S3329,S3655,S3776,S3900,S3949,S3966,S4158,S4347,S5773,S6781</NoWarn>
</PropertyGroup>
<ItemGroup>
Binary file not shown.
+76
View File
@@ -216,3 +216,79 @@ for (int i = 0; i < periods.Length; i++)
}
}
Console.WriteLine($"\nAll Vectorized Stream/Batch values match: {allMatch}");
#!markdown
## 5. Handling Invalid Values (NaN/Infinity)
Both `Ema` and `EmaVector` use **last-value substitution** for invalid inputs. When a non-finite value (NaN, PositiveInfinity, NegativeInfinity) is encountered, it is replaced with the last valid value. This provides output continuity instead of propagating invalid values through the calculation.
#!csharp
Console.WriteLine("\n--- Handling Invalid Values ---");
// Single EMA
var emaNaN = new Ema(10);
// Feed valid values first
emaNaN.Update(new TValue(DateTime.Now, 100.0));
emaNaN.Update(new TValue(DateTime.Now.AddMinutes(1), 110.0));
Console.WriteLine($"After valid values: {emaNaN.Value.Value:F2}");
// Feed NaN - should use last valid value (110)
var resultAfterNaN = emaNaN.Update(new TValue(DateTime.Now.AddMinutes(2), double.NaN));
Console.WriteLine($"After NaN input: {resultAfterNaN.Value:F2} (IsFinite: {double.IsFinite(resultAfterNaN.Value)})");
// Feed Infinity - should use last valid value (110)
var resultAfterInf = emaNaN.Update(new TValue(DateTime.Now.AddMinutes(3), double.PositiveInfinity));
Console.WriteLine($"After Infinity input: {resultAfterInf.Value:F2} (IsFinite: {double.IsFinite(resultAfterInf.Value)})");
// Continue with valid value
var resultAfterValid = emaNaN.Update(new TValue(DateTime.Now.AddMinutes(4), 120.0));
Console.WriteLine($"After valid value (120): {resultAfterValid.Value:F2}");
#!csharp
Console.WriteLine("\n--- Batch Processing with Invalid Values ---");
// Create series with NaN values interspersed
var seriesWithNaN = new TSeries();
seriesWithNaN.Add(DateTime.Now.Ticks, 100.0);
seriesWithNaN.Add(DateTime.Now.Ticks + 1, 110.0);
seriesWithNaN.Add(DateTime.Now.Ticks + 2, double.NaN);
seriesWithNaN.Add(DateTime.Now.Ticks + 3, 120.0);
seriesWithNaN.Add(DateTime.Now.Ticks + 4, double.PositiveInfinity);
seriesWithNaN.Add(DateTime.Now.Ticks + 5, 130.0);
var emaBatchNaN = new Ema(3);
var resultsWithNaN = emaBatchNaN.Update(seriesWithNaN);
Console.WriteLine("Input → Output:");
for (int i = 0; i < seriesWithNaN.Count; i++)
{
var input = seriesWithNaN[i].Value;
var output = resultsWithNaN[i].Value;
var inputStr = double.IsFinite(input) ? input.ToString("F2") : input.ToString();
Console.WriteLine($" {inputStr,-10} → {output:F2} (IsFinite: {double.IsFinite(output)})");
}
#!csharp
Console.WriteLine("\n--- Vectorized EMA with Invalid Values ---");
int[] periodsNaN = { 5, 10 };
var emaVectorNaN = new EmaVector(periodsNaN);
// Feed values including invalid ones
var inputsNaN = new double[] { 100, 110, double.NaN, 120, double.PositiveInfinity, 130 };
var time = DateTime.Now;
foreach (var val in inputsNaN)
{
var results = emaVectorNaN.Update(new TValue(time, val));
var inputStr = double.IsFinite(val) ? val.ToString("F2") : val.ToString();
Console.WriteLine($"Input: {inputStr,-10} → EMA(5): {results[0].Value:F2}, EMA(10): {results[1].Value:F2}");
time = time.AddMinutes(1);
}
Console.WriteLine("\nAll outputs are finite - invalid inputs were substituted with last valid values.");
+102
View File
@@ -225,4 +225,106 @@ public class EmaTests
Assert.Equal(100.0, result, 1e-10);
}
[Fact]
public void Ema_NaN_Input_UsesLastValidValue()
{
var ema = new Ema(10);
// Feed some valid values
ema.Update(new TValue(DateTime.Now, 100));
ema.Update(new TValue(DateTime.Now, 110));
double valueBeforeNaN = ema.Value;
// Feed NaN - should use last valid value (110)
var resultAfterNaN = ema.Update(new TValue(DateTime.Now, double.NaN));
// Result should be finite (not NaN)
Assert.True(double.IsFinite(resultAfterNaN.Value));
// EMA should continue to evolve (may differ slightly due to substitution)
Assert.NotEqual(0, resultAfterNaN.Value);
}
[Fact]
public void Ema_Infinity_Input_UsesLastValidValue()
{
var ema = new Ema(10);
// Feed some valid values
ema.Update(new TValue(DateTime.Now, 100));
ema.Update(new TValue(DateTime.Now, 110));
// Feed positive infinity - should use last valid value
var resultAfterPosInf = ema.Update(new TValue(DateTime.Now, double.PositiveInfinity));
Assert.True(double.IsFinite(resultAfterPosInf.Value));
// Feed negative infinity - should use last valid value
var resultAfterNegInf = ema.Update(new TValue(DateTime.Now, double.NegativeInfinity));
Assert.True(double.IsFinite(resultAfterNegInf.Value));
}
[Fact]
public void Ema_MultipleNaN_ContinuesWithLastValid()
{
var ema = new Ema(10);
// Feed valid values
ema.Update(new TValue(DateTime.Now, 100));
ema.Update(new TValue(DateTime.Now, 110));
ema.Update(new TValue(DateTime.Now, 120));
// Feed multiple NaN values
var r1 = ema.Update(new TValue(DateTime.Now, double.NaN));
var r2 = ema.Update(new TValue(DateTime.Now, double.NaN));
var r3 = ema.Update(new TValue(DateTime.Now, double.NaN));
// All results should be finite
Assert.True(double.IsFinite(r1.Value));
Assert.True(double.IsFinite(r2.Value));
Assert.True(double.IsFinite(r3.Value));
// EMA should converge toward last valid value (120) with repeated substitution
// Values should be getting closer to 120
Assert.True(r3.Value > r1.Value || Math.Abs(r3.Value - 120) < Math.Abs(r1.Value - 120));
}
[Fact]
public void Ema_BatchCalc_HandlesNaN()
{
var ema = new Ema(10);
// Create series with NaN values interspersed
var series = new TSeries();
series.Add(DateTime.Now.Ticks, 100);
series.Add(DateTime.Now.Ticks + 1, 110);
series.Add(DateTime.Now.Ticks + 2, double.NaN);
series.Add(DateTime.Now.Ticks + 3, 120);
series.Add(DateTime.Now.Ticks + 4, double.PositiveInfinity);
series.Add(DateTime.Now.Ticks + 5, 130);
var results = ema.Update(series);
// All results should be finite
foreach (var result in results)
{
Assert.True(double.IsFinite(result.Value), $"Expected finite value but got {result.Value}");
}
}
[Fact]
public void Ema_Reset_ClearsLastValidValue()
{
var ema = new Ema(10);
// Feed values including NaN
ema.Update(new TValue(DateTime.Now, 100));
ema.Update(new TValue(DateTime.Now, double.NaN));
// Reset
ema.Reset();
// After reset, first valid value should establish new baseline
var result = ema.Update(new TValue(DateTime.Now, 50));
Assert.Equal(50.0, result.Value, 1e-10);
}
}
+37 -11
View File
@@ -6,9 +6,9 @@ namespace QuanTAlib;
public struct EmaState
{
public double Ema;
public double E;
public bool IsHot;
public double Ema { get; set; }
public double E { get; set; }
public bool IsHot { get; set; }
public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false };
}
@@ -26,6 +26,7 @@ public class Ema
private readonly double _alpha;
private EmaState _state = EmaState.New();
private EmaState _p_state = EmaState.New();
private double _lastValidValue;
/// <summary>
/// Creates EMA with specified period.
@@ -36,7 +37,7 @@ public class Ema
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
_alpha = 2.0 / (period + 1);
}
@@ -48,7 +49,7 @@ public class Ema
{
if (alpha <= 0 || alpha > 1)
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
_alpha = alpha;
}
@@ -62,22 +63,42 @@ public class Ema
/// </summary>
public bool IsHot => _state.IsHot;
/// <summary>
/// Gets a valid input value, using last-value substitution for non-finite inputs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
_lastValidValue = input;
return input;
}
return _lastValidValue;
}
/// <summary>
/// Core EMA calculation kernel.
/// Assumes input has already been validated via GetValidValue().
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Compute(double input, double alpha, ref EmaState state)
{
state.Ema += alpha * (input - state.Ema);
double result;
if (!state.IsHot)
{
state.E *= (1.0 - alpha);
state.IsHot = state.E <= 1e-10;
return state.Ema / (1.0 - state.E);
result = state.Ema / (1.0 - state.E);
}
return state.Ema;
else
{
result = state.Ema;
}
return result;
}
/// <summary>
@@ -98,7 +119,9 @@ public class Ema
_state = _p_state;
}
double val = Compute(input.Value, _alpha, ref _state);
// Last-value substitution: replace non-finite inputs with last valid value
double val = GetValidValue(input.Value);
val = Compute(val, _alpha, ref _state);
Value = new TValue(input.Time, val);
return Value;
}
@@ -123,10 +146,12 @@ public class Ema
// Local state for batch processing
EmaState state = _state;
for (int i = 0; i < len; i++)
{
double val = Compute(sourceValues[i], _alpha, ref state);
// Last-value substitution: replace non-finite inputs with last valid value
double val = GetValidValue(sourceValues[i]);
val = Compute(val, _alpha, ref state);
tSpan[i] = sourceTimes[i];
vSpan[i] = val;
}
@@ -158,6 +183,7 @@ public class Ema
{
_state = EmaState.New();
_p_state = _state;
_lastValidValue = 0;
Value = default;
}
}
+29
View File
@@ -103,6 +103,35 @@ TSeries source = ...;
TSeries[] seriesResults = emaVector.Calculate(source);
```
### Handling Invalid Values (NaN/Infinity)
Both `Ema` and `EmaVector` use **last-value substitution** for handling invalid inputs:
```csharp
var ema = new Ema(10);
// Valid values establish baseline
ema.Update(new TValue(time, 100));
ema.Update(new TValue(time, 110));
// NaN or Infinity inputs are replaced with last valid value (110)
var result = ema.Update(new TValue(time, double.NaN));
Console.WriteLine(double.IsFinite(result.Value)); // true
// Works identically for batch operations
var series = new TSeries();
series.Add(time, 100);
series.Add(time + 1, double.NaN); // Will use 100
series.Add(time + 2, 120);
var results = ema.Update(series); // All values are finite
```
**Behavior:**
- When `NaN`, `PositiveInfinity`, or `NegativeInfinity` is encountered, the last valid value is substituted
- This provides output continuity instead of propagating invalid values
- Both scalar (`Ema`) and SIMD (`EmaVector`) implementations use identical logic
- `Reset()` clears the last valid value, so the next valid input establishes a new baseline
### Performance Characteristics
* **O(1) Complexity:** The calculation time is constant regardless of the period length.
+142
View File
@@ -135,4 +135,146 @@ public class EmaVectorTests
Assert.Equal(200.0, res[0].Value, 1e-9);
}
[Fact]
public void Update_NaN_Input_UsesLastValidValue()
{
int[] periods = { 10, 20 };
var emaVector = new EmaVector(periods);
// Feed some valid values
emaVector.Update(new TValue(DateTime.Now, 100.0));
emaVector.Update(new TValue(DateTime.Now, 110.0));
// Feed NaN - should use last valid value (110)
var resultAfterNaN = emaVector.Update(new TValue(DateTime.Now, double.NaN));
// All results should be finite (not NaN)
foreach (var result in resultAfterNaN)
{
Assert.True(double.IsFinite(result.Value), $"Expected finite value but got {result.Value}");
}
}
[Fact]
public void Update_Infinity_Input_UsesLastValidValue()
{
int[] periods = { 10, 20 };
var emaVector = new EmaVector(periods);
// Feed some valid values
emaVector.Update(new TValue(DateTime.Now, 100.0));
emaVector.Update(new TValue(DateTime.Now, 110.0));
// Feed positive infinity - should use last valid value
var resultAfterPosInf = emaVector.Update(new TValue(DateTime.Now, double.PositiveInfinity));
foreach (var result in resultAfterPosInf)
{
Assert.True(double.IsFinite(result.Value));
}
// Feed negative infinity - should use last valid value
var resultAfterNegInf = emaVector.Update(new TValue(DateTime.Now, double.NegativeInfinity));
foreach (var result in resultAfterNegInf)
{
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void Update_MultipleNaN_ContinuesWithLastValid()
{
int[] periods = { 5, 10 };
var emaVector = new EmaVector(periods);
// Feed valid values
emaVector.Update(new TValue(DateTime.Now, 100.0));
emaVector.Update(new TValue(DateTime.Now, 110.0));
emaVector.Update(new TValue(DateTime.Now, 120.0));
// Feed multiple NaN values
var r1 = emaVector.Update(new TValue(DateTime.Now, double.NaN));
var r2 = emaVector.Update(new TValue(DateTime.Now, double.NaN));
var r3 = emaVector.Update(new TValue(DateTime.Now, double.NaN));
// All results should be finite
foreach (var result in r1) Assert.True(double.IsFinite(result.Value));
foreach (var result in r2) Assert.True(double.IsFinite(result.Value));
foreach (var result in r3) Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Calculate_Series_HandlesNaN()
{
int[] periods = { 5, 10 };
var emaVector = new EmaVector(periods);
// Create series with NaN values interspersed
var t = new System.Collections.Generic.List<long>();
var v = new System.Collections.Generic.List<double>();
var now = DateTime.Now;
t.Add(now.Ticks); v.Add(100.0);
t.Add(now.AddMinutes(1).Ticks); v.Add(110.0);
t.Add(now.AddMinutes(2).Ticks); v.Add(double.NaN);
t.Add(now.AddMinutes(3).Ticks); v.Add(120.0);
t.Add(now.AddMinutes(4).Ticks); v.Add(double.PositiveInfinity);
t.Add(now.AddMinutes(5).Ticks); v.Add(130.0);
var series = new TSeries(t, v);
var results = emaVector.Calculate(series);
// All results should be finite for all periods
foreach (var periodResults in results)
{
foreach (var val in periodResults.Values)
{
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
}
}
}
[Fact]
public void Reset_ClearsLastValidValue()
{
int[] periods = { 10 };
var emaVector = new EmaVector(periods);
// Feed values including NaN
emaVector.Update(new TValue(DateTime.Now, 100.0));
emaVector.Update(new TValue(DateTime.Now, double.NaN));
// Reset
emaVector.Reset();
// After reset, first valid value should establish new baseline
var result = emaVector.Update(new TValue(DateTime.Now, 50.0));
Assert.Equal(50.0, result[0].Value, 1e-9);
}
[Fact]
public void NaN_Handling_MatchesSingleEma()
{
int[] periods = { 5, 10, 20 };
var emaVector = new EmaVector(periods);
var emaSingles = periods.Select(p => new Ema(p)).ToArray();
// Data with NaN values
var values = new double[] { 10, 20, double.NaN, 40, double.PositiveInfinity, 60, 70 };
var time = DateTime.Now;
foreach (var val in values)
{
var tVal = new TValue(time, val);
var multiRes = emaVector.Update(tVal);
for (int i = 0; i < periods.Length; i++)
{
var singleRes = emaSingles[i].Update(tVal);
Assert.Equal(singleRes.Value, multiRes[i].Value, 1e-9);
}
time = time.AddMinutes(1);
}
}
}
+41 -17
View File
@@ -9,6 +9,7 @@ namespace QuanTAlib;
/// <summary>
/// Multi-Alpha Exponential Moving Average (EMA) - SIMD optimized.
/// Calculates multiple EMAs with different periods/alphas for the same input series in parallel.
/// Uses last-value substitution for invalid inputs (NaN/Infinity).
/// </summary>
public class EmaVector
{
@@ -16,7 +17,8 @@ public class EmaVector
private readonly double[] _emas;
private readonly double[] _Es;
private readonly int _count;
private double _lastValidValue;
/// <summary>
/// Current EMA values for all periods.
/// </summary>
@@ -33,7 +35,7 @@ public class EmaVector
_emas = new double[_count];
_Es = new double[_count];
Values = new TValue[_count];
for (int i = 0; i < _count; i++)
{
if (periods[i] <= 0) throw new ArgumentException("Period must be greater than 0", nameof(periods));
@@ -53,7 +55,7 @@ public class EmaVector
_emas = new double[_count];
_Es = new double[_count];
Values = new TValue[_count];
for (int i = 0; i < _count; i++)
{
if (alphas[i] <= 0 || alphas[i] > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alphas));
@@ -68,6 +70,20 @@ public class EmaVector
_Es[index] = 1.0;
}
/// <summary>
/// Gets a valid input value, using last-value substitution for non-finite inputs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
_lastValidValue = input;
return input;
}
return _lastValidValue;
}
/// <summary>
/// Resets all EMA states.
/// </summary>
@@ -77,23 +93,27 @@ public class EmaVector
{
ResetAt(i);
}
_lastValidValue = 0;
Array.Clear(Values);
}
/// <summary>
/// Updates EMAs with the given value.
/// Uses last-value substitution: invalid inputs (NaN/Infinity) are replaced with
/// the last known good value, providing continuity in the output series.
/// </summary>
/// <param name="input">Input value</param>
/// <returns>Array of compensated EMA values</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue[] Update(TValue input)
{
double val = input.Value;
// Last-value substitution: replace non-finite inputs with last valid value
double val = GetValidValue(input.Value);
// SIMD Loop
int vecCount = Vector<double>.Count;
int i = 0;
if (Vector.IsHardwareAccelerated && _count >= vecCount)
{
var vecInput = new Vector<double>(val);
@@ -110,7 +130,7 @@ public class EmaVector
// Update EMA
// ema += alpha * (input - ema)
vecEma += vecAlpha * (vecInput - vecEma);
// Update E (warmup factor)
// E *= (1 - alpha)
vecE *= (vecOne - vecAlpha);
@@ -123,7 +143,7 @@ public class EmaVector
// Vector.LessThanOrEqual returns Vector<long> with all-1s for true, all-0s for false
// We reinterpret as Vector<double> for use with ConditionalSelect
var isHotMask = Vector.LessThanOrEqual(vecE, vecEpsilon);
// Select result: if hot (E <= epsilon), use raw EMA; otherwise use compensated
// ConditionalSelect: mask=true -> first arg, mask=false -> second arg
var vecResult = Vector.ConditionalSelect(
@@ -149,7 +169,7 @@ public class EmaVector
{
double alpha = _alphas[i];
_emas[i] += alpha * (val - _emas[i]);
double result = _emas[i];
if (_Es[i] > 1e-10)
{
@@ -159,7 +179,7 @@ public class EmaVector
result = _emas[i] / (1.0 - _Es[i]);
}
}
Values[i] = new TValue(input.Time, result);
}
@@ -175,7 +195,7 @@ public class EmaVector
{
int len = source.Count;
var resultSeries = new TSeries[_count];
// Pre-allocate lists
var tLists = new List<long>[_count];
var vLists = new List<double>[_count];
@@ -190,7 +210,7 @@ public class EmaVector
var sourceValues = source.Values;
var sourceTimes = source.Times;
int vecCount = Vector<double>.Count;
var vecOne = Vector<double>.One;
var vecEpsilon = new Vector<double>(1e-10);
@@ -199,6 +219,10 @@ public class EmaVector
{
double val = sourceValues[t];
long time = sourceTimes[t];
// Last-value substitution: replace non-finite inputs with last valid value
val = GetValidValue(val);
var vecInput = new Vector<double>(val);
int i = 0;
@@ -212,12 +236,12 @@ public class EmaVector
vecEma += vecAlpha * (vecInput - vecEma);
vecE *= (vecOne - vecAlpha);
var vecCompensated = vecEma / (vecOne - vecE);
// Check warmup condition: E <= 1e-10 means "hot" (use raw EMA)
var isHotMask = Vector.LessThanOrEqual(vecE, vecEpsilon);
// Select result: if hot, use raw EMA; otherwise use compensated
var vecResult = Vector.ConditionalSelect(
Vector.AsVectorDouble(isHotMask),
@@ -241,7 +265,7 @@ public class EmaVector
{
double alpha = _alphas[i];
_emas[i] += alpha * (val - _emas[i]);
double result = _emas[i];
if (_Es[i] > 1e-10)
{
@@ -251,7 +275,7 @@ public class EmaVector
result = _emas[i] / (1.0 - _Es[i]);
}
}
CollectionsMarshal.AsSpan(tLists[i])[t] = time;
CollectionsMarshal.AsSpan(vLists[i])[t] = result;
}
+72
View File
@@ -9,15 +9,64 @@ namespace QuanTAlib;
/// </summary>
public static class SimdExtensions
{
/// <summary>
/// Checks if span contains any non-finite values (NaN or Infinity).
/// Returns true if any non-finite value is found.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ContainsNonFinite(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return false;
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
int i = 0;
for (; i <= span.Length - vectorSize; i += vectorSize)
{
var vector = new Vector<double>(span.Slice(i, vectorSize));
// Check for NaN: NaN != NaN is true
// Check for Infinity: IsInfinity
for (int j = 0; j < vectorSize; j++)
{
if (!double.IsFinite(vector[j]))
return true;
}
}
// Check remaining elements
for (; i < span.Length; i++)
{
if (!double.IsFinite(span[i]))
return true;
}
return false;
}
// Scalar fallback
for (int i = 0; i < span.Length; i++)
{
if (!double.IsFinite(span[i]))
return true;
}
return false;
}
/// <summary>
/// Calculates sum using SIMD vectorization when available.
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
/// Returns NaN if any input value is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double SumSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return 0.0;
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return double.NaN;
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
Vector<double> sum = Vector<double>.Zero;
@@ -53,6 +102,7 @@ public static class SimdExtensions
/// <summary>
/// Calculates minimum value using SIMD vectorization when available.
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
/// Returns NaN if any input value is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double MinSIMD(this ReadOnlySpan<double> span)
@@ -60,6 +110,9 @@ public static class SimdExtensions
if (span.IsEmpty) return double.NaN;
if (span.Length == 1) return span[0];
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return double.NaN;
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
@@ -104,6 +157,7 @@ public static class SimdExtensions
/// <summary>
/// Calculates maximum value using SIMD vectorization when available.
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
/// Returns NaN if any input value is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double MaxSIMD(this ReadOnlySpan<double> span)
@@ -111,6 +165,9 @@ public static class SimdExtensions
if (span.IsEmpty) return double.NaN;
if (span.Length == 1) return span[0];
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return double.NaN;
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;
@@ -155,25 +212,34 @@ public static class SimdExtensions
/// <summary>
/// Calculates average using SIMD vectorization when available.
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
/// Returns NaN if any input value is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double AverageSIMD(this ReadOnlySpan<double> span)
{
if (span.IsEmpty) return double.NaN;
// SumSIMD already guards against non-finite, which will propagate NaN
return span.SumSIMD() / span.Length;
}
/// <summary>
/// Calculates variance using SIMD vectorization (Welford's online algorithm adapted).
/// More numerically stable than naive two-pass algorithm.
/// Returns NaN if any input value is non-finite or if mean is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
{
if (span.Length < 2) return double.NaN;
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return double.NaN;
double m = mean ?? span.AverageSIMD();
// Guard against non-finite mean (could be passed in or computed from non-finite values)
if (!double.IsFinite(m)) return double.NaN;
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
var meanVec = new Vector<double>(m);
@@ -216,16 +282,19 @@ public static class SimdExtensions
/// <summary>
/// Calculates standard deviation using SIMD vectorization.
/// Returns NaN if any input value is non-finite or if mean is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double StdDevSIMD(this ReadOnlySpan<double> span, double? mean = null)
{
// VarianceSIMD already guards against non-finite, which will propagate NaN through Sqrt
return Math.Sqrt(span.VarianceSIMD(mean));
}
/// <summary>
/// Finds both min and max in a single pass using SIMD vectorization.
/// More efficient than calling MinSIMD and MaxSIMD separately.
/// Returns (NaN, NaN) if any input value is non-finite.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan<double> span)
@@ -233,6 +302,9 @@ public static class SimdExtensions
if (span.IsEmpty) return (double.NaN, double.NaN);
if (span.Length == 1) return (span[0], span[0]);
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return (double.NaN, double.NaN);
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
{
int vectorSize = Vector<double>.Count;