fix: convergence-check pattern fix swept across 27 optimizers (#1351 follow-up) - #1360
Conversation
Sweep of the AdamOptimizer convergence-check bug fixed in PR #1351 across the rest of the optimizer suite. UpdateBestSolution copies currentStepData into bestStepData on the first iteration (because bestStepData starts uninitialised), so the convergence check |bestStepData - currentStepData| < tolerance always fires after epoch 0 and Optimize returns after exactly 1 epoch regardless of MaxIterations. Fix: compare against previousStepData (the prior epoch's score) so the convergence signal is per-epoch progress: "the fitness stopped changing from one epoch to the next." This commit fixes 5 adam-family optimizers (group 1/6): - adam8bitoptimizer.cs:351 (multi-line variant) - adamwoptimizer.cs:217 (multi-line variant) - adagradoptimizer.cs:185 (single-line variant) - adadeltaoptimizer.cs:220 (single-line variant) - adamaxoptimizer.cs:229 (single-line variant) Related: #1340 / PR #1351. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y (10/27) Continued sweep of the AdamOptimizer convergence-check bug fixed in PR #1351. See the first commit in this PR for full background. This commit fixes 5 quasi-newton / coordinate-search optimizers (group 2/6): - amsgradoptimizer.cs:143 - bfgsoptimizer.cs:140 - dfpoptimizer.cs:143 - conjugategradientoptimizer.cs:134 - coordinatedescentoptimizer.cs:133 Related: #1340 / PR #1351. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (15/27) Continued sweep of the AdamOptimizer convergence-check bug fixed in PR #1351. See the first commit in this PR for full background. This commit fixes 5 large-batch / second-order optimizers (group 3/6): - ftrloptimizer.cs:315 - lamboptimizer.cs:215 (multi-line variant) - larsoptimizer.cs:193 (multi-line variant) - lbfgsoptimizer.cs:156 - levenbergmarquardtoptimizer.cs:150 Related: #1340 / PR #1351. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h (20/27) Continued sweep of the AdamOptimizer convergence-check bug fixed in PR #1351. See the first commit in this PR for full background. This commit fixes 5 SGD / direct-search optimizers (group 4/6): - lionoptimizer.cs:158 (uses break;, not return) - minibatchgradientdescentoptimizer.cs:150 (per-batch convergence check) - momentumoptimizer.cs:177 (multi-line variant) - nadamoptimizer.cs:174 - neldermeadoptimizer.cs:186 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ssical (25/27) Continued sweep of the AdamOptimizer convergence-check bug fixed in PR #1351. See the first commit in this PR for full background. This commit fixes 5 second-order / classical optimizers (group 5/6): - nesterovacceleratedgradientoptimizer.cs:150 - newtonmethodoptimizer.cs:140 - powelloptimizer.cs:235 - proximalgradientdescentoptimizer.cs:238 - rootmeansquarepropagationoptimizer.cs:211 (uses break;, not return) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (27/27) Final commit in the AdamOptimizer convergence-check sweep started in PR #1351. See the first commit in this PR for full background. This commit fixes the last 2 optimizers (group 6/6): - stochasticgradientdescentoptimizer.cs:153 (multi-line variant) - trustregionoptimizer.cs:265 All 27 in-scope optimizers in src/Optimizers/ are now fixed. Excluded: - adamoptimizer.cs is owned by PR #1351 and is not modified here. - normaloptimizer.cs / gradientdescentoptimizer.cs / advanced metaheuristics (admm / bayesian / cmaes / differential evolution / genetic algorithm / particle swarm / simulated annealing / tabu search) do NOT contain the broken pattern (see the PR description for classification). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parametrised xunit theory that runs each fixed optimiser on a deterministic 2D quadratic regression problem with maxiterations=10 and asserts that result.iterations > 1. The pre-fix value was always exactly 1 because the convergence check fired immediately after epoch 0. Coverage: 27 optimizers — adam8bit, adamw, adagrad, adadelta, adamax, amsgrad, bfgs, conjugategradient, coordinatedescent, dfp, ftrl, lamb, lars, lbfgs, levenbergmarquardt, lion, minibatchgradientdescent, momentum, nadam, neldermead, nesterovacceleratedgradient, newtonmethod, powell, proximalgradientdescent, rootmeansquarepropagation, stochasticgradientdescent, trustregion. Adding a new optimiser to the sweep is a single line in optimizerfactories(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eck bug set tolerance=0.0 on all 27 optimizer test rows so the convergence check fires only on genuine no-progress plateaus, not on small-but-real per-epoch deltas. the pre-fix bug surfaces regardless of tolerance (|best - current| is exactly 0 after updatebestsolution copies current into best on the first iteration), so this guard ensures the regression test isolates the specific bug pattern fixed in the sweep. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThis PR changes early-stopping in many optimizers to compare the previous epoch's fitness against the current epoch's fitness (instead of best-so-far vs current), preventing immediate termination after the first iteration. It also adds an integration test suite that verifies each optimizer runs more than one iteration on a deterministic quadratic fixture. ChangesConvergence Check Pattern Fix
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly Related PRs
Suggested Labels
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Optimizers/CoordinateDescentOptimizer.cs`:
- Around line 133-139: The convergence check is running on the first epoch using
an uninitialized previousStepData, causing false early exit; modify the
condition around
NumOps.LessThan(NumOps.Abs(NumOps.Subtract(previousStepData.FitnessScore,
currentStepData.FitnessScore)), NumOps.FromDouble(_options.Tolerance)) to skip
checking until a real previousStepData exists (e.g., only run this comparison
when a previous step has been populated or when epoch/iteration index > 0),
ensuring previousStepData is assigned from an actually evaluated epoch before
performing the convergence test; keep UpdateBestSolution and currentStepData
logic unchanged but gate this convergence block so the first-epoch default value
cannot trigger termination.
In `@src/Optimizers/DFPOptimizer.cs`:
- Around line 143-149: The convergence check is using previousStepData before it
has been set, causing false convergence on epoch 0; modify the convergence logic
around the NumOps.LessThan(...) call to skip the check on the first iteration
(e.g., only perform the comparison if previousStepData is initialized or
iterationIndex > 0) so previousStepData.FitnessScore is read only after
previousStepData has been assigned from an evaluated step; keep the same
comparison using NumOps.Abs(NumOps.Subtract(previousStepData.FitnessScore,
currentStepData.FitnessScore)) and _options.Tolerance but gate it behind a
non-first-epoch condition to avoid premature exit.
In `@src/Optimizers/FTRLOptimizer.cs`:
- Around line 315-321: The convergence check uses previousStepData.FitnessScore
on epoch 0 which may be uninitialized; add an explicit epoch-0 guard so we skip
this tolerance comparison on the first iteration. Wrap the existing
NumOps.LessThan(...) condition with a check like "if (epoch > 0 && ...)" or "if
(previousStepDataIsInitialized && ...)" in the method inside FTRLOptimizer
(where previousStepData and currentStepData are compared) so the optimizer only
tests convergence after at least one prior epoch.
In `@src/Optimizers/LBFGSOptimizer.cs`:
- Around line 156-163: The convergence check is comparing
previousStepData.FitnessScore before a prior epoch has been evaluated, allowing
premature first-epoch exit; modify the block around the NumOps.LessThan(...)
call in LBFGSOptimizer so it only runs when a valid previous evaluation exists
(e.g., guard by a boolean/flag or iteration count), for example check that
previousStepData has been evaluated (or iterationIndex > 0) before computing
NumOps.Abs(NumOps.Subtract(previousStepData.FitnessScore,
currentStepData.FitnessScore)) against NumOps.FromDouble(_options.Tolerance);
ensure the new guard uses the same identifiers (previousStepData,
currentStepData, _options.Tolerance) so first-epoch comparisons are skipped.
In `@src/Optimizers/LevenbergMarquardtOptimizer.cs`:
- Around line 150-157: The convergence check currently compares
previousStepData.FitnessScore on epoch 0 and can falsely trigger; update the
logic in LevenbergMarquardtOptimizer (where the block with previousStepData and
currentStepData is) to first guard that previousStepData is real before
performing the NumOps.LessThan(...) comparison — e.g., check a validity flag or
that previousStepData has been set (or that iteration/epoch > 0) and only then
evaluate NumOps.Abs(NumOps.Subtract(previousStepData.FitnessScore,
currentStepData.FitnessScore)) < tolerance; ensure previousStepData is still
assigned as before (UpdateBestSolution/currentStepData copy behavior) so the
guard prevents the early exit on the first epoch.
In `@src/Optimizers/NadamOptimizer.cs`:
- Around line 174-180: The convergence check is comparing currentStepData to a
default/uninitialized previousStepData on epoch 0 (using
NumOps.LessThan/NumOps.Abs/NumOps.Subtract and _options.Tolerance), which can
falsely trigger early exit; fix this by skipping the convergence comparison on
the first iteration (or only performing it when previousStepData has been set) —
e.g., add a guard using an iteration counter or a boolean like
"hasPreviousStepData" around the existing if (NumOps.LessThan(...)) so
UpdateBestSolution and subsequent logic run normally without comparing against a
synthetic baseline on epoch 0.
In `@src/Optimizers/NelderMeadOptimizer.cs`:
- Around line 186-193: The convergence check in NelderMeadOptimizer is comparing
previousStepData.FitnessScore (which may be a placeholder) to
currentStepData.FitnessScore, allowing early termination on iteration 0; fix by
ensuring previousStepData is initialized from an evaluated solution before any
convergence comparison or by skipping the tolerance check on the first
iteration—e.g., set previousStepData to a copy of the first evaluated
currentStepData (or use an "isFirstIteration" guard) so the
NumOps.LessThan(NumOps.Abs(NumOps.Subtract(previousStepData.FitnessScore,
currentStepData.FitnessScore)), NumOps.FromDouble(_options.Tolerance))
comparison only runs when previousStepData contains a real evaluated fitness.
In `@src/Optimizers/NesterovAcceleratedGradientOptimizer.cs`:
- Around line 150-157: The convergence check in
NesterovAcceleratedGradientOptimizer currently compares
previousStepData.FitnessScore to currentStepData.FitnessScore on epoch 0,
causing false early exit; modify the logic in the method containing that
if-statement to add a first-epoch guard (e.g., skip the NumOps.LessThan
comparison when epochIndex == 0 or when previousStepData.IsDefault) or
initialize previousStepData from the evaluated initial solution before the loop
so previousStepData.FitnessScore is a true prior value; ensure the referenced
check using NumOps.Abs(NumOps.Subtract(previousStepData.FitnessScore,
currentStepData.FitnessScore)) and _options.Tolerance only runs when a valid
previous epoch value exists (use the epoch counter or a boolean like
hasPreviousEpoch to gate the comparison).
In `@src/Optimizers/NewtonMethodOptimizer.cs`:
- Around line 140-147: The convergence check uses previousStepData which is
default-initialized and can cause a false-positive on the first iteration;
modify the code so the NumOps.LessThan(...) check is skipped on the first
completed iteration by gating it behind a "has previous" condition (e.g., an
iteration counter > 0 or a boolean like hasPreviousStep set after the first
iteration completes). Concretely, update the block containing previousStepData
and currentStepData (and the NumOps.Abs/NumOps.Subtract check against
_options.Tolerance) to only run when the previousStepData has been initialized
(or iterationIndex > 0), and ensure you set that flag (or increment the counter)
after UpdateBestSolution/current iteration finalization so subsequent iterations
perform the convergence test normally.
In `@src/Optimizers/PowellOptimizer.cs`:
- Around line 235-242: The convergence check uses previousStepData vs
currentStepData before previousStepData has a real evaluation, causing a false
positive on iteration 0; modify the condition around the tolerance comparison in
PowellOptimizer (the block using previousStepData, currentStepData and
NumOps.LessThan(... FromDouble(_options.Tolerance))) to only run when iteration
> 0 (or alternatively set previousStepData from the initial solution before the
loop); ensure the guard prevents the existing NumOps.Abs/NumOps.Subtract check
from executing on the first iteration so UpdateBestSolution behavior no longer
triggers an immediate exit.
In `@src/Optimizers/ProximalGradientDescentOptimizer.cs`:
- Around line 238-245: The convergence check is comparing previousStepData
(which may be default/uninitialized) to currentStepData and can short-circuit on
epoch 0; modify the logic in the block using previousStepData, currentStepData,
NumOps.LessThan(..., NumOps.FromDouble(_options.Tolerance)) to skip the
convergence test on the first iteration — e.g., add a guard that only performs
this delta check if previousStepData has been initialized or if epochIndex > 0
(or use an explicit flag set after the first iteration); ensure you do this
where UpdateBestSolution and the convergence if-statement are located so the
first-epoch default previousStepData cannot trigger early exit.
In `@src/Optimizers/RootMeanSquarePropagationOptimizer.cs`:
- Around line 211-217: The convergence check is using previousStepData before
it's populated, causing a possible false early exit on epoch 0; update the code
in RootMeanSquarePropagationOptimizer so the comparison using
NumOps.LessThan(NumOps.Abs(NumOps.Subtract(previousStepData.FitnessScore,
currentStepData.FitnessScore)), NumOps.FromDouble(_options.Tolerance)) only runs
after previousStepData has been set (or explicitly skip the check on the first
epoch). Concretely, either move the previousStepData = currentStepData (or the
logic that initializes previousStepData) to execute before this convergence
check, or add an isFirstEpoch guard around the comparison, ensuring
previousStepData is valid when referenced.
In `@src/Optimizers/TrustRegionOptimizer.cs`:
- Around line 265-271: The convergence check uses previousStepData before it's
initialized, allowing a false positive on iteration 0; fix by ensuring
previousStepData is set from the real iteration data (or skip the check) before
evaluating
NumOps.LessThan(NumOps.Abs(NumOps.Subtract(previousStepData.FitnessScore,
currentStepData.FitnessScore)), NumOps.FromDouble(_options.Tolerance));
specifically, either move the tolerance-check block to after the code that
assigns previousStepData (or call UpdateBestSolution/currentStepData copy first)
or add a guard (e.g., if (previousStepData == null || firstIteration) skip
check) so the comparison only runs when previousStepData holds a prior iteration
value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 567deed2-a1ef-46f8-b8a9-5c8b909079a5
📒 Files selected for processing (28)
src/Optimizers/AMSGradOptimizer.cssrc/Optimizers/AdaDeltaOptimizer.cssrc/Optimizers/AdaMaxOptimizer.cssrc/Optimizers/AdagradOptimizer.cssrc/Optimizers/Adam8BitOptimizer.cssrc/Optimizers/AdamWOptimizer.cssrc/Optimizers/BFGSOptimizer.cssrc/Optimizers/ConjugateGradientOptimizer.cssrc/Optimizers/CoordinateDescentOptimizer.cssrc/Optimizers/DFPOptimizer.cssrc/Optimizers/FTRLOptimizer.cssrc/Optimizers/LAMBOptimizer.cssrc/Optimizers/LARSOptimizer.cssrc/Optimizers/LBFGSOptimizer.cssrc/Optimizers/LevenbergMarquardtOptimizer.cssrc/Optimizers/LionOptimizer.cssrc/Optimizers/MiniBatchGradientDescentOptimizer.cssrc/Optimizers/MomentumOptimizer.cssrc/Optimizers/NadamOptimizer.cssrc/Optimizers/NelderMeadOptimizer.cssrc/Optimizers/NesterovAcceleratedGradientOptimizer.cssrc/Optimizers/NewtonMethodOptimizer.cssrc/Optimizers/PowellOptimizer.cssrc/Optimizers/ProximalGradientDescentOptimizer.cssrc/Optimizers/RootMeanSquarePropagationOptimizer.cssrc/Optimizers/StochasticGradientDescentOptimizer.cssrc/Optimizers/TrustRegionOptimizer.cstests/AiDotNet.Tests/IntegrationTests/Optimizers/OptimizerConvergenceCheckPatternTests.cs
…review) CodeRabbit on PR #1360: every optimizer's Optimize loop checks convergence by comparing |previousStepData - currentStepData| < Tolerance. At epoch 0, previousStepData is a default-constructed OptimizationStepData (FitnessScore = default(T) = 0), so an already-near-zero initial loss can produce a false-positive convergence and the optimizer returns without running a single real update step. Fix: gate the convergence comparison with `epoch > 0` so it only fires after a real previous epoch's score has been recorded. Same pattern applied to all 13 optimizers flagged: - CoordinateDescentOptimizer - DFPOptimizer - FTRLOptimizer - LBFGSOptimizer - LevenbergMarquardtOptimizer - NadamOptimizer - NelderMeadOptimizer - NesterovAcceleratedGradientOptimizer - NewtonMethodOptimizer - PowellOptimizer - ProximalGradientDescentOptimizer - RootMeanSquarePropagationOptimizer - TrustRegionOptimizer 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Optimizers/NelderMeadOptimizer.cs`:
- Line 192: In the convergence guard inside NelderMeadOptimizer where you
compare previousStepData.FitnessScore and currentStepData.FitnessScore, replace
the undefined variable epoch with the actual loop index iteration (use iteration
> 0) so the if condition reads against iteration; keep the existing
NumOps.Abs/NumOps.Subtract/NumOps.FromDouble(_options.Tolerance) logic unchanged
to preserve the tolerance check.
In `@src/Optimizers/PowellOptimizer.cs`:
- Line 241: The code in PowellOptimizer's convergence check references a
non-existent variable `epoch`; replace `epoch` with the actual loop variable
`iteration` used in the surrounding loop so the condition reads something like
checking `iteration > 0` before comparing fitness values; update the conditional
that uses `previousStepData`, `currentStepData`, and `_options.Tolerance` to use
`iteration` to fix the CS0103 compilation error.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 927acff9-c7f6-4d2d-8fcb-2ceef8ad225d
📒 Files selected for processing (13)
src/Optimizers/CoordinateDescentOptimizer.cssrc/Optimizers/DFPOptimizer.cssrc/Optimizers/FTRLOptimizer.cssrc/Optimizers/LBFGSOptimizer.cssrc/Optimizers/LevenbergMarquardtOptimizer.cssrc/Optimizers/NadamOptimizer.cssrc/Optimizers/NelderMeadOptimizer.cssrc/Optimizers/NesterovAcceleratedGradientOptimizer.cssrc/Optimizers/NewtonMethodOptimizer.cssrc/Optimizers/PowellOptimizer.cssrc/Optimizers/ProximalGradientDescentOptimizer.cssrc/Optimizers/RootMeanSquarePropagationOptimizer.cssrc/Optimizers/TrustRegionOptimizer.cs
…rgence guards My PR #1360 batch script swept `if (NumOps.LessThan(` → `if (epoch > 0 && NumOps.LessThan(` across all 13 optimizers, but two of them (NelderMead, Powell) name their iteration variable `iteration`, not `epoch` — so the rewrite produced a CS0103 compile error in those two files. Replace `epoch > 0` with `iteration > 0` in NelderMeadOptimizer.cs and PowellOptimizer.cs to match their actual loop-variable names. The other 11 optimizers do use `epoch` (verified by grep) and are unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Sweeps the same convergence-check bug pattern that PR #1351 fixed in
AdamOptimizer.Optimizeacross all 27 other gradient-based optimizers undersrc/Optimizers/. Each one was silently terminating after exactly 1 iteration because the convergence delta|bestStepData.FitnessScore - currentStepData.FitnessScore|was always 0 (afterUpdateBestSolutioncopies current into best on the first iteration), regardless of tolerance.Fix is mechanical: compare against
previousStepDatainstead ofbestStepData— the same one-line change as PR #1351, replicated.Bug pattern (cite PR #1351)
Optimizers fixed (27/27)
Committed in 6 family batches:
Regression test
tests/AiDotNet.Tests/IntegrationTests/Optimizers/OptimizerConvergenceCheckPatternTests.cs— parametrized[Theory]with one row per optimizer. Asserts each one runs MORE THAN 1 iteration on a deterministic 2D quadratic problem.Test uses
Tolerance=0.0explicitly to isolate the pre-fix bug pattern (the diff is exactly 0 afterUpdateBestSolution, regardless of tolerance — so the bug surfaces even at Tolerance=0).Validation
Predecessor
This PR is a direct follow-up to #1351 which fixed the same bug in
AdamOptimizerand explicitly flagged: "this same bug pattern exists in 27 other optimizers undersrc/Optimizers/".Closes
n/a (tracked by task #118 in HarmonicEngine consumer)
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests