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
+25 -36
View File
@@ -21,6 +21,8 @@ namespace QuanTAlib;
/// - MASE = 1 means same as naive forecast
/// - MASE > 1 means worse than naive forecast
/// - Robust to zero actual values (unlike MAPE)
///
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
/// </remarks>
[SkipLocalsInit]
public sealed class Mase : AbstractBase
@@ -32,6 +34,8 @@ public sealed class Mase : AbstractBase
private record struct State(
double ErrorSum,
double ScaleSum,
double ErrorComp,
double ScaleComp,
double LastValidActual,
double LastValidPredicted,
double PrevActual,
@@ -39,8 +43,6 @@ public sealed class Mase : AbstractBase
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
public Mase(int period)
{
if (period <= 0)
@@ -50,8 +52,8 @@ public sealed class Mase : AbstractBase
_errorBuffer = new RingBuffer(period);
_scaleBuffer = new RingBuffer(period);
_state = new State(0, 0, 0, 0, double.NaN, 0);
_p_state = new State(0, 0, 0, 0, double.NaN, 0);
_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0);
_p_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0);
Name = $"Mase({period})";
WarmupPeriod = period + 1; // Need one extra for scale calculation
}
@@ -89,34 +91,36 @@ public sealed class Mase : AbstractBase
{
_p_state = _state;
// Update error buffer
// Update error buffer — Kahan compensated
double removedError = _errorBuffer.Count == _errorBuffer.Capacity ? _errorBuffer.Oldest : 0.0;
_state.ErrorSum = _state.ErrorSum - removedError + absError;
{
double delta = absError - removedError;
double y = delta - _state.ErrorComp;
double t = _state.ErrorSum + y;
_state.ErrorComp = (t - _state.ErrorSum) - y;
_state.ErrorSum = t;
}
_errorBuffer.Add(absError);
// Update scale buffer
// Update scale buffer — Kahan compensated
double removedScale = _scaleBuffer.Count == _scaleBuffer.Capacity ? _scaleBuffer.Oldest : 0.0;
_state.ScaleSum = _state.ScaleSum - removedScale + naiveDiff;
{
double delta = naiveDiff - removedScale;
double y = delta - _state.ScaleComp;
double t = _state.ScaleSum + y;
_state.ScaleComp = (t - _state.ScaleSum) - y;
_state.ScaleSum = t;
}
_scaleBuffer.Add(naiveDiff);
_state.PrevActual = actualVal;
_state.TickCount++;
if (_state.TickCount >= ResyncInterval)
{
// Keep TickCount > period to maintain post-warmup state
_state.TickCount = _errorBuffer.Capacity + 1;
_state.ErrorSum = _errorBuffer.RecalculateSum();
_state.ScaleSum = _scaleBuffer.RecalculateSum();
}
}
else
{
_state = _p_state;
// Bar correction: update buffer and recalculate sums
// Note: _p_state was saved BEFORE the Add, but buffer still has the added value
// So we update newest and recalculate to ensure consistency
_errorBuffer.UpdateNewest(absError);
_scaleBuffer.UpdateNewest(naiveDiff);
@@ -174,8 +178,8 @@ public sealed class Mase : AbstractBase
{
_errorBuffer.Clear();
_scaleBuffer.Clear();
_state = new State(0, 0, 0, 0, double.NaN, 0);
_p_state = new State(0, 0, 0, 0, double.NaN, 0);
_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0);
_p_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0);
Last = default;
}
@@ -293,7 +297,6 @@ public sealed class Mase : AbstractBase
prevActual = act;
}
int tickCount = 0;
for (; i < len; i++)
{
double act = actual[i];
@@ -336,20 +339,6 @@ public sealed class Mase : AbstractBase
output[i] = scale > 1e-10 ? mae / scale : mae;
prevActual = act;
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
double recalcError = 0, recalcScale = 0;
for (int k = 0; k < period; k++)
{
recalcError += errorBuffer[k];
recalcScale += scaleBuffer[k];
}
errorSum = recalcError;
scaleSum = recalcScale;
}
}
}
@@ -359,4 +348,4 @@ public sealed class Mase : AbstractBase
TSeries results = Batch(actual, predicted, period);
return (results, indicator);
}
}
}