Skip to content

Visualizations

Requires the [plot] extra

These helpers import matplotlib at module top, so they are not part of import mesoltm. Install with pip install "mesoltm[plot]" and import from mesoltm.visualizations. MP4 export additionally needs ffmpeg on the PATH (GIF falls back to Pillow).

See Visualizations and Movement animations for usage.

Plots

Most plots are re-exported from mesoltm.visualizations. Two — plot_travel_time_distribution and plot_link_travel_times — are currently only importable from the mesoltm.visualizations.plots submodule (noted below).

plot_cumulative_curves

plot_cumulative_curves(sim: Simulation, link_ids: Sequence[int] | None = None, ax: Axes | None = None) -> Axes

Plot cumulative inflow (solid) and outflow (dashed) for links over time.

Parameters:

Name Type Description Default
sim Simulation

A run :class:~mesoltm.core.simulation.Simulation.

required
link_ids Sequence[int] | None

Link ids to plot; defaults to all links in the simulation.

None
ax Axes | None

Optional matplotlib axes to draw on; a new one is created if omitted.

None

Returns:

Type Description
Axes

The matplotlib axes containing the plot.

plot_link_flow(sim: Simulation, link_ids: Iterable[int], window: int = 60, window_seconds: float | None = None, ax: Axes | None = None) -> Axes

Plot the flow (veh/h) through one or a collection of links over time.

When several link ids are given their flows are summed, so this doubles as a "flow across a cut" plot (e.g. total flow leaving a region).

Parameters:

Name Type Description Default
sim Simulation

A run simulation.

required
link_ids Iterable[int]

One or more link ids whose outflow is aggregated.

required
window int

Averaging/sampling window in steps for the flow rate.

60
window_seconds float | None

Same window expressed in seconds; when given it takes precedence over window (converted via the simulation's time step). Prefer this when the simulation step is small, so the sampling interval of the flow curve is independent of dt.

None
ax Axes | None

Optional axes; created if omitted.

None

Returns:

Type Description
Axes

The matplotlib axes containing the plot.

plot_link_flows(sim: Simulation, link_ids: Iterable[int], labels: Sequence[str] | None = None, window: int = 60, window_seconds: float | None = None, ax: Axes | None = None) -> Axes

Plot flow (veh/h) over time for each link as its own labelled line.

Unlike :func:plot_link_flow (which sums links into one "cut" curve), this draws one line per link so it is clear which link carries which flow — handy for small scenarios (e.g. a freeway merge, parallel lanes).

Parameters:

Name Type Description Default
sim Simulation

A run simulation.

required
link_ids Iterable[int]

The links to plot, one line each.

required
labels Sequence[str] | None

Optional legend labels, one per link id (defaults to link <id>).

None
window int

Averaging/sampling window in steps for the flow rate.

60
window_seconds float | None

Same window expressed in seconds; when given it takes precedence over window (converted via the simulation's time step). Prefer this when the simulation step is small, so the sampling interval of the flow curve is independent of dt.

None
ax Axes | None

Optional axes; created if omitted.

None

Returns:

Type Description
Axes

The matplotlib axes containing the plot.

plot_link_time_series(sim: Simulation, link_ids: Sequence[int], labels: Sequence[str] | None = None, window: int = 5, axes=None)

One subplot per link: the travel time experienced on it, over time.

Each vehicle that crossed a link contributes one point (time it entered the link, time it took to cross). Points are sorted by entry time and smoothed with a trailing moving average, so a link where a queue builds up shows its travel time climbing over the run — the formation of congestion made visible per link.

Samples are taken from completed vehicles' recorded trajectories (see :attr:mesoltm.core.vehicle.Vehicle.trajectory).

Parameters:

Name Type Description Default
sim Simulation

A run simulation.

required
link_ids Sequence[int]

The links to plot, one subplot each (e.g. four links -> four side-by-side panels).

required
labels Sequence[str] | None

Optional per-link subplot titles (defaults to link <id>).

None
window int

Number of samples in the trailing moving average.

5
axes

Optional sequence of axes (one per link); a new row of subplots is created if omitted.

None

Returns:

Type Description

The sequence of axes, one per link.

plot_network

plot_network(state: NetworkState, color_by: str = 'occupancy', ax: Axes | None = None, node_size: float = 260.0, annotate_links: bool = False) -> Axes

Draw the network with links coloured by a per-link quantity.

Uses the same drawing helpers as the animation (:mod:._draw), so the static map and the video render identical arcs and node markers — only the link colour differs (here it encodes the chosen quantity). Requires node positions (set when building the network). Connector links are omitted; only real links are drawn. Links that share an edge — true parallel links and the two directions of a bidirectional edge (A->B and B->A) — are fanned onto separate arcs so their arrows never overlap; a lone link is drawn straight. Works for arbitrary node placements (not just grid-aligned) and any number of links between a pair.

Parameters:

Name Type Description Default
state NetworkState

A :class:~mesoltm.network.state.NetworkState.

required
color_by str

Link quantity to colour by: "flow" (total vehicles that have traversed the link so far, i.e. cumulative outflow — most useful after a run), "occupancy" (live vehicle count), "density" or "capacity".

'occupancy'
ax Axes | None

Optional axes; created if omitted.

None
node_size float

Node marker area (matplotlib scatter s).

260.0
annotate_links bool

If True, label each link on its arc with its id and value — so it is clear which link carries which flow.

False

Returns:

Type Description
Axes

The matplotlib axes containing the plot.

plot_travel_time_distribution

plot_travel_time_distribution(trips: Sequence[dict], ax: Axes | None = None, bins: int = 20) -> Axes

Plot the distribution of per-vehicle overall travel times.

Parameters:

Name Type Description Default
trips Sequence[dict]

Trip records from :func:mesoltm.metrics.collect_trips.

required
ax Axes | None

Optional axes; created if omitted.

None
bins int

Number of histogram bins.

20

Returns:

Type Description
Axes

The matplotlib axes containing the plot.

plot_link_travel_times(trips: Sequence[dict], ax: Axes | None = None) -> Axes

Plot the mean travel time on each link, aggregated over all trips.

Parameters:

Name Type Description Default
trips Sequence[dict]

Trip records from :func:mesoltm.metrics.collect_trips.

required
ax Axes | None

Optional axes; created if omitted.

None

Returns:

Type Description
Axes

The matplotlib axes containing the plot.

Animation

Render a recorded SimulationHistory to a video or per-step frames. The high-level entry points are animate_history, animate_from_history_file, and animate_simulation.

animate_simulation

animate_simulation(sim: Simulation, out_path: str, *, classify: ClassifyFn | None = None, **kwargs) -> str

Run a simulation with history logging and render it in one call.

Convenience for the batch case (no custom stepping): enables recording, runs sim and renders the result. For a custom step loop that injects vehicles, compile with record_history=True and render sim.history yourself.

Parameters:

Name Type Description Default
sim Simulation

A compiled simulation.

required
out_path str

Output video path.

required
classify ClassifyFn | None

Optional per-agent category classifier.

None
**kwargs

Forwarded to :func:save_animation (fps, subsample, frames_dir, highlight_links, ...).

{}

Returns:

Type Description
str

The path the video was written to.

animate_history

animate_history(history: SimulationHistory, out_path: str, **kwargs) -> str

Render a recorded :class:SimulationHistory to a video (builds the layout).

animate_from_history_file

animate_from_history_file(history_path: str, out_path: str, **kwargs) -> str

Load a saved history file and render it, guiding the user if it is missing.

Parameters:

Name Type Description Default
history_path str

Path to a JSON history written by :meth:SimulationHistory.save (or via history_path at compile).

required
out_path str

Output video path.

required
**kwargs

Forwarded to :func:save_animation.

{}

Raises:

Type Description
FileNotFoundError

If history_path does not exist — with a message explaining how to enable history logging.

save_animation

save_animation(frames: Sequence[Frame], layout: NetworkLayout, out_path: str, *, fps: int = 25, subsample: float = 1.0, dpi: int = 150, palette: dict[str, str] | None = None, frames_dir: str | None = None, **render_kw) -> str

Render the frames to a video (MP4 or GIF) and return the output path.

Parameters:

Name Type Description Default
frames Sequence[Frame]

The recorded frames (a list of :class:Frame).

required
layout NetworkLayout

The :class:NetworkLayout.

required
out_path str

Output file; .mp4 uses ffmpeg (falls back to .gif if ffmpeg is missing), .gif uses Pillow.

required
fps int

Frames per second (default 25).

25
subsample float

Frames shown per simulation step — >1 slows the video down (each step held longer), <1 speeds it up (steps dropped).

1.0
dpi int

Output resolution.

150
palette dict[str, str] | None

Optional fixed category -> colour map.

None
frames_dir str | None

If given, also write the individual per-step PNGs there — so one call can store both the video and the separate pictures. When omitted, only the video is written.

None
**render_kw

Forwarded to :func:render_frame.

{}

Returns:

Type Description
str

The path the video was written to (.gif if ffmpeg was unavailable).

save_frames

save_frames(frames: Sequence[Frame], layout: NetworkLayout, out_dir: str, *, stride: int = 1, dpi: int = 150, prefix: str = 'frame_', palette: dict[str, str] | None = None, **render_kw) -> list[str]

Save individual per-step PNGs (the "separate pictures" option).

Parameters:

Name Type Description Default
frames Sequence[Frame]

The recorded frames (a list of :class:Frame).

required
layout NetworkLayout

The :class:NetworkLayout.

required
out_dir str

Directory to write PNGs into (created if missing).

required
stride int

Write every stride-th frame (decimation for fewer images).

1
dpi int

Output resolution.

150
prefix str

Filename prefix; files are <prefix>0000.png ...

'frame_'
palette dict[str, str] | None

Optional fixed category -> colour map (resolved once so all frames share colours/legend).

None
**render_kw

Forwarded to :func:render_frame (e.g. highlight_links, title, show_next_link, max_slots).

{}

Returns:

Type Description
list[str]

The list of written file paths.

render_frame

render_frame(frame: Frame, layout: NetworkLayout, ax: Axes | None = None, *, palette: dict[str, str] | None = None, color_by: str | Callable[..., str] | None = DEFAULT_COLOR_BY, detail: str = 'auto', show_agent_ids: bool | None = None, show_next_link: bool | None = None, show_node_labels: bool | None = None, max_slots: int = 8, highlight_links: Iterable[int] | None = None, title: str | None = None) -> Axes

Draw a single recorded frame onto ax (created if omitted).

Marker, node and label sizes scale to the network so the frame stays readable from a small bottleneck to a large dense grid. How much per-agent annotation is drawn is controlled by detail (with per-annotation overrides), so dense scenarios can stay clean while the extra detail remains available on request.

Parameters:

Name Type Description Default
frame Frame

The :class:~mesoltm.recording.Frame to draw.

required
layout NetworkLayout

The :class:NetworkLayout for the network.

required
ax Axes | None

Optional axes; a new figure/axes is created if omitted.

None
palette dict[str, str] | None

category -> colour map; resolved from the frame if omitted.

None
color_by str | Callable[..., str] | None

What the agent dot colour encodes. "category" (default) uses the recorded classify category; "next_link" colours by the link the agent enters next (a diverge's split at a glance); None gives every agent one colour (colouring off); or pass a callable fn(snapshot) -> str to colour by anything on the snapshot — most usefully the vehicle's props metadata (e.g. lambda a: a.props.get("class", "car")) or its destination (lambda a: str(a.route[-1]) if a.route else "exit").

DEFAULT_COLOR_BY
detail str

"auto" (full annotations for compact networks, minimal for large/dense ones), "full" or "minimal".

'auto'
show_agent_ids bool | None

Override — draw the id inside each agent marker.

None
show_next_link bool | None

Override — draw each agent's next-link intention arrow + label.

None
show_node_labels bool | None

Override — draw node id labels.

None
max_slots int

Maximum FIFO slots drawn per link; longer queues show a +k overflow tag (when agent ids are shown).

8
highlight_links Iterable[int] | None

Optional link ids drawn dashed-red (e.g. a bottleneck).

None
title str | None

Optional title drawn on the axes.

None

Returns:

Type Description
Axes

The matplotlib axes containing the drawing.

NetworkLayout

NetworkLayout(positions: dict[str, tuple[float, float]], labels: dict[str, str], links: dict[str, tuple[tuple[float, float], tuple[float, float], float]], xlim: tuple[float, float], ylim: tuple[float, float], scale: float, min_link: float)

Precomputed drawing geometry for a network (positions, link curves, limits).

Built once and reused for every frame so the video does not jitter and the per-frame cost stays low. Create it with :meth:from_state (live simulation) or :meth:from_history (a loaded history file).

Attributes:

Name Type Description
positions

str(node) -> (x, y) drawing positions.

labels

str(node) -> label (the node's original id, stringified).

links

str(link_id) -> (p0, p1, rad) endpoint positions + arc.

xlim / ylim

fixed axis limits including a margin.

scale

A characteristic length used to size markers/offsets.

min_link

The shortest drawn link length (data units) — sets how big a marker can be before agents on a link overlap, so markers/labels shrink automatically on dense networks (e.g. a large grid).

from_state classmethod

from_state(state: NetworkState) -> NetworkLayout

Build a layout from a live compiled :class:NetworkState.

from_history classmethod

from_history(history: SimulationHistory) -> NetworkLayout

Build a layout from a recorded :class:SimulationHistory.

resolve_palette

resolve_palette(frames: Sequence[Frame], palette: dict[str, str] | None = None, color_by: str | Callable[..., str] | None = DEFAULT_COLOR_BY) -> dict[str, str]

Return a stable category -> colour map for a sequence of frames.

Categories keep any colours given in palette; the rest are assigned from the Okabe-Ito palette in order of first appearance across all frames, so agent colours are stable over the whole animation (no per-frame flicker). With color_by="next_link" the categories are next-link ids (the palette cycles when there are more than eight).

expand_frame_indices

expand_frame_indices(n_frames: int, subsample: float) -> list[int]

Map output-frame index -> source-frame index for a playback subsample.

subsample is frames-shown-per-step: >1 repeats each step's frame (a slower video), <1 drops steps (a faster video), 1 is one-to-one.