pooltool.system

The system container and its associated objects

Overview

Classes

MultiSystem

A storage for System objects

System

A class representing the billiards system.

Classes

class pooltool.system.MultiSystem(multisystem: List[System] = UNKNOWN)[source]

A storage for System objects

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

multisystem

A list of System objects (default = [])

Type:

List[pooltool.system.datatypes.System]

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:

>>> gui = pt.ShotViewer()
>>> gui.show(multisystem, title="Press 'n' for next, 'p' for previous")

Methods:

append(system: System) None[source]

Append a system to the multisystem

This appends system to multisystem.

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 pooltool.system.System(cue: Cue, table: Table, balls: Dict[str, Ball], t: float = 0.0, events: List[Event] = UNKNOWN)[source]

A class representing the billiards system.

This class holds:

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

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

  3. a table (pooltool.objects.table.datatypes.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.datatypes.Event objects is stored in this class.

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

cue

A cue stick.

Type:

pooltool.objects.cue.datatypes.Cue

table

A table.

Type:

pooltool.objects.table.datatypes.Table

balls

A dictionary of balls.

Warning

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

Type:

Dict[str, pooltool.objects.ball.datatypes.Ball]

t

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.

Type:

float

events

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

Type:

List[pooltool.events.datatypes.Event]

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:

>>> gui = pt.ShotViewer()
>>> gui.show(system)
property continuized

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.datatypes.Ball.history_cts.

property simulated

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: pooltool.objects.ball.sets.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.ball.sets.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 (pooltool.objects.ball.sets.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()[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 self.balls,

(1) pooltool.objects.ball.datatypes.Ball.history is set to BallHistory() (2) pooltool.objects.ball.datatypes.Ball.history_cts is set to BallHistory() (3) The t attribute of pooltool.objects.ball.datatypes.Ball.state is set to 0.0

Calling this method thus erases any history.

reset_balls()[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 ball’s 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()[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 merely an alias for pooltool.objects.cue.datatypes.Cue.set_state()

Parameters:

kwargs -- **kwargs Cue stick parameters.

get_system_energy() float[source]

Calculate the energy of the system in Joules

Return type:

float

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

Randomize ball positions on the table--ensure no overlap

This “algorithm” 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. This is an inefficient algorithm, in case that needs to be said.

Parameters:
  • ball_ids (Optional[List[str]]) -- 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.datatypes.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 for each ball can be easily regenerated (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 the events of the shot, 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.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.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)
>>> gui = pt.ShotViewer()
>>> gui.show(system)
Return type:

System