diff --git a/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py b/pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py index 850536ad..b6579417 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, # arbitrary: collision outcome depends only on omega_ratio, not k_n. + eta_squared=(beta_t_by_beta_n / omega_ratio**2), ) Dv_n = (v_n_f - v_n_0) / beta_n @@ -93,6 +93,34 @@ 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 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 model: BallLCushionModel = attrs.field( default=BallLCushionModel.STRONGE_COMPLIANT, init=False, repr=False ) @@ -100,11 +128,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 +143,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 152e4b8b..d08fa82e 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,13 +71,11 @@ def default_resolver() -> Resolver: c=1.088, ), ), - ball_linear_cushion=Mathavan2010Linear( - max_steps=1000, - delta_p=0.001, + ball_linear_cushion=StrongeCompliantLinear( + omega_ratio=1.8, ), - ball_circular_cushion=Mathavan2010Circular( - max_steps=1000, - delta_p=0.001, + ball_circular_cushion=StrongeCompliantCircular( + omega_ratio=1.8, ), ball_pocket=CanonicalBallPocket(), stick_ball=InstantaneousPoint( diff --git a/pooltool/physics/resolve/stronge_compliant.py b/pooltool/physics/resolve/stronge_compliant.py index c77af165..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) @@ -431,16 +473,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