pooltool

The top-level API for the pooltool library.

Important and highly used objects are placed in this top-level API. For example, System can be imported directly from the top module:

>>> import pooltool as pt
>>> system = pt.System.example()

Alternatively, it can be imported directly from its lower-level API location:

>>> from pooltool.system import System
>>> system = System.example()

If the object you’re looking for isn’t in this top-level API, search for it in the subpackages/submodules listed below. Relatedly, if you believe that an objects deserves to graduate to the top-level API, your input is valuable and such changes can be considered.

Subpackages

Submodules

Classes

class EventType[source]

An Enum of event types

Base Classes:

pooltool.utils.strenum.StrEnum

Attributes:

NONE

The null event.

BALL_BALL

A ball-ball collision.

BALL_LINEAR_CUSHION

A ball collision with a linear cushion segment.

BALL_CIRCULAR_CUSHION

A ball collision with a circular cushion segment.

BALL_POCKET

A ball pocket “collision”. This marks the point at which the ball crosses the point of no return.

STICK_BALL

A cue-stick ball collision.

SPINNING_STATIONARY

A ball transition from spinning to stationary.

ROLLING_STATIONARY

A ball transition from rolling to stationary.

ROLLING_SPINNING

A ball transition from rolling to spinning.

SLIDING_ROLLING

A ball transition from sliding to rolling.

Methods:

is_collision() bool[source]

Returns whether the member is a collision

Return type:

bool

is_transition() bool[source]

Returns whether the member is a transition

Return type:

bool

has_ball() bool[source]

Returns True if this event type can involve a Ball.

Return type:

bool

has_cushion() bool[source]

Returns True if this event type can involve a cushion (linear or circular).

Return type:

bool

has_pocket() bool[source]

Returns True if this event type can involve a Pocket.

Return type:

bool

has_stick() bool[source]

Returns True if this event type can involve a CueStick.

Return type:

bool

class GameType[source]

An Enum for supported game types

Base Classes:

pooltool.utils.strenum.StrEnum

Attributes:

EIGHTBALL
NINEBALL
THREECUSHION
SNOOKER
SUMTOTHREE
class Game(config=ShowBaseConfig(window_type='onscreen', window_size=None, fb_prop=None, monitor=False))[source]

This class runs the pooltool application

Base Classes:

Interface

Methods:

attach_system(system: System) None[source]
attach_ruleset(ruleset: Ruleset) None[source]
start()[source]
class Ball(id: str, state: BallState = BallState.default, params: BallParams = BallParams.default, ballset: BallSet | None = None, initial_orientation: BallOrientation = BallOrientation.random, history: BallHistory = BallHistory.factory, history_cts: BallHistory = BallHistory.factory)[source]

A billiards ball.

This class represents a billiards ball. It stores its parameters (mass, radius, etc.), it’s state (coordinates, velocity, spin, etc), its history (a time-resolved trajectory of its state), amongst other things.

Attributes:

id : str

An ID for the ball.

Use strings (e.g. “1” not 1).

state : BallState

The ball’s state.

This is the current state of the ball.

See also

  • See the Important section below for a description of the role of states during simulation.

params : BallParams

The ball’s physical parameters.

The physical parameters of the ball.

ballset : BallSet | None

The ball set that the ball belongs to.

Important if rendering the ball in a scene.

See also

initial_orientation : BallOrientation

The initial rendered orientation of the ball.

Important if rendering the ball in a scene.

This is the orientation of the ball at \(t = 0\).

history : BallHistory

The ball’s state history

The historical states of the ball from \(t_{initial}\) to \(t_{final}\).

See also

  • See the Important section below for a description of the role of history during simulation.

history_cts : BallHistory

The ball’s continuous state history

The historical states of the ball from \(t_{initial}\) to \(t_{final}\) densely sampled with respect to time.

See also

  • See pooltool.evolution.continuize() for a details about continuizing a simulated system.

  • See the Important section below for a description of the role of history_cts during simulation.

Important

To instantiate this class, consider using the create() constructor. Or, use functions within pooltool.layouts to generate entire collection of balls. Or, of course, construct as normal with __init__.

Important

The following explains how a Ball object is modified when its parent system is simulated (pooltool.evolution.simulate()).

At the start of the simulation process, state represents the ball state at \(t = 0\). A copy of state is appended to history.

For each timestep of the simulation, state is used to inform how the system should advance forward in time. Once determined, state is updated to reflect the ball’s new state. A copy of state is appended to history.

When the simulation is finished, state represents the final resting state of the ball. So too does history[-1].

Finally, if the system is continuized (see pooltool.evolution.continuize()), history_cts is populated. Otherwise it remains empty.

property xyz

The displacement (from origin) vector of the ball.

A shortcut for self.state.rvw[0].

property vel

The velocity vector of the ball.

A shortcut for self.state.rvw[1].

property avel

The angular velocity vector of the ball.

A shortcut for self.state.rvw[2].

Methods:

set_ballset(ballset: BallSet) None[source]

Update the ballset

Raises:

ValueError -- If the ball ID doesn’t match to a model name of the ballset.

See also

copy(drop_history: bool = False) Ball[source]

Create a copy

Parameters:

drop_history : bool

If True, the returned copy history and history_cts attributes are both set to empty pooltool.objects.BallHistory objects.

Return type:

Ball

static create(id: str, *, xy: Sequence[float] | None = None, ballset: BallSet | None = None, **kwargs) Ball[source]

Create a ball using keyword arguments.

This constructor flattens the tunable parameter space, allowing one to construct a Ball without directly instancing objects like like pooltool.objects.BallParams and pooltool.objects.BallState.

Parameters:
Return type:

Ball

static dummy(id: str = 'dummy') Ball[source]
Return type:

Ball

class BallParams(m: float = 0.170097, R: float = 0.028575, u_s: float = 0.2, u_r: float = 0.01, u_sp_proportionality: float = 0.4444444444444444, u_b: float = 0.05, e_b: float = 0.95, e_c: float = 0.85, f_c: float = 0.2, g: float = 9.81)[source]

Ball parameters and physical constants

Note

The presence of an attribute does not guarantee its usage by the physics engine. For example, if the frictionless elastic ball-ball collision model is used, then \(u_b\), the ball-ball sliding coefficient of friction, will have no affect on the simulation.

Attributes:

m : float

The mass of the ball.

R : float

The radius of the ball.

u_s : float

The sliding coefficient of friction.

u_r : float

The rolling coefficient of friction.

u_sp_proportionality : float

The spinning coefficient of friction, with \(R\) factored out.

See also

  • For the coefficient of spinning friction, use the property u_sp().

u_b : float

The ball-ball coefficient of sliding friction.

e_b : float

The ball-ball coefficient of restitution.

e_c : float

The cushion coefficient of restitution.

Note

This is a potentially model-dependent ball-cushion parameter and should be placed elsewhere, either as a model parameter or as a cushion segment parameter.

f_c : float

The cushion coefficient of friction.

Note

This is a potentially model-dependent ball-cushion parameter and should be placed elsewhere, either as a model parameter or as a cushion segment parameter.

g : float

The gravitational constant.

Most of the default values (SI units) are taken from or based off of https://billiards.colostate.edu/faq/physics/physical-properties/.

Some of the parameters aren’t truly ball parameters, e.g. the gravitational constant. However, it is nice to be able to tune such parameters on a ball-by-ball basis, so they are included here.

property u_sp: float[source]

Coefficient of spinning friction.

This is equal to u_sp_proportionality * R.

Return type:

float

Methods:

copy() BallParams[source]

Return a copy

Note

  • Since the class is frozen and its attributes are immutable, this just returns self.

Return type:

BallParams

classmethod default(game_type: GameType = GameType.EIGHTBALL) BallParams[source]

Return prebuilt ball parameters based on game type

Parameters:

game_type : GameType

What type of game is being played?

Returns:

The prebuilt ball parameters associated with the passed game type.

Return type:

BallParams

classmethod prebuilt(name: PrebuiltBallParams) BallParams[source]

Return prebuilt ball parameters based on name

Parameters:

name : PrebuiltBallParams

A pooltool.objects.PrebuiltBallParams member.

Return type:

BallParams

All prebuilt ball parameters are are members of the pooltool.objects.PrebuiltBallParams Enum. This constructor takes a prebuilt name and returns the corresponding ball parameters.

class Cue(id: str = 'cue_stick', V0: float = 2.0, phi: float = 0.0, theta: float = 0.0, a: float = 0.0, b: float = 0.25, cue_ball_id: str = 'cue', specs: CueSpecs = CueSpecs.default, model_name: str | None = None)[source]

A cue stick.

Attributes:

id : str

An ID for the cue.

V0 : float

The impact speed.

Units are m/s.

Note

This is the speed of the cue stick upon impact, not the speed of the ball upon impact.

phi : float

The directional strike angle.

The horizontal direction of the cue’s orientation relative to the table layout. Specified in degrees.

If you imagine facing from the head rail (where the cue is positioned for a break shot) towards the foot rail (where the balls are racked),

  • \(\phi = 0\) corresponds to striking the cue ball to the right

  • \(\phi = 90\) corresponds to striking the cue ball towards the foot rail

  • \(\phi = 180\) corresponds to striking the cue ball to the left

  • \(\phi = 270\) corresponds to striking the cue ball towards the head rail

  • \(\phi = 360\) corresponds to striking the cue ball to the right

theta : float

The cue inclination angle.

The vertical angle of the cue stick relative to the table surface. Specified in degrees.

  • \(\theta = 0\) corresponds to striking the cue ball parallel with the table (no massé)

  • \(\theta = 90\) corresponds to striking the cue ball downwards into the table (max massé)

a : float

The amount and direction of side spin.

  • \(a = -1\) is the rightmost side of ball

  • \(a = +1\) is the leftmost side of the ball

b : float

The amount of top/bottom spin.

  • \(b = -1\) is the bottom-most side of the ball

  • \(b = +1\) is the top-most side of the ball

cue_ball_id : str

The ball ID of the ball being cued.

specs : CueSpecs

The cue specs.

model_name : str | None

The name of the cue model directory under pooltool/models/cue/.

Important if rendering the cue in a scene.

Methods:

copy() Cue[source]

Create a copy

Note

specs is shared between self and the copy, but that’s ok because it’s frozen and has no mutable attributes.

Return type:

Cue

reset_state() None[source]

Resets V0, phi, theta, a and b to their defaults.

set_state(V0: float | None = None, phi: float | None = None, theta: float | None = None, a: float | None = None, b: float | None = None, cue_ball_id: str | None = None) None[source]

Set the cueing parameters

Parameters:

If any arguments are None, they will be left untouched--they will not be set to None.

classmethod from_game_type(game_type: GameType, id: str | None = None) Cue[source]
Return type:

Cue

classmethod default() Cue[source]

Construct a cue with defaults

Return type:

Cue

class Table(cushion_segments: CushionSegments, pockets: dict[str, Pocket], table_type: TableType, model_descr: TableModelDescr | None = None, height: float = 0.708, lights_height: float = 1.99)[source]

A table.

While a table can be constructed by passing all of the following initialization parameters, there are many easier ways, all of which are detailed in the Table Specification </resources/table_specs> resource.

Attributes:

cushion_segments : CushionSegments

The table’s linear and circular cushion segments.

pockets : dict[str, Pocket]

The table’s pockets.

table_type : TableType

An Enum specifying the type of table.

height : float

The height of the playing surface (measured from the ground).

This is just used for visualization.

lights_height : float

The height of the table lights (measured from the playing surface).

This is just used for visualization.

property w: float

The width of the table.

Warning

This assumes the table follows the layout similar to this diagram. Specifically, it must have the linear cushion segments with IDs "3"` and "12".

Return type:

float

property l: float

The length of the table.

Warning

This assumes the table follows the layout similar to this diagram. Specifically, it must have the linear cushion segments with IDs "9"` and "18".

Return type:

float

property center: tuple[float, float]

Return the 2D coordinates of the table’s center

Warning

This assumes l() and w() are defined.

Return type:

tuple[float, float]

property has_linear_cushions: bool
Return type:

bool

property has_circular_cushions: bool
Return type:

bool

property has_pockets: bool
Return type:

bool

Methods:

set_cushion_height(height: float) None[source]

Set the height of all cushion segments.

Parameters:

height : float

The new height to set for all cushion segments.

copy() Table[source]

Create a copy.

Return type:

Table

static from_table_specs(specs: pooltool.objects.table.specs.TableSpecs) Table[source]

Build a table from a table specifications object

Parameters:

specs : pooltool.objects.table.specs.TableSpecs

A valid table specification.

Accepted objects:

Returns:

A table matching the specifications of the input.

Return type:

Table

classmethod prebuilt(name: TableName) Table[source]

Create a default table based on name

Parameters:

name : TableName

The name of the prebuilt table specs.

Returns:

A prebuilt table.

Return type:

Table

classmethod default(table_type: TableType = TableType.POCKET) Table[source]

Create a default table based on table type

A default table is associated to each table type.

Parameters:

table_type : TableType

The type of table.

Returns:

The default table for the given table type.

Return type:

Table

classmethod from_game_type(game_type: GameType) Table[source]

Create a default table based on table type

A default table is associated with each game type.

Parameters:

game_type : GameType

The game type.

Returns:

The default table for the given game type.

Return type:

Table

class TableType[source]

An Enum describing the table type.

Base Classes:

pooltool.utils.strenum.StrEnum

Attributes:

POCKET
BILLIARD
SNOOKER
OTHER
class Player(name: str, ai: AIPlayer | None = None)[source]

A player

Attributes:

name : str

Player’s name.

ai : AIPlayer | None

Not implemented yet...

property is_ai: bool
Return type:

bool

class MultiSystem(multisystem: list[System] = list)[source]

A storage for System objects

Houses a collection of systems, for example, shots taken sequentially in a game.

Attributes:

multisystem : list[System]

A list of System objects (default = [])

Example

This example illustrates the basics of multisystems.

First, make a system and evolve it.

>>> import pooltool as pt
>>> import numpy as np
>>> system = pt.System.example()
>>> system.strike(phi=90)
>>> pt.simulate(system, inplace=True)

Now add it to a multisystem.

>>> multisystem = pt.MultiSystem()
>>> multisystem.append(system)

Now copy the system, reset it’s history, strike it differently, simulate it, and add it to the mulisystem:

>>> next_system = multisystem[-1].copy()
>>> next_system.strike(phi=0)
>>> pt.simulate(next_system, inplace=True)
>>> multisystem.append(next_system)

The multisystem has a length,

>>> len(multisystem)
2

supports basic indexing,

>>> multisystem[0].t
6.017032496778012

and can be iterated through:

>>> for shot in multisystem: print(len(shot.events))
15
10

Now visualize the multisystem:

>>> pt.show(multisystem, title="Press 'n' for next, 'p' for previous")
property active: System
Return type:

System

property empty: bool
Return type:

bool

property max_index

Methods:

reset() None[source]
append(system: System) None[source]

Append a system to the multisystem

This appends system to multisystem.

extend(systems: list[System]) None[source]
set_active(i) None[source]
save(path: pooltool.serialize.serializers.Pathish) None[source]

Save the multisystem to file in a serialized format.

Supported file extensions:

  1. .json

  2. .msgpack

Parameters:

path : pooltool.serialize.serializers.Pathish

Either a pathlib.Path object or a string. The extension should match the supported filetypes mentioned above.

See also

classmethod load(path: pooltool.serialize.serializers.Pathish) MultiSystem[source]

Load a multisystem from a file in a serialized format.

Supported file extensions:

  1. .json

  2. .msgpack

Parameters:

path : pooltool.serialize.serializers.Pathish

Either a pathlib.Path object or a string representing the file path. The extension should match the supported filetypes mentioned above.

Returns:

The deserialized MultiSystem object loaded from the file.

Return type:

MultiSystem

See also

class System(cue: Cue, table: Table, balls: Any, t: float = 0.0, events: list[Event] = list)[source]

A class representing the billiards system.

This class holds:

  1. a collection of balls (pooltool.objects.Ball)

  2. a cue stick (pooltool.objects.Cue)

  3. a table (pooltool.objects.Table)

Together, these objects, referred to as the system, fully describe the billiards system.

This object is a mutable object that can be evolved over the course of system’s evolution. When a billiards system is simulated, a list of pooltool.events.Event objects is stored in this class.

This class also stores the duration of simulated time elapsed as t, measured in seconds.

Attributes:

cue : Cue

A cue stick.

table : Table

A table.

balls : dict[str, Ball]

A dictionary of balls.

Warning

Each key must match each value’s id (e.g. {"2": Ball(id="1")} is invalid).

Note

If, during construction, a sequence (e.g. list, tuple, etc.) of balls is passed instead of a dictionary, it will be converted to a dictionary.

t : float

The elapsed simulation time. If the system is in the process of being simulated, t is updated to be the number of seconds the system has evolved. After being simulated, t remains at the final simulation time.

events : list[Event]

The sequence of events in the simulation. Like t, this is updated incrementally as the system is evolved. (default = [])

Examples

Constructing a system requires a cue, a table, and a dictionary of balls:

>>> import pooltool as pt
>>> pt.System(
>>>     cue=pt.Cue.default(),
>>>     table=pt.Table.default(),
>>>     balls={"1": pt.Ball.create("1", xy=(0.2, 0.3))},
>>> )

If you need a quick system to experiment with, call example():

>>> import pooltool as pt
>>> system = pt.System.example()

You can simulate this system and introspect its attributes:

>>> pt.simulate(system, inplace=True)
>>> system.simulated
True
>>> len(system.events)
14
>>> system.cue
<Cue object at 0x7fb838080190>
 ├── V0    : 1.5
 ├── phi   : 95.07668213305062
 ├── a     : 0.0
 ├── b     : -0.3
 └── theta : 0.0

This system can also be visualized in the GUI:

>>> pt.show(system)
property continuized: bool

Checks if all balls have a non-empty continuous history.

Returns:

True if all balls have a non-empty continuous history, False otherwise.

Return type:

bool

See also

For a proper definition of continuous history, please see pooltool.objects.Ball.history_cts.

property simulated: bool

Checks if the simulation has any events.

If there are events, it is assumed that the system has been simulated.

Returns:

True if there are events, False otherwise.

Return type:

bool

Methods:

set_ballset(ballset: BallSet) None[source]

Sets the ballset for each ball in the system.

Important only if rendering the system in a scene and you are manually creating balls (rather than relying on built-in utilities in pooltool.layouts)

In this case, you need to manually associate a pooltool.objects.BallSet to the balls in the system, so that the proper model skin can be applied to each. That’s what this method does.

Parameters:

ballset : BallSet

The ballset to be assigned to each ball.

Raises:

ValueError -- If any ball’s ID does not correspond to a model name associated with the ball set.

See also

reset_history() None[source]

Resets the history for all balls, clearing events and resetting time.

Operations that this method performs:

  1. t is set to 0.0

  2. events is set to []

Additionally, for each ball in balls,

  1. pooltool.objects.Ball.history is set to BallHistory()

  2. pooltool.objects.Ball.history_cts is set to BallHistory()

  3. The t attribute of pooltool.objects.Ball.state is set to 0.0

Calling this method thus erases any history.

reset_balls() None[source]

Resets balls to their initial states based on their history

This sets the state of each ball to the ball’s initial historical state (i.e. before evolving the system). It doesn’t erase the history.

Example

This example shows that calling this method resets the balls’ states to before the system is simulated.

First, create a system and store the cue ball’s state

>>> import pooltool as pt
>>> system = pt.System.example()
>>> cue_ball_initial_state = system.balls["cue"].state.copy()
>>> cue_ball_initial_state
BallState(rvw=array([[0.4953  , 0.9906  , 0.028575],
       [0.      , 0.      , 0.      ],
       [0.      , 0.      , 0.      ]]), s=0, t=0.0)

Now simulate the system and assert that the cue ball’s new state has changed:

>>> pt.simulate(system, inplace=True)
>>> assert system.balls["cue"].state != cue_ball_initial_state

But after resetting the balls, the cue ball state once again matches the state before simulation.

>>> system.reset_balls()
>>> assert system.balls["cue"].state == cue_ball_initial_state

The system history is not erased:

>>> system.simulated
True
>>> len(system.events)
14
>>> system.t
5.193035203405666
stop_balls() None[source]

Change ball states to stationary and remove all momentum

This method removes all kinetic energy from the system by:

  1. Setting the velocity and angular velocity vectors of each ball to <0, 0, 0>

  2. Setting the balls’ motion states to stationary (i.e. 0)

strike(**kwargs) None[source]

Set cue stick parameters

This is an alias for pooltool.objects.Cue.set_state()

Parameters:

kwargs

**kwargs Cue stick parameters.

randomize_positions(ball_ids: list[str] | None = None, niter=100) bool[source]

Randomize ball positions on the table--ensure no overlap

This initializes a random state, and checks that all the balls are non-overlapping. If any are, a new state is initialized and the process is repeated.

Note

This is a very inefficient algorithm.

Parameters:
  • ball_ids : list[str] | None

    Only these balls will be randomized.

  • niter

    The number of iterations tried until the algorithm gives up.

Returns:

True if all balls are non-overlapping. Returns False otherwise.

Return type:

bool

is_balls_overlapping() bool[source]

Determines if any balls are overlapping.

Returns:

True if any balls overlap, False otherwise.

Return type:

bool

copy() System[source]

Creates a deep-ish copy of the system.

This method generates a copy of the system with a level of deep copying that is contingent on the mutability of the objects within the system. Immutable objects, frozen data structures, and read-only numpy arrays (array.flags["WRITEABLE"] = False) remain shared between the original and the copied system.

TLDR For all intents and purposes, mutating the system copy will not impact the original system, and vice versa.

Returns:

A deepcopy of the system.

Return type:

System

Example

>>> import pooltool as pt
>>> system = pt.System.example()
>>> system_copy = pt.System.example()
>>> pt.simulate(system, inplace=True)
>>> system.simulated
True
>>> system_copy.simulated
False
save(path: pooltool.serialize.serializers.Pathish, drop_continuized_history: bool = False) None[source]

Save a System to file in a serialized format.

Supported file extensions:

  1. .json

  2. .msgpack

Parameters:
  • path : pooltool.serialize.serializers.Pathish

    Either a pathlib.Path object or a string. The extension should match the supported filetypes mentioned above.

  • drop_continuized_history : bool

    If True, pooltool.objects.Ball.history_cts is wiped before the save operation, which can save a lot of disk space and increase save/load speed. If loading (deserializing) at a later time, the history_cts will have to be repopulated via simulation (see Examples).

Example

An example of saving to, and loading from, JSON:

>>> import pooltool as pt
>>> system = pt.System.example()
>>> system.save("case1.json")
>>> loaded_system = pt.System.load("case1.json")
>>> assert system == loaded_system

You can also save simulated systems:

>>> pt.simulate(system, inplace=True)
>>> system.save("case2.json")

Simulated systems contain event and ball trajectory data, so they’re larger:

$ du -sh case1.json case2.json

12K case1.json 68K case2.json

Example

JSON may be human readable, but MSGPACK is faster:

>>> import pooltool as pt
>>> system = pt.System.example()
>>> pt.simulate(system, inplace=True)
>>> print("saving:")
>>> %timeit system.save("readable.json")
>>> %timeit system.save("fast.msgpack")
>>> print("loading:")
>>> %timeit pt.System.load("readable.json")
>>> %timeit pt.System.load("fast.msgpack")
saving:
5.4 ms ± 470 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
725 µs ± 55.8 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
loading:
3.16 ms ± 38.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.9 ms ± 15.2 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

Example

If the system has been continuized (see pooltool.evolution.continuize()), disk space and save/load times can be decreased by using drop_continuized_history:

>>> import pooltool as pt
>>> system = pt.System.example()
>>> # simulate and continuize the results
>>> pt.simulate(system, continuous=True, inplace=True)
>>> print("saving")
>>> %timeit system.save("no_drop.json")
>>> %timeit system.save("drop.json", drop_continuized_history=True)
>>> print("loading")
>>> %timeit pt.System.load("no_drop.json")
>>> %timeit pt.System.load("drop.json")
saving
36 ms ± 803 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
7.59 ms ± 342 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
loading
18.3 ms ± 1.15 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.14 ms ± 30.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
$ du -sh drop.json no_drop.json

68K drop.json

584K no_drop.json

However, the loaded system is no longer continuized. If you need it to be, call pooltool.evolution.continuize():

>>> loaded_system = pt.System.load("drop.json")
>>> assert loaded_system != system
>>> pt.continuize(loaded_system, inplace=True)
>>> assert loaded_system == system

See also

Load systems with load().

classmethod load(path: pooltool.serialize.serializers.Pathish) System[source]

Load a System from a file in a serialized format.

Supported file extensions:

  1. .json

  2. .msgpack

Parameters:

path : pooltool.serialize.serializers.Pathish

Either a pathlib.Path object or a string representing the file path. The extension should match the supported filetypes mentioned above.

Returns:

The deserialized System object loaded from the file.

Return type:

System

Raises:

Examples:

Please refer to the examples in save().

See also

Save systems with save().

classmethod example() System[source]

A simple example system

This system features 2 balls (IDs “1” and “cue”) on a pocket table. The cue stick parameters are set to pocket the “1” ball in the top-left pocket.

Example

The system can be constructed and introspected like so:

>>> import pooltool as pt
>>> system = pt.System.example()
>>> system.balls["cue"].xyz
array([0.4953  , 0.9906  , 0.028575])
>>> system.balls["1"].xyz
array([0.4953  , 1.4859  , 0.028575])
>>> system.cue
<Cue object at 0x7f7a2641ce40>
 ├── V0    : 1.5
 ├── phi   : 95.07668213305062
 ├── a     : 0.0
 ├── b     : -0.3
 └── theta : 0.0

It can be simulated and visualized:

>>> pt.simulate(system, inplace=True)
>>> pt.show(system)
Return type:

System

Functions

continuize(system: System, dt: float = 0.01, inplace: bool = False) System[source]

Create a BallHistory for each ball with many timepoints

When pooltool simulates a shot, it evolves the system using an event-based shot evolution algorithm. This means pooltool only timestamps the ball states during events--not between events. This makes simulation fast, but provides insufficient trajectory information if you wanted to visualize or plot ball trajectories over time.

Continuizing the shot means tracking the ball states with higher temporal resolution, so that the ball trajectories between events can be recapitulated. It’s a misnomer because the states are still tracked over discrete time steps dt seconds apart. i.e. not continuous.

This function calculates the “continous” timestamps for each ball and stores them in pooltool.objects.Ball.history_cts (the event-based timestamps are preserved, and are stored in pooltool.objects.Ball.history)

The continous timepoints are shared between all balls and are uniformly spaced (except for the last timepoint, which occurs at the final event, which necessarily occurs less than dt after the second last timepoint).

Parameters:
  • dt : float

    This is the spacing between each timepoint. 0.01 looks visually accurate at 60fps at a playback speed of 1. Function runtime is inversely proportional to dt.

  • inplace : bool

    By default, a copy of the passed system is continuized and returned. This leaves the passed system unmodified. If inplace is set to True, the passed system is modified in place, meaning no copy is made and the returned system is the passed system. For a more practical distinction, see Examples below.

Return type:

System

Examples

Standard usage:

>>> import pooltool as pt
>>> system = pt.simulate(pt.System.example())

The system has been simulated, so their history attributes are populated:

>>> len(system.balls["cue"].history)
14
>>> system.balls["cue"].history[0]
BallState(rvw=array([[0.4953  , 0.9906  , 0.028575],
       [0.      , 0.      , 0.      ],
       [0.      , 0.      , 0.      ]]), s=0, t=0.0)
>>> system.balls["cue"].history[-1]
BallState(rvw=array([[0.7464286761774921, 1.247940272192023 , 0.028575          ],
       [0.                , 0.                , 0.                ],
       [0.                , 0.                , 0.                ]]), s=0, t=5.193035203405666)

However, the system has not been continuized, so their history_cts attributes are empty:

>>> len(system.balls["cue"].history_cts)
0

After continuizing, the continuous ball histories are populated with many timestamps:

>>> continuized_system = pt.continuize(system, inplace=False)
>>> continuized_system.continuized
True
>>> len(continuized_system.balls["cue"].history_cts)
523

You can also modify the system in place:

>>> import pooltool as pt
>>> system = pt.simulate(pt.System.example())
>>> continuized_system = pt.continuize(system, inplace=True)
>>> assert system.continuized
>>> assert continuized_system.continuized
>>> assert system is continuized_system

Notice that the returned system is the continuized system. Therefore, there is no point catching the return object when inplace is True:

>>> import pooltool as pt
>>> system = pt.simulate(pt.System.example())
>>> assert not system.continuized
>>> pt.continuize(system, inplace=True)
>>> assert system.continuized
interpolate_ball_states(ball: Ball, timestamps: NDArray[float64] | Sequence[float], *, extrapolate: bool = False) list[BallState][source]

Calculate exact ball states at arbitrary timestamps.

This function calculates the precise ball states at arbitrary timestamps by evolving the ball from the nearest preceding event state using the same physics model as the simulation. It provides physically accurate positions, velocities, and angular velocities according to the ball’s motion equations.

Parameters:
  • ball : Ball

    The Ball object containing the history and physical parameters.

  • timestamps : NDArray[float64] | Sequence[float]

    A sequence or numpy array of timestamps at which to calculate ball states. Should be in ascending order and within the history’s time range.

  • extrapolate : bool

    If True, timestamps outside the history’s time range will use the nearest boundary state (initial or final). If False (default), a ValueError is raised for timestamps outside the range.

Returns:

A list of BallState objects corresponding to the given timestamps.

Raises:

ValueError -- If history is empty or if timestamps are out of range and extrapolate is False.

Return type:

list[BallState]

Examples

>>> import pooltool as pt
>>> import numpy as np
>>> system = pt.simulate(pt.System.example())
>>> ball = system.balls["cue"]
>>> # Get ball states at specific timestamps
>>> timestamps = np.array([0.5, 1.0, 1.5])
>>> states = pt.interpolate_ball_states(ball, timestamps)
>>> # Use the states
>>> states[0].rvw[0]  # Position at t=0.5
array([x, y, z])
simulate(shot: System, engine: PhysicsEngine | None = None, inplace: bool = False, continuous: bool = False, dt: float | None = None, t_final: float | None = None, include: set[EventType] = INCLUDED_EVENTS, max_events: int = 0) System[source]

Run a simulation on a system and return it

Parameters:
  • shot : System

    The system you would like simulated. The system should already have energy, otherwise there will be nothing to simulate.

  • engine : PhysicsEngine | None

    The engine holds all of the physics. You can instantiate your very own pooltool.physics.PhysicsEngine object, or you can modify ~/.config/pooltool/physics/resolver.json to change the default engine.

  • inplace : bool

    By default, a copy of the passed system is simulated and returned. This leaves the passed system unmodified. If inplace is set to True, the passed system is modified in place, meaning no copy is made and the returned system is the passed system. For a more practical distinction, see Examples below.

  • continuous : bool

    If True, the system will not only be simulated, but it will also be “continuized”. This means each ball will be populated with a ball history with small fixed timesteps that make it ready for visualization.

  • dt : float | None

    The small fixed timestep used when continuous is True.

  • t_final : float | None

    If set, the simulation will end prematurely after the calculation of an event with event.time > t_final.

  • include : set[EventType]

    Which EventType are you interested in resolving? By default, all detected events are resolved.

  • max_events : int

    If this is greater than 0, and the shot has more than this many events, the simulation is stopped and the balls are set to stationary.

Returns:

The simulated system.

Return type:

System

Examples

Standard usage:

>>> # Simulate a system
>>> import pooltool as pt
>>> system = pt.System.example()
>>> simulated_system = pt.simulate(system)
>>> assert not system.simulated
>>> assert simulated_system.simulated

The returned system is simulated, but the passed system remains unchanged.

You can also modify the system in place:

>>> # Simulate a system in place
>>> import pooltool as pt
>>> system = pt.System.example()
>>> simulated_system = pt.simulate(system, inplace=True)
>>> assert system.simulated
>>> assert simulated_system.simulated
>>> assert system is simulated_system

Notice that the returned system _is_ the simulated system. Therefore, there is no point catching the return object when inplace is True:

>>> # Simulate a system in place
>>> import pooltool as pt
>>> system = pt.System.example()
>>> assert not system.simulated
>>> pt.simulate(system, inplace=True)
>>> assert system.simulated

You can continuize the ball trajectories with continuous

>>> # Simulate a system in place
>>> import pooltool as pt
>>> system = pt.simulate(pt.System.example(), continuous=True)
>>> for ball in system.balls.values(): assert len(ball.history_cts) > 0
show(*args, **kwargs)[source]

Opens the interactive interface for one or more shots.

Important

For instructions on how to use the interactive interface, see The Interface.

Parameters:
  • shot_or_shots

    The shot or collection of shots to visualize. This can be a single pooltool.system.System object or a pooltool.system.MultiSystem object containing multiple systems.

    Note

    If a multisystem is passed, the systems can be scrolled through by pressing n (next) and p (previous). When using show(), press Enter to toggle parallel visualization mode where all systems play simultaneously with reduced opacity except the active one. In parallel mode, use n and p to change which system has full opacity. Note that parallel visualization is only available in show() and not when playing the game through the CLI run-pooltool.

  • title

    The title to display in the visualization. Defaults to an empty string.

  • camera_state

    The initial camera state that the visualization is rendered with.

Example

This example visualizes a single shot.

>>> import pooltool as pt
>>> system = pt.System.example()

Make sure the shot is simulated, otherwise it will make for a boring visualization:

>>> pt.simulate(system, inplace=True)

Now visualize the shot:

>>> pt.show(system)

(Press escape to exit the interface and continue script execution)

generate_layout(blueprint: list[BallPos], table: Table, ballset: BallSet | None = None, ball_params: BallParams | None = None, spacing_factor: float = 0.001, seed: int | None = None) dict[str, Ball][source]

Generate Ball objects based on a given blueprint and table dimensions.

The function calculates the absolute position of each ball on the table using the translations provided in the blueprint relative to table anchors. It then randomly assigns ball IDs to each position, ensuring no ball ID is used more than once.

Parameters:
  • blueprint : list[BallPos]

    A list of ball positions represented as BallPos objects, which describe their location relative to table anchors or other positions.

  • table : Table

    A Table. This must exist so the rack can be created with respect to the table’s dimensions.

  • ball_params : BallParams | None

    A BallParams object, which all balls will be created with. This contains info like ball radius.

  • spacing_factor : float

    This factor adjusts the spacing between balls to ensure they do not touch each other directly. Instead of being in direct contact, each ball is allocated within a larger, virtual radius defined as (1 + spacing_factor) * R, where R represents the actual radius of the ball. Within this expanded radius, the ball’s position is determined randomly, allowing for a controlled separation between each ball. The spacing_factor therefore dictates the degree of this separation, with higher values resulting in greater distances between adjacent balls. Setting this to 0 is not recommended.

  • seed : int | None

    Set a seed for reproducibility. That’s because getting a rack involves two random procedures. First, some ball positions can be satisfied with many different ball IDs. For example, in 9 ball, only the 1 ball and 9 ball are predetermined, the positions of the other balls are random. The second source of randomnness is from spacing_factor.

Returns:

A dictionary mapping ball IDs to their respective Ball objects, with their absolute positions on the table.

Return type:

Dict[str, Ball]

Notes

  • The table dimensions are normalized such that the bottom-left corner is (0.0, 0.0) and the top-right corner is (1.0, 1.0).

get_rack(game_type: GameType, table: Table, ball_params: BallParams | None = None, ballset: BallSet | None = None, spacing_factor: float = 0.001) dict[str, Ball][source]

Generate a ball rack.

This function ultimately delegates to pooltool.layouts.generate_layout().

Parameters:
  • game_type : GameType

    The game type being played. This will determine what rack is returned.

  • table : Table

    A table. This must exist so the rack can be created with respect to the table’s dimensions.

  • ball_params : BallParams | None

    Ball parameters that all balls will be created with.

  • spacing_factor : float

    This factor adjusts the spacing between balls to ensure they do not touch each other directly. Instead of being in direct contact, each ball is allocated within a larger, virtual radius defined as (1 + spacing_factor) * R, where R represents the actual radius of the ball. Within this expanded radius, the ball’s position is determined randomly, allowing for a controlled separation between each ball. The spacing_factor therefore dictates the degree of this separation, with higher values resulting in greater distances between adjacent balls. Setting this to 0 is not recommended.

Returns:

A dictionary mapping ball IDs to their respective Ball objects, with their absolute positions on the table.

Return type:

Dict[str, Ball]

get_ruleset(game: GameType, enforce_rules: bool = True) type[Ruleset][source]

Retrieve a ruleset class

Parameters:
  • game : GameType

    The game type.

  • enforce_rules : bool

    Whether to enforce game rules. If False, returns ruleless mode.

Returns:

An uninitialized class object representing a game.

Return type:

Type[Ruleset]