Core¶
The core mesoscopic LTM: the road links that carry the discrete
fundamental diagram, the Vehicle agent tracked individually through
the network, and the Simulation engine that runs the four-phase
time loop. See The Model for the theory behind these
classes.
Links¶
Link is the discrete-LTM road link (its demand/supply arithmetic is ported
verbatim from the reference). BaseLink is the interface all links implement, and
ConnectorLink is the auto-inserted, transparent one-cell buffer that attaches
origins/destinations to a general graph.
Link
¶
Bases: BaseLink
A directed road link governed by the discrete LTM.
The link is parameterised by a triangular fundamental diagram: free-flow
speed v_f, backward shock-wave speed w and jam density rho_jam.
Capacity is derived as rho_jam * v_f * w / (v_f + w). State is stored as
cumulative inflow/outflow counts plus two integer capacity-token series that
make the node flows integer-valued (one unit of flow == one vehicle).
Paper notation (de Souza et al., SIMPAT 140 (2025) 103088), for cross-reading
the equations cited in the methods below — code name -> paper symbol:
cumulative_inflows -> F_a(i), cumulative_outflows -> G_a(i);
cap_disc_downstream -> q̂^d_a(i) (discharge budget),
cap_disc_upstream -> q̂^u_a(i) (entry budget);
_demand -> D̂_a(i), _supply -> Ŝ_a(i);
_inflow -> f̂_a(i), _outflow -> ĝ_a(i);
capacity -> C_a, rho_jam -> K_a, length -> L_a,
v_f -> V_a, w -> W_a, time_step -> Δt, step index t -> i.
The discrete link model is Section 3.3 (Eqs. 6-7); the underlying continuous
LTM identities and demand/supply are Section 3.1 (Eqs. 1-2).
Attributes:
| Name | Type | Description |
|---|---|---|
link_id |
int
|
Unique link identifier. |
length |
float
|
Link length in metres. |
v_f |
float
|
Free-flow speed (m/s). |
w |
float
|
Backward shock-wave speed (m/s). |
rho_jam |
float
|
Jam density (veh/m). |
capacity |
float
|
Capacity (veh/s), derived in :meth: |
critical_occupancy |
int
|
Largest whole-vehicle count that is still free-flowing
(floored |
jam_occupancy |
int
|
Maximum whole-vehicle count that fits on the link (floored
|
cumulative_inflows |
list[float]
|
Cumulative vehicles entered by step index. |
cumulative_outflows |
list[float]
|
Cumulative vehicles exited by step index. |
vehicles |
list[Vehicle]
|
FIFO queue of vehicles currently on the link. |
Create a link.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**kwargs
|
object
|
Link parameters set directly on the instance. Typically
|
{}
|
start
¶
Allocate cumulative-count and capacity-token arrays and derive lags.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
time_step
|
float
|
Simulation step |
required |
total_time
|
float
|
Total simulated horizon in seconds. |
required |
set_inflow
¶
set_inflow(vehicles: list[Vehicle], step: int) -> None
Record vehicles entering the upstream end and append them to the queue.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vehicles
|
list[Vehicle]
|
Vehicles crossing into this link this step. |
required |
step
|
int
|
Current step index, supplied by the calling node for per-vehicle trajectory logging (it does not enter the flow arithmetic). |
required |
set_outflow
¶
set_outflow(num_vehicles: int, step: int) -> list[Vehicle]
Pop num_vehicles from the front of the queue (FIFO) and return them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_vehicles
|
int
|
Number of vehicles leaving the downstream end this step. |
required |
step
|
int
|
Current step index, supplied by the calling node for per-vehicle trajectory logging (it does not enter the flow arithmetic). |
required |
get_next_step_demand
¶
Return the 0/1 look-ahead demand flag for the next step.
Not used by the core flow arithmetic; provided for signalized (traffic-
light) node models, which read it to anticipate demand on an approach when
deciding whether to hold or extend a green phase (as in the reference
abmmeso signalizedNode).
get_cumulative_demand_term
¶
Return the un-capacitated sending-flow term used by node models.
get_vehicle_from_index
¶
get_vehicle_from_index(index: int) -> Vehicle
Peek the index-th queued vehicle without removing it.
update_state_variables
¶
Commit the step's flows into cumulative counts and refill capacity tokens.
The two capacity-token series behave like token buckets: each step they
replenish by capacity * dt and are debited by the actual flow, capped at
ceil(capacity * dt) + 1. This is what enforces an integer discharge/entry
budget while tracking the continuous average capacity.
Implements the cumulative-count identities of Eq. (1) and the capacity-token recursion of Eq. (6) (paper Section 3.1 and 3.3).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
int
|
Current step index. |
required |
time_step
|
float
|
Simulation step |
required |
get_flows_in_the_past_steps
¶
Return the outflow over the last steps steps.
Not used by the core flow arithmetic; provided for signalized (traffic-
light) node models (as in the reference abmmeso signalizedNode,
e.g. for minimum-green / permitted-flow logic).
compute_demand_and_supplies
¶
Compute integer sending (demand) and receiving (supply) flows for step t.
Demand is the free-flow sending flow — vehicles that entered T1 steps
ago and have not yet left — capped by the downstream capacity token.
Supply is the receiving flow — jam storage rho_jam * length freed by
vehicles that left T2 steps ago, minus those still present — capped by
the upstream capacity token. Both first terms are floored to integers.
This is the discrete link model, Eq. (7) of paper Section 3.3:
D̂_a(i) = min{ floor(F_a(i-T1+1) - G_a(i)), floor(q̂^d_a(i)) }
Ŝ_a(i) = min{ floor(G_a(i-T2+1) + K_a*L_a - F_a(i)), floor(q̂^u_a(i)) }
i.e. the floored continuous demand/supply of Eq. (2) capped by the floored
capacity token of Eq. (6). The first min term is free-flow/queue driven,
the second is discretised-capacity driven.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
int
|
Current step index (paper index |
required |
get_output_records
¶
Return per-interval inflow/outflow records sampled at sample_time.
This is an output/aggregation helper, not part of the model dynamics. It
turns the cumulative curves F_a (cumulative_inflows) and G_a
(cumulative_outflows) of Eq. (1) into interval-averaged flow rates: the
average flow over a window is the slope of the cumulative curve across it,
[F(t2) - F(t1)] / (t2 - t1) (veh/s). sample_time may be coarser than
the simulation step, so each output window spans several simulation steps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sample_time
|
float
|
Output sampling interval in seconds (may be coarser than the simulation step). |
required |
sim_time_step
|
float
|
Simulation step |
required |
total_time
|
float
|
Total simulated horizon in seconds, passed in by the loop. |
required |
Returns:
| Type | Description |
|---|---|
list[dict]
|
A list of dicts with keys |
list[dict]
|
|
BaseLink
¶
Interface that every link type must provide to the node models and loop.
A link owns a first-in-first-out queue of vehicles and, at every time step,
reports a sending flow (demand) and a receiving flow (supply) that the
node models use to decide how many vehicles may cross each boundary. Concrete
subclasses implement the actual LTM dynamics.
start
¶
Allocate per-step state for a simulation of total_time seconds.
set_inflow
¶
set_inflow(vehicles: list[Vehicle], step: int) -> None
Append vehicles entering the link this step to its queue.
step is the current step index (supplied by the calling node) used only
for per-vehicle trajectory logging, not for the flow arithmetic.
set_outflow
¶
set_outflow(num_vehicles: int, step: int) -> list[Vehicle]
Remove and return the first num_vehicles vehicles leaving the link.
step is the current step index (supplied by the calling node) used only
for per-vehicle trajectory logging, not for the flow arithmetic.
get_next_step_demand
¶
Return a 0/1 look-ahead demand flag for the next step (signal models).
get_supply
¶
Return the number of vehicles the upstream end can accept this step.
get_cumulative_demand_term
¶
Return the un-capacitated sending-flow term (used by node models).
get_vehicle_from_index
¶
get_vehicle_from_index(index: int) -> Vehicle
Peek the index-th queued vehicle without removing it.
update_state_variables
¶
Commit this step's flows into the cumulative counts and state vars.
compute_demand_and_supplies
¶
Compute this step's sending (demand) and receiving (supply) flows.
get_output_records
¶
Return per-interval flow records for CSV/analysis output.
ConnectorLink
¶
Bases: Link
A one-cell, free-flow LTM link inserted automatically at origins/destinations.
The fundamental-diagram parameters are derived from the time step and a
vehicle_budget (the number of vehicles that could ever pass through the
connector) so that neither its storage nor its per-step capacity is ever the
binding constraint. See the module docstring for the rationale.
Create an auto-configured connector link.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
link_id
|
int
|
Unique link identifier. |
required |
time_step
|
float
|
Simulation step |
required |
vehicle_budget
|
int
|
An upper bound on the number of vehicles that can ever traverse this connector (e.g. the scenario's total vehicle count). Storage and capacity are sized from it so the connector never backs up onto the origin and never caps its own discharge. |
required |
**kwargs
|
object
|
Additional attributes forwarded to :class: |
{}
|
Vehicle¶
Every unit of flow is one Vehicle. It carries its own mutable route, a
position pointer, a free-form props metadata dict, and an automatically
populated trajectory.
Vehicle
¶
Vehicle(vehicle_id: int = 0, origin: NodeId = 0, destination: NodeId = 0, scheduled_departure: float = 0.0, route: Sequence[int] | None = None, props: dict | None = None, **kwargs: object)
A single vehicle with an explicit route through the network.
In the mesoscopic model every unit of node flow corresponds to exactly one
Vehicle, so vehicles are tracked individually as they move front-of-queue
from link to link. This is the discrete counterpart of the Trip object in
the reference abmmeso implementation.
Attributes:
| Name | Type | Description |
|---|---|---|
vehicle_id |
Unique identifier of the vehicle. |
|
origin |
Origin node/link identifier (bookkeeping only). |
|
destination |
Destination node/link identifier (bookkeeping only). |
|
scheduled_departure |
The requested departure time in seconds (an input).
The vehicle is only released at the first discrete step at or after this
time (see
:meth: |
|
departure_time |
float | None
|
The actual departure time in seconds — the moment the
vehicle is put into the origin queue / onto its first (connector) link,
stamped by :meth: |
route |
list[int]
|
Ordered sequence of |
position |
Index of the vehicle's current link within |
|
props |
dict
|
Free-form per-vehicle metadata (a plain |
arrival_time |
float | None
|
Arrival time in seconds, stamped by the destination node when
the vehicle exits the network ( |
trajectory |
list[dict]
|
Ordered per-link travel log, one entry per link the vehicle
enters: |
journeys |
list[dict]
|
The single source of truth for the trips this vehicle has
completed. Every time the vehicle is absorbed at a destination its
just-finished trip is snapshotted (see :meth: |
active |
bool
|
|
Create a vehicle.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vehicle_id
|
int
|
Unique identifier. |
0
|
origin
|
NodeId
|
Origin identifier (bookkeeping). |
0
|
destination
|
NodeId
|
Destination identifier (bookkeeping). |
0
|
scheduled_departure
|
float
|
Scheduled departure time in seconds (see the class attribute; the vehicle is released at the first discrete step at or after this time). |
0.0
|
route
|
Sequence[int] | None
|
Ordered |
None
|
props
|
dict | None
|
Optional free-form metadata dict (copied into a mutable dict);
see :attr: |
None
|
**kwargs
|
object
|
Extra attributes set directly on the instance (kept for compatibility with the reference implementation's keyword style). |
{}
|
record_entry
¶
Log that the vehicle entered link_id at simulation step step.
Opens a new trajectory segment whose exit_step is filled in later by
:meth:record_exit. Called automatically by the link when the vehicle is
placed onto it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
link_id
|
int
|
The link just entered. |
required |
step
|
int
|
The simulation step index of entry. |
required |
is_connector
|
bool
|
Whether the link is an auto-inserted O/D connector (these are excluded from real per-link travel times by default). |
False
|
record_exit
¶
Log that the vehicle left link_id at simulation step step.
Closes the most recent still-open segment for that link. Matching on
link_id (rather than assuming the last segment) is required because some
node models place a vehicle on its next link before discharging it from the
current one, so the open segments are not always in exit order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
link_id
|
int
|
The link just left. |
required |
step
|
int
|
The simulation step index of exit. |
required |
next_link
¶
Return the link the vehicle should enter after current_link_id.
Resolution uses position when it is consistent with the current link
(the common case, and robust to routes that revisit a link); otherwise it
falls back to the first occurrence of current_link_id in route —
matching the reference implementation's behaviour.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current_link_id
|
int
|
The link the vehicle is currently on. |
required |
Returns:
| Type | Description |
|---|---|
int | None
|
The next |
int | None
|
the last one in the route. |
advance_to
¶
Advance the vehicle's position pointer onto link_id.
Called when the vehicle actually moves onto its next link so that the
position index stays in sync with the vehicle's location.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
link_id
|
int
|
The link the vehicle has just entered. |
required |
snapshot_journey
¶
Freeze the current (just-completed) journey into an immutable record.
Called by the destination node the moment the vehicle is absorbed. The
returned dict is self-contained — it carries the journey's endpoints, its
scheduled_departure (requested), departure_time (actual) and
arrival_time (all seconds), its position in the vehicle's journeys
list (journey_index), and a copy of the
per-link trajectory — so it survives unchanged even if the vehicle is
later re-injected and its live fields are reset. It is the atomic unit all
trip metrics are computed from (see :mod:mesoltm.metrics).
Returns:
| Type | Description |
|---|---|
dict
|
A journey record: ``{"vehicle_id", "origin", "destination", |
dict
|
"scheduled_departure", "departure_time", "arrival_time", |
dict
|
"journey_index", "trajectory"} |
dict
|
copy of the segment list (its segment dicts are never mutated once the |
dict
|
journey has ended), so the snapshot is decoupled from later journeys. |
reset_for_new_journey
¶
Clear the live journey state so the vehicle can start a fresh trip.
Wipes the current-journey working fields (trajectory,
departure_time, arrival_time, position) while leaving the
completed :attr:journeys untouched. Called by
:meth:~mesoltm.network.state.NetworkState.inject when an already-used
vehicle is re-injected; the new route is set by the injection splice
immediately afterwards, and departure_time is re-stamped when the vehicle
next enters the origin queue.
Simulation¶
Simulation runs the LTM over a set of links and nodes. Use run() for a batch
run, or start()/step() to drive the loop yourself and inject() vehicles
between steps.
Simulation
¶
Runs the mesoscopic LTM over a set of links and nodes.
The per-step ordering is ported verbatim from abmmeso
(simulationengine/simulationRunner.py): (1) plugins run first,
(2) nodes prepare, (3) links compute demand/supply, (4) nodes move vehicles,
(5) links commit their state. The plugin phase (plugins, the generalised
general_purpose_objects slot) is the hook where external code can read
state and change the simulation (e.g. reroute vehicles) before flows are
computed.
Two ways to drive the loop:
- :meth:
run— initialise, run the whole horizon, write outputs. This is the batch entry point and is kept byte-for-byte identical to the reference. - :meth:
start+ :meth:step— advance the simulation one step at a time so external code can observe state and :meth:injectnew vehicles between steps (e.g. an external controller releasing vehicles on demand and re-injecting them at their current node into the next step's demand).
Attributes:
| Name | Type | Description |
|---|---|---|
links |
list[Link]
|
All links in the network. |
nodes |
list[BaseNode]
|
All nodes in the network. |
time_step |
float
|
Simulation step |
total_time |
float
|
Simulated horizon in seconds. |
plugins |
list[Plugin] | None
|
Optional objects with |
current_step |
int
|
Index of the next step to execute ( |
network_state |
NetworkState | None
|
Read-only state view attached by |
Create a simulation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**kwargs
|
object
|
Typically |
{}
|
total_steps
property
¶
Number of discrete steps in the horizon (total_time / dt, floored).
start
¶
start() -> Simulation
Initialise all links, nodes and plugins for the horizon.
Idempotent: calling it again (or via :meth:run) after the first time is
a no-op, so start() then step() and a bare run() are safe to
mix. Must be called before :meth:step.
Returns:
| Type | Description |
|---|---|
Simulation
|
|
step
¶
Advance the simulation by exactly one step and return that step index.
Runs the four LTM phases for current_step and increments it. Raises if
the simulation has not been :meth:start-ed or the horizon is exhausted.
Between steps, external code may read :attr:network_state and call
:meth:inject to add vehicles to the upcoming step's demand.
Returns:
| Type | Description |
|---|---|
int
|
The index of the step that was just executed. |
run
¶
run(progress: bool = True) -> Simulation
Initialise all objects, run the time loop, and write any outputs.
Equivalent to :meth:start followed by :meth:step until the horizon is
exhausted, then :meth:write_outputs. The traffic-flow computation is kept
behaviourally identical to the reference batch runner; the optional progress
bar only draws to sys.stderr and does not touch simulation state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
progress
|
bool
|
When |
True
|
Returns:
| Type | Description |
|---|---|
Simulation
|
|
inject
¶
inject(node_id: NodeId, vehicle: Vehicle, at_time: float | None = None, check_reentry_node: bool = True) -> None
Inject a vehicle into the network to depart from node_id.
The vehicle's route must be a sequence of real link ids reachable from
node_id; connector links (origin/destination access) are spliced on
automatically, exactly as for static demand. Injected vehicles enter the
origin's departure queue and are released once their departure time is
reached and the first link has supply.
The same vehicle can be injected more than once to make several trips.
Each trip is recorded as a separate journey on
:attr:~mesoltm.core.vehicle.Vehicle.journeys (the single source of truth
that all metrics read), so a re-injected vehicle produces one trip record
per journey — consistent with how a static demand profile produces one
vehicle, one journey. Re-injection is only allowed once the vehicle's
previous journey has completed (it was absorbed at a destination); otherwise
a :class:RuntimeError is raised. By default it must also re-enter at the
same real node where it last left the network (check_reentry_node).
Compile with injection_budget set to at least the number of injections
(counting each re-injection) you intend: the origin/destination connectors
are sized for it. If more vehicles are injected than that budget, a
:class:RuntimeWarning is emitted because the connector buffer may be too
small — the affected vehicle then waits in the origin queue (possibly not
entering within the horizon) instead of being silently discarded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
An origin node (must have been marked via
|
required |
vehicle
|
Vehicle
|
The vehicle to inject; its |
required |
at_time
|
float | None
|
Departure time in seconds. Defaults to the current step's time
( |
None
|
check_reentry_node
|
bool
|
When re-injecting an already-used vehicle, require it
to re-enter at the real node it last left from. Defaults to |
True
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If no compiled network state is attached, or the vehicle is still active (its current journey has not completed). |
ValueError
|
If |
get_times
¶
Return the sequence of simulated times in seconds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
added_step
|
int
|
Extra steps to append (e.g. |
0
|
Returns:
| Type | Description |
|---|---|
list[float]
|
A list of times |
save_history
¶
Write the recorded history to path (defaults to history_path).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | None
|
Destination JSON file; falls back to |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The path written to. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If nothing was recorded ( |
write_outputs
¶
Write per-link and per-trip CSV outputs (and the history if enabled).
Identifiers¶
A link id is always a plain int. A node id has no fixed type — it is any
hashable value you pass to Network.add_node: grid_network labels
nodes with (row, col) integer tuples, corridor_network uses strings, and your own
code may use any scheme. The mesoltm.NodeId alias (defined in mesoltm.core.ids)
names that intentionally-general type wherever a node id flows through the API. In a
recorded SimulationHistory ids round-trip through JSON, so a link id
there is int | str (an int while live, a str after loading).