Skip to content

LOOP caps each panel's relaxation at its own stiffness, so narrow tip panels no longer diverge; unrefined section loads are area-weighted - #402

Open
1-Bort-1 wants to merge 6 commits into
mainfrom
agent/395-the-curved-reference-wing-does-not-conve
Open

1-Bort-1 wants to merge 6 commits into
mainfrom
agent/395-the-curved-reference-wing-does-not-conve

Conversation

@1-Bort-1

@1-Bort-1 1-Bort-1 commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

TL;DR

The LOOP solver now relaxes each panel by at most 1 / (1 + π c |z_airf ⋅ AIC_ii|), so cosine-spaced tip panels (1.3 cm on a 1 m chord) no longer blow the iteration up to NaN. The curved reference wing now converges at the default settings in about 300 iterations. Making those solves converge exposed that test_unrefined_dist.jl had never checked anything, because it solved a wing that diverged. Fixing that test meant fixing its polar and the unrefined aggregation, which were both wrong.

Curved reference wing, VSM LOOP at 5.71°: main goes NaN at iteration 11, this branch converges in 302 iterations

Why LOOP diverged

The geometry is not the cause: the AIC, panel frames and starting gamma of the curved wing are all finite. NONLIN finds its fixed point (CL 0.4223, residual 1e-13). The Jacobian of the LOOP map there has an eigenvalue at −210, from the narrow tip panels' self-induction, so a uniform step is stable only for relaxation_factor < 2/(1+210) ≈ 0.0095. Main's 0.03 and its one retry at 0.015 both diverge: max|gamma| reaches 3.8e50 by iteration 10 and NaN by 20.

Halving further (your option A) reaches that limit, but the iteration count grows as 1/ω. At 40 panels solver_test_wing needs ω ≤ 0.004 and more than 1,500 iterations, so halving alone just moves the failure. The stiffness sits on the diagonal: 1 + π c_i |z_airf_i ⋅ AIC_ii| tracks each panel's diagonal of I − J to within about 20% (215 against 176 at the tip, measured). So each panel's step is capped at that diagonal step: min(relaxation_factor, 1/stiffness_i). The fixed point is unchanged, and only panels stiffer than 1/relaxation_factor take smaller steps. I first tried scaling against the median and then against the geometric mean of the stiffness. Both slowed V3 by 6% and 12%. The cap leaves every wing that already converged exactly as it was:

wing, α main iterations branch iterations
V3, 5° / 10° 843 / 889 843 / 889
V3 coarse, 5° / 10° 823 / 972 823 / 972
pyramid, 5° / 10° 1012 / 1060 1012 / 1060
curved (#395), 5.71° / 10° NaN / NaN 302 / 300

As you asked, the halving stays as a fallback: solve_base! halves relaxation_factor while it is above 1e-3, then warns once, whether or not log is on, naming the factor it started and stopped at and whether it converged. One warning per solve rather than one per halving keeps a caller that solves every time step, or linearize's Dual solve, from getting five per failing call. Each attempt now stops at the first non-finite residual instead of running 1,500 iterations on NaN. throw_on_fail=true still raises SolveFailure. On the suite, the only solves that still reach the halving are the three capped at max_iterations 1 or 2 on purpose, and the α = 20° post-stall artificial-viscosity solve, which fails on main too (FAILURE both, CL 0.692 → 0.668).

What the stricter solves exposed in test_unrefined_dist.jl

  1. The test polar has no zero-lift angle. solver_test_polar.csv had three points (−10° → cl 0.1, 0° → 1.0, 10° → 1.8) and extrapolates flat, so cl never drops below 0.1 at any angle. A refined tip panel sits near −10° and cannot unload, so the fixed point disappears as the tips get narrower. LOOP and NONLIN both failed at 16–20 panels on main, at every angle I tried. I added a −20° row (cl −0.8, continuing the −10…0° slope) so cl crosses zero. Pinned solver_test_wing values in the suite do not move, because no pinned case has a panel below −10°. Refined unpinned cases do move, for example 10 panels at α = 0: CL 0.4305 → 0.4279.
  2. The consistency checks had never run. They solved that wing at 20 panels, got NaN, and !isnan guards skipped every check. Once the solve converged, the checks failed by 17%: cl·c·w of a section was 0.355 against a panel sum of 0.430.
  3. The aggregation was a plain mean over panels of unequal width. cl/cd/cm/alpha_unrefined_dist are now area-weighted means, and chord_unrefined_dist is section area over width, so coefficient × chord_unrefined_dist · width_unrefined_dist is exactly the section load. The two older testsets in the same file pinned the plain mean; they now pin the area-weighted mean through one helper. No other repo reads these four fields (GitHub code search across OpenSourceAWE).
  4. moment_unrefined_dist [N·m] was divided by the panel count, while moment_coeff_unrefined_dist was summed. It is now the section sum, so linearize(...; aero_coeffs=false) agrees with the coefficient path. StableAWEModels reads only the coefficient, which does not change.

The testsets now assert solver_status == FEASIBLE after every solve, and the consistency checks are exact (rtol = 1e-12, previously 5% behind NaN guards). The isnan branches that let a NaN coefficient pass are gone, so a diverging solve can no longer hide.

x/y/z_airf_unrefined_dist are still plain means of unit vectors, so they are neither unit length nor orthogonal. Nothing reads them. That is older than this PR and needs a decision on what a section frame should be, so it is #404.

Verification

  • Reproduced first on 5de4c94: solve! → FAILURE, 39/39 gamma NaN at 5.71° and 10°; LLT on the same wing converges
  • test/solver/test_solver.jl new unit test of panel_relaxation_factors!: a panel with stiffness 2 keeps ω = 0.03 exactly, one with stiffness 100 gets 1/100 (green, 94/94 in the file)
  • test/body_aerodynamics/test_body_aerodynamics.jl new curved LOOP testset: red before (FAILURE == FEASIBLE, NaN ≈ 0.422308), green after (4807/4807)
  • test/solver/test_unrefined_dist.jl: red on main's solver and polar (11 failures: 5× FAILURE == FEASIBLE, 6× NaN ≈ NaN), green after (54/54)
  • 18 affected test files green in the worktree session: test_body_aerodynamics, test_results, test_unrefined_dist, test_solver, test_flow_curvature, test_viscous_drag_correction, test_attached_trailed_force, test_moment_units, test_forwarddiff, test_polar_vectors, test_backend_comparison, test_wing_directions, test_stability, test_verification, test_wing_constructor, test_refinement_validation, test_VortexStepMethod, bench.jl
  • gamma_loop! allocations 6 / 208 B, same as main (bench.jl bound ≤ 10)
  • After c62bd58: test_solver.jl, test_unrefined_dist.jl, test_body_aerodynamics.jl green in the worktree session
  • Docs build clean (at f7afff7) · up to date with main · local CI (agent ci-local, Julia 1.13, one cell): PASS in 8 min at f7afff7, rerun at c62bd58 running at time of writing · GitHub CI: pending
  • Benchmark: n/a. Iteration counts above are exact counts from solve_base!'s log, not timings.
  • Risk: a wing whose panels are all stiffer than 1/relaxation_factor (≈ 33 at the default) now takes smaller steps everywhere than main did. None of V3, pyramid or the test wings is like that, but a very fine uniform mesh could need more iterations.

Scope

+143 / −95 across 7 files. src/solver.jl +73 / −36: the 17-line panel_relaxation_factors!, the halving loop with its one warning, the area-weighted aggregation, docstrings wrapped to 92 columns. Tests +62 / −59: the curved solve, the relaxation-cap unit test, FEASIBLE checks, and the area_mean helper replacing six copies of the plain mean and the NaN branches. The fail-fast: false in CI.yml is its own cleanup, queued as #401.

Opened by 1-Bort-1, an AI agent working for @1-Bart-1.
Closes #395 · task VortexStepMethod.jl-395

1-Bort-1 and others added 5 commits September 30, 2026 14:18
…non-finite gamma

The :curved reference wing's stiffest mode needs relaxation below 0.0095; the
single retry at 0.015 diverged to NaN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ficients are area-weighted

Narrow tip panels set the LOOP stability limit; scaling their relaxation by
median/own stiffness lets the default factor converge on the curved wing and on
solver_test_wing up to 40 panels. The solver_test_wing polar gains a -20 deg row so
its cl crosses zero and refined tips can unload. cl/cd/cm/alpha_unrefined_dist become
area-weighted means, so coefficient times section area is the section load.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iffness mean

Leaves the relaxation of panels that were already stable unchanged: V3 and pyramid
iteration counts match main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cs entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@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 30, 2026

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

Copy link
Copy Markdown
Member 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 · 2 inline, 0 off the diff

Good

  • Does what the card says: each panel's step is min(relaxation_factor, 1/stiffness_i) (solver.jl:911), so a panel only takes a smaller step when its stiffness is above 1/ω. That supports the unchanged V3 and pyramid iteration counts in the card.
  • The residual divides each panel's step by its own panel_relaxation (solver.jl:1129), so the convergence test still measures F(gamma) - gamma. The fixed point and the tolerance mean the same as before.
  • The new buffer uses solver.cache[9] from a LazyBufferCache, so the Dual/linearize path (solver.jl:1211-1232, relaxation_factor = TD(...)) gets a buffer of the right element type without extra wiring.
  • The tests now fail on the bug instead of hiding it: solver_status == FEASIBLE after every solve, and 5% tolerances behind NaN guards became exact rtol=1e-12 checks, which area weighting makes possible. This makes the tests stricter.
  • moment_unrefined_dist is now summed like moment_coeff_unrefined_dist, so the two unrefined moments are aggregated the same way. The test at test_unrefined_dist.jl:149 pins this.
  • The area_mean helper replaces six copies of the plain mean. The polar row and the aggregation fix are both named and justified in the card as fixes to code next to the change.
  • panel_relaxation_factors! is in docs/src/private_functions.md, so its docstring can be reached from the built docs.

Not good

  • src/solver.jl:736 — A LOOP solve that does not converge now retries five times (0.015 down to 0.00094) instead of once, each up to max_iterations, so a post-stall case like the α=20° artificial-viscosity solve costs about 6× main's time for the same FAILURE. Consider capping the number of halvings, or stopping once the cap already holds every panel below the halved factor.
  • src/solver.jl:738 — The @warn is now unconditional and runs inside the halving loop, so a caller that calls solve! every simulation step, or through linearize's Dual solve, gets up to five warnings per failing call. Main kept this behind log; if the card's 'as you asked' covers only the fallback, keep the warning behind log or use maxlog.
  • Local CI was still running and GitHub CI was pending when the card was written; neither result is shown.
  • The card says the user asked for the halving fallback, but not for making the warning unconditional. Dropping the log && gate changes behaviour for callers that solve every time step.
  • Testsets 1 and 3 still have isnan(expected_cl) branches and the 'Handle NaN values that can occur in INVISCID models' comment. Now that FEASIBLE is asserted and these are VSM solves, those branches look unreachable; the compaction pass (§7) should have removed them.
  • x_airf/y_airf/z_airf/va_vec_unrefined_dist are still plain count means of unit vectors and are not renormalised, next to coefficients that are now area-weighted. This predates the PR but sits in the block it rewrote.
  • The inline comment at solver.jl:532-533 lists what each aggregate is. That belongs in the field docstrings, which already say it, so the comment fails the deletion test.
  • There is no direct unit test of panel_relaxation_factors! (for example, a panel with stiffness below 1/ω keeps ω exactly). It is covered only indirectly through the curved-wing integration solve.
  • The gamma_loop! docstring signature (solver.jl:917-919) still lists AIC_x/AIC_y/AIC_z arguments the function does not take. The PR edited the docstring right below it and left this stale.

claude, 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 src/solver.jl
Comment thread src/solver.jl Outdated
@1-Bort-1 1-Bort-1 added agent:queued Agent task state agent:running Agent task state and removed agent:ci Agent task state agent:queued Agent task state labels Sep 30, 2026
…t-test the relaxation cap

Review of #402: one warning naming the final relaxation_factor instead of one per
halving, the gamma_loop! docstring signature matches the function, and the
unrefined-dist testsets no longer branch on NaN now that FEASIBLE is asserted.

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

Copy link
Copy Markdown
Member Author

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

@codecov

codecov Bot commented Sep 30, 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 30, 2026
@1-Bort-1
1-Bort-1 requested a review from 1-Bart-1 September 30, 2026 13:22

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:review Agent task state

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The :curved reference wing does not converge: LOOP returns NaN gamma at 5.7° and 10°

1 participant