march_edges takes each station's tangent from its neighbours, so no cut near a closing tip grazes the surface - #376
Conversation
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
left a comment
There was a problem hiding this comment.
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 ofle(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 wholemarch_edgesin the worktree. - Dropping
tangentfrom the rows follows from the change:rg tangent srcshows the only reader isobj_to_yaml, which usesm.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:345—neighboursholds the index each station takes its central difference around, not the neighbours themselves, sole[j + 1] .- le[j - 1]reads as off by one at a glance. A name likecentrewould say whatjis.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
BoundsErrorinstead of a clear message. The card names this risk but the code has no guard and noerror(...)for it. - The card defers hoisting the nested
cut/marchclosures (§6) to acleanup: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_sectionreturnsnothingfor a station, the test errors onsection.y_airfoilinstead 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.
| 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) |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Reworded in 87624a0: the end stations reuse their inner neighbour's.
…est cleanly on a lost section Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Local full suite: PASS (6 min, Julia 1.13.0, one cell of the matrix) |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…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>
…and row locals Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
I need figures in the PR body for before/after. |
|
Added to the #376 description under Before → after: depth/chord at every station and station 79's section, pre-fix vs this branch. |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rch-edges-nested-cut-mar Hoist march_edges' cut, march and tip bisection to module-level functions
TL;DR
march_edgesnow 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-fixobj_slice.jlfrom 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, 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 stationi. It stored that tangent with the station,build_section→airfoil_framebuilt the cutting plane from it, and at a tip that turns aft and curls down the stale direction pointed up. Reproduced ondata/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 framebuild_sectionslices 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: atn_bins=90its chord is 3.4% of the longest, abovestation_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_binsPlaced 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.007n_sections=10, wingtip_distance=0: every section moves by at most 0.008What 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
march_edgescarries its logic in two nested closures,cutandmarch, whichCLEAN_CODE.md§6 rules out. Hoisting them is a move of about 60 lines and would bury this three-line change, so it is queued as its owncleanup:task, cleanup: hoist march_edges' nested cut/march closures to module-level... #377.Verification
n_bins=60slices at0.804of its chord, station 3 at0.805every marched station cuts across the surface, closing tips includedintest/obj_adapter/test_obj_adapter.jl: red on unchangedsrc(204 passed, 2 failed), green after (206/206), juliaserver. A station whose section comes backnothingcounts 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.jlrerun after the review round: 260/260test/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), withtest/generated/rebuilt again:obj_adapter/test_obj_adapter.jl269/269,solver/test_wing_directions.jl14/14docsenv has no manifest here)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 ForwardDiffPOLAR_MATRICESflake (0.0557, itsnorm_fdbit-identical to the passing macOS job's).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