Routing
Routing turns the laid-out MetroGraph (stations, ports, and junctions
with coordinates) into a list of RoutedPath polylines, one per edge.
The entry point is route_edges in
src/nf_metro/layout/routing/core.py.
Lines are drawn as horizontal runs joined by 45-degree diagonal transitions; inter-section edges use L-shaped (horizontal + vertical) routing.
Rail mode short-circuit
Section titled “Rail mode short-circuit”Before the normal dispatch, route_edges checks the graph’s
line_spread (a LineSpread of BUNDLE / CENTERED / RAILS):
- When
line_spread is LineSpread.RAILS, the whole graph is routed byroute_rail_edgesinrouting/rail.pyandroute_edgesreturns early. - When only some sections opt into rails (
has_rail_sections), the edges internal to those rail sections are routed byroute_rail_edgesup front; the rest fall through to the normal handler chain below.
Rail routing does not bundle: each line runs along a single fixed
horizontal rail Y (assigned in layout/rail_mode.py), so each edge is a
straight horizontal run at its line’s rail Y, and shared stations render
as interchange pills bridging the rails.
Mermaid source
%%metro title: Rail mode: variant-calling sample routes%%metro style: dark%%metro line_spread: rails%%metro label_angle: 45%%metro line: germline | Germline | #2db572%%metro line: tumor_only | Tumour-only | #f4a300%%metro line: pair_n | Pair (normal) | #0570b0%%metro line: pair_t | Pair (tumour) | #e63946%%metro legend_combo: pair_n, pair_t | Tumour-normal pair%%metro legend: bl%%metro file: cram_in | CRAM%%metro file: samples_csv | CSV | Samples%%metro file: vcf_out | VCF%%metro off_track: samples_csv
graph LR subgraph calling [Variant calling] cram_in[ ] align[Alignment] markdup[Mark duplicates] bqsr[BQSR] samples_csv[ ] callvar[Call variants] somatic[Somatic filter] concord[Concordance]
cram_in -->|germline,tumor_only,pair_n,pair_t| align align -->|germline,tumor_only,pair_n,pair_t| markdup markdup -->|germline,tumor_only,pair_n,pair_t| bqsr samples_csv -->|pair_n,pair_t| bqsr
bqsr -->|germline,tumor_only,pair_n,pair_t| callvar callvar -->|tumor_only,pair_n,pair_t| somatic callvar -->|germline| concord somatic -->|pair_n,pair_t| concord end
subgraph annotate [Annotation and output] norm[Normalize] vep[Annotate] vcf_out[ ]
norm -->|germline,tumor_only,pair_n,pair_t| vep vep -->|germline,tumor_only,pair_n,pair_t| vcf_out endCLI command
nf-metro render examples/rail_mode.mmd -o rail_mode.svgRendered map
Dispatch order
Section titled “Dispatch order”route_edges first builds a _RoutingCtx (a dataclass of shared
pre-computed state: merge-junction classification, fold X, bundle info,
per-station offsets, fork stations, etc.), then routes each edge by
trying a fixed sequence of handler functions in priority order. The
first handler that returns a RoutedPath wins; handlers that do not
apply return None.
The order in route_edges is:
_route_inter_section- edges crossing a section boundary (port/junction to port/junction). Dispatches internally to a large family of sub-handlers (L-shape, top-entry L-shape, left/right-entry wraps, TB bottom-exit, merge trunk/branch, bypass, stepped descent, inter-row corridors, around-section-below)._route_tb_section- edges touching aTBsection. Dispatches over the ordered_TB_SECTION_SHAPEStuple (first match wins): internal vertical drops (_route_tb_internal), internal station to a LEFT/RIGHT exit port (_route_tb_lr_exit), LEFT/RIGHT entry port to an internal station (_route_tb_lr_entry), and TOP/BOTTOM port to an internal station (_route_perp_entry). Each shape describes a centreline and fans it through the bundle builder (build_tapered_bundle/build_offset_bundle), so no handler hand-assembles per-line points or curve radii; the perpendicular-entry corridor variant (_route_perp_entry_from_corridor) routes the same way._route_entry_runway- flow-side entry port to a deep internal station: compresses the diagonal into the entry region and runs a horizontal runway past the bypassed early-layer stations._route_intra_section- the general intra-section case: diagonals, cross-row fold routing, and straight lines. This is also the fallback for port/junction-to-port/junction edges that the inter- section family did not claim.
The fan-out below illustrates all four handler families at once: inter-section L-shapes connect the sections, TB-section routing applies inside the vertical connector, entry runways compress the diagonal at each section’s input port, and intra-section diagonals handle the station-to-station edges within each horizontal section.
Mermaid source
%%metro title: Section Directions%%metro style: dark%%metro line: rna | RNA-seq | #2db572%%metro line: dna | DNA-seq | #e63946%%metro legend: bl
graph LR subgraph preprocessing [Pre-processing] fastqc[FastQC] trim[Trimming] fastqc -->|rna,dna| trim end
subgraph rna_analysis [RNA Analysis] star[STAR] salmon[Salmon] star -->|rna| salmon end
subgraph dna_analysis [DNA Analysis] bwa[BWA-MEM] gatk[GATK] bwa -->|dna| gatk end
subgraph postprocessing [Post-processing] %%metro direction: TB samtools[SAMtools] picard[Picard] bedtools[BEDTools] samtools -->|rna,dna| picard picard -->|rna,dna| bedtools end
subgraph reporting [Reporting] multiqc[MultiQC] report[Report] multiqc -->|rna,dna| report end
trim -->|rna| star trim -->|dna| bwa salmon -->|rna| samtools gatk -->|dna| samtools bedtools -->|rna,dna| multiqcCLI command
nf-metro render examples/guide/04_directions.mmd -o 04_directions.svgRendered map
After all edges are routed, route_edges runs a series of post-passes
that adjust the assembled polylines as a set (for example
_spread_diagonal_bundles, _materialize_gap_slots,
_materialize_trunk_slots, _coincide_same_line_tracks).
Bundles and offsets
Section titled “Bundles and offsets”When several lines travel between the same pair of endpoints they form a
bundle. Per-line offsets (computed by compute_station_offsets,
applied through _RoutingCtx.station_offsets) fan the bundle out into
parallel tracks so individual lines stay visually distinct. Bundle
ordering is preserved across multi-corner paths by handedness-aware
offset propagation at each corner (the corner-radius helpers live in
routing/corners.py). The runtime guard
check_bundle_order_preserved (in routing/invariants.py) catches any
regression where a line crosses over its bundle-mates.
The debug overlay below makes the offset geometry visible: each line’s parallel track and the station markers it must pass through.
Rendered map
Render-time guards
Section titled “Render-time guards”assert_render_curve_invariants (in routing/invariants.py) runs a set of
correctness checks on the final route_edges output every render — the exact
geometry the renderer is about to draw — so a defective route aborts the
render with a message naming the offending edge rather than being shipped. It
is always on, independent of compute_layout’s validate flag.
Among these are the endpoint guards, which assert that a routed segment terminates at a real anchor rather than hanging in open space:
check_merge_branches_meet_trunk— a merge feeder must land on its trunk’s channel (merge junctions only).check_no_hanging_routes— the general backstop: every route’s two endpoints must each lie within2 * CURVE_RADIUSof a station/port/junction marker or of another route it joins (a bundle mate, a branch onto a trunk, a peel-off). Rail-mode endpoints are skipped (a rail stub terminates on its rail). This generalises the merge-only check to any route family; the family-specific checks remain as sharper diagnostics.
The descriptor catalogue (WRAP_TABLE)
Section titled “The descriptor catalogue (WRAP_TABLE)”routing/inter_section.py
defines WRAP_TABLE, a declarative mirror of the inter-section
if-cascade. It is a documentation reference only: nothing in
routing consumes it at runtime. Each entry is keyed by
(exit_side, entry_side, drow_sign, dcol_sign)where exit_side is None when the source is a junction without a port
side, and drow_sign / dcol_sign are sign() values in
{-1, 0, +1} describing the section-grid step. Each value is a
WrapDescriptor recording the RouteKind (e.g. L_SHAPE,
TOP_ENTRY_L_SHAPE, LEFT_ENTRY_WRAP, RIGHT_ENTRY_WRAP,
TB_BOTTOM_EXIT), a ChannelKind, and a TurnSequence - the ordered
list of Corners (each an (incoming, outgoing) Direction pair).
TurnSequence.parity counts how many times the corner handedness
(CW / CCW) flips along the path; it is the contract pinned by
tests/test_inter_section_descriptor.py,
which also sanity-checks that every key is well-formed and that the
table stays non-empty. Same-row L-shapes and the straight TB
(BOTTOM, TOP, 1, 0) drop are intentionally absent because the
dispatcher handles those degenerate cases directly.
Module map
Section titled “Module map”| Module | Responsibility |
|---|---|
core.py | route_edges dispatcher; re-exports handlers from sibling modules for backward-compatible imports |
context.py | _RoutingCtx dataclass and _build_routing_context; per-station offset helpers; shared section-geometry helpers (_resolve_section_col, _has_intervening_sections, compute_junction_fan_info, …) |
inter_section_handlers.py | handler 1 family: bypass, left/right entry wraps, around-section, inter-row corridors, stepped descent, L-shape |
tb_handlers.py | TB section shapes dispatched by _route_tb_section over _TB_SECTION_SHAPES (_route_tb_internal, _route_tb_lr_exit, _route_tb_lr_entry, _route_perp_entry, _route_perp_entry_from_corridor) and _compute_diagonal_placement |
intra_handlers.py | _route_entry_runway and _route_intra_section (the general intra-section fallback) |
bundle.py | constructive bundle-curve builders (build_concentric_bundle, build_tapered_bundle, build_offset_bundle); fans a centreline into per-line offset paths with concentric corners |
centrelines.py | centreline templates and bundle-gathering helpers (gather_member_edges, route_along, route_tapered, …) layered over bundle.py |
postprocess.py | post-routing passes: diagonal bundle spread and bubble-station centring |
normalize.py | channel and trunk normalization passes (_materialize_gap_slots, htrunk restacking, riser/port-approach alignment, …) |
common.py | RoutedPath, Direction, bundle/channel helpers |
corners.py | corner radii and curve smoothing |
inter_section.py | WRAP_TABLE descriptor catalogue (documentation reference; not used at runtime) |
offsets.py | per-station Y offsets for parallel lines |
reversal.py | fold/reversal (serpentine row) routing |
invariants.py | runtime routing guards (check_bundle_order_preserved) |
rail.py | route_rail_edges straight-rail router for rail mode |