Skip to content

linearize and make_dual_shadow take a BodyAerodynamics with several wings - #355

Open
1-Bort-1 wants to merge 5 commits into
mainfrom
agent/344-linearize-and-make-dual-shadow-accept-a-
Open

1-Bort-1 wants to merge 5 commits into
mainfrom
agent/344-linearize-and-make-dual-shadow-accept-a-

Conversation

@1-Bort-1

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

Copy link
Copy Markdown
Contributor

TL;DR

linearize and make_dual_shadow no longer throw on a body with more than one wing: the dual shadow copies every wing, and theta_idxs/delta_idxs run over the unrefined sections of all wings in order. #330's stability derivatives need this to cover multi-wing bodies.

What changed

Only the one-wing guards stood in the way. solve! already handled several wings, and calc_forces! already filled moment_unrefined_dist wing by wing, offset by the section counts of the wings before. The inputs now use that same order, from the same place.

  • make_dual_shadow builds the shadow body from _wing_with_eltype of every wing, not just wings[1]. It also passes on Add the Gaunaa et al. 2024 spanwise-flow viscous drag correction as an opt-in solver setting #352's is_with_viscous_drag_correction, which came in with the merge from main.
  • A private unrefined_section_range(body_aero, wing_idx) gives the indices of one wing's unrefined sections. calc_forces! now uses it instead of its own running offset, so the outputs and the inputs share one definition of the order.
  • A private method unrefined_deform!(body_aero, theta_angles, delta_angles) hands each wing a view of its own range of angles. linearize calls it, then reinit! as before.
  • linearize checks theta_idxs/delta_idxs against U, the solver's total number of unrefined sections, which is also the length of moment_unrefined_dist. The result length is 6 + U.
  • Cleanup nearby, no behaviour change:
    • om is renamed omega and be is renamed ad_backend.
    • s is renamed section in _wing_with_eltype.
    • The unused smooth=false argument is no longer passed.
    • A wing_pair test helper replaces four copies of the same two-wing setup.

Tests

Three testsets under solve! on a two-wing body in test/body_aerodynamics/test_body_aerodynamics.jl.

  • Each wing gets its own angles. unrefined_deform! on the body with a different twist and deflection for every section must give each wing the same theta_dist and delta_dist as deforming that wing on its own with its range of angles. The expected ranges are written out by hand, not taken from the helper. The INVISCID Jacobians below cannot see deflection, so this is the only check on the deflection split.
  • Wings 1e4 m apart. Each wing's twist moves only its own sections' moments. Twisting wing 2 moves wing 2's rows the same way twisting wing 1 moves wing 1's: measured relative difference 1.3e-12, test bound 1e-9. The cross blocks are 5.7e-9 of the diagonal blocks, test bound 1e-7. If the split into wings were wrong, these would be off by O(1).
  • Wings one chord apart, so they induce on each other. The ForwardDiff Jacobian matches AutoFiniteDiff (steps 1e-6) to a relative difference of 2.7e-7. The test allows 1e-4.

Notes for the reviewer

  • Error message: a body with no unrefined sections and some theta_idxs now gets the length-mismatch ArgumentError instead of "Cannot use theta_idxs…". It is the same exception type.
  • Still one wing: solve! and linearize on a multi-wing body throw with type_initial_gamma_distribution=ELLIPTIC, from the guard in calculate_circulation_distribution_elliptical_wing. The default (ZEROS) works. Follow-up: ELLIPTIC initial circulation for a body with more than one wing #357.
  • Now reachable through linearize: calc_forces! and calculate_results use wings[1].spanwise_direction for the force directions of every panel, so wings with different span directions (a vertical fin, say) get wrong forces from wing 2 onward. The fix changes results, so it is not made here. Follow-up: calc_forces! and calculate_results use the first wing's spanwise_direction for the panels of every wing #358.
  • A wing with no unrefined sections cannot reach unrefined_deform!(body_aero, ...): BodyAerodynamics refuses a wing that has not been refined ("Wing 2 has not been refined"), and refining needs sections.
  • CI setting: .github/workflows/CI.yml still sets fail-fast: false. That is a separate cleanup: PR, not done here.

Verification

Scope

+135 / −47 across 5 files. src/solver.jl shrinks (−13 net): three per-wing checks became one loop, and calc_forces! lost its running offset. src/body_aerodynamics.jl +31 is the range helper and the body method. The test file's +67 net is the three testsets and the wing_pair/linearize_body helpers.

Closes #344 · task VortexStepMethod.jl-344

1-Bort-1 and others added 2 commits September 17, 2026 01:15
theta_idxs and delta_idxs run over the unrefined sections of all wings in
order, the order calc_forces! fills moment_unrefined_dist in.

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

  • Matches the card: the one-wing guards in make_dual_shadow and linearize are removed, and the only new source is unrefined_deform!(body_aero, ...); git diff 246237a --stat shows +103/−41 across the 4 files the card names
  • U really is the total number of unrefined sections across wings (src/solver.jl:194 sums n_unrefined_sections), so n_results = 6 + U gives the same size the old length(moment_unrefined_dist) did
  • Angles are split per wing in the same order calc_forces! fills moment_unrefined_dist (the unrefined_idx += wing.n_unrefined_sections loop at src/solver.jl:474-520), so inputs and outputs line up
  • Three validation branches became one loop with no change in behaviour except the error message the card mentions; no test checks the old 'Cannot use theta_idxs' text (grep finds none)
  • Dropping smooth=false changes nothing: it is already the default in unrefined_deform!(wing, ...; smooth=false) at src/wing_geometry.jl:428
  • The far-apart testset would catch a wrong offset: if wing 2 were handed wing 1's slice, the second diagonal block would be zero or off by O(1) against own_first
  • The renames (omomega, bead_backend, ssection) and the wing_pair helper are named in the card as cleanup and are plain Julia locals and test code, not API
  • The new docstring is reachable: the bare unrefined_deform! entry in docs/src/private_functions.md:84 picks up every method's docstring
  • Every added line fits in 92 columns; the long lines in src/body_aerodynamics.jl were already there

Not good

  • test/body_aerodynamics/test_body_aerodynamics.jl:536delta_idxs is split per wing, but the INVISCID wings ignore deflection, so every delta column is zero and no assertion reads them. A swapped or shifted delta offset in unrefined_deform!(body_aero, ...) would pass both testsets.
  • The far-apart test uses rtol=1e-4 and a 1e-4 cross-block bound, but the card measured 1.3e-12 and 5.7e-9, so the test allows about 8 orders of magnitude more than it needs to pin the split
  • unrefined_deform!(body_aero, ...) is a third place that works out per-wing unrefined offsets (with calc_forces! and calculate_results); a shared per-wing range helper would give one source for that order
  • If one wing has no non_deformed_sections, the per-wing call now throws 'Call refine!' even for an empty view, where the old code gave a clear idxs error; this is an edge case and nothing tests it
  • The shadow body built from wings with different panel counts dispatches dynamically, and no test covers it; the card flags this and it is not blocking
  • The card lists the ELLIPTIC guard and wings[1].spanwise_direction in calc_forces! as known limits but links no follow-up issue; now that linearize reaches the second one, it should have an issue

claude, rubric CLEAN_CODE.md. A different lab from the implementer
on purpose: a reviewer sharing its blind spots would not flag its mistakes.

solver = Solver(body_aero; use_gamma_prev=false, rtol=1e-10)
y0 = [zeros(2n_sections); body_aero.va; zeros(3)]
return VortexStepMethod.linearize(solver, body_aero, y0;
theta_idxs=1:n_sections, delta_idxs=n_sections+1:2n_sections,

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: delta_idxs is split per wing, but the INVISCID wings ignore deflection, so every delta column is zero and no assertion reads them. A swapped or shifted delta offset in unrefined_deform!(body_aero, ...) would pass both testsets.

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.

Fixed in 574b15b: the new testset "unrefined_deform! hands each wing its own run of angles" fails when wing 2 is handed wing 1's deflections (4884 passed, 1 failed).

@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 16, 2026
@codecov

codecov Bot commented Sep 16, 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:ci Agent task state agent:review Agent task state and removed agent:running Agent task state agent:ci Agent task state labels Sep 16, 2026
@1-Bort-1

1-Bort-1 commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

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

@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 17, 2026
…t-a-

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@1-Bart-1

Copy link
Copy Markdown
Member

Address the comments, fix the conflicts, and open a new issue to allow multi-wing to work with elliptic distribution.

…e split

unrefined_section_range(body_aero, wing_idx) replaces the running offsets in
calc_forces! and unrefined_deform!(body_aero, ...). A new testset checks that
each wing receives its own twist and deflection angles, which the INVISCID
Jacobian tests could not see, and the far-apart Jacobian bounds are tightened
to what they measure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@1-Bort-1 1-Bort-1 added agent:queued Agent task state and removed agent:running Agent task state agent:queued Agent task state labels Sep 17, 2026
@1-Bort-1 1-Bort-1 added the agent:running Agent task state label Sep 17, 2026
…t-a-

Keeps main's va_vec and this branch's omega in linearize.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@1-Bort-1 1-Bort-1 added agent:queued Agent task state agent:running Agent task state and removed agent:running Agent task state agent:queued Agent task state labels Sep 17, 2026
@1-Bort-1

Copy link
Copy Markdown
Contributor Author

Conflicts: merged main in 4ccdea6 and again in 0b28330. In the second merge, linearize keeps main's va_vec and this branch's omega. Review comments: 574b15b (deflection-split test, far-apart bounds tightened to 1e-9/1e-7, one unrefined_section_range for calc_forces! and unrefined_deform!). ELLIPTIC on several wings: #357. wings[1].spanwise_direction: #358.

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

linearize and make_dual_shadow accept a BodyAerodynamics with more...

2 participants