From 808a4ea6eb4cc247fac8b4029019fcb12ffae5ea Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Sun, 25 Jan 2026 14:17:31 -0700 Subject: [PATCH 1/7] Set stronge as default --- pooltool/physics/resolve/resolver.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/pooltool/physics/resolve/resolver.py b/pooltool/physics/resolve/resolver.py index 152e4b8b..d039ca51 100644 --- a/pooltool/physics/resolve/resolver.py +++ b/pooltool/physics/resolve/resolver.py @@ -22,9 +22,9 @@ BallCCushionCollisionStrategy, BallLCushionCollisionStrategy, ) -from pooltool.physics.resolve.ball_cushion.mathavan_2010.model import ( - Mathavan2010Circular, - Mathavan2010Linear, +from pooltool.physics.resolve.ball_cushion.stronge_compliant.model import ( + StrongeCompliantCircular, + StrongeCompliantLinear, ) from pooltool.physics.resolve.ball_pocket import ( BallPocketStrategy, @@ -46,7 +46,7 @@ RESOLVER_PATH = pooltool.config.paths.PHYSICS_DIR / "resolver.yaml" """The location of the resolver path YAML.""" -VERSION: int = 8 +VERSION: int = 9 run = Run() @@ -71,14 +71,8 @@ def default_resolver() -> Resolver: c=1.088, ), ), - ball_linear_cushion=Mathavan2010Linear( - max_steps=1000, - delta_p=0.001, - ), - ball_circular_cushion=Mathavan2010Circular( - max_steps=1000, - delta_p=0.001, - ), + ball_linear_cushion=StrongeCompliantLinear(), + ball_circular_cushion=StrongeCompliantCircular(), ball_pocket=CanonicalBallPocket(), stick_ball=InstantaneousPoint( english_throttle=1.0, From 3eb3cd6e32529caf9956f31df7622461c71a3a17 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Sun, 25 Jan 2026 16:00:50 -0700 Subject: [PATCH 2/7] Add docstrings --- pooltool/physics/resolve/stronge_compliant.py | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/pooltool/physics/resolve/stronge_compliant.py b/pooltool/physics/resolve/stronge_compliant.py index c77af165..58b72f8b 100644 --- a/pooltool/physics/resolve/stronge_compliant.py +++ b/pooltool/physics/resolve/stronge_compliant.py @@ -431,16 +431,40 @@ def collision_duration(t_c, e_n): def resolve_collinear_compliant_frictional_inelastic_collision( - v_t_0: float, # tangential velocity (must be <= 0) - v_n_0: float, # normal velocity (must be <0) - m: float, # collision effective mass - beta_t: float, # mass-matrix coefficient - beta_n: float, # mass-matrix coefficient - mu: float, # friction coefficient - e_n: float, # coefficient of restitution - k_n: float, # normal spring stiffness - eta_squared: float, # ratio of normal spring stiffness to tangential spring stiffness + v_t_0: float, + v_n_0: float, + m: float, + beta_t: float, + beta_n: float, + mu: float, + e_n: float, + k_n: float, + eta_squared: float, ) -> tuple[float, float]: + """Resolve a collinear compliant frictional inelastic collision. + + Computes the post-collision tangential and normal velocities for a sphere + colliding with a half-space, accounting for friction, compliance (spring-like + deformation), and inelastic energy loss. + + Args: + v_t_0: Initial tangential velocity, must be <= 0. + v_n_0: Initial normal velocity, must be < 0. + m: Collision effective mass. + beta_t: Tangential mass-matrix coefficient. + beta_n: Normal mass-matrix coefficient. + mu: Friction coefficient. + e_n: Coefficient of restitution in the normal direction. + k_n: Normal spring stiffness. This value is arbitrary as only the frequency + ratio omega_t/omega_n affects the result, which depends on the ratio + beta_t/beta_n/eta_squared. + eta_squared: Ratio of normal to tangential spring stiffness. Together with + beta_t and beta_n, this determines the frequency ratio omega_t/omega_n, + which must be in the range (1, 2). + + Returns: + Final tangential and normal velocities after collision. + """ assert v_t_0 <= 0 assert v_n_0 < 0 From 0e9075483024cf5907e33321f928e3503067b8ae Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Sun, 25 Jan 2026 19:45:17 -0700 Subject: [PATCH 3/7] Add Poisson ratio conversion fns --- pooltool/physics/resolve/stronge_compliant.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pooltool/physics/resolve/stronge_compliant.py b/pooltool/physics/resolve/stronge_compliant.py index 58b72f8b..51eabc0c 100644 --- a/pooltool/physics/resolve/stronge_compliant.py +++ b/pooltool/physics/resolve/stronge_compliant.py @@ -12,9 +12,51 @@ @jit(nopython=True, cache=const.use_numba_cache) def normal_tangent_stiffness_ratio(poisson_ratio): + """Calculate eta_squared from Poisson ratio. + + Args: + poisson_ratio: Poisson's ratio of the cushion. + + Returns: + eta_squared: Ratio of normal to tangential stiffness. + """ return (2 - poisson_ratio) / (2 * (1 - poisson_ratio)) +def poisson_ratio_from_omega_ratio( + omega_ratio: float, beta_t: float = 3.5, beta_n: float = 1.0 +) -> float: + """Convert from tangential/normal frequency ratio to Poisson ratio. + + Args: + omega_ratio: Frequency ratio omega_t/omega_n. Must be in range (1, 2). + beta_t: Tangential mass-matrix coefficient for sphere-half-space collision. + beta_n: Normal mass-matrix coefficient for sphere-half-space collision. + + Returns: + Poisson's ratio. + """ + eta_squared = (beta_t / beta_n) / (omega_ratio**2) + return (2 * eta_squared - 2) / (2 * eta_squared - 1) + + +def omega_ratio_from_poisson_ratio( + poisson_ratio: float, beta_t: float = 3.5, beta_n: float = 1.0 +) -> float: + """Convert Poisson ratio to the tangential/normal frequency ratio. + + Args: + poisson_ratio: Poisson's ratio of the cushion. + beta_t: Tangential mass-matrix coefficient for sphere-half-space collision. + beta_n: Normal mass-matrix coefficient for sphere-half-space collision. + + Returns: + Frequency ratio omega_t/omega_n. + """ + eta_squared = normal_tangent_stiffness_ratio(poisson_ratio) + return np.sqrt((beta_t / beta_n) / eta_squared) + + @jit(nopython=True, cache=const.use_numba_cache) def t_c_shift(e_n): return (math.pi / 2) * (1 - 1 / e_n) From b498279e57d0a8c8d03ad81d80032a8075507c7c Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Sun, 25 Jan 2026 20:49:32 -0700 Subject: [PATCH 4/7] Add omega_ratio as model parameter --- .../ball_cushion/stronge_compliant/model.py | 45 ++++++++++++++++--- pooltool/physics/resolve/resolver.py | 8 +++- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py b/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py index 850536ad..c8c7538b 100644 --- a/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py +++ b/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) -def _solve(ball: Ball, cushion: Cushion) -> tuple[Ball, Cushion]: +def _solve(ball: Ball, cushion: Cushion, omega_ratio: float) -> tuple[Ball, Cushion]: rvw = ball.state.rvw.copy() logger.debug(f"v={rvw[1]}, w={rvw[2]}") @@ -55,8 +55,8 @@ def _solve(ball: Ball, cushion: Cushion) -> tuple[Ball, Cushion]: beta_n=beta_n, mu=ball.params.f_c, e_n=ball.params.e_c, - k_n=1e3, # TODO: cushion params - eta_squared=(beta_t_by_beta_n / 1.7**2), # TODO: cushion params + k_n=1e3, + eta_squared=(beta_t_by_beta_n / omega_ratio**2), ) Dv_n = (v_n_f - v_n_0) / beta_n @@ -93,6 +93,38 @@ def _solve(ball: Ball, cushion: Cushion) -> tuple[Ball, Cushion]: @attrs.define class StrongeCompliantLinear(CoreBallLCushionCollision): + """Ball-cushion collision resolver using Stronge's compliant collision model. + + This model accounts for the compliant (spring-like) nature of cushion deformation + during collision. + + Attributes: + omega_ratio: + Frequency ratio omega_t/omega_n controlling collision compliance, must be in + range (1, 2). Higher values = stiffer cushion, lower values = softer. + + Notes: + Architecturally, omega_ratio represents a cushion material property (Poisson's + ratio) and should ideally be a cushion attribute rather than a model parameter. + However, it is exposed here as a model parameter for pragmatic reasons. First, + omega_ratio is intuitive to adjust. It ranges from [1, 2] and controls the + frequency ratio omega_t/omega_n in the collision dynamics equations. This is the + first model requiring cushion material properties, so we defer adding cushion + attributes until needed by multiple models. + + Migration Path to Cushion Properties: + When cushion material properties are added to cushion segments: + 1. Add ``poisson_ratio: float`` attribute to Linear/CircularCushionSegment + 2. Add ``youngs_modulus: float``` (k_n) if collision duration is needed + 3. Use ``omega_ratio_from_poisson_ratio()`` to convert + 4. Deprecate omega_ratio model parameter in favor of cushion property + + See ``poisson_ratio_from_omega_ratio()`` and + ``omega_ratio_from_poisson_ratio()`` in ``stronge_compliant.py`` for conversion + utilities. + """ + + omega_ratio: float = 1.7 model: BallLCushionModel = attrs.field( default=BallLCushionModel.STRONGE_COMPLIANT, init=False, repr=False ) @@ -100,11 +132,14 @@ class StrongeCompliantLinear(CoreBallLCushionCollision): def solve( self, ball: Ball, cushion: LinearCushionSegment ) -> tuple[Ball, LinearCushionSegment]: - return _solve(ball, cushion) + return _solve(ball, cushion, self.omega_ratio) @attrs.define class StrongeCompliantCircular(CoreBallCCushionCollision): + """See :class:`StrongeCompliantLinear`.""" + + omega_ratio: float = 1.7 model: BallCCushionModel = attrs.field( default=BallCCushionModel.STRONGE_COMPLIANT, init=False, repr=False ) @@ -112,4 +147,4 @@ class StrongeCompliantCircular(CoreBallCCushionCollision): def solve( self, ball: Ball, cushion: CircularCushionSegment ) -> tuple[Ball, CircularCushionSegment]: - return _solve(ball, cushion) + return _solve(ball, cushion, self.omega_ratio) diff --git a/pooltool/physics/resolve/resolver.py b/pooltool/physics/resolve/resolver.py index d039ca51..3f14bf1f 100644 --- a/pooltool/physics/resolve/resolver.py +++ b/pooltool/physics/resolve/resolver.py @@ -71,8 +71,12 @@ def default_resolver() -> Resolver: c=1.088, ), ), - ball_linear_cushion=StrongeCompliantLinear(), - ball_circular_cushion=StrongeCompliantCircular(), + ball_linear_cushion=StrongeCompliantLinear( + omega_ratio=1.7, + ), + ball_circular_cushion=StrongeCompliantCircular( + omega_ratio=1.7, + ), ball_pocket=CanonicalBallPocket(), stick_ball=InstantaneousPoint( english_throttle=1.0, From 31a695d97b2437c555408344c43e41f0282d9ad9 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Sun, 25 Jan 2026 21:01:24 -0700 Subject: [PATCH 5/7] Add comment on independence from k_n --- .../physics/resolve/ball_cushion/stronge_compliant/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py b/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py index c8c7538b..cf907866 100644 --- a/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py +++ b/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py @@ -55,7 +55,7 @@ def _solve(ball: Ball, cushion: Cushion, omega_ratio: float) -> tuple[Ball, Cush beta_n=beta_n, mu=ball.params.f_c, e_n=ball.params.e_c, - k_n=1e3, + k_n=1e3, # arbitrary: collision outcome depends only on omega_ratio, not k_n. eta_squared=(beta_t_by_beta_n / omega_ratio**2), ) From 25e6c90fad289f14ab7bf8799b59c4cb880cfeb7 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Sun, 25 Jan 2026 22:13:34 -0700 Subject: [PATCH 6/7] Up default to 1.8 --- pooltool/physics/resolve/resolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pooltool/physics/resolve/resolver.py b/pooltool/physics/resolve/resolver.py index 3f14bf1f..d08fa82e 100644 --- a/pooltool/physics/resolve/resolver.py +++ b/pooltool/physics/resolve/resolver.py @@ -72,10 +72,10 @@ def default_resolver() -> Resolver: ), ), ball_linear_cushion=StrongeCompliantLinear( - omega_ratio=1.7, + omega_ratio=1.8, ), ball_circular_cushion=StrongeCompliantCircular( - omega_ratio=1.7, + omega_ratio=1.8, ), ball_pocket=CanonicalBallPocket(), stick_ball=InstantaneousPoint( From 3043b23c4954e7da4cdba2434ac2667205c6b5a9 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Sun, 25 Jan 2026 22:44:24 -0700 Subject: [PATCH 7/7] Update docs --- .../ball_cushion/stronge_compliant/model.py | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py b/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py index cf907866..b6579417 100644 --- a/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py +++ b/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py @@ -106,22 +106,18 @@ class StrongeCompliantLinear(CoreBallLCushionCollision): Notes: Architecturally, omega_ratio represents a cushion material property (Poisson's ratio) and should ideally be a cushion attribute rather than a model parameter. - However, it is exposed here as a model parameter for pragmatic reasons. First, - omega_ratio is intuitive to adjust. It ranges from [1, 2] and controls the - frequency ratio omega_t/omega_n in the collision dynamics equations. This is the - first model requiring cushion material properties, so we defer adding cushion - attributes until needed by multiple models. - - Migration Path to Cushion Properties: - When cushion material properties are added to cushion segments: - 1. Add ``poisson_ratio: float`` attribute to Linear/CircularCushionSegment - 2. Add ``youngs_modulus: float``` (k_n) if collision duration is needed - 3. Use ``omega_ratio_from_poisson_ratio()`` to convert - 4. Deprecate omega_ratio model parameter in favor of cushion property - - See ``poisson_ratio_from_omega_ratio()`` and - ``omega_ratio_from_poisson_ratio()`` in ``stronge_compliant.py`` for conversion - utilities. + However, it is exposed here as a model parameter because + + (1) It's intuitive to adjust, ranging between [1, 2], and representing the + ratio of spring coefficients between tangential and normal components. + + (2) This is the first model requiring cushion material properties, so we + defer adding cushion attributes until needed by multiple models. + + When cushion material properties are added to cushion segments, we should add + Poisson ratio as a cushion parameter, and use + ``poisson_ratio_from_omega_ratio()`` and ``omega_ratio_from_poisson_ratio()`` to + convert to/from omega_ratio. """ omega_ratio: float = 1.7