Skip to content

fix: 6 critical time series bugs (#834-#839) + integration tests - #840

Merged
ooples merged 14 commits into
masterfrom
fix/time-series-bugs-834-839
Feb 11, 2026
Merged

ooples merged 14 commits into
masterfrom
fix/time-series-bugs-834-839

Conversation

@ooples

@ooples ooples commented Feb 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

Bug Details

Issue Model Root Cause Fix
#834 ARIMA Coefficients trained on differenced data but applied to raw data Override Forecast to difference/undifference properly
#835 SARIMA PrepareForecastFeatures creates vector too short for seasonal lags Override Forecast with seasonal-aware prediction logic
#836 ExpSmoothing Predict() resets level/trend/seasonal from initial values Save trained state, use it in Forecast override
#837 NBEATS ComputeBatchLoss indexes x[i,j] beyond column count Guard with Math.Min(LookbackWindow, x.Columns)
#838 Autoformer x.GetRow(i) returns 1 element for univariate data Extract lookback windows from y vector instead
#839 Informer Same as #838 + Convert.ToDouble() in hot loops + large defaults Fix TrainCore, use _numOps.ToDouble(), reduce defaults

Test plan

  • All 34 time series integration tests pass (28 existing + 6 new)
  • Build succeeds on both net10.0 and net471 with 0 errors
  • New tests verify train-and-forecast workflow (not just construction)
  • Verify ARIMA forecast with d=1 produces finite, reasonable predictions
  • Verify SARIMA with seasonal P=1, m=12 does not crash
  • Verify ExponentialSmoothing consecutive forecasts differ with trend enabled
  • Verify NBEATS training with single-column matrix does not throw
  • Verify Autoformer training with small data completes without crash
  • Verify Informer training completes in reasonable time

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Forecasting added to ARIMA, SARIMA, and Exponential Smoothing models returning predictions on the original scale.
  • Enhancements

    • Autoformer and Informer training revamped to use lookback-window sampling, batched/shuffled epochs, and convergence guards.
    • Exponential Smoothing now preserves end-of-training state for continued forecasting.
    • Feed-forward and tensor shape corrections for model internals.
  • Bug Fixes

    • Safer input handling for N-BEATS (pads short/univariate inputs).
  • Configuration

    • Informer defaults adjusted (smaller embeddings, fewer epochs).
  • Tests

    • New integration tests covering training and forecast workflows for time-series models.

ooples and others added 9 commits February 10, 2026 06:37
Override Forecast in ARIMAModel to properly handle differencing:
- Difference history before extracting lag features
- Apply AR/MA prediction on differenced values
- Undifference forecasts by cumulative integration
- Store last d original values during training for undifferencing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Override Forecast in SARIMAModel to:
- Apply both regular and seasonal differencing before prediction
- Extract enough lag values (Math.Max(p, P*m)) from differenced history
- Apply both AR and seasonal AR components
- Undifference with both regular and seasonal integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add trained state fields (level, trend, seasonal factors)
- Save end-of-training state by running through training data
- Override Forecast to start from trained state instead of resetting
- Update state between forecast steps using forecast values

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use Math.Min(LookbackWindow, x.Columns) when extracting input vectors
in ComputeBatchLoss, padding remaining values with zero when the input
matrix has fewer columns than the lookback window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…#838)

Override TrainCore to construct proper lookback windows from the target
vector y instead of using x.GetRow(i), which returns only 1 element for
univariate time series. For each sample index i, extract y[i-lookback:i]
as input and y[i:i+forecastHorizon] as target.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Override TrainCore to extract lookback windows from y vector instead
  of using x.GetRow(i) which returns 1 element for univariate data
- Replace all Convert.ToDouble() with _numOps.ToDouble() for proper
  generic type conversion
- Reduce default EmbeddingDim from 512 to 64 and Epochs from 100 to 10
- Add early termination when loss converges

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SARIMA's Forecast should always use its own prediction logic even when
d=0 and D=0, because PredictSingle requires Math.Max(p, P*m) lag values
but base.Forecast only provides LagOrder (typically 2) values via
PrepareForecastFeatures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ff1Weight tensor is stored as [ffDim, embDim] but MatrixMultiply
expects inner dimensions to match. Transpose ff1W and ff2W before
matmul in both encoder and decoder ProcessLayerAutodiff methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add integration tests that actually train and forecast (not just
construct) to catch the bugs fixed in issues #834-#839:
- ARIMA with d=1 differencing produces finite forecasts
- SARIMA with seasonal P=1, m=12 does not crash
- ExponentialSmoothing does not reset state between forecast steps
- NBEATS with single-column input does not index out of range
- Autoformer with small data does not crash with rank-1 tensors
- Informer training completes in reasonable time

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 10, 2026 12:28
@vercel

vercel Bot commented Feb 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aidotnet-playground-api Ready Ready Preview, Comment Feb 11, 2026 4:11am

@coderabbitai

coderabbitai Bot commented Feb 10, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds/changes forecasting and training flows: Informer and Autoformer training rebuilt to use lookback windows from targets with batched, shuffled sampling; ARIMA/SARIMA/ExponentialSmoothing gain Forecast implementations and undifferencing/state persistence; N-BEATS supports univariate inputs; Informer defaults reduced; integration tests added.

Changes

Cohort / File(s) Summary
Configuration
src/Models/Options/InformerOptions.cs
Changed default values: EmbeddingDim 512 → 64, Epochs 100 → 10.
Informer training
src/TimeSeries/InformerModel.cs
Rewrote training to sample lookback windows from y, precompute & shuffle sample indices, batch training with gradient accumulation; ComputeGradients now returns (gradients, prediction); numeric conversions replaced with _numOps.ToDouble; added epoch loss tracking and convergence stop. Review: batched gradient accumulation and new signature are broad internal changes — inspect gradient correctness and thread-safety.
Autoformer training & shapes
src/TimeSeries/AutoformerModel.cs
TrainCore now samples from y windows with shuffled indices; fixed FF tensor alignment by transposing weight matrices. Focus: verify all transposes maintain numerical equivalence and downstream shapes.
ARIMA forecasting
src/TimeSeries/ARIMAModel.cs
Added public override Vector<T> Forecast(Vector<T> history, int steps) to support differenced forecasting and private UndifferenceForecasts and ComputeIntegrationTailValues. Review: ensure future MA terms treated appropriately (currently assumed zero) and tail computation correctness.
SARIMA forecasting
src/TimeSeries/SARIMAModel.cs
Added public override Vector<T> Forecast(Vector<T> history, int steps) (twice — duplicate copy present) plus UndoRegularDifferencing and UndoSeasonalDifferencing. BLOCKING: duplicate Forecast method must be removed; verify undifferencing correctness and boundary handling.
Exponential Smoothing state & forecast
src/TimeSeries/ExponentialSmoothingModel.cs
Persisted trained state (_trainedLevel, _trainedTrend, _trainedSeasonalFactors, _trainingLength); added SaveTrainedState(Vector<T> y); public override Vector<T> Forecast(Vector<T> history, int steps) added; serialization updated. Review: serialized format/backward-compat fallbacks and correctness of replaying history for forecasting.
N-BEATS input guard
src/TimeSeries/NBEATSModel.cs
ComputeBatchLoss now supports univariate inputs by constructing/padding lookback from y when x.Columns < LookbackWindow. Check for edge cases with negative indices and zero-padding.
Integration tests
tests/AiDotNet.Tests/IntegrationTests/TimeSeries/TimeSeriesIntegrationTests.cs
Added multiple train-and-forecast integration tests and synthetic data helpers for ARIMA, SARIMA, ExponentialSmoothing, NBEATS, Autoformer, and Informer.
Numeric & shape fixes
src/TimeSeries/...
Replaced many Convert.ToDouble calls with _numOps.ToDouble and adjusted tensor transposes in FF layers and attention/activation/normalization code. Review: confirm no loss of generic numeric semantics and verify performance impact.
Other concerns
src/TimeSeries/SARIMAModel.cs, src/TimeSeries/ARIMAModel.cs
Multiple new forecasting methods assume MA/SMA future terms are zero — validate model assumptions and document limitations. All added forecasting logic must be checked for off-by-one and history-length requirements. BLOCKING: remove duplicate SARIMA Forecast and audit all new Forecast overloads for public API stability.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Trainer as Trainer
    participant DataPrep as DataPrep
    participant Batcher as Batcher
    participant Model as Model
    participant Optim as Optimizer

    Trainer->>DataPrep: compute valid sampleIndices from y (lookback windows)
    Trainer->>Trainer: shuffle sampleIndices each epoch
    loop each epoch
        Trainer->>Batcher: request next batch of indices
        Batcher->>DataPrep: build batch inputs/targets from y windows
        Batcher->>Model: forward pass -> predictions
        Model->>Model: ComputeGradients(batch) -> (gradients, predictions)
        Model->>Optim: AccumulateGradients(gradients)
        Batcher->>Trainer: report batch loss (from predictions)
        alt end of batch
            Trainer->>Optim: ApplyGradients()
            Trainer->>Optim: ResetGradientAccumulators()
        end
    end
    Trainer->>Model: SaveTrainedState / finalize parameters
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

feature

Poem

🧩 Windows shuffled, gradients pooled in line,
Differenced forecasts stitched back into time.
Defaults trimmed light, missing guards bolstered near,
Tests call "run" — please fix the duplicate SARIMA here.
🚩 Audit tails, transposes, and undifference paths clear.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing 6 critical time series bugs and adding integration tests, with specific issue references (#834-#839).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/time-series-bugs-834-839

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets a set of correctness and stability fixes across multiple time-series models (ARIMA/SARIMA/Exponential Smoothing/N-BEATS/Autoformer/Informer), and adds integration tests that exercise train→forecast flows to prevent regressions.

Changes:

  • Add/override forecasting/training logic in several models to fix crashes/hangs and handle univariate inputs more robustly.
  • Improve Informer hot-loop numeric conversions and reduce default training sizes.
  • Add 6 integration tests covering train-and-forecast for the fixed models.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
tests/AiDotNet.Tests/IntegrationTests/TimeSeries/TimeSeriesIntegrationTests.cs Adds train-and-forecast integration tests for multiple models.
src/TimeSeries/ARIMAModel.cs Adds differencing-aware Forecast and stores tail values for undifferencing.
src/TimeSeries/SARIMAModel.cs Adds seasonal-aware Forecast that applies/undoes differencing.
src/TimeSeries/ExponentialSmoothingModel.cs Persists end-of-training state and overrides Forecast to avoid state resets.
src/TimeSeries/NBEATSModel.cs Prevents IndexOutOfRange in batch-loss input extraction for single-column matrices.
src/TimeSeries/AutoformerModel.cs Builds lookback windows from y and fixes FF matmul dimensions in autodiff path.
src/TimeSeries/InformerModel.cs Extracts training windows from y, adds early stopping, and replaces Convert.ToDouble in hot paths.
src/Models/Options/InformerOptions.cs Reduces default EmbeddingDim and Epochs to speed training.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/TimeSeries/AutoformerModel.cs Outdated
Comment thread src/TimeSeries/InformerModel.cs
Comment thread src/TimeSeries/ARIMAModel.cs Outdated
Comment thread src/TimeSeries/ARIMAModel.cs Outdated
Comment thread src/TimeSeries/InformerModel.cs Outdated
Comment thread src/TimeSeries/InformerModel.cs
Comment thread src/TimeSeries/AutoformerModel.cs Outdated
Comment thread src/TimeSeries/SARIMAModel.cs Outdated
Comment thread src/TimeSeries/ExponentialSmoothingModel.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/TimeSeries/ExponentialSmoothingModel.cs (1)

133-179: ⚠️ Potential issue | 🔴 Critical

Persist trained forecast state in serialization/reset (blocking).

Forecast now depends on _trainedLevel, _trainedTrend, and _trainedSeasonalFactors, but these are not serialized/deserialized or cleared in Reset(). A reloaded or reset model will forecast from zeros/stale state. Persist them (or recompute on load) to keep forecasts correct.
As per coding guidelines, incomplete features are blocking issues.

💾 Persist trained-state fields
 protected override void SerializeCore(BinaryWriter writer)
 {
     writer.Write(Convert.ToDouble(_alpha));
     writer.Write(Convert.ToDouble(_beta));
     writer.Write(Convert.ToDouble(_gamma));
     writer.Write(_initialValues.Length);
@@
     foreach (var value in _initialValues)
     {
         writer.Write(Convert.ToDouble(value));
     }
+
+    writer.Write(Convert.ToDouble(_trainedLevel));
+    writer.Write(Convert.ToDouble(_trainedTrend));
+    writer.Write(_trainedSeasonalFactors.Length);
+    foreach (var value in _trainedSeasonalFactors)
+    {
+        writer.Write(Convert.ToDouble(value));
+    }
 }
 
 protected override void DeserializeCore(BinaryReader reader)
 {
@@
     for (int i = 0; i < initialValuesLength; i++)
     {
         _initialValues[i] = NumOps.FromDouble(reader.ReadDouble());
     }
+
+    _trainedLevel = NumOps.FromDouble(reader.ReadDouble());
+    _trainedTrend = NumOps.FromDouble(reader.ReadDouble());
+    int trainedSeasonalLength = reader.ReadInt32();
+    _trainedSeasonalFactors = new Vector<T>(trainedSeasonalLength);
+    for (int i = 0; i < trainedSeasonalLength; i++)
+    {
+        _trainedSeasonalFactors[i] = NumOps.FromDouble(reader.ReadDouble());
+    }
 }
 
 public override void Reset()
 {
     _alpha = NumOps.Zero;
     _beta = NumOps.Zero;
     _gamma = NumOps.Zero;
     _initialValues = Vector<T>.Empty();
+    _trainedLevel = NumOps.Zero;
+    _trainedTrend = NumOps.Zero;
+    _trainedSeasonalFactors = Vector<T>.Empty();
 }
src/TimeSeries/ARIMAModel.cs (1)

94-125: ⚠️ Potential issue | 🔴 Critical

Use provided history for undifferencing + validate length (blocking).

Forecast ignores the supplied history for undifferencing and relies on _lastOriginalValues captured at training time. This produces incorrect scale when history differs, and a deserialized model (empty _lastOriginalValues) will throw when building tail values. Derive tails from history and validate length; then remove or serialize _lastOriginalValues.
As per coding guidelines, missing validation of external inputs is a blocking issue.

🧮 Use history tail for undifferencing
 public override Vector<T> Forecast(Vector<T> history, int steps)
 {
@@
     int d = _arimaOptions.D;
@@
+    if (history.Length <= d)
+    {
+        throw new ArgumentException(
+            $"History length ({history.Length}) is too short for differencing order {d}.",
+            nameof(history));
+    }
@@
-    var tempTail = new List<T>();
-    for (int i = 0; i < _lastOriginalValues.Length; i++)
-    {
-        tempTail.Add(_lastOriginalValues[i]);
-    }
+    var tempTail = new List<T>(d);
+    for (int i = history.Length - d; i < history.Length; i++)
+    {
+        tempTail.Add(history[i]);
+    }

Also applies to: 392-404, 510-639

🤖 Fix all issues with AI agents
In `@src/TimeSeries/AutoformerModel.cs`:
- Around line 355-364: The code currently returns early when there isn’t enough
data (endIdx <= startIdx); instead, in AutoformerModel replace the silent return
with a thrown exception (e.g., InvalidOperationException) that clearly states
the minimum required series length and the current length. Compute
requiredMinimum = lookback + forecastHorizon + 1 (since y.Length must be >
lookback + forecastHorizon), and throw with a message like “Insufficient data:
need at least {requiredMinimum} observations (lookback {lookback} +
forecastHorizon {forecastHorizon} + 1) but got {y.Length}” so callers can fail
fast and correct configuration/data; update the block using the existing
variables startIdx, endIdx, lookback, forecastHorizon, and y.Length.

In `@src/TimeSeries/ExponentialSmoothingModel.cs`:
- Around line 910-1001: Forecast currently starts from the stored trained state
but never applies the supplied history, and uses history.Length to guess the
seasonal index which misaligns seasons; fix Forecast by first replaying the
provided history values into the model state (level, trend, seasonalFactors)
using the same update equations used during training so the seasonal index and
state reflect the actual recent observations, then proceed with the existing
forecast loop; reference the Forecast method and fields _trainedLevel,
_trainedTrend, _trainedSeasonalFactors, Options.SeasonalPeriod, _alpha, _beta,
_gamma and reuse the same deseasonalization/level/trend/seasonal update logic to
update state for each element in history before computing forecasts.

In `@src/TimeSeries/InformerModel.cs`:
- Around line 218-230: In TrainCore remove the unused local declaration "int
forecastHorizon = _options.ForecastHorizon;" since it's never referenced; keep
the existing lookback variable and logic (validIndices loop) intact and simply
delete that dead variable to avoid confusion and compiler warnings.

In `@src/TimeSeries/SARIMAModel.cs`:
- Around line 699-852: The Forecast method calls ApplyDifferencing and
seasonal/regular undifferencing without validating that history is long enough,
which can cause negative/underflow allocations when history.Length < _m * _D or
< _d; add the same minimum-history guard used in TrainCore before calling
ApplyDifferencing: validate history != null and then check history.Length >=
Math.Max( _d + 1, _m * _D + 1 ) (or the exact condition used in TrainCore) and
throw ArgumentException/InvalidOperation if too short; update Forecast
(reference function name Forecast, call to ApplyDifferencing, and fields _m, _D,
_d) to perform this pre-check so subsequent differencing and vector allocations
are safe.

In
`@tests/AiDotNet.Tests/IntegrationTests/TimeSeries/TimeSeriesIntegrationTests.cs`:
- Around line 532-553: The test InformerModel_Train_CompletesInReasonableTime
currently only checks for exceptions and can hang; run the training call
model.Train(x, y) inside a Task (e.g., Task.Run) and assert it completes within
a fixed timeout (for example 5 seconds) by using Task.Wait(timeout) or
Task.WhenAny and then Assert.True that the task completed in time and
Assert.Null for any exception from the task; update the test method
InformerModel_Train_CompletesInReasonableTime to start the training task, await
or wait with a timeout, fail if the timeout elapses, and propagate/assert any
exception thrown by model.Train so the test both verifies it finishes and
doesn't throw.

Comment thread src/TimeSeries/AutoformerModel.cs
Comment thread src/TimeSeries/ExponentialSmoothingModel.cs
Comment thread src/TimeSeries/InformerModel.cs
Comment thread src/TimeSeries/SARIMAModel.cs
Comment thread src/TimeSeries/ARIMAModel.cs Fixed
Comment thread src/TimeSeries/InformerModel.cs Fixed
Comment thread src/TimeSeries/SARIMAModel.cs Fixed
Comment thread src/TimeSeries/ARIMAModel.cs Fixed
Comment on lines +707 to +852
{
if (!IsTrained)
{
throw new InvalidOperationException("The model must be trained before forecasting.");
}

if (history == null)
{
throw new ArgumentNullException(nameof(history), "History cannot be null.");
}

if (steps <= 0)
{
throw new ArgumentException("Number of forecast steps must be positive.", nameof(steps));
}

// Apply the same differencing as in TrainCore
Vector<T> diffHistory = ApplyDifferencing(history);

// Build a working list of differenced values
int maxLag = Math.Max(_p, _P * _m);
List<T> extendedDiff = new List<T>(diffHistory.Length + steps);
for (int i = 0; i < diffHistory.Length; i++)
{
extendedDiff.Add(diffHistory[i]);
}

// Generate forecasts on the differenced scale
Vector<T> diffForecasts = new Vector<T>(steps);
for (int step = 0; step < steps; step++)
{
T prediction = _constant;

// Non-seasonal AR component
for (int j = 0; j < _p && j < extendedDiff.Count; j++)
{
prediction = NumOps.Add(prediction, NumOps.Multiply(
_arCoefficients[j], extendedDiff[extendedDiff.Count - 1 - j]));
}

// Seasonal AR component
for (int j = 0; j < _P; j++)
{
int lagIdx = extendedDiff.Count - (j + 1) * _m;
if (lagIdx >= 0)
{
prediction = NumOps.Add(prediction, NumOps.Multiply(
_sarCoefficients[j], extendedDiff[lagIdx]));
}
}

// MA/SMA components are assumed zero for future predictions

diffForecasts[step] = prediction;
extendedDiff.Add(prediction);
}

// Undifference: first undo regular differencing, then seasonal differencing
// (reverse order of ApplyDifferencing which does seasonal first, then regular)

// Undo regular (non-seasonal) differencing d times
var currentForecasts = new List<T>(steps);
for (int i = 0; i < steps; i++)
{
currentForecasts.Add(diffForecasts[i]);
}

// We need the tail of history at each intermediate differencing level
// ApplyDifferencing does: seasonal D times -> then regular d times
// So to undo: first undo regular d times, then undo seasonal D times

// Compute the series after seasonal differencing but before regular differencing
Vector<T> afterSeasonalDiff = history;
for (int i = 0; i < _D; i++)
{
afterSeasonalDiff = SeasonalDifference(afterSeasonalDiff, _m);
}

// Undo regular differencing d times
// Each level needs the last value of the series at that level
Vector<T> tempSeries = afterSeasonalDiff;
var regularTailValues = new T[_d];
for (int level = 0; level < _d; level++)
{
regularTailValues[level] = tempSeries[tempSeries.Length - 1];
// Difference for next level
Vector<T> nextLevel = new Vector<T>(tempSeries.Length - 1);
for (int i = 1; i < tempSeries.Length; i++)
{
nextLevel[i - 1] = NumOps.Subtract(tempSeries[i], tempSeries[i - 1]);
}
tempSeries = nextLevel;
}

// Undo regular differencing in reverse
for (int level = _d - 1; level >= 0; level--)
{
T lastVal = regularTailValues[level];
for (int i = 0; i < currentForecasts.Count; i++)
{
T undiff = NumOps.Add(currentForecasts[i], lastVal);
currentForecasts[i] = undiff;
lastVal = undiff;
}
}

// Undo seasonal differencing D times
// For seasonal undifferencing: forecast[i] = diffForecast[i] + value[i - m]
// We need the last m values from the previous level for each D
for (int level = 0; level < _D; level++)
{
// Compute the series at this seasonal differencing level
Vector<T> seriesAtLevel = history;
for (int d2 = 0; d2 < _D - 1 - level; d2++)
{
seriesAtLevel = SeasonalDifference(seriesAtLevel, _m);
}

// Get the last m values from this series
var seasonalTail = new List<T>();
for (int i = Math.Max(0, seriesAtLevel.Length - _m); i < seriesAtLevel.Length; i++)
{
seasonalTail.Add(seriesAtLevel[i]);
}

// Undo seasonal differencing: forecast[i] = diffForecast[i] + value[i - m]
// where value[i-m] comes from either the tail or previous forecasts
var combined = new List<T>(seasonalTail);
for (int i = 0; i < currentForecasts.Count; i++)
{
int refIdx = combined.Count - _m;
T refVal = refIdx >= 0 ? combined[refIdx] : NumOps.Zero;
T undiff = NumOps.Add(currentForecasts[i], refVal);
currentForecasts[i] = undiff;
combined.Add(undiff);
}
}

Vector<T> result = new Vector<T>(steps);
for (int i = 0; i < steps; i++)
{
result[i] = currentForecasts[i];
}

return result;
}

Check notice

Code scanning / CodeQL

Block with too many statements Note

Block with too many statements (4 complex statements in the block).

Copilot Autofix

AI 8 months ago

In general, to fix "block with too many statements" in a method, you factor coherent sub-tasks into separate private methods. The top-level method then orchestrates the flow by calling these helpers, reducing the number of complex statements (loops/conditionals) in its own body while preserving the behavior.

For this Forecast method in src/TimeSeries/SARIMAModel.cs, the best approach is to extract the two main internal phases into private helpers:

  1. A helper that generates the forecasts on the differenced scale and returns diffForecasts:

    • Inputs: Vector<T> diffHistory, int steps.
    • Internal: build extendedDiff, run the AR and seasonal AR contributions in loops, assume MA/SMA = 0, and fill a Vector<T> of differenced forecasts.
    • Output: Vector<T> diffForecasts.
  2. A helper that undoes differencing and builds the final result vector:

    • Inputs: Vector<T> diffForecasts, Vector<T> history, int steps.
    • Internal: copy diffForecasts into a List<T>, call existing UndoRegularDifferencing and UndoSeasonalDifferencing, then copy into a Vector<T> and return it.
    • Output: Vector<T> result.

The Forecast method will keep its parameter checks and the call to ApplyDifferencing, then delegate to these two helpers. This reduces the number of loops and conditionals in Forecast’s block, satisfying the rule without any behavior change. No new imports or external dependencies are required; only new private methods within the same class are added.

Concretely:

  • In SARIMAModel<T> in src/TimeSeries/SARIMAModel.cs, above the current Forecast method, insert two new private methods: GenerateDifferencedForecasts and BuildFinalForecastsFromDifferenced.
  • Replace the body of Forecast (lines 707–790) so that:
    • It performs the same argument validation as now.
    • Calls ApplyDifferencing(history) as now.
    • Calls GenerateDifferencedForecasts(diffHistory, steps) to get diffForecasts.
    • Calls BuildFinalForecastsFromDifferenced(diffForecasts, history, steps) and returns its result.
  • Remove the now-duplicated logic (the loops that build extendedDiff, compute diffForecasts, manage currentForecasts, and copy into result) from Forecast, since that logic lives in the helpers.
Suggested changeset 1
src/TimeSeries/SARIMAModel.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/TimeSeries/SARIMAModel.cs b/src/TimeSeries/SARIMAModel.cs
--- a/src/TimeSeries/SARIMAModel.cs
+++ b/src/TimeSeries/SARIMAModel.cs
@@ -732,6 +732,21 @@
         // Apply the same differencing as in TrainCore
         Vector<T> diffHistory = ApplyDifferencing(history);
 
+        // Generate forecasts on the differenced scale
+        Vector<T> diffForecasts = GenerateDifferencedForecasts(diffHistory, steps);
+
+        // Undo differencing and build the final result
+        return BuildFinalForecastsFromDifferenced(diffForecasts, history, steps);
+    }
+
+    /// <summary>
+    /// Generates future values on the differenced scale using the trained SARIMA model.
+    /// </summary>
+    /// <param name="diffHistory">The historical time series values after differencing.</param>
+    /// <param name="steps">The number of future steps to forecast.</param>
+    /// <returns>A vector of forecasted values on the differenced scale.</returns>
+    private Vector<T> GenerateDifferencedForecasts(Vector<T> diffHistory, int steps)
+    {
         // Build a working list of differenced values
         List<T> extendedDiff = new List<T>(diffHistory.Length + steps);
         for (int i = 0; i < diffHistory.Length; i++)
@@ -769,6 +784,18 @@
             extendedDiff.Add(prediction);
         }
 
+        return diffForecasts;
+    }
+
+    /// <summary>
+    /// Undoes differencing and builds the final forecast vector on the original scale.
+    /// </summary>
+    /// <param name="diffForecasts">Forecasts on the differenced scale.</param>
+    /// <param name="history">The original historical time series values.</param>
+    /// <param name="steps">The number of future steps to forecast.</param>
+    /// <returns>A vector of forecasted values on the original scale.</returns>
+    private Vector<T> BuildFinalForecastsFromDifferenced(Vector<T> diffForecasts, Vector<T> history, int steps)
+    {
         // Undifference: first undo regular differencing, then seasonal differencing
         // (reverse order of ApplyDifferencing which does seasonal first, then regular)
         var currentForecasts = new List<T>(steps);
EOF
@@ -732,6 +732,21 @@
// Apply the same differencing as in TrainCore
Vector<T> diffHistory = ApplyDifferencing(history);

// Generate forecasts on the differenced scale
Vector<T> diffForecasts = GenerateDifferencedForecasts(diffHistory, steps);

// Undo differencing and build the final result
return BuildFinalForecastsFromDifferenced(diffForecasts, history, steps);
}

/// <summary>
/// Generates future values on the differenced scale using the trained SARIMA model.
/// </summary>
/// <param name="diffHistory">The historical time series values after differencing.</param>
/// <param name="steps">The number of future steps to forecast.</param>
/// <returns>A vector of forecasted values on the differenced scale.</returns>
private Vector<T> GenerateDifferencedForecasts(Vector<T> diffHistory, int steps)
{
// Build a working list of differenced values
List<T> extendedDiff = new List<T>(diffHistory.Length + steps);
for (int i = 0; i < diffHistory.Length; i++)
@@ -769,6 +784,18 @@
extendedDiff.Add(prediction);
}

return diffForecasts;
}

/// <summary>
/// Undoes differencing and builds the final forecast vector on the original scale.
/// </summary>
/// <param name="diffForecasts">Forecasts on the differenced scale.</param>
/// <param name="history">The original historical time series values.</param>
/// <param name="steps">The number of future steps to forecast.</param>
/// <returns>A vector of forecasted values on the original scale.</returns>
private Vector<T> BuildFinalForecastsFromDifferenced(Vector<T> diffForecasts, Vector<T> history, int steps)
{
// Undifference: first undo regular differencing, then seasonal differencing
// (reverse order of ApplyDifferencing which does seasonal first, then regular)
var currentForecasts = new List<T>(steps);
Copilot is powered by AI and may make mistakes. Always verify output.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
11.1% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

- ARIMAModel: remove unused variable, extract UndifferenceForecasts helper,
  derive tail values from history parameter instead of _lastOriginalValues
- InformerModel: remove unused variable, throw on insufficient data instead
  of silent return, return prediction from ComputeGradients to avoid
  redundant forward pass
- AutoformerModel: throw on insufficient data, fix off-by-one in sample
  range calculation
- SARIMAModel: remove unused variable, add minimum history length validation
- ExponentialSmoothingModel: use EsOptions.UseTrend consistently instead of
  Options.IncludeTrend, store _trainingLength for correct seasonal index
  alignment, serialize/deserialize trained state fields
- Fix test doc comment that mentioned noise term not present in code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/TimeSeries/AutoformerModel.cs (1)

344-406: ⚠️ Potential issue | 🟡 Minor

Fix off‑by‑one in insufficient‑data error message.
The logic allows a single sample when y.Length == lookback + forecastHorizon, but the message says + 1. That can mislead users when diagnosing input size issues.

🛠️ Suggested fix
-                $"Require at least {lookback + forecastHorizon + 1} points, got {y.Length}.",
+                $"Require at least {lookback + forecastHorizon} points, got {y.Length}.",
src/TimeSeries/ExponentialSmoothingModel.cs (1)

701-707: ⚠️ Potential issue | 🟠 Major

Reset should clear the new trained-state fields.
Leaving trained values after Reset can leak stale state into serialization or later forecasts.

🧹 Suggested reset fix
 public override void Reset()
 {
     _alpha = NumOps.Zero;
     _beta = NumOps.Zero;
     _gamma = NumOps.Zero;
     _initialValues = Vector<T>.Empty();
+    _trainedLevel = NumOps.Zero;
+    _trainedTrend = NumOps.Zero;
+    _trainedSeasonalFactors = Vector<T>.Empty();
+    _trainingLength = 0;
 }
🤖 Fix all issues with AI agents
In `@src/TimeSeries/ARIMAModel.cs`:
- Around line 510-633: The Forecast method must validate that the provided
history is long enough for differencing: before calling
TimeSeriesHelper<T>.DifferenceSeries and before using _arimaOptions.D, check
that history.Length >= d + 1 (where d = _arimaOptions.D) and throw a clear
ArgumentException/ArgumentNullException with a descriptive message if not; add
this precondition near the start of Forecast (after the null and steps checks)
so UndifferenceForecasts and its tail extraction logic cannot index empty or
too-short history.

Comment thread src/TimeSeries/ARIMAModel.cs
Comment thread src/TimeSeries/ARIMAModel.cs Fixed
Add validation that history.Length >= d + 1 before attempting to
difference the series, preventing runtime failures when history is
too short for the configured differencing order.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/TimeSeries/ARIMAModel.cs`:
- Around line 94-99: Remove the dead field _lastOriginalValues: delete its
private declaration, remove its initialization where the model is constructed,
and remove the population logic inside TrainCore that assigns values to
_lastOriginalValues; also remove any using references and ensure
UndifferenceForecasts and serialization/deserialization remain unchanged (no
reliance on _lastOriginalValues) and that the project compiles and tests pass
after removing those assignments and the field.

Comment thread src/TimeSeries/ARIMAModel.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/AiDotNet.Tests/IntegrationTests/TimeSeries/TimeSeriesIntegrationTests.cs Outdated
Comment thread src/TimeSeries/ExponentialSmoothingModel.cs Outdated
Comment thread src/TimeSeries/ExponentialSmoothingModel.cs Outdated
Comment thread src/TimeSeries/NBEATSModel.cs Outdated
Comment thread src/TimeSeries/ARIMAModel.cs Outdated
- ARIMAModel: remove dead _lastOriginalValues field (written but never read)
- ExponentialSmoothingModel: add backward-compatible deserialization for
  trained state fields, replay smoothing updates over provided history in
  Forecast instead of ignoring it
- NBEATSModel: construct lookback windows from y values for univariate
  input instead of silently padding with zeros
- Tests: make InformerModel timeout test async, strengthen ExponentialSmoothing
  forecast assertion with fixed params and monotonicity check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/TimeSeries/ExponentialSmoothingModel.cs`:
- Around line 670-690: When legacy serialized payloads lack the trained-state
block, do not zero-out forecasting state; instead seed it from the model's
stored initial values so seasonality and baseline are preserved. In the
deserialization branch that currently sets _trainedLevel, _trainedTrend,
_trainedSeasonalFactors, and _trainingLength to zeros (the else after the
reader.BaseStream.Position check), initialize _trainedLevel and _trainedTrend
from the appropriate entries in _initialValues (or computed from them), set
_trainedSeasonalFactors to a Vector<T> populated from the last (or appropriate)
seasonal slice of _initialValues with length matching the model's seasonal
period, and set _trainingLength to _initialValues.Length (or the correct count
used during training) so legacy models continue to forecast with seasonality and
a valid baseline.

Comment thread src/TimeSeries/ExponentialSmoothingModel.cs Outdated
- Seed trained state from initial values in ExponentialSmoothingModel
  legacy deserialization so backward-compat forecasts remain correct
- Extract ComputeIntegrationTailValues helper in ARIMAModel to reduce
  UndifferenceForecasts complexity
- Extract UndoRegularDifferencing and UndoSeasonalDifferencing helpers
  in SARIMAModel to reduce Forecast method complexity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/TimeSeries/ExponentialSmoothingModel.cs`:
- Around line 669-702: The current deserialization uses
reader.BaseStream.Position < reader.BaseStream.Length to detect presence of
newer trained-state fields which is brittle; change serialization to write an
explicit version marker (e.g., an int version) at the start of the model blob
and update the deserialization to read that version and conditionally read
_trainedLevel, _trainedTrend, _trainedSeasonalFactors and _trainingLength only
when version >= the new version number. Update the corresponding serialize/write
routine to emit the version marker and bump the version when adding fields, and
update the deserialize/read logic (the code block that uses reader and sets
_trainedLevel/_trainedTrend/_trainedSeasonalFactors/_trainingLength and
references Options.SeasonalPeriod and _initialValues) to use a version check
instead of Position < Length.

Comment thread src/TimeSeries/ExponentialSmoothingModel.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/TimeSeries/ExponentialSmoothingModel.cs Outdated
Comment thread src/TimeSeries/ExponentialSmoothingModel.cs Outdated
Comment thread tests/AiDotNet.Tests/IntegrationTests/TimeSeries/TimeSeriesIntegrationTests.cs Outdated
…r test

- Add serialization versioning with backward-compatible format detection
- Fix forecast double-replay by initializing from initial values not trained state
- Fix seasonal factor update formula to use level consistently with training
- Replace informer hang test timeout attribute with explicit cancellation token

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 14b39f6e Deployed Feb 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants