Skip to content

Fit the contour generate_airfoils is handed, not a second wrap of it - #294

Merged
1-Bart-1 merged 2 commits into
mainfrom
agent/BeyondTheSim.jl-17
Sep 12, 2026
Merged

1-Bart-1 merged 2 commits into
mainfrom
agent/BeyondTheSim.jl-17

Conversation

@1-Bort-1

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

Copy link
Copy Markdown
Contributor

TL;DR

generate_airfoils shrink-wrapped a contour both of its callers had already wrapped, and that second wrap tips the least-squares Kulfan fit into rank deficiency for about a fifth of the SK100's sections, writing their airfoil .dat files at 1e4 scale. It now fits the contour it is given — which is what its own docstring says it receives — so the wrap_method the caller configured is the only one that runs. Closes #293.

What was wrong

Both front ends wrap before they call. obj_to_yaml does it at obj_to_yaml.jl:221 and the Surfplan adapter at SurfplanAdapter.jl:71, each with the wrap_method its settings name, and both hand the result in as x_fit/y_fit. generate_airfoils then called generate_airfoil_aero(solver, x::Vector, y::Vector), whose whole job is to shrink_wrap a raw slice before fitting — and with a bare ShrinkWrap(), so the configured wrap was quietly replaced by the default one as well.

Wrapping a wrapped contour is not a small perturbation of the fit. The second wrap rounds an already-rounded convex corner into an arc of near-coincident vertices; smoothed_curvature divides the turn across a span of order 1e-16, so resample_arc's curvature-weighted measure blows up and crowds most of the upper stations into one narrow band of x. The upper Bernstein block of the fit matrix then goes numerically singular — on SK100 section 16, cond(A) moves from 1.07e3 to 2.52e11 — and A \ y_norm at kulfan.jl:147 returns weights of order 1e5. The residual is normal, because those weights cancel on the stations they were fitted to; kulfan_to_coordinates re-evaluates them on its own cosine grid, where they do not, and y reaches -13013.

Nine of the 45 sections come out that way, and the contours written from them — airfoils/{6,8,9,10,12,16,27,29,43}.dat — carry coordinates from 1e2 to 1e4 where a clean section is 0..1. Nothing in the pipeline notices: the polars still solve, the node counts are still right, the run reports every airfoil as converged, and the geometry is only refused much later by a consumer that maps the mesh onto a structure. In SymbolicAWEModels that reads AeroPressure wing 1: max surface-node→point distance is 35776.8× the local chord, exceeding frame_tol_frac=2.0, which is where this was found: it is what stopped 1-Bart-1/BeyondTheSim.jl#17 from building a model at all.

How the test pins it

The written contour has to be the fit of the contour that was handed in. Wrapping again inflates the section by the default clearance, 0.006, so the test measures the extremes of the written .dat against kulfan_to_coordinates(fit_kulfan_parameters(x_fit, y_fit, LeastSquaresFit())) and allows 1e-4. With the second wrap that gap is 6.27e-3; without it, 6.31e-6 — the resampling error, three orders of magnitude clear of the tolerance. It uses the suite's own test_airfoil.dat, so it needs no fixture, and it fails on any airfoil rather than only on the sections whose fit happens to blow up.

Where I would push back

This removes the trigger, not the fragility. The fit at kulfan.jl:147 is an unregularised A \ y with no rank check, no coefficient bound and no check on what kulfan_to_coordinates then produces — while KulfanBasis in deform.jl:44 already ridge-regularises the same kind of solve. Any other input that crowds the resample will do the same thing just as silently, and deform_section still wraps once more per deflection on a default ShrinkWrap() rather than the caller's. Raised separately rather than widened into this diff.

One thing worth knowing when judging generated tables: a blown-up section is not always obvious from its bounding box. 10_d10.dat looks normalised because it was rescaled by a large chord — its lower surface is a constant min_clearance/chord. Thickness is the honest check, not x ∈ [0, 1].

Verification

  • Reproduced first: airfoils/10.dat starting 144.26245312 47009.20858195, and the consumer's max surface-node→point distance is 35776.8× the local chord
  • test/airfoil_aero/test_airfoil_aero.jl red before the fix (1 passed, 1 failed), green after (all testsets pass, exit 0)
  • test/obj_adapter/test_obj_adapter.jl not run locally: this worktree has no juliaserver session of its own — its directory name collides with another task's upstream worktree — so the tests were run from the dependent's environment, which does not carry the suite's YAML and Aqua. GitHub CI is the authority for the rest of the suite.
  • Risk: the fix changes generated geometry for every user of both adapters. Tables generated before it do not reproduce after it — a section that was fitting a doubly-wrapped contour now fits the caller's, so its polars move by the difference between one wrap and two.

Scope

+19 / -1 across 3 files: the one call in src/airfoil_aero/geometry_gen.jl, its regression test, and the changelog entry. No stack — no open pull request here touches either file.

From 1-Bart-1/BeyondTheSim.jl#17 · task BeyondTheSim.jl-17

Both adapters shrink-wrap a slice with the wrap_method their settings name and
pass the result in as x_fit/y_fit, and generate_airfoils then called the
convenience method whose job is to wrap a raw slice — a second wrap, with a
default ShrinkWrap() rather than the configured one. On the SK100 that
degenerates the least-squares Kulfan fit for nine of 45 sections and writes
their contours at 1e2 to 1e4 instead of 0..1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eV7cQVw5begFgMvCkRsW3
@codecov

codecov Bot commented Sep 10, 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

Copy link
Copy Markdown
Contributor Author

This is the case that made me want figures in PR cards, so let's do it here first.

The argument is entirely visual and the diff is three lines — a reader can't check cond(A) 1.07e3 → 2.52e11 by reading it, but they can see that a blown-up section stops looking like an airfoil. Add to the card:

  • 2D slice, before vs after, for at least one of the sections that break ({6,8,9,10,12,16,27,29,43}), on identical axes, against the contour that was handed in. Plot thickness rather than the bounding box — you noted yourself that 10_d10.dat looks normalised because a large chord rescaled it, and a bounding-box figure would hide exactly that.
  • Section polars for the same sections, before and after, over several delta. The generated polars moving is the real risk of this change, so show it.

Render the before from the merge base in a separate worktree, same inputs and same axes limits as the after. Existing exports in the Makie ext (plot_airfoil_fit, plot_airfoils, plot_section_polars) should get you most of the way; new functions there are fine if they're worth shipping to users.

Hosting is 1-Bart-1/Agents#226 and may not be ready. If it isn't, say so here and hold — don't commit PNGs to this branch. Keep the existing text; the figures are evidence for it, not a replacement.

@1-Bort-1

Copy link
Copy Markdown
Contributor Author

The fix is right and the test pins it at δ=0. What it doesn't cover is the δ grid.

generate_airfoils writes airfoils/{id}_d<δ>.dat and the _cp/_cf node tables off generate_airfoil_aero's per-δ columns, and those inherit the base fit: kulfan_to_coordinates(base)deform_section per δ (rotate about crease_frac, re-wrap at clearance=0, refit). The new test leaves delta_range unset, so only the δ=0 contour is checked — while on the SK100 the deflected files were wrong too, 10_d10.dat with its lower surface pinned at a constant min_clearance/chord.

Worth adding, here or as a follow-up: one generate_airfoils run with a delta_range spanning both signs over a wrapped contour — the suite's own test_airfoil.dat, wrapped, as this test already does — asserting every written _d<δ>.dat on thickness and chordwise extent. Thickness rather than bounding box: a section rescaled by a large chord passes extrema(x) ≈ [0, 1], which is the form the existing deform_section flap assertions use.

The unregularised fit underneath is #295.

@1-Bart-1

1-Bart-1 commented Sep 11, 2026

Copy link
Copy Markdown
Member

@1-Bort-1 Add a delta range test to this PR

@1-Bort-1

Copy link
Copy Markdown
Contributor Author

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

@1-Bort-1 1-Bort-1 added the agent:blocked Agent task state label Sep 11, 2026
@1-Bart-1

Copy link
Copy Markdown
Member

Fix the error, try again

@1-Bart-1

Copy link
Copy Markdown
Member

Which example can I run to see an alpha, delta vs Cl plot?

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

Copy link
Copy Markdown
Contributor Author

No example does. plot_polar_data is the only thing in the repo that draws Cl over (α, δ), and its only callers are test/plotting/test_plotting.jl:164 and :167, both with is_show=false.

These lines from the repo root give you the figure below, in 5 s once the session is warm:

using MakieControlPlots, GLMakie, VortexStepMethod
using VortexStepMethod.ObjAdapter
using VortexStepMethod.AirfoilAero: NeuralFoilSolver

yaml = obj_to_yaml(joinpath("data", "ram_air_kite", "ram_air_kite_body.obj"), mktempdir();
    n_sections=4, Re=1e6, alpha_range=-10:2:20, delta_range=-10:5:10,
    aero_solver=NeuralFoilSolver(), wingtip_distance=0.05, verbose=false)
body_aero = BodyAerodynamics([Wing(yaml; n_panels=16)])

plot_polar_data(body_aero;
    alphas=collect(deg2rad.(-10:0.5:20)), delta_tes=collect(deg2rad.(-10:0.5:10)))

Cl, Cd and Cm over alpha and delta for a 4-section ram-air wing generated with NeuralFoil (alpha -10..20 deg, delta -10..10 deg), drawn by plot_polar_data

Two things that are easy to trip over. The extension trigger is MakieControlPlots, not GLMakie (Project.toml: VortexStepMethodMakieExt = ["MakieControlPlots"]), so using GLMakie alone leaves you with MethodError: no method matching plot_polar_data. And alphas/delta_tes both default to -5:0.3:25 degrees, so on a table generated over anything narrower most of the surface you are looking at is extrapolation — pass the ranges you generated.

It needs POLAR_MATRICES, so a geometry built with a delta_range. Of the examples only linearize_check.jl:20-24 (δ = -5:5:5, NeuralFoil) and ram_air_kite.jl:41-44 (δ = 0:1, XFoil, so a two-point δ axis) build such a body, and adding plot_polar_data(body_aero) to either is one line. Say the word and it goes in as its own PR — it is a different idea from this branch, so I have not put it on this one.

Separately, on this branch: the box could not install the worktree because ./bin/install -y ran without a display and GLFW cannot initialise while precompiling the Makie extension. With DISPLAY=:99 it is exit 0, so test/obj_adapter/test_obj_adapter.jl — the one item the card here listed as not run — is now run and green, ObjAdapter | 52 52 42.3s, with test/airfoil_aero/test_airfoil_aero.jl all-pass beside it.

@1-Bort-1 1-Bort-1 added agent:ci Agent task state and removed agent:running Agent task state labels Sep 12, 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 · 0 inline, 0 off the diff

Good

  • The fix removes the double-shrink-wrap path by passing already-fitted Kulfan parameters to generate_airfoil_aero, matching the docstring and caller behavior.
  • Regression test uses the existing test_airfoil.dat fixture and a tolerance tied to resampling error, failing with the second wrap and passing without it.
  • CHANGELOG entry records the user-visible geometry change under Fixed.
  • Diff stays tightly scoped to the one behavioral fix, its test, and the changelog entry.

Not good

  • Test comment at lines 286-287 spans two lines; the rubric asks for one-line inline comments, with rationale kept in the PR description.

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

@1-Bort-1

1-Bort-1 commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Local full suite: FAIL (18 min, Julia 1.12.7, one cell of the matrix)

  generated_slices reads the deflected .dat under its generated name | �[32m   6  �[39m�[91m      �[39m�[33m        �[39m�[36m    6  �[39m�[0m   15.2s
  Audit slices (Makie)                                               | �[32m   7  �[39m�[91m      �[39m�[33m        �[39m�[36m    7  �[39m�[0m 1m03.1s
  Kite Geometry Tests                                                | �[32m  21  �[39m�[91m      �[39m�[33m     1  �[39m�[36m   22  �[39m�[0m   41.4s
  Test settings.jl                                                   | �[32m   9  �[39m�[91m      �[39m�[33m        �[39m�[36m    9  �[39m�[0m    3.5s
  mesh and airfoil blocks                                            | �[32m  17  �[39m�[91m      �[39m�[33m        �[39m�[36m   17  �[39m�[0m    1.7s
  an unnamed mesh block slices as an unconfigured call               | �[32m   2  �[39m�[91m      �[39m�[33m        �[39m�[36m    2  �[39m�[0m   41.4s
  Solver Constructor Tests                                           | �[32m   3  �[39m�[91m      �[39m�[33m        �[39m�[36m    3  �[39m�[0m   27.2s
  NONLIN solve! re-runs across calls                                 | �[32m   2  �[39m�[91m      �[39m�[33m        �[39m�[36m    2  �[39m�[0m    0.3s
  NONLIN converges past stall, where LOOP already did                | �[32m   3  �[39m�[91m      �[39m�[33m        �[39m�[36m    3  �[39m�[0m    0.0s
  LOOP converges on the residual, not on the relaxed step            | �[32m   1  �[39m�[91m   1  �[39m�[33m        �[39m�[36m    2  �[39m�[0m    4.3s
RNG of the outermost testset: Random.Xoshiro(0x007a20f36bdfd78f, 0x6e23886ee61dec36, 0x96f7ca05cfe97bb3, 0xe9ef7ea08400325e, 0xa34822c0c9ca6742)
�[91m�[1mERROR: �[22m�[39mLoadError: �[91mSome tests did not pass: 5259 passed, 1 failed, 0 errored, 1 broken.�[39m
in expression starting at /home/agent/worktrees/VortexStepMethod.jl-294/test/runtests.jl:73
�[91m�[1mERROR: �[22m�[39mPackage VortexStepMethod errored during testing
Stacktrace:
  [1] �[0m�[1mpkgerror�[22m�[0m�[1m(�[22m�[90mmsg�[39m::�[0mString�[0m�[1m)�[22m
�[90m    @�[39m �[35mPkg.Types�[39m �[90m~/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Pkg/src/�[39m�[90m�[4mTypes.jl:68�[24m�[39m
  [2] �[0m�[1mtest�[22m�[0m�[1m(�[22m�[90mctx�[39m::�[0mPkg.Types.Context, �[90mpkgs�[39m::�[0mVector�[90m{PackageSpec}�[39m; �[90mcoverage�[39m::�[0mBool, �[90mjulia_args�[39m::�[0mCmd, �[90mtest_args�[39m::�[0mCmd, �[90mtest_fn�[39m::�[0mNothing, �[90mforce_latest_compatible_version�[39m::�[0mBool, �[90mallow_earlier_backwards_compatible_versions�[39m::�[0mBool, �[90mallow_reresolve�[39m::�[0mBool�[0m�[1m)�[22m
�[90m    @�[39m �[35mPkg.Operations�[39m �[90m~/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Pkg/src/�[39m�[90m�[4mOperations.jl:2642�[24m�[39m
  [3] �[0m�[1mtest�[22m
�[90m    @�[39m �[90m~/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Pkg/src/�[39m�[90m�[4mOperations.jl:2487�[24m�[39m�[90m [inlined]�[39m
  [4] �[0m�[1mtest�[22m�[0m�[1m(�[22m�[90mctx�[39m::�[0mPkg.Types.Context, �[90mpkgs�[39m::�[0mVector�[90m{PackageSpec}�[39m; �[90mcoverage�[39m::�[0mBool, �[90mtest_fn�[39m::�[0mNothing, �[90mjulia_args�[39m::�[0mCmd, �[90mtest_args�[39m::�[0mCmd, �[90mforce_latest_compatible_version�[39m::�[0mBool, �[90mallow_earlier_backwards_compatible_versions�[39m::�[0mBool, �[90mallow_reresolve�[39m::�[0mBool, �[90mkwargs�[39m::�[0m@Kwargs�[90m{io::IOContext{IO}}�[39m�[0m�[1m)�[22m
�[90m    @�[39m �[35mPkg.API�[39m �[90m~/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Pkg/src/�[39m�[90m�[4mAPI.jl:552�[24m�[39m
  [5] �[0m�[1mtest�[22m�[0m�[1m(�[22m�[90mpkgs�[39m::�[0mVector�[90m{PackageSpec}�[39m; �[90mio�[39m::�[0mIOContext�[90m{IO}�[39m, �[90mkwargs�[39m::�[0m@Kwargs�[90m{}�[39m�[0m�[1m)�[22m
�[90m    @�[39m �[35mPkg.API�[39m �[90m~/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Pkg/src/�[39m�[90m�[4mAPI.jl:169�[24m�[39m
  [6] �[0m�[1mtest�[22m�[0m�[1m(�[22m�[90mpkgs�[39m::�[0mVector�[90m{PackageSpec}�[39m�[0m�[1m)�[22m
�[90m    @�[39m �[35mPkg.API�[39m �[90m~/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Pkg/src/�[39m�[90m�[4mAPI.jl:158�[24m�[39m
  [7] �[0m�[1mtest�[22m�[0m�[1m(�[22m; �[90mname�[39m::�[0mNothing, �[90muuid�[39m::�[0mNothing, �[90mversion�[39m::�[0mNothing, �[90murl�[39m::�[0mNothing, �[90mrev�[39m::�[0mNothing, �[90mpath�[39m::�[0mNothing, �[90mmode�[39m::�[0mPackageMode, �[90msubdir�[39m::�[0mNothing, �[90mkwargs�[39m::�[0m@Kwargs�[90m{}�[39m�[0m�[1m)�[22m
�[90m    @�[39m �[35mPkg.API�[39m �[90m~/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Pkg/src/�[39m�[90m�[4mAPI.jl:186�[24m�[39m
  [8] �[0m�[1mtest�[22m�[0m�[1m(�[22m�[0m�[1m)�[22m
�[90m    @�[39m �[35mPkg.API�[39m �[90m~/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Pkg/src/�[39m�[90m�[4mAPI.jl:175�[24m�[39m
  [9] top-level scope
�[90m    @�[39m �[90m�[4mnone:1�[24m�[39m
 [10] �[0m�[1meval�[22m�[0m�[1m(�[22m�[90mm�[39m::�[0mModule, �[90me�[39m::�[0mAny�[0m�[1m)�[22m
�[90m    @�[39m �[90mCore�[39m �[90m./�[39m�[90m�[4mboot.jl:489�[24m�[39m
 [11] �[0m�[1mexec_options�[22m�[0m�[1m(�[22m�[90mopts�[39m::�[0mBase.JLOptions�[0m�[1m)�[22m
�[90m    @�[39m �[90mBase�[39m �[90m./�[39m�[90m�[4mclient.jl:283�[24m�[39m
 [12] �[0m�[1m_start�[22m�[0m�[1m(�[22m�[0m�[1m)�[22m
�[90m    @�[39m �[90mBase�[39m �[90m./�[39m�[90m�[4mclient.jl:550�[24m�[39m

@1-Bart-1
1-Bart-1 enabled auto-merge September 12, 2026 10:21
@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 12, 2026
@1-Bart-1
1-Bart-1 merged commit 9e7e320 into main Sep 12, 2026
15 of 16 checks passed
@1-Bort-1 1-Bort-1 added agent:ci Agent task state agent:queued Agent task state agent:running Agent task state agent:done Agent task state and removed agent:running Agent task state agent:queued Agent task state agent:ci Agent task state labels Sep 12, 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.

obj_to_yaml shrink-wraps twice, degenerating the Kulfan fit for a fifth of the SK100 sections

2 participants