Skip to content

feat(rate_of_closure): course showcase — realistic heads, kinetics, putting, course scene, target optimization, launcher styling, yards units (H1-H7, #4125) - #4129

Merged
dieterolson merged 15 commits into
feat/investigation-suitefrom
feat/course-showcase
Aug 5, 2026
Merged

feat(rate_of_closure): course showcase — realistic heads, kinetics, putting, course scene, target optimization, launcher styling, yards units (H1-H7, #4125)#4129
dieterolson merged 15 commits into
feat/investigation-suitefrom
feat/course-showcase

Conversation

@dieterolson

Copy link
Copy Markdown
Collaborator

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)

H7a — golf-course scene

  • Ground renders as a course in both UIs: palette-derived grass family (rough / lighter fairway strip along the target line / distinct putting green + hole/flag at a configurable distance), tee marker at origin.
  • 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.
  • Ball/Ground checkboxes unchanged; new 'Course Elements' checkbox in the swing 3D scene, FlightView, and the web Simulation panel.

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.
  • Solver: additive 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.py facade adds hold_stats/hold_fraction + the course-layout bridge.
  • PyQt6: TargetPanel on 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.
  • Web: model/targets.ts (pinned parity), TS solver extended with the region goal + 'Optimize to Target' button, TargetSection entries + containment stats, dashed target overlay on the top-down canvas, Variation landing canvas colored by containment with the hold-% headline.
  • Tests: signed-distance inside/boundary/outside pins (both languages), optimizer reaches a reachable green from a cold start (both solvers), hand-counted 3-of-5 hold fixture.

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.
  • 'Distance' quantity: yards DEFAULT, metres selectable (canonical stays SI). Units drop-downs in both UIs; applied to flight result rows (carry/lateral/putt roll-out; apex stays metres), flight/putting view axes (tick formatters, canonical data), plotting-catalog flight/putting distance variables (incl. CSV export headers), variation output stats, and target-region entries. Conversion + default-is-yards tests on both sides.

Gates (all green)

  • pytest tests/rate_of_closure: 413 passed
  • swing_sim in-package: 309 passed
  • ruff check / format: clean (scoped to rate_of_closure + swing_sim + tests)
  • mypy (rate_of_closure + swing_sim): clean, 116 files
  • web: vitest 174 passed, tsc clean, eslint clean, vite build OK

Deviations (documented)

  • Target drag handle in the flight top-down view: typed entries are the edit seam (matplotlib drag is not cheap); the view renders the region live as entries change.
  • Web optimize-to-target reuses the TS Nelder–Mead solver (it was cheap — landing lateral already computable); no WASM/worker upgrade (still P7).
  • Carried forward from the merged branches: web 3D kinetics playback overlay and triple-pendulum kinetics deferred (H2), putting catalog stays an additive registry (H3).

DO NOT MERGE — draft for review into feat/investigation-suite.

🤖 Generated with Claude Code

dieterolson and others added 12 commits August 4, 2026 17:44
…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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 @@
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@dieterolson

Copy link
Copy Markdown
Collaborator Author

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.

@dieterolson
dieterolson merged commit 80d0fa1 into feat/investigation-suite Aug 5, 2026
5 of 14 checks passed
codex-scheduled-tasks Bot pushed a commit that referenced this pull request Aug 5, 2026
…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>
dieterolson added a commit that referenced this pull request Aug 17, 2026
…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>
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.

1 participant