Skip to content

Quarter-car suspension

A quarter-car suspension driving over a road step.

  • Name: quarter_car
  • Field: engineering/mechatronics
  • Formalism: ordinary differential equations
  • References: Fritzson 2015

Select this system

from skcomplex.systems import create_simulation, create_system, describe

system = create_system('quarter_car')
entry = describe('quarter_car')
trajectory = create_simulation('quarter_car').prepare().run().trajectory_

Factory

@register_system(
    field='engineering/mechatronics',
    tags=('mechatronics', 'suspension', 'mass spring damper', 'ode', 'second order'),
    references=('Fritzson 2015',),
    defaults={'until': 10.0, 'dt': 0.005},
    observables=lambda system: [ObservableSpec('energy', _spring_energy)],
)
def quarter_car(
    height: float = 0.05,
    *,
    at: float = 0.5,
    m1: float = 250.0,
    m2: float = 30.0,
    k: float = 16000.0,
    b: float = 1000.0,
    kt: float = 160000.0,
) -> System:
    """Return a quarter-car suspension driving over a road step.

    A body of mass `m1` rests on a spring and a damper above a wheel of mass `m2`, and a tire spring joins the wheel to
    the road, which steps to `height` at time `at`. Both displacements settle at the height of the step.

    Args:
        height:
            The height of the road step.

        at:
            The time of the road step.

        m1:
            The mass of the body.

        m2:
            The mass of the wheel.

        k:
            The stiffness of the suspension.

        b:
            The damping of the suspension.

        kt:
            The stiffness of the tire.

    Returns:
        system:
            The system, with the displacements `car.y1` of the body and `car.y2` of the wheel.

    Examples:
        >>> from skcomplex.systems.engineering.mechatronics import quarter_car
        >>> quarter_car(height=0.1).elements[0].height
        0.1
    """
    car = QuarterCarElement('car', m1=m1, m2=m2, k=k, b=b, kt=kt, height=height, at=at)
    suspension = SpringDamperInteraction(
        'suspension',
        target='car',
        states=('y1', 'y2'),
        stiffness='k',
        damping='b',
        divisor={'y1': 'm1', 'y2': 'm2'},
    )
    return System(elements=[car], interactions=[suspension])

Components

Elements:

  • QuarterCarElement, from Element

Interactions:

  • SpringDamperInteraction, from GradientInteraction
class QuarterCarElement(Element):
    """Body of displacement `y1` and wheel of displacement `y2`, with a tire between the wheel and a road step.

    The element holds the tire, whose force on the wheel is `kt (road - y2)`, and a suspension interaction joins the
    body and the wheel.

    Args:
        label:
            The label of the element, unique among the parts of its system.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        y1:
            The initial displacement of the body.

        y1_d1:
            The initial velocity of the body.

        y2:
            The initial displacement of the wheel.

        y2_d1:
            The initial velocity of the wheel.

        m1:
            The mass of the body.

        m2:
            The mass of the wheel.

        k:
            The stiffness of the suspension.

        b:
            The damping of the suspension.

        kt:
            The stiffness of the tire.

        height:
            The height of the road step.

        at:
            The time of the road step.

    Examples:
        >>> from skcomplex.systems.engineering.mechatronics import QuarterCarElement
        >>> QuarterCarElement('car', height=0.05).height
        0.05
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        y1: Annotated[object, StateSpec(order=2)] = 0.0,
        y1_d1: object = 0.0,
        y2: Annotated[object, StateSpec(order=2)] = 0.0,
        y2_d1: object = 0.0,
        m1: Annotated[object, ParameterSpec(domain=POSITIVE)] = 250.0,
        m2: Annotated[object, ParameterSpec(domain=POSITIVE)] = 30.0,
        k: Annotated[object, ParameterSpec(domain=POSITIVE)] = 16000.0,
        b: Annotated[object, ParameterSpec(domain=POSITIVE)] = 1000.0,
        kt: Annotated[object, ParameterSpec(domain=POSITIVE)] = 160000.0,
        height: Annotated[object, ParameterSpec()] = 0.05,
        at: Annotated[object, ParameterSpec()] = 0.5,
    ) -> None:
        super().__init__(label, visual=visual)
        self.y1 = y1
        self.y1_d1 = y1_d1
        self.y2 = y2
        self.y2_d1 = y2_d1
        self.m1 = m1
        self.m2 = m2
        self.k = k
        self.b = b
        self.kt = kt
        self.height = height
        self.at = at

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the acceleration of the wheel by the tire, the suspension apart."""
        return [RateLaw(s.y2, s.kt * (step(s.t, s.at, s.height) - s.y2) / s.m2)]

class SpringDamperInteraction(GradientInteraction):
    """Linear spring and damper between two coordinates `x1` and `x2` of order 2 of one element.

    The spring is the gradient of the energy `k (x1 - x2)^2 / 2`, and the damper adds `-b (x1' - x2')` to the first
    coordinate and its opposite to the second, both divided by the divisor of every coordinate, such as its own mass.
    The stiffness and the damping are named parameters of the element.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the element with the two coordinates.

        states:
            The names of the two coordinates.

        stiffness:
            The name of the parameter of the stiffness `k`.

        damping:
            The name of the parameter of the damping `b`.

        divisor:
            The divisor of both coordinates, or a mapping from every coordinate to its own divisor.

    Raises:
        ValidationError:
            The states are not two names, the stiffness or the damping is not a name, or a divisor is invalid.

    Examples:
        >>> from skcomplex.systems.engineering.common.mechanics import SpringDamperInteraction
        >>> SpringDamperInteraction('suspension', target='car', states=('y1', 'y2')).stiffness
        'k'
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        states: Sequence[str] = ('x1', 'x2'),
        stiffness: str = 'k',
        damping: str = 'b',
        divisor: str | Callable[[SimpleNamespace], object] | Mapping[str, str] | None = None,
    ) -> None:
        super().__init__(label, target=target, states=states, divisor=divisor)
        self.stiffness = stiffness
        self.damping = damping

    def _check(self) -> None:
        """Raise an error for states that are not two names, or a stiffness or damping that is not a name.

        Raises:
            ValidationError:
                The states are not two names, the stiffness or the damping is not a name, or a divisor is invalid.
        """
        super()._check()
        if len(self._get_names()) != N_STATES:
            error_msg = f'{self.label!r} joins the states {self.states!r}. Give two states.'
            raise ValidationError(error_msg)
        for name in (self.stiffness, self.damping):
            if not (isinstance(name, str) and name.isidentifier()):
                error_msg = f'{self.label!r} reads the parameter {name!r}. Give the name of a parameter.'
                raise ValidationError(error_msg)

    def _get_potential(self, namespace: SimpleNamespace) -> Expression:
        """Return the energy `k (x1 - x2)^2 / 2` of the spring.

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

        Returns:
            energy:
                The energy of the spring.
        """
        first, second = (getattr(namespace.target, name) for name in self._get_names())
        return getattr(namespace.target, self.stiffness) * (first - second) ** 2 / 2

    def _get_laws(self, namespace: SimpleNamespace) -> list[BasePart]:
        """Return the rates of the spring and of the damper of both coordinates.

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

        Returns:
            laws:
                The rate laws of the two coordinates.
        """
        laws = super()._get_laws(namespace)
        member = namespace.target
        names = self._get_names()
        first, second = (getattr(member, name) for name in names)
        force = -getattr(member, self.damping) * (derivative(first, 1) - derivative(second, 1))
        for name, state, sign in ((names[0], first, 1), (names[1], second, -1)):
            divisor = self._get_divisor(member, name)
            laws.append(RateLaw(state, sign * force if divisor is None else sign * force / divisor))
        return laws

Equations

car.y1: diff(y1_car(t), t, t) == -k_car*(y1_car(t) - y2_car(t))/m1_car - b_car*(diff(y1_car(t), t) - diff(y2_car(t), t))/m1_car
car.y2: diff(y2_car(t), t, t) == kt_car*(where(t >= at_car, height_car, 0) - y2_car(t))/m2_car + k_car*(y1_car(t) - y2_car(t))/m2_car + b_car*(diff(y1_car(t), t) - diff(y2_car(t), t))/m2_car