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 source location: >>> from pooltool.system.datatypes import System >>> system = System.example() If the object you're looking for isn't in this top-level API, **search for it in the 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 utils/index.rst Submodules ---------- .. toctree:: :titlesonly: :maxdepth: 1 constants/index.rst 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:`ShotViewer ` - An interface for viewing shots from within python. * - :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:`simulate `\ (shot, engine, inplace, continuous, dt, t_final, quartic_solver, include, max_events) - Run a simulation on a system and return it * - :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) - 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 .. autoclass:: GameType Bases: :py:obj:`pooltool.utils.strenum.StrEnum` .. autoclass:: Game(config=ShowBaseConfig.default()) Bases: :py:obj:`Interface` .. rubric:: Methods: .. py:method:: enter_game() Close the menu, setup the visualization, and start the game .. py:method:: create_system() Create the multisystem and game objects FIXME This is where menu options for game type and further specifications should plug into. .. autoclass:: ShotViewer(config=ShowBaseConfig.default()) Bases: :py:obj:`Interface` .. rubric:: Methods: .. py:method:: show(shot_or_shots: Union[pooltool.system.datatypes.System, pooltool.system.datatypes.MultiSystem], title: str = '', camera_state: Optional[pooltool.ani.camera.CameraState] = None) 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.datatypes.System` object or a :class:`pooltool.system.datatypes.MultiSystem` object containing multiple systems. .. note:: If a multisystem is passed, the systems can be scrolled through by pressing *n* (next) and *p* (previous). :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) Create a :class:`ShotViewer` object: >>> gui = pt.ShotViewer() Now visualize the shot: >>> gui.show(system) (Press *escape* to exit the interface and continue script execution) .. admonition:: Example This example explains the order in which events and script execution happens. .. code-block:: python import pooltool as pt system = pt.System.example() pt.simulate(system, inplace=True) # This line takes a view seconds to execute. It will generate a visible # window. Once the window has been generated, script execution continues gui = pt.ShotViewer() # When this line is called, the window is populated with an animated # scene of the shot. gui.show(system) # This line will not execute until is pressed while the window is # active. print('script continues') # For subsequent calls to `show`, you must use the same `ShotViewer` # object: gui.show(system) .. 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 :mod:`pooltool.objects.ball.sets` for details about ball sets. - See :meth:`pooltool.system.datatypes.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:`BallHistory` objects. .. py:method:: create(id: str, *, xy: Optional[Sequence[float]] = None, ballset: Optional[pooltool.objects.ball.sets.BallSet] = 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.balls.params.BallParams` and :class:`BallState`. :param xy: The x and y coordinates of the ball position. :param ballset: A ballset. :param \*\*kwargs: Arguments accepted by :class:`pooltool.objects.balls.params.BallParams` .. 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:`PrebuiltBallParams` member. All prebuilt ball parameters are named with the :class:`PrebuiltBallParams` Enum. This constructor takes a prebuilt name and returns the corresponding ball parameters. .. seealso:: - :class:`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: Optional[float] = None, phi: Optional[float] = None, theta: Optional[float] = None, a: Optional[float] = None, b: Optional[float] = None, cue_ball_id: Optional[str] = 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. .. rubric:: Methods: .. 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.table.specs.PocketTableSpecs` - :class:`pooltool.objects.table.specs.BilliardTableSpecs` - :class:`pooltool.objects.table.specs.SnookerTableSpecs` :returns: A table matching the specifications of the input. - :class:`pooltool.objects.table.specs.PocketTableSpecs` has :attr:`table_type` set to `pooltool.objects.table.specs.TableType.POCKET` - :class:`pooltool.objects.table.specs.BilliardTableSpecs` has :attr:`table_type` set to `pooltool.objects.table.specs.TableType.BILLIARD` - :class:`pooltool.objects.table.specs.SnookerTableSpecs` has :attr:`table_type` set to `pooltool.objects.table.specs.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 .. autoclass:: MultiSystem .. rubric:: Methods: .. py:method:: append(system: System) -> None Append a system to the multisystem This appends ``system`` to :attr:`multisystem`. .. 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:`System.save` and :meth:`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:`System.save` and :meth:`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.datatypes.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.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. :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 :mod:`pooltool.objects.ball.sets` for details about ball sets. - See :meth:`pooltool.objects.ball.datatypes.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 ``self.balls``, (1) :attr:`pooltool.objects.ball.datatypes.Ball.history` is set to ``BallHistory()`` (2) :attr:`pooltool.objects.ball.datatypes.Ball.history_cts` is set to ``BallHistory()`` (3) The ``t`` attribute of :attr:`pooltool.objects.ball.datatypes.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 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 .. 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 merely an alias for :meth:`pooltool.objects.cue.datatypes.Cue.set_state` :param kwargs: **kwargs Cue stick parameters. .. seealso:: :meth:`pooltool.objects.cue.datatypes.Cue.set_state` .. py:method:: get_system_energy() -> float Calculate the energy of the system in Joules .. py:method:: randomize_positions(ball_ids: Optional[List[str]] = None, niter=100) -> bool 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. :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.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). .. 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 the events of the shot, 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.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.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) >>> gui = pt.ShotViewer() >>> gui.show(system) Functions --------- .. py:function:: simulate(shot: pooltool.system.datatypes.System, engine: Optional[pooltool.physics.engine.PhysicsEngine] = None, inplace: bool = False, continuous: bool = False, dt: Optional[float] = None, t_final: Optional[float] = 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.engine.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.continuize` .. py:function:: generate_layout(blueprint: List[BallPos], table: pooltool.objects.table.datatypes.Table, ballset: Optional[pooltool.objects.ball.sets.BallSet] = None, ball_params: Optional[pooltool.objects.ball.datatypes.BallParams] = None, spacing_factor: float = 0.001, seed: Optional[int] = 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: Optional[pooltool.objects.ball.datatypes.BallParams] = None, ballset: Optional[pooltool.objects.ball.sets.BallSet] = None, spacing_factor: float = 0.001) -> Dict[str, pooltool.objects.ball.datatypes.Ball] Generate a ball rack. This function ultimately delegates to :func:`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) -> Type[datatypes.Ruleset] Retrieve a ruleset class :param game: The game type. :returns: An uninitialized class object representing a game. :rtype: Type[Ruleset]