Source code for modelly.zoo.nagel_schreckenberg.model

"""Nagel-Schreckenberg traffic on a ring road.

A single-lane ring road of ``length`` cells, each holding at most one car. Every step, all
cars update in parallel:

1. accelerate: ``v = min(v + 1, max_speed)``;
2. brake to the gap: ``v = min(v, gap)``, the number of empty cells to the car ahead;
3. dawdle: with probability ``dawdle``, ``v = max(v - 1, 0)``;
4. move ``v`` cells forward.

Cars are ``Car`` agents whose ``position`` and ``velocity`` are ``Column`` attributes, so
the whole road updates with a handful of numpy operations on the column table. Cars never
overtake, so the rows stay in road order (cyclically) and the car ahead of row ``i`` is row
``i + 1``.

Validation: the fundamental diagram, flow ``J = density * mean speed`` against density.
Without dawdling the steady-state flow is exactly ``min(density * max_speed, 1 - density)``:
free flow below the critical density ``1 / (max_speed + 1)``, a jam moving backwards above
it. With dawdling the flow rises with density, peaks at an interior density and falls; at
low density every car runs at ``max_speed - 1`` with probability ``dawdle`` and at
``max_speed`` otherwise, so ``J = density * (max_speed - dawdle)``.

References:
    Nagel, K. and Schreckenberg, M. (1992). A cellular automaton model for freeway
    traffic. Journal de Physique I, 2(12), 2221-2229.
"""

from __future__ import annotations

import numpy as np

from modelly.core.agent import Agent
from modelly.core.columns import Column
from modelly.core.model import Model
from modelly.data.collector import DataCollector
from modelly.scenarios.scenario import Scenario


[docs] class TrafficScenario(Scenario): """Parameters of the Nagel-Schreckenberg model. Attributes: length: number of cells of the ring road. density: share of cells holding a car. max_speed: the speed limit, in cells per step. dawdle: probability per step that a car slows down by one for no reason. steps: default run length used by ``run``. """ length: int = 1000 density: float = 0.15 max_speed: int = 5 dawdle: float = 0.25 steps: int = 500
[docs] class Car(Agent): """A car: its cell on the ring and its speed, both stored in columns.""" position = Column(np.int64) velocity = Column(np.int64)
[docs] class NagelSchreckenberg(Model): """Cars on a ring road, updated in parallel on their column table. Example: >>> model = NagelSchreckenberg(TrafficScenario(rng=1, steps=20)) >>> model.run() >>> len(model.cars) == round(0.15 * 1000) True """ def __init__(self, scenario: TrafficScenario = TrafficScenario) -> None: """Place the cars on distinct random cells, standing still.""" super().__init__(scenario=scenario) s = self.scenario n = round(s.density * s.length) if not 0 < n <= s.length: raise ValueError( f"density {s.density} gives {n} cars on {s.length} cells; " "need at least one car and at most one per cell" ) Car.create_agents(self, n) self.cars = self.columns[Car] cells = self.rng.choice(s.length, size=n, replace=False) self.cars.position = np.sort(cells) self.datacollector = DataCollector( model_reporters={"flow": "flow", "mean_speed": "mean_speed"} ) self.datacollector.collect(self)
[docs] def gaps(self) -> np.ndarray: """Empty cells between every car and the car ahead of it.""" position = self.cars.position ahead = np.roll(position, -1) return (ahead - position - 1) % self.scenario.length
[docs] def step(self) -> None: """Accelerate, brake to the gap, dawdle, move: all cars at once.""" s = self.scenario cars = self.cars speed = np.minimum(cars.velocity + 1, s.max_speed) speed = np.minimum(speed, self.gaps()) slow = self.rng.random(speed.shape[0]) < s.dawdle speed = np.maximum(speed - slow, 0) cars.velocity = speed cars.position = (cars.position + speed) % s.length self.datacollector.collect(self)
[docs] def run(self, steps: int | None = None) -> None: """Run ``steps`` steps (the scenario's ``steps`` by default).""" self.run_for(self.scenario.steps if steps is None else steps)
@property def mean_speed(self) -> float: """Mean speed of the cars, in cells per step.""" return float(self.cars.velocity.mean()) @property def flow(self) -> float: """Cars passing a point per step: density times mean speed.""" return float(self.cars.velocity.sum() / self.scenario.length)
[docs] def fundamental_diagram(densities, transient: int, measured: int, **params) -> list: """Mean flow after ``transient`` steps, over ``measured`` steps, at each density. ``params`` are the other ``TrafficScenario`` parameters (``rng``, ``dawdle``...). Example: >>> flows = fundamental_diagram([0.1, 0.5], 50, 50, rng=1, dawdle=0.0) >>> [round(f, 2) for f in flows] [0.5, 0.5] """ flows = [] for density in densities: model = NagelSchreckenberg(TrafficScenario(density=density, **params)) model.run(transient) total = 0.0 for _ in range(measured): model.step() total += model.flow flows.append(total / measured) return flows