Skip to content

Ek/make kiss 3d linear cushion - #373

Open
ekiefl wants to merge 10 commits into
mainfrom
ek/make-kiss-3d-linear-cushion
Open

Ek/make kiss 3d linear cushion#373
ekiefl wants to merge 10 commits into
mainfrom
ek/make-kiss-3d-linear-cushion

Conversation

@ekiefl

@ekiefl ekiefl commented Aug 2, 2026

Copy link
Copy Markdown
Owner

What the algorithm does

The job: the detector hands make_kiss a ball that's slightly wrong — overlapping the cushion by a hair, or
floating a hair away — because floating-point. Before the resolver computes the bounce, the ball needs to
be exactly barely-touching.

The old 2D way

treat the cushion as a flat vertical wall, shove the ball perpendicular to it.

The new 3D way

Five steps:

  1. Model the cushion as a pipe. The nose is a cylinder of radius nose_radius running along the p1→p2 line.
    The ball is just touching when its center sits at distance R + nose_radius from the pipe's centerline.
  2. Slide the ball along its own velocity, not perpendicular. Ask: "at what time t — forward or backward —
    along its straight-line path is the ball at exactly the right distance from the centerline?" That's a
    quadratic in t. Take the root closest to zero (smallest correction) and move to r + t*v. This beats shoving
    it sideways because you're following the path the ball actually took, which is more accurate and also reduces
    likelihood of intersecting with an object as a result of the nudge.
  3. Only the perpendicular plane matters. Strip out the components of position and velocity that point
    along the cushion axis, since sliding along a pipe doesn't change your distance from it. That's what makes
    it a clean quadratic instead of something ugly.
  4. Then fix the height. Step 2 can leave the ball at a physically wrong height — sunk into the table, or
    hovering above it. _constrain_to_table spins the ball around the pipe's axis, like a bead on a wire, until
    the height is right: exactly z = R if it's on the table, at least z = R if airborne. Because it's a
    rotation about the axis, the distance to the pipe never changes — the spacing from step 2 survives intact.
  5. Fallback. If the ball isn't moving, or the correction would be absurd (grazing shot), skip the velocity
    trick: push straight out perpendicular from the centerline, then apply the same height fix.

The clever part is step 4. Two constraints that would normally fight — distance-from-cushion and
height-above-table — and rotating about the cushion axis satisfies the second for free without disturbing
the first.


This doesn't handle circular cushion.

Summary by CodeRabbit

  • Bug Fixes

    • Improved ball-to-cushion contact resolution for more accurate positioning and motion.
    • Better handles angled, airborne, stationary, and edge-case collisions.
    • Preserves airborne behavior while ensuring balls resting on the table remain correctly constrained.
    • Improved fallback handling for challenging or invalid collision scenarios.
    • Enhanced consistency across linear cushions and circular pocket cushions.
  • Tests

    • Added broad coverage for cushion contact behavior, including randomized and degenerate-motion cases.

ekiefl added 10 commits August 2, 2026 12:26
get_real_smallest_magnitude_root re-inlined the realness logic that
is_real_number now provides, and that its sibling
get_real_positive_smallest_root already delegates to.
When the velocity is parallel to the cushion axis there is no perpendicular
component, so alpha and beta are both zero and the quadratic degenerates. The
solver returns nans, get_real_smallest_magnitude_root returns inf, and t * v
evaluates to nan on the axis-aligned components. The displacement guard then
silently failed, because a nan comparison is always False, and the nan flowed
into the ball position. Fall back explicitly on a non-finite root.

Also break the tie in _constrain_to_table when the ball sits exactly on the
vertical through the cushion axis. np.sign returned 0 there, zeroing the
horizontal component of the new direction and leaving it non-unit, which placed
the ball well inside the cushion surface.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Linear and circular cushion kissing now use quadratic root selection, shared fallback displacement handling, cushion nose radius, and table-height constraints. New tests cover geometry, airborne states, degenerate velocities, fallback cases, and randomized inputs.

Changes

Cushion kissing

Layer / File(s) Summary
Shared real-root selection
pooltool/ptmath/roots/core.py, pooltool/ptmath/roots/__init__.py
Adds and exports a Numba-jitted helper that selects the smallest-magnitude real root.
Cushion solver and fallback positioning
pooltool/physics/resolve/ball_cushion/core.py
Updates linear and circular kissing calculations with quadratic roots, shared fallback handling, cushion nose radius, and table constraints.
Geometry and regression coverage
tests/physics/resolve/ball_cushion/*
Adds cushion fixtures and tests for distance, displacement, airborne behavior, fallback cases, degenerate motion, and randomized states.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • ekiefl/pooltool#327 — Both changes modify cushion-collision solvers and root-selection utilities.
  • ekiefl/pooltool#194 — Both changes modify cushion geometry and cushion-collision behavior.

Suggested reviewers: derek-mcblane

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: updating 3D linear cushion make_kiss behavior.
Docstring Coverage ✅ Passed Docstring coverage is 83.87% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ek/make-kiss-3d-linear-cushion

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.80645% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.75%. Comparing base (50ca8a2) to head (144b59e).

Files with missing lines Patch % Lines
pooltool/ptmath/roots/core.py 18.18% 9 Missing ⚠️
pooltool/physics/resolve/ball_cushion/core.py 88.23% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #373      +/-   ##
==========================================
- Coverage   47.77%   47.75%   -0.03%     
==========================================
  Files         159      159              
  Lines       10758    10788      +30     
==========================================
+ Hits         5140     5152      +12     
- Misses       5618     5636      +18     
Flag Coverage Δ
service 47.75% <75.80%> (-0.03%) ⬇️
service-no-ani 58.04% <75.80%> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ekiefl

ekiefl commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

@derek-mcblane now that your linear cushion detection is merged, this is ready for review. I imagine the "make kiss" paradigm is relatively bespoke and unique to pooltool, so no pressure to review thoroughly. In truth I don't know if it helps/hurts simulation stability with floating point precision... I do know it's been a major pain in my as* though. Either way, I am pretty satisfied with the solution here in this PR.

@ekiefl

ekiefl commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
pooltool/physics/resolve/ball_cushion/core.py (1)

203-243: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Clamp the rotation cosine before np.sqrt to avoid a silent NaN.

a = (R - c[2]) / arm_len (line 238) can exceed 1 in magnitude whenever abs(R - cushion.height) > arm_len (arm_len is R + nose_radius + spacer in the normal call paths). Under PocketTableSpecs defaults this stays safely within bounds, but LinearCushionSegment accepts arbitrary p1/p2 heights and nose_radius, so a custom cushion configuration with an unusually tall or short nose relative to R would push abs(a) > 1, making 1.0 - a * a negative and np.sqrt return NaN, silently corrupting the returned position with no error raised.

Clamp a to [-1.0, 1.0] before computing b to make this function robust to arbitrary (but still geometrically valid) cushion configurations.

🛡️ Proposed defensive clamp
     a = (R - c[2]) / arm_len
+    a = max(-1.0, min(1.0, a))
     side = 1.0 if np.dot(direction, h_hat) >= 0 else -1.0
     b = side * np.sqrt(1.0 - a * a)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pooltool/physics/resolve/ball_cushion/core.py` around lines 203 - 243, In
_constrain_to_table, clamp the computed rotation cosine a to the inclusive range
[-1.0, 1.0] before calculating b with np.sqrt. Preserve the existing side
selection and returned-position logic while ensuring valid custom cushion
configurations cannot produce a negative square-root argument or NaN.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pooltool/physics/resolve/ball_cushion/core.py`:
- Around line 162-169: Update the circular cushion path in the function
containing roots_complex and get_real_smallest_magnitude_root to check
np.isfinite(t) before using t in ptmath.norm3d or updating ball.state.rvw[0].
Route non-finite roots through _apply_fallback_positioning_circular, mirroring
the existing linear cushion guard, and add a regression test in the make_kiss
tests for zero XY velocity.

---

Nitpick comments:
In `@pooltool/physics/resolve/ball_cushion/core.py`:
- Around line 203-243: In _constrain_to_table, clamp the computed rotation
cosine a to the inclusive range [-1.0, 1.0] before calculating b with np.sqrt.
Preserve the existing side selection and returned-position logic while ensuring
valid custom cushion configurations cannot produce a negative square-root
argument or NaN.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d75a77d-5c45-4813-bba3-09e6134b81bd

📥 Commits

Reviewing files that changed from the base of the PR and between 50ca8a2 and 144b59e.

📒 Files selected for processing (6)
  • pooltool/physics/resolve/ball_cushion/core.py
  • pooltool/ptmath/roots/__init__.py
  • pooltool/ptmath/roots/core.py
  • tests/physics/resolve/ball_cushion/conftest.py
  • tests/physics/resolve/ball_cushion/test_ball_cushion.py
  • tests/physics/resolve/ball_cushion/test_make_kiss.py

Comment on lines 162 to +169
roots_complex = ptmath.roots.quadratic.solve(alpha, beta, gamma)
t = ptmath.roots.get_real_smallest_magnitude_root(roots_complex)

imag_mag = np.abs(roots_complex.imag)
real_mag = np.abs(roots_complex.real)
keep = (imag_mag / real_mag) < 1e-3
roots = roots_complex[keep].real
t = roots[np.abs(roots).argmin()]

if ptmath.norm3d(t * v) > 5 * spacer:
ball.state.rvw[0] = self._apply_fallback_positioning_circular(
ball, cushion, spacer
)
return ball
if ptmath.norm3d(t * v) > FALLBACK_DISPLACEMENT_FACTOR * spacer:
return _apply_fallback_positioning_circular(ball, cushion, spacer)

ball.state.rvw[0] = r + t * v

This comment was marked as low quality.

@ekiefl
ekiefl requested a review from derek-mcblane August 2, 2026 22:16
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