Skip to content

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.

\[ \vec{F} = m\vec{g} - \tfrac{1}{2}\rho A C_d \lvert\vec{v}\rvert \vec{v} + \tfrac{1}{2}\rho A C_l \lvert\vec{v}\rvert^2 \hat{n} \]

The coefficients depend on the spin parameter \(S_p = r\omega / v\) — the ratio of surface speed to translational speed:

\[ C_d = a + b\,S_p + c\,S_p^2 \qquad C_l = \max\!\left(0,\; d + e\,S_p + f\,S_p^2\right) \]

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:

\[ \omega(t) = \omega_0 \, e^{-\lambda t} \]

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

scripts/start-kiosk.sh --no-ballistics

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.