Skip to content

Take the panel normal from the quarter-chord step - #273

Open
1-Bart-1 wants to merge 2 commits into
mainfrom
fix/panel-normal-quarter-chord
Open

Take the panel normal from the quarter-chord step#273
1-Bart-1 wants to merge 2 commits into
mainfrom
fix/panel-normal-quarter-chord

Conversation

@1-Bart-1

@1-Bart-1 1-Bart-1 commented Sep 5, 2026

Copy link
Copy Markdown
Member

Fixes #272.

panel_axes built the panel normal from the leading-edge step:

span_vec = panel_span_vector(le_1, te_1, le_2, te_2)   # quarter-chord step
y_airf   = orient .* (span_vec ./ width)
z_cross  = cross(x_airf, le_1 .- le_2)                 # leading-edge step

The bound vortex, width and y_airf all come from the quarter-chord step, so
z_airf was the odd one out. On a panel whose two sections have
differently-directed chords the normal is then not square to the vortex the loads
are built from, and since alpha = atan(v·z_airf, v·x_airf) feeds every polar
lookup, the error propagates into cl, cd and cm for that panel.

One character of the fix: cross(x_airf, span_vec). The frame now closes as
z_airf = x_airf × y_airf, which is what the Panel field docstring
(src/panel.jl:28) has always claimed.

Why it went unnoticed

Taper alone cannot trigger it. (le_1 - le_2) - span_vec equals
¼(chord_vec_2 - chord_vec_1), which is purely chordwise when the two chords are
parallel, and cross(x_airf, ·) annihilates the chordwise part exactly. It takes
twist, sweep or dihedral — something that changes the chord direction between
the two sections — for the normals to separate at all.

The reference wings barely do that:

reference wing angle(LE step, QC step) normal changes?
:rectangular 0.84°, from its ±0.5 rad twist yes
:curved 0.00° — LE and TE share (y, z) no
:elliptical 34.01° no — planar, so the normal is ±ẑ regardless

The elliptical wing separates the two steps by 34° and still cannot see the
difference. Nothing in the suite combines sweep with a changing chord direction,
which is the only regime where the choice matters.

Changes to the reference oracle

test/thesis_oriol_cayon.jl makes the same mix (its x_airf is the normal, its
z_airf the spanwise axis):

x_airf = cross(VSMpoint - LLpoint, section["p2"] - section["p1"])  # LE step
z_airf = bound["x2"] - bound["x1"]                                 # QC step

so the inconsistency is inherited from the original formulation rather than
introduced by the port, and the oracle has to move with the code or it pins the
old convention. It now takes the normal from the bound filament it already builds
— a two-line change that reuses bound["x2"] - bound["x1"] instead of recomputing
from the leading edges.

This is the part worth reviewing carefully, since it is the test changing to
match the code. It is defensible here because the oracle's own spanwise axis is
already the quarter-chord step, so the change makes it self-consistent rather than
merely agreeing with us. Without it, 156 assertions fail (2 models × 39 panels × 2
asserts, all on :rectangular) at ~0.25° — real but small.

To avoid leaning on the oracle for the thing being fixed, the new test in
test/panel/test_panel.jl states the invariant directly on a swept, twisted panel
where the two definitions are 13.7° apart: the normal is square to the bound
vortex and to the chord, the frame closes as z = x × y, and orient flips y
and z together. It carries a guard asserting the geometry actually discriminates
the two definitions, so it cannot silently decay into a test of nothing.

Measured effect

SK100 leading-edge-inflatable kite, 44 panels, outermost rib bay raked 58–68°,
where the old and new normals are 5.6–13.8° apart. Coupled beam + VSM flight,
identical settle (1200/1200 steps):

z_cross from steps flown before the VSM solve stops converging
le_1 - le_2 122
span_vec 189

Same failure mode either way — a tip-stall divergence — so the frame error
aggravates it rather than causing it, but it is worth ~55% more flight time here.

Deliberately not included

Orthogonalising y_airf against x_airf. It measures better on the kite (217
steps), but that metric records how long a stall divergence is deferred, not
accuracy, and both of y_airf's uses argue for keeping the raw quarter-chord
direction: dir_lift is Kutta–Joukowski (F = ρ v × Γ, Γ along the bound
vortex), and q_dyn = ½ρ|v × y|² is the simple-sweep normal-to-quarter-chord
component. Settling that needs a swept, tapered validation case with a known
answer, which does not exist in the suite yet. Left open on #272.

Downstream

SymbolicAWEModels.jl rebuilds this same expression symbolically in
build_panel_force_eqs and again in wagner_reference_frame. Those need the
matching change or its symbolic force assembly disagrees with this solver.

🤖 Generated with Claude Code

panel_axes built z_airf as cross(x_airf, le_1 - le_2) while the bound
vortex, width and y_airf all come from panel_span_vector. On a panel whose
two sections have differently-directed chords the normal was not square to
the vortex the loads are built from, and alpha is measured against that
normal, so every panel coefficient inherited the error. The frame now closes
as z_airf = x_airf x y_airf, which is what the Panel field docstring already
described.

Taper alone never triggered it: (le_1 - le_2) - span_vec is a quarter of
chord_vec_2 - chord_vec_1, purely chordwise when the two chords are parallel,
and cross(x_airf, .) annihilates it. Twist, sweep and dihedral do trigger it.

The reference implementation makes the same mix, so it moves with the code:
its normal now comes off the bound filament it already builds. Its three
wings barely exercise the difference - the elliptical one separates the two
steps by 34 degrees and still cannot see it, being planar - so the new panel
test states the invariant directly on a swept, twisted panel where the two
definitions are 13.7 degrees apart.

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

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Comment thread src/panel_aerodynamics.jl Outdated
Airfoil frame and size of the panel between two sections, as
`(; x_airf, y_airf, z_airf, chord, width)`.

`x_airf` is chordwise and `y_airf` runs along the quarter-chord line the bound

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.

@1-Bort-1 rewrite this functions docstring

@1-Bort-1

1-Bort-1 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Picked up as VortexStepMethod.jl-273, on the branch this pr already has.

@1-Bort-1 1-Bort-1 added agent:queued Agent task state agent:running Agent task state and removed agent:queued Agent task state labels Sep 6, 2026
@1-Bort-1

1-Bort-1 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixed

The panel_axes docstring explained why the normal comes from the
quarter-chord step. CLEAN_CODE.md §4: a docstring says what, never why — the
rationale belongs in the PR body above, where it is dated and cannot rot.

Changes

  • src/panel_aerodynamics.jl:46 — docstring rewritten to state only what comes
    back: x_airf chordwise, y_airf along the quarter-chord line and not square
    to it on a swept panel, z_airf the normalised x_airf × y_airf, plus what
    the two optional arguments mean. The chord_weight paragraph's
    symbolic-build rationale went with it — the offset form it describes is
    visible in lean two lines below.
  • CHANGELOG.md — same treatment: two paragraphs of derivation cut to the
    user-visible statement, five lines.

Verification

  • test/panel/test_panel.jl green on the box (24 + 8 passes, 0 errors) ·
    local full-suite mirror started · GitHub CI: pending
  • Docs build not run — docs/ carries no manifest and instantiating it is out
    of bounds; nothing was added or renamed, and the one @ref
    (panel_chord_weight) is unchanged.
  • Risk: none. This commit touches no executable code.

Commits

  • 39d757f Say what the panel frame is, not why it is that way

Review just these changes

Task VortexStepMethod.jl-273

@1-Bort-1 1-Bort-1 added agent:review Agent task state and removed agent:running Agent task state labels Sep 6, 2026

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

Copy link
Copy Markdown
Contributor

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 · 0 inline, 0 off the diff

Good

  • The one-line fix cross(x_airf, span_vec) is mathematically sound: z_airf = orient·(x×y) exactly equals the documented x_airf × y_airf (normalised), verified against panel.jl:28 and panel_span_vector (quarter-chord step).
  • The docstring rewrite removes the stale chord_weight rationale (a "why") and states what each axis is, matching rubric §4; the x_airf × y_airf normalised claim is exactly what the code computes.
  • The new test states the invariant independently of the oracle — square to vortex/chord, z = x×y closure, orient flips — plus a guard (separation > 5) so it cannot decay into testing nothing.

Not good

  • The matching build_panel_force_eqs/wagner_reference_frame change in SymbolicAWEModels.jl is deferred with no linked issue/task, so the two repos risk silently diverging on z_airf.
  • The orient test passes chord_weight=0.5 positionally (line 202), burying a magic number that duplicates the signature default.

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

@1-Bort-1

1-Bort-1 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

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

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.

Panel normal z_airf is built from the leading-edge step, not the quarter-chord step the bound vortex uses

2 participants