Skip to content

Normalize wing section order to +y → -y - #258

Merged
1-Bart-1 merged 8 commits into
mainfrom
fix/refined-surface-tables
Aug 17, 2026
Merged

Normalize wing section order to +y → -y#258
1-Bart-1 merged 8 commits into
mainfrom
fix/refined-surface-tables

Conversation

@1-Bart-1

@1-Bart-1 1-Bart-1 commented Aug 16, 2026

Copy link
Copy Markdown
Member

Problem

Panel y_airf and z_airf are derived from the order a wing's sections are stored in (y_airf = normalize(bound_1 - bound_2), bound_1 from section i, bound_2 from section i+1). That order was never an invariant:

  • refine!'s sort (_section_sort_key, rev=true) was the only normalization anywhere, and it is skipped whenever a caller passes sort_sections=false — which is what building a particle wing does.
  • A geometry file written -y → +y therefore travelled unmodified into BodyAerodynamics and inverted every panel normal.
  • Worse, a wing whose sections are replaced after its panels were built (a structural remesh) inverted them mid-run, one panel at a time as each crossed spanwise_direction.

Panel.reinit! compensated by re-deriving a flip from live geometry on every call. That is what produced the observed chatter — measured on a V3 particle lattice, tracking dot(z_now, z_prev) per panel per step:

step 1–50 : dot = 1.0            normals do not move at all
step 61   : dot = -0.99   FLIPPED [17, 18]
step 62   : dot = -0.999  FLIPPED [17, 18, 22, 23, 24]
step 63   : dot = -0.985  FLIPPED [19, 20, 21]
step 67   : FLIPPED 40 panels

180° in a single step, neighbouring panels disagreeing at the same step. AeroPressure builds its frozen traction pattern on z_airf, so a flipped panel's aero force reverses with it.

Every scheme that samples the order fails, because the order legitimately changes: per panel (neighbours disagree under deformation), at construction (the sections are replaced afterwards), cached in a struct field (it serializes into model bins and goes stale).

Change

Normalize instead of compensate. normalize_span_order! puts sections in +y → -y at the boundaries:

  • on YAML load, before any structural pairing exists, so a reversed file is fixed on arrival;
  • in refine!, for wings assembled through add_section!. Gated on recompute_mapping, so the per-step call that only updates positions still never reorders. sort_sections stays for a genuinely scrambled list.

obj_to_yaml and surfplan_to_aero_yaml emit +y → -y as well. Not breaking — files of either order load identically now.

With the order canonical, nothing has to decide a flip from live geometry. BodyAerodynamics drops the span_flip field it cached, and reinit! reads wing_span_flip directly as a guard for a hand-built wing.

+y → -y is the convention already in use: SurfPlan exports it (SK100 aero_geometry.yaml, +8.03 → −8.03), cfd_aero_geometry.yaml (+4.11 → −4.11), the V3 structural geometry (2.873 → −3.892) and refine!'s own sort all agree. It is also the kite seen from the front, so spanwise distribution plots now put +y on the left — via one span_axis helper replacing nine repeated Axis constructions.

Verification

V3 beam wing, whose aero geometry file is the one written the wrong way round:

inverted normals after init! after relax_bridle!
before 72 of 72 56
after 0 0

Sections also stay monotonic through relaxation now, where they did not before. The V3 particle lattice, already correctly ordered, is unchanged (0 both before and after).

Both section orderings give identical aero with the flip branch exercised:

order fwd: min dot(y_airf, +y) = 1.0 | CL = 0.40954 | CD = 0.008288
order rev: min dot(y_airf, +y) = 1.0 | CL = 0.40954 | CD = 0.008288

Also included

The shrink_wrap rolling-ball rewrite, the obj_to_yaml wingtip fix, the read_node_table speedup, the named-window plotting change, and the V3_neuralfoil / ram_air_kite example updates that were already in the tree.

reinit!(panel, …) loses its spanwise_direction positional argument and takes flip::Bool instead. That method takes internal panel_props slices and is not user-facing, so it is not a breaking change; test/panel/test_panel.jl is updated.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.11111% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
ext/VortexStepMethodMakieExt.jl 52.94% 16 Missing ⚠️
src/wing_geometry.jl 94.11% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@1-Bart-1 1-Bart-1 changed the title Decide panel spanwise orientation once per wing Normalize wing section order to +y → -y Aug 16, 2026
1-Bart-1 and others added 5 commits August 17, 2026 11:39
compute_refined_section_interpolation! reblended every refined section's
SectionAero from the unrefined sections unconditionally, while aero_data was
preserved under use_prior_polar. A wing rebuilt onto fewer structural stations
therefore kept full-resolution polars and lost the contour/Cp/cf tables that
pressure integration reads: on the TU Delft V3, 37 generated tables collapsed to
the 10 sampled onto its struts, and both the pressure loads and the lofted
airfoil plot were interpolations of those 10.

The reblend is now skipped whenever the polars are being preserved and the
refined sections already carry tables, so it still fills them the first time
round.

obj_to_yaml gains geometry_path, naming the geometry YAML instead of always
writing output_dir/geometry.yaml. Emitted table references carry the path from
the YAML's directory to output_dir, which is what the loader resolves them
against, so a generated dataset can keep its bulk in a subdirectory while the
geometry sits with the hand-written ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
reinit! re-derived each panel's flip from dot(y_airf, spanwise_direction) on
every call. A deforming wing can carry one panel's y_airf across that plane
while its neighbours stay put, so the panel's z_airf inverted by 180° in a
single step — and often back again the next one, chattering across the
threshold. The traction pattern AeroPressure freezes is built on z_airf, so the
aero force on that panel reversed with it.

The orientation follows from the order a wing's sections run in, which is a
property of its definition, so wing_span_flip computes it at BodyAerodynamics
construction and every panel of that wing is reinitialized with it. Panels of
one wing can no longer disagree and the value cannot change mid-run.

reinit!(panel, …) drops spanwise_direction and takes flip::Bool instead: it no
longer decides anything, it applies what the caller decided.

Also carries the obj_to_yaml wingtip fix, the read_node_table speedup and the
named-window plotting change that were already in the tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The panel method of reinit! takes internal panel_props slices and is not
user-facing; users call reinit!(wing) and reinit!(body_aero). An internal
signature change needs no changelog entry of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Panel y_airf and z_airf are derived from the order sections are stored in, but
that order was never an invariant: refine!'s sort was the only normalization,
and callers that build particle wings pass sort_sections=false. A geometry file
written -y -> +y therefore inverted every panel normal, and a wing whose
sections were replaced after its panels were built inverted them mid-run, one
panel at a time as each crossed spanwise_direction.

normalize_span_order! settles it at the boundaries: on YAML load, before any
structural pairing exists, and in refine! for wings assembled through
add_section!. The refine! hook is gated on recompute_mapping so the per-step
call that only updates positions still never reorders. sort_sections stays for
a scrambled list.

obj_to_yaml and surfplan_to_aero_yaml emit +y -> -y as well. Not breaking:
files of either order load the same now.

With the order canonical, nothing has to decide a per-panel or per-wing flip
from live geometry, so BodyAerodynamics drops the span_flip field it cached —
which serialized into model bins and went stale — and reinit! reads
wing_span_flip directly as a guard for hand-built wings.

Measured on a V3 beam wing whose aero geometry was written the wrong way round:
72 of 72 panel normals inverted after init! and 56 after bridle relaxation, both
now 0, with the sections staying monotonic through relaxation where they did
not before.

Spanwise distribution plots put +y on the left to match, through one span_axis
helper replacing nine repeated Axis constructions.

Also carries the shrink_wrap rolling-ball rewrite, the V3_neuralfoil and
ram_air_kite example updates already in the tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@1-Bart-1
1-Bart-1 force-pushed the fix/refined-surface-tables branch from 8352468 to f9e4529 Compare August 17, 2026 09:45
Documenter's missing_docs check fails the docs build on any docstring in a
checked module that no @docs block lists, and four new ones were not listed.
span_order_key subsumes _section_sort_key, which named the same coordinate for
the same purpose, and both it and can_reuse_prior_refined_surface_tables lose
the underscore prefix now that they are documented names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@1-Bart-1
1-Bart-1 force-pushed the fix/refined-surface-tables branch from f9e4529 to 89641dd Compare August 17, 2026 09:53
The rolling-ball rewrite rode along in the section-order commit and is unrelated
to it. It also regresses the XFoil path: the wrapped NACA0012 of
test/solver/test_backend_comparison.jl no longer converges at any angle, so
generate_aero_matrices has nothing to interpolate the NaNs from and throws.

The distance-field wrap comes back for now. The rewrite continues on
wrap/rolling-ball, together with the two resampling fixes it needs and a record
of what still fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Its positional defaults make the docstring's signature a Union of four arities,
which no call-form @docs entry can match, so Documenter counted the docstring as
missing however it was listed. The text was an orphan attached to the sort-key
helper before this branch, and the sibling refined-section overload carries none
either. Verified with a local docs build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@1-Bart-1
1-Bart-1 merged commit 567c129 into main Aug 17, 2026
9 checks passed
@1-Bart-1
1-Bart-1 deleted the fix/refined-surface-tables branch August 17, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant