Skip to content

Grow the trust region for stepped-back steps whose proposal hit the radius - #90

Merged
FFroehlich merged 3 commits into
mainfrom
claude/fix-78-reflect-single-ec8365
Oct 25, 2026
Merged

FFroehlich merged 3 commits into
mainfrom
claude/fix-78-reflect-single-ec8365

Conversation

@FFroehlich

Copy link
Copy Markdown
Contributor

Fixes #78

Problem

update_tr_radius only increases the radius when the step is at least 0.9 * delta long. A step that was stepped back to stay within the bounds (step.alpha < 1) is shorter than that because of the bounds, not because of the trust region. So the radius never grew for such steps, even at tr_ratio ≈ 1. With reflect_single and several active bounds, every iteration's best step is a single reflection that the next bound cuts short. On the 4-D reproducer from the issue, the radius stays at 2 and the run crawls to MAXITER (1000 steps, median alpha 0.13).

Change

fides/minimize.py, one line in update_tr_radius. Whether the trust-region constraint was active is now decided on the proposal before step back, norm(step.og_ss + step.ss0), instead of on the step that is taken:

  • Steps that were not stepped back (alpha == 1, so og_ss == ss): unchanged.
  • Stepped-back steps whose proposal reached the trust-region boundary count as boundary steps. A good ratio grows the radius, as it would have without the bound. This is the reflect_single case.
  • Stepped-back steps whose proposal was interior to the trust region (e.g. a Newton step that hits a bound) still do not grow the radius. A larger radius would not change that proposal.

Why not option 1 from the issue (alpha < 1 ⇒ boundary step)

Option 1 fixes the 2D reproducer but makes the radius grow without bound whenever bound-limited steps keep a good ratio. With scg + reflect_single on the issue's reproducer, the radius doubles every iteration while the step stays about 1 long. It reaches 1e301 at iteration 1000 and inf at about 1024, and the run still hits MAXITER. In the fuzz run below (first 33 problems), option 1 also changed the exit of the other strategies, from FTOL/GTOL to DELTA_TOO_SMALL, with many more rejected steps:

33 problems × 42 configs main DELTA_TOO_SMALL option 1 this PR main rejected steps option 1 this PR
truncate 7 28 7 663 1017 663
mixed 7 13 7 670 769 670
refine 0 17 0 312 565 297
reflect_single 4 14 4 597 790 604

This partial run still used a wall-clock time limit. The refine row excludes the runs that hit it: 4 on main, 13 with option 1, 11 with this PR. The full comparison below re-runs those with a CPU-time limit.

Fuzz: main vs this PR

300 random problems (seeds 100–399 of the #75 fuzz set: bounded quadratics and Rosenbrock, n = 1–12, five box types, four start types) × 42 configurations (2D/full/scg × all step-back strategies × exact/BFGS/SR1; scg has no refine), maxiter = 500. That makes 12,600 runs per side. Runs that hit the time limit on either side were re-run on both sides with a 60 s CPU-time limit (70 per side, none timed out). "Optimum" means f - fref <= 1e-6 + 1e-4 |fref|, against a scipy BVLS / L-BFGS-B reference.

strategy runs MAXITER main → PR at optimum main → PR fixed broken iterations (sum) main → PR rejected steps main → PR
reflect_single 2700 98 → 56 2428 → 2461 33 0 146,176 → 120,652 10,895 → 11,038
reflect 2700 49 → 47 2465 → 2468 3 0 111,461 → 110,191 11,107 → 11,181
truncate 2700 47 → 48 2470 → 2470 0 0 108,145 → 108,382 11,361 → 11,393
mixed 2700 42 → 42 2471 → 2471 0 0 107,052 → 107,036 11,503 → 11,498
refine 1800 46 → 41 1673 → 1676 4 1 76,124 → 74,342 7,086 → 7,086

By solver, MAXITER for reflect_single goes 18 → 7 (2D), 29 → 29 (full) and 51 → 20 (scg). For reflect on scg it goes 16 → 13. No exceptions, NaN returns or bound violations on either side.

The one broken run (seed 172, Rosenbrock, 2D/refine/BFGS, FTOL → MAXITER) and the single-run changes for truncate/reflect (seeds 229 and 346, nonconvex Rosenbrock with quasi-Newton updates) come from trajectory divergence. In each, the new rule changed the radius decision at 1–4 iterations and the rest of the path differs. Neither side reached the optimum in the truncate/reflect cases.

Not fixed here

scg + reflect_single on the issue's reproducer still stalls. Steihaug CG stops after one iteration there (residual tolerance 0.5 |g|, #77), so the CG step is the gradient step. That step is dominated by the two variables on their bounds and is cut at alpha ~ 1e-3, and its single reflection hits the other bound. The step length does not depend on the radius, so no radius rule helps. The new regression test therefore covers 2D/full.

Tests

  • test_several_active_bounds: the issue's 4-D reproducer for 2D/full × all step-back strategies, maxiter = 100. It asserts no MAXITER and the optimum in f and x. On main, 2D-reflect_single and 2D-refine fail (MAXITER far from the optimum); here they converge in 12 and 15 iterations. The test does not require a positive exit flag because full/refine stops with DELTA_TOO_SMALL at the optimum, on main as well. The predicted decrease there is below the resolution of f, which is unrelated to this change.
  • pytest tests: 1229 passed, 3 skipped (after merging current main).

🤖 Generated with Claude Code

FFroehlich and others added 3 commits October 25, 2026 01:13
update_tr_radius only increased the radius for steps with norm >= 0.9 *
delta. Steps that were stepped back to stay within the bounds are shorter
than the radius because of the bounds, so the radius never grew for them,
even at tr_ratio ~1. With reflect_single and several active bounds, every
step is a single reflection cut short by the next bound and runs crawled
to MAXITER.

Decide whether the trust region constraint was active based on the
proposal before step back (og_ss + ss0). This is unchanged for steps that
were not stepped back. Steps whose proposal was interior to the trust
region (e.g. a Newton step that hits a bound) still do not increase the
radius, which avoids unbounded radius growth while steps are bound-limited.

Fixes #78

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2D/refine also crawled to MAXITER on the gh-78 reproducer before the
radius fix.

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

codecov Bot commented Oct 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.03%. Comparing base (586389f) to head (91d78d8).

Additional details and impacted files
@@           Coverage Diff           @@
##             main      #90   +/-   ##
=======================================
  Coverage   97.03%   97.03%           
=======================================
  Files          10       10           
  Lines        1179     1179           
=======================================
  Hits         1144     1144           
  Misses         35       35           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@FFroehlich
FFroehlich merged commit 069e825 into main Oct 25, 2026
11 checks passed
@FFroehlich
FFroehlich deleted the claude/fix-78-reflect-single-ec8365 branch October 25, 2026 08:18
@FFroehlich FFroehlich mentioned this pull request Oct 25, 2026
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.

reflect_single crawls to MAXITER when several variables sit on bounds

1 participant