ram_air_matrix_wing (test/test_data_utils.jl:26) builds its wing by slicing data/ram_air_kite/ram_air_kite_body.obj and sweeping NeuralFoil over it into test/generated/ (gitignored, so CI regenerates it every run). That fixture is not reproducible across machines, and not even across two attempts of the same job.
norm(jac_fwd) from the @info "POLAR_MATRICES linearize jacobian norms" line in test/solver/test_forwarddiff.jl, run 34401975496 on #285 — same commit, same fixture config (n_panels=8, n_sections=4, alpha_range = deg2rad.(-5:5:15), delta_range = deg2rad.(-3:3:3)):
| where |
norm_fwd |
| attempt 1, Julia 1.12 ubuntu with code coverage |
2.1143274312638014 |
| attempt 3, Julia 1.12 macOS aarch64 / 1.12 windows x64 / 1.11 ubuntu coverage |
2.114573062599269 |
| attempt 3, Julia 1.11 ubuntu / 1.12 ubuntu / 1.12 ubuntu coverage |
2.165929233230906 |
| agent box, Debian x64 |
2.2058861369294172 |
A 4.3% spread, and the same job — Julia 1.12 ubuntu with code coverage — gives 2.1143274312638014 on attempt 1 and 2.165929233230906 on attempt 3.
It is the fixture and not the code. The INVISCID sibling in the same file, which builds its wing in Julia and touches no generated file, reads norm_fwd = 11.110746084832735, norm_fd = 11.110746104003324 on this box and the same 17 digits in that failing coverage job. So VSM's solver, geometry pipeline, ForwardDiff and FiniteDiff paths are bit-reproducible, coverage build included; what moves is what obj_to_yaml writes.
Two things are ruled out:
- Not the
%.4f polar rounding (write_polar_matrix_csv, src/airfoil_aero/airfoil_solvers/common.jl:62). Jittering every Cl/Cd/Cm entry by ±1 unit in the last written decimal, 15 draws, moves norm_fwd by ±0.02% — two orders below the observed spread.
- Not run-to-run on one box. Regenerating three times here with BLAS pinned to one thread gives byte-identical
geometry.yaml, polars/*.csv and airfoils/*.dat. Only the full-precision pressure/*_cf.csv differ, by a few ulps, and those do not feed cl/cd/cm.
Scale of it in physical terms: scaling every Cl by 5% moves norm_fwd by 2.7% and the panels' converged alphas by 0.09°, so a 4.3% spread is of order 0.1–0.2° of panel alpha — enough to move a panel across the polar table's 5° alpha grid.
That is what makes #287 a lottery: each run draws a different set of panel alphas, and a draw that lands one within the finite-difference stencil of a table knot fails the AD-vs-FD check. #287 fixes that check's own error budget; it does not make the fixture stop moving, and every other test asserting a number on this geometry is drawing from the same hat.
What I would look at first: whether the difference is in geometry.yaml's section coordinates or in the polar values, by having a run print a hash of each generated file. The coordinates go through yaml_scalar(x::Real) = string(round(Float64(x); digits=3)) (src/airfoil_aero/geometry_yaml.jl:22), 1 mm on a 0.168 m tip chord, which is ~0.3° of tip incidence per rounding unit — so a last-digit flip there is not small. Whether a fixed fixture should be committed, or the generation made deterministic, is the decision this needs.
Found while working #287, which changes only test/solver/test_forwarddiff.jl and leaves this alone.
ram_air_matrix_wing(test/test_data_utils.jl:26) builds its wing by slicingdata/ram_air_kite/ram_air_kite_body.objand sweeping NeuralFoil over it intotest/generated/(gitignored, so CI regenerates it every run). That fixture is not reproducible across machines, and not even across two attempts of the same job.norm(jac_fwd)from the@info "POLAR_MATRICES linearize jacobian norms"line intest/solver/test_forwarddiff.jl, run 34401975496 on #285 — same commit, same fixture config (n_panels=8,n_sections=4,alpha_range = deg2rad.(-5:5:15),delta_range = deg2rad.(-3:3:3)):norm_fwdA 4.3% spread, and the same job — Julia 1.12 ubuntu with code coverage — gives 2.1143274312638014 on attempt 1 and 2.165929233230906 on attempt 3.
It is the fixture and not the code. The INVISCID sibling in the same file, which builds its wing in Julia and touches no generated file, reads
norm_fwd = 11.110746084832735,norm_fd = 11.110746104003324on this box and the same 17 digits in that failing coverage job. So VSM's solver, geometry pipeline, ForwardDiff and FiniteDiff paths are bit-reproducible, coverage build included; what moves is whatobj_to_yamlwrites.Two things are ruled out:
%.4fpolar rounding (write_polar_matrix_csv,src/airfoil_aero/airfoil_solvers/common.jl:62). Jittering every Cl/Cd/Cm entry by ±1 unit in the last written decimal, 15 draws, movesnorm_fwdby ±0.02% — two orders below the observed spread.geometry.yaml,polars/*.csvandairfoils/*.dat. Only the full-precisionpressure/*_cf.csvdiffer, by a few ulps, and those do not feed cl/cd/cm.Scale of it in physical terms: scaling every Cl by 5% moves
norm_fwdby 2.7% and the panels' converged alphas by 0.09°, so a 4.3% spread is of order 0.1–0.2° of panel alpha — enough to move a panel across the polar table's 5° alpha grid.That is what makes #287 a lottery: each run draws a different set of panel alphas, and a draw that lands one within the finite-difference stencil of a table knot fails the AD-vs-FD check. #287 fixes that check's own error budget; it does not make the fixture stop moving, and every other test asserting a number on this geometry is drawing from the same hat.
What I would look at first: whether the difference is in
geometry.yaml's section coordinates or in the polar values, by having a run print a hash of each generated file. The coordinates go throughyaml_scalar(x::Real) = string(round(Float64(x); digits=3))(src/airfoil_aero/geometry_yaml.jl:22), 1 mm on a 0.168 m tip chord, which is ~0.3° of tip incidence per rounding unit — so a last-digit flip there is not small. Whether a fixed fixture should be committed, or the generation made deterministic, is the decision this needs.Found while working #287, which changes only
test/solver/test_forwarddiff.jland leaves this alone.