Skip to content

Pendulum on a sliding cart

A pendulum hanging from a cart that slides without friction along a horizontal track.

  • Name: cart_pendulum
  • Field: physics/mechanics
  • Formalism: ordinary differential equations
  • References: Ryzhov et al. 2021

Factory

@register_model(
    field='physics/mechanics',
    tags=('mechanics', 'ode', 'second order', 'lagrangian', 'momentum'),
    references=('Ryzhov et al. 2021',),
    defaults={'until': 10.0, 'dt': 0.02},
)
def cart_pendulum(
    theta: float = 0.5,
    y: float = 0.0,
    *,
    m1: float = 1.0,
    m2: float = 1.0,
    l: float = 1.0,
    g: float = 9.81,
    F: float = 0.0,
) -> System:
    """Return a pendulum hanging from a cart that slides without friction along a horizontal track.

    The Lagrange equations `(m1 + m2) y'' + m2 l (θ'' cos θ - θ'² sin θ) = F` and `l θ'' + y'' cos θ + g sin θ = 0`
    couple the position `y` of the cart and the angle `θ` of the pendulum. Without a force the horizontal momentum is
    conserved, so the cart swings against the pendulum.

    Args:
        theta:
            The initial angle of the pendulum.

        y:
            The initial position of the cart.

        m1:
            The mass of the cart.

        m2:
            The mass of the bob.

        l:
            The length of the rod.

        g:
            The gravitational acceleration.

        F:
            A constant horizontal force on the cart.

    Returns:
        system:
            The model, with the cart position `cart.y` and the angle `cart.theta`, both of order 2.

    Examples:
        >>> from skcomplex.models.physics.mechanics import cart_pendulum
        >>> cart_pendulum(m1=5.0).elements[0].m1
        5.0
    """
    return System(
        elements=[CartPendulumElement('cart', y=y, theta=theta, m1=m1, m2=m2, l=l, g=g, F=F)],
        interactions=[CartPendulumInteraction('motion', target='cart')],
    )

Components

class CartPendulumElement(Element):
    """Cart on a horizontal track at the position `y`, with a pendulum of angle `theta` hanging from it.

    The position and the angle have order 2. The motion comes from `CartPendulumInteraction`.

    Args:
        label:
            The label of the cart and pendulum.

        visual:
            How the cart and pendulum are drawn, or `None` for its class visual.

        y:
            The initial position of the cart.

        y_d1:
            The initial velocity of the cart.

        theta:
            The initial angle of the pendulum.

        theta_d1:
            The initial angular velocity of the pendulum.

        m1:
            The mass of the cart.

        m2:
            The mass of the bob.

        l:
            The length of the rod.

        g:
            The gravitational acceleration.

        F:
            A constant horizontal force on the cart.

    Examples:
        >>> from skcomplex.models.physics.mechanics import CartPendulumElement
        >>> CartPendulumElement('cart', m1=5.0).m1
        5.0
    """

    visual = VisualSpec('bodies', {'positions': _cart_and_bob, 'rods': True, 'traced': [1]})

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        y: Annotated[float, StateSpec(order=2)] = 0.0,
        y_d1: float = 0.0,
        theta: Annotated[float, StateSpec(period=2 * pi, order=2)] = 0.5,
        theta_d1: float = 0.0,
        m1: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        m2: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        l: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        g: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 9.81,
        F: Annotated[float, ParameterSpec()] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.y = y
        self.y_d1 = y_d1
        self.theta = theta
        self.theta_d1 = theta_d1
        self.m1 = m1
        self.m2 = m2
        self.l = l
        self.g = g
        self.F = F

class CartPendulumInteraction(EulerLagrangeInteraction):
    """Motion of a cart and the pendulum that hangs from it, the Euler-Lagrange equations with a force on the cart.

    The Lagrangian is `(m1 + m2) y'² / 2 + m2 l y' θ' cos θ + m2 l² θ'² / 2 + m2 g l cos θ`, and the force `F` of the
    target drives the position `y` of the cart.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the cart and pendulum.

    Examples:
        >>> from skcomplex.models.physics.mechanics import CartPendulumInteraction
        >>> CartPendulumInteraction('motion', target='cart').states
        ('y', 'theta')
    """

    def __init__(self, label: str, *, target: Annotated[str, RoleSpec()]) -> None:
        super().__init__(label, target=target, states=('y', 'theta'), forcing=self._get_forces_on_coordinates)

    @staticmethod
    def _get_forces_on_coordinates(namespace: SimpleNamespace) -> list[Expression]:
        """Return the generalized forces, the force `F` on the cart and none on the angle."""
        return [namespace.target.F, 0]

    def _get_lagrangian(self, namespace: SimpleNamespace) -> Expression:
        """Return the Lagrangian of the cart and pendulum.

        Args:
            namespace:
                The namespace of the symbols the Lagrangian can use.

        Returns:
            lagrangian:
                The Lagrangian.
        """
        p = namespace.target
        v, w = derivative(p.y, 1), derivative(p.theta, 1)
        kinetic = (p.m1 + p.m2) * v**2 / 2 + p.m2 * p.l * v * w * cos(p.theta) + p.m2 * p.l**2 * w**2 / 2
        return kinetic + p.m2 * p.g * p.l * cos(p.theta)

Equations

cart.y: diff(y_cart(t), t, t) == -(g_cart*l_cart*m2_cart*sin(theta_cart(t)) + (l_cart*m2_cart*sin(theta_cart(t))*diff(theta_cart(t), t)^2 + F_cart)*l_cart*m2_cart*cos(theta_cart(t))/(m1_cart + m2_cart))*l_cart*m2_cart*cos(theta_cart(t))/((l_cart^2*m2_cart^2*cos(theta_cart(t))^2/(m1_cart + m2_cart) - l_cart^2*m2_cart)*(m1_cart + m2_cart)) + (l_cart*m2_cart*sin(theta_cart(t))*diff(theta_cart(t), t)^2 + F_cart)/(m1_cart + m2_cart)
cart.theta: diff(theta_cart(t), t, t) == (g_cart*l_cart*m2_cart*sin(theta_cart(t)) + (l_cart*m2_cart*sin(theta_cart(t))*diff(theta_cart(t), t)^2 + F_cart)*l_cart*m2_cart*cos(theta_cart(t))/(m1_cart + m2_cart))/(l_cart^2*m2_cart^2*cos(theta_cart(t))^2/(m1_cart + m2_cart) - l_cart^2*m2_cart)