``pooltool.evolution`` ====================== .. py:module:: pooltool.evolution Shot evolution algorithm routines and utilities 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, 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 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`