Skip to content

Charbonneau traffic

Charbonneau's traffic model, where random braking of single drivers sets off traffic jams.

  • Name: charbonneau_traffic
  • Field: engineering/traffic
  • Formalism: agent-based model
  • References: Charbonneau 2017

Factory

@register_model(
    field='engineering/traffic',
    tags=('traffic', 'jams', 'agents', 'avalanches', 'stochastic'),
    references=('Charbonneau 2017',),
    defaults={'until': 100},
)
def charbonneau_traffic(
    n_cars: int = 100,
    *,
    probability: float = 0.1,
    safe_distance: float = 5.0,
    speed_limit: float = 10.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return Charbonneau's traffic model, where random braking of single drivers sets off traffic jams.

    Cars on a one-way road start at rest with random spacings between 3 and 17. Every step, each driver brakes by 3
    when the car ahead is closer than the safe distance and accelerates by 1 otherwise, within the speed limit. A random
    driver brakes with a probability every step, and then every car moves by its speed.

    Args:
        n_cars:
            The number of cars.

        probability:
            The probability that a random driver brakes in a step.

        safe_distance:
            The distance to the car ahead below which drivers brake.

        speed_limit:
            The largest speed.

        random_state:
            The seed or generator of the initial spacings.

    Returns:
        system:
            The model, with the group `cars` of positions `x`, speeds `v`, and ranks `rank`.

    Examples:
        >>> from skcomplex.models.engineering.traffic import charbonneau_traffic
        >>> charbonneau_traffic(5).elements[0].check().prepare().size_
        5
    """
    rng = np.random.default_rng(random_state)
    positions = np.cumsum(rng.uniform(3.0, 17.0, n_cars))
    cars = GroupElement(
        'cars',
        element=MotoristElement,
        size=n_cars,
        values={
            'x': positions,
            'v': np.zeros(n_cars),
            'rank': np.arange(n_cars, dtype=float),
            'safe_distance': safe_distance,
            'speed_limit': speed_limit,
        },
    )
    return System(
        elements=[cars],
        interactions=[CarelessBrakingInteraction('braking', cars='cars', probability=probability)],
    )

Components

class MotoristElement(Element):
    """Car of Charbonneau's traffic model, which brakes when it is closer than a safe distance to the car ahead.

    Args:
        label:
            The label of the agent or group.

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

        x:
            The position along the road.

        v:
            The speed.

        rank:
            The order of the car along the road, 0 for the last car.

        safe_distance:
            The distance to the car ahead below which the driver brakes.

        braking:
            The speed lost when braking.

        acceleration:
            The speed gained when accelerating.

        speed_limit:
            The largest speed.

    Examples:
        >>> from skcomplex.core.elements import GroupElement
        >>> from skcomplex.models.engineering.traffic import MotoristElement
        >>> GroupElement('cars', element=MotoristElement, size=3, values={'x': [0.0, 10.0, 20.0]}).size
        3
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = VisualSpec('agents', {'position': 'x', 'color_by': 'v'}),
        *,
        x: Annotated[float, StateSpec(role='position')] = 0.0,
        v: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo), role='velocity')] = 0.0,
        rank: Annotated[float, StateSpec(domain=NN)] = 0.0,
        safe_distance: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 5.0,
        braking: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 3.0,
        acceleration: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        speed_limit: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 10.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.x = x
        self.v = v
        self.rank = rank
        self.safe_distance = safe_distance
        self.braking = braking
        self.acceleration = acceleration
        self.speed_limit = speed_limit

    @classmethod
    def _get_view(cls) -> type[MotoristView]:
        """Return the view of the cars, which types their states and parameters."""
        return MotoristView

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the speed adjustment rule.

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

        Returns:
            laws:
                The laws of the component.
        """
        return [RuleLaw(self.adjust)]

    def adjust(self, agent: MotoristView, model: ModelView) -> None:
        """Brake when the car ahead is closer than the safe distance and accelerate otherwise.

        The lead car, which has no car ahead, keeps its distance to the car behind it instead.

        Args:
            agent:
                The car.

            model:
                The model.
        """
        cars = model.agents(agent.group, MotoristView)
        ahead = next((car for car in cars if int(car.rank) == int(agent.rank) + 1), None)
        if ahead is None:
            behind = next((car for car in cars if int(car.rank) == int(agent.rank) - 1), None)
            distance = agent.x - behind.x if behind is not None else np.inf
        else:
            distance = ahead.x - agent.x
        change = -agent.braking if distance < agent.safe_distance else agent.acceleration
        agent.v = float(np.clip(agent.v + change, 0.0, agent.speed_limit))

class CarelessBrakingInteraction(Interaction):
    """Model rule of Charbonneau's traffic model: a random driver brakes now and then, and every car moves.

    Every step, with probability `probability`, one random driver brakes by the braking of its group whatever the
    distance to the car ahead. Then every car moves by its speed, but no closer than `gap` to the car ahead.

    Args:
        label:
            The label of the interaction.

        cars:
            The path of the group of cars.

        probability:
            The probability that a random driver brakes in a step.

        gap:
            The smallest distance a car keeps to the car ahead.

    Examples:
        >>> from skcomplex.models.engineering.traffic import CarelessBrakingInteraction
        >>> CarelessBrakingInteraction('bozo', cars='cars').probability
        0.1
    """

    def __init__(
        self,
        label: str,
        *,
        cars: Annotated[str, RoleSpec()],
        probability: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
        gap: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    ) -> None:
        super().__init__(label)
        self.cars = cars
        self.probability = probability
        self.gap = gap

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the rule of random braking and motion.

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

        Returns:
            laws:
                The laws of the component.
        """
        return [RuleLaw(self.drive)]

    def drive(self, model: ModelView) -> None:
        """Make a random driver brake with the interaction's probability, then move every car.

        Args:
            model:
                The model.
        """
        cars = sorted(model.agents('cars', MotoristView), key=lambda car: car.rank)
        if not cars:
            return
        if model.rng.random() < model.parameter('probability'):
            careless = cars[int(model.rng.integers(len(cars)))]
            careless.v = max(careless.v - careless.braking, 0.0)
        gap = model.parameter('gap')
        positions = [car.x for car in cars]
        for index, car in enumerate(cars):
            moved = car.x + car.v
            car.x = min(moved, positions[index + 1] - gap) if index + 1 < len(cars) else moved