Skip to content

Car following with delay

A chain of cars that react to the speed difference to the car ahead after a reaction time.

  • Name: delayed_car_following
  • Field: engineering/traffic
  • Formalism: ordinary differential equations
  • References: Gros 2015; Chandler, Herman, and Montroll 1958

Factory

@register_model(
    field='engineering/traffic',
    tags=('traffic', 'delay', 'car following', 'instability'),
    references=('Gros 2015', 'Chandler, Herman, and Montroll 1958'),
    defaults={'until': 60.0, 'dt': 0.5},
)
def delayed_car_following(
    n_cars: int = 8,
    *,
    spacing: float = 10.0,
    u: float = 1.0,
    amplitude: float = 0.1,
    omega: float = 0.5,
    reaction: float = 1.0,
    T: float = 1.0,
    power: float = 0.0,
) -> System:
    """Return a chain of cars that react to the speed difference to the car ahead after a reaction time.

    Car `j + 1` follows car `j` as `x''_{j+1}(t) = λ Δv(t - T) / Δx(t - T)^power`, where `Δv` and `Δx` are the speed
    and position differences to the car ahead. The lead car drives at `u (1 - amplitude sin(ω t))`. Oscillations grow
    along the chain when `ω < 2 λ sin(ω T)` for the linear model, `power = 0`.

    Args:
        n_cars:
            The number of cars, the lead car included.

        spacing:
            The initial distance between neighbouring cars.

        u:
            The mean speed of the lead car.

        amplitude:
            The relative amplitude of the speed oscillation of the lead car.

        omega:
            The angular frequency of the speed oscillation of the lead car.

        reaction:
            The reaction constant `λ`.

        T:
            The reaction time.

        power:
            The power of the distance in the reaction, 0 for the linear model.

    Returns:
        system:
            The model, with the lead car `car_0` and the following cars `car_<j>`, each with position `x` and speed `v`.

    Examples:
        >>> from skcomplex.models.engineering.traffic import delayed_car_following
        >>> delayed_car_following(n_cars=3).elements[2].x
        -20.0
    """
    lead = LeadCarElement('car_0', x=0.0, v=u, u=u, amplitude=amplitude, omega=omega)
    followers = [CarElement(f'car_{index}', x=-index * spacing, v=u) for index in range(1, n_cars)]
    interactions = [
        DelayedFollowingInteraction(
            f'follow_{index}',
            target=f'car_{index}',
            leader=f'car_{index - 1}',
            reaction=reaction,
            T=T,
            power=power,
        )
        for index in range(1, n_cars)
    ]
    return System(elements=[lead, *followers], interactions=interactions)

Components

class LeadCarElement(Element):
    """Lead car whose speed swings about a cruising speed `u` with an amplitude and a frequency."""

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        x: Annotated[object, StateSpec(role='position')] = 0.0,
        v: Annotated[object, StateSpec(role='velocity')] = 1.0,
        u: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        amplitude: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
        omega: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    ) -> None:
        super().__init__(label, visual=visual)
        self.x = x
        self.v = v
        self.u = u
        self.amplitude = amplitude
        self.omega = omega

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the advance of the position and the forced swing of the speed."""
        return [RateLaw(s.x, s.v), RateLaw(s.v, -s.u * s.amplitude * s.omega * cos(s.omega * s.t))]

class CarElement(Element):
    """Car on a road whose position `x` advances at its speed `v`."""

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        x: Annotated[object, StateSpec(role='position')] = 0.0,
        v: Annotated[object, StateSpec(role='velocity')] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.x = x
        self.v = v

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the advance of the position at the speed."""
        return [RateLaw(s.x, s.v)]

class DelayedFollowingInteraction(RateInteraction):
    """Acceleration of a follower car by the speed difference to its leader, both seen a reaction time late.

    The speed of the follower follows the Gazis-Herman-Rothery law
    `v_f'(t) = λ (v_l - v_f)(t - T) / (x_l - x_f)(t - T)^m` of the sensitivity `λ`, the reaction time `T`, and the
    power `m` of the gap.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the follower car.

        leader:
            The path of the car ahead of the follower.

        reaction:
            The sensitivity `λ`.

        T:
            The reaction time.

        power:
            The power `m` of the gap.

    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        leader: Annotated[str, RoleSpec()],
        reaction: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        T: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        power: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    ) -> None:
        super().__init__(label, target=target, state='v')
        self.leader = leader
        self.reaction = reaction
        self.T = T
        self.power = power

    def _get_rate(self, namespace: SimpleNamespace) -> object:
        """Return the delayed response of the follower to its leader."""
        s, leader, follower = namespace, namespace.leader, namespace.target
        gap = delayed(leader.x, s.T) - delayed(follower.x, s.T)
        return s.reaction * (delayed(leader.v, s.T) - delayed(follower.v, s.T)) / gap**s.power

Equations

car_0.x: diff(x_car_0(t), t) == v_car_0(t)
car_0.v: diff(v_car_0(t), t) == -amplitude_car_0*omega_car_0*u_car_0*cos(omega_car_0*t)
car_1.x: diff(x_car_1(t), t) == v_car_1(t)
car_1.v: diff(v_car_1(t), t) == reaction_follow_1*(v_car_0(-T_follow_1 + t) - v_car_1(-T_follow_1 + t))/(x_car_0(-T_follow_1 + t) - x_car_1(-T_follow_1 + t))^power_follow_1
car_2.x: diff(x_car_2(t), t) == v_car_2(t)
car_2.v: diff(v_car_2(t), t) == reaction_follow_2*(v_car_1(-T_follow_2 + t) - v_car_2(-T_follow_2 + t))/(x_car_1(-T_follow_2 + t) - x_car_2(-T_follow_2 + t))^power_follow_2
car_3.x: diff(x_car_3(t), t) == v_car_3(t)
car_3.v: diff(v_car_3(t), t) == reaction_follow_3*(v_car_2(-T_follow_3 + t) - v_car_3(-T_follow_3 + t))/(x_car_2(-T_follow_3 + t) - x_car_3(-T_follow_3 + t))^power_follow_3
car_4.x: diff(x_car_4(t), t) == v_car_4(t)
car_4.v: diff(v_car_4(t), t) == reaction_follow_4*(v_car_3(-T_follow_4 + t) - v_car_4(-T_follow_4 + t))/(x_car_3(-T_follow_4 + t) - x_car_4(-T_follow_4 + t))^power_follow_4
car_5.x: diff(x_car_5(t), t) == v_car_5(t)
car_5.v: diff(v_car_5(t), t) == reaction_follow_5*(v_car_4(-T_follow_5 + t) - v_car_5(-T_follow_5 + t))/(x_car_4(-T_follow_5 + t) - x_car_5(-T_follow_5 + t))^power_follow_5
car_6.x: diff(x_car_6(t), t) == v_car_6(t)
car_6.v: diff(v_car_6(t), t) == reaction_follow_6*(v_car_5(-T_follow_6 + t) - v_car_6(-T_follow_6 + t))/(x_car_5(-T_follow_6 + t) - x_car_6(-T_follow_6 + t))^power_follow_6
car_7.x: diff(x_car_7(t), t) == v_car_7(t)
car_7.v: diff(v_car_7(t), t) == reaction_follow_7*(v_car_6(-T_follow_7 + t) - v_car_7(-T_follow_7 + t))/(x_car_6(-T_follow_7 + t) - x_car_7(-T_follow_7 + t))^power_follow_7