Skip to content

march_edges takes each station's tangent from its neighbours, so no cut near a closing tip grazes the surface - #376

Merged
1-Bart-1 merged 6 commits into
mainfrom
agent/361-march-edges-gives-each-station-the-previ
Sep 22, 2026
Merged

1-Bart-1 merged 6 commits into
mainfrom
agent/361-march-edges-gives-each-station-the-previ

Conversation

@1-Bort-1

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

Copy link
Copy Markdown
Contributor

TL;DR

march_edges now gives each station the central difference of the leading edge over its neighbouring stations, instead of the step that reached it. That step was one station stale, and near the ram-air kite's tip it tilted the cut until it grazed the surface: station 79 returned a section 0.80 thick for its chord.

Before → after

Ram-air kite body, n_bins=60, pre-fix obj_slice.jl from the merge base (9ed3cd6) evaluated beside this branch's in one session, same mesh, same stations, same axes. Left: section depth over chord at every marched station. Right: station 79's sliced section, normalised, with station 78 for reference.

before: stations 3 and 79 slice at 0.80 of their chord; station 79's section is a blob

after: every station between 0.15 and 0.22; station 79 slices to an airfoil at 0.178

Before, the cut at station 79 runs along the curling tip rather than across it and collects the surface as a 0.80-deep cloud; after, it crosses the section and returns an airfoil at 0.178. The two straight segments inside the after profile are the tip's closing panel, which the cut now crosses: the pre-fix slice at station 80 shows the same segments, so they are the mesh's, not this change's.

What was wrong

The march keeps its running tangent, normalize(le[i-1] - le[i-2]) at station i. It stored that tangent with the station, build_sectionairfoil_frame built the cutting plane from it, and at a tip that turns aft and curls down the stale direction pointed up. Reproduced on data/ram_air_kite/ram_air_kite_body.obj, n_bins=60: the section at station 79 comes out at 0.804 of its chord, and its mirror, station 3, at 0.805. Every other station is between 0.16 and 0.25.

What changed

After the march, tangent[i] = normalize(le[i+1] - le[i-1]). The march itself and the tip bisection keep their running tangent, so every station's leading edge, trailing edge and plane origin are unchanged, and only the frame build_section slices in moves. Since nothing reads the per-row tangent any more, the rows no longer carry one.

I departed from the issue's sketch at the two ends. Its one-sided difference, le[end] - le[end-1], moves the blob onto the tip station: the last leading-edge step there is almost pure chord ([0.993, 0.114, 0.028]), and station 81 slices at 0.816. Each end now takes the central difference of the station next to it. The tip station matters: at n_bins=90 its chord is 3.4% of the longest, above station_indices' 1% cutoff, so a section can land on it.

Section depth over chord, every station (probe over `build_section`)

Before, at n_bins=60: tip station 0.156, worst station inside the tips 0.804 (79, mirrored by 3 at 0.805).

n_bins issue's one-sided ends: tip station this branch: tip station · worst station inside
40 1.108 0.174 · 0.216
60 0.816 0.153 · 0.217
90 0.865 0.162 · 0.229

Placed sections, perpendicular_sections, before → after:

  • n_sections=4, wingtip_distance=0.05: [0.805, 0.196, 0.196, 0.804][0.178, 0.196, 0.196, 0.178]
  • n_sections=10, wingtip_distance=0.05: outer pair 0.805 / 0.804 → 0.178 / 0.178, inner sections move by at most 0.007
  • n_sections=10, wingtip_distance=0: every section moves by at most 0.008

What this costs

Every station's frame moves, so every mesh re-slices and every generated geometry shifts slightly; the changelog says to regenerate. No generated geometry is tracked in the repo, so nothing checked in goes stale.

Found on the way, not done here

Verification

  • Reproduced first: station 79 at n_bins=60 slices at 0.804 of its chord, station 3 at 0.805
  • New testset every marched station cuts across the surface, closing tips included in test/obj_adapter/test_obj_adapter.jl: red on unchanged src (204 passed, 2 failed), green after (206/206), juliaserver. A station whose section comes back nothing counts as a failure rather than erroring. The 0.3 bound sits between the worst sane station (0.229) and the grazing cut (0.80); a later mesh that moves a sane station past it is one worth looking at.
  • test/obj_adapter/test_obj_adapter.jl rerun after the review round: 260/260
  • On 87624a0, with test/generated/ rebuilt, all green: obj_adapter/test_obj_adapter.jl (260), ram_geometry/test_kite_geometry.jl (17 + 1 broken already on main), settings/test_settings.jl, body_aerodynamics/test_results.jl (30), test_refinement_validation.jl (15), wake/test_wake.jl, solver/test_forwarddiff.jl (10), plotting/test_plotting.jl. After merging main (5db7cc8, a changelog conflict only, both Fixed entries kept), with test/generated/ rebuilt again: obj_adapter/test_obj_adapter.jl 269/269, solver/test_wing_directions.jl 14/14
  • Up to date with main · no new public symbols · no REUSE in this repo · docs build: not run locally (the docs env has no manifest here)
  • Local CI mirror (Pkg.test, Julia 1.13, one cell) on 5db7cc8: PASS in 6 min · GitHub CI on 5db7cc8: running. On 87624a0 GitHub CI passed; the Windows 1.12 job needed one re-run for the test_forwarddiff.jl's POLAR_MATRICES check flakes at ~4%, and the oper #287 ForwardDiff POLAR_MATRICES flake (0.0557, its norm_fd bit-identical to the passing macOS job's).
  • Risk: a mesh that marches to fewer than three stations would index out of range. The tip bisection finds a station on each side even at a step of twice the ram-air kite's span (3 stations), so it takes a mesh whose only valid cut is the centre one.

Scope

+30 / −7 across 3 files: the tangent in src/obj_adapter/obj_slice.jl (+5 net, rows no longer carry one), one testset, one changelog line.

Closes #361 · task VortexStepMethod.jl-361

The tangent stored with a station was the leading-edge step that reached it,
one station stale. Near a closing tip that cut grazed the surface: station 79
of the ram-air kite sliced as an 80%-thick section. Each station now gets the
central difference over its neighbouring leading edges, and each end that of the
station next to it. The march itself is unchanged.

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

Good

  • The fix is small and targeted: after the march, tangent[i] becomes the central difference of le (obj_slice.jl:344-347). The march and the tip bisection still use their running tangent, so LE, TE and the plane origin stay the same. Checked by reading the whole march_edges in the worktree.
  • Dropping tangent from the rows follows from the change: rg tangent src shows the only reader is obj_to_yaml, which uses m.tangent[i] from the return value, and that is still there.
  • Leaving the issue's one-sided difference at the ends is justified in the card with numbers: that version slices the tip station at 0.816–1.108 of its chord, against about 0.16 on this branch.
  • The centre station's hard-coded [0,1,0] tangent now uses the same formula as every other station, which removes a special case.
  • The new testset names what it protects, slices every station at two n_bins, and the card reports it red on unchanged src (2 failures) and green after.
  • The changelog entry is one compact item under Fixed and tells users to regenerate geometry, which matches the cost stated in the card.

Not good

  • src/obj_adapter/obj_slice.jl:345neighbours holds the index each station takes its central difference around, not the neighbours themselves, so le[j + 1] .- le[j - 1] reads as off by one at a glance. A name like centre would say what j is.
  • src/obj_adapter/obj_slice.jl:282 — 'that of the next station in at either end' is hard to parse. Something like 'end stations reuse their inner neighbour's' says the same thing in plain words.
  • A mesh that marches to fewer than three stations gets a BoundsError instead of a clear message. The card names this risk but the code has no guard and no error(...) for it.
  • The card defers hoisting the nested cut/march closures (§6) to a cleanup: PR but links no issue for it, so nothing tracks the follow-up.
  • The test threshold of 0.3 leaves about 0.07 of headroom over the worst in-span station (0.229 at n_bins=90), so a later mesh or contour change could trip it. The margin is acceptable but tight.
  • If build_section returns nothing for a station, the test errors on section.y_airfoil instead of reporting a clean failure.
  • Docs build not run; only an existing docstring changed, so the risk is low.

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

Comment thread src/obj_adapter/obj_slice.jl Outdated
return (; le=[r.le for r in rows], te=[r.te for r in rows],
point=[r.point for r in rows], tangent=[r.tangent for r in rows])
le = [r.le for r in rows]
neighbours = clamp.(eachindex(le), 2, length(le) - 1)

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: neighbours holds the index each station takes its central difference around, not the neighbours themselves, so le[j + 1] .- le[j - 1] reads as off by one at a glance. A name like centre would say what j is.

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.

Renamed to middle in 87624a0.

Comment thread src/obj_adapter/obj_slice.jl Outdated
along the span, the LE/TE points and each cut's plane origin and tangent. Build the
airfoil for a chosen station with `build_section`.
along the span, the LE/TE points, each cut's plane origin, and the LE tangent: the
central difference over the neighbouring stations, that of the next station in at

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: 'that of the next station in at either end' is hard to parse. Something like 'end stations reuse their inner neighbour's' says the same thing in plain words.

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.

Reworded in 87624a0: the end stations reuse their inner neighbour's.

@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
…est cleanly on a lost section

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

1-Bort-1 commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

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

@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 and removed agent:running Agent task state agent:queued Agent task state labels Sep 21, 2026
…ions

cut_station, march_stations and tip_station replace the closures inside
march_edges; the marched stations are bitwise identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@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 21, 2026
@1-Bort-1
1-Bort-1 requested a review from 1-Bart-1 September 21, 2026 23:20
…and row locals

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

Copy link
Copy Markdown
Member

I need figures in the PR body for before/after.

@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 22, 2026
@1-Bort-1

Copy link
Copy Markdown
Contributor Author

Added to the #376 description under Before → after: depth/chord at every station and station 79's section, pre-fix vs this branch.

@1-Bort-1 1-Bort-1 added agent:ci Agent task state agent:queued Agent task state agent:review Agent task state agent:running Agent task state and removed agent:running Agent task state agent:ci Agent task state agent:queued Agent task state labels Sep 22, 2026
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:review Agent task state labels Sep 22, 2026
…rch-edges-nested-cut-mar

Hoist march_edges' cut, march and tip bisection to module-level functions
@1-Bart-1
1-Bart-1 enabled auto-merge September 22, 2026 12:06
@1-Bort-1 1-Bort-1 added agent:ci Agent task state agent:review Agent task state and removed agent:queued Agent task state agent:ci Agent task state labels Sep 22, 2026
@1-Bart-1
1-Bart-1 merged commit a324968 into main Sep 22, 2026
6 checks passed
@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.

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

2 participants