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.
- Ridge-regularise or rank-truncate the solve at
kulfan.jl:147, the way deform.jl:44 already does.
- Clamp the curvature density in
smoothed_curvature, or cap the weighted measure in resample_arc relative to the true arclength.
- 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.
fit_kulfan_parameters(x, y, ::LeastSquaresFit)solvesA \ y_normatsrc/airfoil_aero/kulfan.jl:147with no ridge, no rank or condition check, no bound on the coefficients, and no check on whatkulfan_to_coordinatesthen 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 contourThe residual is 0.0207 in both rows.
kulfan_to_coordinatesevaluates the weights on its own cosine grid rather than on the fitted stations, which is where the cancellation stops holding and the shape explodes.KulfanBasisinsrc/airfoil_aero/deform.jl:44already 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 fromsmoothed_curvature(:229). Its only guard isspan > 0at:242, andpivot_contouremits 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_airfoilsonly errors when everyclisNaN, and a needle-shaped section still returns finitecl, so the id is reported as converged and its polar andCp/cftables 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.datfrom the same run looks normalised because it was rescaled by a large chord, its lower surface sitting at a constantmin_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.
kulfan.jl:147, the waydeform.jl:44already does.smoothed_curvature, or cap the weighted measure inresample_arcrelative to the true arclength.write_section_aerothat 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_airfoilswrapping 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.