Merge dev into main: v0.8.7 Kahan compensated summation

This commit is contained in:
Miha Kralj
2026-03-13 22:01:52 -07:00
79 changed files with 2923 additions and 2495 deletions
+28 -28
View File
@@ -10,6 +10,7 @@ namespace QuanTAlib;
/// Computes the linear regression slope over a rolling window, then accumulates
/// it via discrete integration (running sum) to reconstruct a smoothed price-level
/// signal. The integration step introduces a natural momentum quality.
/// Kahan compensated summation prevents floating-point drift without periodic resync.
///
/// Algorithm: slope via O(1) incremental linreg, then ILRS += slope.
/// Initialized to first price value.
@@ -32,16 +33,14 @@ public sealed class Ilrs : AbstractBase
[StructLayout(LayoutKind.Auto)]
private record struct State(
double SumY, double SumXY,
double SumYComp, double SumXYComp,
double Integral, double LastVal,
double LastValidValue, bool Initialized);
private State _s;
private State _ps;
private int _tickCount;
private bool _isNew;
private const int ResyncInterval = 1000;
public override bool IsHot => _buffer.IsFull;
public bool IsNew => _isNew;
@@ -175,18 +174,39 @@ public sealed class Ilrs : AbstractBase
double oldest = _buffer.Oldest;
double prevSumY = _s.SumY;
// O(1) update for SumXY (reversed-x convention)
_s.SumXY = Math.FusedMultiplyAdd(-_period, oldest, _s.SumXY + prevSumY);
_s.SumY = _s.SumY - oldest + val;
// Kahan compensated update for SumXY: sumXY += (prevSumY - period * oldest)
double deltaXY = Math.FusedMultiplyAdd(-_period, oldest, prevSumY);
double yXY = deltaXY - _s.SumXYComp;
double tXY = _s.SumXY + yXY;
_s.SumXYComp = (tXY - _s.SumXY) - yXY;
_s.SumXY = tXY;
// Kahan compensated update for SumY: sumY += (val - oldest)
double deltaY = val - oldest;
double yY = deltaY - _s.SumYComp;
double tY = _s.SumY + yY;
_s.SumYComp = (tY - _s.SumY) - yY;
_s.SumY = tY;
_buffer.Add(val);
}
else
{
if (_buffer.Count > 0)
{
_s.SumXY += _s.SumY;
// Kahan compensated addition for SumXY: sumXY += sumY
double yXY = _s.SumY - _s.SumXYComp;
double tXY = _s.SumXY + yXY;
_s.SumXYComp = (tXY - _s.SumXY) - yXY;
_s.SumXY = tXY;
}
_s.SumY += val;
// Kahan compensated addition for SumY
double yY = val - _s.SumYComp;
double tY = _s.SumY + yY;
_s.SumYComp = (tY - _s.SumY) - yY;
_s.SumY = tY;
_buffer.Add(val);
}
@@ -201,13 +221,6 @@ public sealed class Ilrs : AbstractBase
// Integrate: ILRS += slope
_s.Integral += ComputeSlope(_s);
}
_tickCount++;
if (_buffer.IsFull && _tickCount >= ResyncInterval)
{
_tickCount = 0;
Resync();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -239,18 +252,6 @@ public sealed class Ilrs : AbstractBase
return -Math.FusedMultiplyAdd(n, state.SumXY, -sx * state.SumY) / denom;
}
private void Resync()
{
_s.SumY = _buffer.Sum;
_s.SumXY = 0;
var span = _buffer.GetSpan();
for (int i = 0; i < span.Length; i++)
{
int x = span.Length - 1 - i;
_s.SumXY = Math.FusedMultiplyAdd(x, span[i], _s.SumXY);
}
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (var value in source)
@@ -401,7 +402,6 @@ public sealed class Ilrs : AbstractBase
_s.LastValidValue = double.NaN;
_ps = default;
Last = default;
_tickCount = 0;
}
protected override void Dispose(bool disposing)
+26 -31
View File
@@ -8,6 +8,7 @@ namespace QuanTAlib;
/// </summary>
/// <remarks>
/// Linear regression endpoint with O(1) updates using running sums.
/// Kahan compensated summation prevents floating-point drift without periodic resync.
/// Projects trend line value at current bar (or offset position).
///
/// Calculation: <c>LSMA = b - m × offset</c> where <c>m = (n×Σxy - Σx×Σy) / denom</c>.
@@ -27,15 +28,12 @@ public sealed class Lsma : AbstractBase
private int _disposed;
[StructLayout(LayoutKind.Auto)]
private record struct State(double SumY, double SumXY, double LastVal, double LastValidValue);
private record struct State(double SumY, double SumXY, double SumYComp, double SumXYComp, double LastVal, double LastValidValue);
private State _state;
private State _p_state;
private int _tickCount;
private bool _isNew;
private const int ResyncInterval = 1000;
public override bool IsHot => _buffer.IsFull;
public bool IsNew => _isNew;
@@ -97,12 +95,19 @@ public sealed class Lsma : AbstractBase
double oldest = _buffer.Oldest;
double prev_sum_y = _state.SumY;
// O(1) update for sum_xy
// sum_xy_new = sum_xy_old + sum_y_prev - n * oldest
_state.SumXY = Math.FusedMultiplyAdd(-_period, oldest, _state.SumXY + prev_sum_y);
// Kahan compensated update for SumXY: sumXY += (prev_sum_y - period * oldest)
double deltaXY = Math.FusedMultiplyAdd(-_period, oldest, prev_sum_y);
double yXY = deltaXY - _state.SumXYComp;
double tXY = _state.SumXY + yXY;
_state.SumXYComp = (tXY - _state.SumXY) - yXY;
_state.SumXY = tXY;
// O(1) update for sum_y
_state.SumY = _state.SumY - oldest + val;
// Kahan compensated update for SumY: sumY += (val - oldest)
double deltaY = val - oldest;
double yY = deltaY - _state.SumYComp;
double tY = _state.SumY + yY;
_state.SumYComp = (tY - _state.SumY) - yY;
_state.SumY = tY;
_buffer.Add(val);
}
@@ -110,30 +115,21 @@ public sealed class Lsma : AbstractBase
{
if (_buffer.Count > 0)
{
_state.SumXY += _state.SumY;
// Kahan compensated addition for SumXY: sumXY += sumY (shift existing values)
double yXY = _state.SumY - _state.SumXYComp;
double tXY = _state.SumXY + yXY;
_state.SumXYComp = (tXY - _state.SumXY) - yXY;
_state.SumXY = tXY;
}
_state.SumY += val;
// Kahan compensated addition for SumY
double yY = val - _state.SumYComp;
double tY = _state.SumY + yY;
_state.SumYComp = (tY - _state.SumY) - yY;
_state.SumY = tY;
_buffer.Add(val);
}
_tickCount++;
if (_buffer.IsFull && _tickCount >= ResyncInterval)
{
_tickCount = 0;
Resync();
}
}
private void Resync()
{
_state.SumY = _buffer.Sum;
_state.SumXY = 0;
var span = _buffer.GetSpan();
for (int i = 0; i < span.Length; i++)
{
int x = span.Length - 1 - i;
_state.SumXY = Math.FusedMultiplyAdd(x, span[i], _state.SumXY);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -412,7 +408,6 @@ public sealed class Lsma : AbstractBase
_state.LastValidValue = double.NaN;
_p_state = default;
Last = default;
_tickCount = 0;
}
/// <summary>
+90 -63
View File
@@ -8,6 +8,7 @@ namespace QuanTAlib;
/// </summary>
/// <remarks>
/// Quadratic weighting (w[i]=i²) emphasizing recent values via O(1) triple running sums.
/// Kahan compensated summation prevents floating-point drift without periodic resync.
///
/// Calculation: <c>PWMA = Σ(i²×P_i) / Σ(i²)</c> with efficient incremental updates.
/// </remarks>
@@ -21,12 +22,10 @@ public sealed class Pwma : AbstractBase
private readonly TValuePublishedHandler _handler;
[StructLayout(LayoutKind.Auto)]
private record struct State(double Sum, double WSum, double PSum, double LastInput, double LastValidValue, int TickCount);
private record struct State(double Sum, double WSum, double PSum, double SumComp, double WSumComp, double PSumComp, double LastInput, double LastValidValue);
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
public override bool IsHot => _buffer.IsFull;
public Pwma(int period)
@@ -77,39 +76,53 @@ public sealed class Pwma : AbstractBase
double oldWSum = _state.WSum;
double oldest = _buffer.Oldest;
_state.Sum = _state.Sum - oldest + val;
_state.WSum = Math.FusedMultiplyAdd(_period, val, _state.WSum - oldSum);
_state.PSum = Math.FusedMultiplyAdd((double)_period * _period, val, _state.PSum - 2 * oldWSum + oldSum);
// Kahan compensated update for Sum: sum += (val - oldest)
double deltaS = val - oldest;
double yS = deltaS - _state.SumComp;
double tS = _state.Sum + yS;
_state.SumComp = (tS - _state.Sum) - yS;
_state.Sum = tS;
// Kahan compensated update for WSum: wsum += (period * val - oldSum)
double deltaW = Math.FusedMultiplyAdd(_period, val, -oldSum);
double yW = deltaW - _state.WSumComp;
double tW = _state.WSum + yW;
_state.WSumComp = (tW - _state.WSum) - yW;
_state.WSum = tW;
// Kahan compensated update for PSum: psum += (period² * val - 2 * oldWSum + oldSum)
double deltaP = Math.FusedMultiplyAdd((double)_period * _period, val, -2 * oldWSum + oldSum);
double yP = deltaP - _state.PSumComp;
double tP = _state.PSum + yP;
_state.PSumComp = (tP - _state.PSum) - yP;
_state.PSum = tP;
}
else
{
int count = _buffer.Count + 1;
_state.Sum += val;
_state.WSum = Math.FusedMultiplyAdd(count, val, _state.WSum);
_state.PSum = Math.FusedMultiplyAdd((double)count * count, val, _state.PSum);
// Kahan compensated addition for Sum
double yS = val - _state.SumComp;
double tS = _state.Sum + yS;
_state.SumComp = (tS - _state.Sum) - yS;
_state.Sum = tS;
// Kahan compensated addition for WSum
double wVal = count * val;
double yW = wVal - _state.WSumComp;
double tW = _state.WSum + yW;
_state.WSumComp = (tW - _state.WSum) - yW;
_state.WSum = tW;
// Kahan compensated addition for PSum
double pVal = (double)count * count * val;
double yP = pVal - _state.PSumComp;
double tP = _state.PSum + yP;
_state.PSumComp = (tP - _state.PSum) - yP;
_state.PSum = tP;
}
_buffer.Add(val);
_state.TickCount++;
if (_buffer.IsFull && _state.TickCount >= ResyncInterval)
{
_state.TickCount = 0;
double recalcSum = 0;
double recalcWsum = 0;
double recalcPsum = 0;
int i = 1;
foreach (double item in _buffer)
{
recalcSum += item;
recalcWsum = Math.FusedMultiplyAdd(i, item, recalcWsum);
recalcPsum = Math.FusedMultiplyAdd((double)i * i, item, recalcPsum);
i++;
}
_state.Sum = recalcSum;
_state.WSum = recalcWsum;
_state.PSum = recalcPsum;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -203,7 +216,9 @@ public sealed class Pwma : AbstractBase
_state.Sum = 0;
_state.WSum = 0;
_state.PSum = 0;
_state.TickCount = 0;
_state.SumComp = 0;
_state.WSumComp = 0;
_state.PSumComp = 0;
for (int i = startIndex; i < len; i++)
{
@@ -270,12 +285,16 @@ public sealed class Pwma : AbstractBase
double sum = 0;
double wsum = 0;
double psum = 0;
double sumComp = 0;
double wsumComp = 0;
double psumComp = 0;
double lastValid = 0;
Span<double> buffer = period <= 512 ? stackalloc double[period] : new double[period];
int bufferIdx = 0;
int i = 0;
// Warmup phase with Kahan compensated additions
int warmupEnd = Math.Min(period, len);
for (; i < warmupEnd; i++)
{
@@ -289,16 +308,33 @@ public sealed class Pwma : AbstractBase
val = lastValid;
}
sum += val;
wsum = Math.FusedMultiplyAdd(i + 1, val, wsum);
psum = Math.FusedMultiplyAdd((double)(i + 1) * (i + 1), val, psum);
// Kahan compensated addition for sum
double yS = val - sumComp;
double tS = sum + yS;
sumComp = (tS - sum) - yS;
sum = tS;
// Kahan compensated addition for wsum
double wVal = (i + 1) * val;
double yW = wVal - wsumComp;
double tW = wsum + yW;
wsumComp = (tW - wsum) - yW;
wsum = tW;
// Kahan compensated addition for psum
double pVal = (double)(i + 1) * (i + 1) * val;
double yP = pVal - psumComp;
double tP = psum + yP;
psumComp = (tP - psum) - yP;
psum = tP;
buffer[i] = val;
double currentDivisor = ((double)i + 1.0) * ((double)i + 2.0) * (2.0 * ((double)i + 1.0) + 1.0) / 6.0;
output[i] = psum / currentDivisor;
}
int tickCount = period;
// Steady-state: sliding window with Kahan compensated triple sums
for (; i < len; i++)
{
double val = source[i];
@@ -315,9 +351,26 @@ public sealed class Pwma : AbstractBase
double oldWSum = wsum;
double oldest = buffer[bufferIdx];
sum = sum - oldest + val;
wsum = Math.FusedMultiplyAdd(period, val, wsum - oldSum);
psum = Math.FusedMultiplyAdd((double)period * period, val, psum - 2 * oldWSum + oldSum);
// Kahan compensated update for Sum: sum += (val - oldest)
double deltaS = val - oldest;
double yS = deltaS - sumComp;
double tS = sum + yS;
sumComp = (tS - sum) - yS;
sum = tS;
// Kahan compensated update for WSum: wsum += (period * val - oldSum)
double deltaW = Math.FusedMultiplyAdd(period, val, -oldSum);
double yW = deltaW - wsumComp;
double tW = wsum + yW;
wsumComp = (tW - wsum) - yW;
wsum = tW;
// Kahan compensated update for PSum: psum += (period² * val - 2 * oldWSum + oldSum)
double deltaP = Math.FusedMultiplyAdd((double)period * period, val, -2 * oldWSum + oldSum);
double yP = deltaP - psumComp;
double tP = psum + yP;
psumComp = (tP - psum) - yP;
psum = tP;
buffer[bufferIdx] = val;
bufferIdx++;
@@ -326,32 +379,6 @@ public sealed class Pwma : AbstractBase
bufferIdx = 0;
}
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
double recalcSum = 0;
double recalcWsum = 0;
double recalcPsum = 0;
for (int k = 0; k < period; k++)
{
int idx = bufferIdx + k;
if (idx >= period)
{
idx -= period;
}
double v = buffer[idx];
recalcSum += v;
recalcWsum = Math.FusedMultiplyAdd(k + 1, v, recalcWsum);
recalcPsum = Math.FusedMultiplyAdd((double)(k + 1) * (k + 1), v, recalcPsum);
}
sum = recalcSum;
wsum = recalcWsum;
psum = recalcPsum;
}
output[i] = psum / divisor;
}
}
+61 -64
View File
@@ -9,6 +9,7 @@ namespace QuanTAlib;
/// <remarks>
/// Weights each bar's contribution by its price range (high - low), giving
/// greater influence to volatile bars and less to narrow-range bars.
/// Kahan compensated summation prevents floating-point drift without periodic resync.
/// <c>RWMA = Σ(close_i × range_i) / Σ(range_i)</c> where <c>range_i = max(high_i - low_i, 0)</c>.
///
/// Requires TBar (OHLC) inputs. When all bars have zero range the output
@@ -21,17 +22,11 @@ namespace QuanTAlib;
public sealed class Rwma : ITValuePublisher
{
[StructLayout(LayoutKind.Auto)]
private record struct State(double SumCR, double SumR, int Index, int Head, int Count, int SyncCounter)
private record struct State(double SumCR, double SumR, double SumCRComp, double SumRComp, int Index, int Head, int Count)
{
public static State New() => new() { SumCR = 0, SumR = 0, Index = 0, Head = 0, Count = 0, SyncCounter = 0 };
public static State New() => new() { SumCR = 0, SumR = 0, SumCRComp = 0, SumRComp = 0, Index = 0, Head = 0, Count = 0 };
}
/// <summary>
/// Resync interval to limit floating-point drift in running sums.
/// Full recalculation every N bars.
/// </summary>
private const int ResyncInterval = 1000;
private readonly int _period;
private readonly double[] _closeBuffer;
private readonly double[] _rangeBuffer;
@@ -119,27 +114,6 @@ public sealed class Rwma : ITValuePublisher
return lastValid;
}
/// <summary>
/// Recalculates running sums from buffer to eliminate accumulated floating-point drift.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ResyncRunningTotals(ref State s)
{
double sumCR = 0;
double sumR = 0;
for (int i = 0; i < _period; i++)
{
double c = _closeBuffer[i];
double r = _rangeBuffer[i];
sumCR = Math.FusedMultiplyAdd(c, r, sumCR);
sumR += r;
}
s.SumCR = sumCR;
s.SumR = sumR;
}
/// <summary>
/// Updates RWMA with a TBar input (uses close, high, low).
/// </summary>
@@ -222,13 +196,35 @@ public sealed class Rwma : ITValuePublisher
if (s.Count >= _period)
{
s.SumCR = Math.FusedMultiplyAdd(-oldClose, oldRange, s.SumCR);
s.SumR -= oldRange;
}
// Kahan compensated update for SumCR: sumCR += (close*range - oldClose*oldRange)
double deltaCR = Math.FusedMultiplyAdd(currentClose, currentRange, -oldClose * oldRange);
double yCR = deltaCR - s.SumCRComp;
double tCR = s.SumCR + yCR;
s.SumCRComp = (tCR - s.SumCR) - yCR;
s.SumCR = tCR;
// Add new values
s.SumCR = Math.FusedMultiplyAdd(currentClose, currentRange, s.SumCR);
s.SumR += currentRange;
// Kahan compensated update for SumR: sumR += (currentRange - oldRange)
double deltaR = currentRange - oldRange;
double yR = deltaR - s.SumRComp;
double tR = s.SumR + yR;
s.SumRComp = (tR - s.SumR) - yR;
s.SumR = tR;
}
else
{
// Kahan compensated addition for SumCR
double crVal = currentClose * currentRange;
double yCR = crVal - s.SumCRComp;
double tCR = s.SumCR + yCR;
s.SumCRComp = (tCR - s.SumCR) - yCR;
s.SumCR = tCR;
// Kahan compensated addition for SumR
double yR = currentRange - s.SumRComp;
double tR = s.SumR + yR;
s.SumRComp = (tR - s.SumR) - yR;
s.SumR = tR;
}
// Store in circular buffer
_closeBuffer[s.Head] = currentClose;
@@ -244,14 +240,6 @@ public sealed class Rwma : ITValuePublisher
{
s.Count++;
}
// Periodic resync to limit floating-point drift
s.SyncCounter++;
if (s.SyncCounter >= ResyncInterval && s.Count >= _period)
{
s.SyncCounter = 0;
ResyncRunningTotals(ref s);
}
}
// Calculate RWMA: Σ(close × range) / Σ(range)
@@ -392,7 +380,8 @@ public sealed class Rwma : ITValuePublisher
if (double.IsFinite(low[k])) { lastValidLow = low[k]; break; }
}
int syncCounter = 0;
double sumCRComp = 0;
double sumRComp = 0;
for (int i = 0; i < len; i++)
{
@@ -421,13 +410,35 @@ public sealed class Rwma : ITValuePublisher
if (count >= period)
{
sumCR = Math.FusedMultiplyAdd(-oldClose, oldRange, sumCR);
sumR -= oldRange;
}
// Kahan compensated update for SumCR
double deltaCR = Math.FusedMultiplyAdd(currentClose, currentRange, -oldClose * oldRange);
double yCR = deltaCR - sumCRComp;
double tCR = sumCR + yCR;
sumCRComp = (tCR - sumCR) - yCR;
sumCR = tCR;
// Add new values
sumCR = Math.FusedMultiplyAdd(currentClose, currentRange, sumCR);
sumR += currentRange;
// Kahan compensated update for SumR
double deltaR = currentRange - oldRange;
double yR = deltaR - sumRComp;
double tR = sumR + yR;
sumRComp = (tR - sumR) - yR;
sumR = tR;
}
else
{
// Kahan compensated addition for SumCR
double crVal = currentClose * currentRange;
double yCR = crVal - sumCRComp;
double tCR = sumCR + yCR;
sumCRComp = (tCR - sumCR) - yCR;
sumCR = tCR;
// Kahan compensated addition for SumR
double yR = currentRange - sumRComp;
double tR = sumR + yR;
sumRComp = (tR - sumR) - yR;
sumR = tR;
}
// Store in circular buffer
closeBuffer[head] = currentClose;
@@ -440,20 +451,6 @@ public sealed class Rwma : ITValuePublisher
count++;
}
// Periodic resync
syncCounter++;
if (syncCounter >= ResyncInterval && count >= period)
{
syncCounter = 0;
sumCR = 0;
sumR = 0;
for (int j = 0; j < period; j++)
{
sumCR = Math.FusedMultiplyAdd(closeBuffer[j], rangeBuffer[j], sumCR);
sumR += rangeBuffer[j];
}
}
output[i] = sumR > double.Epsilon ? sumCR / sumR : currentClose;
}
}
+46 -73
View File
@@ -13,6 +13,7 @@ namespace QuanTAlib;
/// </summary>
/// <remarks>
/// Arithmetic mean of the last n values using running sum for O(1) updates.
/// Kahan compensated summation prevents floating-point drift without periodic resync.
/// SIMD-accelerated batch processing (AVX-512/AVX2/NEON).
///
/// Calculation: <c>SMA = Σ(values) / n</c>.
@@ -28,12 +29,10 @@ public sealed class Sma : AbstractBase
private bool _disposed;
[StructLayout(LayoutKind.Auto)]
private record struct State(double Sum, double LastValidValue, int TickCount);
private record struct State(double Sum, double Compensation, double LastValidValue);
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
/// <summary>
/// Creates SMA with specified period.
/// </summary>
@@ -165,21 +164,22 @@ public sealed class Sma : AbstractBase
return _state.LastValidValue;
}
/// <summary>
/// Updates the running sum using Kahan compensated summation for O(1) drift-free updates.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateState(double val)
{
double removedValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0;
_state.Sum = Math.FusedMultiplyAdd(-1.0, removedValue, _state.Sum + val);
// Kahan compensated sliding window update
double delta = val - removedValue;
double y = delta - _state.Compensation;
double t = _state.Sum + y;
_state.Compensation = (t - _state.Sum) - y;
_state.Sum = t;
_buffer.Add(val);
_state.TickCount++;
if (_buffer.IsFull && _state.TickCount >= ResyncInterval)
{
_state.TickCount = 0;
_state.Sum = _buffer.RecalculateSum();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -205,7 +205,6 @@ public sealed class Sma : AbstractBase
// Use buffer's authoritative sum (UpdateNewest already did the differential update internally)
_state = restoredState with { Sum = _buffer.Sum };
// Note: Resync is only done on isNew=true path via UpdateState()
}
double result = _buffer.Count > 0 ? _state.Sum / _buffer.Count : double.NaN;
@@ -258,7 +257,7 @@ public sealed class Sma : AbstractBase
/// <summary>
/// Calculates SMA in-place, writing results to pre-allocated output span.
/// Zero-allocation method for maximum performance.
/// Uses stackalloc circular buffer for NaN-safe sliding window calculation.
/// Uses Kahan compensated summation for drift-free sliding window calculation.
/// Automatically uses SIMD acceleration for large, clean datasets.
/// </summary>
/// <param name="source">Input values</param>
@@ -325,6 +324,9 @@ public sealed class Sma : AbstractBase
return (results, sma);
}
/// <summary>
/// Scalar batch path with Kahan compensated summation and NaN handling.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateScalarCore(ReadOnlySpan<double> source, Span<double> output, int period)
{
@@ -339,6 +341,7 @@ public sealed class Sma : AbstractBase
try
{
double sum = 0;
double comp = 0; // Kahan compensation
double lastValid = double.NaN;
// Find first valid value to seed lastValid
@@ -354,6 +357,7 @@ public sealed class Sma : AbstractBase
int bufferIndex = 0;
int i = 0;
// Warmup phase: accumulating values before buffer is full
int warmupEnd = Math.Min(period, len);
for (; i < warmupEnd; i++)
{
@@ -367,12 +371,17 @@ public sealed class Sma : AbstractBase
val = lastValid;
}
sum += val;
// Kahan compensated addition during warmup
double y = val - comp;
double t = sum + y;
comp = (t - sum) - y;
sum = t;
buffer[i] = val;
output[i] = sum / (i + 1);
}
int tickCount = 0;
// Steady-state: sliding window with Kahan compensated delta
for (; i < len; i++)
{
double val = source[i];
@@ -385,7 +394,13 @@ public sealed class Sma : AbstractBase
val = lastValid;
}
sum = Math.FusedMultiplyAdd(-1.0, buffer[bufferIndex], sum + val);
// Kahan compensated sliding window: sum += (newVal - oldVal)
double delta = val - buffer[bufferIndex];
double y = delta - comp;
double t = sum + y;
comp = (t - sum) - y;
sum = t;
buffer[bufferIndex] = val;
bufferIndex++;
@@ -395,18 +410,6 @@ public sealed class Sma : AbstractBase
}
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
@@ -418,6 +421,10 @@ public sealed class Sma : AbstractBase
}
}
/// <summary>
/// AVX-512 SIMD batch path. Uses prefix-sum over deltas for vectorized SMA.
/// No periodic resync needed — double precision drift is negligible over batch runs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static void CalculateAvx512Core(ReadOnlySpan<double> source, Span<double> output, int period)
{
@@ -444,7 +451,6 @@ public sealed class Sma : AbstractBase
var vInvPeriod = Vector512.Create(invPeriod);
int simdEnd = period + (len - period) / VectorWidth * VectorWidth;
int tickCount = 0;
for (int i = period; i < simdEnd; i += VectorWidth)
{
@@ -470,30 +476,21 @@ public sealed class Sma : AbstractBase
vResult.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
sum = vSums.GetElement(7);
tickCount += VectorWidth;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
int lastIdx = i + VectorWidth - 1;
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
recalcSum += Unsafe.Add(ref srcRef, lastIdx - k);
}
sum = recalcSum;
}
}
for (int i = simdEnd; i < len; i++)
{
double newVal = Unsafe.Add(ref srcRef, i);
double oldVal = Unsafe.Add(ref srcRef, i - period);
sum = Math.FusedMultiplyAdd(-1.0, oldVal, sum + newVal);
sum += newVal - oldVal;
Unsafe.Add(ref outRef, i) = sum * invPeriod;
}
}
/// <summary>
/// AVX2 SIMD batch path. Uses prefix-sum over deltas for vectorized SMA.
/// No periodic resync needed — double precision drift is negligible over batch runs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static void CalculateAvx2Core(ReadOnlySpan<double> source, Span<double> output, int period)
{
@@ -521,7 +518,6 @@ public sealed class Sma : AbstractBase
var vInvPeriod = Vector256.Create(invPeriod);
var vZero = Vector256<double>.Zero;
int simdEnd = period + (len - period) / VectorWidth * VectorWidth;
int tickCount = 0;
for (int i = period; i < simdEnd; i += VectorWidth)
{
@@ -545,30 +541,21 @@ public sealed class Sma : AbstractBase
vResult.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
sum = vSums.GetElement(3);
tickCount += VectorWidth;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
int lastIdx = i + VectorWidth - 1;
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
recalcSum += Unsafe.Add(ref srcRef, lastIdx - k);
}
sum = recalcSum;
}
}
for (int i = simdEnd; i < len; i++)
{
double newVal = Unsafe.Add(ref srcRef, i);
double oldVal = Unsafe.Add(ref srcRef, i - period);
sum = Math.FusedMultiplyAdd(-1.0, oldVal, sum + newVal);
sum += newVal - oldVal;
Unsafe.Add(ref outRef, i) = sum * invPeriod;
}
}
/// <summary>
/// NEON SIMD batch path. Uses prefix-sum over deltas for vectorized SMA.
/// No periodic resync needed — double precision drift is negligible over batch runs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static void CalculateNeonCore(ReadOnlySpan<double> source, Span<double> output, int period)
{
@@ -595,7 +582,6 @@ public sealed class Sma : AbstractBase
var vInvPeriod = Vector128.Create(invPeriod);
int simdEnd = period + (len - period) / VectorWidth * VectorWidth;
int tickCount = 0;
for (int i = period; i < simdEnd; i += VectorWidth)
{
@@ -616,26 +602,13 @@ public sealed class Sma : AbstractBase
vResult.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
sum = ps1;
tickCount += VectorWidth;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
int lastIdx = i + VectorWidth - 1;
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
recalcSum += Unsafe.Add(ref srcRef, lastIdx - k);
}
sum = recalcSum;
}
}
for (int i = simdEnd; i < len; i++)
{
double newVal = Unsafe.Add(ref srcRef, i);
double oldVal = Unsafe.Add(ref srcRef, i - period);
sum = Math.FusedMultiplyAdd(-1.0, oldVal, sum + newVal);
sum += newVal - oldVal;
Unsafe.Add(ref outRef, i) = sum * invPeriod;
}
}
+26 -32
View File
@@ -9,6 +9,7 @@ namespace QuanTAlib;
/// <remarks>
/// Projects the linear regression line one step forward, forecasting the
/// next bar's value based on the least-squares trend over the lookback period.
/// Kahan compensated summation prevents floating-point drift without periodic resync.
///
/// Calculation: <c>TSF = slope × period + intercept</c> (standard convention)
/// or equivalently <c>TSF = b m</c> (reversed-x convention where b = current bar value).
@@ -30,15 +31,12 @@ public sealed class Tsf : AbstractBase
private int _disposed;
[StructLayout(LayoutKind.Auto)]
private record struct State(double SumY, double SumXY, double LastVal, double LastValidValue);
private record struct State(double SumY, double SumXY, double SumYComp, double SumXYComp, double LastVal, double LastValidValue);
private State _s;
private State _ps;
private int _tickCount;
private bool _isNew;
private const int ResyncInterval = 1000;
public override bool IsHot => _buffer.IsFull;
public bool IsNew => _isNew;
@@ -98,13 +96,19 @@ public sealed class Tsf : AbstractBase
double oldest = _buffer.Oldest;
double prevSumY = _s.SumY;
// O(1) update for SumXY (reversed-x convention)
// New value enters at x=0, existing values shift x+1, oldest drops off
// sumXY_new = sumXY_old + sumY_prev - n * oldest
_s.SumXY = Math.FusedMultiplyAdd(-_period, oldest, _s.SumXY + prevSumY);
// Kahan compensated update for SumXY: sumXY += (prevSumY - period * oldest)
double deltaXY = Math.FusedMultiplyAdd(-_period, oldest, prevSumY);
double yXY = deltaXY - _s.SumXYComp;
double tXY = _s.SumXY + yXY;
_s.SumXYComp = (tXY - _s.SumXY) - yXY;
_s.SumXY = tXY;
// O(1) update for SumY
_s.SumY = _s.SumY - oldest + val;
// Kahan compensated update for SumY: sumY += (val - oldest)
double deltaY = val - oldest;
double yY = deltaY - _s.SumYComp;
double tY = _s.SumY + yY;
_s.SumYComp = (tY - _s.SumY) - yY;
_s.SumY = tY;
_buffer.Add(val);
}
@@ -112,30 +116,21 @@ public sealed class Tsf : AbstractBase
{
if (_buffer.Count > 0)
{
_s.SumXY += _s.SumY;
// Kahan compensated addition for SumXY: sumXY += sumY
double yXY = _s.SumY - _s.SumXYComp;
double tXY = _s.SumXY + yXY;
_s.SumXYComp = (tXY - _s.SumXY) - yXY;
_s.SumXY = tXY;
}
_s.SumY += val;
// Kahan compensated addition for SumY
double yY = val - _s.SumYComp;
double tY = _s.SumY + yY;
_s.SumYComp = (tY - _s.SumY) - yY;
_s.SumY = tY;
_buffer.Add(val);
}
_tickCount++;
if (_buffer.IsFull && _tickCount >= ResyncInterval)
{
_tickCount = 0;
Resync();
}
}
private void Resync()
{
_s.SumY = _buffer.Sum;
_s.SumXY = 0;
var span = _buffer.GetSpan();
for (int i = 0; i < span.Length; i++)
{
int x = span.Length - 1 - i;
_s.SumXY = Math.FusedMultiplyAdd(x, span[i], _s.SumXY);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -409,7 +404,6 @@ public sealed class Tsf : AbstractBase
_s.LastValidValue = double.NaN;
_ps = default;
Last = default;
_tickCount = 0;
}
protected override void Dispose(bool disposing)
+293 -316
View File
@@ -11,6 +11,7 @@ namespace QuanTAlib;
/// </summary>
/// <remarks>
/// Linear weighting giving more weight to recent values. O(1) via dual running sums.
/// Kahan compensated summation prevents floating-point drift without periodic resync.
/// SIMD-accelerated batch processing (AVX-512/AVX2/NEON).
///
/// Calculation: <c>WMA = Σ(w_i × P_i) / Σ(w_i)</c> where <c>w_i = i</c>.
@@ -27,7 +28,7 @@ public sealed class Wma : AbstractBase
private bool _disposed;
[StructLayout(LayoutKind.Auto)]
private record struct State(double Sum, double WSum, double LastInput, double LastValidValue, int TickCount, bool HasSeenValidData);
private record struct State(double Sum, double WSum, double SumComp, double WSumComp, double LastInput, double LastValidValue, bool HasSeenValidData);
private State _state;
private State _pState;
@@ -37,8 +38,6 @@ public sealed class Wma : AbstractBase
/// </summary>
public double DefaultLastValidValue { get; set; } = double.NaN;
private const int ResyncInterval = 10000;
private static readonly Vector512<long> V512Idx1 = Vector512.Create(0L, 0, 1, 2, 3, 4, 5, 6);
private static readonly Vector512<long> V512Idx2 = Vector512.Create(0L, 0, 0, 1, 2, 3, 4, 5);
private static readonly Vector512<long> V512Idx4 = Vector512.Create(0L, 0, 0, 0, 0, 1, 2, 3);
@@ -95,6 +94,9 @@ public sealed class Wma : AbstractBase
return _state.HasSeenValidData ? _state.LastValidValue : DefaultLastValidValue;
}
/// <summary>
/// Updates both running sums using Kahan compensated summation.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateState(double val)
{
@@ -102,36 +104,57 @@ public sealed class Wma : AbstractBase
{
double oldSum = _state.Sum;
double oldest = _buffer.Oldest;
_state.Sum = Math.FusedMultiplyAdd(-1.0, oldest, _state.Sum + val);
_state.WSum = Math.FusedMultiplyAdd(-1.0, oldSum, _state.WSum + _period * val);
// Kahan compensated update for Sum: sum += (val - oldest)
double deltaS = val - oldest;
double yS = deltaS - _state.SumComp;
double tS = _state.Sum + yS;
_state.SumComp = (tS - _state.Sum) - yS;
_state.Sum = tS;
// Kahan compensated update for WSum: wsum += (period * val - oldSum)
double deltaW = (_period * val) - oldSum;
double yW = deltaW - _state.WSumComp;
double tW = _state.WSum + yW;
_state.WSumComp = (tW - _state.WSum) - yW;
_state.WSum = tW;
}
else
{
int count = _buffer.Count + 1;
_state.Sum += val;
_state.WSum = Math.FusedMultiplyAdd(count, val, _state.WSum);
// Kahan compensated addition for Sum
double yS = val - _state.SumComp;
double tS = _state.Sum + yS;
_state.SumComp = (tS - _state.Sum) - yS;
_state.Sum = tS;
// Kahan compensated addition for WSum
double wVal = count * val;
double yW = wVal - _state.WSumComp;
double tW = _state.WSum + yW;
_state.WSumComp = (tW - _state.WSum) - yW;
_state.WSum = tW;
}
_buffer.Add(val);
_state.TickCount++;
bool isNaN = double.IsNaN(_state.Sum) || double.IsNaN(_state.WSum);
bool needResync = _buffer.IsFull && _state.TickCount >= ResyncInterval;
if (needResync || (isNaN && double.IsFinite(val)))
// NaN recovery: if sums went NaN but input is finite, recalculate from buffer
if ((double.IsNaN(_state.Sum) || double.IsNaN(_state.WSum)) && double.IsFinite(val))
{
_state.TickCount = 0;
double recalcSum = 0;
double recalcWsum = 0;
int weight = 1;
foreach (double item in _buffer)
{
recalcSum += item;
recalcWsum = Math.FusedMultiplyAdd(weight, item, recalcWsum);
recalcWsum += weight * item;
weight++;
}
_state.Sum = recalcSum;
_state.WSum = recalcWsum;
_state.SumComp = 0;
_state.WSumComp = 0;
}
}
@@ -231,7 +254,8 @@ public sealed class Wma : AbstractBase
_buffer.Clear();
_state.Sum = 0;
_state.WSum = 0;
_state.TickCount = 0;
_state.SumComp = 0;
_state.WSumComp = 0;
// Process window
for (int i = startIndex; i < len; i++)
@@ -310,6 +334,9 @@ public sealed class Wma : AbstractBase
return (results, indicator);
}
/// <summary>
/// Scalar batch path with Kahan compensated dual running sums and NaN handling.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateScalarCore(ReadOnlySpan<double> source, Span<double> output, int period)
{
@@ -317,12 +344,15 @@ public sealed class Wma : AbstractBase
double divisor = (double)period * (period + 1) * 0.5;
double sum = 0;
double wsum = 0;
double sumComp = 0;
double wsumComp = 0;
double lastValid = double.NaN;
Span<double> buffer = period <= 512 ? stackalloc double[period] : new double[period];
int bufferIdx = 0;
int i = 0;
// Warmup phase
int warmupEnd = Math.Min(period, len);
for (; i < warmupEnd; i++)
{
@@ -336,15 +366,26 @@ public sealed class Wma : AbstractBase
val = lastValid;
}
sum += val;
wsum = Math.FusedMultiplyAdd(i + 1, val, wsum);
// Kahan compensated addition for sum
double yS = val - sumComp;
double tS = sum + yS;
sumComp = (tS - sum) - yS;
sum = tS;
// Kahan compensated addition for wsum
double wVal = (i + 1) * val;
double yW = wVal - wsumComp;
double tW = wsum + yW;
wsumComp = (tW - wsum) - yW;
wsum = tW;
buffer[i] = val;
double currentDivisor = (double)(i + 1) * (i + 2) * 0.5;
output[i] = wsum / currentDivisor;
}
int tickCount = 0;
// Steady-state: sliding window with Kahan compensated dual sums
for (; i < len; i++)
{
double val = source[i];
@@ -359,8 +400,20 @@ public sealed class Wma : AbstractBase
double oldSum = sum;
double oldest = buffer[bufferIdx];
sum = Math.FusedMultiplyAdd(-1.0, oldest, sum + val);
wsum = Math.FusedMultiplyAdd(-1.0, oldSum, wsum + period * val);
// Kahan compensated update for Sum: sum += (val - oldest)
double deltaS = val - oldest;
double yS = deltaS - sumComp;
double tS = sum + yS;
sumComp = (tS - sum) - yS;
sum = tS;
// Kahan compensated update for WSum: wsum += (period * val - oldSum)
double deltaW = (period * val) - oldSum;
double yW = deltaW - wsumComp;
double tW = wsum + yW;
wsumComp = (tW - wsum) - yW;
wsum = tW;
buffer[bufferIdx] = val;
bufferIdx++;
@@ -370,33 +423,13 @@ public sealed class Wma : AbstractBase
}
output[i] = wsum / divisor;
tickCount++;
bool isNaN = double.IsNaN(sum) || double.IsNaN(wsum);
if (tickCount >= ResyncInterval || (isNaN && double.IsFinite(val)))
{
tickCount = 0;
double recalcSum = 0;
double recalcWsum = 0;
for (int k = 0; k < period; k++)
{
int idx = bufferIdx + k;
if (idx >= period)
{
idx -= period;
}
double v = buffer[idx];
recalcSum += v;
recalcWsum = Math.FusedMultiplyAdd(k + 1, v, recalcWsum);
}
sum = recalcSum;
wsum = recalcWsum;
}
}
}
/// <summary>
/// AVX-512 SIMD batch path. Uses prefix-sum over deltas for vectorized WMA.
/// No periodic resync needed — double precision drift is negligible over batch runs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static void CalculateAvx512Core(ReadOnlySpan<double> source, Span<double> output, int period)
{
@@ -433,77 +466,53 @@ public sealed class Wma : AbstractBase
var vSumState = Vector512.Create(sum);
var vWsumState = Vector512.Create(wsum);
int idx = period;
while (idx < simdEnd)
for (int idx = period; idx < simdEnd; idx += vectorWidth)
{
int nextSync = Math.Min(simdEnd, idx + ResyncInterval);
var vNew = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx));
var vOld = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx - period));
for (; idx < nextSync; idx += vectorWidth)
{
var vNew = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx));
var vOld = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx - period));
var vDeltaS = Avx512F.Subtract(vNew, vOld);
var vDeltaS = Avx512F.Subtract(vNew, vOld);
// Prefix sum of DeltaS
var vShiftS1 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vDeltaS, V512Idx1), V512Mask1);
var vPs1 = Avx512F.Add(vDeltaS, vShiftS1);
// Prefix sum of DeltaS
var vShiftS1 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vDeltaS, V512Idx1), V512Mask1);
var vPs1 = Avx512F.Add(vDeltaS, vShiftS1);
var vShiftS2 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vPs1, V512Idx2), V512Mask2);
var vPs2 = Avx512F.Add(vPs1, vShiftS2);
var vShiftS2 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vPs1, V512Idx2), V512Mask2);
var vPs2 = Avx512F.Add(vPs1, vShiftS2);
var vShiftS4 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vPs2, V512Idx4), V512Mask4);
var vPs4 = Avx512F.Add(vPs2, vShiftS4);
var vShiftS4 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vPs2, V512Idx4), V512Mask4);
var vPs4 = Avx512F.Add(vPs2, vShiftS4);
var vSums = Avx512F.Add(vSumState, vPs4);
var vSums = Avx512F.Add(vSumState, vPs4);
// Calculate Wsum update
var vSumsShifted = Avx512F.Subtract(vSums, vDeltaS);
var vU = Avx512F.FusedMultiplySubtract(vPeriod, vNew, vSumsShifted);
// Calculate Wsum update
var vSumsShifted = Avx512F.Subtract(vSums, vDeltaS);
var vU = Avx512F.FusedMultiplySubtract(vPeriod, vNew, vSumsShifted);
// Prefix sum of vU
var vShiftW1 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vU, V512Idx1), V512Mask1);
var vPw1 = Avx512F.Add(vU, vShiftW1);
// Prefix sum of vU
var vShiftW1 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vU, V512Idx1), V512Mask1);
var vPw1 = Avx512F.Add(vU, vShiftW1);
var vShiftW2 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vPw1, V512Idx2), V512Mask2);
var vPw2 = Avx512F.Add(vPw1, vShiftW2);
var vShiftW2 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vPw1, V512Idx2), V512Mask2);
var vPw2 = Avx512F.Add(vPw1, vShiftW2);
var vShiftW4 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vPw2, V512Idx4), V512Mask4);
var vPw4 = Avx512F.Add(vPw2, vShiftW4);
var vShiftW4 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vPw2, V512Idx4), V512Mask4);
var vPw4 = Avx512F.Add(vPw2, vShiftW4);
var vWsums = Avx512F.Add(vWsumState, vPw4);
var vWsums = Avx512F.Add(vWsumState, vPw4);
var vResult = Avx512F.Multiply(vWsums, vInvDivisor);
vResult.StoreUnsafe(ref Unsafe.Add(ref outRef, idx));
var vResult = Avx512F.Multiply(vWsums, vInvDivisor);
vResult.StoreUnsafe(ref Unsafe.Add(ref outRef, idx));
// Update state for next iteration
vSumState = Vector512.Create(vSums.GetElement(7));
vWsumState = Vector512.Create(vWsums.GetElement(7));
}
if (idx < len)
{
int lastIdx = idx - 1;
double recalcSum = 0;
double recalcWsum = 0;
for (int k = 0; k < period; k++)
{
double val = Unsafe.Add(ref srcRef, lastIdx - k);
recalcSum += val;
recalcWsum += (period - k) * val;
}
sum = recalcSum;
wsum = recalcWsum;
vSumState = Vector512.Create(sum);
vWsumState = Vector512.Create(wsum);
}
// Update state for next iteration
vSumState = Vector512.Create(vSums.GetElement(7));
vWsumState = Vector512.Create(vWsums.GetElement(7));
}
sum = vSumState.GetElement(0);
wsum = vWsumState.GetElement(0);
for (; idx < len; idx++)
for (int idx = simdEnd; idx < len; idx++)
{
double val = Unsafe.Add(ref srcRef, idx);
double oldSum = sum;
@@ -514,6 +523,10 @@ public sealed class Wma : AbstractBase
}
}
/// <summary>
/// AVX2 SIMD batch path. Uses prefix-sum over deltas for vectorized WMA.
/// No periodic resync needed — double precision drift is negligible over batch runs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static void CalculateSimdCore(ReadOnlySpan<double> source, Span<double> output, int period)
{
@@ -552,173 +565,158 @@ public sealed class Wma : AbstractBase
var vWsumState = Vector256.Create(wsum);
int idx = period;
while (idx < simdEnd)
// Unrolled loop: process 8 elements (2 vectors of 4) at a time
int unrolledEnd = simdEnd - (2 * vectorWidth);
for (; idx <= unrolledEnd; idx += 2 * vectorWidth)
{
int nextSync = Math.Min(simdEnd, idx + ResyncInterval);
var vNew1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx));
var vOld1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx - period));
var vNew2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx + vectorWidth));
var vOld2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx + vectorWidth - period));
int unrolledSync = nextSync - (2 * vectorWidth);
for (; idx <= unrolledSync; idx += 2 * vectorWidth)
var vDeltaS1 = Avx.Subtract(vNew1, vOld1);
var vDeltaS2 = Avx.Subtract(vNew2, vOld2);
var vShiftS11 = Avx2.Permute4x64(vDeltaS1.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShiftS11 = Avx.Blend(vZero, vShiftS11, 0b_1110);
var vPsDeltaS1 = Avx.Add(vDeltaS1, vShiftS11);
var vShiftS21 = Avx2.Permute4x64(vPsDeltaS1.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShiftS21 = Avx.Blend(vZero, vShiftS21, 0b_1100);
vPsDeltaS1 = Avx.Add(vPsDeltaS1, vShiftS21);
var vShiftS12 = Avx2.Permute4x64(vDeltaS2.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShiftS12 = Avx.Blend(vZero, vShiftS12, 0b_1110);
var vPsDeltaS2 = Avx.Add(vDeltaS2, vShiftS12);
var vShiftS22 = Avx2.Permute4x64(vPsDeltaS2.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShiftS22 = Avx.Blend(vZero, vShiftS22, 0b_1100);
vPsDeltaS2 = Avx.Add(vPsDeltaS2, vShiftS22);
var vSums1 = Avx.Add(vSumState, vPsDeltaS1);
var vLastS1 = Avx2.Permute4x64(vSums1.AsUInt64(), 0b_11_11_11_11).AsDouble(); // skipcq: CS-R1131
var vSums2 = Avx.Add(vLastS1, vPsDeltaS2);
var vSumsShifted1 = Avx.Subtract(vSums1, vDeltaS1);
var vSumsShifted2 = Avx.Subtract(vSums2, vDeltaS2);
Vector256<double> vU1, vU2;
if (Fma.IsSupported)
{
var vNew1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx));
var vOld1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx - period));
var vNew2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx + vectorWidth));
var vOld2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx + vectorWidth - period));
var vDeltaS1 = Avx.Subtract(vNew1, vOld1);
var vDeltaS2 = Avx.Subtract(vNew2, vOld2);
var vShiftS11 = Avx2.Permute4x64(vDeltaS1.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShiftS11 = Avx.Blend(vZero, vShiftS11, 0b_1110);
var vPsDeltaS1 = Avx.Add(vDeltaS1, vShiftS11);
var vShiftS21 = Avx2.Permute4x64(vPsDeltaS1.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShiftS21 = Avx.Blend(vZero, vShiftS21, 0b_1100);
vPsDeltaS1 = Avx.Add(vPsDeltaS1, vShiftS21);
var vShiftS12 = Avx2.Permute4x64(vDeltaS2.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShiftS12 = Avx.Blend(vZero, vShiftS12, 0b_1110);
var vPsDeltaS2 = Avx.Add(vDeltaS2, vShiftS12);
var vShiftS22 = Avx2.Permute4x64(vPsDeltaS2.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShiftS22 = Avx.Blend(vZero, vShiftS22, 0b_1100);
vPsDeltaS2 = Avx.Add(vPsDeltaS2, vShiftS22);
var vSums1 = Avx.Add(vSumState, vPsDeltaS1);
var vLastS1 = Avx2.Permute4x64(vSums1.AsUInt64(), 0b_11_11_11_11).AsDouble(); // skipcq: CS-R1131
var vSums2 = Avx.Add(vLastS1, vPsDeltaS2);
var vSumsShifted1 = Avx.Subtract(vSums1, vDeltaS1);
var vSumsShifted2 = Avx.Subtract(vSums2, vDeltaS2);
Vector256<double> vU1, vU2;
if (Fma.IsSupported)
{
vU1 = Fma.MultiplySubtract(vPeriod, vNew1, vSumsShifted1);
vU2 = Fma.MultiplySubtract(vPeriod, vNew2, vSumsShifted2);
}
else
{
var vTerm1 = Avx.Multiply(vPeriod, vNew1);
var vTerm2 = Avx.Multiply(vPeriod, vNew2);
vU1 = Avx.Subtract(vTerm1, vSumsShifted1);
vU2 = Avx.Subtract(vTerm2, vSumsShifted2);
}
var vShiftW11 = Avx2.Permute4x64(vU1.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShiftW11 = Avx.Blend(vZero, vShiftW11, 0b_1110);
var vPw11 = Avx.Add(vU1, vShiftW11);
var vShiftW21 = Avx2.Permute4x64(vPw11.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShiftW21 = Avx.Blend(vZero, vShiftW21, 0b_1100);
var vPw21 = Avx.Add(vPw11, vShiftW21);
var vShiftW12 = Avx2.Permute4x64(vU2.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShiftW12 = Avx.Blend(vZero, vShiftW12, 0b_1110);
var vPw12 = Avx.Add(vU2, vShiftW12);
var vShiftW22 = Avx2.Permute4x64(vPw12.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShiftW22 = Avx.Blend(vZero, vShiftW22, 0b_1100);
var vPw22 = Avx.Add(vPw12, vShiftW22);
var vWsums1 = Avx.Add(vWsumState, vPw21);
var vLastW1 = Avx2.Permute4x64(vWsums1.AsUInt64(), 0b_11_11_11_11).AsDouble(); // skipcq: CS-R1131
var vWsums2 = Avx.Add(vLastW1, vPw22);
Vector256<double> vResult1, vResult2;
if (Fma.IsSupported)
{
vResult1 = Fma.MultiplyAdd(vWsums1, vInvDivisor, vZero);
vResult2 = Fma.MultiplyAdd(vWsums2, vInvDivisor, vZero);
}
else
{
vResult1 = Avx.Multiply(vWsums1, vInvDivisor);
vResult2 = Avx.Multiply(vWsums2, vInvDivisor);
}
vResult1.StoreUnsafe(ref Unsafe.Add(ref outRef, idx));
vResult2.StoreUnsafe(ref Unsafe.Add(ref outRef, idx + vectorWidth));
vSumState = Avx2.Permute4x64(vSums2.AsUInt64(), 0b_11_11_11_11).AsDouble(); // skipcq: CS-R1131
vWsumState = Avx2.Permute4x64(vWsums2.AsUInt64(), 0b_11_11_11_11).AsDouble(); // skipcq: CS-R1131
vU1 = Fma.MultiplySubtract(vPeriod, vNew1, vSumsShifted1);
vU2 = Fma.MultiplySubtract(vPeriod, vNew2, vSumsShifted2);
}
else
{
var vTerm1 = Avx.Multiply(vPeriod, vNew1);
var vTerm2 = Avx.Multiply(vPeriod, vNew2);
vU1 = Avx.Subtract(vTerm1, vSumsShifted1);
vU2 = Avx.Subtract(vTerm2, vSumsShifted2);
}
for (; idx < nextSync; idx += vectorWidth)
var vShiftW11 = Avx2.Permute4x64(vU1.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShiftW11 = Avx.Blend(vZero, vShiftW11, 0b_1110);
var vPw11 = Avx.Add(vU1, vShiftW11);
var vShiftW21 = Avx2.Permute4x64(vPw11.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShiftW21 = Avx.Blend(vZero, vShiftW21, 0b_1100);
var vPw21 = Avx.Add(vPw11, vShiftW21);
var vShiftW12 = Avx2.Permute4x64(vU2.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShiftW12 = Avx.Blend(vZero, vShiftW12, 0b_1110);
var vPw12 = Avx.Add(vU2, vShiftW12);
var vShiftW22 = Avx2.Permute4x64(vPw12.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShiftW22 = Avx.Blend(vZero, vShiftW22, 0b_1100);
var vPw22 = Avx.Add(vPw12, vShiftW22);
var vWsums1 = Avx.Add(vWsumState, vPw21);
var vLastW1 = Avx2.Permute4x64(vWsums1.AsUInt64(), 0b_11_11_11_11).AsDouble(); // skipcq: CS-R1131
var vWsums2 = Avx.Add(vLastW1, vPw22);
Vector256<double> vResult1, vResult2;
if (Fma.IsSupported)
{
var vNew = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx));
var vOld = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx - period));
vResult1 = Fma.MultiplyAdd(vWsums1, vInvDivisor, vZero);
vResult2 = Fma.MultiplyAdd(vWsums2, vInvDivisor, vZero);
}
else
{
vResult1 = Avx.Multiply(vWsums1, vInvDivisor);
vResult2 = Avx.Multiply(vWsums2, vInvDivisor);
}
vResult1.StoreUnsafe(ref Unsafe.Add(ref outRef, idx));
vResult2.StoreUnsafe(ref Unsafe.Add(ref outRef, idx + vectorWidth));
var vDeltaS = Avx.Subtract(vNew, vOld);
vSumState = Avx2.Permute4x64(vSums2.AsUInt64(), 0b_11_11_11_11).AsDouble(); // skipcq: CS-R1131
vWsumState = Avx2.Permute4x64(vWsums2.AsUInt64(), 0b_11_11_11_11).AsDouble(); // skipcq: CS-R1131
}
var vShiftS1 = Avx2.Permute4x64(vDeltaS.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShiftS1 = Avx.Blend(vZero, vShiftS1, 0b_1110);
var vPs1 = Avx.Add(vDeltaS, vShiftS1);
// Process remaining vectors
for (; idx < simdEnd; idx += vectorWidth)
{
var vNew = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx));
var vOld = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx - period));
var vShiftS2 = Avx2.Permute4x64(vPs1.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShiftS2 = Avx.Blend(vZero, vShiftS2, 0b_1100);
var vPs2 = Avx.Add(vPs1, vShiftS2);
var vDeltaS = Avx.Subtract(vNew, vOld);
var vSums = Avx.Add(vSumState, vPs2);
var vShiftS1 = Avx2.Permute4x64(vDeltaS.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShiftS1 = Avx.Blend(vZero, vShiftS1, 0b_1110);
var vPs1 = Avx.Add(vDeltaS, vShiftS1);
var vSumsShifted = Avx.Subtract(vSums, vDeltaS);
Vector256<double> vU;
if (Fma.IsSupported)
{
vU = Fma.MultiplySubtract(vPeriod, vNew, vSumsShifted);
}
else
{
var vTerm1 = Avx.Multiply(vPeriod, vNew);
vU = Avx.Subtract(vTerm1, vSumsShifted);
}
var vShiftS2 = Avx2.Permute4x64(vPs1.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShiftS2 = Avx.Blend(vZero, vShiftS2, 0b_1100);
var vPs2 = Avx.Add(vPs1, vShiftS2);
var vShiftW1 = Avx2.Permute4x64(vU.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShiftW1 = Avx.Blend(vZero, vShiftW1, 0b_1110);
var vPw1 = Avx.Add(vU, vShiftW1);
var vSums = Avx.Add(vSumState, vPs2);
var vShiftW2 = Avx2.Permute4x64(vPw1.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShiftW2 = Avx.Blend(vZero, vShiftW2, 0b_1100);
var vPw2 = Avx.Add(vPw1, vShiftW2);
var vWsums = Avx.Add(vWsumState, vPw2);
Vector256<double> vResult = Fma.IsSupported
? Fma.MultiplyAdd(vWsums, vInvDivisor, vZero)
: Avx.Multiply(vWsums, vInvDivisor);
vResult.StoreUnsafe(ref Unsafe.Add(ref outRef, idx));
vSumState = Avx2.Permute4x64(vSums.AsUInt64(), 0b_11_11_11_11).AsDouble(); // skipcq: CS-R1131
vWsumState = Avx2.Permute4x64(vWsums.AsUInt64(), 0b_11_11_11_11).AsDouble(); // skipcq: CS-R1131
var vSumsShifted = Avx.Subtract(vSums, vDeltaS);
Vector256<double> vU;
if (Fma.IsSupported)
{
vU = Fma.MultiplySubtract(vPeriod, vNew, vSumsShifted);
}
else
{
var vTerm1 = Avx.Multiply(vPeriod, vNew);
vU = Avx.Subtract(vTerm1, vSumsShifted);
}
if (idx < len)
{
int lastIdx = idx - 1;
double recalcSum = 0;
double recalcWsum = 0;
for (int k = 0; k < period; k++)
{
double val = Unsafe.Add(ref srcRef, lastIdx - k);
recalcSum += val;
recalcWsum += (period - k) * val;
}
sum = recalcSum;
wsum = recalcWsum;
var vShiftW1 = Avx2.Permute4x64(vU.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShiftW1 = Avx.Blend(vZero, vShiftW1, 0b_1110);
var vPw1 = Avx.Add(vU, vShiftW1);
vSumState = Vector256.Create(sum);
vWsumState = Vector256.Create(wsum);
}
var vShiftW2 = Avx2.Permute4x64(vPw1.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShiftW2 = Avx.Blend(vZero, vShiftW2, 0b_1100);
var vPw2 = Avx.Add(vPw1, vShiftW2);
var vWsums = Avx.Add(vWsumState, vPw2);
Vector256<double> vResult = Fma.IsSupported
? Fma.MultiplyAdd(vWsums, vInvDivisor, vZero)
: Avx.Multiply(vWsums, vInvDivisor);
vResult.StoreUnsafe(ref Unsafe.Add(ref outRef, idx));
vSumState = Avx2.Permute4x64(vSums.AsUInt64(), 0b_11_11_11_11).AsDouble(); // skipcq: CS-R1131
vWsumState = Avx2.Permute4x64(vWsums.AsUInt64(), 0b_11_11_11_11).AsDouble(); // skipcq: CS-R1131
}
sum = vSumState.GetElement(0);
wsum = vWsumState.GetElement(0);
// Scalar tail
for (; idx < len; idx++)
{
double val = Unsafe.Add(ref srcRef, idx);
double oldSum = sum;
double oldest = Unsafe.Add(ref srcRef, idx - period);
sum = Math.FusedMultiplyAdd(-1.0, oldest, sum + val);
wsum = Math.FusedMultiplyAdd(-1.0, oldSum, wsum + period * val);
sum = sum - oldest + val;
wsum = wsum - oldSum + period * val;
Unsafe.Add(ref outRef, idx) = wsum * invDivisor;
}
}
/// <summary>
/// NEON SIMD batch path. Uses prefix-sum over deltas for vectorized WMA.
/// No periodic resync needed — double precision drift is negligible over batch runs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static void CalculateNeonCore(ReadOnlySpan<double> source, Span<double> output, int period)
{
@@ -755,103 +753,82 @@ public sealed class Wma : AbstractBase
double wsumState = wsum;
int idx = period;
while (idx < simdEnd)
// Unrolled loop: process 4 elements (2 vectors) at a time
int unrolledEnd = simdEnd - (2 * vectorWidth);
for (; idx <= unrolledEnd; idx += 2 * vectorWidth)
{
int nextSync = Math.Min(simdEnd, idx + ResyncInterval);
var vNew1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx));
var vOld1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx - period));
var vNew2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx + vectorWidth));
var vOld2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx + vectorWidth - period));
// Unrolled loop: process 4 elements (2 vectors) at a time
int unrolledSync = nextSync - (2 * vectorWidth);
for (; idx <= unrolledSync; idx += 2 * vectorWidth)
{
var vNew1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx));
var vOld1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx - period));
var vNew2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx + vectorWidth));
var vOld2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx + vectorWidth - period));
var vDeltaS1 = AdvSimd.Arm64.Subtract(vNew1, vOld1);
var vDeltaS2 = AdvSimd.Arm64.Subtract(vNew2, vOld2);
var vDeltaS1 = AdvSimd.Arm64.Subtract(vNew1, vOld1);
var vDeltaS2 = AdvSimd.Arm64.Subtract(vNew2, vOld2);
// Prefix sum for first vector: [d0, d0+d1]
double d10 = vDeltaS1.GetElement(0);
double d11 = vDeltaS1.GetElement(1);
double ps10 = sumState + d10;
double ps11 = ps10 + d11;
// Prefix sum for first vector: [d0, d0+d1]
double d10 = vDeltaS1.GetElement(0);
double d11 = vDeltaS1.GetElement(1);
double ps10 = sumState + d10;
double ps11 = ps10 + d11;
// Prefix sum for second vector
double d20 = vDeltaS2.GetElement(0);
double d21 = vDeltaS2.GetElement(1);
double ps20 = ps11 + d20;
double ps21 = ps20 + d21;
// Prefix sum for second vector
double d20 = vDeltaS2.GetElement(0);
double d21 = vDeltaS2.GetElement(1);
double ps20 = ps11 + d20;
double ps21 = ps20 + d21;
// Calculate Wsum update: W_new = W_old - S_prev + n*new
double u10 = Math.FusedMultiplyAdd(period, vNew1.GetElement(0), -sumState);
double u11 = Math.FusedMultiplyAdd(period, vNew1.GetElement(1), -ps10);
double u20 = Math.FusedMultiplyAdd(period, vNew2.GetElement(0), -ps11);
double u21 = Math.FusedMultiplyAdd(period, vNew2.GetElement(1), -ps20);
// Calculate Wsum update: W_new = W_old - S_prev + n*new
// For element i: u_i = period * new_i - S_(i-1)
double u10 = Math.FusedMultiplyAdd(period, vNew1.GetElement(0), -sumState);
double u11 = Math.FusedMultiplyAdd(period, vNew1.GetElement(1), -ps10);
double u20 = Math.FusedMultiplyAdd(period, vNew2.GetElement(0), -ps11);
double u21 = Math.FusedMultiplyAdd(period, vNew2.GetElement(1), -ps20);
// Prefix sum of U values
double pw10 = wsumState + u10;
double pw11 = pw10 + u11;
double pw20 = pw11 + u20;
double pw21 = pw20 + u21;
// Prefix sum of U values
double pw10 = wsumState + u10;
double pw11 = pw10 + u11;
double pw20 = pw11 + u20;
double pw21 = pw20 + u21;
var vWsums1 = Vector128.Create(pw10, pw11);
var vWsums2 = Vector128.Create(pw20, pw21);
var vWsums1 = Vector128.Create(pw10, pw11);
var vWsums2 = Vector128.Create(pw20, pw21);
var vResult1 = AdvSimd.Arm64.Multiply(vWsums1, vInvDivisor);
var vResult2 = AdvSimd.Arm64.Multiply(vWsums2, vInvDivisor);
var vResult1 = AdvSimd.Arm64.Multiply(vWsums1, vInvDivisor);
var vResult2 = AdvSimd.Arm64.Multiply(vWsums2, vInvDivisor);
vResult1.StoreUnsafe(ref Unsafe.Add(ref outRef, idx));
vResult2.StoreUnsafe(ref Unsafe.Add(ref outRef, idx + vectorWidth));
vResult1.StoreUnsafe(ref Unsafe.Add(ref outRef, idx));
vResult2.StoreUnsafe(ref Unsafe.Add(ref outRef, idx + vectorWidth));
sumState = ps21;
wsumState = pw21;
}
sumState = ps21;
wsumState = pw21;
}
// Process remaining pairs
for (; idx < simdEnd; idx += vectorWidth)
{
var vNew = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx));
var vOld = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx - period));
// Process remaining pairs
for (; idx < nextSync; idx += vectorWidth)
{
var vNew = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx));
var vOld = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, idx - period));
var vDeltaS = AdvSimd.Arm64.Subtract(vNew, vOld);
var vDeltaS = AdvSimd.Arm64.Subtract(vNew, vOld);
double d0 = vDeltaS.GetElement(0);
double d1 = vDeltaS.GetElement(1);
double ps0 = sumState + d0;
double ps1 = ps0 + d1;
double d0 = vDeltaS.GetElement(0);
double d1 = vDeltaS.GetElement(1);
double ps0 = sumState + d0;
double ps1 = ps0 + d1;
double u0 = Math.FusedMultiplyAdd(period, vNew.GetElement(0), -sumState);
double u1 = Math.FusedMultiplyAdd(period, vNew.GetElement(1), -ps0);
double u0 = Math.FusedMultiplyAdd(period, vNew.GetElement(0), -sumState);
double u1 = Math.FusedMultiplyAdd(period, vNew.GetElement(1), -ps0);
double pw0 = wsumState + u0;
double pw1 = pw0 + u1;
double pw0 = wsumState + u0;
double pw1 = pw0 + u1;
var vWsums = Vector128.Create(pw0, pw1);
var vResult = AdvSimd.Arm64.Multiply(vWsums, vInvDivisor);
var vWsums = Vector128.Create(pw0, pw1);
var vResult = AdvSimd.Arm64.Multiply(vWsums, vInvDivisor);
vResult.StoreUnsafe(ref Unsafe.Add(ref outRef, idx));
vResult.StoreUnsafe(ref Unsafe.Add(ref outRef, idx));
sumState = ps1;
wsumState = pw1;
}
// Resync to prevent floating-point drift
if (idx < len)
{
int lastIdx = idx - 1;
double recalcSum = 0;
double recalcWsum = 0;
for (int k = 0; k < period; k++)
{
double val = Unsafe.Add(ref srcRef, lastIdx - k);
recalcSum += val;
recalcWsum = Math.FusedMultiplyAdd(period - k, val, recalcWsum);
}
sumState = recalcSum;
wsumState = recalcWsum;
}
sumState = ps1;
wsumState = pw1;
}
sum = sumState;
@@ -863,9 +840,9 @@ public sealed class Wma : AbstractBase
double val = Unsafe.Add(ref srcRef, idx);
double oldSum = sum;
double oldest = Unsafe.Add(ref srcRef, idx - period);
sum = Math.FusedMultiplyAdd(-1.0, oldest, sum + val);
wsum = Math.FusedMultiplyAdd(-1.0, oldSum, wsum + period * val);
sum = sum - oldest + val;
wsum = wsum - oldSum + period * val;
Unsafe.Add(ref outRef, idx) = wsum * invDivisor;
}
}
}
}