Skip to content

LOOP converges on the relaxed step, so rtol is 33x looser than it reads - #306

Merged
1-Bart-1 merged 3 commits into
mainfrom
agent/284-loop-converges-on-the-relaxed-step-so-rt
Sep 12, 2026
Merged

LOOP converges on the relaxed step, so rtol is 33x looser than it reads#306
1-Bart-1 merged 3 commits into
mainfrom
agent/284-loop-converges-on-the-relaxed-step-so-rt

Conversation

@1-Bort-1

@1-Bort-1 1-Bort-1 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

TL;DR

LOOP tested convergence on the under-relaxed step rather than on the fixed-point residual, so rtol was silently rtol / relaxation_factor — 33x looser than it reads at the defaults. Dividing the measured step by the relaxation actually used makes rtol mean what it says, and makes solve_base!'s half-relaxation retry stop loosening the tolerance a second time.

What was wrong

gamma_loop!'s LOOP branch forms gamma_new = (1 - relaxation_factor) * gamma + relaxation_factor * F(gamma) and then converged on max|gamma_new - gamma| / ref < rtol. That difference is relaxation_factor * (F(gamma) - gamma), so the loop stopped at a relative fixed-point residual of rtol / relaxation_factor.

I measured it by evaluating one unrelaxed step F(g) from each converged answer on test/solver/solver_test_wing.yaml (4 panels, alpha=5.0, beta=0.0, wind_speed=10.0, ELLIPTIC start). The ratio is flat across the sweep, as the algebra says it should be, and NONLIN — which already tested the Newton step against the residual — sits at 1e-12 or better everywhere.

aoa LOOP rel. residual, before after NONLIN
0.0° 2.96e-4 9.35e-6 1.37e-15
5.7° 3.04e-4 9.14e-6 1.48e-12
11.3° 2.96e-4 9.26e-6 1.25e-12
16.7° 3.07e-4 9.13e-6 1.33e-13
26.6° 3.10e-4 9.40e-6 2.17e-13

Peak circulation at 26.6° was LOOP 9.106263 against NONLIN 9.107758 — LOOP's own error, 16x its stated rtol. It is now 9.107713, 5e-6 relative from NONLIN.

What changed

One line: the measured step is divided by relaxation_factor before it is compared to rtol. The divisor is the relaxation of that call, not solver.relaxation_factor, so the retry at half relaxation now converges to the same residual as the attempt that failed instead of to twice as loose an answer. Solver.rtol, SolverSettings.rtol and the gamma_loop! docstring say what the tolerance is measured on; the residual also stops being called error, which shadowed Base.error.

The regression test solves at 0° and at 26.6° past stall, then evaluates one unrelaxed step from each converged answer and asserts the residual is under rtol — the quantity that was wrong, rather than a reference number that happens to move with it. Before the fix it reports 0.000769 < 2.60e-5 and 0.002704 < 9.11e-5.

What this costs, and what it moves

Every LOOP solve tightens by 1 / relaxation_factor and takes about 1.4x the iterations — on the test wing 134/137/146/148/183 → 189/193/205/208/243 against max_iterations = 1500.

The worst case in the repo is the shipped rtol=1e-6, relaxation_factor=0.01 settings, 100x tighter on the largest geometries. On data/TUDELFT_V3_KITE/vsm_settings.yaml (50 panels, max_iterations=5000) every solve still converges with room to spare, and CL moves in the fifth digit:

V3 kite, before → after
alpha CL before CL after iterations
0.0 0.12530613 0.12531090 600 → 890
7.4 0.71598272 0.71595387 560 → 839
15.0 1.01259119 1.01257982 797 → 1192
20.0 0.99213197 0.99212517 850 → 1280

No reference number in the suite needed re-fitting. The hard-coded coefficients in test/body_aerodynamics/test_body_aerodynamics.jl still hold at their tolerances: that solve already ran at rtol=1e-8, so its effective residual tolerance went from 3.3e-7 to 1e-8, while the tightest assertion on it is 1.5e-5 relative. test/solver/test_flow_curvature.jl:151 compares two LOOP solves at the default and passes for the same reason — both sides moved together.

Where I would push back

relaxation_factor no longer buys looser convergence, only more iterations, which is what it should always have meant — but anyone who had tuned a difficult case by lowering it was also buying an earlier stop, and will now pay for that in wall time. The V3 numbers above are the measurement of that cost on the largest shipped geometry.

is_with_artificial_damping adds damp to the step, so with damping on the criterion now weights that term by 1 / relaxation_factor. It stays zero exactly where the iteration stops moving, so convergence still means convergence; nothing in the suite exercises that path.

Verification

  • Reproduced first: max|F(g)-g| / max|g| = 2.96e-4 … 3.10e-4 against rtol = 1e-5
  • test/solver/test_solver.jl red before (0.000769 < 2.60e-5, 0.002704 < 9.11e-5), green after (juliaserver, exit 0)
  • test/solver/{test_flow_curvature,test_unrefined_dist,test_backend_comparison,test_forwarddiff}.jl and test/body_aerodynamics/test_body_aerodynamics.jl: PASS
  • Local full suite at the head 872ee8c (agent ci-local, one matrix cell, Julia 1.12.7): 6336 pass, 0 fail, 0 error, 1 pre-existing @test_skip, 9m16s
  • GitHub CI at the head 872ee8c: all nine checks green — Julia 1.11 and 1.12 across ubuntu, macOS and windows, both coverage runs, Documentation, Test end-user and developer setup, codecov/patch. That last one was the red check on the previous head, and it was write_section_aero writes an all-NaN deflected .dat, and read_section_ #300 rather than this branch; the fix for it landed on main as Setup Test is failing #312 and this merge brings it in.
  • Docs build clean, doctests included · up to date with main at 50b1454, merged as 872ee8c: CHANGELOG.md was the only conflict — this branch's LOOP entry against main's plot_slices_3d one, both kept · REUSE lint: n/a, the repo carries no REUSE setup · jetls is not installed on this box, so static analysis was not run
  • Benchmark: n/a — iteration counts are reported above, not timings
  • Risk: a user configuration with a very low relaxation_factor and a max_iterations sized to the old early stop will now hit the iteration cap and report FAILURE where it used to report a loose FEASIBLE.

Scope

+55 / -8 across 4 files: one line of solver logic, three docstrings, a CHANGELOG entry and one testset. test/solver/test_solver.jl repeats the settings → Wingrefine!BodyAerodynamics setup in four testsets now; a helper would have rewritten three testsets this change has no other reason to open, so it is left for a cleanup: PR.

Closes #284 · task VortexStepMethod.jl-284

@1-Bort-1 1-Bort-1 added agent:running Agent task state agent:ci Agent task state and removed agent:running Agent task state labels Sep 11, 2026
@1-Bort-1

1-Bort-1 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Local full suite: PASS (6 min, Julia 1.12.7, one cell of the matrix)

@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@1-Bort-1 1-Bort-1 added agent:queued Agent task state agent:running Agent task state and removed agent:review Agent task state agent:queued Agent task state labels Sep 11, 2026
@1-Bort-1 1-Bort-1 added agent:ci Agent task state and removed agent:running Agent task state labels Sep 11, 2026
@1-Bort-1 1-Bort-1 added agent:waiting-human Agent task state and removed agent:ci Agent task state labels Sep 12, 2026
@1-Bart-1

Copy link
Copy Markdown
Member

Resolve conflicts

@1-Bort-1 1-Bort-1 added agent:queued Agent task state agent:running Agent task state and removed agent:waiting-human Agent task state agent:queued Agent task state labels Sep 12, 2026
…ges-on-the-relaxed-step-so-rt

# Conflicts:
#	CHANGELOG.md
@1-Bort-1

Copy link
Copy Markdown
Contributor Author

Resolved in 872ee8c: CHANGELOG.md was the only conflict, both ### Fixed entries kept. #306 is MERGEABLE and all nine checks are green.

@1-Bort-1 1-Bort-1 added agent:ci Agent task state and removed agent:running Agent task state labels Sep 12, 2026
@1-Bort-1 1-Bort-1 added agent:review Agent task state and removed agent:ci Agent task state labels Sep 12, 2026
@1-Bart-1
1-Bart-1 merged commit 11996ff into main Sep 12, 2026
9 checks passed
@1-Bart-1
1-Bart-1 deleted the agent/284-loop-converges-on-the-relaxed-step-so-rt branch September 12, 2026 11:12
@1-Bort-1 1-Bort-1 added agent:done Agent task state and removed agent:review Agent task state labels Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:done Agent task state

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LOOP converges on the relaxed step, so rtol is 33x looser than it reads

2 participants