Projectile
A particle launched under uniform gravity, optionally slowed by linear drag.
- Name:
projectile - Field:
physics/mechanics - Formalism: ordinary differential equations
- References: Newton 1687
Factory
@register_model(
field='physics/mechanics',
tags=('mechanics', 'ode'),
references=('Newton 1687',),
defaults={'until': 2.0, 'dt': 0.01},
)
def projectile(speed: float = 10.0, angle: float = 45.0, *, mass: float = 1.0, drag: float = 0.0) -> System:
"""Return a particle launched under uniform gravity, optionally slowed by linear drag.
Args:
speed:
The launch speed.
angle:
The launch angle above the horizon, in degrees.
mass:
The mass.
drag:
The linear drag coefficient.
Returns:
system:
The model, with the particle `ball` in two dimensions.
Examples:
>>> from skcomplex.models.physics.mechanics import projectile
>>> projectile(speed=5.0, angle=90.0).elements[0].x_d1[1]
5.0
"""
radians = np.radians(angle)
velocity = [round(float(speed * np.cos(radians)), 12), round(float(speed * np.sin(radians)), 12)]
interactions: list[BaseInteraction] = [
ConstantForceInteraction('gravity', target='ball', F=[0.0, -GRAVITY * mass]),
]
if drag:
interactions.append(DampingInteraction('drag', target='ball', gamma=drag))
return System(
elements=[MassPointElement('ball', x=[0.0, 0.0], x_d1=velocity, m=mass)],
interactions=interactions,
space=ContinuousSpace(n_dim=2),
)
Components
class MassPointElement(PointElement):
"""Point mass, a point whose position `x` has order 2, with its velocity `x_d1` and a mass `m`.
The forces on the point divide by the mass through their divisor `'m'`.
Args:
label:
The label of the point.
x:
The initial position. A number applies to every component.
x_d1:
The initial velocity. A number applies to every component.
visual:
How the point is drawn, or `None` for `default_visual_`.
m:
The mass.
Attributes:
default_visual_:
Points at the positions.
Examples:
>>> from skcomplex.models.physics.common.particles import MassPointElement
>>> MassPointElement('ball', x=[0.0, 1.0], m=2.0).m
2.0
"""
def __init__(
self,
label: str,
x: npt.ArrayLike = 0.0,
x_d1: npt.ArrayLike = 0.0,
visual: VisualSpec | None = None,
*,
m: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
super().__init__(label, x=x, x_d1=x_d1, visual=visual)
self.m = m
class ConstantForceInteraction(RateInteraction):
"""Constant force `F` on a target, the second derivative `F / m` of its position of order 2.
It is the rate `F` with the mass as divisor, and the LAMMPS backend runs it as `fix addforce`.
Args:
label:
The label of the interaction.
target:
The path of the element or group the force acts on.
F:
The force vector. A number applies to every component.
state:
The name of the vector state of order 2 that holds the position of the target.
divisor:
The name of the state or parameter that holds the mass of the target.
Examples:
>>> from skcomplex.models.physics.common.particles import ConstantForceInteraction
>>> ConstantForceInteraction('gravity', target='ball', F=[0, -9.81]).F
[0, -9.81]
"""
def __init__(
self,
label: str,
*,
target: Annotated[str, RoleSpec()],
F: Annotated[npt.ArrayLike, ParameterSpec(vector=True)] = 0.0,
state: str = 'x',
divisor: str = 'm',
) -> None:
super().__init__(label, target=target, state=state, divisor=divisor)
self.F = F
def _get_rate(self, namespace: SimpleNamespace) -> object:
"""Return the force `F`.
Args:
namespace:
The namespace of the symbols the rate can use.
Returns:
rate:
The force, before the division by the mass.
"""
return namespace.F
Equations
ball.x[0]: diff(x_ball_0(t), t, t) == F_gravity_0/m_ball
ball.x[1]: diff(x_ball_1(t), t, t) == F_gravity_1/m_ball