Nodes¶
Node models resolve how vehicles move across junctions each step, turning link
demand/supply into integer flows. BaseNode is the interface (and the routing
seam); the concrete models implement the paper's flow-resolution algorithms.
OriginNode/DestinationNode inject and absorb vehicles. See
Nodes & flow resolution for the theory.
Junction models¶
BaseNode
¶
Base class for all node models.
A node connects inbound links to outbound links and, each step, moves
vehicles across the junction subject to link demand and supply. Nodes that
branch (diverge/general) resolve each vehicle's next link through a
:class:~mesoltm.routing.policy.RoutingPolicy attached as routing_policy;
when unset they fall back to the vehicle's own route, reproducing the
reference behaviour.
Attributes:
| Name | Type | Description |
|---|---|---|
node_id |
NodeId
|
Unique node identifier. |
routing_policy |
RoutingPolicy | None
|
Optional routing policy overriding next-link decisions. |
network_state |
NetworkState | None
|
Optional read-only network state passed to the policy. |
start
¶
Allocate per-step state for the simulation horizon.
prepare_step
¶
Hook run before link demand/supply is computed (e.g. load departures).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
step
|
int
|
The current simulation step index. |
required |
time
|
float
|
The current simulation time in seconds ( |
required |
compute_flows
¶
Move vehicles across the node for the current step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
step
|
int
|
The current simulation step index. |
required |
time
|
float
|
The current simulation time in seconds ( |
required |
get_arrived_trips
¶
Return records of vehicles that finished their trip at this node.
peek_flows
¶
peek_flows(step: int, supply_overrides: dict[int, int] | None = None) -> dict[int, list[tuple[Vehicle, int]]]
Predict this step's crossings per outbound link, without moving anything.
A read-only replay of this node's own flow algorithm (same priorities,
FIFO order, per-outbound supply bookkeeping), so — unlike
:meth:demand_for_outbound, which lists everyone wanting a movement —
it returns only the vehicles the node would actually transfer this step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
step
|
int
|
The current simulation step index. |
required |
supply_overrides
|
dict[int, int] | None
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
dict[int, list[tuple[Vehicle, int]]]
|
|
dict[int, list[tuple[Vehicle, int]]]
|
crossing order; every outbound link id is present (possibly empty). |
demand_for_outbound
¶
demand_for_outbound(out_link_id: int, step: int) -> list[tuple[Vehicle, int]]
Return the vehicles demanding to cross onto one outbound link this step.
For every inbound link (real approaches and any origin connector — its
queued vehicles carry routes that may point at out_link_id too), the
current sending flow (the first get_demand() vehicles, FIFO) is scanned
and those whose next link resolves to out_link_id are kept. Resolution
uses :meth:_resolve_outbound_index, so an attached routing policy is
honoured exactly as in :meth:compute_flows (else the vehicle's own route).
compute_demand_and_supplies(step) is called on each inbound link first:
that is what makes get_demand() reflect this step's sending flow when the
query runs before the simulation's demand phase (e.g. from a plugin). It only
refreshes the transient demand/supply scalars, so the query stays a pure read.
Returns (vehicle, inbound_link_id) pairs, in inbound-link then FIFO order.
OneToOneNode
¶
Bases: BaseNode
Moves min(inbound demand, outbound supply) vehicles across the junction.
Ported from abmmeso (discrete/oneToOneNode.py). Used where a link
connects to exactly one downstream link.
Attributes:
| Name | Type | Description |
|---|---|---|
node_id |
Unique node identifier. |
|
inbound_link |
The single upstream link. |
|
outbound_link |
The single downstream link. |
Create a one-to-one node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
Unique identifier. |
required |
inbound_link
|
BaseLink
|
Upstream link. |
required |
outbound_link
|
BaseLink
|
Downstream link. |
required |
prepare_step
¶
No-op; the transfer happens during flow computation.
compute_flows
¶
Transfer the feasible number of vehicles from inbound to outbound.
Paper Section 3.4.1, Eq. (11): ĝ_u(i) = f̂_d(i) = min{D̂_u(i), Ŝ_d(i)}.
Since the discrete demand D̂ and supply Ŝ (Eq. 7) are already integers, the
one-to-one node needs no further rounding — the min is an integer count
of vehicles moved from the upstream to the downstream link.
peek_flows
¶
peek_flows(step: int, supply_overrides: dict[int, int] | None = None) -> dict[int, list[tuple[Vehicle, int]]]
Predict this step's crossings onto the outbound link, moving nothing.
Applies Eq. (11) to freshly refreshed demand/supply and peeks the first
min(demand, supply) vehicles of the inbound queue instead of moving
them, so it is a pure query. See :meth:BaseNode.peek_flows for the
contract.
DivergeNode
¶
Bases: BaseNode
Routes each vehicle from one inbound link to its chosen outbound link.
Ported from abmmeso (discrete/divergeNode.py). Vehicles are served in
strict first-in-first-out order: the front vehicle advances only if its target
outbound link has supply; if it is blocked, every vehicle behind it is blocked
too (FIFO diverge). The target outbound link is resolved through the routing
policy (defaulting to the vehicle's own route).
Attributes:
| Name | Type | Description |
|---|---|---|
node_id |
Unique node identifier. |
|
inbound_link |
The single upstream link. |
|
outbound_links |
Candidate downstream links. |
Create a diverge node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
Unique identifier. |
required |
inbound_link
|
BaseLink
|
Upstream link. |
required |
outbound_links
|
Sequence[BaseLink]
|
List of downstream links. |
required |
prepare_step
¶
No-op; the transfer happens during flow computation.
compute_flows
¶
Advance vehicles in FIFO order while their target link has supply.
Paper Section 3.4.2, Algorithm 1 (the discrete, vehicle-level diverge). The continuous counterpart is Eq. (4); here it is replaced by an exact FIFO walk that keeps flows integer: process the D̂_u sendable vehicles strictly in entry order and push each to its next link while that link still has supply Ŝ >= 1. The FIFO discipline means the first vehicle that cannot proceed (its target link is full) blocks all vehicles behind it — the loop stops.
The walk itself lives in :meth:_plan_flows (shared with the read-only
:meth:peek_flows); this method applies the resulting plan.
peek_flows
¶
peek_flows(step: int, supply_overrides: dict[int, int] | None = None) -> dict[int, list[tuple[Vehicle, int]]]
Predict this step's crossings per outbound link, without moving anything.
Replays the Algorithm 1 FIFO walk of :meth:compute_flows (shared via
:meth:_plan_flows) — including its front-blocking discipline — against
freshly refreshed demands/supplies and discards the plan instead of
applying it, so it is a pure query. See :meth:BaseNode.peek_flows for
the contract.
MergeNode
¶
MergeNode(node_id: NodeId, outbound_link: BaseLink, inbound_links: Sequence[BaseLink], priority_vector: list[int] | None = None, alpha: list[float] | None = None)
Bases: BaseNode
Merges several inbound links into one outbound link by priority shares.
Ported from abmmeso (discrete/mergeNode.py). Merge priorities are
expressed as shares alpha_1, alpha_2, ... (alpha[i] is the fraction of
the outbound supply inbound link i may claim). Internally the ported
algorithm consumes the equivalent integer priority_vector — a circular list
of inbound indices served round-robin (e.g. alpha = [0.75, 0.25] ⇔
[0, 0, 0, 1], serving inbound 0 three times as often as inbound 1). When
neither is supplied the priorities default to being proportional to the
inbound links' capacities (max flow rho_jam*v_f*w/(v_f+w)), resolved in
:meth:start once those capacities are known.
Attributes:
| Name | Type | Description |
|---|---|---|
node_id |
Unique node identifier. |
|
outbound_link |
The single downstream link. |
|
inbound_links |
Upstream links being merged. |
|
priority_vector |
Circular list of inbound indices encoding merge shares. |
|
priority_index |
Current position in |
Create a merge node.
Provide either priority_vector (the reference integer form) or
alpha (priority shares). If neither is given, the priorities default to
capacity-proportional (see the class docstring), resolved in :meth:start.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
Unique identifier. |
required |
outbound_link
|
BaseLink
|
Downstream link. |
required |
inbound_links
|
Sequence[BaseLink]
|
List of upstream links. |
required |
priority_vector
|
list[int] | None
|
Circular list of inbound indices encoding priorities. |
None
|
alpha
|
list[float] | None
|
Priority shares |
None
|
start
¶
Default the priorities to capacity-proportional if none were given.
Runs after the links' own start (the simulation starts links first), so
each inbound link's capacity is known and can weight the priority vector.
prepare_step
¶
No-op; the transfer happens during flow computation.
compute_flows
¶
Serve inbound links round-robin by priority until supply runs out.
Paper Section 3.4.3, Algorithm 2 (the discrete priority merge). The
continuous merge is Eq. (5), where inbound shares are set by the priorities
alpha_p (alpha_1 + alpha_2 = 1). Here those shares are encoded as the
integer priority_vector (e.g. alpha = [0.75, 0.25] -> [0, 0, 0, 1]) and
the node walks it round-robin, moving one whole vehicle per served slot from
the current inbound link into the shared downstream link while its supply
Ŝ_d >= 1. This keeps every merge flow integer and honours the priorities on
average without ever splitting a vehicle.
The walk itself lives in :meth:_plan_flows (shared with the read-only
:meth:peek_flows); this method applies the resulting plan.
peek_flows
¶
peek_flows(step: int, supply_overrides: dict[int, int] | None = None) -> dict[int, list[tuple[Vehicle, int]]]
Predict this step's crossings onto the outbound link, moving nothing.
Replays the Algorithm 2 walk of :meth:compute_flows (shared via
:meth:_plan_flows) against freshly refreshed demands/supplies and
discards the plan instead of applying it, so it is a pure query. See
:meth:BaseNode.peek_flows for the contract.
GeneralNodeModel
¶
GeneralNodeModel(node_id: NodeId, inbound_links: Sequence[BaseLink], outbound_links: Sequence[BaseLink], priority_vector: list[int] | None = None, alpha: list[float] | None = None)
Bases: BaseNode
M-inbound by N-outbound node with inbound priority shares (alpha).
Merge priorities among the inbound links are expressed as shares alpha_1,
alpha_2, ... and consumed internally as the equivalent integer
priority_vector (see :class:~mesoltm.core.nodes.merge_node.MergeNode).
When neither is supplied they default to capacity-proportional, resolved in
:meth:start.
Attributes:
| Name | Type | Description |
|---|---|---|
node_id |
Unique node identifier. |
|
inbound_links |
Upstream links. |
|
outbound_links |
Downstream links. |
|
priority_vector |
Circular list of inbound indices encoding merge shares. |
|
priority_index |
Current position in |
Create a general node.
Provide either priority_vector or alpha (priority shares).
If neither is given, the priorities default to capacity-proportional (see
:class:~mesoltm.core.nodes.merge_node.MergeNode), resolved in :meth:start.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
Unique identifier. |
required |
inbound_links
|
Sequence[BaseLink]
|
List of upstream links. |
required |
outbound_links
|
Sequence[BaseLink]
|
List of downstream links. |
required |
priority_vector
|
list[int] | None
|
Circular list of inbound indices encoding priorities. |
None
|
alpha
|
list[float] | None
|
Priority shares |
None
|
start
¶
Default the priorities to capacity-proportional if none were given.
Runs after the links' own start (the simulation starts links first), so
each inbound link's capacity is known and can weight the priority vector.
prepare_step
¶
No-op; the transfer happens during flow computation.
compute_flows
¶
Resolve node flows over all inbound/outbound links for the current step.
Paper Section 3.4.4, Algorithm 3 (the general M-in x N-out node). It combines the priority-vector merge of Algorithm 2 (over inbound links) with the per-vehicle FIFO diverge of Algorithm 1 (to outbound links), and adds the outbound-locking rule: once an outbound link is saturated (or an inbound link has a fractional-but-not-whole vehicle pending) it is locked so a blocked movement cannot starve the others. All flows stay integer.
The walk itself lives in :meth:_plan_flows (shared with the read-only
:meth:peek_flows); this method applies the resulting plan.
peek_flows
¶
peek_flows(step: int, supply_overrides: dict[int, int] | None = None) -> dict[int, list[tuple[Vehicle, int]]]
Predict this step's crossings per outbound link, without moving anything.
Replays the Algorithm 3 walk of :meth:compute_flows (shared via
:meth:_plan_flows) against freshly refreshed demands/supplies and
discards the plan instead of applying it, so it is a pure query. See
:meth:BaseNode.peek_flows for the contract.
Origins and destinations¶
OriginNode
¶
Bases: BaseNode
Feeds vehicles onto a link, holding a vertical queue when the link is full.
Ported from abmmeso (discrete/originNode.py). Vehicles whose departure
time has passed are released onto the link up to its available supply; any
excess wait in entry_queue — the simple, configuration-free entry
queueing the model relies on. Because a congested downstream lowers the link's
supply, back-pressure naturally accumulates the queue here at the origin.
Attributes:
| Name | Type | Description |
|---|---|---|
node_id |
Unique node identifier. |
|
link |
The link vehicles are injected onto. |
|
demand_trips |
Vehicles sorted by departure time, not yet released. |
|
entry_queue |
list[int]
|
Number of waiting vehicles at the origin by step index. |
outflow |
list[int]
|
Number of vehicles injected onto the link by step index (kept for post-processing). |
Create an origin node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
Unique identifier. |
required |
link
|
BaseLink
|
The (real or connector) link to inject vehicles onto. |
required |
demand_trips
|
list[Vehicle]
|
Vehicles to release, sorted by |
required |
**kwargs
|
object
|
Extra attributes set directly on the instance. |
{}
|
start
¶
Allocate the entry-queue and outflow series for the horizon.
prepare_step
¶
Move vehicles whose departure time has arrived into the waiting buffer.
Origins hold a vertical (point) entry queue: vehicles wait here at zero
length until the first link has supply to admit them, so back-pressure from
the network builds up at the origin rather than being lost. A vehicle joins
the waiting buffer once its departure time is reached
(scheduled_departure <= time), i.e. at step ceil(scheduled/dt).
This queue-join is the vehicle's actual departure: the moment it enters
the network's origin queue (before any admission to the first link). The
current simulation time (passed down from the simulation) is stamped onto
vehicle.departure_time here, so travel time is measured from when the
vehicle really started — which, for a vehicle injected with a departure time
already in the past, is later than ceil(scheduled/dt) (it cannot depart
before it exists).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
step
|
int
|
The current simulation step index (unused here; kept for the uniform node-step interface). |
required |
time
|
float
|
The current simulation time in seconds ( |
required |
add_trip
¶
add_trip(vehicle: Vehicle) -> None
Add a vehicle to the pending demand during a run (dynamic injection).
Inserted in departure-time order so :meth:prepare_step's sorted scan (it
stops at the first not-yet-departing vehicle) stays valid. Typically called
via :meth:~mesoltm.network.state.NetworkState.inject, which also splices
on the origin/destination connector links.
The vehicle is flagged active here — the single point every vehicle
(static demand and dynamic injection alike) passes through on its way into
the network — so re-injection can tell a still-travelling vehicle from an
idle one.
compute_flows
¶
Inject min(waiting, link supply) vehicles; record the leftover queue.
The origin's sending flow is the number of vehicles waiting; the admitted
flow is capped by the first link's discrete supply Ŝ (Eq. 7), so at most
min(waiting, Ŝ) whole vehicles enter this step and the rest stay in the
vertical queue (recorded in entry_queue).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
step
|
int
|
The current simulation step index. |
required |
time
|
float
|
The current simulation time in seconds (unused here; kept for the uniform node-step interface). |
required |
DestinationNode
¶
DestinationNode(node_id: NodeId, link: BaseLink)
Bases: BaseNode
Removes arriving vehicles from a link and records their arrival time.
Ported from abmmeso (discrete/destinationNode.py). A destination always
accepts the full downstream demand of its link, so it never constrains flow.
Attributes:
| Name | Type | Description |
|---|---|---|
node_id |
Unique node identifier. |
|
link |
The link whose arrivals are absorbed. |
|
inflow |
list[int]
|
Number of vehicles absorbed by step index (kept for post-processing). |
arrived_vehicles |
list[Vehicle]
|
The vehicle objects absorbed here, in arrival order (an event log; a re-injected vehicle that arrives twice appears twice). |
completed_journeys |
list[dict]
|
The completed-journey records that ended here, one per
arrival. Each entry is the same dict object appended to the vehicle's
:attr: |
Create a destination node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_id
|
NodeId
|
Unique identifier. |
required |
link
|
BaseLink
|
The (real or connector) link feeding this destination. |
required |
start
¶
Allocate the per-step arrival series for the horizon.
prepare_step
¶
No-op; destinations act only during flow computation.
compute_flows
¶
Pull all vehicles ready to leave the link and record their journeys.
For each arriving vehicle the just-finished trip is frozen into a journey
record (:meth:~mesoltm.core.vehicle.Vehicle.snapshot_journey) and appended
both to the vehicle's journeys (its own history, and the guard used when
it is re-injected) and to this node's completed_journeys (the per-
destination index the metrics read). The vehicle is marked idle
(active = False) so it may be injected again for a further trip. Each
vehicle's arrival_time is stamped with the current simulation time
(passed down from the simulation).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
step
|
int
|
The current simulation step index. |
required |
time
|
float
|
The current simulation time in seconds ( |
required |
get_arrived_trips
¶
Return one arrival record per completed journey that ended here.
Merge priorities¶
MergeNode/GeneralNodeModel accept priorities either as an integer
priority_vector (the reference form) or as shares alpha. This helper converts
shares to the equivalent integer vector the node arithmetic consumes.
priority_vector_from_alpha
¶
Build an integer, round-robin priority vector approximating alpha shares.
Each inbound index i appears in the returned vector a number of times
proportional to alpha_i; iterating the vector round-robin therefore serves
the inbound links in (approximately) their alpha proportions. The counts
are reduced by their gcd to keep the vector short and interleaved so approaches
are served in rotation rather than in blocks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha
|
list[float]
|
Non-negative priority shares, one per inbound link. They need not sum to exactly 1 (they are normalised internally); a share of 0 still receives a single slot so the approach is never fully starved. |
required |
resolution
|
int
|
Granularity of the integer approximation (higher = finer). |
12
|
Returns:
| Type | Description |
|---|---|
list[int]
|
A circular |