pooltool Data Flow

Physics-based pool simulation — from a cue strike to rendered animation. Repository: ekiefl/pooltool  ·  commit 79d1df6

Pipeline at a Glance

The BallState Tensor (rvw)

Every ball's kinematic state is a 3×3 NumPy array called rvw. Each row is a 3D vector:

r
rvw[0]
Position (x, y, z)
v
rvw[1]
Linear velocity
w
rvw[2]
Angular velocity (spin)

Ball Motion States

Each ball carries an integer state s that controls which physics equations apply:

0 · stationary 1 · spinning 2 · sliding 3 · rolling 4 · pocketed

Sliding decays to rolling (kinetic friction), rolling decays to spinning or stationary, spinning decays to stationary. Pocketed balls are removed from further simulation.

Public API

Minimal usage example
import pooltool as pt

# 1. Build system
system = pt.System(
    cue=pt.Cue.default(),
    table=(table := pt.Table.default()),
    balls=pt.get_rack(pt.GameType.NINEBALL, table=table),
)

# 2. Aim and strike
system.strike(V0=8.0, phi=pt.aim.at_ball(system, "1"))

# 3. Simulate physics
pt.simulate(system, inplace=True)

# 4. Densify for animation (optional)
pt.continuize(system, dt=0.01, inplace=True)

# 5. View in Panda3D window
pt.show(system)

Key Modules

pooltool/__init__.py

Public API surface. Re-exports System, simulate, continuize, show, Ball, Cue, Table.

pooltool/system/datatypes.py

Root container class System (line 30). Holds balls, cue, table, elapsed time, and event history.

pooltool/evolution/event_based/simulate.py

The main simulation loop. simulate() at line 103 drives _SimulationState until quiescence.

pooltool/physics/evolve/__init__.py

Numba-compiled evolve_ball_motion(). Analytically advances rvw based on the current motion state.

pooltool/evolution/event_based/detect/

One module per event type. detector.py orchestrates all detectors and picks the earliest event.

pooltool/physics/resolve/

Pluggable collision models for every event type. resolver.py dispatches to the chosen strategy.

Full data flow — annotated call graph
pt.simulate(system)
└── simulate()                          simulate.py:103
    ├── SimulationEngine()              engine.py:12
    │   ├── .detector  EventDetector   detector.py:105
    │   └── .resolver  Resolver        resolver.py:98
    ├── _SimulationState.init()         simulate.py:38
    │   └── ball.history.reset() × N
    └── while not_done:
        └── _SimulationState.step()     simulate.py:42
            ├── EventDetector.get_next_event()   detector.py:123
            │   ├── get_next_stick_ball_event()
            │   ├── transition_cache.get_next()
            │   ├── get_next_ball_linear_cushion_event()
            │   ├── get_next_ball_circular_cushion_event()
            │   ├── get_next_ball_pocket_event()
            │   ├── get_next_ball_ball_2d_event()
            │   └── → earliest Event by _get_event_priority()
            ├── evolve(shot, dt=event.time)      simulate.py:80
            │   └── evolve_ball_motion() × N     evolve/__init__.py:28
            ├── Resolver.resolve(event)          resolver.py:116
            │   └── strategy.resolve(event, shot)
            └── System._update_history(event)

pt.continuize(system)                   continuous.py:17
└── interpolate_ball_states() × N balls continuous.py:194
    └── populates ball.history_cts

pt.show(system)                         interact.py:8
└── ShotViewer(system)                  animate.py:327
    ├── SystemRender.from_system()
    └── Panda3D animation loop → ball.history_cts