Ring-road driver agents
Drivers on a circular highway, where small random errors of speed set off traffic jams.
- Name:
ring_road_drivers - Field:
engineering/traffic - Formalism: agent-based model
- References: Downey 2018; Resnick 1994
Factory
@register_model(
field='engineering/traffic',
tags=('traffic', 'jams', 'agents', 'stochastic'),
references=('Downey 2018', 'Resnick 1994'),
defaults={'until': 100},
)
def ring_road_drivers(
n_cars: int = 30,
length: float = 1000.0,
*,
eps: float = 0.02,
speed_limit: float = 40.0,
) -> System:
"""Return drivers on a circular highway, where small random errors of speed set off traffic jams.
The cars start at rest, equally spaced. Every driver accelerates by 1 per step up to the speed limit, the speed
takes a random relative error of up to `eps`, and a speed that would reach the car ahead becomes 0.
Args:
n_cars:
The number of cars.
length:
The length of the ring road.
eps:
The relative amplitude of the random error of speed.
speed_limit:
The largest speed.
Returns:
system:
The model, with the group `drivers` of speeds `speed` on a ring road of length `length`.
Examples:
>>> from skcomplex.models.engineering.traffic import ring_road_drivers
>>> ring_road_drivers(10).space.check().prepare().extent_[0]
1000.0
"""
positions = np.linspace(0.0, length, n_cars, endpoint=False)
drivers = GroupElement(
'drivers',
element=DriverElement,
size=n_cars,
values={'rank': np.arange(n_cars, dtype=float), 'eps': eps, 'speed_limit': speed_limit},
location=[(float(position), ROAD_WIDTH / 2) for position in positions],
)
return System(elements=[drivers], space=ContinuousSpace(extent=(length, ROAD_WIDTH), boundary='periodic'))
Components
class DriverElement(Element):
"""Driver on a ring road who accelerates as much as allowed and stops when the speed would cause a collision.
The speed changes by the acceleration, bounded by `min_acc` and `max_acc`, is multiplied by a random factor between
`1 - eps` and `1 + eps`, and stays between 0 and the speed limit. A speed larger than the distance to the car ahead
becomes 0. The car then moves along the ring by its speed, and the ring is as long as the width of the space.
Args:
label:
The label of the agent or group.
visual:
How the element is drawn, or `None` for `default_visual_`.
speed:
The speed.
rank:
The order of the car along the ring.
acceleration:
The acceleration every driver chooses.
min_acc:
The largest braking, as a negative acceleration.
max_acc:
The largest acceleration.
speed_limit:
The largest speed.
eps:
The relative amplitude of the random error of speed.
Examples:
>>> from skcomplex.core.elements import GroupElement
>>> from skcomplex.models.engineering.traffic import DriverElement
>>> GroupElement('drivers', element=DriverElement, size=2, location=[(0.0, 0.5), (10.0, 0.5)]).size
2
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'speed'}),
*,
speed: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
rank: Annotated[float, StateSpec(domain=NN)] = 0.0,
acceleration: Annotated[float, ParameterSpec()] = 1.0,
min_acc: Annotated[float, ParameterSpec()] = -10.0,
max_acc: Annotated[float, ParameterSpec()] = 1.0,
speed_limit: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 40.0,
eps: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.02,
) -> None:
super().__init__(label, visual=visual)
self.speed = speed
self.rank = rank
self.acceleration = acceleration
self.min_acc = min_acc
self.max_acc = max_acc
self.speed_limit = speed_limit
self.eps = eps
@classmethod
def _get_view(cls) -> type[DriverView]:
"""Return the view of the drivers, which types their states and parameters."""
return DriverView
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the driving rule.
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, agent: DriverView, model: ModelView) -> None:
"""Choose the speed from the distance to the car ahead and move along the ring.
Args:
agent:
The driver.
model:
The model.
"""
cars = model.agents(agent.group, DriverView)
if model.dimensions is None:
error_msg = 'Drivers move along a ring road, a continuous space with an extent.'
raise SimulationError(error_msg)
length = model.dimensions[0]
ahead = DriverElement._car_ahead(agent, model, len(cars))
x, y = DriverElement._position(agent)
distance = (DriverElement._position(ahead)[0] - x) % length if ahead is not None else length
acceleration = min(max(agent.acceleration, agent.min_acc), agent.max_acc)
speed = (agent.speed + acceleration) * model.rng.uniform(1 - agent.eps, 1 + agent.eps)
speed = min(max(speed, 0.0), agent.speed_limit)
agent.speed = 0.0 if speed > distance else speed
agent.move_to(((x + agent.speed) % length, y))
@staticmethod
def _position(agent: AgentView) -> tuple[float, float]:
"""Return the position of an agent in a continuous space."""
x, y = np.asarray(agent.location, dtype=float)
return float(x), float(y)
@staticmethod
def _car_ahead(agent: DriverView, model: ModelView, n_cars: int) -> DriverView | None:
"""Return the car whose rank follows the agent's rank on a road of `n_cars` cars, or `None` for the last car."""
rank = (int(agent.rank) + 1) % n_cars if n_cars else 0
return next(
(car for car in model.agents(agent.group, DriverView) if int(car.rank) == rank and car != agent),
None,
)