Skip to content

Spread sliced sections evenly over the span, not over leading-edge arc length - #350

Merged
1-Bart-1 merged 9 commits into
mainfrom
agent/275-section-slicing-spreads-stations-by-lead
Sep 21, 2026
Merged

1-Bart-1 merged 9 commits into
mainfrom
agent/275-section-slicing-spreads-stations-by-lead

Conversation

@1-Bort-1

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

Copy link
Copy Markdown
Member

TL;DR

station_indices now places the n_sections targets evenly along the quarter-chord line with its chordwise (x) step dropped, instead of along the leading edge's arc length. At a raked or closing tip the leading edge runs almost straight aft, so arc-length targets piled into the last few centimetres of span, and wingtip_distance gave almost no inset there.

What was wrong

march_edges summed the full 3D leading-edge step into arclen, and station_indices spread its targets over that. A station where the leading edge moves 40 mm aft for 3 mm of span counts as 40 mm, so the tip cap takes a share of the sections far beyond its span.

The SK100 mesh from the issue isn't in the repo, so I reproduced the problem on a synthetic march: a straight leading edge over |y| ≤ 1 m, then 20 cap stations per side running 40 mm aft per 3 mm of span (half-span 1.06 m). With 9 sections, main gives:

y    = [-1.06, -1.027, -0.9, -0.45, 0.0, 0.45, 0.9, 1.027, 1.06]
diff = [0.033, 0.127, 0.45, 0.45, 0.45, 0.45, 0.127, 0.033]   (even spacing: 0.265)
wingtip_distance = 0.3 → outermost y = ±1.039                 (a 0.3 m inset: ±0.76)

A 0.3 m wingtip_distance moved the outermost section 2 cm. That is the "1.2 m of tip inset gives 4.9 cm of span" from the issue.

What changed

  • station_indices computes the quarter-chord point of every marched station and adds up hypot(Δy, Δz) between neighbours. The targets, the min_chord_frac trim and wingtip_distance all use that length now.
  • march_edges no longer returns arclen. station_indices was its only reader.
  • Docstrings, docs/src/airfoil_pipeline.md, docs/src/settings.md and the WingSettings field doc now describe wingtip_distance as a spanwise length [m]. The changelog entry is under Changed, marked BREAKING:: the same mesh and the same wingtip_distance give different section positions, so tuned values and the geometry generated from them have to be redone.
  • test/plotting/test_plotting.jl now asks cl, not cd, whether the deflection reached the plot. That assertion is the red check this branch carried: the polar tables are written with %.4f and Cd ≈ 0.009, so a 1° deflection moves Cd by about 2e-5 and rounds away, leaving the deflected and undeflected curves bit-identical. On main it passed on one unit in the last written decimal — one bracketing section rounding to 0.0099 against the other's 0.0098 — which re-slicing the fixture takes away. cl moves by 0.038 over the same deflection, and the undeflected curve is pinned on cl too, so that call is now checked against the operating point it is supposed to fall back to. The table precision itself is POLAR_MATRICES tables are written with %.4f, so Cd carries no deflection dependence at small deltas #362.

Why not plain y, the issue's first suggestion? The repo's ram-air kite curls down at the tips. Near station 80 the leading edge moves dy=0.0185, dz=-0.1206 per step. Placing sections by y would spread the outer ones far apart along the real span. Dropping only x handles both a curved span and a raked tip.

Why drop x only here? At a closing tip the leading edge rakes aft until it meets the trailing edge, and somewhere along that curve it stops being a leading edge and becomes the side of the wing, at no point one could mark. A quarter-chord length that keeps x still counts that side as span, so only dropping the chordwise step stops the cap soaking up sections: it spends 25.9 m of arc per metre of span.

Refinement keeps the full 3D quarter-chord length. LINEAR/COSINE refinement works on sections that are already cut, where the quarter-chord line is the bound vortex. That line is what Gaunaa et al. 2026 (TORQUE, CP1) define as the local span direction, and the solver's forces and perpendicular velocity are built on it. So the two measures differ on purpose: dropping x is a slicing rule for an ill-defined tip, not a new definition of span. A swept wing refined with LINEAR therefore gets panels of equal bound-vortex length, not equal yz width. For a short while this branch moved refinement onto the slicer's measure too; f06df0b reverts that, and the tree is identical to bfb6487.

On a strongly raked tip

Synthetic wing whose tip rakes 1 m aft over 10 cm of span, 12 sections, wingtip_distance 0.05: main (LE arc length), quarter chord with x kept, and this branch (quarter chord, x dropped)

A synthetic thick wing, chord 1 m, straight to |y| = 1 m, then an elliptic tip whose leading edge sweeps the full metre aft to meet the trailing edge over the last 10 cm of span. It is sliced into 12 sections with wingtip_distance = 0.05 and 240 march bins. All three columns share one march and one set of axes. The left column runs main's obj_slice.jl, loaded from origin/main. The middle column is a probe that measures the quarter-chord length with x kept. The right column is this branch.

placement outermost section y section gaps largest / smallest
main: LE arc length 1.100 0.009 – 0.357 m 38.7
quarter chord, x kept 1.100 0.013 – 0.312 m 23.8
this branch: quarter chord, x dropped 1.050 0.183 – 0.193 m 1.1

Main puts three of its 12 sections in the last 4 cm, outboard of the 5 cm inset it was asked for. Keeping x in the quarter-chord length only halves the problem, because the side of the wing still counts as span. Dropping x puts the outermost section on the inset line and spaces the rest evenly.

Effect on the repo's ram-air kite

Ram-air kite front view, 10 sections: before, placed by leading-edge arc length; after, evenly over the span

The "before" panel ran main's obj_slice.jl (loaded from origin/main) on the same march, with the same n_sections=10, wingtip_distance=0 and axes. The largest panel span divided by the smallest drops from 1.44 to 1.10. This mesh has no long raked cap, so the change is modest here.

With wingtip_distance=0.05 it is not modest, and not in the way I'd have liked. 5 cm of span inboard of the last sliceable station is marched station 79, where arc-length placement landed on station 80 and walked past it — and station 79 is a bad cut: it reports a section 0.407 m deep on a 0.506 m chord (t/c = 0.80) where stations 78 and 80 give 0.19 and 0.17. That is #361, a stale tangent in march_edges that predates this branch; I measured a four-line fix and left it there rather than in this diff, because it moves every station's frame on every mesh. Until it lands, this change makes that station reachable: at n_sections=4 (the plotting fixture) the blob survives into the output and NeuralFoil clamps its whole table at Cd = 1.0000. obj_to_yaml's own degeneracy guard catches it from about n_sections=10 upward — thickness=0.428 against a median of 0.122 trips 2 × median and the section borrows its neighbour's airfoil — so the exposure is meshes sliced into very few sections, where half the sections are tip blobs and the median that guard trusts is itself polluted.

Where I'd push back

  • Sections still snap to the nearest marched station. When n_sections gets close to the number of stations, rounding dominates. On this mesh, n_sections=45 with the default n_bins=60 gets worse: the span ratio goes from 2.07 to 4.45, because the curled tip stations are about 0.12 m apart in y-z while the inboard ones are 0.055 m apart. With n_bins=240 it improves instead, from 1.90 to 1.36. Placing targets finer than the station spacing would mean interpolating between stations, which is a separate change.
  • I couldn't check the SK100 cap. Its numbers in the issue (d_arc=0.0816, dx=0.0807, dy=0.0032) leave about 12 mm of z per station. If that z is the nose sliding vertically as the section closes, rather than real span, those stations still count about 3.8× their y step (it was 25.9×). Someone with that mesh should rerun the panel-sweep table.
  • Old outputs don't regenerate on their own. The wingtip_distance values and generated geometries already out there were placed the old way. obj_to_yaml reuses an existing geometry.yaml, and test/generated/ is keyed on settings rather than on the code, so both have to be regenerated.

Left out

Verification

  • Reproduced first on a synthetic raked-tip march: main gives the spacing and inset shown above. On main the new test's march errors with FieldError: type NamedTuple has no field arclen; with arclen supplied it fails on the numbers.
  • test/obj_adapter/test_obj_adapter.jl "station_indices spreads sections evenly in span past a raked tip": green after the change (3/3, juliaserver)
  • The red check reproduced locally with test/generated/ cleared — POLAR_MATRICES panel is evaluated at the passed delta failed on deflected[2] != stored[2] with CI's own numbers to 15 digits (0.6330187742107254, …), 111 passed / 1 failed; green after both test commits, 112/112 (juliaserver, Julia 1.13)
  • Affected files, with test/generated/ rebuilt by this code: obj_adapter/test_obj_adapter.jl 55/55 · settings/test_settings.jl 33/33 · test_refinement_validation.jl 15/15 · wake/test_wake.jl 3/3 · body_aerodynamics/test_results.jl 30/30 · solver/test_forwarddiff.jl 7/7 (on 9af0690); after merging main's Solver constructor deprecation, the two files that merge touched re-run green: test_obj_adapter.jl 55/55 and test_plotting.jl 112/112 (juliaserver, Julia 1.13.0)
  • Docs build clean (size warnings only, CI Documentation job green on f06df0b) · no REUSE lint in this repo · merges cleanly with current main
  • Local CI mirror, full Pkg.test() on Julia 1.13.0 with test/generated/ regenerated: PASS on f06df0b, 6713 passed and 1 broken (the existing @test_skip in test_kite_geometry.jl), 5m · GitHub CI: PASS on f06df0b, Julia 1.12 and 1.13 on Linux, macOS and Windows, plus docs and the setup workflow
  • Raked-tip figure: wingtip_distance = 0.05, 12 sections, gap ratio 38.7 on main and 1.1 on this branch (script in the section above, main's slicer loaded from origin/main)
  • Benchmark: n/a
  • Risk: wingtip_distance now walks a mesh into stations arc length used to skip, and one of them on this repo's own kite is the bad cut of march_edges gives each station the previous station's leading-edge step, so a cut near a closing tip grazes the surface and reports an 80%-thick section #361.

Scope

+62 / −35 across 8 files. obj_slice.jl is about net zero: arclen comes out of march_edges and the spanwise sum goes into station_indices. Also in the diff: the new test (+19), the changelog (+7), docstring and doc wording, and two lines of test_plotting.jl. Rewrapping the obj_to_yaml docstring paragraph is the only tidy-up. I searched for arclen, station_indices, wingtip_distance, "arc length" and quarter-chord helpers; none existed to reuse.

Closes #275 · task VortexStepMethod.jl-275

…c length

station_indices now places its targets along the quarter-chord line with the
chordwise component dropped, so a tip whose leading edge runs aft no longer
gathers sections, and wingtip_distance is a spanwise inset. march_edges drops
its arclen field, which only station_indices read.

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

Good

  • The fix matches the card: station_indices now spreads targets over the quarter-chord hypot(Δy, Δz) length, and the min_chord_frac trim and wingtip_distance use that same length (obj_slice.jl:464-473).
  • Removing arclen from march_edges is safe: a repo-wide rg arclen finds no other reader, and plot_slices_3d in the Makie extension only calls station_indices.
  • The docs are consistent: settings.jl, settings.md, airfoil_pipeline.md, both docstrings and the changelog all call wingtip_distance a spanwise length [m], and rg 'arc length' finds nothing stale in the slicer path.
  • The new test checks both even spacing and the size of the inset; I worked it through by hand: all 61 stations stay above min_chord_frac, the targets land within one 0.05 m station of the expected spacing, and the tolerance holds.
  • The change stays in scope: the only tidy-up is rewrapping the obj_to_yaml docstring, as the card says, and min_chord_frac passthrough and #272/#273 are left out on purpose.

Not good

  • src/obj_adapter/obj_slice.jl:468 — The card says this matches what refine_mesh_for_linear_cosine_distribution! measures, but that function (and compute_refined_section_interpolation!) uses full 3D quarter-chord length with x included. The repo now has two different ideas of span position, and refinement still places panels by the length this PR stops using. Either correct the card and say so, or explain why the two may differ.
  • The test has z = 0 everywhere, so it only checks that x is dropped. Changing the code to use plain y would still pass, even though the card's reason for including z is the curled ram-air tip.
  • If closed tip stations step only in x, their span values equal span[first(usable)]. argmin then returns the first of the tied indices, which is a closed station, and that silently undoes the min_chord_frac trim on the left tip. This is unlikely on real meshes but worth a line in the card.
  • The station_indices docstring is 6 lines, above the rubric's 1-4. Its last two sentences could be cut down to state only what is left out and what wingtip_distance does.

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

span = zeros(length(quarter_chord))
for i in 2:length(span)
step = quarter_chord[i] .- quarter_chord[i-1]
span[i] = span[i-1] + hypot(step[2], step[3])

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 card says this matches what refine_mesh_for_linear_cosine_distribution! measures, but that function (and compute_refined_section_interpolation!) uses full 3D quarter-chord length with x included. The repo now has two different ideas of span position, and refinement still places panels by the length this PR stops using. Either correct the card and say so, or explain why the two may differ.

@1-Bort-1

1-Bort-1 commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

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

@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:review Agent task state and removed agent:ci Agent task state labels Sep 16, 2026
Comment thread CHANGELOG.md Outdated
- The Makie `plot!` methods for a `Panel` or a `BodyAerodynamics` return a
`Vector{Makie.AbstractPlot}` instead of a `Vector{Any}`; for a `BodyAerodynamics`
drawn as flat panels it is one flat list rather than a list per panel.
- `obj_to_yaml` and `perpendicular_sections` spread the sections evenly over the span,

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.

this is a breaking change

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.

Marked BREAKING: in 1fe72b9.

@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 20, 2026
The same mesh and wingtip_distance now give different section positions, and
march_edges no longer returns arclen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@1-Bort-1 1-Bort-1 added agent:ci Agent task state and removed agent:running Agent task state labels Sep 20, 2026
1-Bort-1 and others added 2 commits September 20, 2026 18:53
The polar tables are written with four decimals, and a 1 deg deflection moves
Cd by about 2e-5 on these sections, so the two curves are bit-identical once
the fixture re-slices.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cd check could not tell the two operating points apart, so it held whatever
delta the plot used.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@1-Bort-1 1-Bort-1 added agent:ci Agent task state and removed agent:running Agent task state labels Sep 20, 2026
The cumulative-length helper allocated three arrays per call, which took
refine! past its allocation budgets in test/bench.jl (UNCHANGED 12 > 5).
spanwise_step now gives the length between two sections as a scalar and
span_position walks the sections, so refine! allocates less than before
the branch (UNCHANGED 0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@1-Bort-1 1-Bort-1 changed the title Spread sliced sections evenly over the span, not over leading-edge arc length Place sliced sections and LINEAR/COSINE panels evenly over the span, measured without the chordwise step Sep 21, 2026
@1-Bort-1 1-Bort-1 added agent:ci Agent task state agent:review Agent task state agent:steered Agent task state and removed agent:running Agent task state agent:ci Agent task state agent:review Agent task state labels Sep 21, 2026
Reverts 09d4e84 and 40f69c3. Only the slicer drops the chordwise step:
there the leading edge turns into the side of the wing at no definite
point. Refinement works on sections already cut and keeps measuring the
bound vortex line, the local span direction of Gaunaa et al. 2026 (CP1).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@1-Bort-1 1-Bort-1 changed the title Place sliced sections and LINEAR/COSINE panels evenly over the span, measured without the chordwise step Spread sliced sections evenly over the span, not over leading-edge arc length Sep 21, 2026
@1-Bort-1

Copy link
Copy Markdown
Member Author

Tried in 40f69c3, then reverted in f06df0b after we talked it through: only the slicer drops x, because there the LE turns into the side of the wing at no definite point. Refinement keeps the full 3D quarter-chord length, the bound-vortex span of Gaunaa 2026 CP1.

@1-Bort-1 1-Bort-1 added agent:queued Agent task state agent:steered Agent task state agent:running Agent task state agent:ci Agent task state agent:review Agent task state and removed agent:steered Agent task state agent:queued Agent task state agent:running Agent task state agent:ci Agent task state labels Sep 21, 2026
@1-Bart-1
1-Bart-1 merged commit adff953 into main Sep 21, 2026
7 checks passed
@1-Bart-1
1-Bart-1 deleted the agent/275-section-slicing-spreads-stations-by-lead branch September 21, 2026 15:32
@1-Bort-1 1-Bort-1 added agent:done Agent task state and removed agent:review Agent task state labels Sep 21, 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.

Section slicing spreads stations by leading-edge arc length, which collapses to nothing in span at a raked tip

2 participants