feat(rate_of_closure): course showcase — realistic heads, kinetics, putting, course scene, target optimization, launcher styling, yards units (H1-H7, #4125) - #4129
Conversation
…pmeter derivation, sloped-green sim (#4125 H3) Self-façaded shared/python/swing_sim/putting/ (parent facade untouched): - impact.py: 1-D COR impulse along the lofted face normal (putter COR 0.78, typical published value) + 2/7 rolling-cap tangential transfer -> launch angle and initial backspin; pendulum backstroke proxy. - roll.py: first-principles skid closed forms (pure roll at v = omega r, (5 v0 + 2 omega0 r)/7); USGA stimpmeter geometry derived to the quoted ~1.83 m/s release speed and inverted to mu_r = v^2/(2 g S) with an exact round-trip (test-enforced). - green.py: uniform planar slope (grade % + downhill aspect), deterministic fixed-step RK4 with a SLIDING/ROLLING mode machine, break, and the geometric lip-capture bound R*sqrt(g/2r) ~= 0.82 m/s (Holmes 1991 cited for the full-chord variant). MINIMAL_PUTTERS specs are clearly marked for H1 club-library reconciliation. Concepts adopted with credit from UpstreamDrift's putting engine (stimp-as-friction parameterization, COR 0.78, explicit roll-mode machine); all derivations redone from first principles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…log, glossary/help entries (#4125 H3) - PyQt6 ui/pyqt6/putting_tab.py: library-putter picker (minimal-spec fallback), clubhead-speed or backstroke pace input, stimp/grade/ aspect/distance controls with sourced tooltips, clickable result rows with glossary-linked explanations, phase-coded top-down green + speed-vs-distance plot with the capture bound; registered in the main window with per-tab help. - putting.py bridge: H1 club-library putters -> PutterSpec; putt row explanations shared by both UIs. - plotting/putting_catalog.py: additive PuttResult-scoped variable registry (pinned SimulationRun catalog untouched). - Glossary: 5 new terms (stimp, skid, pure_roll, capture_speed, break) in glossary_entries_putting.py + TS mirror + regenerated parity fixture; FIELD_TO_TERM putt mappings. - Web: model/putting.ts mirror (same constants, same 2 ms RK4) with parity pins mirroring tests/rate_of_closure/test_putting.py value-for-value; components/PuttingPanel.tsx SVG green view (adapts UpstreamDrift PuttingGreen.tsx concepts, credited); App tab, helptext, hover-hint sweep coverage. - Tests: package physics suite (skid-roll continuity, stimp round-trip, slope mirror symmetry, capture behaviour, monotonicity, determinism), app bridge/catalog/parity pins, GUI smoke. - SPEC.md: dated H3 section + 1.12.0 changelog row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… putters, hosel-true shafts (H1, #4125) - club/head_profiles.py: per-type parametric head profiles (superellipse loft sections with per-section vertical centers at a per-type reference mass): woods keep the historical rounded-crown envelope, hybrids are an intermediate ~70% depth silhouette, irons are blade profiles (thin topline, ~22 mm depth vs ~110 mm woods, cavity-back recess via an inset tail-cap fan), wedges bias rear mass toward the sole, and putters come in two generic unbranded forms - a deep semicircular-plan mallet and an anser-style blade (shallow rectangle, lower flange back, ~9.5 mm plumber's-neck hosel set-back). ClubSpec gains a HeadStyle enum; hosel_point/face_center_point exported. - parametric_head drives off the type profile and now winds the whole solid consistently outward (body bands were inward - invisible under |n.L| shading, fatal to signed-volume integrals); wood meshes remain bit-identical, so all prior parity pins stand. - club/volumetrics.py: divergence-theorem volume + centroid with a combinatorial watertightness precondition and positive/sane-volume postconditions; validated against analytic solids (cube exact, UV sphere <1%); head_cog() reconciles the geometric COG with the spec's published-typical CG in per-type plausible bands. - Hosel-true shafts: PyQt6 Club3DView and web ClubCanvas attach the shaft line at the generated head's per-type hosel point along the lie angle; GUI test pins shaft attachment == hosel point. - 'Show CG' checkboxes (sourced divergence-theorem tooltip) in the 3D and strike views of both UIs; themed marker at the volumetric COG with spec-CG fallback for non-watertight loaded STLs. - Library grows to 16 clubs: Blade Putter (350 g, 2500 g*cm2) and Mallet Putter (360 g, 4500 g*cm2), typical published values in SI. - Glossary +7 terms (hosel, plumber's neck, bounce, mallet/blade putter, centroid, divergence theorem); TS mirror + fixture regenerated. - Web parity: clubHeads.ts + volumetrics.ts with volume/COG/hosel parity pins vs pytest; CG toggles on ClubCanvas/StrikeCanvas; per-type strike-view face extents. New tests: test_club_heads.py, heads.test.ts, volumetrics.test.ts, cgToggle.test.tsx, GUI smokes. - SPEC.md dated section + changelog row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…plots and 3D overlays (#4125 H2) Kinetics core (simulation/kinetics.py): per-sample inverse dynamics over the double-pendulum swing using the swing_sim EOM surfaces (mass_matrix / coriolis_vector / gravity_vector / damping_vector) — KineticsSeries with the net/gravity/damping/applied torque breakdown per joint, joint powers (tau*omega, sums to dKE/dt), Newton-Euler joint reaction forces in the app frame, a point-mass clubhead-force estimate, and ball-aligned joint geometry. Documented sign convention (positive torque counter-clockwise about the swing-plane normal). simulate_forced backs the tests: inverse-dynamics round trip recovers a known torque profile to O(dt^2), applied power integrates to dE (undamped forced), net joint power integrates to dKE (passive), static-hang force pin. New public DoublePendulumSwing.state_at. Presentation mirrors the movement optimizer (plot_renderer.py / vector_overlay.py / swingset_forces.py, credited): 'Time (s)' / 'Torque (N·m)' / 'Power (W)' / 'Force (N)' labels, unit-suffixed fields, zero lines, dashed power total, chart-cycle joint colors, 270-degree torque arcs (sign = sweep direction), capped force arrows. Catalog: new 'Kinetics' series category (11 keys) in the custom wizard of both UIs (NaN for sources without joint states); built-ins Joint Torques / Joint Power / Reaction Forces; parity fixture regenerated (51 keys). PyQt6: 'Show Kinetics' overlay checkbox in the swing viewer (kinetics_overlay.py geometry, magnitude-carrying legend); Kinetics sub-tab (kinetics_panel.py) with the three plots, a downswing-timed peak table, and glossary-linked explanations. New glossary terms: inverse_dynamics, joint_reaction_force, moment_of_force, power. Web: model/kinetics.ts inverse-dynamics mirror parity-pinned tightly against the pytest-generated kinetics_parity.json; Kinetics view in the Simulation panel (three canvas charts + peak table); catalog and glossary mirrors. Deferred (documented in SPEC.md): web playback overlay (P7 WASM) and triple-pendulum kinetics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflicts resolved: SPEC.md (kept both dated sections), glossary TS mirrors (union of H1+H2 entries, 71 terms), glossary fixture regenerated from the merged Python source, SimulationPanel.tsx (Kinetics view added on top of H1's clubSpec-aware strike view).
Conflicts resolved: SPEC.md (all dated sections kept; H1/H2/H3 changelog rows consolidated into a single 1.12.0 row), glossary FIELD_TO_TERM union in Python and TS (76 terms), fixture regenerated from the merged Python source, App.tsx tab wiring union (Putting tab inserted before Glossary on top of H1/H2 wiring).
…r library H1 replaced the generic 'Putter' library entry with 'Blade Putter' and 'Mallet Putter'; the putting bridge already prefers library putters by name, so only the H3 tests' keys were stale. Full gates green on the merged tree (pytest 373, swing_sim 299, ruff, mypy, web vitest 161 + tsc + eslint + build). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ay strip, green + flag, tee marker (#4125 H7a) Both UIs render the simulation/flight displays as a course. New ui/course.py derives every scene tone from the shared chart palette (blend of the palette green toward black/white — no widget hex) with a configurable CourseLayout (green distance/radius, fairway half-width); ui/pyqt6/course_scene.py paints the 3D ground plane, side-profile and top-down panels. Swing 3D scene and FlightView gain a 'Course Elements' checkbox (ball/ground toggles unchanged; course styling is the ground's new look) with sourced guidance; set_course_layout is the H7b target seam. Web mirror: model/theme.ts (shared chartColors + blend/withAlpha), model/course.ts (same fractions, parity-tested), course-styled swingSceneDraw + FlightCanvases, Course Elements checkbox in the Simulation panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… with variation hold-% (#4125 H7b) swing_sim: solver/targets.py TargetRegion (green circle at distance with lateral offset / fairway corridor: distance band x half-width), exact signed distance + containment; ImpactGoal gains an additive target_region (+weight) — the objective appends one residual (distance outside the region, 0 inside, small centering pull, carry-scaled) and solve() reports achieved landing_lateral_m / target_distance_m and a target_region_m per-goal entry. Tests: inside/boundary/outside pins for both kinds, residual composition, region-only goal validity, and the optimizer landing a reachable green from a cold start. App: simulation/targets.py facade (hold_stats/hold_fraction — the Monte-Carlo headline — and the H7a course-layout bridge). PyQt6: new TargetPanel embedded in the Solver panel ('Optimize to Target' reusing the partition/progress/cancel machinery; solver rows split into solver_rows.py for the 500-LOC budget), FlightView top-down target boundary + Variation landing-scatter overlay reporting shots holding the target, SimulationTab/main_window wiring (VariationTab studyCompleted signal). Tests: hand-counted hold fixture, GUI panel / wiring / overlay tests. Web: model/targets.ts parity mirror (pinned vs Python), TS solver extended with the region goal (Optimize to Target button), Flight-view TargetSection entries + containment readout, top-down canvas target overlay, Variation landing canvas hold-% headline with containment coloring; target state lifted to App. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ance quantity (#4125 H6) (a) Showcase styling: new ui/pyqt6/app_style.py applies the UpstreamDrift launcher's visual language (studied from the launcher ToolCard/launchButton QSS in shared theme stylesheets.py) across the PyQt6 app — buttons with hover/pressed states and a subtle bottom-edge shadow, rounded launcher-card group boxes, hover- highlighted tabs — every color derived from the live QPalette (palette() roles + low-alpha rgba tints; a test asserts no hex in the sheet and that borders/backgrounds are palette/rgba only). Supersedes the bare selection_stylesheet at the window level while carrying the V4 selected-row rules forward. Web: KineticsSection/ClubCanvas accent hexes aligned onto the shared model/theme.ts chart palette. (b) Distance quantity: units.py gains DISTANCE_UNITS (yd first = default, m selectable; canonical stays SI metres), a 'distance' entry in QUANTITY_UNITS (Units drop-down grows a Distance row in both UIs), a session display-unit seam (display_distance_unit / set_display_distance_unit / format_distance_m) and the web mirror in units.ts (formatDistanceM; App units state defaults distance: 'yd'). Applied to: flight/simulation result rows (carry, lateral; apex stays metres), FlightView + putting-green axes (tick formatter — data stays canonical), the plotting catalog's flight/putting distance variables (new DISTANCE_KEYS; render pipeline converts values + labels, CSV headers follow), variation output stats (carry/lateral rows), and the H7b target-region entries (both UIs, canonical round-trip). Conversion + default-is-yards tests on both sides; pinned tests updated to the yards default (247.5 m carry now reads 270.7 yd). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… changelog row Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 41cc5ea143
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @@ -0,0 +1,503 @@ | |||
| /** | |||
There was a problem hiding this comment.
Split the new web putting panel below 400 lines
This newly added component is 503 lines, so it exceeds the repository's AGENTS.md ceiling for monolithic files; because it bundles controls, result rows, SVG green rendering, and speed plotting in one file, it will be flagged by the repo's maintainability rules and should be split by responsibility before landing.
AGENTS.md reference: AGENTS.md:L142-L145
Useful? React with 👍 / 👎.
| @@ -0,0 +1,495 @@ | |||
| """Swing kinetics: inverse dynamics over the pendulum swing (#4125 H2). | |||
There was a problem hiding this comment.
Split the new kinetics module below 400 lines
This new module is 495 lines, above the AGENTS.md 400-line maximum; it combines EOM terms, inverse dynamics, forced simulation, reaction-force math, run adaptation, and caching, so it violates the repo-specific file-size invariant and should be decomposed into focused modules.
AGENTS.md reference: AGENTS.md:L142-L145
Useful? React with 👍 / 👎.
| @@ -0,0 +1,429 @@ | |||
| """Putting tab — putter, stroke, green, roll-out (#4125 H3). | |||
There was a problem hiding this comment.
Split the new PyQt putting tab below 400 lines
This new PyQt tab is 429 lines, exceeding the AGENTS.md file-size limit; the widget currently contains control construction, result formatting, explanation wiring, and two plot renderers in one module, so it should be split before it fails the repository's maintainability assessment.
AGENTS.md reference: AGENTS.md:L142-L145
Useful? React with 👍 / 👎.
| }); | ||
|
|
||
| describe("optimize to target (TS solver reuse)", () => { | ||
| it("reaches a reachable green from a cold start", () => { |
There was a problem hiding this comment.
Keep the target solver test under Vitest's timeout
This new test drives solveGoals(..., 400, target), and under the package's npm test script it exceeded Vitest's default 5s per-test limit; running this file alone with an increased timeout passed but took about 5.17s in the test body, so the default suite fails before the assertions complete. Reduce the evaluation count, optimize the target solve, or give this specific long-running test an explicit timeout.
Useful? React with 👍 / 👎.
|
Hardening update pushed in e889708:\n\n- adds visible clickable reference-frame guidance and whole-row launch explanations\n- fixes signed web numeric editing, including negative Spin Axis Tilt and select-all focus behavior\n- auto-populates both Swing views and renders complete double/triple pendulum skeletons\n- resets source-specific impact time on source changes (fixes native Triple Pendulum failure)\n- loads a representative Driver 10.5 deg + engineering CG marker by default\n- upgrades deterministic watertight clubheads to 1,792-triangle driver geometry with matched Python/TypeScript shading and volumetrics\n- splits the production web bundle and hardens computational test timeouts\n\nLocal verification: Python 416/416; Vitest 187/187; ESLint zero warnings; TypeScript clean; Vite production build clean; Ruff/format/diff hygiene clean; all touched source files <=500 LOC. Playwright verified -12.5 deg persists and runs; native screen QA verified the four-joint Triple Pendulum skeleton at auto impact time. |
…ewers, variation engine, glossary & help system (V1-V4, #4120) (#4124) * feat(rate_of_closure): investigative plotting suite — advanced plots, custom plot wizard, exports (#4120 V1) Phase V1 of the Investigation & Variation Suite epic (#4120), stacked on the consolidated platform branch (PR #4119). Python (src/rate_of_closure/plotting/): - catalog.py: DbC-validated registry of all 40 plottable SimulationRun variables (key, label, unit, category, extractor, axis-scale hint); key list pinned by contract test. - spec.py: frozen PlotSpec (x/y keys, optional series key, kind line|scatter|sweep|histogram, title, log flags, sweep range) with JSON round-trip under the shared rate_of_closure.plot_spec/1 schema. - render.py: one compute/render pipeline — sweep kind re-runs the full swing→impact→flight simulation per grid point; themed matplotlib rendering via the shared get_chart_color palette; CSV/JSON exports of exactly the plotted data. - builtins.py: built-in advanced plots as PlotSpec factories — migrated closure sweep, delivery-vs-τ sweep, launch-vs-toe/high offset maps, swing time series, side and top-down flight profiles. PyQt6: new Plots tab replaces/absorbs the Closure Sweep tab — managed plot list (add builtin / duplicate / remove), 3-step Custom Plot wizard (scope → catalog-grouped X/Y (+ sweep range) → style with live preview), navigation toolbar, PNG/SVG/CSV/JSON + save/load definition exports, tooltips on every control; renders against the latest Simulation-tab run (lazy manual-source run otherwise), deferred while hidden. Web parity (practical): plotcatalog.ts mirrors the catalog key-for-key (pinned against the pytest-exported plotcatalog.fixture.json; TS-port gaps marked unsupported), plotspec.ts ports schema + pipeline, and the Plots tab offers builtin picker, simplified custom builder, canvas rendering with axis labels/units, PNG/CSV/JSON downloads, and plot-definition import/export interoperable with the desktop app. Tests: test_plotting.py (catalog pins + shapes, spec round-trip, headless Agg rendering of every builtin, closure-sweep numbers matching model.sweep(), export well-formedness), test_plots_gui.py (tab replacement, list management, wizard completion, exports, tooltip coverage), plotcatalog.test.ts + plotspec.test.ts (parity pins, vitest round-trip). SPEC.md dated section + changelog row. Documented deviation: the swing time series plots clubhead speed and angular speed rather than pendulum joint θ/ω — SimulationRun stores clubhead poses/twists, not joint states. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure): scale-separated viewers, standalone flight explorer, small-window fixes (V2, #4120) Three scale-separated viewers as Strike/Swing/Flight sub-tabs in the Simulation tab's display area, each with its own display-parameter checklist and sourced hover guidance: - ui/pyqt6/strike_view.py: face-scale impact-zone view (superellipse face outline sized from the club mass envelope, bulge/roll sagitta contours, impact marker + strike-history scatter, path/face/AoA delivery vectors in the face plane, club info; extents hard-capped at STRIKE_MAX_EXTENT_MM = +/-120 mm, never flight scale). - simulation_view.py scoped to swing scale: flight polyline removed from the default display; new 'Show Ball Flight' checkbox (default OFF) opts into the flight envelope with guidance warning that it dwarfs the ~3 m swing. - ui/pyqt6/flight_view.py: dedicated flight-scale viewer (side profile + top-down 2D panels + 3D polyline, landing/apex annotated), reusable with a bare trajectory. Standalone Ball-Flight Explorer (new top-level tab, no swing needed): pure logic in simulation/flight_explorer.py (direct launch entry with app sign conventions, or delivery entry through swing_sim.impact + rigid-body solve; any of the 7 literature flight models), presentation in ui/pyqt6/flight_explorer_tab.py (unit drop-down, model picker, result rows incl. new lateral_m explanation, flight viewer). Small-window layout defect fixed: window minimum lowered to 1024x700, control columns scroll, typed entries carry >=84 px minimum widths, result-row labels tooltip full text; test_layout_minsize.py walks every nested tab at 1024x700 headlessly asserting >=64 px entries and no zero-height visible widgets. Web practical parity: Strike/Swing/Flight segmented views (strike + flight profile canvases, swing scene extracted to swingSceneDraw.ts), separated Show-Ball-Flight toggle, standalone Flight Explorer panel parity-banded against the pytest pinned case (167 mph / 10.9 deg / 2686 rpm -> ~247.5 m carry, Waterloo/Penner), responsive min-widths with title-attribute truncation. Tests: viewer scale invariants, flight-explorer end-to-end pins in both modes, sign-convention checks, all-7-model runs, TS parity pins, GUI smoke for every new tab, sourced-tooltip enforcement. SPEC.md dated section + changelog row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(swing_sim): shared variation/Monte Carlo engine + Variation tab (V3, #4120) Shared 'how parameters vary' engine in swing_sim/variation: namespaced variable registry (delivery / swing / club / launch categories, register_variable extension seam), frozen NoiseSpec + VariationPlan with lossless JSON schema, seeded parallel N-run executor (per-variable subset-stable numpy streams, solver-shaped ProgressReport/cancel_event, NaN-recorded per-run failures, worker-count-invariant results), dispersion + one-at-a-time sensitivity + Spearman + 2-sigma landing ellipse analysis, and documented CSV/JSON dataset IO with import back. PyQt6: new top-level Variation tab in the Rate of Closure explorer (registry-driven noise rows with sourced unit-aware tooltips, QThread worker with progress/cancel, summary/sensitivity/Spearman tables, themed matplotlib landing scatter with the 2-sigma ellipse, dataset export and plan save/load). Web mirror: variation engine over the existing TS physics (mulberry32 + Box-Muller, FNV-1a per-variable streams, delivery + launch modes, bounded <=500 runs pending the WASM/worker upgrade), same plan JSON schema as the desktop tab, Variation tab with summary + sensitivity heat tables, canvas landing scatter + ellipse, CSV/JSON downloads. Cross-implementation parity pinned statistically via a Python-generated fixture (exact RNG parity deliberately not attempted; documented). Prior art surveyed and credited in module docstrings: UpstreamDrift ball_enhanced_simulator Monte Carlo, perturbation package, pendulum_simulator perturbation_analysis, movement_optimizer parallel/progress/cancel machinery (reused via swing_sim.solver). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure): rename Derivation & Traceability tab to Calculation Description (#4120 V4) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure): selected-row clarity + full app glossary in both UIs (#4120 V4) - ResultRow persistent selected state (dynamic property + palette-derived stylesheet, no hard-coded colors); one selection across all row groups in the main window, Simulation tab, and Flight Explorer; explanation panels lead with the selected row's name as a prominent header. - New DbC glossary (60 sourced terms) with searchable PyQt6 Glossary tab and web Glossary section; explanation panels link to the glossary and pre-select the mapped term (FIELD_TO_TERM covers every explanation field). Web mirror pinned key-for-key by a Python-generated fixture checked from both test suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure): full-model derivations in the Calculation Description tab (#4120 V4) Sectioned coverage via new derivation_models.py (DerivationConfig + DerivationSection) over per-domain content modules (derivation_impact / derivation_flight / derivation_swing, honoring the 500-LOC budget): closure chain (kept), impact model (impulse-momentum with COR, MOI-tensor triple-product effective mass, 2/7 friction cap, D-plane, gear-effect recoil — sourced from swing_sim.impact docstrings), ball flight (EOM + the ACTIVE model's coefficient law with its registry citation + spin decay), and swing models (double-pendulum Lagrangian mass matrix / Coriolis / plane-tilt gravity projection with live tilt substitution, triple-pendulum step conditional). Sections render conditionally per the live configuration; SimulationTab emits configChanged and the DerivationView re-renders. Web mirror derivationModels.ts + sectioned Derivation.tsx; section keys, toggling, and the plane-gravity mirror parity-tested; every formula parses as mathtext (pytest) and strict KaTeX (vitest). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure): per-tab help system in both UIs (#4120 V4) PyQt6: helptext.py (DbC-validated cold-user help — what each tab does, workflow, control reference, tips) + a '?' corner button on the tab bar opening a rich-text help panel for the current tab. Web: helptext.ts mirror + a collapsible 'How to Use This Page' section at the top of every tab (web users arrive with zero context). Contract tests on both sides assert every tab has substantive help (>300 chars) with workflow coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure): hover-hint completeness sweep, test-enforced (#4120 V4) PyQt6: headless walk over every (nested) tab asserting an effective tooltip on all interactive widgets (buttons/combos/sliders/spins/line edits, Qt chrome excluded); gaps fixed (3D view Pause button, preset combo). Web: vitest render-and-assert title/aria-label on interactive elements of every panel (canvas 2D context stubbed); titles added to tab nav, result rows (their explanations), unit selects, run/export/ playback/solver controls across App, SimulationPanel, SolverPanel, FlightExplorerPanel, and ClubCanvas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(spec): dated V4 section + 1.11.0 changelog row (#4120) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(rate_of_closure): split glossary data modules to honor the 500-LOC budget glossary.py / glossary.ts keep the public surface (GLOSSARY, FIELD_TO_TERM, search) and merge the split entry-data modules (glossary_entries_a_l / _l_z, glossaryEntriesAL/LZ.ts, shared type modules). No behavior change; all glossary tests and parity pins pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure): course showcase — realistic heads, kinetics, putting, course scene, target optimization, launcher styling, yards units (H1-H7, #4125) (#4129) * feat(swing_sim): putting package — putter impact, skid/roll with stimpmeter derivation, sloped-green sim (#4125 H3) Self-façaded shared/python/swing_sim/putting/ (parent facade untouched): - impact.py: 1-D COR impulse along the lofted face normal (putter COR 0.78, typical published value) + 2/7 rolling-cap tangential transfer -> launch angle and initial backspin; pendulum backstroke proxy. - roll.py: first-principles skid closed forms (pure roll at v = omega r, (5 v0 + 2 omega0 r)/7); USGA stimpmeter geometry derived to the quoted ~1.83 m/s release speed and inverted to mu_r = v^2/(2 g S) with an exact round-trip (test-enforced). - green.py: uniform planar slope (grade % + downhill aspect), deterministic fixed-step RK4 with a SLIDING/ROLLING mode machine, break, and the geometric lip-capture bound R*sqrt(g/2r) ~= 0.82 m/s (Holmes 1991 cited for the full-chord variant). MINIMAL_PUTTERS specs are clearly marked for H1 club-library reconciliation. Concepts adopted with credit from UpstreamDrift's putting engine (stimp-as-friction parameterization, COR 0.78, explicit roll-mode machine); all derivations redone from first principles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure): Putting tab in both UIs, additive putting catalog, glossary/help entries (#4125 H3) - PyQt6 ui/pyqt6/putting_tab.py: library-putter picker (minimal-spec fallback), clubhead-speed or backstroke pace input, stimp/grade/ aspect/distance controls with sourced tooltips, clickable result rows with glossary-linked explanations, phase-coded top-down green + speed-vs-distance plot with the capture bound; registered in the main window with per-tab help. - putting.py bridge: H1 club-library putters -> PutterSpec; putt row explanations shared by both UIs. - plotting/putting_catalog.py: additive PuttResult-scoped variable registry (pinned SimulationRun catalog untouched). - Glossary: 5 new terms (stimp, skid, pure_roll, capture_speed, break) in glossary_entries_putting.py + TS mirror + regenerated parity fixture; FIELD_TO_TERM putt mappings. - Web: model/putting.ts mirror (same constants, same 2 ms RK4) with parity pins mirroring tests/rate_of_closure/test_putting.py value-for-value; components/PuttingPanel.tsx SVG green view (adapts UpstreamDrift PuttingGreen.tsx concepts, credited); App tab, helptext, hover-hint sweep coverage. - Tests: package physics suite (skid-roll continuity, stimp round-trip, slope mirror symmetry, capture behaviour, monotonicity, determinism), app bridge/catalog/parity pins, GUI smoke. - SPEC.md: dated H3 section + 1.12.0 changelog row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure): realistic type-specific heads, volumetric COG, putters, hosel-true shafts (H1, #4125) - club/head_profiles.py: per-type parametric head profiles (superellipse loft sections with per-section vertical centers at a per-type reference mass): woods keep the historical rounded-crown envelope, hybrids are an intermediate ~70% depth silhouette, irons are blade profiles (thin topline, ~22 mm depth vs ~110 mm woods, cavity-back recess via an inset tail-cap fan), wedges bias rear mass toward the sole, and putters come in two generic unbranded forms - a deep semicircular-plan mallet and an anser-style blade (shallow rectangle, lower flange back, ~9.5 mm plumber's-neck hosel set-back). ClubSpec gains a HeadStyle enum; hosel_point/face_center_point exported. - parametric_head drives off the type profile and now winds the whole solid consistently outward (body bands were inward - invisible under |n.L| shading, fatal to signed-volume integrals); wood meshes remain bit-identical, so all prior parity pins stand. - club/volumetrics.py: divergence-theorem volume + centroid with a combinatorial watertightness precondition and positive/sane-volume postconditions; validated against analytic solids (cube exact, UV sphere <1%); head_cog() reconciles the geometric COG with the spec's published-typical CG in per-type plausible bands. - Hosel-true shafts: PyQt6 Club3DView and web ClubCanvas attach the shaft line at the generated head's per-type hosel point along the lie angle; GUI test pins shaft attachment == hosel point. - 'Show CG' checkboxes (sourced divergence-theorem tooltip) in the 3D and strike views of both UIs; themed marker at the volumetric COG with spec-CG fallback for non-watertight loaded STLs. - Library grows to 16 clubs: Blade Putter (350 g, 2500 g*cm2) and Mallet Putter (360 g, 4500 g*cm2), typical published values in SI. - Glossary +7 terms (hosel, plumber's neck, bounce, mallet/blade putter, centroid, divergence theorem); TS mirror + fixture regenerated. - Web parity: clubHeads.ts + volumetrics.ts with volume/COG/hosel parity pins vs pytest; CG toggles on ClubCanvas/StrikeCanvas; per-type strike-view face extents. New tests: test_club_heads.py, heads.test.ts, volumetrics.test.ts, cgToggle.test.tsx, GUI smokes. - SPEC.md dated section + changelog row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure): swing kinetics — torques, forces, powers with plots and 3D overlays (#4125 H2) Kinetics core (simulation/kinetics.py): per-sample inverse dynamics over the double-pendulum swing using the swing_sim EOM surfaces (mass_matrix / coriolis_vector / gravity_vector / damping_vector) — KineticsSeries with the net/gravity/damping/applied torque breakdown per joint, joint powers (tau*omega, sums to dKE/dt), Newton-Euler joint reaction forces in the app frame, a point-mass clubhead-force estimate, and ball-aligned joint geometry. Documented sign convention (positive torque counter-clockwise about the swing-plane normal). simulate_forced backs the tests: inverse-dynamics round trip recovers a known torque profile to O(dt^2), applied power integrates to dE (undamped forced), net joint power integrates to dKE (passive), static-hang force pin. New public DoublePendulumSwing.state_at. Presentation mirrors the movement optimizer (plot_renderer.py / vector_overlay.py / swingset_forces.py, credited): 'Time (s)' / 'Torque (N·m)' / 'Power (W)' / 'Force (N)' labels, unit-suffixed fields, zero lines, dashed power total, chart-cycle joint colors, 270-degree torque arcs (sign = sweep direction), capped force arrows. Catalog: new 'Kinetics' series category (11 keys) in the custom wizard of both UIs (NaN for sources without joint states); built-ins Joint Torques / Joint Power / Reaction Forces; parity fixture regenerated (51 keys). PyQt6: 'Show Kinetics' overlay checkbox in the swing viewer (kinetics_overlay.py geometry, magnitude-carrying legend); Kinetics sub-tab (kinetics_panel.py) with the three plots, a downswing-timed peak table, and glossary-linked explanations. New glossary terms: inverse_dynamics, joint_reaction_force, moment_of_force, power. Web: model/kinetics.ts inverse-dynamics mirror parity-pinned tightly against the pytest-generated kinetics_parity.json; Kinetics view in the Simulation panel (three canvas charts + peak table); catalog and glossary mirrors. Deferred (documented in SPEC.md): web playback overlay (P7 WASM) and triple-pendulum kinetics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(rate_of_closure): reconcile H3 putting bridge tests with H1 putter library H1 replaced the generic 'Putter' library entry with 'Blade Putter' and 'Mallet Putter'; the putting bridge already prefers library putters by name, so only the H3 tests' keys were stale. Full gates green on the merged tree (pytest 373, swing_sim 299, ruff, mypy, web vitest 161 + tsc + eslint + build). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure): golf-course scene — themed grass ground, fairway strip, green + flag, tee marker (#4125 H7a) Both UIs render the simulation/flight displays as a course. New ui/course.py derives every scene tone from the shared chart palette (blend of the palette green toward black/white — no widget hex) with a configurable CourseLayout (green distance/radius, fairway half-width); ui/pyqt6/course_scene.py paints the 3D ground plane, side-profile and top-down panels. Swing 3D scene and FlightView gain a 'Course Elements' checkbox (ball/ground toggles unchanged; course styling is the ground's new look) with sourced guidance; set_course_layout is the H7b target seam. Web mirror: model/theme.ts (shared chartColors + blend/withAlpha), model/course.ts (same fractions, parity-tested), course-styled swingSceneDraw + FlightCanvases, Course Elements checkbox in the Simulation panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure, swing_sim): target regions + optimize-to-target with variation hold-% (#4125 H7b) swing_sim: solver/targets.py TargetRegion (green circle at distance with lateral offset / fairway corridor: distance band x half-width), exact signed distance + containment; ImpactGoal gains an additive target_region (+weight) — the objective appends one residual (distance outside the region, 0 inside, small centering pull, carry-scaled) and solve() reports achieved landing_lateral_m / target_distance_m and a target_region_m per-goal entry. Tests: inside/boundary/outside pins for both kinds, residual composition, region-only goal validity, and the optimizer landing a reachable green from a cold start. App: simulation/targets.py facade (hold_stats/hold_fraction — the Monte-Carlo headline — and the H7a course-layout bridge). PyQt6: new TargetPanel embedded in the Solver panel ('Optimize to Target' reusing the partition/progress/cancel machinery; solver rows split into solver_rows.py for the 500-LOC budget), FlightView top-down target boundary + Variation landing-scatter overlay reporting shots holding the target, SimulationTab/main_window wiring (VariationTab studyCompleted signal). Tests: hand-counted hold fixture, GUI panel / wiring / overlay tests. Web: model/targets.ts parity mirror (pinned vs Python), TS solver extended with the region goal (Optimize to Target button), Flight-view TargetSection entries + containment readout, top-down canvas target overlay, Variation landing canvas hold-% headline with containment coloring; target state lifted to App. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rate_of_closure): launcher-language styling + yards-default Distance quantity (#4125 H6) (a) Showcase styling: new ui/pyqt6/app_style.py applies the UpstreamDrift launcher's visual language (studied from the launcher ToolCard/launchButton QSS in shared theme stylesheets.py) across the PyQt6 app — buttons with hover/pressed states and a subtle bottom-edge shadow, rounded launcher-card group boxes, hover- highlighted tabs — every color derived from the live QPalette (palette() roles + low-alpha rgba tints; a test asserts no hex in the sheet and that borders/backgrounds are palette/rgba only). Supersedes the bare selection_stylesheet at the window level while carrying the V4 selected-row rules forward. Web: KineticsSection/ClubCanvas accent hexes aligned onto the shared model/theme.ts chart palette. (b) Distance quantity: units.py gains DISTANCE_UNITS (yd first = default, m selectable; canonical stays SI metres), a 'distance' entry in QUANTITY_UNITS (Units drop-down grows a Distance row in both UIs), a session display-unit seam (display_distance_unit / set_display_distance_unit / format_distance_m) and the web mirror in units.ts (formatDistanceM; App units state defaults distance: 'yd'). Applied to: flight/simulation result rows (carry, lateral; apex stays metres), FlightView + putting-green axes (tick formatter — data stays canonical), the plotting catalog's flight/putting distance variables (new DISTANCE_KEYS; render pipeline converts values + labels, CSV headers follow), variation output stats (carry/lateral rows), and the H7b target-region entries (both UIs, canonical round-trip). Conversion + default-is-yards tests on both sides; pinned tests updated to the yards default (247.5 m carry now reads 270.7 yd). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(spec): consolidated #4125 course-showcase dated section + 1.13.0 changelog row Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(rate_of_closure): honor PyQt module budgets * feat(rate_of_closure): harden engineering interactions --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(rate-of-closure): build the modern prescribed-input and variation workbench (#4141) * fix(signal-toolkit): preserve torque polynomial coefficient order * fix(rate-of-closure): use selected club in web impact physics * feat(swing-sim): add persistent prescribed torque profiles * feat(rate-of-closure): add typed fixed-ball impact outcomes * feat(rate-of-closure): add web torque profile contracts * feat(rate-of-closure): handle web impact misses explicitly * feat(swing-sim): execute and export prescribed torques * feat(rate-of-closure): run prescribed torques from PyQt * feat(rate-of-closure): export reusable torque histories * feat(rate-of-closure): present fixed-ball misses in PyQt * feat(swing-sim): add truthful double-pendulum joint locks * feat(rate-of-closure): run and persist web torque profiles * fix(rate-of-closure): bound torque preview precision * fix(rate-of-closure): keep standalone launch responsive * feat(rate-of-closure): add constrained and counterfactual workflows * feat(variation): add ensemble geometry and quiet-zone analysis * feat(variation): add grouped plans and miss-safe swing ensembles * fix(rate-of-closure): restore Python 3.10 enum compatibility * feat(rate-of-closure): add physical setup and variation workflows * fix(rate-of-closure): lint Python 3.10 string enums --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ate (#4514) Both documents had drifted far enough to actively misdirect the next agent, and the root document had also outgrown its own policy by 18x. CLAUDE.md requires handoff docs to be current-state only and at most 150 lines, with history in git. The root doc had instead accumulated 137 dated entries across 2,708 lines. Those entries are moved verbatim to docs/agent_handoff_archive/2026-08_tools_root_handoff_log.md, not deleted — the archived body is byte-identical to the original (165,356 chars, 137 entries), so no working-tree context is lost. The live doc is now 132 lines and carries an explicit do-not against appending dated entries again. Content corrections. The root doc presented PR #4119 as open with auto-merge armed and #4124/#4129 as open drafts stacked on it; in fact #4119 closed without merging and #4124/#4129 both merged. Epics #4142 and #4433 were absent entirely. Delivery has shifted from stacked PRs to scoped consolidations rebuilt onto current main. It now records the seven live epics, the four open consolidations (#4446, #4447, #4449, #4466) as the real queue, and the fact that 39 of 58 open PRs are codex/4142-* / codex/4433-* drafts superseded by merged #4473 — whose own description says so. Those should be triaged and closed, not rebased. The pendulum doc described issue #4406 as active on branch research/shoulder-velocity-drift-transfer. #4406 is closed and shipped via consolidation #4450. It now records what landed, keeps the fail-closed triple/golfer tier boundary as an explicit do-not, and points at the UpstreamDrift #8684 qualification state — including the finite-ground result where the preregistered screen admitted 0 of 384 cells, so a moving base is not a free upgrade for the higher tiers. Also recorded: the four pre-existing ruff format failures under src/data_processing/ that are on main and should not be absorbed into an unrelated PR, and the --regenerate-api-baseline trap. Co-authored-by: codex-scheduled <codex-scheduled@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Final integration PR for epic #4125 (H1–H7). Supersedes #4126, #4127, and #4128 — those branches are merged here (in order: realistic-heads → swing-kinetics → putting-vertical) with cross-branch conflicts resolved, then the H7 course scene / target optimization and the H6 launcher styling + yards units land on top so everything covers the new surfaces uniformly.
Integration (H1–H3)
feat/realistic-heads(feat(rate_of_closure): realistic type-specific heads, volumetric COG, putters, hosel-true shafts (H1, #4125) #4127),feat/swing-kinetics(feat(rate_of_closure): swing kinetics — torques, forces, powers with plots and 3D overlays (H2, #4125) #4128),feat/putting-vertical(feat(swing_sim,rate_of_closure): putting vertical — impact, skid/roll model, green sim, Putting tab (H3, #4125) #4126).H7a — golf-course scene
ui/course.py+ui/pyqt6/course_scene.py(desktop),model/theme.ts+model/course.ts(web, same blend fractions, parity-tested). No hard-coded widget hex — all tones blend the shared chartColors palette.H7b — target optimization
swing_sim/solver/targets.py:TargetRegion(green circle at distance + radius + lateral offset; fairway corridor = distance band × half-width), exact signed distance, containment.ImpactGoal.target_region— one extra residual (distance outside region, 0 inside, small centering term, carry-scaled);solve()reports landing point + signed distance.rate_of_closure/simulation/targets.pyfacade addshold_stats/hold_fraction+ the course-layout bridge.TargetPanelon the Solver panel ('Optimize to Target' reuses partition/progress/cancel; entries are the place/edit seam — the flight top-down view renders the region live and the course green moves to the target). Variation tie-in: completed studies overlay their landing scatter on the flight top-down view with the "N/M shots hold the target (x%)" headline.model/targets.ts(pinned parity), TS solver extended with the region goal + 'Optimize to Target' button,TargetSectionentries + containment stats, dashed target overlay on the top-down canvas, Variation landing canvas colored by containment with the hold-% headline.H6 — showcase styling + yards
ui/pyqt6/app_style.py: UpstreamDrift launcher visual language app-wide (button hover/pressed + subtle bottom-edge shadow, rounded launcher-card group boxes, hover/selected tabs) — palette-derived only (test-enforced: no hex, palette()/rgba only). Web accents in KineticsSection/ClubCanvas aligned onto the shared palette module.Gates (all green)
pytest tests/rate_of_closure: 413 passedDeviations (documented)
DO NOT MERGE — draft for review into
feat/investigation-suite.🤖 Generated with Claude Code