test_forwarddiff.jl's POLAR_MATRICES check flakes at ~4%, and the oper - #292
Conversation
The POLAR_MATRICES check compares forward AD against a finite difference on a piecewise-linear table. With a 1e-5 step and a LOOP solve stopped at rtol=1e-7, the secant spans a 5 deg alpha knot whenever a panel's converged alpha lands within ~3e-4 deg of one, and the two estimates then disagree by 10%. Converge the solve to 1e-11 and take the difference at 1e-8, which puts the whole knot scan — exactly on the knot included — between 2.2e-8 and 7.1e-8, and tighten the bound to 1e-4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014o6rKDPYWZK8MADjKdsZbK
1-Bort-1
left a comment
There was a problem hiding this comment.
Independent review (advisory)
Verdict: APPROVE WITH COMMENTS · 1 inline, 0 off the diff
Good
- Only test/solver/test_forwarddiff.jl is changed, matching the single-idea, minimal scope in the PR card.
- Tightens rtol to 1e-11 and shrinks the finite-difference step together, addressing the real discontinuity instead of loosening the bound.
- The test bound is tightened from 1e-3 to 1e-4, so the fix strengthens rather than weakens the assertion.
- The PR description includes the scan data and opens #291 for the redrawn fixture, so the remaining risk is tracked.
Not good
test/solver/test_forwarddiff.jl:83— The added comment describes the old, wider 1e-5 step, while the code below uses a narrower 1e-8 step; a future reader can invert the intent. Rephrase it to say the small step keeps the secant inside one polar cell, or delete it.- The comment at line 83 says a wider step spans a knot, but the new step is narrower; it will mislead the next reader.
- rtol=1e-11 and aoa=7.5 are driven by the polar-grid discontinuity, yet the code does not mention that relationship for readers who skip the PR description.
opencode, rubric CLEAN_CODE.md. A different lab from the implementer
on purpose: a reviewer sharing its blind spots would not flag its mistakes.
| theta_idxs=1:4, va_idxs=5:7, omega_idxs=8:10, | ||
| aero_coeffs=true, | ||
| backend=AutoFiniteDiff(absstep=1e-5, relstep=1e-5)) | ||
| # A wider step lets the secant span a knot of the polar's 5° alpha grid. |
There was a problem hiding this comment.
MINOR: The added comment describes the old, wider 1e-5 step, while the code below uses a narrower 1e-8 step; a future reader can invert the intent. Rephrase it to say the small step keeps the secant inside one polar cell, or delete it.
|
Local full suite: PASS (10 min, Julia 1.12.7, one cell of the matrix) |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
| aero_coeffs=true, | ||
| backend=AutoFiniteDiff(absstep=1e-5, relstep=1e-5)) | ||
| # A wider step lets the secant span a knot of the polar's 5° alpha grid. | ||
| backend=AutoFiniteDiff(absstep=1e-8, relstep=1e-8)) |
There was a problem hiding this comment.
Now this is only changed in the step. Shouldn't this be the new default for the src code as well? @1-Bort-1
There was a problem hiding this comment.
Done in 1a4a039: linearize's fd_absstep/fd_relstep default to 1e-8 instead of 1e-3, and the test now passes backend=nothing rather than restating the step. Numbers behind the choice are in the PR body.
`fd_absstep`/`fd_relstep` set the step of the `AutoFiniteDiff` that `linearize` builds when handed `backend=nothing` — the only derivative a `NONLIN` caller can get, since forward AD is rejected there — and 1e-3 left three decimal places of the jacobian on the table. Against a forward-AD reference the old default returns 3.3e-4 relative error on the ram-air matrix wing where 1e-8 returns 5.7e-8, and 5.2e-5 against 5.3e-9 on the inviscid wing. 1e-8 is the sqrt(eps) scale the NONLIN Newton Jacobian already perturbs by, and the POLAR_MATRICES test's own step, so the test now takes the default rather than restating it and its 1e-4 bound guards the default against widening. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hh1KVRgU7AcoAu7UKBkvDK
…ddiff-jl-s-polar-matrices-che # Conflicts: # CHANGELOG.md
…polar-matrices-che' into agent/287-test-forwarddiff-jl-s-polar-matrices-che
#292 moved the 1e-8 finite-difference step into `linearize`'s default and had the POLAR_MATRICES check take `backend=nothing`, which deleted the call-site step the comment on this branch reworded. Resolved to that side: the comment has nothing left to describe, and the branch now carries no change over #292. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013CHKQ3LruEdTwH5MBxCKkk
TL;DR
The POLAR_MATRICES check compares forward AD against a finite difference on a polar table that is piecewise-linear with knots every 5°, and its 1e-5 step spans a knot whenever a panel's converged alpha lands within ~3e-4° of one — which is how CI got 0.0400. Converging the LOOP solve to 1e-11 and taking the difference at 1e-8 keeps the secant inside one cell everywhere I could put it, exactly on a knot included, and the bound goes from 1e-3 to 1e-4 because the measured floor is now 7.1e-8.
What is actually discontinuous
Nothing in VSM. The INVISCID sibling in the same file reads
norm_fwd = 11.110746084832735,norm_fd = 11.110746104003324on this box and the same 17 digits in the ubuntu-1.12-coverage job that failed the POLAR check on #285 — so the solver, the geometry pipeline and both backends are bit-reproducible, coverage build and all.What is discontinuous is the model the check differentiates.
build_interps(src/panel.jl:225) handslinear_interpolationa 5-knot alpha grid, socl(alpha)is a broken line, and a forward difference straddling one of those corners measures the average of two slopes while forward AD takes one of them. Bisecting the freestream alpha until panel 3's converged alpha sits on the 10° knot, everything else exactly as the test has it:rel_errCI's 0.04001429189872729 sits inside that range, and 300x of margin sits just outside it. Whether a given run lands in the window is decided by the NeuralFoil fixture, which is redrawn every run and does not come out the same twice — that part is #291, and it is why this cannot be fixed by choosing a better operating point.
The two knobs that decide how wide the window is
The step, obviously: the window scales with it. But also
rtol, because the LOOP loop stops onnormalized_error < rtol(src/solver.jl:1080), so the iteration count is a step function of the input and the converged alpha carries the leftover of whichever iteration it stopped on. Sampling CFz along theta_2 in 2000 steps of the test's own 1e-5, the one iteration-count change in that span moves the finite difference by 1.45e-4 — normalised, 1.05e-4, a tenth of the whole 1e-3 budget, from a step the test does not control. Atrtol=1e-11that staircase is gone (worst jitter 5.3e-6, and that is curvature). Until the solve is that tight, the panel alpha itself wobbles by ~1e-5°, so shrinking the step alone does not help: it is the pair that works.Scanned across the same knot, offset in degrees of freestream alpha:
The chosen pair never leaves 2.2e-08..7.1e-08 anywhere on that scan. The bound follows: 1e-4 is 1400x above the measured floor and still ten times above the worst the exit staircase can contribute at this step, and it is a tightening, not a loosening.
What I did not do
I did not move the operating point. #287 is right that 320e756's
defl_radreached nothing —deltais exactly 0.0 for every panel, but at an exact delta knot the alpha-direction slope is the same from either cell, anddeltais not a differentiated input, so a delta knot cannot produce this. Moving the freestream alpha to maximise clearance from the alpha knots would buy about 1° of margin against a fixture that moves panel alphas by 0.1–0.2° per draw, which is a smaller factor than it sounds and would need moving again the next time the fixture moves.The INVISCID testset keeps its 1e-5 step and 1e-3 bound: it has no table, so no knot, and it has never flaked.
rtol=1e-11costs 685 LOOP iterations against 390, out ofmax_iterations=1500— and 671–685 across ±10% on every Cl, twenty times the fixture's own spread, so the halve-the-relaxation retry stays far away; warm, the file's wall time is 0.1 s either way, so the CI figure for this testset is compilation. If #284 lands andrtolstarts meaning the unrelaxed residual, every number here only improves.Verification
Expression: rel_err < 0.001/Evaluated: 0.10032484514560956 < 0.001test/solver/test_forwarddiff.jlred before that change, green after at the same operating point (7/7), and green at the committed operating point (7/7, juliaserver, 49.7 s cold)agent ci-local— see below · GitHub CI: see belowmain(branch parent is 3f75701) · docs not run — no public symbol touched · no REUSE lint in this repo ·jetlsnot installed on this boxScope
+5 / −4 in
test/solver/test_forwarddiff.jland nothing else. Not stacked: #285 changes onlygamma_loop!'s NONLIN branch. Opened #291 for the fixture that is redrawn every run, which this does not fix.Closes #287 · task
VortexStepMethod.jl-287