Network¶
The Network builder is the high-level entry point: add nodes and links, mark
origins/destinations, then compile() to a runnable Simulation.
NetworkState is the read-only-plus-mutation view that plugins and routing
policies see at run time. The convenience builders construct common topologies.
See Building networks.
Network builder¶
Network
¶
A mutable description of a road network that compiles to a simulation.
Attributes:
| Name | Type | Description |
|---|---|---|
default_fd |
Default fundamental-diagram parameters ( |
Create an empty network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
default_fd
|
dict[str, float] | None
|
Optional default |
None
|
add_node
¶
Add a node (idempotent) and optionally record its (x, y) position.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
Any hashable identifier. |
required |
pos
|
tuple[float, float] | None
|
Optional position used for auto link length and plotting. |
None
|
Returns:
| Type | Description |
|---|---|
NodeId
|
The node identifier. |
add_link
¶
add_link(u: NodeId, v: NodeId, length: float | None = None, link_id: int | None = None, **fd: float) -> int
Add a directed link from u to v and return its id.
Multiple links between the same pair are allowed (each gets a distinct
id): a slow lane is a parallel link with lower v_f/rho_jam, and a
detour is a parallel link whose length is larger than the direct
distance — no intermediate nodes required.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
u
|
NodeId
|
Upstream node id (auto-created if new). |
required |
v
|
NodeId
|
Downstream node id (auto-created if new). |
required |
length
|
float | None
|
Link length in metres. If omitted, uses the Euclidean distance between node positions when both are known. |
None
|
link_id
|
int | None
|
Optional explicit id; auto-assigned when omitted. |
None
|
**fd
|
float
|
Fundamental-diagram overrides ( |
{}
|
Returns:
| Type | Description |
|---|---|
int
|
The assigned |
set_origin
¶
set_origin(node_id: NodeId, vehicles: list[Vehicle] | None = None) -> None
Mark a node as an origin and attach demand vehicles to release from it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
The origin node. |
required |
vehicles
|
list[Vehicle] | None
|
Vehicles to release (routes over real link ids). Additional calls append more vehicles. |
None
|
set_destination
¶
Mark a node as a destination that absorbs arriving vehicles.
set_merge_priorities
¶
Override the merge priority shares of a node's inbound links.
By default a merge/general junction serves its inbound links with priority
shares alpha_i proportional to each link's capacity. Call this to set
the shares explicitly at the point where the node's links are defined —
e.g. to give a main road priority over a ramp regardless of capacity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
The merge/general junction whose priorities to set. |
required |
alpha
|
dict[int, float]
|
Mapping |
required |
compile
¶
compile(time_step: float, total_time: float, routing_policy: RoutingPolicy | None = None, plugins: list[Plugin] | None = None, injection_budget: int = 100, record_history: bool = False, history_path: str | None = None, history_classify: ClassifyFn | None = None) -> Simulation
Build links, nodes and connectors and return a runnable simulation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
time_step
|
float
|
Simulation step |
required |
total_time
|
float
|
Simulated horizon in seconds. |
required |
routing_policy
|
RoutingPolicy | None
|
Optional policy overriding per-vehicle next-link decisions at branching nodes. When omitted, vehicles follow their own (connector-spliced) routes. |
None
|
plugins
|
list[Plugin] | None
|
Optional per-step plugins run before flows each step (loop hooks, e.g. rerouting logic, link gating, or auctions). |
None
|
injection_budget
|
int
|
Upper bound on the number of vehicles that will be
added dynamically via :meth: |
100
|
record_history
|
bool
|
If |
False
|
history_path
|
str | None
|
Optional JSON file the history is written to (on
|
None
|
history_classify
|
ClassifyFn | None
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
Simulation
|
A configured :class: |
link_capacity
¶
Return the triangular-FD capacity rho_jam * v_f * w / (v_f + w) in veh/s.
Network state¶
NetworkState exposes topology, static link kinematics, and live per-step
quantities (occupancy, density, queues), plus the mutation seams used for
rerouting (set_route) and dynamic demand (inject).
NetworkState
¶
NetworkState(links_by_id: dict[int, Link], out_links: dict[NodeId, list[int]], in_links: dict[NodeId, list[int]], origin_nodes: dict[NodeId, OriginNode], node_positions: dict[NodeId, tuple[float, float] | None], endpoints: dict[int, tuple[NodeId, NodeId]], nodes_by_id: dict[NodeId, BaseNode] | None = None)
A read-only accessor over the links and topology of a compiled network.
Instances are created by :meth:~mesoltm.network.network.Network.compile and
handed to routing policies and plugins so external logic can inspect the
live network without reaching into internal objects. Live quantities
(vehicles on a link, instantaneous density and queue) are read from each
link's current queue; cumulative quantities are read from the link's
cumulative-count arrays at a given step.
Attributes:
| Name | Type | Description |
|---|---|---|
links_by_id |
Mapping |
|
step |
The current simulation step, updated by the engine each step. |
Create a network state view.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
links_by_id
|
dict[int, Link]
|
|
required |
out_links
|
dict[NodeId, list[int]]
|
|
required |
in_links
|
dict[NodeId, list[int]]
|
|
required |
origin_nodes
|
dict[NodeId, OriginNode]
|
|
required |
node_positions
|
dict[NodeId, tuple[float, float] | None]
|
|
required |
endpoints
|
dict[int, tuple[NodeId, NodeId]]
|
|
required |
nodes_by_id
|
dict[NodeId, BaseNode] | None
|
|
None
|
links_between
¶
Return all real link ids going directly from node u to node v.
endpoints
¶
Return the (u, v) node endpoints of a real link, or None.
position
¶
Return the (x, y) position of node_id if one was given.
continuous_free_flow_time
¶
Return the continuous-time free-flow travel time (s) of a link.
This is the continuous LTM value length / v_f — the exact time a vehicle
would need to traverse the link at free-flow speed, independent of the
simulation time step. The discrete model advances vehicles in whole steps,
so the achievable free-flow time is instead the link's integer wave lag
T1 * dt (see :func:mesoltm.metrics.free_flow_time for the route-level
discrete value). Use this continuous value where a fine-grained, dt-agnostic
cost is wanted (e.g. as a routing edge weight).
vehicles_on
¶
vehicles_on(link_id: int) -> list[Vehicle]
Return the vehicles currently queued on a link (live).
occupancy
¶
Return the number of vehicles currently on a link (live).
entry_queue
¶
Return the number of vehicles waiting to enter at an origin node.
waiting_vehicles
¶
waiting_vehicles(node_id: NodeId) -> list[Vehicle]
Return the vehicles waiting in an origin's vertical entry queue (live).
These are vehicles whose departure time has passed but that the first
link has not yet admitted (see :class:~mesoltm.core.nodes.origin_node).
Unlike :meth:entry_queue (a count), this returns the vehicles
themselves so callers (e.g. the animation recorder) can read each one's
id, next link and category. Empty for non-origin nodes.
inject
¶
inject(node_id: NodeId, vehicle: Vehicle, at_time: float | None = None, check_reentry_node: bool = True) -> None
Add a vehicle to an origin's demand during the run (dynamic injection).
The vehicle's route is treated as a sequence of real link ids; the
origin/destination connector links are spliced on automatically (matching
how static demand is compiled), so external callers only ever reason about
real links. The vehicle then joins the origin's departure queue in
departure-time order and is released like any other vehicle.
The same vehicle may be injected repeatedly to make several trips, each
recorded as its own journey (see
:attr:~mesoltm.core.vehicle.Vehicle.journeys). Two guardrails protect
re-injection:
- The vehicle must not still be moving through, or waiting to enter, the
network — its previous journey must have completed (it was absorbed at a
destination). Otherwise a :class:
RuntimeErroris raised. - By default the vehicle must re-enter at the same real node where it
last left the network (its previous journey's final real link's downstream
node — auxiliary O/D connector nodes are never considered). A mismatch
raises :class:
ValueError; passcheck_reentry_node=Falseto allow a deliberate re-entry elsewhere.
If the number of injections exceeds the injection_budget the connectors
were sized for (see :meth:~mesoltm.network.network.Network.compile), a
:class:RuntimeWarning is emitted: the connector buffer may then be too
small to admit the vehicle promptly, so it waits in the origin queue (and
may not enter within the horizon) rather than being silently discarded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
An origin node (marked via |
required |
vehicle
|
Vehicle
|
The vehicle to inject; |
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 |
|---|---|
ValueError
|
If |
RuntimeError
|
If the vehicle is still active (its current journey has not completed). |
vehicles_in_network
¶
vehicles_in_network() -> list[VehicleView]
Return a snapshot of every vehicle currently on a real link.
Used by :class:~mesoltm.plugins.plugin.ReroutingPlugin: each view
carries the vehicle, the link it is on, its remaining real-link route and
its destination. Connector links are skipped — a vehicle on an access
connector has no downstream choice to reroute yet. Called at the start of a
step (before flows), when every vehicle is on exactly one link.
movement_demand
¶
movement_demand(node_id: NodeId, out_link_id: int) -> list[VehicleView]
Return the vehicles demanding to cross onto one outbound link this step.
For the movement at node_id toward out_link_id, returns one
:class:VehicleView per vehicle whose next link resolves to out_link_id,
taken from the current sending flow of the node's inbound links — real
approaches and any origin connector, whose queued vehicles also carry
routes that may load this movement — in FIFO order (len(...) is the
count). Each view carries the vehicle and the inbound link it is on, so a
caller can ration the movement and reroute the rest with :meth:set_route.
Next-link resolution matches the node model (an attached routing policy, else
the vehicle's own route). It is a pure query: it refreshes the inbound
links' demand for self.step so it works from a plugin (which runs before
the demand phase) without changing any flow result. Returns [] if the
node has no through-junction model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
The junction the movement is at. |
required |
out_link_id
|
int
|
The outbound (downstream) link id of the movement. |
required |
peek_flows
¶
peek_flows(node_id: NodeId, supply_overrides: dict[int, int] | None = None) -> dict[int, list[VehicleView]]
Predict the vehicles that will cross node_id this step, per movement.
A read-only replay of the junction's own flow algorithm — same
priorities, FIFO order, per-outbound supply bookkeeping and locking —
keyed by outbound link id, each value the predicted crossing vehicles in
order (every outbound link id of the junction is present, possibly with
an empty list). Unlike :meth:movement_demand, which lists every vehicle
wanting a movement, this returns only those the node model would
actually transfer, so under congestion it is the realistic per-step
crossing set.
supply_overrides maps out_link_id -> supply and replaces the
matching links' receiving flow in the replay only — e.g. to model a cap
an access-control plugin will itself enforce. The prediction assumes
routes stay as they are between the query and the flow phase.
Like :meth:movement_demand it is a pure query: it refreshes the
junction's adjacent links' demand/supply for self.step (so it works
from a plugin, before the demand phase) and never touches vehicles,
flows, or the node's persistent priority state. Returns {} if the
node has no through-junction model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
The junction to replay. |
required |
supply_overrides
|
dict[int, int] | None
|
Optional per-outbound-link supply replacements. |
None
|
remaining_real_route
¶
remaining_real_route(vehicle: Vehicle, current_link_id: int | None = None) -> list[int]
Return the real (non-connector) links of vehicle.route, forward-only.
This reads vehicle.route (the plan the vehicle already carries) and
never recomputes a route: the recorder and rerouting logic use it so the
logged/replaced plan always matches what actually ran. When
current_link_id lies on the route the tail from there onward is
returned (starting with the current link); otherwise — e.g. a vehicle
still queued at its origin, not yet on a real link — the full real route
is returned.
set_route
¶
set_route(vehicle: Vehicle, real_route: list[int]) -> None
Replace an in-network vehicle's route with a new real-link route.
The new route must start at the link the vehicle is currently on — the
vehicle keeps moving, so its remaining plan can only be rewritten from
where it is. We read the current link from route[position] (the pointer
the movement logic keeps in sync) and reject a mismatch, so a bad update
can never silently strand the vehicle. The destination access connector is
re-attached and the position pointer reset to the (unchanged) current link,
so :meth:Vehicle.next_link resolves the new next link seamlessly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vehicle
|
Vehicle
|
An in-network vehicle (see :meth: |
required |
real_route
|
list[int]
|
Ordered real link ids from the current link to the destination, starting with the current link. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
cumulative_inflow
¶
Return cumulative inflow of a link at step t (default: current step).
cumulative_outflow
¶
Return cumulative outflow of a link at step t (default: current step).
VehicleView
¶
Bases: NamedTuple
A read-only snapshot of one in-network vehicle, for rerouting logic.
Attributes:
| Name | Type | Description |
|---|---|---|
vehicle |
Vehicle
|
The vehicle itself (carries |
link_id |
int
|
The real link the vehicle is currently travelling on (its location). |
route |
list[int]
|
The remaining ordered real link ids from the current link onward (connectors excluded), i.e. the plan the reroute logic may replace. |
destination |
NodeId
|
The vehicle's destination node (convenience copy). |
Builders¶
grid_network
¶
grid_network(rows: int, cols: int, link_length: float = 200.0, spacing: float = 1.0, fd: dict[str, float] | None = None, bidirectional: bool = True, skip_nodes: Iterable[tuple[int, int]] | None = None, skip_edges: Iterable[tuple[tuple[int, int], tuple[int, int]]] | None = None, all_nodes_od: bool = False) -> Network
Build a rectangular grid, optionally partial (missing nodes/edges).
Nodes are (i, j) tuples for row i and column j. Adjacent nodes are
connected horizontally and vertically; with bidirectional a link is added
in each direction. Custom / partially-connected layouts are supported by
excluding nodes (skip_nodes) or individual directed edges (skip_edges).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rows
|
int
|
Number of grid rows. |
required |
cols
|
int
|
Number of grid columns. |
required |
link_length
|
float
|
Length (m) assigned to every grid link. |
200.0
|
spacing
|
float
|
Geometric spacing between nodes (for positions/plots). |
1.0
|
fd
|
dict[str, float] | None
|
Optional fundamental-diagram params for all links. |
None
|
bidirectional
|
bool
|
If |
True
|
skip_nodes
|
Iterable[tuple[int, int]] | None
|
Iterable of |
None
|
skip_edges
|
Iterable[tuple[tuple[int, int], tuple[int, int]]] | None
|
Iterable of directed |
None
|
all_nodes_od
|
bool
|
If |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
Network
|
class: |
corridor_network
¶
corridor_network(lengths: list[float], fd: dict[str, float] | None = None, node_prefix: str = 'n') -> Network
Build a linear corridor of consecutive links with an origin and destination.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lengths
|
list[float]
|
Length (m) of each link in order; |
required |
fd
|
dict[str, float] | None
|
Optional fundamental-diagram params applied to all links. |
None
|
node_prefix
|
str
|
Prefix for the generated node ids ( |
'n'
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
Network
|
class: |
Network
|
destination (no demand attached yet — use |
network_to_dict
¶
network_to_dict(net: Network) -> dict
Serialise a network's topology to a plain dict (JSON-friendly).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
net
|
Network
|
The network to serialise. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
A dict with |
dict
|
ids are stringified so the result round-trips through JSON; demand |
dict
|
vehicles are not included (attach them after loading). |
network_from_dict
¶
network_from_dict(data: dict) -> Network
Reconstruct a :class:Network from :func:network_to_dict output.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
dict
|
A dict with |
required |
Returns:
| Type | Description |
|---|---|
Network
|
The reconstructed network (without demand vehicles). |