Skip to content

Optimal velocity

The optimal velocity model of cars on a ring road, where uniform flow breaks into stop-and-go waves.

  • Name: optimal_velocity
  • Field: engineering/traffic
  • Formalism: ordinary differential equations
  • References: Bando et al. 1995; Chowdhury, Santen, and Schadschneider 2000

Factory

@register_model(
    field='engineering/traffic',
    tags=('traffic', 'jams', 'ode', 'car following'),
    references=('Bando et al. 1995', 'Chowdhury, Santen, and Schadschneider 2000'),
    defaults={'until': 200.0, 'dt': 1.0},
)
def optimal_velocity(
    n_cars: int = 20,
    length: float = 40.0,
    *,
    a: float = 1.0,
    V0: float = 1.0,
    hc: float = 2.0,
    perturbation: float = 0.1,
) -> System:
    """Return the optimal velocity model of cars on a ring road, where uniform flow breaks into stop-and-go waves.

    Car `i` relaxes its speed towards the optimal velocity of its headway `h`, `v_i' = a (V(h) - v_i)` with
    `V(h) = V0 (tanh(h - hc) + tanh(hc))`. Uniform flow with headway `b = length / n_cars` is unstable when
    `V'(b) > a / 2`, and then a jam forms that moves against the direction of travel. The cars are one group, and the
    headway of car `i` is the distance along the ring to member `i + 1`.

    Args:
        n_cars:
            The number of cars.

        length:
            The length of the ring road.

        a:
            The sensitivity of drivers.

        V0:
            The scale of the optimal velocity, which tends to `V0 (1 + tanh(hc))` on an empty road.

        hc:
            The headway at which the optimal velocity changes fastest.

        perturbation:
            The forward shift of the first car from uniform spacing.

    Returns:
        system:
            The model, with the group `cars` of positions `cars.x` and speeds `cars.v`, car `i + 1` ahead of car `i`.

    Examples:
        >>> from skcomplex.models.engineering.traffic import optimal_velocity
        >>> system = optimal_velocity(n_cars=5)
        >>> system.elements[0].size, len(system.interactions)
        (5, 1)
    """
    headway = length / n_cars
    speed = V0 * (np.tanh(headway - hc) + np.tanh(hc))
    cars = GroupElement(
        'cars',
        element=CarElement,
        size=n_cars,
        values={'x': _ring_positions(n_cars, length, perturbation), 'v': float(speed)},
    )
    following = OptimalVelocityInteraction('follow', target='cars', a=a, V0=V0, hc=hc, road_length=length)
    return System(elements=[cars], interactions=[following])

Components

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 OptimalVelocityInteraction(RelaxationInteraction):
    """Relaxation of the speed `v` of every car of a ring road towards the optimal velocity of its headway.

    The speed follows `v' = a (V(h) - v)` with the optimal velocity `V(h) = V0 (tanh(h - hc) + tanh(hc))` of the
    headway `h` to the next car, a relaxation over the time `1 / a`.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the group of cars, in the order they drive.

        a:
            The sensitivity, the inverse of the relaxation time.

        V0:
            The scale of the optimal velocity.

        hc:
            The safe headway, where the optimal velocity turns.

        road_length:
            The length of the ring road.

    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        a: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        V0: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        hc: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
        road_length: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 40.0,
    ) -> None:
        super().__init__(label, target=target, state='v')
        self.a = a
        self.V0 = V0
        self.hc = hc
        self.road_length = road_length

    def _get_goal(self, namespace: SimpleNamespace) -> object:
        """Return the optimal velocity of the headway of every car."""
        headway = OptimalVelocityInteraction._headway(namespace.target.x, namespace.road_length)
        return namespace.V0 * (tanh(headway - namespace.hc) + tanh(namespace.hc))

    def _get_time(self, namespace: SimpleNamespace) -> object:
        """Return the relaxation time `1 / a`."""
        return 1 / namespace.a

    @staticmethod
    def _headway(x: Expression, road_length: Expression) -> Expression:
        """Return the distance from every car of a ring road to the car ahead, the next member of the group."""
        return mod(shift_member(x, 1) - x, road_length)

Equations

cars.x[i]: diff(x_cars(t), t) == v_cars(t)
cars.v[i]: diff(v_cars(t), t) == (V0_follow*(tanh(hc_follow) + tanh(-hc_follow + mod(x_cars__shift0 - x_cars(t), road_length_follow))) - v_cars(t))*a_follow