Skip to content

Ball-speed cosine correction + opt-in calculated spin - #1

Closed
johnpacino wants to merge 51 commits into
mainfrom
feat/ball-speed-spin
Closed

Ball-speed cosine correction + opt-in calculated spin#1
johnpacino wants to merge 51 commits into
mainfrom
feat/ball-speed-spin

Conversation

@johnpacino

Copy link
Copy Markdown
Owner

Summary

Two opt-in launch-monitor accuracy features (both off by default) plus a UI provenance indicator. The shot-detection and K-LD7 angle pipeline are untouched — these consume the launch angle the existing estimator produces, so they're independent of the in-progress two-ray angle work.

Ball-speed cosine correction — --ball-speed-cosine-correction

The OPS243 reads the radial component of a ball departing at the launch angle, so the raw reading is compressed by cos(launch − ball-elevation-from-radar). correct_ball_speed() divides that out using the measured VLA + radar geometry (--kld7-ball-distance, new --kld7-radar-height-inches). Works with any launch-angle source (naive / geometry / two_ray). TrackMan-validated at ~0.9 mph MAE across 7-iron / PW / driver.

Calculated spin — --calculated-spin

The 24 GHz return has no usable spin line. When the vertical launch angle was radar/camera-measured, spin_rpm is rewritten with the kinematic estimate 170·v·sin(LA)^1.2 (spin_estimate.py); the displaced radar value is preserved in spin_rpm_measured. Runs after the cosine correction (the model is calibrated on true ball speed). Falls through untouched when the launch angle is a club-table estimate or outside the calibrated 2°–60° range.

UI

The Spin Rate card shows an "estimated" subtext when spin came from the model, "radar" when measured — mirroring the H. Launch card's source subtext. spin_source added to the shot payload + Shot type.

Testing

  • Python: full suite 606 passed, ruff clean, pylint 10.00/10 on the new modules.
  • UI: build + lint clean, 13 tests pass.
  • New: test_speed_correction, test_spin_estimate, TestApplyCalculatedSpin (radar/camera gate, model range, rewrite-and-preserve).

🤖 Generated with Claude Code

dependabot Bot and others added 30 commits May 3, 2026 12:13
Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 7.0.1 to 7.1.1.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/eslint-plugin-react-hooks@7.1.1/packages/eslint-plugin-react-hooks)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-hooks
  dependency-version: 7.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [globals](https://github.com/sindresorhus/globals) from 17.5.0 to 17.6.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](sindresorhus/globals@v17.5.0...v17.6.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Adds `ballistics.py` with a deterministic drag + Magnus ODE simulator
(RK4, 500 Hz) and a `resolve_launch()` helper that substitutes
club-typical spin when measured spin confidence is below the "high"
threshold. Output is a single committed trajectory — no probabilistic
range — so it composes cleanly with future simulator handoffs.

Aerodynamic model uses Hill-type Cl(Sp) and linear Cd(Sp) fits
consistent with Bearman & Harvey (1976) and Kensrud & Smith (2018)
for dimpled balls past the drag crisis. Spin decays exponentially
at 4%/s per Kiratidis & Leinweber (2018).

No UI or server changes — module is standalone. Tests cover
launch-resolution policy and end-to-end trajectory sanity across
driver, iron, fade/draw, and launch-angle sweeps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update base drag and saturation coefficients for modern dimpled balls as measured by Kensrud & Smith (2018)
scripts/analysis/validate_ballistics.py runs the model on TrackMan
inputs (physics-only check) and on OpenFlight inputs (end-to-end check),
comparing each prediction to TrackMan's normalized flat carry. Air
density defaults to 1.184 kg/m^3 to match TM's 77F/sea-level
normalization. Per-shot CSV, per-club + per-session stats, and a
scatter plot per source.

scripts/analysis/sweep_ballistic_coeffs.py fits the four
ballistics.py aero constants (CD_BASE, CD_SPIN_COEFF, CL_SATURATION,
CL_HALF_SP) via differential-evolution + Nelder-Mead refinement against
TrackMan flat carry. Optional --loso flag does leave-one-session-out
cross-validation so we can see whether the fit generalizes.

Both scripts accept multiple --trackman / --comparison files with
matching --session-label values; shots are concatenated, tagged with
their origin, and broken out per-session in the reports and scatters
(club -> color, session -> marker).
server.py: new --no-ballistics CLI flag (default off, i.e. ballistics
on). When set, on_shot_detected skips resolve_launch/simulate entirely
and routes every non-mock shot through the table-based estimator. The
table-path log message now identifies which condition triggered the
fallback (no launch angle vs ballistics disabled), and the chosen mode
is announced at server startup.

start-kiosk.sh: parses --no-ballistics, forwards it to the server
command, and logs the mode alongside the existing camera/trigger lines.

tests/test_server.py: new test_carry_skips_ballistic_when_ballistics_disabled
that pins the contract — when the flag is off, resolve_launch and
simulate must not run even with a valid launch angle present.
…ops-sync

Fix OPS trigger timing with per-shot clock sync
The K-LD7 angle radar failed on clean installs with "kld7 package not
installed" because kld7 was an optional extra that no install path
actually pulled in: setup.sh installs .[ui,analysis], `uv sync` and CI
don't install extras, and only start-kiosk.sh's error hint mentioned
.[ui,kld7].

Move kld7>=0.2.1 into base dependencies. It's a tiny pure-Python wheel
whose only requirement (pyserial) is already a base dep, and the --kld7
runtime flag still gates actual hardware use, so it can't be forgotten
by any install path again. The defensive find_spec("kld7") guards remain.

- Remove the now-redundant kld7 optional extra
- Update start-kiosk.sh hint to .[ui]
- Update stale `pip install kld7` / `uv add kld7` messages in the kld7
  driver and helper scripts to point at the project install
- Add a regression test asserting kld7 is a base dependency
…arn/ui/globals-17.6.0

build(deps-dev): bump globals from 17.5.0 to 17.6.0 in /ui
…arn/ui/eslint-plugin-react-hooks-7.1.1

build(deps-dev): bump eslint-plugin-react-hooks from 7.0.1 to 7.1.1 in /ui
The script sourced .venv/bin/activate, which bakes in an absolute
VIRTUAL_ENV path at creation time. Moving the project directory left
activate pointing at a stale path, so openflight-server resolved to a
non-existent bin and startup failed with "command not found".

Resolve the environment dynamically instead: require uv, run uv sync to
reconcile against uv.lock, and launch with `uv run`. A moved, stale, or
missing venv now repairs itself rather than failing cryptically.
Document quick start, scripts, socket connection model, project
layout, and troubleshooting in place of the default Vite boilerplate.
docs(ui): replace Vite template README with OpenFlight UI docs
…-uv-env

fix(kiosk): launch server via uv run so a moved venv self-heals
Flip ballistics_enabled default to False so all shots use the legacy
table estimator unless the user opts in with --ballistics (CLI) or
--ballistics (start-kiosk.sh). Keeps the simulator available for A/B
work while Cd/Cl coefficients are still being validated against TM.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…cs-model

# Conflicts:
#	scripts/start-kiosk.sh
jewbetcha and others added 21 commits June 9, 2026 13:16
wait_for_hardware_trigger bounded TOTAL elapsed time, so a sound
trigger firing in the last ~5s of the 30s wait window had its ~46KB
rolling-buffer dump cut off mid-stream at the deadline ("Incomplete
capture (missing: Q)"), silently losing the shot. At a 30s re-arm
cadence that's roughly 1 in 6 random impacts.

Once the first byte arrives the trigger has fired and the dump is in
flight: extend the deadline to first_byte + dump_grace (8s, vs the
~4-5s a dump actually takes). The existing completeness and inactivity
exits still return as soon as the Q array closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.6.0 to 25.9.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
…yarn/ui/types/node-25.9.3

Bump @types/node from 25.6.0 to 25.9.3 in /ui
Two opt-in launch-monitor accuracy features, both off by default.

--ball-speed-cosine-correction: the OPS243 reads the radial component of
a ball departing at the launch angle; correct_ball_speed() divides out
cos(launch − ball elevation from radar) using the measured VLA + radar
geometry (--kld7-ball-distance, new --kld7-radar-height-inches). Works
with any launch-angle source.

--calculated-spin: the 24 GHz return carries no usable spin line, so when
the vertical launch angle was radar/camera-measured, spin_rpm is rewritten
with the kinematic estimate 170·v·sin(LA)^1.2 (spin_estimate.py); the
displaced radar value is kept in spin_rpm_measured. Runs after the cosine
correction (the model is calibrated on true ball speed).

Adds spin_source / ball_speed_raw_mph provenance to the shot payload and
the kiosk flag passthroughs. Tests: test_speed_correction, test_spin_estimate,
and TestApplyCalculatedSpin (radar/camera gate, model range, rewrite).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Spin Rate card shows an "estimated" subtext when spin came from the
calculated model (spin_source === 'calculated') and "radar" when measured,
mirroring the H. Launch card's source subtext. Adds spin_source to the
Shot type (and the DisplayMode test mock).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@johnpacino

Copy link
Copy Markdown
Owner Author

Closing — this targeted the fork's own main by mistake. The branch feat/ball-speed-spin is now rebased onto jewbetcha:main (same feature, clean gate: 755 py tests / UI build+lint+13) and ready for an upstream PR into jewbetcha:main.

@johnpacino
johnpacino force-pushed the feat/ball-speed-spin branch from 15984dd to 51d6206 Compare June 16, 2026 04:20
@johnpacino johnpacino closed this Jun 16, 2026
johnpacino pushed a commit that referenced this pull request Jun 17, 2026
…ht#115 review #1)

server._forward_shot_to_simulators guards each send with except OSError and documents it never raises into on_shot_detected. But TcpSimClient.send_raw raised RuntimeError when _sock was None, and there is a real TOCTOU window: is_connected() can return True, then _close_socket() nulls _sock before the next send acquires the lock. That RuntimeError slipped past the OSError guard into the shot thread. ConnectionError is an OSError subclass, so the existing guard now catches the raced disconnect (and send_shot's 'Raises OSError' docstring becomes accurate).

Tests: bug-first -- send_raw while disconnected now raises an OSError subclass (was RuntimeError), failing before the fix. Guarantee -- _forward_shot_to_simulators swallows a connector ConnectionError, emits sim_send_failed, and still delivers to other connectors. Also reset the server-wiring fixture's sim globals at teardown so fakes don't leak into other test modules.

Addresses PR open-flight#115 review comment #1 (merge blocker).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants