write_section_aero writes an all-NaN deflected .dat, and read_section_aero loads it as a NaN contour without complaint
When XFoil converges at no angle of attack for one deflection, generate_airfoil_aero leaves that whole contour column at its fill(NaN, …) initial value, write_section_aero writes it out as airfoils/<id>_d<tag>.dat full of NaN NaN rows, and read_section_aero then loads it back into the wing's contour interpolants without a word. The loaded Wing has a NaN airfoil shape at every deflection, including exactly 0°.
The chain
src/airfoil_aero/airfoil_solvers/xfoil_solver.jl:60-63 — a non-converged angle returns a SectionSolution with empty node arrays. (NeuralFoilSolver always fills s.x = def.x at neuralfoil_solver.jl:32-42, so this is XFoil-only.)
src/airfoil_aero/section_aero_gen.jl:27-39 — the contour matrices start as fill(NaN, n_node, n_delta) and a column is written only from a solution whose length(s.cp) == n_node, the modal node count over the whole grid. Every angle of that deflection hitting the continue on line 32 leaves x[:, jd] entirely NaN. A column is all-finite or all-NaN, never mixed. fill_node_nans! repairs only cp/cf, never x/y.
src/airfoil_aero/geometry_gen.jl:48-49 — generate_airfoils checks only column 1 (all(isnan, clvals) over sols[1]), so a deflection that converged nowhere does not stop the id being reported as solved.
src/airfoil_aero/section_aero_gen.jl:137-141 — write_section_aero writes every non-zero deflection unconditionally, before the all-NaN guard on :142-146 runs. That guard's own comment states the hazard the deflected files are not protected from:
# never write `$dat_prefix.dat` all-NaN (read_dat_coordinates would drop it to empty)
finite(jd) = any(isfinite, view(aero.x, :, jd))
The two readers disagree, and the lenient one is the dangerous one
read_dat_coordinates (src/airfoil_aero/airfoil_io.jl:12) skips any line whose first character is not a digit, - or ., so it returns zero points and the caller crashes.
read_dat (src/section_aero.jl:126-137) has no such filter, and tryparse(Float64, "NaN") succeeds, so read_section_aero gets a full-length NaN vector, the lengths match, x[:, jd] .= xd succeeds, and the NaN reaches x_interp/y_interp (section_aero.jl:82-85). Linear interpolation between a finite and a NaN node is NaN everywhere, so section_surface returns a NaN contour at every delta — 0.0 included — and hands it to live_polar.jl:136 and the Makie skin plot. No error, no warning.
How it surfaced
CI job Test end-user and developer setup, twice on the same commit (runs 34585324837), on examples/ram_air_kite.jl with delta_range = 0:1 and XFoilSolver. Wing(yaml) on line 74 loaded fine — through the lenient reader, with a NaN contour. Line 79's audit plot, which goes through the strict reader, is what actually failed:
LoadError: ArgumentError: reducing over an empty collection is not allowed
[14] normalize_airfoil @ src/airfoil_aero/kulfan.jl:71 (argmin on empty x)
[15] fit_kulfan_parameters @ src/airfoil_aero/kulfan.jl:136
[17] generated_slices @ ext/VortexStepMethodMakieExt.jl:1630
[25] plot_slices_3d(path; delta=1.0, obj_path=...)
So the crash is the lucky case. The silent one is the default path for anyone loading a generated geometry.yaml.
Ten lines reproduce it with no solver at all — a SectionAero whose 1° column is NaN:
wrote: 7_d1.dat isfile=true
NaN NaN
read back: 0 points
fit_kulfan_parameters threw: ArgumentError: reducing over an empty collection is not allowed
Reproducible on the GitHub runner, not on the agent box, where XFoil converges for all 8 solved sections of that mesh and every _d1.dat reads back 239 clean points. The setup-test workflow resolves dependencies fresh on each run (test/test_setup.sh:62-65) and the example, unlike ram_air_matrix_dir (test/test_data_utils.jl:51-52), does not pin BLAS threads — so which deflections converge moves between runs. Related: #291 on that fixture's instability.
What I would do
- Skip the column in
write_section_aero's deflection loop when finite(jd) is false, hoisting the helper above the loop — one condition, and it reuses the guard already sitting three lines below. No all-NaN .dat is ever written, so isfile remains the honest test for every reader.
- Make
read_dat and read_dat_coordinates agree on NaN rows. Two readers of one format with different tolerances is how the silent case got in; the strict one's behaviour is the one worth keeping.
- Decide whether
generate_airfoils should fail an id outright when a deflection converged at no angle, the way it already does for column 1 (geometry_gen.jl:48-49). That is a behaviour question rather than a bug, and it is the one I would want an opinion on.
No test asserts a deflected .dat is re-readable; the one round-trip testset (test/airfoil_aero/test_airfoil_aero.jl:131) builds an all-finite contour and round-trips through the lenient reader, so it could not have caught this.
Related to #295, which names the same write_section_aero guard — but that issue is about a degenerate finite fit at 1e4 scale, not an all-NaN column, and neither fix covers the other.
Found while working #298 (#299), whose red Test end-user and developer setup check this is. That diff only changes which filename generated_slices asks for, and for the delta=1.0 this example uses the old and new schemes produce the identical _d1.dat — verified by evaluating both, and against the generated directory itself. Out of scope there.
write_section_aero writes an all-NaN deflected .dat, and read_section_aero loads it as a NaN contour without complaint
When XFoil converges at no angle of attack for one deflection,
generate_airfoil_aeroleaves that whole contour column at itsfill(NaN, …)initial value,write_section_aerowrites it out asairfoils/<id>_d<tag>.datfull ofNaN NaNrows, andread_section_aerothen loads it back into the wing's contour interpolants without a word. The loadedWinghas a NaN airfoil shape at every deflection, including exactly 0°.The chain
src/airfoil_aero/airfoil_solvers/xfoil_solver.jl:60-63— a non-converged angle returns aSectionSolutionwith empty node arrays. (NeuralFoilSolveralways fillss.x = def.xatneuralfoil_solver.jl:32-42, so this is XFoil-only.)src/airfoil_aero/section_aero_gen.jl:27-39— the contour matrices start asfill(NaN, n_node, n_delta)and a column is written only from a solution whoselength(s.cp) == n_node, the modal node count over the whole grid. Every angle of that deflection hitting thecontinueon line 32 leavesx[:, jd]entirely NaN. A column is all-finite or all-NaN, never mixed.fill_node_nans!repairs onlycp/cf, neverx/y.src/airfoil_aero/geometry_gen.jl:48-49—generate_airfoilschecks only column 1 (all(isnan, clvals)oversols[1]), so a deflection that converged nowhere does not stop the id being reported as solved.src/airfoil_aero/section_aero_gen.jl:137-141—write_section_aerowrites every non-zero deflection unconditionally, before the all-NaN guard on:142-146runs. That guard's own comment states the hazard the deflected files are not protected from:The two readers disagree, and the lenient one is the dangerous one
read_dat_coordinates(src/airfoil_aero/airfoil_io.jl:12) skips any line whose first character is not a digit,-or., so it returns zero points and the caller crashes.read_dat(src/section_aero.jl:126-137) has no such filter, andtryparse(Float64, "NaN")succeeds, soread_section_aerogets a full-length NaN vector, the lengths match,x[:, jd] .= xdsucceeds, and the NaN reachesx_interp/y_interp(section_aero.jl:82-85). Linear interpolation between a finite and a NaN node is NaN everywhere, sosection_surfacereturns a NaN contour at every delta — 0.0 included — and hands it tolive_polar.jl:136and the Makie skin plot. No error, no warning.How it surfaced
CI job
Test end-user and developer setup, twice on the same commit (runs 34585324837), onexamples/ram_air_kite.jlwithdelta_range = 0:1andXFoilSolver.Wing(yaml)on line 74 loaded fine — through the lenient reader, with a NaN contour. Line 79's audit plot, which goes through the strict reader, is what actually failed:So the crash is the lucky case. The silent one is the default path for anyone loading a generated
geometry.yaml.Ten lines reproduce it with no solver at all — a
SectionAerowhose 1° column is NaN:Reproducible on the GitHub runner, not on the agent box, where XFoil converges for all 8 solved sections of that mesh and every
_d1.datreads back 239 clean points. The setup-test workflow resolves dependencies fresh on each run (test/test_setup.sh:62-65) and the example, unlikeram_air_matrix_dir(test/test_data_utils.jl:51-52), does not pin BLAS threads — so which deflections converge moves between runs. Related: #291 on that fixture's instability.What I would do
write_section_aero's deflection loop whenfinite(jd)is false, hoisting the helper above the loop — one condition, and it reuses the guard already sitting three lines below. No all-NaN.datis ever written, soisfileremains the honest test for every reader.read_datandread_dat_coordinatesagree on NaN rows. Two readers of one format with different tolerances is how the silent case got in; the strict one's behaviour is the one worth keeping.generate_airfoilsshould fail an id outright when a deflection converged at no angle, the way it already does for column 1 (geometry_gen.jl:48-49). That is a behaviour question rather than a bug, and it is the one I would want an opinion on.No test asserts a deflected
.datis re-readable; the one round-trip testset (test/airfoil_aero/test_airfoil_aero.jl:131) builds an all-finite contour and round-trips through the lenient reader, so it could not have caught this.Related to #295, which names the same
write_section_aeroguard — but that issue is about a degenerate finite fit at 1e4 scale, not an all-NaN column, and neither fix covers the other.Found while working #298 (#299), whose red
Test end-user and developer setupcheck this is. That diff only changes which filenamegenerated_slicesasks for, and for thedelta=1.0this example uses the old and new schemes produce the identical_d1.dat— verified by evaluating both, and against the generated directory itself. Out of scope there.