Skip to content

Commit

Permalink
Update examples (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
ekiefl authored Jan 4, 2025
1 parent 3d8fd67 commit 29572ba
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 31 deletions.
57 changes: 27 additions & 30 deletions docs/examples/30_degree_rule.pct.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
#
# We'll start with a table. Since we don't want collisions with cushions to interfere with our trajectory, let's make an unrealistically large $10\text{m} \times 10\text{m}$ [Table](../autoapi/pooltool/index.rst#pooltool.Table).

# %% trusted=true
# %%
import pooltool as pt

table_specs = pt.objects.BilliardTableSpecs(l=10, w=10)
Expand All @@ -85,20 +85,20 @@
# %% [markdown]
# Next, we'll create two [Ball](../autoapi/pooltool/index.rst#pooltool.Ball) objects.

# %% trusted=true
# %%
cue_ball = pt.Ball.create("cue", xy=(2.5, 1.5))
obj_ball = pt.Ball.create("obj", xy=(2.5, 3.0))

# %% [markdown]
# Next, we'll need a [Cue](../autoapi/pooltool/index.rst#pooltool.Cue).

# %% trusted=true
# %%
cue = pt.Cue(cue_ball_id="cue")

# %% [markdown]
# Finally, we'll need to wrap these objects up into a [System](../autoapi/pooltool/index.rst#pooltool.System). We'll call this our system *template*, with the intention of reusing it for many different shots.

# %% trusted=true
# %%
system_template = pt.System(
table=table,
cue=cue,
Expand All @@ -112,7 +112,7 @@
#
# So in the function call below, `pt.aim.at_ball(system, "obj", cut=30)` returns the angle `phi` that the cue ball should be directed at such that a cut angle of 30 degrees with the object ball is achieved.

# %% trusted=true
# %%
# Creates a deep copy of the template
system = system_template.copy()

Expand All @@ -122,7 +122,7 @@
# %% [markdown]
# Now, we [simulate](../autoapi/pooltool/index.rst#pooltool.simulate) the shot and then [continuize](../autoapi/pooltool/evolution/continuize/index.html#pooltool.evolution.continuize.continuize) it to store ball state data (like coordinates) in $10\text{ms}$ timestep intervals.

# %% trusted=true
# %%
# Create a default physics engine and overwrite ball-ball model with frictionless, elastic model.
engine = pt.physics.PhysicsEngine()
engine.resolver.ball_ball = pt.physics.get_ball_ball_model(pt.physics.BallBallModel.FRICTIONLESS_ELASTIC)
Expand All @@ -144,7 +144,7 @@
#
# Since that can't be embedded into the documentation, we'll instead plot the trajectory of the cue ball and object ball by accessing ther historical states.

# %% trusted=true
# %%
cue_ball = system.balls["cue"]
obj_ball = system.balls["obj"]
cue_history = cue_ball.history_cts
Expand All @@ -154,7 +154,7 @@
# %% [markdown]
# The [BallHistory](../autoapi/pooltool/objects/index.rst#pooltool.objects.BallHistory) holds the ball's historical states, each stored as a [BallState](../autoapi/pooltool/objects/index.rst#pooltool.objects.BallState) object. Each attribute of the ball states can be concatenated into numpy arrays with the [BallHistory.vectorize](../autoapi/pooltool/objects/index.rst#pooltool.objects.BallHistory.vectorize) method.

# %% trusted=true
# %%
rvw_cue, s_cue, t_cue = cue_history.vectorize()
rvw_obj, s_obj, t_obj = obj_history.vectorize()

Expand All @@ -165,12 +165,12 @@
# %% [markdown]
# We can grab the xy-coordinates from the `rvw` array by with the following.

# %% trusted=true
# %%
coords_cue = rvw_cue[:, 0, :2]
coords_obj = rvw_obj[:, 0, :2]
coords_cue.shape

# %% trusted=true editable=true slideshow={"slide_type": ""} tags=[]
# %% editable=true slideshow={"slide_type": ""} tags=[]
import plotly.graph_objects as go
import plotly.io as pio

Expand Down Expand Up @@ -203,28 +203,28 @@
#
# As mentioned before, the carom angle is the angle between the cue ball velocity right before collision, and the cue ball velocity post-collision, once the ball has stopped sliding on the cloth. Hidden somewhere in the system **event list** one can find the events corresponding to these precise moments in time:

# %% trusted=true
# %%
system.events[:6]

# %% [markdown]
# Programatically, we can pick out these two events of interest with event selection syntax.
#
# Since there is only one ball-ball collision, it's easy to select with [filter_type](../autoapi/pooltool/events/index.rst#pooltool.events.filter_type):

# %% trusted=true
# %%
collision = pt.events.filter_type(system.events, pt.EventType.BALL_BALL)[0]
collision

# %% [markdown]
# To get the event when the cue ball stops sliding, we can similarly try filtering by the sliding to rolling transition event:

# %% trusted=true
# %%
pt.events.filter_type(system.events, pt.EventType.SLIDING_ROLLING)

# %% [markdown]
# But there are many sliding to rolling transition events, and to make matters worse, they are shared by both the cue ball and the object ball. What we need is the **first** **sliding to rolling** transition that the **cue ball** undergoes **after** the **ball-ball** collision. We can achieve this multi-criteria query with [filter_events](../autoapi/pooltool/events/index.rst#pooltool.events.filter_events):

# %% trusted=true
# %%
transition = pt.events.filter_events(
system.events,
pt.events.by_time(t=collision.time, after=True),
Expand All @@ -236,16 +236,13 @@
# %% [markdown]
# Now, we can dive into these two events and pull out the cue ball velocities we need to calculate the carom angle.

# %% trusted=true
# %%
# Velocity prior to impact
for agent in collision.agents:
if agent.id == "cue":
# agent.initial is a copy of the Ball before resolving the collision
velocity_initial = agent.initial.state.rvw[1, :2]
velocity_initial = collision.get_ball("cue", initial=True).vel[:2]

# Velocity post sliding
# We choose `final` here for posterity, but the velocity is the same both before and after resolving the transition.
velocity_final = transition.agents[0].final.state.rvw[1, :2]
# We choose the "final" here for posterity, but the velocity is the same both before and after resolving the transition.
velocity_final = transition.get_ball("cue", initial=False).vel[:2]

carom_angle = pt.ptmath.utils.angle_between_vectors(velocity_final, velocity_initial)

Expand All @@ -259,7 +256,7 @@
# We calculated the carom angle for a single cut angle, 30 degrees. Let's write a function called `get_carom_angle` so we can do that repeatedly for different cut angles.


# %% trusted=true
# %%
def get_carom_angle(system: pt.System) -> float:
assert system.simulated

Expand All @@ -283,7 +280,7 @@ def get_carom_angle(system: pt.System) -> float:
# `get_carom_angle` assumes the passed system has already been simulated, so we'll need another function to take care of that. We'll cue stick speed and cut angle as parameters.


# %% trusted=true
# %%
def simulate_experiment(V0: float, cut_angle: float) -> pt.System:
system = system_template.copy()
phi = pt.aim.at_ball(system, "obj", cut=cut_angle)
Expand All @@ -296,7 +293,7 @@ def simulate_experiment(V0: float, cut_angle: float) -> pt.System:
# We'll also want the ball hit fraction:


# %% trusted=true
# %%
import numpy as np

def get_ball_hit_fraction(cut_angle: float) -> float:
Expand All @@ -306,7 +303,7 @@ def get_ball_hit_fraction(cut_angle: float) -> float:
# %% [markdown]
# With these functions, we are ready to simulate how carom angle varies as a function of cut angle.

# %% trusted=true
# %%
import pandas as pd

data = {
Expand All @@ -329,7 +326,7 @@ def get_ball_hit_fraction(cut_angle: float) -> float:
# %% [markdown]
# From this dataframe we can make some plots. On top of the ball-hit fraction, plot, I'll create a box between a $1/4$ ball hit and a $3/4$ ball hit, since this is the carom angle range that the 30-degree rule is defined with respect to.

# %% trusted=true editable=true slideshow={"slide_type": ""} tags=["nbsphinx-thumbnail"]
# %% editable=true slideshow={"slide_type": ""} tags=["nbsphinx-thumbnail"]
import matplotlib.pyplot as plt

x_min = 0.25
Expand All @@ -354,7 +351,7 @@ def get_ball_hit_fraction(cut_angle: float) -> float:
#
# For your reference, here is the same plot but with cut angle $\phi$ as the x-axis:

# %% trusted=true
# %%
fig, ax = plt.subplots()
ax.scatter(frame['phi'], frame['theta'], color='#1f77b4')
ax.set_title('Carom Angle vs Cut Angle', fontsize=20)
Expand All @@ -378,7 +375,7 @@ def get_ball_hit_fraction(cut_angle: float) -> float:
# Since pooltool's baseline physics engine makes the same assumptions, we should expect the results to be the same. Let's directly compare:


# %% trusted=true
# %%
def get_theoretical_carom_angle(phi) -> float:
return np.atan2(np.sin(phi) * np.cos(phi), (np.sin(phi) ** 2 + 2 / 5))

Expand Down Expand Up @@ -425,7 +422,7 @@ def get_theoretical_carom_angle(phi) -> float:
#
# Interestingly, the carom angle is independent of the speed:

# %% trusted=true
# %%
for V0 in np.linspace(1, 4, 20):
system = simulate_experiment(V0, 30)
carom_angle = get_carom_angle(system)
Expand All @@ -434,7 +431,7 @@ def get_theoretical_carom_angle(phi) -> float:
# %% [markdown]
# This doesn't mean that the trajectories are the same though. Here are the trajectories:

# %% trusted=true
# %%
import numpy as np
import plotly.graph_objects as go

Expand Down
2 changes: 1 addition & 1 deletion docs/examples/straight_shot.pct.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def create_system(d, D):
obj_ball = create_object_ball(cue_ball, d)

return pt.System(
cue=pt.Cue.default(),
cue=pt.Cue(cue_ball_id="CB"),
balls=(cue_ball, obj_ball),
table=table,
)
Expand Down

0 comments on commit 29572ba

Please sign in to comment.