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 = C_{d,\text{base}} + k_d S_p \qquad C_l = \frac{C_{l,\text{sat}} \, S_p}{C_{l,\text{half}} + S_p} \]

Drag rises linearly with spin. Lift follows a Hill-type saturating form: it approaches a ceiling as spin increases rather than growing without bound, which is what the wind-tunnel data shows.

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_BASE 0.205 Drag at zero spin
CD_SPIN_COEFF 0.18 Linear drag rise with \(S_p\)
CL_SATURATION 0.32 Lift ceiling
CL_HALF_SP 0.15 \(S_p\) at half the lift ceiling
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.

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.