Ballistics & Carry¶
Carry is not measured — it is simulated. OpenFlight integrates the ball's trajectory from the launch conditions it measured, accounting for drag and the Magnus force from spin.
Implemented in src/openflight/ballistics.py.
The model¶
Three forces act on the ball: gravity, aerodynamic drag opposing motion, and the Magnus lift generated by backspin.
The coefficients depend on the spin parameter \(S_p = r\omega / v\) — the ratio of surface speed to translational speed:
Both are second-order polynomials in \(S_p\), the form Ferguson, McNally & McPhee (2022) fitted to 1040 measured shots; the coefficients are their published values. Lift rises with spin, peaks near \(S_p \approx 0.52\) and then falls, which is what the measured data shows and what a saturating form cannot represent. Above \(S_p = 0.75\), the top of the fitted range, both curves are held at their end value rather than extrapolated.
These parametric forms are consistent with Bearman & Harvey (1976) and Kensrud & Smith (2018) for dimpled balls past the drag crisis (Re ≈ 5×10⁴–2×10⁵), which covers the full range of realistic golf shots.
Spin decays exponentially over the flight:
at roughly 4 %/s per Kiratidis & Leinweber (2018) — small per second, but it matters across a six-second flight.
Constants¶
| Constant | Value | Note |
|---|---|---|
BALL_MASS_KG |
0.04593 | USGA maximum-conforming ball, 45.93 g |
BALL_RADIUS_M |
0.02135 | 42.7 mm diameter |
AIR_DENSITY_STD |
1.225 kg/m³ | Sea level, 15 °C ISA |
CD_POLY |
(0.1304, 0.9287, -0.8259) | \(C_d\) polynomial \((a, b, c)\) |
CL_POLY |
(0.0504, 1.2031, -1.1490) | \(C_l\) polynomial \((d, e, f)\) |
SP_FIT_MAX |
0.75 | Top of the fitted \(S_p\) range; curves held beyond it |
SPIN_DECAY_RATE |
0.04 /s | ≈4 %/s |
GRAVITY |
9.81 m/s² | |
DT_SECONDS |
0.002 | 500 Hz integration |
MAX_FLIGHT_SECONDS |
15.0 | Safety cap |
SAMPLE_INTERVAL_S |
0.05 | Cadence of returned trajectory points |
Why the maximum-conforming ball
Mass and radius use the USGA maximum rather than an average, so carry estimates are upper-bounded by the rules rather than by a guess about which specific ball is in play.
Integration¶
Fourth-order Runge-Kutta at 500 Hz. RK4 error is \(O(\Delta t^5)\), so at this step size the integration is effectively exact for the timescales involved; noticeably larger steps begin to shorten long drives.
The solver returns points every 50 ms rather than every step — integration still runs at the full rate, but the sampled list keeps WebSocket payloads and session logs a reasonable size.
Real shots terminate in 5–9 seconds. The 15-second cap exists so a solver instability or pathological input fails loudly instead of spinning.
Inputs and fallbacks¶
The simulator needs ball speed, launch angle, and spin. Ball speed always comes from the OPS243. The other two may be missing:
| Input | When present | When missing |
|---|---|---|
| Launch angle | IWR6843 measurement | Falls back to the legacy carry table estimator |
| Spin | OPS243, if confidence is high | Club-typical spin from TrackMan PGA Tour averages, by ClubType |
Measured spin is only trusted above SPIN_CONFIDENCE_HIGH (0.7). Below that,
the club-typical value is used — see
rolling buffer & spin detection for why.
--calculated-spin forces the kinematic estimate
\(170 \cdot v \cdot \sin(\text{LA})^{1.2}\) even when a measured value exists,
keeping the measured number in spin_rpm_measured for offline scoring.
Shot finalization in the server is the only place that writes
carry_spin_adjusted for a live shot: the simulator when it can run, the
spin table otherwise. The same committed number is what the kiosk shows and
what the simulator connectors receive.
Disabling it¶
Uses the legacy carry-table estimator for every shot. The simulator is the default; shots without a vertical launch angle fall back to the table anyway.
Related¶
- Rolling buffer & spin detection — where spin comes from
- Constants — the full constant set