pooltool ======== .. py:module:: 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 ----------- .. toctree:: :titlesonly: :maxdepth: 1 events/index.rst evolution/index.rst game/index.rst objects/index.rst physics/index.rst ptmath/index.rst ruleset/index.rst system/index.rst Submodules ---------- .. toctree:: :titlesonly: :maxdepth: 1 interact/index.rst layouts/index.rst Overview -------- .. list-table:: Classes :header-rows: 0 :widths: auto :class: summarytable * - :py:obj:`EventType ` - An Enum of event types * - :py:obj:`GameType ` - An Enum for supported game types * - :py:obj:`Game ` - This class runs the pooltool application * - :py:obj:`Ball ` - A billiards ball. * - :py:obj:`BallParams ` - Ball parameters and physical constants * - :py:obj:`Cue ` - A cue stick. * - :py:obj:`Table ` - A table. * - :py:obj:`TableType ` - An Enum describing the table type. * - :py:obj:`Player ` - A player * - :py:obj:`MultiSystem ` - A storage for System objects * - :py:obj:`System ` - A class representing the billiards system. .. list-table:: Function :header-rows: 0 :widths: auto :class: summarytable * - :py:obj:`continuize `\ (system, dt, inplace) - Create a ``BallHistory`` for each ball with many timepoints * - :py:obj:`interpolate_ball_states `\ (ball, timestamps, \*None, extrapolate) - Calculate exact ball states at arbitrary timestamps. * - :py:obj:`simulate `\ (shot, engine, inplace, continuous, dt, t_final, quartic_solver, include, max_events) - Run a simulation on a system and return it * - :py:obj:`show `\ (\*args, \*\*kwargs) - Opens the interactive interface for one or more shots. * - :py:obj:`generate_layout `\ (blueprint, table, ballset, ball_params, spacing_factor, seed) - Generate Ball objects based on a given blueprint and table dimensions. * - :py:obj:`get_rack `\ (game_type, table, ball_params, ballset, spacing_factor) - Generate a ball rack. * - :py:obj:`get_ruleset `\ (game, enforce_rules) - Retrieve a ruleset class Classes ------- .. autoclass:: EventType Bases: :py:obj:`pooltool.utils.strenum.StrEnum` .. rubric:: Methods: .. py:method:: is_collision() -> bool Returns whether the member is a collision .. py:method:: is_transition() -> bool Returns whether the member is a transition .. py:method:: has_ball() -> bool Returns True if this event type can involve a Ball. .. py:method:: has_cushion() -> bool Returns True if this event type can involve a cushion (linear or circular). .. py:method:: has_pocket() -> bool Returns True if this event type can involve a Pocket. .. py:method:: has_stick() -> bool Returns True if this event type can involve a CueStick. .. autoclass:: GameType Bases: :py:obj:`pooltool.utils.strenum.StrEnum` .. autoclass:: Game(config=ShowBaseConfig.default()) Bases: :py:obj:`Interface` .. rubric:: Methods: .. py:method:: attach_system(system: pooltool.system.datatypes.System) -> None .. py:method:: attach_ruleset(ruleset: pooltool.ruleset.datatypes.Ruleset) -> None .. py:method:: start() .. autoclass:: Ball .. py:property:: xyz The displacement (from origin) vector of the ball. A shortcut for ``self.state.rvw[0]``. .. py:property:: vel The velocity vector of the ball. A shortcut for ``self.state.rvw[1]``. .. py:property:: avel The angular velocity vector of the ball. A shortcut for ``self.state.rvw[2]``. .. rubric:: Methods: .. py:method:: set_ballset(ballset: pooltool.objects.ball.sets.BallSet) -> None Update the ballset :raises ValueError: If the ball ID doesn't match to a model name of the ballset. .. seealso:: - See :class:`pooltool.objects.BallSet` for details about ball sets. - See :meth:`pooltool.system.System.set_ballset` for setting the ballset for all the balls in a system. .. py:method:: copy(drop_history: bool = False) -> Ball Create a copy :param drop_history: If True, the returned copy :attr:`history` and :attr:`history_cts` attributes are both set to empty :class:`pooltool.objects.BallHistory` objects. .. py:method:: create(id: str, *, xy: collections.abc.Sequence[float] | None = None, ballset: pooltool.objects.ball.sets.BallSet | None = None, **kwargs) -> Ball :staticmethod: 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 :class:`pooltool.objects.BallParams` and :class:`pooltool.objects.BallState`. :param xy: The x and y coordinates of the ball position. :param ballset: A ballset. :param \*\*kwargs: Arguments accepted by :class:`pooltool.objects.BallParams` .. py:method:: dummy(id: str = 'dummy') -> Ball :staticmethod: .. autoclass:: BallParams .. py:property:: u_sp :type: float Coefficient of spinning friction This is equal to :attr:`u_sp_proportionality` * :attr:`R` .. cached_property_note:: .. rubric:: Methods: .. py:method:: copy() -> BallParams Return a copy .. note:: - Since the class is frozen and its attributes are immutable, this just returns ``self``. .. py:method:: default(game_type: pooltool.game.datatypes.GameType = GameType.EIGHTBALL) -> BallParams :classmethod: Return prebuilt ball parameters based on game type :param game_type: What type of game is being played? :returns: The prebuilt ball parameters associated with the passed game type. :rtype: BallParams .. py:method:: prebuilt(name: PrebuiltBallParams) -> BallParams :classmethod: Return prebuilt ball parameters based on name :param name: A :class:`pooltool.objects.PrebuiltBallParams` member. All prebuilt ball parameters are are members of the :class:`pooltool.objects.PrebuiltBallParams` Enum. This constructor takes a prebuilt name and returns the corresponding ball parameters. .. seealso:: - :class:`pooltool.objects.PrebuiltBallParams` .. autoclass:: Cue .. rubric:: Methods: .. py:method:: copy() -> Cue Create a copy .. note:: :attr:`specs` is shared between ``self`` and the copy, but that's ok because it's frozen and has no mutable attributes. .. py:method:: reset_state() -> None Resets :attr:`V0`, :attr:`phi`, :attr:`theta`, :attr:`a` and :attr:`b` to their defaults. .. py:method:: 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 Set the cueing parameters :param V0: See :attr:`V0` :param phi: See :attr:`phi` :param theta: See :attr:`theta` :param a: See :attr:`a` :param b: See :attr:`b` :param cue_ball_id: See :attr:`cue_ball_id` If any arguments are ``None``, they will be left untouched--they will not be set to None. .. py:method:: default() -> Cue :classmethod: Construct a cue with defaults .. autoclass:: Table .. py:property:: w :type: 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"``. .. py:property:: l :type: 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"``. .. py:property:: center :type: tuple[float, float] Return the 2D coordinates of the table's center .. warning:: This assumes :meth:`l` and :meth:`w` are defined. .. py:property:: has_linear_cushions :type: bool .. py:property:: has_circular_cushions :type: bool .. py:property:: has_pockets :type: bool .. rubric:: Methods: .. py:method:: set_cushion_height(height: float) -> None Set the height of all cushion segments. :param height: The new height to set for all cushion segments. .. py:method:: copy() -> Table Create a copy. .. py:method:: from_table_specs(specs: pooltool.objects.table.specs.TableSpecs) -> Table :staticmethod: Build a table from a table specifications object :param specs: A valid table specification. Accepted objects: - :class:`pooltool.objects.PocketTableSpecs` - :class:`pooltool.objects.BilliardTableSpecs` - :class:`pooltool.objects.SnookerTableSpecs` :returns: A table matching the specifications of the input. - :class:`pooltool.objects.PocketTableSpecs` has :attr:`table_type` set to `pooltool.objects.TableType.POCKET` - :class:`pooltool.objects.BilliardTableSpecs` has :attr:`table_type` set to `pooltool.objects.TableType.BILLIARD` - :class:`pooltool.objects.SnookerTableSpecs` has :attr:`table_type` set to `pooltool.objects.TableType.SNOOKER` :rtype: Table .. py:method:: prebuilt(name: pooltool.objects.table.collection.TableName) -> Table :classmethod: Create a default table based on name :param name: The name of the prebuilt table specs. :returns: A prebuilt table. :rtype: Table .. py:method:: default(table_type: pooltool.objects.table.specs.TableType = TableType.POCKET) -> Table :classmethod: Create a default table based on table type A default table is associated to each table type. :param table_type: The type of table. :returns: The default table for the given table type. :rtype: Table .. py:method:: from_game_type(game_type: pooltool.game.datatypes.GameType) -> Table :classmethod: Create a default table based on table type A default table is associated with each game type. :param game_type: The game type. :returns: The default table for the given game type. :rtype: Table .. autoclass:: TableType Bases: :py:obj:`pooltool.utils.strenum.StrEnum` .. autoclass:: Player .. py:property:: is_ai :type: bool .. autoclass:: MultiSystem .. py:property:: active :type: System .. py:property:: empty :type: bool .. py:property:: max_index .. rubric:: Methods: .. py:method:: reset() -> None .. py:method:: append(system: System) -> None Append a system to the multisystem This appends ``system`` to :attr:`multisystem`. .. py:method:: extend(systems: list[System]) -> None .. py:method:: set_active(i) -> None .. py:method:: save(path: pooltool.serialize.serializers.Pathish) -> None Save the multisystem to file in a serialized format. Supported file extensions: (1) ``.json`` (2) ``.msgpack`` :param path: Either a ``pathlib.Path`` object or a string. The extension should match the supported filetypes mentioned above. .. seealso:: - To load a multisystem, see :meth:`load`. - To save/load single systems, see :meth:`pooltool.system.System.save` and :meth:`pooltool.system.System.load` .. py:method:: load(path: pooltool.serialize.serializers.Pathish) -> MultiSystem :classmethod: Load a multisystem from a file in a serialized format. Supported file extensions: (1) ``.json`` (2) ``.msgpack`` :param path: 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. :rtype: MultiSystem .. seealso:: - To save a multisystem, see :meth:`save`. - To save/load single systems, see :meth:`pooltool.system.System.save` and :meth:`pooltool.system.System.load` .. autoclass:: System .. py: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. :rtype: bool .. seealso:: For a proper definition of *continuous history*, please see :attr:`pooltool.objects.Ball.history_cts`. .. py: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. :rtype: bool .. rubric:: Methods: .. py:method:: set_ballset(ballset: pooltool.objects.ball.sets.BallSet) -> None 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 :mod:`pooltool.layouts`) In this case, you need to manually associate a :class:`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. :param 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. .. seealso:: - See :class:`pooltool.objects.BallSet` for details about ball sets. - See :meth:`pooltool.objects.Ball.set_ballset` for setting the ballset of an individual ball. .. py:method:: reset_history() Resets the history for all balls, clearing events and resetting time. Operations that this method performs: (1) :attr:`t` is set to 0.0 (2) :attr:`events` is set to ``[]`` Additionally, for each ball in :attr:`balls`, (1) :attr:`pooltool.objects.Ball.history` is set to ``BallHistory()`` (2) :attr:`pooltool.objects.Ball.history_cts` is set to ``BallHistory()`` (3) The ``t`` attribute of :attr:`pooltool.objects.Ball.state` is set to ``0.0`` Calling this method thus erases any history. .. py:method:: reset_balls() 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. .. admonition:: 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 .. py:method:: stop_balls() 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) .. py:method:: strike(**kwargs) -> None Set cue stick parameters This is an alias for :meth:`pooltool.objects.Cue.set_state` :param kwargs: **kwargs Cue stick parameters. .. seealso:: :meth:`pooltool.objects.Cue.set_state` .. py:method:: get_system_energy() -> float Calculate the energy of the system in Joules. .. py:method:: randomize_positions(ball_ids: list[str] | None = None, niter=100) -> bool 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. :param ball_ids: Only these balls will be randomized. :param niter: The number of iterations tried until the algorithm gives up. :returns: True if all balls are non-overlapping. Returns False otherwise. :rtype: bool .. py:method:: is_balls_overlapping() -> bool Determines if any balls are overlapping. :returns: True if any balls overlap, False otherwise. :rtype: bool .. py:method:: copy() -> System 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. :rtype: System .. admonition:: 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 .. py:method:: save(path: pooltool.serialize.serializers.Pathish, drop_continuized_history: bool = False) -> None Save a System to file in a serialized format. Supported file extensions: (1) ``.json`` (2) ``.msgpack`` :param path: Either a ``pathlib.Path`` object or a string. The extension should match the supported filetypes mentioned above. :param drop_continuized_history: If True, :attr:`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). .. admonition:: 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 .. admonition:: 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) .. admonition:: Example If the system has been continuized (see :func:`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 :func:`pooltool.evolution.continuize`: >>> loaded_system = pt.System.load("drop.json") >>> assert loaded_system != system >>> pt.continuize(loaded_system, inplace=True) >>> assert loaded_system == system .. seealso:: Load systems with :meth:`load`. .. py:method:: load(path: pooltool.serialize.serializers.Pathish) -> System :classmethod: Load a System from a file in a serialized format. Supported file extensions: (1) ``.json`` (2) ``.msgpack`` :param path: 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. :rtype: System :raises AssertionError: If the file specified by `path` does not exist. :raises ValueError: If the file extension is not supported. Examples: Please refer to the examples in :meth:`save`. .. seealso:: Save systems with :meth:`save`. .. py:method:: example() -> System :classmethod: 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. .. admonition:: 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 ├── 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) Functions --------- .. py:function:: continuize(system: pooltool.system.datatypes.System, dt: float = 0.01, inplace: bool = False) -> pooltool.system.datatypes.System 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 :attr:`pooltool.objects.Ball.history_cts` (the event-based timestamps are preserved, and are stored in :attr:`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). :param dt: 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. :param inplace: 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. .. admonition:: 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 .. seealso:: - :attr:`pooltool.objects.Ball.history_cts` - :func:`pooltool.evolution.simulate` .. py:function:: interpolate_ball_states(ball: pooltool.objects.ball.datatypes.Ball, timestamps: numpy.typing.NDArray[numpy.float64] | collections.abc.Sequence[float], *, extrapolate: bool = False) -> list[pooltool.objects.ball.datatypes.BallState] 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. :param ball: The Ball object containing the history and physical parameters. :param timestamps: 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. :param extrapolate: 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. .. admonition:: 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]) .. py:function:: simulate(shot: pooltool.system.datatypes.System, engine: pooltool.physics.engine.PhysicsEngine | None = None, inplace: bool = False, continuous: bool = False, dt: float | None = None, t_final: float | None = None, quartic_solver: pooltool.ptmath.roots.quartic.QuarticSolver = QuarticSolver.HYBRID, include: set[pooltool.events.EventType] = INCLUDED_EVENTS, max_events: int = 0) -> pooltool.system.datatypes.System Run a simulation on a system and return it :param shot: The system you would like simulated. The system should already have energy, otherwise there will be nothing to simulate. :param engine: The engine holds all of the physics. You can instantiate your very own :class:`pooltool.physics.PhysicsEngine` object, or you can modify ``~/.config/pooltool/physics/resolver.json`` to change the default engine. :param inplace: 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. :param continuous: 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. :param dt: The small fixed timestep used when continuous is True. :param t_final: If set, the simulation will end prematurely after the calculation of an event with ``event.time > t_final``. :param quartic_solver: Which QuarticSolver do you want to use for solving quartic polynomials? :param include: Which EventType are you interested in resolving? By default, all detected events are resolved. :param max_events: 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. :rtype: System .. admonition:: 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 .. seealso:: - :func:`pooltool.evolution.continuize` .. py:function:: show(*args, **kwargs) Opens the interactive interface for one or more shots. .. important:: For instructions on how to use the interactive interface, see :doc:`The Interface `. :param shot_or_shots: The shot or collection of shots to visualize. This can be a single :class:`pooltool.system.System` object or a :class:`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 ``pt.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 ``pt.show()`` and not when playing the game through ``run-pooltool``. :param title: The title to display in the visualization. Defaults to an empty string. :param camera_state: The initial camera state that the visualization is rendered with. .. admonition:: 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) .. py:function:: generate_layout(blueprint: list[BallPos], table: pooltool.objects.table.datatypes.Table, ballset: pooltool.objects.ball.sets.BallSet | None = None, ball_params: pooltool.objects.ball.datatypes.BallParams | None = None, spacing_factor: float = 0.001, seed: int | None = None) -> dict[str, pooltool.objects.ball.datatypes.Ball] 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. :param blueprint: A list of ball positions represented as BallPos objects, which describe their location relative to table anchors or other positions. :param table: A Table. This must exist so the rack can be created with respect to the table's dimensions. :param ball_params: A BallParams object, which all balls will be created with. This contains info like ball radius. :param spacing_factor: 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. :param seed: 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. :rtype: Dict[str, Ball] .. admonition:: 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). .. py:function:: get_rack(game_type: pooltool.game.datatypes.GameType, table: pooltool.objects.table.datatypes.Table, ball_params: pooltool.objects.ball.datatypes.BallParams | None = None, ballset: pooltool.objects.ball.sets.BallSet | None = None, spacing_factor: float = 0.001) -> dict[str, pooltool.objects.ball.datatypes.Ball] Generate a ball rack. This function ultimately delegates to :func:`pooltool.layouts.generate_layout`. :param game_type: The game type being played. This will determine what rack is returned. :param table: A table. This must exist so the rack can be created with respect to the table's dimensions. :param ball_params: Ball parameters that all balls will be created with. :param spacing_factor: 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. :rtype: Dict[str, Ball] .. py:function:: get_ruleset(game: pooltool.game.datatypes.GameType, enforce_rules: bool = True) -> type[datatypes.Ruleset] Retrieve a ruleset class :param game: The game type. :param enforce_rules: Whether to enforce game rules. If False, returns ruleless mode. :returns: An uninitialized class object representing a game. :rtype: Type[Ruleset]