Skip to content

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

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

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

Allocate per-step state for the simulation horizon.

prepare_step

prepare_step(step: int, time: float) -> None

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 (step * dt), supplied by the simulation (the single owner of the clock) so nodes need not store the time step themselves.

required

compute_flows

compute_flows(step: int, time: float) -> None

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 (step * dt).

required

get_arrived_trips

get_arrived_trips() -> list[dict]

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 out_link_id -> supply replacing the matching outbound links' receiving flow in the replay.

None

Returns:

Type Description
dict[int, list[tuple[Vehicle, int]]]

out_link_id -> [(vehicle, inbound_link_id), ...] in predicted

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

OneToOneNode(node_id: NodeId, inbound_link: BaseLink, outbound_link: BaseLink)

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

prepare_step(step: int, time: float) -> None

No-op; the transfer happens during flow computation.

compute_flows

compute_flows(step: int, time: float) -> None

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

DivergeNode(node_id: NodeId, inbound_link: BaseLink, outbound_links: Sequence[BaseLink])

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

prepare_step(step: int, time: float) -> None

No-op; the transfer happens during flow computation.

compute_flows

compute_flows(step: int, time: float) -> None

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 priority_vector (persists across steps).

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 alpha_1, alpha_2, ... per inbound link; converted to a priority_vector internally.

None

start

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

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

prepare_step(step: int, time: float) -> None

No-op; the transfer happens during flow computation.

compute_flows

compute_flows(step: int, time: float) -> None

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 priority_vector (persists across steps).

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 alpha_1, alpha_2, ... per inbound link; converted to a priority_vector internally.

None

start

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

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

prepare_step(step: int, time: float) -> None

No-op; the transfer happens during flow computation.

compute_flows

compute_flows(step: int, time: float) -> None

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

OriginNode(node_id: NodeId, link: BaseLink, demand_trips: list[Vehicle], **kwargs: object)

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

required
**kwargs object

Extra attributes set directly on the instance.

{}

start

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

Allocate the entry-queue and outflow series for the horizon.

prepare_step

prepare_step(step: int, time: float) -> None

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 (step * dt).

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

compute_flows(step: int, time: float) -> None

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:~mesoltm.core.vehicle.Vehicle.journeys — there is exactly one journey record per completed trip (the single source of truth); this list is just the per-destination index the trip metrics aggregate over (see :func:mesoltm.metrics.collect_trips).

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

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

Allocate the per-step arrival series for the horizon.

prepare_step

prepare_step(step: int, time: float) -> None

No-op; destinations act only during flow computation.

compute_flows

compute_flows(step: int, time: float) -> None

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 (step * dt).

required

get_arrived_trips

get_arrived_trips() -> list[dict]

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

priority_vector_from_alpha(alpha: list[float], resolution: int = 12) -> list[int]

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 priority_vector of inbound indices.