Skip to content

Continuous wrapping method - #262

Merged
1-Bart-1 merged 5 commits into
mainfrom
wrap/rolling-ball
Sep 7, 2026
Merged

1-Bart-1 merged 5 commits into
mainfrom
wrap/rolling-ball

Conversation

@1-Bart-1

@1-Bart-1 1-Bart-1 commented Aug 17, 2026 •

Copy link
Copy Markdown
Member

Split out of #258, which carried this rewrite alongside unrelated section-order work and failed CI because of it.

What changes

shrink_wrap was a distance field: threshold at the ball radius, flood fill, erode back to clearance, trace the level set with marching squares, fair the result. Every step was bounded by the grid, so cell_size set the fidelity of the wrap and padding the field for min_concave_radius cost time in both directions.

pivot_contour now rolls the ball around the cloud and emits what its contact side sweeps: an arc of radius clearance about each point it touches, joined by an arc of the ball radius across each gap it cannot enter. Those arcs are the wrap boundary, so there is no resolution to choose.

  • ShrinkWrap's cell_size becomes min_clearance — a floor on clearance rather than a grid pitch. Still accepted under the old name.
  • The wrap sits at exactly clearance from the cloud instead of a cell over it: a V3 canopy's aft strip is 2 * clearance thick where the grid gave 3.5 * cell_size.
  • min_concave_radius stops costing anything. One V3 slice at radius 0.4 drops from 173 ms to 10 ms; at the default radius, 15 ms to 3 ms.

Resampling had to follow

push_arc! steps each arc to a chord tolerance, so its segments scale as sqrt(radius) and the trace's node spacing alternates by a factor of several between the two arc families. Diffusing the turning per node carried that alternation into the sampling measure, leaving resampled panel lengths oscillating by up to 5.7x between neighbours.

  • smoothed_curvature averages the turning over a fixed arclength band instead, which is independent of how the arcs happen to be stepped. Neighbour ratio drops to the 3.0 of a plain cosine-clustered .dat.
  • enforce_min_spacing! keeps cosine clustering — which thins as the square of the station count — from putting leading-edge panels a fraction of a percent of chord long.

Known limitation — not ready to merge

XFoil's viscous solver still fails to converge on the wrapped NACA0012 in test/solver/test_backend_comparison.jl, where the distance-field wrap succeeded, so that test errors in interpolate_matrix_nans! with nothing to interpolate from.

What is established so far:

  • XFoil reports no LEFIND warning, so the contour is well formed; it is the viscous march that fails, returning an alpha-independent cl (~-0.484 at -4, 0 and +4 deg alike).
  • repanel=true converges 6/6, so XFoil's own repaneling repairs whatever it dislikes.
  • Convergence is non-monotone in the panel size floor: 0.1 gives 6/6, 0.2-0.5 gives 0/6, 0.7 gives 6/6. So neither grading nor absolute panel size is the discriminator, and tuning a constant until it passes would not be a fix.
  • Current suspect: the round trailing-edge cap of radius clearance. The contour both starts and ends inside it, so each surface turns ~90 deg around the cap before the boundary layer can start. A sharp-TE airfoil has no such cap in the distance-field wrap.

Next step is to confirm that mechanism and, per the validation bar this repo needs, check any fix through a written .dat roundtrip plus ~1e-7 coordinate noise over several sections rather than a single in-memory pass.

🤖 Generated with Claude Code

1-Bart-1 and others added 2 commits August 17, 2026 12:16
The wrap was a distance field: threshold at the ball radius, flood fill, erode
back to clearance, trace the level set with marching squares, then fair the
result. Every one of those steps was bounded by the grid, so cell_size set the
fidelity of the wrap and padding the field for min_concave_radius cost time in
both directions.

pivot_contour rolls the ball around the cloud and emits what its contact side
sweeps: an arc of radius clearance about each point it touches, joined by an arc
of the ball radius across each gap it cannot enter. Those arcs are the wrap
boundary, so there is no resolution to choose. cell_size becomes min_clearance,
a floor on clearance rather than a grid pitch, and is still accepted under the
old name.

The wrap now sits at exactly clearance from the cloud rather than a cell over it,
so a V3 canopy's aft strip is 2*clearance thick where the grid gave
3.5*cell_size. One V3 slice at min_concave_radius 0.4 drops from 173 ms to 10 ms,
and at the default radius from 15 ms to 3 ms.

The resampling had to follow. push_arc! steps each arc to a chord tolerance, so
its segments scale as sqrt(radius) and the trace's node spacing alternates by a
factor of several between the two arc families. Diffusing the turning per node
carried that alternation into the sampling measure and left the resampled panel
lengths oscillating by up to 5.7x between neighbours; smoothed_curvature averages
the turning over a fixed arclength band instead, which brings that down to the
3.0 of a plain cosine-clustered .dat. enforce_min_spacing! then keeps cosine
clustering, which thins as the square of the station count, from putting
leading-edge panels a fraction of a percent of chord long.

Known limitation: XFoil's viscous solver still fails to converge on the wrapped
NACA0012 of test/solver/test_backend_comparison.jl, where the distance-field wrap
succeeded, and does so non-monotonically in the panel size. XFoil reports no
LEFIND warning, so the contour is well formed; the suspect is the round trailing
edge cap, which the contour both starts and ends inside. That is why this is a
branch of its own and not part of #258.

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

1-Bart-1 commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

@1-Bort-1 rebase and try to fix the continuous wrapping method such that the tests pass.

@1-Bort-1

1-Bort-1 commented Sep 5, 2026

Copy link
Copy Markdown
Member

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

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

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

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.

Superseded by the review further down this thread.

@1-Bart-1

1-Bart-1 commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

@1-Bort-1 the tests are still failing...

@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 5, 2026
1-Bort-1 and others added 3 commits September 6, 2026 00:43
The rolling-ball wrap floored clearance at min_clearance for every convex
corner, so a closed airfoil wrapped at clearance=0 grew a round trailing-edge
cap of min_clearance radius. XFoil flags the contour sharp (first point equals
last) yet the geometry is blunt, and the blunt TE shifts the Kutta point: cl
came out +-0.486 instead of +-0.434 and the viscous march failed to converge.

A closed loop now uses its true clearances, so clearance=0 hugs the input and
keeps a sharp trailing edge sharp; an open single-membrane cloud still gets the
min_clearance capsule.
@1-Bort-1

1-Bort-1 commented Sep 5, 2026 •

Copy link
Copy Markdown
Member

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

@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!

@1-Bart-1

1-Bart-1 commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

@1-Bort-1 do you think this is ready to merge? Is it stable/does it work well?

@1-Bort-1

1-Bort-1 commented Sep 6, 2026

Copy link
Copy Markdown
Member

Question from VortexStepMethod.jl-262

Diagnosed, not built: AGENTS.md §1 puts tool-permission policy and egress allowlists among the three things I never change on my own initiative, so this one needs your word before I branch.

It is one line. src/harness/claude.ts:178 gives the chat role --allowedTools Read,Grep,Glob; in print mode that list is the whole grant, everything else prompts, stdin is closed, and the prompt comes back as "requires approval". HARNESS_DEFAULT is claude, so that is what you hit.

Not a separate bug for the agent commands: src/chat.ts advertises agent status|logs|cost in the chat prompt while the same line denies Bash. Under opencode they already work — its chat agent allows agent status|logs|cost, rg, git log|show|diff and denies the rest.

The thing worth your attention before saying yes. A chat turn runs as the ordinary agent unix user with the agent's own environment; what contains it today is that tool list plus starting in ~/repos — which is what the comment at the top of chat.ts means by "the credentials the agents use are not reachable". Grant Bash, even prefix-scoped, and containment becomes "the prefix matcher holds" against text arriving from Slack. PLAN §2 says that boundary should be a credential, not a prompt, and there is no read-only GitHub credential on the box to be one.

So: which shape do you want?

A) Credential first. You provision a read-only GitHub token for the chat turn; I put it in the turn's environment and allow gh pr view/list/search, gh api, agent status|logs|cost, WebFetch and WebSearch under both harnesses. A write then fails at GitHub whatever the model types, so the allowlist stops being the only thing standing between a Slack message and your repos.

B) Patterns only, today, no new credential. Same allowlist, the agent's own token stays in the environment, and gh api -X POST is one typed flag from a write.

My recommendation: A. B buys you the answer to "is my PR merged" this week and quietly hands the chat side write rights it is documented not to have; the whole grant is worth having only once the credential is what limits it. If you want B as a stopgap say so and I will land it with the gap named in the card.

Plan and the full diagnosis are in .agent/plan.md in the worktree; nothing is committed.

@1-Bort-1 1-Bort-1 added agent:waiting-human Agent task state agent:queued Agent task state agent:running Agent task state and removed agent:running Agent task state agent:waiting-human Agent task state agent:queued Agent task state labels Sep 6, 2026
@1-Bort-1 1-Bort-1 added 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
Member

Yes — I would merge it. (Sorry this is late: my last turn posted a harness question
here instead of answering you.)

Green. GitHub CI 9/9 on 1e13d92 — Julia 1.11 + 1.12, Linux/macOS/Windows, docs,
coverage. Local full suite PASS, 21 min. Base main is already merged in; GitHub says
mergeable.

Does it work well. Beyond the suite, XFoil converges 6/6 on δ ∈ {0,1,3} × crease ∈
{0.75,0.9}, and 6/6 through a written-.dat roundtrip with 1e-7 coordinate noise at
δ ∈ {0,1,2,−1,5} — the bar the PR body set for itself. The wrap of a clean airfoil at
zero clearance now lands on it to 3e-17, where the alpha-hull trace sat 2.7e-3 = 0.27%
of chord inside it.

Is it stable. The thing that made this "not ready to merge" is gone at the root
rather than tuned around. The scallop was an artifact of tracing the alpha hull over
a point cloud; the non-monotonicity in every knob was that artifact's phase — whether
a resampled station happened to land on a bump. Nothing left in the path depends on
where a station falls. The diff is +284/−288, so the file came out shorter.

What I would not claim:

  • XFoil runs in exactly one test file (test/solver/test_backend_comparison.jl, on
    NACA0012). The clearance > 0 kite-slice path is covered by NeuralFoil and the
    obj/Surfplan tests, and I A/B'd five V3 and five ram-air slices against the pre-fix
    code — no NaN, cloud_to_wrap ≈ clearance, timings equal or slightly faster. That
    is unregressed, not proven better.
  • The reflex-vertex join is a chamfer, not a true mitre: exact for shallow reflex,
    trims a deep concavity slightly flat. The mitre is exact but singular at a −π reflex,
    which the double wrap inside generate_airfoil_aero really does produce.
  • The CHANGELOG's timing numbers (173 ms → 10 ms) predate my work and I never A/B'd
    them. Directionally right; not measured by me.
  • min_panel_fraction = 0.25 is now a knob nobody needs — with the floor removed
    entirely the shortest panel is 0.68% of the mean and XFoil still converges 6/6.
    Removing it changes every wrap's output, so it is its own change, not a ride-along.

One thing I would offer to land first: my own standing review comment — pivoting from
argmin(x) silently wraps one connected component of a multi-cluster slice, where the
distance-field code warned. A warning, a handful of lines. Say the word and it goes on
this branch; otherwise merge as is and I will open it as a cleanup: PR.

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

1-Bart-1 commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

@1-Bort-1 rebase onto main and address the comment as long as it doesn't hurt performance.

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

1-Bort-1 commented Sep 6, 2026

Copy link
Copy Markdown
Member

Fixed

The suite is green. First, CI had never seen the earlier trailing-edge fix — it was
rebased locally and never pushed, so the branch on GitHub was still db00f13. It is
rebuilt as a fast-forward now (merge of main, no force-push).

The real defect: pivot_contour emitted the alpha hull — an arc about each empty
ball centre. Between two contacts d apart that arc dips inward by r − √(r² − d²/4),
which on the NACA0012 cloud (spacing to 0.0199, ball 0.02) is 0.0027 = 0.27% of chord.
The wrap was scalloped at the cloud's own sampling wavelength: panel turning alternated
sign on 144 of 236 panels, against 6 of 156 on the raw airfoil. Whether XFoil's
viscous march survived that depended on where the resampled stations happened to land —
the non-monotonicity in every knob the PR reported was this, not a tuning problem.

Changes

  • src/airfoil_aero/shrink_wrap.jl:150 — pivot_contour traces the alpha shape, the
    polygon through the contacts, and offsets that: a round join at convex vertices, a
    chamfer across the bisector at reflex ones. min_concave_radius's concave bridging is
    unchanged; only the dip between neighbouring samples goes.
  • src/airfoil_aero/shrink_wrap.jl:348 — the ball's dip-avoidance auto-raise
    (1.5·(linking²/4 + gap²)/(2·gap)) existed only to bound that dip, and goes with it.
  • test/airfoil_aero/test_airfoil_aero.jl:78 — the wrap of a clean Kulfan airfoil at
    zero clearance must lie on it (< 1e-9). Fails at 2.7e-3 without the fix.

Verification

  • On the box, Julia 1.12.7: test_airfoil_aero.jl 19/19 ·
    test_backend_comparison.jl 15/15 · test_obj_adapter.jl 52/52 ·
    test_surfplan.jl 5/5 · test_live_polar.jl all pass. Full-suite mirror started;
    GitHub CI is the authority.
  • The bar this PR asked for: XFoil 6/6 on δ ∈ {0,1,3} × crease ∈ {0.75,0.9}, and 6/6
    on a written-.dat roundtrip with 1e-7 coordinate noise at δ ∈ {0,1,2,−1,5}. V3
    and ram-air slices A/B'd against the pre-fix code: no NaN, cloud_to_wrap ≈
    clearance, timings equal or slightly faster.
  • Risk: the reflex chamfer. The exact answer is a mitre, but it is singular at a −π
    reflex, which the double wrap inside generate_airfoil_aero really does produce (423
    contacts on a 239-point cloud) — that was a NaN before I chamfered instead. The
    chamfer holds clearance from every contact but trims a deep concavity slightly flat.
Two things found on the way
  • enforce_min_spacing!'s min_panel_fraction = 0.25 is untouched but no longer
    load-bearing: with the floor removed the shortest panel is 0.68% of the mean and XFoil
    still converges 6/6. It is a smoothing choice now, not the tuned knob my earlier note
    called it. Removing it changes every wrap's output, so it is its own change.
  • largest_linking_gap answers a NaN in the cloud with BoundsError: 199-element BitVector at index [0] instead of reporting bad input. No caller produces NaN now.

Commits

  • 4a926cb Keep refined surface tables through a coarsening remesh
  • 912c86b Decide panel spanwise orientation once per wing
  • 0d2120f Drop the BREAKING note on the panel reinit! signature
  • aebfd96 Optional flow-curvature pitch-rate moment
  • 2ac530b Normalize wing section order to +y -> -y on load
  • 5f27ade Different field for corrected vs uncorrected
  • 9364a2b Update default manifests
  • 89641dd Document the new span-order and table-path helpers
  • 3340832 Non-interactive Julia selection for bin/install
  • 0621b27 Backtick the underscored @ref targets
  • 2030908 Leave the shrink-wrap rewrite out of this branch
  • 3d627b1 Leave the Section method of reinit! undocumented
  • 8c0a647 Validate the moment relation against measured section data
  • 567c129 Merge pull request Normalize wing section order to +y → -y #258 from OpenSourceAWE/fix/refined-surface-tables
  • 60c2651 Merge pull request Optional flow-curvature pitch-rate moment #260 from OpenSourceAWE/feat/flow-curvature
  • 179eca9 Bump version
  • 34f43c3 Rotate a twisted section's chord fully about the spanwise axis
  • 05166ca Merge pull request Rotate a twisted section's chord fully about the spanwise axis #263 from OpenSourceAWE/fix/twist-rodrigues-axial-term
  • eea12cf Take the billow axis from the section pair, not a global projection
  • c88bb46 Merge pull request Take the billow axis from the section pair, not a global projection #264 from OpenSourceAWE/fix/billow-axis-span-order
  • a385be7 Bump version
  • eee5982 Write the panel aerodynamics once, as traceable functions
  • 1a4da79 Update version
  • dd97f12 Reconstruct NeuralFoil's surface pressure from edge velocity in arc length
  • 19817fd Regenerate polars from the deformed shape instead of a flap angle
  • d62f83e Continue a TAYLOR polar linearly past its fit window
  • 34987f8 Say what a live polar refresh actually allocates
  • dec31c9 Draw the airfoil the live polars were actually generated from
  • 74fb314 Put panel_contour ahead of the skin's docstring
  • 5c42181 Take the chord line out of a deflection, and never return a negative drag
  • db488d7 Keep generated aero tables out of git
  • 7c304b1 Sample a live polar instead of fitting one
  • 27e2b1a Put a live polar next to XFoil on the shape it was generated from
  • 0793622 March XFoil onto the deformed shape instead of jumping to its angle
  • ba28226 Give chord_line its own docstring and file the AirfoilAero privates
  • 5a2e796 Backtick the identifier cross-references Documenter 1.18 now requires
  • 89af20b A live polar is a polar, not a model of its own
  • dcebe32 Let a short polar table scan for its knot instead of bisecting
  • fbdbaef Take a live polar onto a wing whose table carries a delta axis
  • 84d3869 Add polar_model to the manual
  • 9249f98 Bump version
  • 92987eb Let a polar refresh write into the panel instead of rebuilding it
  • becd3ba Let a live polar refresh run without touching the heap
  • 67dc29b Make KulfanParameters mutable so a panel holds the shape, not a copy
  • 6fadaf9 Note what the allocation work buys a multi-core sweep
  • 394859e Measure the refresh allocations without boxing its return value
  • fa4dc89 Bump version
  • 3afe670 Merge remote-tracking branch 'origin/main' into wrap/rolling-ball
  • ecc852c Keep a closed loop's trailing edge sharp at zero clearance
  • 1e13d92 Offset the rolling ball's contact polygon instead of its rim

Review just these changes

Task VortexStepMethod.jl-262

@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
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review (advisory)

Verdict: REQUEST CHANGES · 1 inline, 0 off the diff

Good

  • The entire distance-field subsystem (distance_parabolas!, squared_distance_transform!, grid_sampler, flood_outside, trace_level_set, smooth_turning!) is deleted cleanly with no dangling callers — grep confirms zero remaining references, satisfying the §2 removal rule.
  • cell_size → min_clearance is a documented, backward-compatible migration: the constructor keeps a cell_size=nothing alias, so existing ShrinkWrap(cell_size=...) call sites keep working.
  • Docs stay in sync: airfoil_pipeline.md prose and private_functions.md's @docs list both reflect the new rolling-ball names with the old entries removed.
  • The resampling change (smoothed_curvature over an arclength band, enforce_min_spacing! floor) is well documented and directly targets the two defects named in the PR card (per-node diffusion and cosine thinning).

Not good

  • src/airfoil_aero/shrink_wrap.jl:349 — The PR card itself declares this 'Known limitation — not ready to merge': XFoil's viscous solver still fails on the wrapped NACA0012 (exercised via deform_section here and in test/solver/test_backend_comparison.jl), which the round clearance-radius trailing-edge cap now produces. Shipping a known-failing validation test, before the .dat-roundtrip-plus-noise check the author lists as the repo's bar, is blocking.
  • The outer pivot bound 4*length(x)+100 (and the +100) is an unexplained magic cap with no justification in docstring or PR.
  • cell_size is accepted as a silent alias with no deprecation path, so the old name can linger forever — §3 discourages renaming API without a removal plan.
  • private_functions.md lists enforce_min_spacing! before resample_arc and smoothed_curvature last, out of source order, which is only cosmetic.

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

return (cx + (gap - d) * gx / len, cy + (gap - d) * gy / len)
end
ball = max(method.min_concave_radius, 1.01 * linking / 2)
px, py = pivot_contour(xn, yn, ball, gap)

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.

MAJOR: The PR card itself declares this 'Known limitation — not ready to merge': XFoil's viscous solver still fails on the wrapped NACA0012 (exercised via deform_section here and in test/solver/test_backend_comparison.jl), which the round clearance-radius trailing-edge cap now produces. Shipping a known-failing validation test, before the .dat-roundtrip-plus-noise check the author lists as the repo's bar, is blocking.

@1-Bart-1
1-Bart-1 merged commit 257d084 into main Sep 7, 2026
9 checks passed
@1-Bart-1
1-Bart-1 deleted the wrap/rolling-ball branch September 7, 2026 08:44
@1-Bort-1 1-Bort-1 added agent:done Agent task state and removed agent:review Agent task state labels Sep 7, 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.

2 participants