From 5c8eeedca5f8178597ccc84c3a7d9306cea3f078 Mon Sep 17 00:00:00 2001 From: Derek McBlane Date: Sun, 1 Feb 2026 17:36:46 -0500 Subject: [PATCH 1/4] add ball-ball collision plotting functions --- sandbox/ball_ball_collisions.py | 496 ++++++++++++++++++++++++++++++++ 1 file changed, 496 insertions(+) create mode 100644 sandbox/ball_ball_collisions.py diff --git a/sandbox/ball_ball_collisions.py b/sandbox/ball_ball_collisions.py new file mode 100644 index 00000000..b718bb16 --- /dev/null +++ b/sandbox/ball_ball_collisions.py @@ -0,0 +1,496 @@ +import math + +import attrs +import numpy as np +import plotly.graph_objects as go +import plotly.io as pio + +import pooltool.ptmath as ptmath +from pooltool.objects.ball.datatypes import Ball, BallParams +from pooltool.physics.resolve.ball_ball.core import BallBallCollisionStrategy + +pio.renderers.default = "browser" + + +def natural_roll_spin_rate(ball_speed: float, R: float): + return ball_speed / R + + +def gearing_sidespin(ball_speed: float, R: float, cut_angle: float): + speed_tangent = ball_speed * math.sin(cut_angle) + return -speed_tangent / R + + +def gearing_sidespin_factor(cut_angle: float): + return math.sin(cut_angle) + + +def cue_strike_spin_rate(impulse_offset: float, ball_speed: float, R: float): + """From impulse momentum equations""" + return 2.5 * ball_speed * impulse_offset / R**2 + + +def cue_strike_spin_rate_factor(impulse_offset: float, R: float) -> float: + """spin_rate / natural_roll""" + return 2.5 * impulse_offset / R + + +def cue_strike_spin_rate_factor_fractional_offset( + impulse_offset_fraction: float, +) -> float: + """spin_rate / natural_roll""" + return 2.5 * impulse_offset_fraction + + +def cue_strike_spin_rate_factor_percent_english( + percent_english: float, max_english_radius_fraction=0.5 +): + return cue_strike_spin_rate_factor_fractional_offset( + (percent_english / 100) * max_english_radius_fraction + ) + + +@attrs.define(frozen=True) +class BallBallCollisionExperimentConfig: + model: BallBallCollisionStrategy + params: BallParams + xy_line_of_centers_angle_radians: float = 0.0 + + +@attrs.define +class BallBallCollisionExperiment: + config: BallBallCollisionExperimentConfig + + ob_i: Ball = attrs.field(init=False) + + @ob_i.default + def __default_ob(self): + ob_i = Ball(id="object", params=self.config.params) + ob_i.state.rvw[0] = np.array([0, 0, self.config.params.R]) + return ob_i + + cb_i: Ball = attrs.field(init=False) + + @cb_i.default + def __default_cb(self): + cb_i = Ball(id="cue", params=self.config.params) + BallBallCollisionExperiment.place_ball_next_to( + cb_i, self.ob_i, self.config.xy_line_of_centers_angle_radians + math.pi + ) + return cb_i + + def setup( + self, + cb_speed: float, + cb_topspin: float = 0.0, + cb_sidespin: float = 0.0, + cut_angle: float = 0.0, + ): + BallBallCollisionExperiment.setup_ball_motion( + self.cb_i, + cb_speed, + self.config.xy_line_of_centers_angle_radians - cut_angle, + cb_topspin, + cb_sidespin, + ) + + def result(self): + return self.config.model.resolve(self.cb_i, self.ob_i, inplace=False) + + @staticmethod + def place_ball_next_to( + ball: Ball, other_ball: Ball, angle: float, separation: float = 0.0 + ): + ball.state.rvw[0] = other_ball.xyz + ptmath.coordinate_rotation( + np.array([other_ball.params.R + separation + ball.params.R, 0, 0]), angle + ) + + @staticmethod + def setup_ball_motion(ball: Ball, speed: float, direction, topspin, sidespin): + ball.state.rvw[1] = ptmath.coordinate_rotation( + np.array([speed, 0.0, 0.0]), direction + ) + ball.state.rvw[2] = ptmath.coordinate_rotation( + np.array([0.0, topspin, sidespin]), direction + ) + + +def collision_results_versus_cut_angle( + config: BallBallCollisionExperimentConfig, + cut_angles, + speeds, + topspin_factors=None, + sidespin_factors=None, +): + if topspin_factors is None: + topspin_factors = [0.0] + if sidespin_factors is None: + sidespin_factors = [0.0] + + n_cut_angles = np.size(cut_angles) + + results = {} + collision_experiment = BallBallCollisionExperiment(config) + + for speed in speeds: + natural_roll = natural_roll_spin_rate(speed, collision_experiment.cb_i.params.R) + for topspin_factor in topspin_factors: + topspin = topspin_factor * natural_roll + for sidespin_factor in sidespin_factors: + sidespin = sidespin_factor * natural_roll + vel = np.empty((n_cut_angles, 3)) + avel = np.empty((n_cut_angles, 3)) + induced_vel = np.empty((n_cut_angles, 3)) + induced_avel = np.empty((n_cut_angles, 3)) + throw_angles = np.empty(n_cut_angles) + for i, cut_angle in enumerate(cut_angles): + collision_experiment.setup(speed, topspin, sidespin, cut_angle) + vel[i] = collision_experiment.cb_i.vel + avel[i] = collision_experiment.cb_i.avel + + cb_f, ob_f = collision_experiment.result() + + induced_vel[i] = ptmath.coordinate_rotation( + ob_f.vel, -config.xy_line_of_centers_angle_radians + ) + induced_avel[i] = ptmath.coordinate_rotation( + ob_f.avel, -config.xy_line_of_centers_angle_radians + ) + throw_angles[i] = -np.atan2(induced_vel[i, 1], induced_vel[i, 0]) + + results[(speed, topspin_factor, sidespin_factor)] = ( + vel, + avel, + induced_vel, + induced_avel, + throw_angles, + ) + + return results + + +def collision_results_versus_sidespin( + config: BallBallCollisionExperimentConfig, + sidespin_factors, + speeds, + topspin_factors=None, + cut_angles=None, +): + if topspin_factors is None: + topspin_factors = [0.0] + if cut_angles is None: + cut_angles = [0.0] + + n_sidespins = np.size(sidespin_factors) + + results = {} + collision_experiment = BallBallCollisionExperiment(config) + + for speed in speeds: + natural_roll = natural_roll_spin_rate(speed, collision_experiment.cb_i.params.R) + for topspin_factor in topspin_factors: + topspin = topspin_factor * natural_roll + for cut_angle in cut_angles: + vel = np.empty((n_sidespins, 3)) + avel = np.empty((n_sidespins, 3)) + induced_vel = np.empty((n_sidespins, 3)) + induced_avel = np.empty((n_sidespins, 3)) + throw_angles = np.empty(n_sidespins) + for i, sidespin_factor in enumerate(sidespin_factors): + sidespin = sidespin_factor * natural_roll + collision_experiment.setup(speed, topspin, sidespin, cut_angle) + vel[i] = collision_experiment.cb_i.vel + avel[i] = collision_experiment.cb_i.avel + + cb_f, ob_f = collision_experiment.result() + + induced_vel[i] = ptmath.coordinate_rotation( + ob_f.vel, -config.xy_line_of_centers_angle_radians + ) + induced_avel[i] = ptmath.coordinate_rotation( + ob_f.avel, -config.xy_line_of_centers_angle_radians + ) + throw_angles[i] = -np.atan2(induced_vel[i, 1], induced_vel[i, 0]) + + results[(speed, topspin_factor, cut_angle)] = ( + vel, + avel, + induced_vel, + induced_avel, + throw_angles, + ) + + return results + + +def plot_throw_vs_cut_angle( + title: str, config, speeds, topspin_factors=None, sidespin_factors=None +): + cut_angles = np.linspace(0, np.pi / 2, 90 * 2, endpoint=False) + cut_angles_deg = np.rad2deg(cut_angles) + + results = collision_results_versus_cut_angle( + config, cut_angles, speeds, topspin_factors, sidespin_factors + ) + + fig = go.Figure() + fig.update_layout( + title=title, + xaxis_title="cut angle (deg)", + yaxis_title="throw angle (deg)", + showlegend=True, + ) + + for (speed, topspin_factor, sidespin_factor), ( + _, + _, + _, + _, + throw_angles, + ) in results.items(): + label = f"speed={speed:.3} m/s" + if topspin_factors is not None: + label += f", topspin_factor={topspin_factor}" + if sidespin_factors is not None: + label += f", sidespin_factor={sidespin_factor}" + fig.add_trace( + go.Scatter( + x=cut_angles_deg, + y=np.rad2deg(throw_angles), + mode="lines", + name=label, + ) + ) + + fig.show(config={"displayModeBar": True}) + + +def plot_throw_vs_sidespin_factor( + title: str, config, speeds, topspin_factors=None, cut_angles=None +): + sidespin_factors = np.linspace(-1.25, 1.25, 125 * 2) + + results = collision_results_versus_sidespin( + config, sidespin_factors, speeds, topspin_factors, cut_angles + ) + + fig = go.Figure() + fig.update_layout( + title=title, + xaxis_title="sidespin / natural roll", + yaxis_title="throw angle (deg)", + showlegend=True, + ) + + for (speed, topspin_factor, cut_angle), ( + _, + _, + _, + _, + throw_angles, + ) in results.items(): + label = f"speed={speed:.3} m/s" + if topspin_factors is not None: + label += f", topspin_factor={topspin_factor}" + if cut_angles is not None: + cut_angle_deg = math.degrees(cut_angle) + label += f", cut_angle={cut_angle_deg:.3} deg" + fig.add_trace( + go.Scatter( + x=sidespin_factors, + y=np.rad2deg(throw_angles), + mode="lines", + name=label, + ) + ) + + fig.show(config={"displayModeBar": True}) + + +def plot_throw_vs_percent_sidespin( + title: str, + config, + speeds, + topspin_factors=None, + cut_angles=None, + min_sidespin_percentage=-100, + max_sidespin_percentage=100, + max_english_radius_fraction=0.5, +): + sidespin_percentages = np.linspace( + min_sidespin_percentage, max_sidespin_percentage, 100 * 2 + ) + sidespin_factors = np.array( + [ + cue_strike_spin_rate_factor_percent_english( + sidespin_percentage, max_english_radius_fraction + ) + for sidespin_percentage in sidespin_percentages + ] + ) + + results = collision_results_versus_sidespin( + config, sidespin_factors, speeds, topspin_factors, cut_angles + ) + + fig = go.Figure() + fig.update_layout( + title=title, + xaxis_title="sidespin (% of max)", + yaxis_title="throw angle (deg)", + showlegend=True, + ) + + for (speed, topspin_factor, cut_angle), ( + _, + _, + _, + _, + throw_angles, + ) in results.items(): + label = f"speed={speed:.3} m/s" + if topspin_factors is not None: + label += f", topspin_factor={topspin_factor}" + if cut_angles is not None: + cut_angle_deg = math.degrees(cut_angle) + label += f", cut_angle={cut_angle_deg:.3} deg" + fig.add_trace( + go.Scatter( + x=sidespin_percentages, + y=np.rad2deg(throw_angles), + mode="lines", + name=label, + ) + ) + + fig.show(config={"displayModeBar": True}) + + +def plot_sidespin_transfer_percentage_vs_percent_sidespin( + title: str, + config, + speeds, + topspin_factors=None, + cut_angles=None, + min_sidespin_percentage=-100, + max_sidespin_percentage=100, + max_english_radius_fraction=0.5, +): + sidespin_percentages = np.linspace( + min_sidespin_percentage, max_sidespin_percentage, 100 * 2 + ) + sidespin_factors = np.array( + [ + cue_strike_spin_rate_factor_percent_english( + sidespin_percentage, max_english_radius_fraction + ) + for sidespin_percentage in sidespin_percentages + ] + ) + + results = collision_results_versus_sidespin( + config, sidespin_factors, speeds, topspin_factors, cut_angles + ) + + fig = go.Figure() + fig.update_layout( + title=title, + xaxis_title="sidespin (% of max)", + yaxis_title="sidespin transfer (%)", + showlegend=True, + ) + + for (speed, topspin_factor, cut_angle), ( + _, + avels, + _, + induced_avels, + _, + ) in results.items(): + label = f"speed={speed:.3} m/s" + if topspin_factors is not None: + label += f", topspin_factor={topspin_factor}" + if cut_angles is not None: + cut_angle_deg = math.degrees(cut_angle) + label += f", cut_angle={cut_angle_deg:.3} deg" + sidespin_transfer_percentage = ( + np.divide(-induced_avels[:, 2], avels[:, 2]) * 100 + ) + fig.add_trace( + go.Scatter( + x=sidespin_percentages, + y=sidespin_transfer_percentage, + mode="lines", + name=label, + ) + ) + + fig.show(config={"displayModeBar": True}) + + +def plot_sidespin_transfer_effectiveness_vs_percent_sidespin( + title: str, + config, + speeds, + topspin_factors=None, + cut_angles=None, + min_sidespin_percentage=-100, + max_sidespin_percentage=100, + max_english_radius_fraction=0.5, +): + sidespin_percentages = np.linspace( + min_sidespin_percentage, max_sidespin_percentage, 100 * 2 + ) + sidespin_factors = np.array( + [ + cue_strike_spin_rate_factor_percent_english( + sidespin_percentage, max_english_radius_fraction + ) + for sidespin_percentage in sidespin_percentages + ] + ) + + results = collision_results_versus_sidespin( + config, sidespin_factors, speeds, topspin_factors, cut_angles + ) + + fig = go.Figure() + fig.update_layout( + title=title, + xaxis_title="sidespin (% of max)", + yaxis_title="spin transfer effectiveness (%)", + showlegend=True, + ) + + for (speed, topspin_factor, cut_angle), ( + _, + _, + induced_vels, + induced_avels, + _, + ) in results.items(): + label = f"speed={speed:.3} m/s" + if topspin_factors is not None: + label += f", topspin_factor={topspin_factor}" + if cut_angles is not None: + cut_angle_deg = math.degrees(cut_angle) + label += f", cut_angle={cut_angle_deg:.3} deg" + sidespin_transfer_effectiveness = 100 * np.array( + [ + -induced_avels[i, 2] + / natural_roll_spin_rate( + ptmath.norm3d(induced_vels[i]), config.params.R + ) + for i in range(len(induced_vels)) + ] + ) + fig.add_trace( + go.Scatter( + x=sidespin_percentages, + y=sidespin_transfer_effectiveness, + mode="lines", + name=label, + ) + ) + + fig.show(config={"displayModeBar": True}) From 24bf9f43c1a3857a357defad5efe979b15e7e1dd Mon Sep 17 00:00:00 2001 From: Derek McBlane Date: Sun, 1 Feb 2026 17:38:44 -0500 Subject: [PATCH 2/4] add throw plots from TP A-14 --- sandbox/TP_A-14_plots.py | 156 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100755 sandbox/TP_A-14_plots.py diff --git a/sandbox/TP_A-14_plots.py b/sandbox/TP_A-14_plots.py new file mode 100755 index 00000000..99a07b35 --- /dev/null +++ b/sandbox/TP_A-14_plots.py @@ -0,0 +1,156 @@ +#! /usr/bin/env python + +import math + +import numpy as np +from ball_ball_collisions import ( + BallBallCollisionExperimentConfig, + cue_strike_spin_rate_factor_percent_english, + plot_throw_vs_cut_angle, + plot_throw_vs_percent_sidespin, + plot_throw_vs_sidespin_factor, +) + +from pooltool.objects.ball.datatypes import BallParams +from pooltool.physics.resolve.ball_ball.friction import ( + AlciatoreBallBallFriction, +) +from pooltool.physics.resolve.ball_ball.frictional_inelastic import FrictionalInelastic + + +def technical_proof_A14_plots(config: BallBallCollisionExperimentConfig): + slow_speed = 0.447 + medium_speed = 1.341 + fast_speed = 3.129 + alciatore_speeds = [slow_speed, medium_speed, fast_speed] + + model_str = config.model.model + friction_str = config.model.friction.model + + title = f"Natural-Roll Shot Collision at Various Speeds\nThrow Angle vs. Cut Angle\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_cut_angle(title, config, alciatore_speeds, topspin_factors=[1.0]) + + title = f"Stun Shot Collision at Various Speeds\nThrow Angle vs. Cut Angle\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_cut_angle(title, config, alciatore_speeds) + + title = f"Medium-Speed Shot with Various Amounts of Topspin\nThrow Angle vs. Cut Angle\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_cut_angle( + title, config, [medium_speed], topspin_factors=np.linspace(0, 1, 5) + ) + + title = f"Medium-Speed Head-On Collision with Various Amounts of Topspin\nThrow Angle vs. Sidespin\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_sidespin_factor( + title, config, [medium_speed], topspin_factors=np.linspace(0, 1, 5) + ) + + title = f"Medium-Speed Half-Ball Hit with Various Amounts of Topspin\nThrow Angle vs. Sidespin\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_sidespin_factor( + title, + config, + [medium_speed], + topspin_factors=np.linspace(0, 1, 5), + cut_angles=[math.radians(30)], + ) + + title = f"Head-On Collision at Various Speeds\nThrow Angle vs. Percent English\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_percent_sidespin(title, config, alciatore_speeds) + + title = f"Half-Ball Hit at Various Speeds\nThrow Angle vs. Percent English\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_percent_sidespin( + title, config, alciatore_speeds, cut_angles=[math.radians(30)] + ) + + title = f"Slow-Speed Stun Shot with Various Typical Sidespins\nThrow Angle vs. Cut Angle\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_cut_angle( + title, config, [slow_speed], sidespin_factors=[0.0, -1.0, 1.0] + ) + + title = f"Slow-Speed Stun Shot with Various 25% Sidespins\nThrow Angle vs. Cut Angle\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_cut_angle( + title, + config, + [slow_speed], + sidespin_factors=[ + 0.0, + -cue_strike_spin_rate_factor_percent_english(25), + cue_strike_spin_rate_factor_percent_english(25), + ], + ) + title = f"Medium-Speed Natural-Roll Shot with Various 25% Sidespins\nThrow Angle vs. Cut Angle\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_cut_angle( + title, + config, + [medium_speed], + topspin_factors=[1.0], + sidespin_factors=[ + 0.0, + -cue_strike_spin_rate_factor_percent_english(25), + cue_strike_spin_rate_factor_percent_english(25), + ], + ) + + title = f"Slow-Speed Stun Shot with Various 50% Sidespins\nThrow Angle vs. Cut Angle\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_cut_angle( + title, + config, + [slow_speed], + sidespin_factors=[ + 0.0, + -cue_strike_spin_rate_factor_percent_english(50), + cue_strike_spin_rate_factor_percent_english(50), + ], + ) + title = f"Medium-Speed Natural-Roll Shot with Various 50% Sidespins\nThrow Angle vs. Cut Angle\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_cut_angle( + title, + config, + [medium_speed], + topspin_factors=[1.0], + sidespin_factors=[ + 0.0, + -cue_strike_spin_rate_factor_percent_english(50), + cue_strike_spin_rate_factor_percent_english(50), + ], + ) + + title = f"Slow-Speed Stun Shot with Various 100% Sidespins\nThrow Angle vs. Cut Angle\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_cut_angle( + title, + config, + [slow_speed], + sidespin_factors=[ + 0.0, + -cue_strike_spin_rate_factor_percent_english(100), + cue_strike_spin_rate_factor_percent_english(100), + ], + ) + title = f"Medium-Speed Natural-Roll Shot with Various 100% Sidespins\nThrow Angle vs. Cut Angle\n(model={model_str}, model.friction={friction_str})" + plot_throw_vs_cut_angle( + title, + config, + [medium_speed], + topspin_factors=[1.0], + sidespin_factors=[ + 0.0, + -cue_strike_spin_rate_factor_percent_english(100), + cue_strike_spin_rate_factor_percent_english(100), + ], + ) + + +def main(): + ball_params = BallParams.default() + alciatore_friction = AlciatoreBallBallFriction(a=9.951e-3, b=0.108, c=1.088) + # average_friction = AverageBallBallFriction() + for model in [ + FrictionalInelastic(friction=alciatore_friction), + # FrictionalMathavan(friction=alciatore_friction), + # FrictionalInelastic(friction=average_friction), + # FrictionalMathavan(friction=average_friction), + ]: + config = BallBallCollisionExperimentConfig(model=model, params=ball_params) + technical_proof_A14_plots(config) + + +if __name__ == "__main__": + main() From 47da4423d6bd17ce430a60c12dc199cfc98eb814 Mon Sep 17 00:00:00 2001 From: Derek McBlane Date: Sun, 1 Feb 2026 17:39:06 -0500 Subject: [PATCH 3/4] add spin-transfer plots from TP A-27 --- sandbox/TP_A-27_plots.py | 53 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 sandbox/TP_A-27_plots.py diff --git a/sandbox/TP_A-27_plots.py b/sandbox/TP_A-27_plots.py new file mode 100755 index 00000000..32905154 --- /dev/null +++ b/sandbox/TP_A-27_plots.py @@ -0,0 +1,53 @@ +#! /usr/bin/env python + +from ball_ball_collisions import ( + BallBallCollisionExperimentConfig, + plot_sidespin_transfer_effectiveness_vs_percent_sidespin, + plot_sidespin_transfer_percentage_vs_percent_sidespin, +) + +from pooltool.objects.ball.datatypes import BallParams +from pooltool.physics.resolve.ball_ball.friction import ( + AlciatoreBallBallFriction, +) +from pooltool.physics.resolve.ball_ball.frictional_inelastic import FrictionalInelastic + + +def technical_proof_A27_plots(config: BallBallCollisionExperimentConfig): + slow_speed = 0.447 + medium_speed = 1.341 + fast_speed = 3.129 + alciatore_speeds = [slow_speed, medium_speed, fast_speed] + + model_str = config.model.model + friction_str = config.model.friction.model + + title = f"Medium-Speed Straight-In Stun-Shot at Various Speeds\nSpin Transfer % vs. Percent English\n(model={model_str}, model.friction={friction_str})" + plot_sidespin_transfer_percentage_vs_percent_sidespin( + title, config, alciatore_speeds, topspin_factors=[0], min_sidespin_percentage=1 + ) + + title = f"Medium-Speed Straight-In Stun-Shot at Various Speeds\nSpin Transfer Effectiveness vs. Percent English\n(model={model_str}, model.friction={friction_str})" + plot_sidespin_transfer_effectiveness_vs_percent_sidespin( + title, config, alciatore_speeds, topspin_factors=[0], min_sidespin_percentage=1 + ) + + +def main(): + ball_params = BallParams.default() + alciatore_friction = AlciatoreBallBallFriction(a=9.951e-3, b=0.108, c=1.088) + # average_friction = AverageBallBallFriction() + for model in [ + FrictionalInelastic(friction=alciatore_friction), + # FrictionalMathavan(friction=alciatore_friction), + # FrictionalInelastic(friction=average_friction), + # FrictionalMathavan(friction=average_friction), + ]: + config = BallBallCollisionExperimentConfig( + model=model, params=ball_params, xy_line_of_centers_angle_radians=-234 + ) + technical_proof_A27_plots(config) + + +if __name__ == "__main__": + main() From bab02009cf44ad71d0448358ad99bd95cd984a6f Mon Sep 17 00:00:00 2001 From: Derek McBlane Date: Sun, 1 Feb 2026 17:39:52 -0500 Subject: [PATCH 4/4] add additional sidespin-transfer plots for straight on shots with varying amounts of topspin --- sandbox/ball_ball_spin_transfer_plots.py | 84 ++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100755 sandbox/ball_ball_spin_transfer_plots.py diff --git a/sandbox/ball_ball_spin_transfer_plots.py b/sandbox/ball_ball_spin_transfer_plots.py new file mode 100755 index 00000000..596323cf --- /dev/null +++ b/sandbox/ball_ball_spin_transfer_plots.py @@ -0,0 +1,84 @@ +#! /usr/bin/env python + +from ball_ball_collisions import ( + BallBallCollisionExperimentConfig, + plot_sidespin_transfer_effectiveness_vs_percent_sidespin, + plot_sidespin_transfer_percentage_vs_percent_sidespin, +) + +from pooltool.objects.ball.datatypes import BallParams +from pooltool.physics.resolve.ball_ball.friction import ( + AlciatoreBallBallFriction, +) +from pooltool.physics.resolve.ball_ball.frictional_inelastic import FrictionalInelastic + + +def spin_transfer_various_topspins_speeds(config: BallBallCollisionExperimentConfig): + slow_speed = 0.447 + medium_speed = 1.341 + fast_speed = 3.129 + alciatore_speeds = [slow_speed, medium_speed, fast_speed] + + model_str = config.model.model + friction_str = config.model.friction.model + + title = f"Straight-In Stun Shot at Various Speeds\nSpin Transfer Percentage vs. Percent English\n(model={model_str}, model.friction={friction_str})" + plot_sidespin_transfer_percentage_vs_percent_sidespin( + title, config, alciatore_speeds, topspin_factors=[0], min_sidespin_percentage=1 + ) + title = f"Straight-In Stun Shot at Various Speeds\nSpin Transfer Effectiveness vs. Percent English\n(model={model_str}, model.friction={friction_str})" + plot_sidespin_transfer_effectiveness_vs_percent_sidespin( + title, config, alciatore_speeds, topspin_factors=[0], min_sidespin_percentage=1 + ) + + title = f"Straight-In Half-Rolling Shot at Various Speeds\nSpin Transfer Percentage vs. Percent English\n(model={model_str}, model.friction={friction_str})" + plot_sidespin_transfer_percentage_vs_percent_sidespin( + title, + config, + alciatore_speeds, + topspin_factors=[0.5], + min_sidespin_percentage=1, + ) + title = f"Straight-In Half-Rolling Shot at Various Speeds\nSpin Transfer Effectiveness vs. Percent English\n(model={model_str}, model.friction={friction_str})" + plot_sidespin_transfer_effectiveness_vs_percent_sidespin( + title, + config, + alciatore_speeds, + topspin_factors=[0.5], + min_sidespin_percentage=1, + ) + + title = f"Straight-In Rolling Shot at Various Speeds\nSpin Transfer Percentage vs. Percent English\n(model={model_str}, model.friction={friction_str})" + plot_sidespin_transfer_percentage_vs_percent_sidespin( + title, + config, + alciatore_speeds, + topspin_factors=[1.0], + min_sidespin_percentage=1, + ) + title = f"Straight-In Rolling Shot at Various Speeds\nSpin Transfer Effectiveness vs. Percent English\n(model={model_str}, model.friction={friction_str})" + plot_sidespin_transfer_effectiveness_vs_percent_sidespin( + title, + config, + alciatore_speeds, + topspin_factors=[1.0], + min_sidespin_percentage=1, + ) + + +def main(): + ball_params = BallParams.default() + alciatore_friction = AlciatoreBallBallFriction(a=9.951e-3, b=0.108, c=1.088) + # average_friction = AverageBallBallFriction() + for model in [ + FrictionalInelastic(friction=alciatore_friction), + # FrictionalMathavan(friction=alciatore_friction), + # FrictionalInelastic(friction=average_friction), + # FrictionalMathavan(friction=average_friction), + ]: + config = BallBallCollisionExperimentConfig(model=model, params=ball_params) + spin_transfer_various_topspins_speeds(config) + + +if __name__ == "__main__": + main()