Skip to content

The Kulfan least-squares fit has no rank guard, so a crowded resample writes an airfoil at 1e4 scale with a normal residual #295

Description

@1-Bort-1

fit_kulfan_parameters(x, y, ::LeastSquaresFit) solves A \ y_norm at src/airfoil_aero/kulfan.jl:147 with no ridge, no rank or condition check, no bound on the coefficients, and no check on what kulfan_to_coordinates then evaluates from them. When the contour handed to it has its stations crowded into a narrow band of x, the upper Bernstein columns go numerically dependent and the solve returns weights five orders of magnitude too large — while reporting a perfectly ordinary residual, because those weights cancel on the stations they were fitted against.

On SK100 mesh section 16, measured on the same slice:

cond(A) max abs(coeff) max abs(y) of the resampled contour
stations spread normally 1.073e3 1.23 0.0812
stations crowded 2.519e11 457400 13013

The residual is 0.0207 in both rows. kulfan_to_coordinates evaluates the weights on its own cosine grid rather than on the fitted stations, which is where the cancellation stops holding and the shape explodes.

KulfanBasis in src/airfoil_aero/deform.jl:44 already ridge-regularises the same shape of solve — (shape' * shape + ridge * scale * I) \ shape' — and its docstring describes exactly this failure, so the package has the remedy in one place and not in the other.

What crowds the stations

resample_arc (src/airfoil_aero/shrink_wrap.jl:293) spaces stations in a curvature-weighted measure whose density comes from smoothed_curvature (:229). Its only guard is span > 0 at :242, and pivot_contour emits near-coincident vertices around a rounded convex corner, so a span of order 1e-16 gives a density of order 1e8. On section 16 the weighted measure reaches 103570 for a curve whose arclength is 1.063, with five nodes holding all of it; 118 of the 120 upper stations then land inside a band 0.03 wide. enforce_min_spacing! works in the same distorted measure and does not undo it.

Why it matters beyond the section that triggered it

Nothing between the fit and the written file checks magnitude. write_dat (src/airfoil_aero/airfoil_io.jl:29) writes whatever it is given; write_section_aero's only guard (src/airfoil_aero/section_aero_gen.jl:143) is that a column is not all-NaN; generate_airfoils only errors when every cl is NaN, and a needle-shaped section still returns finite cl, so the id is reported as converged and its polar and Cp/cf tables are written from the broken shape. The failure surfaces much later, in a consumer that maps the mesh onto a structure.

It is also not always visible in a bounding box: 10_d10.dat from the same run looks normalised because it was rescaled by a large chord, its lower surface sitting at a constant min_clearance/chord. Thickness is the honest check.

What I would do

Any one of these closes the hole; the first two are the cheap ones.

  1. Ridge-regularise or rank-truncate the solve at kulfan.jl:147, the way deform.jl:44 already does.
  2. Clamp the curvature density in smoothed_curvature, or cap the weighted measure in resample_arc relative to the true arclength.
  3. Assert in write_section_aero that a section is on the unit chord and has a sane thickness, so a degenerate fit errors instead of being written and polared.

The immediate trigger — generate_airfoils wrapping an already-wrapped contour — is #293 and is fixed by the pull request this issue comes with. This is the reason that trigger was able to do so much damage, and the reason another input will.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions