Skip to content

fix: add SetModel method to IOptimizer to fix NullReferenceException in InitializeRandomSolution - #823

Merged
ooples merged 7 commits into
masterfrom
fix/optimizer-model-association
Feb 6, 2026
Merged

ooples merged 7 commits into
masterfrom
fix/optimizer-model-association

Conversation

@ooples

@ooples ooples commented Feb 6, 2026 •

Copy link
Copy Markdown
Owner

Summary

Root Cause

The optimizer's _model field was never being set after configuration. The model was only passed during optimizer construction, but many constructors passed null for the model parameter. When Optimize() was called, InitializeRandomSolution() would try to access _model.ParameterCount and throw a NullReferenceException.

Changes

  1. IOptimizer interface - Added SetModel(IFullModel<T, TInput, TOutput> model) method
  2. OptimizerBase - Implemented SetModel to set the _model field
  3. ShardedOptimizerBase - Implemented SetModel to delegate to wrapped optimizer
  4. AiModelBuilder - Added SetModel() calls before Optimize() in all 4 training paths:
    • Regular training path
    • Hyperparameter optimization trial
    • Knowledge distillation fallback
    • Deep ensemble member optimizer

Test plan

  • Build passes for net10.0 and net471
  • All existing optimizer and time series tests pass (811 tests)
  • TDAB integration tests pass after fix

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Exposed a SetModel API across optimizers to allow explicit model assignment before training.
  • Bug Fixes

    • Ensured optimizers are bound to models before training, fallback, and ensemble paths to prevent training/runtime issues.
    • Fixed ARIMA prediction handling for absent AR or MA terms to avoid out-of-range writes.
  • Chores

    • Added review/architecture configuration and updated example kernel parameter naming.

…e in initializerandomsolution

The optimizer's _model field was null when InitializeRandomSolution accessed
model.ParameterCount because the model was never set on the optimizer after
configuration.

Changes:
- Add SetModel(IFullModel) method to IOptimizer interface
- Implement SetModel in OptimizerBase to set the _model field
- Implement SetModel in ShardedOptimizerBase to delegate to wrapped optimizer
- Call SetModel in AiModelBuilder before Optimize() in all training paths:
  - Regular training path (line 2107)
  - Hyperparameter optimization trial (line 1940)
  - Knowledge distillation fallback (line 5269)
  - Deep ensemble member optimizer (line 6793)

This fixes Issue #225: [Fix] Kill Time Series Stubs & Implement Real Training
by ensuring the model is always available to the optimizer before training.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 6, 2026 13:52
@vercel

vercel Bot commented Feb 6, 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 6, 2026 6:07pm

@coderabbitai

coderabbitai Bot commented Feb 6, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds IOptimizer.SetModel and implementations in OptimizerBase and ShardedOptimizerBase; updates AiModelBuilder to call SetModel after Reset and before Optimize across training, KD-fallback, ensemble, and deep-ensemble paths; adds no-op SetModel stubs in tests; fixes ARIMA Predict index/length handling; adds .coderabbit.yaml and a small playground rename.

Changes

Cohort / File(s) Summary
Optimizer Interface & Base
src/Interfaces/IOptimizer.cs, src/Optimizers/OptimizerBase.cs
Adds void SetModel(IFullModel<..., ..., ...> model) to the optimizer interface and a concrete implementation in OptimizerBase that stores the model, exposes RequireModel(), and calls OnModelChanged(...).
Distributed Optimizer Delegation
src/DistributedTraining/ShardedOptimizerBase.cs
Adds SetModel(...) that delegates to WrappedOptimizer.SetModel(...) so sharded/wrapped optimizers propagate model binding.
AiModelBuilder call sites
src/AiModelBuilder.cs
Calls to SetModel(model) inserted after Reset() and/or before Optimize() across supervised/trial, regular training, KD-fallback, ensemble, and deep-ensemble member construction flows to ensure optimizer→model binding.
Test optimizers (no-op SetModel)
tests/**
tests/AiDotNet.Serving.Tests/SinglePassTestOptimizer.cs, tests/AiDotNet.Tests/.../Federated*.cs, tests/.../TestOptimizers.cs, tests/.../MetaLearningTestInfrastructure.cs, tests/.../CheckpointManagerTests.cs
Adds no-op SetModel(...) implementations to multiple test optimizer classes and mocks to satisfy the new interface requirement; behavior unchanged.
Time Series Predict Fixes
src/TimeSeries/ARIMAModel.cs
Fixes AR/MA vector lengths to use coefficient lengths and guards writes to last-observed/last-error arrays when P or Q is zero.
Playground minor rename
src/AiDotNet.Playground/Services/ExampleService.cs
Renames Matérn kernel named parameter from length to lengthScale at call site.
Repo review config
.coderabbit.yaml
Adds review/architecture guidance configuration (non-runtime).

Sequence Diagram(s)

sequenceDiagram
  participant Builder as AiModelBuilder
  participant Optim as IOptimizer
  participant Model as IFullModel
  participant Wrapped as Sharded/WrappedOptimizer

  Builder->>Optim: Reset()
  Note right of Optim: internal state cleared
  Builder->>Optim: SetModel(Model)
  Note right of Optim: optimizer bound to model
  Builder->>Optim: Optimize(...)
  Optim->>Model: Read/Modify parameters

  %% Sharded/wrapped delegation
  Builder->>Wrapped: SetModel(Model)
  Wrapped->>Optim: WrappedOptimizer.SetModel(Model)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

I bind the optimizers, one by one,
After Reset the model dance begun.
Ensembles and students line the row,
Parameters found — the updates flow.
Tests nod gently: "No-op, we know."

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.14% 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 PR title accurately and specifically describes the primary change: adding a SetModel method to IOptimizer to resolve a NullReferenceException issue.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/optimizer-model-association

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 addresses a NullReferenceException in OptimizerBase.InitializeRandomSolution by ensuring optimizers have their target model assigned before optimization runs, primarily through a new SetModel(...) API on IOptimizer and call sites in AiModelBuilder.

Changes:

  • Added SetModel(IFullModel<...> model) to IOptimizer and implemented it in OptimizerBase.
  • Implemented SetModel in ShardedOptimizerBase by delegating to the wrapped optimizer.
  • Updated AiModelBuilder training flows to call SetModel(...) before Optimize().

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/Optimizers/OptimizerBase.cs Adds SetModel(...) implementation to assign the internal _model.
src/Interfaces/IOptimizer.cs Extends the optimizer public interface with a new SetModel(...) method.
src/DistributedTraining/ShardedOptimizerBase.cs Delegates SetModel(...) to the wrapped optimizer for sharded/distributed cases.
src/AiModelBuilder.cs Ensures the model is set on optimizers before optimization across several training paths.

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

Comment thread src/AiModelBuilder.cs Outdated
Comment thread src/AiModelBuilder.cs Outdated
Comment thread src/AiModelBuilder.cs Outdated
Comment thread src/Interfaces/IOptimizer.cs
Comment thread src/Optimizers/OptimizerBase.cs Outdated

@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/AiModelBuilder.cs`:
- Around line 5268-5270: The optimizer's internal state must be cleared before
running the fallback training to avoid carrying over partial KD updates: before
calling optimizer.SetModel(studentModel) / optimizer.Optimize(...) reset the
optimizer state (e.g., call optimizer.ResetState() or reinitialize a fresh
optimizer instance and assign it to the same variable) so Optimize runs from a
clean state; if ResetState() does not exist, construct a new optimizer of the
same type (or use a provided factory/CloneWithoutState) and then call
SetModel(studentModel) and Optimize with OptimizerHelper<T, TInput,
TOutput>.CreateOptimizationInputData(...).
🧹 Nitpick comments (1)
src/Optimizers/OptimizerBase.cs (1)

1220-1245: Consider adding a guard clause for _model at the method entry.

InitializeRandomSolution(TInput) accesses _model! with the null-forgiving operator at lines 1245, 1325, 1337, and 1354. If a derived class or a new code path calls this before SetModel, the original NRE will resurface with no clear message. A single guard at the top converts a cryptic NRE into an actionable InvalidOperationException.

🛡️ Proposed guard clause
 protected virtual IFullModel<T, TInput, TOutput> InitializeRandomSolution(TInput trainingData)
 {
     if (trainingData == null) throw new ArgumentNullException(nameof(trainingData));
+    if (_model == null)
+        throw new InvalidOperationException(
+            "Model has not been set. Call SetModel() before optimization.");

     // Compute lower and upper bounds from the training data

Comment thread src/AiModelBuilder.cs Outdated
- Use _arCoefficients.Length instead of LagOrder for lastObservedValues
  to ensure dot product vectors have matching lengths
- Guard lastObservedValues[0] access when P=0 (pure MA model)
- Guard lastErrors[0] access when Q=0 (pure AR model)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add SetModel implementation to all test optimizer classes:
  - FederatedDeterministicDeltaOptimizer
  - FederatedNoOpOptimizer
  - SinglePassTestOptimizer
  - ResetTrackingOptimizer
  - MockOptimizer
  - PassthroughOptimizer
  - SingleStepTrainOptimizer
  - DeterministicNeuralNetworkParameterOptimizer

- Address PR review comments:
  - Improve SetModel comments in AiModelBuilder to clarify purpose
  - Add Reset() before SetModel in KD fallback path for consistency
  - Make SetModel virtual in OptimizerBase with OnModelChanged hook
  - Add breaking change documentation to IOptimizer.SetModel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Reorder HPO objective function: apply trial hyperparameters BEFORE Reset()
  so adaptive state is initialized from new hyperparameters
- Improve SetModel comment wording to not imply Reset() clears the model
- Update OnModelChanged signature to pass oldModel and newModel parameters
  allowing derived classes to react to model changes

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

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 11 out of 11 changed files in this pull request and generated 1 comment.


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

Comment thread src/TimeSeries/ARIMAModel.cs
Configure CodeRabbit code reviews to enforce:
- Facade pattern: users should only use AiModelBuilder and AiModelResult
- Access modifier guidelines: prefer internal over public to hide IP
- Review checklist for maintaining clean public API surface

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The matern-kernel playground example used 'length' but the constructor
parameter is named 'lengthScale', causing compilation test failure.

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

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 13 out of 13 changed files in this pull request and generated 1 comment.


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

Comment thread src/Optimizers/OptimizerBase.cs
Adds a protected RequireModel() helper that returns the model or throws
InvalidOperationException with a clear message pointing to SetModel().
Replaces all _model! usages with RequireModel() for better diagnostics.

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: 0

Caution

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

⚠️ Outside diff range comments (1)
src/Optimizers/OptimizerBase.cs (1)

608-615: ⚠️ Potential issue | 🟡 Minor

Model!.DeepCopy() still uses the null-forgiving operator — missed RequireModel() replacement.

This call site can produce the same opaque NullReferenceException the PR aims to eliminate. Updating it to RequireModel().DeepCopy() would be consistent with the changes elsewhere in the file.

Proposed fix
-        var solution = Model!.DeepCopy();
+        var solution = RequireModel().DeepCopy();
🧹 Nitpick comments (2)
src/Optimizers/OptimizerBase.cs (2)

151-161: SetModel expands the public API surface on optimizer types. As per the coding guidelines, users should interact through AiModelBuilder, and optimizer internals should prefer internal access. Since SetModel is driven by the IOptimizer interface contract, consider using explicit interface implementation to keep the method off the optimizer's public type surface:

void IOptimizer<T, TInput, TOutput>.SetModel(IFullModel<T, TInput, TOutput> model) { ... }

This would still allow AiModelBuilder to call it through the IOptimizer reference while not adding a new public method visible to users who might hold a concrete optimizer type. This is a minor concern given that optimizers are typically accessed via the interface already. As per coding guidelines: "Prefer internal over public for all classes, methods, and properties unless they are part of the facade API."


136-175: Well-designed lifecycle — consider sealing SetModel to protect the invariant.

RequireModel() and the OnModelChanged hook are clean additions. One architectural concern: SetModel is public virtual, which means a derived optimizer can override it and forget to call base.SetModel(...), leaving _model silently null. Since OnModelChanged already provides the extensibility hook, removing virtual from SetModel (making it sealed for override) would close this gap while keeping the same extension surface.

Proposed diff
-    public virtual void SetModel(IFullModel<T, TInput, TOutput> model)
+    public void SetModel(IFullModel<T, TInput, TOutput> model)

If IOptimizer.SetModel must remain overridable for distributed wrappers like ShardedOptimizerBase, consider the Template Method pattern: keep the assignment + hook in a non-virtual public method, and only expose OnModelChanged for override.

#!/bin/bash
# Check if any derived class overrides SetModel (would break if we remove virtual)
rg -n --type=cs 'override\s+.*\bSetModel\b' -C 2

@sonarqubecloud

sonarqubecloud Bot commented Feb 6, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)
B Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@ooples
ooples merged commit 4779eaa into master Feb 6, 2026
42 of 43 checks passed
@ooples
ooples deleted the fix/optimizer-model-association branch February 6, 2026 19:51

This branch was successfully deployed

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

Labels

feature Feature work item

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants