Skip to content

3D Ball-Ball Detection#342

Merged
ekiefl merged 17 commits into
ekiefl:mainfrom
derek-mcblane:3d-ball-ball-detection
May 26, 2026
Merged

3D Ball-Ball Detection#342
ekiefl merged 17 commits into
ekiefl:mainfrom
derek-mcblane:3d-ball-ball-detection

Conversation

@derek-mcblane
Copy link
Copy Markdown
Collaborator

@derek-mcblane derek-mcblane commented May 23, 2026

Implement get_next_ball_ball_3d_event to resolve #305.

There's no reason that this can't just completely replace 2d event detection.

Summary by CodeRabbit

  • New Features

    • Enhanced 3D ball collision detection with unified collision timing for more accurate physics simulation across two and three-dimensional scenarios.
  • Tests

    • Added comprehensive test coverage for ball-ball collision detection, including sliding, airborne, and rolling ball scenarios with varying collision geometries.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dabed4da-be7f-4b66-8f75-9f50ea2c0c04

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR implements full 3D ball-ball collision detection by adding supporting math modules, a real 3D collision-time solver, and consolidating the 2D/3D event detection into a unified dispatcher with backward-compatible test coverage.

Changes

3D Ball-Ball Collision Detection

Layer / File(s) Summary
Ball trajectory polynomial computation
pooltool/evolution/event_based/detect/ball_position_polynomial.py
New ball_position_polynomial function computes position polynomial coefficients r(t) = p[0] + p[1]·t + p[2]·t² for balls in different states. The acceleration coefficient p[2] depends on motion state: zeros for stationary/spinning, surface-velocity terms for rolling/sliding, or fixed vertical acceleration for airborne.
Quartic distance coefficients
pooltool/evolution/event_based/detect/quartic_coefficients.py
New parabola_sphere_distance_quartic_coefficients function derives five quartic coefficients from relative ball position and contact distance, enabling polynomial root-finding for collision time computation.
3D collision-time solver
pooltool/evolution/event_based/detect/ball_ball.py (lines 13–84)
New ball_ball_collision_time(ball1, ball2) computes both balls' trajectory polynomials, forms quartic distance coefficients, and returns the smallest positive real root. Previous scalar-argument 2D implementation renamed to ball_ball_collision_time_2d for legacy use.
Consolidated event dispatcher
pooltool/evolution/event_based/detect/ball_ball.py (lines 135–202)
Single get_next_ball_ball_event(shot, collision_cache, *, is_3d) replaces separate 2D/3D functions. Dispatches to 3D collision-time solver or legacy 2D routine based on is_3d flag, using state-dependent friction parameters for 2D mode.
Public API update
pooltool/evolution/event_based/detect/__init__.py
Imports and exports get_next_ball_ball_event as the consolidated public API entry point, replacing separate 2D/3D-specific getters.
Detector integration
pooltool/evolution/event_based/detect/detector.py
EventDetector.get_next_event calls unified get_next_ball_ball_event(..., is_3d=self.is_3d) instead of selecting separate functions. Imports updated accordingly.
New collision test suite
tests/evolution/event_based/test_ball_ball.py
Four test cases validate sliding, airborne, and rolling ball-ball collisions across 2D/3D modes. Tests verify collision-time computation against analytic distance/speed values and validate contact geometry including non-uniform ball radii.
Existing test updates
tests/evolution/event_based/test_simulate.py
Replaces ball_ball_collision_time calls with ball_ball_collision_time_2d. Parameterizes test_ball_ball_collision_for_intersecting_balls to test both 2D and 3D modes via get_next_ball_ball_event(..., is_3d=is_3d).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ekiefl/pooltool#327: Modifies collision timing and ball-ball detection logic with changes to the solver and event API.
  • ekiefl/pooltool#300: Implements earlier consolidation of 3D ball-ball stub into unified event detection.
  • ekiefl/pooltool#299: Introduces the earlier 2D/3D function separation that this PR consolidates back into a single dispatcher.

Suggested reviewers

  • ekiefl
🚥 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 and specifically describes the main change: implementing 3D ball-ball detection, which is the core objective of this PR.
Linked Issues check ✅ Passed The PR fully addresses issue #305 by implementing the previously stubbed 3D ball-ball collision detection with the get_next_ball_ball_event function supporting 3D mode via an is_3d parameter.
Out of Scope Changes check ✅ Passed The PR introduces necessary supporting infrastructure (position polynomials, quartic coefficients) and comprehensive test coverage, all directly related to implementing 3D ball-ball detection as specified in issue #305.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 23, 2026

Codecov Report

❌ Patch coverage is 60.00000% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.75%. Comparing base (79d1df6) to head (d668965).

Files with missing lines Patch % Lines
...ion/event_based/detect/ball_position_polynomial.py 34.78% 15 Missing ⚠️
...olution/event_based/detect/quartic_coefficients.py 46.15% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #342      +/-   ##
==========================================
+ Coverage   47.72%   47.75%   +0.02%     
==========================================
  Files         157      159       +2     
  Lines       10670    10716      +46     
==========================================
+ Hits         5092     5117      +25     
- Misses       5578     5599      +21     
Flag Coverage Δ
service 47.75% <60.00%> (+0.02%) ⬆️
service-no-ani 58.14% <60.00%> (-0.03%) ⬇️

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

☔ View full report in Codecov by Sentry.
📢 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.

@derek-mcblane derek-mcblane force-pushed the 3d-ball-ball-detection branch from 8bdb40b to 59aaeb7 Compare May 23, 2026 17:24
@derek-mcblane derek-mcblane changed the title Issue #305 get_next_ball_ball_3d_event 3D Ball-Ball Detection May 23, 2026
@ekiefl
Copy link
Copy Markdown
Owner

ekiefl commented May 23, 2026

Wow, the way you composed these functions is really elegant, @derek-mcblane. Is it ready for review?

There's no reason that this can't just completely replace 2d event detection.

I totally agree, it would just involve removing the _2d/_3d suffices and updating the detector logic:

candidates.append(transition_cache.get_next())
candidates.append(get_next_ball_linear_cushion_event(shot, collision_cache))
candidates.append(get_next_ball_circular_cushion_event(shot, collision_cache))
candidates.append(get_next_ball_pocket_event(shot, collision_cache))
if self.is_3d:
candidates.append(get_next_ball_ball_3d_event(shot, collision_cache))
candidates.append(get_next_ball_table_event(shot, collision_cache))
else:
candidates.append(get_next_ball_ball_2d_event(shot, collision_cache))

Once you do this, fingers crossed all the passes test. Then as an extra layer of security to ensure we didn't introduce any regressions in the 2D behavior, I'm going to throw the kitchen sink at the detector and make sure the same shot is simulated across main and this branch.

I know this is still a draft, but at this point here are the things I know I'd like to see in this branch. Could you please let me know what you're interested in / have the energy for? Whatever is left over, I'm happy to take over.

  • some test cases with known analytical answers. E.g. Two airborne balls moving towards each other 1m + R1 + R2 apart, at a rel speed of 1m/s collide after 1s. Airborne ball lands on top of stationary ball. Just easy-to-verify tests like that.
  • consolidate get_next_ball_ball_2d_event and get_next_ball_ball_3d_event into get_next_ball_ball_event
  • ensure no regressions in 2D simulations (I'll do this one, as mentioned)

@ekiefl
Copy link
Copy Markdown
Owner

ekiefl commented May 23, 2026

I'll note that I wouldn't mind taking over this branch, just because it's very clear for me what needs to be done. There's several other projects that require your expertise, so now that you've laid down the framework in this PR, I'm happy to clean it up so if you think your time could be spent better elsewhere.

@derek-mcblane
Copy link
Copy Markdown
Collaborator Author

Wow, the way you composed these functions is really elegant, @derek-mcblane. Is it ready for review?

Thanks! I'm going to do a quick implementation of ball-ball collision resolution so I can spot check this in the simulator.

There's no reason that this can't just completely replace 2d event detection.

I totally agree, it would just involve removing the _2d/_3d suffices and updating the detector logic:

candidates.append(transition_cache.get_next())
candidates.append(get_next_ball_linear_cushion_event(shot, collision_cache))
candidates.append(get_next_ball_circular_cushion_event(shot, collision_cache))
candidates.append(get_next_ball_pocket_event(shot, collision_cache))
if self.is_3d:
candidates.append(get_next_ball_ball_3d_event(shot, collision_cache))
candidates.append(get_next_ball_table_event(shot, collision_cache))
else:
candidates.append(get_next_ball_ball_2d_event(shot, collision_cache))

I might also do something about this.. I'm thinking I will leave the 2d detector as is for now to give time for any bugs in the 3d version to surface, but I can eliminate the code duplication.

I know this is still a draft, but at this point here are the things I know I'd like to see in this branch. Could you please let me know what you're interested in / have the energy for? Whatever is left over, I'm happy to take over.

  • some test cases with known analytical answers. E.g. Two airborne balls moving towards each other 1m + R1 + R2 apart, at a rel speed of 1m/s collide after 1s. Airborne ball lands on top of stationary ball. Just easy-to-verify tests like that.

I can add some tests for sure. I suspect you might have some to add afterwards.

  • consolidate get_next_ball_ball_2d_event and get_next_ball_ball_3d_event into get_next_ball_ball_event

I'll do this. Like I said I think I'm going to levae the 2d detector as is for now.

@ekiefl
Copy link
Copy Markdown
Owner

ekiefl commented May 23, 2026

Ok, I'll reel back my ask for consolidating 2D with 3D on this branch, and instead leave it up to you how you want to slice and dice this. We can consider that checklist I made an eventuality, rather than specific to this branch.

Comment on lines +222 to +225
if is_3d:
dtau_E = ball_ball_collision_time_3d(ball1, ball2)
else:
dtau_E = ball_ball_collision_time(
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Moved the is_3d check to here to avoid code duplication without worrying about altering the 2d behavior quite yet.

@derek-mcblane derek-mcblane force-pushed the 3d-ball-ball-detection branch from 02f9ab2 to d31c055 Compare May 25, 2026 03:57
@derek-mcblane derek-mcblane marked this pull request as ready for review May 25, 2026 06:24
@derek-mcblane derek-mcblane requested a review from ekiefl as a code owner May 25, 2026 06:24
@derek-mcblane derek-mcblane force-pushed the 3d-ball-ball-detection branch from 027dd7b to 07d4524 Compare May 25, 2026 06:25
pass is_3d to get_next_ball_ball_event
instead of using two completely separate functions
so that everything except the collision time prediction can
be shared between 2d and 3d
@derek-mcblane derek-mcblane force-pushed the 3d-ball-ball-detection branch from 07d4524 to 88ae60c Compare May 25, 2026 17:06
@ekiefl
Copy link
Copy Markdown
Owner

ekiefl commented May 25, 2026

👀

@derek-mcblane derek-mcblane force-pushed the 3d-ball-ball-detection branch from 85800e2 to eef44ba Compare May 25, 2026 21:52
Comment thread tests/evolution/event_based/test_simulate.py Outdated
ekiefl and others added 2 commits May 25, 2026 18:06
- get_ball_ball_collision_time -> get_ball_ball_collision_time_2d
- get_ball_ball_collision_time_3d -> get_ball_ball_collision_time
Copy link
Copy Markdown
Owner

@ekiefl ekiefl left a comment

Choose a reason for hiding this comment

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

A few requests but it otherwise looks good to goooo!

I added some docstrings and made the following fn name replacements:

    - get_ball_ball_collision_time -> get_ball_ball_collision_time_2d
    - get_ball_ball_collision_time_3d -> get_ball_ball_collision_time

Comment thread pooltool/evolution/event_based/detect/ball_ball.py Outdated
Comment thread pooltool/evolution/event_based/detect/ball_ball.py Outdated
Comment thread pooltool/evolution/event_based/detect/ball_position_polynomial.py Outdated
Comment thread pooltool/evolution/event_based/detect/ball_position_polynomial.py Outdated
@ekiefl
Copy link
Copy Markdown
Owner

ekiefl commented May 26, 2026

Oh right I'm gonna add some more tests as I mentioned. In that case please leave these comments to me, I'll have a taste of my own medicine

@derek-mcblane
Copy link
Copy Markdown
Collaborator Author

Oh right I'm gonna add some more tests as I mentioned. In that case please leave these comments to me, I'll have a taste of my own medicine

Oh no, I just addressed them haha.. I hope you didn't start.

Comment on lines +14 to +31
"""Build the position-vs-time polynomial for a ball.

The trajectory is expressed as

r(t) = r0 + V0 * t + 1/2 a * t**2
= p[0] + p[1] * t + p[2] * t**2

where each row of the returned ``(3, 3)`` array is an ``(x, y, z)`` vector.

Returns:
A ``(3, 3)`` array ``p`` where rows are the coefficients of the
position polynomial in increasing power of ``t``:

* ``p[0]``: initial position (``rvw[0]``).
* ``p[1]``: initial velocity (``rvw[1]``).
* ``p[2]``: half the acceleration vector (the coefficient of ``t**2``, not
the acceleration itself).
"""
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks :)

@ekiefl
Copy link
Copy Markdown
Owner

ekiefl commented May 26, 2026

Ok I added a ball falling on ball test, and two balls with uneven radii test. Going to merge this.

@ekiefl
Copy link
Copy Markdown
Owner

ekiefl commented May 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/evolution/event_based/detect/ball_ball.py`:
- Line 138: The docstring incorrectly claims the routine only detects "in 2D
mode" while the function now dispatches both 2D and 3D based on is_3d; update
the function docstring in pooltool/evolution/event_based/detect/ball_ball.py to
describe that it detects the next ball-ball collision for both 2D and 3D,
mention the is_3d flag determines which branch runs, and briefly state the
behavior for each branch (e.g., 2D collision logic vs 3D collision logic) while
keeping it short and accurate; reference the is_3d variable and the function
that contains this docstring when making the change.

In `@pooltool/evolution/event_based/detect/ball_position_polynomial.py`:
- Around line 41-43: tangent_surface_velocity(rvw, -unit_z, R) returns a
velocity vector that must be normalized before applying friction acceleration;
replace the direct use of v_hat_c_0 with a unit vector: compute norm =
np.linalg.norm(v_hat_c_0), if norm > 0 set v_hat_c_0 = v_hat_c_0 / norm
(otherwise set to zero), then use -v_hat_c_0 when computing p[2] so the 0.5 *
u_s * g factor scales correctly; update the assignment involving v_hat_c_0 (the
variable in question) accordingly and keep the rest of the calculation
unchanged.
🪄 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

Run ID: 1b5124d6-00de-4417-be4c-320a8184ea3e

📥 Commits

Reviewing files that changed from the base of the PR and between 79d1df6 and c38d4b1.

📒 Files selected for processing (7)
  • pooltool/evolution/event_based/detect/__init__.py
  • pooltool/evolution/event_based/detect/ball_ball.py
  • pooltool/evolution/event_based/detect/ball_position_polynomial.py
  • pooltool/evolution/event_based/detect/detector.py
  • pooltool/evolution/event_based/detect/quartic_coefficients.py
  • tests/evolution/event_based/test_ball_ball.py
  • tests/evolution/event_based/test_simulate.py

def get_next_ball_ball_event(
shot: System, collision_cache: CollisionCache, *, is_3d: bool
) -> Event:
"""Detect the next ball-ball collision in 2D mode."""
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Docstring is stale after 2D/3D consolidation.

Line 138 says this detects collisions "in 2D mode", but the function now dispatches both 2D and 3D via is_3d.

Suggested fix
-    """Detect the next ball-ball collision in 2D mode."""
+    """Detect the next ball-ball collision."""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""Detect the next ball-ball collision in 2D mode."""
"""Detect the next ball-ball collision."""
🤖 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/evolution/event_based/detect/ball_ball.py` at line 138, The
docstring incorrectly claims the routine only detects "in 2D mode" while the
function now dispatches both 2D and 3D based on is_3d; update the function
docstring in pooltool/evolution/event_based/detect/ball_ball.py to describe that
it detects the next ball-ball collision for both 2D and 3D, mention the is_3d
flag determines which branch runs, and briefly state the behavior for each
branch (e.g., 2D collision logic vs 3D collision logic) while keeping it short
and accurate; reference the is_3d variable and the function that contains this
docstring when making the change.

Comment on lines +41 to +43
unit_z = np.array([0, 0, 1])
v_hat_c_0 = tangent_surface_velocity(rvw, -unit_z, R)
p[2] = 0.5 * u_s * g * -v_hat_c_0
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 26, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize sliding contact velocity before applying friction acceleration.

tangent_surface_velocity(...) returns a velocity vector, not a unit direction. Using it directly makes p[2] scale with speed, which breaks the 0.5 * a coefficient and can skew collision-time roots.

Suggested fix
 elif s == const.sliding:
-    unit_z = np.array([0, 0, 1])
-    v_hat_c_0 = tangent_surface_velocity(rvw, -unit_z, R)
-    p[2] = 0.5 * u_s * g * -v_hat_c_0
+    unit_z = np.array([0.0, 0.0, 1.0])
+    v_c_0 = tangent_surface_velocity(rvw, -unit_z, R)
+    v_c_0_norm = ptmath.norm3d(v_c_0)
+    if v_c_0_norm == 0.0:
+        p[2] = np.zeros(3)
+    else:
+        p[2] = -0.5 * u_s * g * (v_c_0 / v_c_0_norm)
🤖 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/evolution/event_based/detect/ball_position_polynomial.py` around
lines 41 - 43, tangent_surface_velocity(rvw, -unit_z, R) returns a velocity
vector that must be normalized before applying friction acceleration; replace
the direct use of v_hat_c_0 with a unit vector: compute norm =
np.linalg.norm(v_hat_c_0), if norm > 0 set v_hat_c_0 = v_hat_c_0 / norm
(otherwise set to zero), then use -v_hat_c_0 when computing p[2] so the 0.5 *
u_s * g factor scales correctly; update the assignment involving v_hat_c_0 (the
variable in question) accordingly and keep the rest of the calculation
unchanged.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Whooops. Good catch!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@derek-mcblane, no worries — easy to miss! The suggested fix normalizes v_c_0 and also guards against the zero-velocity case (ball already at the sliding threshold) to keep the polynomial well-formed. Let me know if you'd like any clarification on the fix.

(=^・ω・^=)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@derek-mcblane This is a real bug. I fixed it in d668965.

Noteworthy that after removing all the 2D dispatch logic and replacing it with 3D, all the tests pass. But I'm gonna keep the 2D dispatch until I do more tests.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@ekiefl ekiefl merged commit 0b344e7 into ekiefl:main May 26, 2026
11 checks passed
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.

[support-3d] A4. 3D ball-ball detection

2 participants