diff --git a/src/openflight/ballistics.py b/src/openflight/ballistics.py index 85a53ed2..8f64c6b8 100644 --- a/src/openflight/ballistics.py +++ b/src/openflight/ballistics.py @@ -44,10 +44,23 @@ # These are simple parametric forms consistent with Bearman & Harvey (1976) # and Kensrud & Smith (2018) for dimpled balls past the drag crisis # (Re ~ 5e4–2e5), which covers the full range of realistic golf shots. -CD_BASE = 0.205 -CD_SPIN_COEFF = 0.18 -CL_SATURATION = 0.32 -CL_HALF_SP = 0.15 +# +# Fitted with scripts/analysis/sweep_ballistic_coeffs.py against the committed +# TrackMan capture (session_logs/OpenFlight-Test.Normalized.csv, 24 shots, +# differential evolution + Nelder-Mead, rho=1.184 to match TrackMan "Flat"). +# Overall carry RMSE against TrackMan: 24.52 -> 3.97 yd. The previous +# CL_HALF_SP of 0.15 sat well above the low-spin driver regime (driver +# Sp ~ 0.05-0.08), so for drivers the lift curve never left its low-lift +# regime and driver carry ran ~37 yd short. Irons and wedges (Sp ~ 0.21-0.63) +# were already past CL_HALF_SP and ran long instead, which is why the error +# flipped sign by club. Resulting Cl is ~0.13-0.16 for drivers, rising to +# ~0.22-0.23 for irons and wedges; the iron/wedge values sit inside the +# 0.18-0.25 band the cited sources report, while the driver values remain +# below it. +CD_BASE = 0.19071 +CD_SPIN_COEFF = 0.31588 +CL_SATURATION = 0.25544 +CL_HALF_SP = 0.04758 # Exponential spin decay: ω(t) = ω₀·exp(-rate·t). # ~4%/s per Kiratidis & Leinweber (2018); small but matters over ~6 s flights. diff --git a/tests/test_ballistics.py b/tests/test_ballistics.py index 4b4ff085..5cc6df70 100644 --- a/tests/test_ballistics.py +++ b/tests/test_ballistics.py @@ -1,11 +1,13 @@ """Tests for the ballistics flight simulator and launch resolution.""" +import math from datetime import datetime import pytest from openflight.ballistics import ( CLUB_TYPICAL_SPIN_RPM, + SPIN_DECAY_RATE, LaunchConditions, resolve_launch, simulate, @@ -136,10 +138,17 @@ def test_trajectory_ends_at_ground(self): assert traj.points[-1].t == pytest.approx(traj.flight_time_s, rel=0.01) def test_spin_decays_over_flight(self): - traj = simulate(_driver(spin_rpm=3000)) + initial_spin = 3000 + traj = simulate(_driver(spin_rpm=initial_spin)) final_spin = traj.points[-1].spin_rpm - # 4%/s for ~6s flight → ~80% of initial - assert 2300 < final_spin < 2900 + + # Assert the decay law itself rather than a hardcoded window, so the + # test stays valid when aero coefficients change the flight time. + expected = initial_spin * math.exp(-SPIN_DECAY_RATE * traj.flight_time_s) + assert final_spin == pytest.approx(expected, rel=1e-3) + + # Sanity: spin must fall, but not collapse over a single flight. + assert 0.6 * initial_spin < final_spin < initial_spin def test_flight_time_reasonable(self): traj = simulate(_driver()) diff --git a/tests/test_ballistics_trackman_regression.py b/tests/test_ballistics_trackman_regression.py index 3e4aed19..4d301d81 100644 --- a/tests/test_ballistics_trackman_regression.py +++ b/tests/test_ballistics_trackman_regression.py @@ -36,25 +36,24 @@ TRACKMAN_CSV = _REPO_ROOT / "session_logs" / "OpenFlight-Test.Normalized.csv" -# Per-club RMSE ceilings in yards, seeded from the measured baseline on -# c9d0cc7 (2026-08-18) with a small tolerance for float/platform drift: +# Per-club RMSE ceilings in yards. Ratcheted down after the aero coefficients +# were re-fit against this same capture (see ballistics.py CD_/CL_ constants). # -# club n rmse mae bias -# 7-iron 9 11.64 11.00 +11.00 -# driver 8 38.60 37.18 -37.18 -# pitching wedge 7 13.56 13.16 +11.02 -# OVERALL 24 24.52 20.36 -5.05 +# before re-fit after re-fit +# club rmse bias rmse bias +# 7-iron 11.64 +11.00 2.96 +1.52 +# driver 38.60 -37.18 1.75 -0.45 +# pitching wedge 13.56 +11.02 6.26 -2.96 +# OVERALL 24.52 -5.05 3.97 -0.45 # -# The driver number is large because Cl is mis-fit at low spin parameter -# (CL_HALF_SP=0.15 sits above the driver's whole Sp range, so the lift curve -# never leaves its low-lift regime). Fixing the aero constants should drop -# driver RMSE substantially -- when it does, lower these ceilings. +# Ceilings sit slightly above the measured values to absorb float/platform +# drift. Lower them again if the model improves; never raise them. RMSE_BUDGET_YARDS = { - "driver": 40.0, - "7-iron": 13.0, - "pitching wedge": 15.0, + "driver": 3.0, + "7-iron": 4.0, + "pitching wedge": 7.5, } -OVERALL_RMSE_BUDGET_YARDS = 26.0 +OVERALL_RMSE_BUDGET_YARDS = 5.0 # The reference capture is a fixed, committed file: if the shot count changes, # the fixture changed and every budget above needs re-deriving.