Skip to content

Wind turbulence hash is not cross-platform reproducible, weakening the PyQt6/React parity fixture #4513

Description

@dieterolson

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

  • Turbulence phase derives from an exactly reproducible integer hash, not fract(sin(x) * k)
  • wind_scenario_golden_v1.json regenerated (or a v2 fixture added)
  • Python and TypeScript assertions restored to full double precision and passing on Linux, Windows and Node
  • A case deliberately placed near a former % 1.0 boundary is included in the fixture

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions