fix(carry): commit carry once in finalization and use it everywhere - #276
Merged
jewbetcha merged 5 commits intoSep 23, 2026
Merged
Conversation
The rolling buffer monitor pre-fills carry_spin_adjusted with the spin-table estimate for every shot whose spin is not rejected. Shot finalization only ran the RK4 simulator when that field was still None, so any shot with a measured spin kept the table number and never reached the physics model. The table never sees the launch angle, which is why kiosk carry read 20-30 yards short of the debug estimate and of commercial launch monitors on the same swings. Run the simulator whenever ballistics is enabled and a launch angle is available, and keep the table fallback only for shots nothing else has filled. Rigs without a vertical launch angle are unchanged.
Remove the rolling buffer monitor's spin-table pre-fill of carry_spin_adjusted so finalization is the single writer: the ballistic simulator when it can run, the spin table otherwise. Shots without a vertical launch angle now go through the server's fallback, which trusts measured spin only at confidence 0.6 or above and otherwise uses the club-optimal spin. Route the committed carry to the simulator connectors as well. The resolver sent the bare launch-angle table to GSPro even when the simulator had produced a carry for the kiosk, so the two disagreed.
SpinResult.is_reliable and the server's table-carry fallback each carried their own 0.6 literal. They must agree, because the monitor used to gate its carry pre-fill on is_reliable and the server fallback now stands in for it on rigs without a launch angle. One constant, one boundary test.
…le collects on older trees
jewbetcha
approved these changes
Sep 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Makes shot finalization in
server.pythe single writer ofcarry_spin_adjusted, and sends that committed carry to the simulator connectors.Three related changes, one story:
server.py: run the RK4 simulator whenever ballistics is enabled and a launch angle exists, replacing whatever is already on the shot; use the spin table only when the simulator cannot run. The old guard ran the simulator onlyif shot.carry_spin_adjusted is None.rolling_buffer/monitor.py: stop pre-fillingcarry_spin_adjustedwith the spin-table estimate in_create_shot. That pre-fill was what made the guard above skip the simulator on every shot with a measured spin. The table helper itself stays; the server's fallback uses it.sim/resolver.py: prefer the committedcarry_spin_adjustedover the bare launch-angle table when building the shot for GSPro and friends, so the connectors and the kiosk report the same carry.Why was this required?
Reported in
#openflight-helptoday: spin-adjusted carry on the kiosk reads far shorter than both the debug (launch-angle table) number and a commercial simulator on the same swings, while club speed and ball speed match closely. Reproduced with the reported 7-iron (104.2 mph ball, 19.1 deg launch, 5164 rpm, 83.7 mph club):The monitor filled carry with a ball-speed table plus a spin penalty that never sees the launch angle; the server then declined to overwrite it. The guard predates the simulator (added for the table path in April); when the simulator was wired inside it in June the short-circuit was born. #266 improved the model itself, but the model was never reached on exactly the shots where spin is measured, which is why merging it did not change what people see on the kiosk.
Rigs without a vertical launch angle are unchanged. The monitor's pre-fill gate was
processed.has_spin, which isSpinResult.is_reliable: confidence at or above 0.6 with medium or high quality. The server fallback already used its own literal 0.6. In the processor those two always agree (every quality demotion also caps confidence at 0.5), so the table carry those rigs see is the same number as before. The two literals are now one constant,SPIN_CONFIDENCE_RELIABLEinlaunch_monitor.py, used by bothSpinResult.is_reliableand the server fallback, with a test pinning them to each other at the boundary.Automated tests
TestBallisticCarryPrecedenceintests/test_server.py(7 cases) drives_finalize_shot_detecteddirectly: simulator replaces a pre-filled 119.2 yd with 144.2 yd; the table fallback replaces a pre-filled value when ballistics is off or no launch angle exists; the fallback fills an empty carry; the fallback's spin gate agrees withSpinResult.is_reliableon both sides of the boundary; and the carry handed to_forward_shot_to_simulatorsresolves to the same number the kiosk gets.test_create_shot_leaves_carry_to_server_finalizationintests/test_rolling_buffer.py: a clean high-confidence spin no longer pre-fills carry.tests/test_sim_resolver.py: committed carry is preferred, table is the fallback.Eight of the ten fail on
mainand pass here (checked by checking outmain'ssrc/under this branch's tests; two of the eight fail there only because the shared constant does not exist yet). The boundary test also goes red when the server floor is moved 0.05 off the constant whileSpinResultstays put, so it detects drift, not just the current value. Full suite: 1596 passed, 7 skipped. Pylint 9.58 (was 9.52). Ruff clean onsrc/openflight; the oneI001intests/test_sim_resolver.pyis on an untouched import line and exists onmain. Strict docs build clean.Manual (human) testing
No hardware run for this PR. What I verified by hand:
carry_spin_adjustedand every reader inui/src(DisplayMode.tsx,liveMetrics.ts) to confirm the kiosk and the live panel both prefer it overestimated_carry_yards, so this is the number users see._forward_shot_to_simulatorsis called inside finalization after carry is committed, so the resolver change cannot see a stale value.Someone with an IWR6843 or KLD7 rig hitting balls next to a reference unit would be the real confirmation. The log makes it visible: measured-spin shots should now print
Ballistic carry:where before they printed nothing.AI assistance
Worked with Claude Code throughout: tracing the carry paths, writing the tests, and drafting this text. The diagnosis matches the one the reporter shared in Discord. The decision to consolidate into a single writer rather than only fix the guard, the check that the two spin gates were already equal, the test cases, and the numbers are mine.
Checklist
uv run pytest tests/ -v)uv run pylint src/openflight/ --fail-under=9)uv run ruff check src/openflight/)cd ui && npm run build) - no UI files touchedcd ui && npm run lint) - no UI files touched