LOOP tests convergence on the relaxed step, not on the residual, so rtol is effectively rtol / relaxation_factor.
gamma_loop!'s LOOP branch forms gamma_new = (1 - relaxation_factor) * gamma + relaxation_factor * F(gamma) and then converges on max|gamma_new - gamma| / ref < rtol. That difference is relaxation_factor * (F(gamma) - gamma), so with the defaults rtol = 1e-5 and relaxation_factor = 0.03 the loop stops at a relative fixed-point residual of rtol / relaxation_factor = 3.3e-4 — 33x looser than rtol reads.
Measured on the wing test/solver/solver_test_wing.yaml builds (alpha=5.0, beta=0.0, wind_speed=10.0, 4 panels), by evaluating one unrelaxed step F(gamma) from each converged answer:
| aoa |
LOOP max|F(g) - g| |
relative to max|g| |
| 0.0° |
7.69e-4 |
2.96e-4 |
| 5.7° |
1.20e-3 |
3.04e-4 |
| 11.3° |
1.56e-3 |
2.96e-4 |
| 16.7° |
2.00e-3 |
3.07e-4 |
| 26.6° |
2.82e-3 |
3.10e-4 |
The ratio is flat across the sweep, as the algebra says it should be. NONLIN, whose test is on the Newton step, lands at 1e-12 or better at all five points, and at 26.6° the two answers differ by 1.6e-4 in peak circulation (9.1063 against 9.1078) — LOOP's own error, 16x its stated rtol.
solve_base! widens it again on the retry path: a LOOP solve that misses the tolerances is re-run at relaxation_factor / 2, which halves the effective residual tolerance a second time, so the retry accepts an answer twice as loose as the attempt that "failed".
The fix is to divide the measured step by relaxation_factor before comparing it to rtol (or to converge on F(gamma) - gamma directly), which makes rtol mean what it says and makes the retry neutral. That tightens every LOOP solve in the suite by ~33x in residual, so it will move reference numbers and iteration counts and wants its own PR with the reference data re-checked — not a rider on a NONLIN fix.
Found while fixing #283; that PR changes only the NONLIN branch and leaves this alone.
LOOPtests convergence on the relaxed step, not on the residual, sortolis effectivelyrtol / relaxation_factor.gamma_loop!'s LOOP branch formsgamma_new = (1 - relaxation_factor) * gamma + relaxation_factor * F(gamma)and then converges onmax|gamma_new - gamma| / ref < rtol. That difference isrelaxation_factor * (F(gamma) - gamma), so with the defaultsrtol = 1e-5andrelaxation_factor = 0.03the loop stops at a relative fixed-point residual ofrtol / relaxation_factor= 3.3e-4 — 33x looser thanrtolreads.Measured on the wing
test/solver/solver_test_wing.yamlbuilds (alpha=5.0, beta=0.0, wind_speed=10.0, 4 panels), by evaluating one unrelaxed stepF(gamma)from each converged answer:max|F(g) - g|max|g|The ratio is flat across the sweep, as the algebra says it should be.
NONLIN, whose test is on the Newton step, lands at 1e-12 or better at all five points, and at 26.6° the two answers differ by 1.6e-4 in peak circulation (9.1063 against 9.1078) — LOOP's own error, 16x its statedrtol.solve_base!widens it again on the retry path: a LOOP solve that misses the tolerances is re-run atrelaxation_factor / 2, which halves the effective residual tolerance a second time, so the retry accepts an answer twice as loose as the attempt that "failed".The fix is to divide the measured step by
relaxation_factorbefore comparing it tortol(or to converge onF(gamma) - gammadirectly), which makesrtolmean what it says and makes the retry neutral. That tightens every LOOP solve in the suite by ~33x in residual, so it will move reference numbers and iteration counts and wants its own PR with the reference data re-checked — not a rider on aNONLINfix.Found while fixing #283; that PR changes only the
NONLINbranch and leaves this alone.