Skip to content

Each panel takes its own wing's spanwise direction in calc_forces! and calculate_results - #367

Merged
1-Bart-1 merged 8 commits into
mainfrom
agent/358-calc-forces-and-calculate-results-use-th
Sep 22, 2026
Merged

1-Bart-1 merged 8 commits into
mainfrom
agent/358-calc-forces-and-calculate-results-use-th

Conversation

@1-Bort-1

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

Copy link
Copy Markdown
Member

TL;DR

calc_forces! and calculate_results now use each panel's own wing's spanwise_direction where every panel used wings[1]'s. That direction sets the panel's drag direction, and on a fin next to a wing main turned the fin's drag into lift. solve also takes wing_span and aspect_ratio_projected from all wings. Both gave wrong numbers with no error, and #355 passes the same directions on to linearize's Jacobians.

How the directions are built

spanwise_direction is a field of each Wing, a unit vector along its span given by the user (default [0, 1, 0]). A horizontal wing spans y; a vertical fin should be built with spanwise_direction = [0, 0, 1]. Each wing keeps its own; nothing here merges them.

For each panel, three directions come out of panel_force_directions:

  • lift: normal to the panel's local inflow, in the plane of its own airfoil section (cross(inflow, y_airf), from the panel's geometry). It never used spanwise_direction, so it was already right per panel on main: up on a wing, sideways on a fin.
  • drag: cross(spanwise, lift). This is where the span direction enters. With the panel's own span it points downstream. On main a fin panel crossed the wing's y with a lift that is itself along ±y, and what was left pointed along ∓z.
  • In calculate_results, the cl/cd/cs_distribution split of each panel's force uses the same span: lift normal to inflow and span, side normal to lift and inflow.

The body-level numbers take one frame, the first wing's: the body's lift/drag/side sums project every panel's force onto the lift and side directions of wings[1]'s span, and wing_span/aspect_ratio_projected measure all wings along that span. For a wing with a fin, lift is the vertical force and side the lateral one, fin included.

The figures: a 6 m × 1.5 m wing at 4° plus a fin of the same size 6 m behind it, deflected 5°, symmetric polar with cd = 0.02, va = 20 m/s along x. Left: looking downstream, the span direction each wing's panels use (green) and the panel lift (blue), the same in both. Right: from the side, the panel drag direction (red), taken from the solved forces as normalize(F − L·dir_lift). The fin's drag, 0.02 × 245 Pa × 9 m² = 44 N, goes into −Fz on main instead of Fx.

before (main): a wing and a fin deflected 5 degrees; the fin's drag points down, so body Fz is 508 N

after (this branch): same wing and fin; the fin's drag points downstream, body Fz is 552 N

The test

The test builds a wing along y and an identical wing turned 90° about x, placed 1000 m away along y so the two do not induce on each other. Inflow is along x, which the rotation leaves unchanged. On main, the turned wing's panels come out with Fx = 1.52 N where the one-wing result, rotated, gives 5.93 N. Its cl_distribution is −0.020 against 0.176–0.283, and its cs_distribution is −0.18 to −0.29 against 0. The lift and drag magnitudes (lift_dist, drag_dist) already agreed; only the directions were wrong.

What changed

  • calc_forces! and calculate_results loop wing → panel_range(body_aero, wing_idx) and use wing.spanwise_direction. panel_range is new, next to unrefined_section_range, and both are now one sum over the preceding wings. The unrefined-section accumulation in calc_forces! uses it too, which drops its hand-kept panel_idx counter and the else branch that skipped wings.
  • In calculate_results the prescribed-va split is one helper, prescribed_va_directions(va, spanwise). It is used three times: for the reference directions, and twice per panel. The per-panel cl/cd/cs_distribution use the panel's own wing. The body's lift/drag/side sums still decompose each panel in the reference frame (wings[1]'s span). Decomposing a fin panel in its own frame and then projecting lift onto lift and side onto side would drop the fin's side force from the body's side. With inflow along x, the test checks lift == Fz, side == Fy and drag == Fx on the two-wing body. For a single wing all three sums are the same arithmetic as before.
  • calculate_span(wings, spanwise_direction): a new method of the exported function, giving the extent of all wings' sections along a direction. It is hi - lo of spanwise_extent(wings, spanwise_direction), the helper Start ELLIPTIC circulation on each wing's own ellipse, so multi-wing bodies solve #366 added for one wing, now taking several wings, so the projection exists once. Start ELLIPTIC circulation on each wing's own ellipse, so multi-wing bodies solve #366's elliptic loop walks the wings with panel_range instead of its own offset counter. wing_span and aspect_ratio_projected in solve now use calculate_span along wings[1]'s span. Two touching 6 m wings give 12 m and AR 8; on main they give 6 m and AR 2.

Where I would push back

  • The body's reference span direction is still wings[1]'s: it sets the body's lift/side axes and now its span. The first wing has to be the main wing. Nothing checks this, but the only alternative is a body-frame setting nobody has asked for.
  • The projected span uses the extent along that direction, so a centreline fin adds nothing, and wings side by side or in tandem add up the way projected_area already does. Two wings far apart count the gap as span too, just as a planform would.
  • The per-panel cl/cs_distribution of a panel whose inflow runs along its own wing's span come out NaN, with no error; the reference directions throw on the same case. main has the same gap with wings[1]'s span. Turning that NaN into an error is a behaviour change, so it is not in here.
  • stability_derivatives (new from stability_derivatives gives roll, pitch and yaw rate derivatives about solver.reference_point #356) scales dp and dr by b = wings[1].span. For a wing with a fin that is the right span, but for wings side by side it is not the wing_span that solve now reports. Switching it to calculate_span(wings, wings[1].spanwise_direction) is one line, but it would change the rate derivatives of multi-wing bodies in a function that just landed, so it is not in here.

Verification

Scope

+148 / −64 ignoring whitespace (+212 / −128 plain, since the two loops gain one indent level). src/ is +66 / −64, flat, while adding the per-wing loops and the multi-wing span; the rest is the new 74-line test, one runtests.jl line, two docs entries and the changelog. Searched for wings[1], spanwise_direction and .span in src/: the other wings[1] read is stability_derivatives' b, above. For the test wing I searched test/ for function .*wing( and add_section! builders: inviscid_wing (test_body_aerodynamics.jl), flat_plate_wing (test_solver.jl) and build_flat_wing (test_billowing.jl) are file-local flat wings, none of them rotatable or carrying a polar table. The new rectangular_wing is a fourth; folding all four into one keyword-driven builder in test/test_data_utils.jl rewrites three test files this PR does not otherwise open, so it is a cleanup: PR of its own.

Closes #358 · task VortexStepMethod.jl-358

1-Bort-1 and others added 3 commits September 21, 2026 14:09
… calculate_results

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…irection

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 21, 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

  • The fix matches the card: both loops go wing → panel_range and use wing.spanwise_direction. The body sums still use wings[1]'s span, as the card says (solver.jl:445-479, body_aerodynamics.jl:950-1001).
  • For a single wing, calculate_span(body_aero.wings, reference_spanwise) gives the same number as the old wings[1].span. span is a virtual property that calls calculate_span(w) (wing_geometry.jl:1755), and that is now calculate_span((wing,), wing.spanwise_direction).
  • panel_range replaces the hand-kept panel_idx counter and the else branch that only skipped panels. The unrefined-section accumulation (solver.jl:526-573) now indexes panels the same way as the force loop.
  • The body-sum decomposition still uses the reference frame, so a fin's lift still shows up in the body's side. The test pins this: side ≈ Fy for the pair, where the fin's lift points along −y.
  • The test reproduces the bug. The rotated wing's F, cl, cd and cs must equal the solo wing's rotated results, and the card reports 5 of 9 failing on main's src.
  • The card's claim that src/ gets shorter checks out. The only other wings[1] read is body_aerodynamics.jl:517, in the elliptical initial distribution, which throws for more than one wing and is tracked as #357.
  • prescribed_va_directions and panel_range are documented in private_functions.md. The docstring of the new calculate_span method is shared with the existing one.

Not good

  • test/solver/test_wing_directions.jl:11rectangular_wing is a fourth near-copy of the flat two-section test wing, next to inviscid_wing (test_body_aerodynamics.jl:18) and flat_plate_wing (test_solver.jl:244). It differs only in rotation, offset and polar. Extending one of those with keyword arguments would keep a single builder (§2).
  • src/body_aerodynamics.jl:944 — The guard computes norm(cross(va_ref_vec, reference_spanwise)) and then throws that result away. Line 947 computes the same cross product and normalises it again inside prescribed_va_directions. The degeneracy check and the direction now have two sources three lines apart.
  • body_aerodynamics.jl:976 introduces panel_va, but line 985 still reads dot3(panel.va, va_ref_unit). The same loop reads the vector two ways.
  • The per-panel prescribed_va_directions(panel_va, spanwise_unit) has no degeneracy guard. A panel whose wing lies along its inflow gets NaN cl/cs_distribution without any error, where the reference directions throw.
  • In test_wing_directions.jl, the aspect-ratio testset passes a literal 4 for the section count, while the testset above uses 2length(body_aero.wings) for the same thing.
  • The card lists what was searched in src/ but not which test helpers were checked before writing a new wing builder (§2 'say what you searched for').

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

A rectangular wing spanning `y`, with every point turned by `rotation` and moved by
`offset`, and its spanwise direction turned with it.
"""
function rectangular_wing(rotation, offset)

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.

MINOR: rectangular_wing is a fourth near-copy of the flat two-section test wing, next to inviscid_wing (test_body_aerodynamics.jl:18) and flat_plate_wing (test_solver.jl:244). It differs only in rotation, offset and polar. Extending one of those with keyword arguments would keep a single builder (§2).

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.

Not here: inviscid_wing, flat_plate_wing and build_flat_wing are each local to a test file this PR does not open, so merging them into one builder in test_data_utils.jl means rewriting three other test files. That belongs in its own cleanup: PR. The card now says what I searched.

Comment thread src/body_aerodynamics.jl Outdated
cross3!(dir_lift_ref, va_ref_vec, spanwise_direction)
dir_lift_ref_norm = norm3(dir_lift_ref)
dir_lift_ref_norm > 0.0 || throw(ArgumentError(
norm(cross(va_ref_vec, reference_spanwise)) > 0.0 || throw(ArgumentError(

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.

MINOR: The guard computes norm(cross(va_ref_vec, reference_spanwise)) and then throws that result away. Line 947 computes the same cross product and normalises it again inside prescribed_va_directions. The degeneracy check and the direction now have two sources three lines apart.

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.

Fixed in f3c2339: the guard now checks the reference_dirs it builds, so the cross product is taken once.

@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 21, 2026
Take the panel's inflow from panel_va throughout the loop, and size the
aspect-ratio test's solver from its wing count.

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

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

1-Bort-1 commented Sep 21, 2026

Copy link
Copy Markdown
Member Author

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

…e-th

Keeps the per-wing loop in calculate_results over main's Panel.va -> va_vec
rename, and both Fixed entries in the changelog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@1-Bort-1 1-Bort-1 removed the agent:running Agent task state label Sep 21, 2026
@1-Bort-1 1-Bort-1 added agent:running Agent task state agent:queued Agent task state agent:ci Agent task state and removed agent:queued Agent task state agent:running Agent task state labels Sep 21, 2026
@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 21, 2026
#366 added spanwise_extent(wing) beside this branch's calculate_span(wings,
direction); both projected the sections on a span axis. spanwise_extent now
takes the wings and a direction, and calculate_span is its hi - lo. The
elliptic circulation loop walks the wings with panel_range. CHANGELOG keeps
both Fixed entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread CHANGELOG.md

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.

I don't fully understand how this works yet. Some figures in the PR body would help. And more explanation on what the spanwise directions are, and how the lift direction is now determined with multiple wings, and what spanwise becomes with multiple wings.

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.

Rewrote the PR description: a new section, 'How the directions are built', with before/after figures of a wing plus a deflected fin, rendered from main's src and d066ca6. In short: each Wing keeps its own spanwise_direction. A panel's lift comes from its own section geometry and never used it. The span direction sets the panel's drag, cross(spanwise, lift), and on main a fin's drag came out along -z, 44 N of lost lift in the figure. The body's lift/side sums and wing_span stay in the first wing's frame, as before.

@1-Bort-1 1-Bort-1 added agent:queued Agent task state agent:running Agent task state agent:ci Agent task state agent:review Agent task state and removed agent:running Agent task state agent:queued Agent task state agent:ci Agent task state labels Sep 21, 2026
@1-Bort-1
1-Bort-1 requested a review from 1-Bart-1 September 21, 2026 21:44
@1-Bart-1
1-Bart-1 merged commit fe89de7 into main Sep 22, 2026
7 checks passed
@1-Bart-1
1-Bart-1 deleted the agent/358-calc-forces-and-calculate-results-use-th branch September 22, 2026 12:00
@1-Bort-1 1-Bort-1 added agent:done Agent task state and removed agent:review Agent task state labels Sep 22, 2026
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.

calc_forces! and calculate_results use the first wing's spanwise_direction for the panels of every wing

2 participants