Summary
WindScenario turbulence is not reproducible across platforms, and the Python/TypeScript parity fixture that is supposed to guard PyQt6↔React equivalence cannot hold at the precision it currently asserts.
Where
src/shared/python/swing_sim/flight/wind.py, _unit_noise:
phase_source = math.sin((seed + 1) * (axis + 1) * 12.9898 + (harmonic + 1) * 78.233)
phase = (phase_source * 43758.5453 % 1.0) * 2.0 * math.pi
This is the GLSL fract(sin(x) * 43758.5453) hash. It multiplies a sine by ~4.4e4 and then takes the fractional part.
Why it is a defect
math.sin is not correctly rounded and differs by ~1 ulp between glibc, MSVC and V8. The hash amplifies that:
- a ~1e-16 relative difference in
sin becomes ~4e-12 absolute in phase_source * 43758.5453
% 1.0 keeps that error, and * 2π carries it into the phase
Measured: the golden fixture case evaluates to 9.786440272809793 on Linux/CPython 3.11 against a recorded 9.7864402728063, a 3.49e-12 absolute difference. The same test passes on Windows/CPython 3.13.
The worse failure mode is not this one. When phase_source * 43758.5453 lands near an integer boundary, a 1-ulp input difference flips the fractional part across the discontinuity and the phase changes by O(1) — so the divergence is unbounded in principle, not merely small. Nothing in the current design prevents a fixture case from sitting near such a boundary.
Current mitigation (not a fix)
The parity assertions were relaxed from 1e-12 to 1e-9 on both sides so CI states a tolerance the algorithm can actually meet:
src/shared/python/swing_sim/flight/tests/test_wind.py
src/rate_of_closure/web/src/model/wind.test.ts
1e-9 m/s is far below any physically meaningful wind speed, so the parity contract still has real content. But it papers over the discontinuity risk rather than removing it.
Suggested fix
Replace the fract(sin(x) * k) hash with an integer-domain hash that is exactly reproducible in both languages — both CPython ints and JS BigInt/Math.imul can evaluate a fixed integer mixing function bit-for-bit. The turbulence field then becomes identical across runtimes by construction, and the fixture can be pinned back to full double precision.
This changes generated turbulence values, so it needs a fixture regeneration and a SPEC note.
Acceptance
Summary
WindScenarioturbulence is not reproducible across platforms, and the Python/TypeScript parity fixture that is supposed to guard PyQt6↔React equivalence cannot hold at the precision it currently asserts.Where
src/shared/python/swing_sim/flight/wind.py,_unit_noise:This is the GLSL
fract(sin(x) * 43758.5453)hash. It multiplies a sine by ~4.4e4 and then takes the fractional part.Why it is a defect
math.sinis not correctly rounded and differs by ~1 ulp between glibc, MSVC and V8. The hash amplifies that:sinbecomes ~4e-12 absolute inphase_source * 43758.5453% 1.0keeps that error, and* 2πcarries it into the phaseMeasured: the golden fixture case evaluates to
9.786440272809793on Linux/CPython 3.11 against a recorded9.7864402728063, a 3.49e-12 absolute difference. The same test passes on Windows/CPython 3.13.The worse failure mode is not this one. When
phase_source * 43758.5453lands near an integer boundary, a 1-ulp input difference flips the fractional part across the discontinuity and the phase changes by O(1) — so the divergence is unbounded in principle, not merely small. Nothing in the current design prevents a fixture case from sitting near such a boundary.Current mitigation (not a fix)
The parity assertions were relaxed from 1e-12 to 1e-9 on both sides so CI states a tolerance the algorithm can actually meet:
src/shared/python/swing_sim/flight/tests/test_wind.pysrc/rate_of_closure/web/src/model/wind.test.ts1e-9 m/s is far below any physically meaningful wind speed, so the parity contract still has real content. But it papers over the discontinuity risk rather than removing it.
Suggested fix
Replace the
fract(sin(x) * k)hash with an integer-domain hash that is exactly reproducible in both languages — both CPython ints and JSBigInt/Math.imulcan evaluate a fixed integer mixing function bit-for-bit. The turbulence field then becomes identical across runtimes by construction, and the fixture can be pinned back to full double precision.This changes generated turbulence values, so it needs a fixture regeneration and a SPEC note.
Acceptance
fract(sin(x) * k)wind_scenario_golden_v1.jsonregenerated (or a v2 fixture added)% 1.0boundary is included in the fixture