Skip to content

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.

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(**kwargs: object)

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:start.

critical_occupancy int

Largest whole-vehicle count that is still free-flowing (floored rho_crit * length, rho_crit = rho_jam*w/(v_f+w)), derived in :meth:start.

jam_occupancy int

Maximum whole-vehicle count that fits on the link (floored rho_jam * length), derived in :meth:start.

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 link_id, length, v_f, w, rho_jam and optionally initial_capacity. The keyword style mirrors the reference implementation so scenarios port over unchanged.

{}

start

start(time_step: float, total_time: float) -> None

Allocate cumulative-count and capacity-token arrays and derive lags.

Parameters:

Name Type Description Default
time_step float

Simulation step dt in seconds.

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_demand

get_demand() -> int

Return this step's sending flow (vehicles ready to leave).

get_next_step_demand

get_next_step_demand() -> int

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_supply

get_supply() -> int

Return this step's receiving flow (vehicles the link can accept).

get_capacity

get_capacity() -> float

Return the link capacity in veh/s.

get_cumulative_demand_term

get_cumulative_demand_term() -> float

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

update_state_variables(t: int, time_step: float) -> None

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 dt in seconds (passed in by the loop; the link no longer stores time state of its own).

required

get_flows_in_the_past_steps

get_flows_in_the_past_steps(t: int, steps: int) -> float

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_demand_and_supplies(t: int) -> None

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 i).

required

get_output_records

get_output_records(sample_time: float, sim_time_step: float, total_time: float) -> list[dict]

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 dt in seconds, passed in by the loop.

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 time, link_id, inflow,

list[dict]

outflow, cumulative_inflow and cumulative_outflow.

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

start(time_step: float, total_time: float) -> None

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_capacity

get_capacity() -> float

Return the link capacity in vehicles per second.

get_demand

get_demand() -> int

Return the number of vehicles ready to leave the downstream end.

get_next_step_demand

get_next_step_demand() -> int

Return a 0/1 look-ahead demand flag for the next step (signal models).

get_supply

get_supply() -> int

Return the number of vehicles the upstream end can accept this step.

get_cumulative_demand_term

get_cumulative_demand_term() -> float

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

update_state_variables(t: int, time_step: float) -> None

Commit this step's flows into the cumulative counts and state vars.

compute_demand_and_supplies

compute_demand_and_supplies(t: int) -> None

Compute this step's sending (demand) and receiving (supply) flows.

get_output_records

get_output_records(sample_time: float, sim_time_step: float, total_time: float) -> list[dict]

Return per-interval flow records for CSV/analysis output.

ConnectorLink(link_id: int, time_step: float, vehicle_budget: int, **kwargs: object)

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 dt in seconds (sets the one-cell length and the free-flow speed so T1 == T2 == 1).

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:Link.

{}

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:~mesoltm.core.nodes.origin_node.OriginNode.prepare_step); :attr:departure_time records when that release actually happened.

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:~mesoltm.core.nodes.origin_node.OriginNode.prepare_step (None until then). This is normally ceil(scheduled_departure / dt) * dt, but is later if the vehicle was injected with a departure time already in the past (it cannot depart before it exists). Travel time is measured from here.

route list[int]

Ordered sequence of link_id values the vehicle intends to traverse. The route may be mutated at any time (e.g. by a plugin or routing policy) to reroute the vehicle at its next node — the network only propagates the vehicle along whatever route it currently holds.

position

Index of the vehicle's current link within route. Used by the routing layer to resolve the next link robustly even when a route revisits a link (as can happen on grids).

props dict

Free-form per-vehicle metadata (a plain dict) set at creation and freely read/updated at any time — e.g. a vehicle class, an operator id, a value of time, or any state a plugin wants to carry on the vehicle. The core model never touches it; it exists so downstream logic (routing plugins, the animation's color_by callable, custom metrics) can attach and evolve information now that every vehicle's location is tracked. Must be JSON-serialisable to survive the animation history round-trip.

arrival_time float | None

Arrival time in seconds, stamped by the destination node when the vehicle exits the network (None while still travelling). Tracked as a time (like :attr:departure_time) so downstream processing needs no step→seconds conversion.

trajectory list[dict]

Ordered per-link travel log, one entry per link the vehicle enters: {"link_id", "entry_step", "exit_step", "is_connector"}. exit_step is None while the vehicle is still on that link. Populated automatically as the vehicle moves; the per-link and overall travel times are derived from it (see :mod:mesoltm.metrics).

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:snapshot_journey) and appended here — a self-contained record of that journey's scheduled_departure, departure_time, arrival_time, route (via its trajectory) and endpoints. This works uniformly no matter how the vehicle came to exist: a vehicle from a static demand profile completes exactly one journey (journeys has one entry), while a vehicle injected — and re-injected — by hand records one entry per trip. All trip metrics (:mod:mesoltm.metrics) are derived from these journey records, so demand-profile and hand-injected runs share one consistent accounting path. The live fields above (route/position/trajectory/departure_time/ arrival_time) always describe the current journey and are reset when the vehicle is re-injected (see :meth:reset_for_new_journey); the completed journeys live on in journeys.

active bool

True from the moment the vehicle is queued at an origin (static demand or dynamic injection) until it is absorbed at a destination — i.e. while it is still moving through, or waiting to enter, the network. :meth:~mesoltm.network.state.NetworkState.inject refuses to (re-)inject a vehicle that is still active.

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 link_id sequence; copied into a mutable list.

None
props dict | None

Optional free-form metadata dict (copied into a mutable dict); see :attr:props. Updatable at any time after creation.

None
**kwargs object

Extra attributes set directly on the instance (kept for compatibility with the reference implementation's keyword style).

{}

record_entry

record_entry(link_id: int, step: int, is_connector: bool = False) -> None

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

record_exit(link_id: int, step: int) -> None

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(current_link_id: int) -> int | None

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 link_id in the route, or None if the current link is

int | None

the last one in the route.

advance_to

advance_to(link_id: int) -> None

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

snapshot_journey() -> dict

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"}. Thetrajectory`` is a shallow

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

reset_for_new_journey() -> None

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

Simulation(**kwargs: object)

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:inject new 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 dt in seconds.

total_time float

Simulated horizon in seconds.

plugins list[Plugin] | None

Optional objects with start / run_step run each step.

current_step int

Index of the next step to execute (0 before the first).

network_state NetworkState | None

Read-only state view attached by Network.compile.

Create a simulation.

Parameters:

Name Type Description Default
**kwargs object

Typically links, nodes, time_step, total_time and optionally plugins, output_link_file, trip_output_file, link_output_sample_time. Keyword style mirrors the reference implementation.

{}

total_steps property

total_steps: int

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

self.

step

step() -> int

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, display a single-line progress bar (steps completed, percentage, elapsed/ETA) on sys.stderr while the horizon is simulated. Off by default so batch runs stay silent and byte-for-byte identical to the reference.

True

Returns:

Type Description
Simulation

self, so callers can inspect link/node state after the run.

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 Network.set_origin) to release the vehicle from.

required
vehicle Vehicle

The vehicle to inject; its scheduled_departure is overwritten with the given departure time, and its live journey state is reset when it is being re-injected.

required
at_time float | None

Departure time in seconds. Defaults to the current step's time (current_step * dt), so the vehicle is considered for release in the very next :meth:step.

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.

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 node_id is not an origin, or (on re-injection with check_reentry_node) the vehicle last left at a different node.

get_times

get_times(added_step: int = 0) -> list[float]

Return the sequence of simulated times in seconds.

Parameters:

Name Type Description Default
added_step int

Extra steps to append (e.g. 1 to include the final boundary matching the cumulative-count arrays).

0

Returns:

Type Description
list[float]

A list of times [0, dt, 2*dt, ...].

save_history

save_history(path: str | None = None) -> str

Write the recorded history to path (defaults to history_path).

Parameters:

Name Type Description Default
path str | None

Destination JSON file; falls back to self.history_path.

None

Returns:

Type Description
str

The path written to.

Raises:

Type Description
RuntimeError

If nothing was recorded (record_history was off) or no path is available.

write_outputs

write_outputs() -> None

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).