Skip to content

test_forwarddiff.jl's POLAR_MATRICES check flakes at ~4%, and the oper - #292

Merged
1-Bart-1 merged 5 commits into
mainfrom
agent/287-test-forwarddiff-jl-s-polar-matrices-che
Sep 11, 2026
Merged

1-Bart-1 merged 5 commits into
mainfrom
agent/287-test-forwarddiff-jl-s-polar-matrices-che

Conversation

@1-Bort-1

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

Copy link
Copy Markdown
Contributor

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.110746104003324 on 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) hands linear_interpolation a 5-knot alpha grid, so cl(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:

panel 3 alpha − 10° rel_err
−4.9e-3° 3.35e-06
−2.4e-4° 4.52e-05
−9.8e-5° 6.88e-02
−1.5e-5° 1.23e-01
+5e-15° 8.83e-03
+2.4e-5° 2.29e-03
+8.1e-5° 5.97e-04
+1.6e-4° 3.22e-06

CI'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 on normalized_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. At rtol=1e-11 that 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:

offset as the test had it rtol 1e-11, step 1e-7 rtol 1e-11, step 1e-8
1e-3° 3.20e-06 3.46e-08 7.09e-08
3e-4° 3.74e-02 3.44e-08 2.25e-08
1e-4° 1.00e-01 3.40e-08 3.29e-08
1e-5° 1.31e-01 3.42e-08 6.30e-08
3e-6° 1.38e-01 2.07e-02 5.00e-08
1e-6° 1.53e-01 8.35e-02 6.85e-08
1e-7° 2.03e-01 1.12e-01 3.40e-08
0 8.83e-03 1.15e-01 5.47e-08

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_rad reached nothing — delta is exactly 0.0 for every panel, but at an exact delta knot the alpha-direction slope is the same from either cell, and delta is 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-11 costs 685 LOOP iterations against 390, out of max_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 and rtol starts meaning the unrelaxed residual, every number here only improves.

Verification

  • Reproduced first: unchanged file at the knot-adjacent operating point (12.810477774611°, panel 3 alpha 9.99995111°) → Expression: rel_err < 0.001 / Evaluated: 0.10032484514560956 < 0.001
  • test/solver/test_forwarddiff.jl red 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)
  • Local full suite: agent ci-local — see below · GitHub CI: see below
  • Up to date with main (branch parent is 3f75701) · docs not run — no public symbol touched · no REUSE lint in this repo · jetls not installed on this box
  • Benchmark: n/a
  • Risk: exactly on a knot the check still relies on forward AD and a forward difference choosing the same side of the corner; that held at every offset I measured, but it is a coincidence of convention rather than something the test asserts.

Scope

+5 / −4 in test/solver/test_forwarddiff.jl and nothing else. Not stacked: #285 changes only gamma_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

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 1-Bort-1 added agent:running Agent task state agent:ci Agent task state and removed agent:running Agent task state labels Sep 9, 2026

@1-Bort-1 1-Bort-1 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread test/solver/test_forwarddiff.jl Outdated
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@1-Bort-1

1-Bort-1 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

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

@codecov

codecov Bot commented Sep 9, 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:review Agent task state and removed agent:ci Agent task state labels Sep 9, 2026
Comment thread test/solver/test_forwarddiff.jl Outdated
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))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now this is only changed in the step. Shouldn't this be the new default for the src code as well? @1-Bort-1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@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 and others added 3 commits September 11, 2026 21:08
`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
@1-Bart-1
1-Bart-1 enabled auto-merge September 11, 2026 19:15
1-Bort-1 added a commit that referenced this pull request Sep 11, 2026
#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
@1-Bart-1
1-Bart-1 merged commit 0da841d into main Sep 11, 2026
8 of 9 checks passed
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.

test_forwarddiff.jl's POLAR_MATRICES check flakes at ~4%, and the oper

2 participants