Skip to content

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