Skip to content

A live polar is a polar, not a model of its own - #268

Merged
1-Bart-1 merged 2 commits into
mainfrom
refactor/live-polar-is-a-polar
Aug 31, 2026
Merged

A live polar is a polar, not a model of its own#268
1-Bart-1 merged 2 commits into
mainfrom
refactor/live-polar-is-a-polar

Conversation

@1-Bart-1

Copy link
Copy Markdown
Member

SAMPLED was a second linear interpolation over a second set of knots, doing what POLAR_VECTORS already does with Interpolations.jl — and quietly diverging from it. Same calculate_cd(panel, alpha) call gave linear extrapolation past the table under one model and a flat hold under the other, and delta needed a rule of its own ("ignores delta"). What a live polar source actually needs is not a model of its own, it is the ability to rewrite a table the panel already has.

What changes

  • SAMPLED is gone from AeroModel. A live polar is a POLAR_VECTORS panel whose table gets rewritten.
  • set_sampled_polar!set_polar!, which rewrites the knots and values in the panel's own storage and rebuilds the three interpolations. The rebuild is needed because Interpolations copies the values it is handed — it holds the knots by reference but not the coefficients. That is the only thing standing between this and a true in-place update; see below.
  • window_alpha holds alpha inside the range the table covers, so a table sampled over a window around one angle stops at its ends rather than extrapolating out of them. This is a property of a bounded table, not of a live one, so the stall-angle scan keys off it instead of off an enum member, and full-range polars — window 0 — are bit-identical to before.
  • init_aero! clears the window and the live shape, so a mesh rebuild that re-seeds a panel from its section cannot leave a stale window clamping the section's own table.
  • sampled_value is deleted.

Not breaking

SAMPLED has never been in a release — it was added and removed inside this cycle, like TAYLOR before it. Relative to v4.2.0 there is no model to remove and no behaviour to change. Existing POLAR_VECTORS and POLAR_MATRICES wings evaluate exactly as before.

Cost

A live polar now requires panels that carry interpolation fields, i.e. a wing built from POLAR_VECTORS sections. Panels built without them (Panel{Float64}(), INVISCID or POLY sections) get a set_polar! method that says so rather than failing on a field type. Two test fixtures were updated accordingly.

Follow-up

The interpolation rebuild per refresh is temporary. An upstream PR to Interpolations.jl for an in-place value update (or a non-copying constructor) would reduce set_polar! to writing into the arrays the interpolant already holds, which is a one-line change here.

Verification

  • test/airfoil_aero/test_live_polar.jl — 83 assertions pass
  • test/panel/, test/body_aerodynamics/, test/solver/test_solver.jl — pass
  • test/plotting/test_plotting.jl — 77 pass
  • docs build clean on Documenter 1.18

🤖 Generated with Claude Code

SAMPLED was a second linear interpolation over a second set of knots, doing
what POLAR_VECTORS already does with Interpolations, and diverging from it:
drag extrapolated linearly under one and was held flat under the other, and
delta needed a rule of its own. What a live source actually needs is not a
model, it is the ability to rewrite a table it already has.

set_polar! rewrites a POLAR_VECTORS panel's knots and values in its own
storage and rebuilds the interpolations, since Interpolations copies the
values it is handed. The panel records the range its table covers, and
window_alpha holds alpha inside it, so a table sampled over a window around
one angle stops at its ends instead of extrapolating out of them. That is a
property of a bounded table rather than of a live one, so the stall scan can
key off it and full-range polars, whose window is zero, are untouched.

init_aero! now clears the window and the live shape, so a mesh rebuild that
re-seeds a panel from its section cannot leave a stale window clamping the
section's own table.

SAMPLED never shipped, so nothing here is breaking.

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

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.44444% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/panel.jl 94.11% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Interpolations finds a gridded knot with searchsortedfirst, which dispatches on
the knot vector, so the search is ours to choose without changing the library.
A binary search over nine angles costs more than the whole lookup around it —
its branches are unpredictable where a scan over contiguous memory is not — and
the live polars a solve rewrites every step are exactly that short.

ScanKnots wraps the angles and scans them, and polar_knots hands it to a table
under SCAN_KNOT_MAX and the plain vector to anything longer, where the scan
would lose badly: 71 ns against 13 at 361 knots. On the 44 panel wing the solve
goes from 215 to 176 us, against 206 for the hand-rolled polar this replaces.

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

Copy link
Copy Markdown
Member Author

Added ScanKnots (69f7408), so the PR carries no hand-rolled interpolation at all.

Interpolations finds a gridded knot with searchsortedfirst, which dispatches on the knot vector — so the search strategy is ours to pick without touching the library. A binary search over 9 angles costs more than the whole lookup around it (5.06 ns of an 8.41 ns evaluation), because its branches are unpredictable where a scan over contiguous memory is not.

polar_knots hands ScanKnots to a table at or under SCAN_KNOT_MAX (24) and the plain vector to anything longer, where a scan loses badly — 71 ns vs 13 ns at 361 knots.

Measured on the 44-panel wing (identical converged force, 1757.681473, in all three):

solve!
main, hand-rolled SAMPLED 206 µs
this PR before ScanKnots 215 µs
this PR with ScanKnots 172–185 µs

For scale, a live-polar wing runs refresh_live_polars! at 8.02 ms per solve (the NeuralFoil forward pass, 10.9 MB), so the whole solve is ~2.6% of the cycle and none of these deltas are visible in a live run. The interpolation rebuild adds 38 kB per whole-wing refresh against that 10.9 MB.

test/body_aerodynamics/test_results.jl (numerical output against reference data) passes, so the knot-container change is behaviour-preserving.

Follow-up upstream: two things worth proposing to Interpolations.jl — a search-strategy option on Gridded so a short table needn't define its own knot type, and a way to take values without copying them (knots are already held by reference, so only the values force the rebuild here).

@1-Bart-1
1-Bart-1 merged commit dcebe32 into main Aug 31, 2026
9 checks passed
@1-Bart-1
1-Bart-1 deleted the refactor/live-polar-is-a-polar branch August 31, 2026 20:33
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