Skip to content

Blend POLY cl and cd into the flat-plate post-stall form over 20–25° and hold cm at its ±20° value - #386

Closed
1-Bort-1 wants to merge 3 commits into
mainfrom
agent/359-poly-cl-and-cd-jump-at-alpha-20-deg-and-
Closed

1-Bort-1 wants to merge 3 commits into
mainfrom
agent/359-poly-cl-and-cd-jump-at-alpha-20-deg-and-

Conversation

@1-Bort-1

@1-Bort-1 1-Bort-1 commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

TL;DR

A POLY (Breukels) section's cl and cd now blend smoothly from the α-polynomial into the flat-plate post-stall form between |α| = 20° and 25°, and cm stops at its ±20° value. Before, both coefficients jumped at ±20°, and on the negative side the post-stall form had the wrong sign: cl came out positive and cd negative.

POLY section polars, lei_poly_coeffs(0.1, 0.08), -60 to 60 deg: gray before (main), red after; the 20 deg jumps are gone, post-stall cl is odd, cd positive, cm bounded

What was wrong

Past |α| > π/9, calculate_cl swapped the polynomial for 2 cos α sin²α and calculate_cd swapped it for 2 sin³α, with no blend. calculate_cm had no branch and ran its quadratic unbounded (−5.5 at 30°, below −8 by 36°). The negative side was worse than the issue says. 2 cos α sin²α is even in α, so at −20.05° cl flipped from −1.33 to +0.22, a jump of 1.55. 2 sin³α is odd, so the drag was negative beyond −20° (−0.25 at −30°).

With lei_poly_coeffs(0.1, 0.08), the largest change between neighbouring samples 0.05° apart over −60°…60° was 1.54 in cl and 0.26 in cd before. After, it is 0.019 in cl and 0.002 in cd.

What changed

  • cl and cd: (1 − w)·poly + w·flat_plate, where w is a cubic smoothstep in |α| from 20° to 25°. That makes cl and cd C¹ in α, so Newton-type solvers see a derivative everywhere. Inside ±20° blend_post_stall returns the polynomial before the flat-plate form is evaluated, so every result there is bit-identical to main (the test checks this with ==) and costs the same.
  • The flat-plate forms are now 2 sin α |sin α| cos α (odd) and 2 |sin α|³ (even, ≥ 0). These are the same curves as before for α > 0.
  • cm: the polynomial evaluated at α clamped to ±20°. This matches how the POLAR_* models already hold cm flat outside their table.
  • The AeroModel docstring said "Core only evaluates the polynomial", which was already untrue. It now describes the post-stall behaviour. POLY_FIT_LIMIT, POLY_BLEND_WIDTH, blend_post_stall, flat_plate_cl and flat_plate_cd are private and listed on the private docs pages.

Where I would push back

The post-stall model itself is the inherited one (it matches the Python VSM), and I kept it. The two models disagree a lot at 20°: cd from the polynomial is 0.183 against 0.080 from the flat-plate form, and cl is 1.07 against 0.22. Any blend therefore gives a falling cd over 20–24° (lowest 0.145 at 24.3°). cl also dips slightly below the flat-plate curve (0.224 at 23.5°, against 0.324 at 25°). The 5° width is my choice of "a few degrees". A wider blend softens the negative lift slope, but it would carry the polynomial further past its fit range. Which post-stall model the polynomial should hand over to is your call, so it is in #388 with three options; this PR keeps the model.

Holding cm flat leaves a kink at ±20°. cm does not enter the circulation solve, so no solver sees it. The ±20° value itself (−2.42 here) is what the Breukels quadratic gives; I have not judged whether it is physical.

Verification

  • Reproduced first, on main: (19.99°, cl 1.0706, cd 0.1832, cm −2.4181), (20.01°, 0.2200, 0.0801, −2.4231), (−30°, cl +0.4330, cd −0.2500, cm −5.5312)
  • test/panel/test_panel.jl testset "POLY polar is continuous past its ±20° fit range": red on main (57 of 153 failing: jump, slope kink, parity, cm unbounded; the inside-±20° guard passed), green after (153/153), juliaserver
  • The C¹ check (largest second difference of cl and cd over −60°…60° at 0.01° steps < 1e-4) separates the two cases: the smoothstep gives 3.0e-5 (cl) and 2.8e-6 (cd), and a linear blend over the same 20–25° gives 2.2e-3 and 2.3e-4.
  • Affected tests via test/runtests.jl in the test env with patterns panel, solver/test_solver, test_verification, airfoil_aero, wing_geometry, yaml_geometry, body_aerodynamics, test_forwarddiff, test_stability: 6299/6299 pass (2m33s). This includes the artificial-viscosity tests on the POLY flat_plate_wing.
  • Docs build clean (only the existing HTML size warnings) · no REUSE in this repo
  • Merged main after Document the body frame as KiteUtils' KA and drop the KB definition #390 in 28b28f7: the only conflict was two independent Fixed entries in CHANGELOG.md, both kept; Document the body frame as KiteUtils' KA and drop the KB definition #390's src/panel.jl change is the Panel docstring alone. test/panel/test_panel.jl on the merged tree: 187/187 pass (juliaserver)
  • GitHub CI on 28b28f7: PASS, all 7 checks · local full suite: PASS on 28b28f7 (7 min, Julia 1.13, one matrix cell)
  • The first Windows run on 28b28f7 failed only AutoForwardDiff matches AutoFiniteDiff (LOOP, POLAR_MATRICES) at 0.0570, the flake Keep calc_forces!' per-panel loads out of line so Julia 1.12 gets linearize's ForwardDiff Jacobian right on AVX-512 Zen #378 addresses. Its finite-difference Jacobian norm (3.078044297716207) is bit-identical to what main's passing cells use, and computing this test's forward and finite-difference Jacobians with main's src and with this branch's gives == results for both, so the diff cannot move that test. The rerun passed.
  • Benchmark: calculate_cl at 5° on a POLY panel is 5.76 ns (main) → 5.81 ns (branch) → 6.05 ns (main again), calculate_cd 5.45 → 5.80 → 5.88 ns, all 0 allocations. Before the early return the branch took 15.1 ns for cl.
    Box: steady, 6–8 of 20 cores busy, 3.1 GHz, agent bench-end: conditions held
  • Risk: any downstream result that uses POLY sections past ±20° moves. This is intended, but a stored reference built on the old jump would shift.

Scope

+87 / −11 across 6 files. The logic is about 35 lines in src/panel.jl, the test is 41 lines and reuses the file's create_panel helper, and the rest is the docstring, CHANGELOG and two private-docs entries. Searched π/9, 20°, post-stall, POLY in src/, test/ and docs/: src/panel.jl is the only implementation of the POLY post-stall branch.

Closes #359 · task VortexStepMethod.jl-359

Past |alpha| = 20 deg a POLY section swapped its polynomial for the
flat-plate form without blending, so cl dropped by 0.85 and cd by 0.10
across one step, and the post-stall forms had the wrong parity (cl > 0
and cd < 0 for alpha < -20 deg). cl and cd now blend over 20-25 deg with
a cubic smoothstep into 2 sin|sin| cos and 2 |sin|^3, and cm holds its
+-20 deg value. Inside +-20 deg nothing changes.

Co-Authored-By: Claude Opus 5.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 23, 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 · 0 inline, 0 off the diff

Good

  • The fix matches the card: blend_post_stall is the single place the polynomial and flat-plate forms are joined, used by both cl and cd; cm is clamped, as described.
  • The weight is exactly 0 for |α| ≤ 20°, so results inside the fit range are unchanged. The test at test_panel.jl:235 checks this with == against evalpoly, and that works because lei_poly_coeffs returns a tuple (src/airfoil_aero/poly.jl:56).
  • The sign bug on the negative side is fixed properly: sin α |sin α| cos α is odd and |sin α|³ is even and non-negative, both match the old forms for α > 0, and the tests at lines 243–249 check this.
  • Only one implementation exists: grepping cl_coeffs, cd_coeffs, cm_coeffs and π/9 across src/ finds the POLY evaluation only in panel.jl's calculate_cl/cd/cm. calculate_cd_cm goes through these, so there is no second path.
  • The smoothstep in |α| is C¹. Inside ±20° the abs and clamp add nothing, and at α = 0 the abs(sin α) sits inside a product that is 0 there, so ForwardDiff sees no new discontinuity.
  • Housekeeping is in order: the stale 'Core only evaluates the polynomial' docstring is corrected and named in the card; the new private symbols are on private_*.md; the CHANGELOG entry is compact and user-facing; the constants carry their units [deg].
  • Scope matches the card (+77/−11, 6 files) with no unrelated edits.

Not good

  • The blend makes cd fall from 0.183 to 0.145 over 20–24° (a negative dcd/dα). The card says so, but a follow-up issue on whether the Breukels cd should be joined to the flat plate at all would put the question to Bart instead of leaving it in PR prose.
  • The test builds a Wing, runs refine! and constructs BodyAerodynamics just to get one panel, while the same file already has a create_panel helper (test_panel.jl:10). Check whether it can take POLY aero data before adding a heavier setup.
  • The jump threshold of 0.01 per 0.01° only guards against gross jumps. A C⁰-but-steep blend would pass it too, so the C¹ claim in the card is not tested.
  • blend_post_stall computes the flat-plate value on every call, including inside ±20° where its weight is 0. The cost is negligible, but the benchmark is marked n/a on a hot path used by every solver iteration.

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

… is C1

blend_post_stall now returns the polynomial early inside +-20 deg, so the
flat-plate form is only evaluated past it; calculate_cl at 5 deg is back
to main's time. The panel test uses create_panel instead of a refined
Wing, and checks the second difference of cl and cd over -60..60 deg so a
C0 blend with a slope kink fails.

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

1-Bort-1 commented Sep 23, 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 23, 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 23, 2026
@1-Bort-1

Copy link
Copy Markdown
Member Author

Fixed in 758806d: the test now uses create_panel, there is a C1 check on second differences, and blend_post_stall returns early inside ±20° (benchmark in the PR body). The cd post-stall question is #388.

@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 23, 2026
@1-Bort-1
1-Bort-1 requested a review from 1-Bart-1 September 23, 2026 10:00
@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 23, 2026
…and-

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@1-Bort-1 1-Bort-1 added agent:queued Agent task state and removed agent:running Agent task state labels Sep 23, 2026
@1-Bort-1 1-Bort-1 added agent:running Agent task state agent:ci Agent task state agent:review Agent task state and removed agent:queued Agent task state agent:running Agent task state agent:ci Agent task state labels Sep 23, 2026
@1-Bart-1 1-Bart-1 closed this Sep 28, 2026
@1-Bort-1 1-Bort-1 added agent:done Agent task state and removed agent:review Agent task state labels Sep 28, 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.

POLY cl and cd jump at |alpha| = 20 deg, and cm runs its quadratic unbounded past it

2 participants