Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions examples/topologies/serpentine_rl_bundle.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
%%metro title: Serpentine RL Bundle
%%metro style: dark
%%metro line: dna | DNA | #4a90d9
%%metro line: rna | RNA | #e63946
%%metro grid: preprocessing | 0,0
%%metro grid: gatk | 1,0
%%metro grid: variant_calling | 2,0
%%metro grid: normalization | 2,1
%%metro grid: consensus | 1,1
%%metro grid: realignment | 0,1

graph LR
subgraph preprocessing [Pre-processing]
%%metro exit: right | dna, rna
pp1[Trim]
pp2[Align]
pp1 -->|dna,rna| pp2
end
subgraph gatk [GATK]
%%metro entry: left | dna, rna
%%metro exit: right | dna, rna
gk1[BQSR]
gk2[Apply]
gk1 -->|dna,rna| gk2
end
subgraph variant_calling [Variant Calling]
%%metro entry: left | dna, rna
%%metro exit: bottom | dna, rna
vc1[Call]
vc2[Filter]
vc1 -->|dna,rna| vc2
end
subgraph normalization [Normalization]
%%metro direction: RL
%%metro entry: top | dna, rna
%%metro exit: left | dna, rna
nz1[Decompose]
nz2[Norm]
nz1 -->|dna,rna| nz2
end
subgraph consensus [Consensus]
%%metro direction: RL
%%metro entry: right | dna, rna
%%metro exit: left | dna, rna
cs1[Merge]
cs2[Vote]
cs1 -->|dna,rna| cs2
end
subgraph realignment [Realignment]
%%metro direction: RL
%%metro entry: right | dna, rna
rl1[Realign]
rl2[Recall]
rl1 -->|dna,rna| rl2
end
pp2 -->|dna,rna| gk1
gk2 -->|dna,rna| vc1
vc2 -->|dna,rna| nz1
nz2 -->|dna,rna| cs1
cs2 -->|dna,rna| rl1
9 changes: 9 additions & 0 deletions scripts/gallery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,15 @@ gallery:
"A two-line bundle folds through a 2x3 section grid, wrapping twice. Each wrap
returns through its own inter-row gap, so the two stacked return runs stay on
distinct channels rather than collapsing onto one."
- id: serpentine_rl_bundle
source_dir: examples/topologies
category: Serpentine Layout
description:
"A true boustrophedon fold of a two-line bundle: the top row flows left to
right, drops straight down the right edge, and the return row flows right to
left via direction: RL. The bundle stays on the outside of the fold the whole
way round (top, right, then bottom) with no crossover, and the descent column
aligns under the exit so the drop is a clean U."
- id: branch_fold_forward
source_dir: examples/topologies
category: Serpentine Layout
Expand Down
97 changes: 77 additions & 20 deletions src/nf_metro/layout/phases/ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,7 @@ def _align_perpendicular_exit_port(
_set_port_y(graph, port_id, exit_y)

_align_drop_target_trunk(graph, port_id, exit_x)
_align_horizontal_drop_target(graph, port_id, exit_x)


def _align_drop_target_trunk(graph: MetroGraph, port_id: str, exit_x: float) -> None:
Expand Down Expand Up @@ -1032,29 +1033,85 @@ def _align_drop_target_trunk(graph: MetroGraph, port_id: str, exit_x: float) ->
graph._cross_column_perp_bridges.add(tgt_section.id)
continue
delta = exit_x - trunk_x
if abs(delta) < SAME_COORD_TOLERANCE:
if abs(delta) >= SAME_COORD_TOLERANCE:
_shift_section_trunk(graph, tgt_section, delta, exit_x)


def _align_horizontal_drop_target(
graph: MetroGraph, port_id: str, exit_x: float
) -> None:
"""Shift a horizontal-flow drop target's trunk so its perp entry aligns to X.

A perpendicular exit dropping into a horizontal-flow (LR/RL) section's
TOP/BOTTOM entry needs the descent to run straight: the entry port must sit
under the exit X. The whole trunk shifts by the same delta, so the entry port
lands on the exit X while the first station keeps its station-elbow offset and
the drop turns into it below. Unlike a TB target (``_align_drop_target_trunk``)
the first station cannot sit on the drop column -- a perpendicular port sharing
a horizontal-flow station's X is a station-as-elbow violation -- so the
alignment reference is the entry port, not the trunk's first station.
"""
for tgt_section, entry_port in _horizontal_drop_targets(graph, port_id):
if not _drop_x_within_section(tgt_section, exit_x):
graph._cross_column_perp_bridges.add(tgt_section.id)
continue
bbox_right = tgt_section.bbox_x + tgt_section.bbox_w
for sid in tgt_section.station_ids:
port = graph.ports.get(sid)
if port is not None and port.side in (PortSide.LEFT, PortSide.RIGHT):
continue
st = graph.stations.get(sid)
if st:
st.x += delta
if port:
port.x += delta
# Grow the box only if the shifted trunk would otherwise overrun it.
overrun = (exit_x + CURVE_RADIUS) - bbox_right
if overrun > 0:
tgt_section.bbox_w += overrun
delta = exit_x - entry_port.x
if abs(delta) >= SAME_COORD_TOLERANCE:
_shift_section_trunk(graph, tgt_section, delta, exit_x)


def _shift_section_trunk(
graph: MetroGraph, section: Section, delta: float, exit_x: float
) -> None:
"""Shift a section's trunk by *delta*, growing the box to fit the new extent.

The trunk -- internal stations and the perpendicular drop entry/exit ports --
moves; the flow-side LEFT/RIGHT ports stay on the grid column edge so a source
and target stacked in one column keep their shared right edge. The box widens
only if the shifted trunk would overrun ``exit_x`` plus a corner radius.
"""
bbox_right = section.bbox_x + section.bbox_w
for sid in section.station_ids:
port = graph.ports.get(sid)
if port is not None and port.side in (PortSide.LEFT, PortSide.RIGHT):
continue
st = graph.stations.get(sid)
if st:
st.x += delta
if port:
port.x += delta
overrun = (exit_x + CURVE_RADIUS) - bbox_right
if overrun > 0:
section.bbox_w += overrun


def _drop_targets(graph: MetroGraph, port_id: str) -> Iterator[Section]:
"""Yield TB/BT sections reached from an exit port via a vertical drop.
"""Yield vertical-flow (TB/BT) sections reached from a perp exit drop."""
return (
sec
for sec, _port in _perp_drop_targets(graph, port_id)
if lanes_run_along_x(sec.direction)
)


def _horizontal_drop_targets(
graph: MetroGraph, port_id: str
) -> Iterator[tuple[Section, Port]]:
"""Yield (horizontal-flow LR/RL section, its perp entry port) for a drop."""
return (
(sec, port)
for sec, port in _perp_drop_targets(graph, port_id)
if lanes_run_along_y(sec.direction)
)


def _perp_drop_targets(
graph: MetroGraph, port_id: str
) -> Iterator[tuple[Section, Port]]:
"""Yield (section, its TOP/BOTTOM entry port) reached from a perp exit drop.

Follows the exit port's edges (directly or through a fan-out junction) to
any TOP/BOTTOM entry port on a TB/BT section.
Follows the exit port's edges, directly or through a fan-out junction, to any
TOP/BOTTOM entry port and its owning section.
"""
for edge in graph.edges_from(port_id):
targets = [edge.target]
Expand All @@ -1069,8 +1126,8 @@ def _drop_targets(graph: MetroGraph, port_id: str) -> Iterator[Section]:
):
continue
sec = graph.sections.get(tp.section_id)
if sec and sec.direction in ("TB", "BT"):
yield sec
if sec:
yield sec, tp


def _align_lr_exit_port(
Expand Down
7 changes: 6 additions & 1 deletion src/nf_metro/layout/routing/perp.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

from __future__ import annotations

from nf_metro.layout.geometry import lanes_run_along_x
from nf_metro.layout.geometry import lanes_run_along_x, lanes_run_along_y
from nf_metro.layout.routing.common import needs_perp_approach_fan
from nf_metro.layout.routing.context import (
_get_offset,
Expand Down Expand Up @@ -126,6 +126,11 @@ def _perp_entry_crossing_x(
feeder_sec = ctx.graph.sections.get(section_id) if section_id else None
if feeder_sec is not None and lanes_run_along_x(feeder_sec.direction):
return port_x + _tb_x_offset(ctx, source, line_id, section_id)
if feeder_sec is not None and lanes_run_along_y(feeder_sec.direction):
# Horizontal-flow feeder: the up-and-over drop lands each line on the
# entry port's own per-line offset, so the crossing tracks that, not the
# feeder's section lane.
return port_x + _get_offset(ctx, entry_port_id, line_id)
return port_x - max_index * ctx.offset_step


Expand Down
12 changes: 10 additions & 2 deletions src/nf_metro/layout/routing/reversal.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,20 @@ def _detect_tb_bottom_top_entries(
src_port
and not src_port.is_entry
and src_port.side == PortSide.BOTTOM
and src.section_id in tb_sections
):
continue
if lanes_run_along_x(section.direction):
vertical_receivers.setdefault(src.section_id, set()).add(sec_id)
# Vertical (TB/BT) receiver: a straight column continuation.
# Only a vertical-flow feeder seeds the positive_fan cascade;
# a horizontal-flow feeder drops in on the trunk column with no
# order flip, so it needs no marking.
if src.section_id in tb_sections:
vertical_receivers.setdefault(src.section_id, set()).add(sec_id)
else:
# Horizontal (LR/RL) receiver: the BOTTOM-exit drop turns into
# the trunk through a corner that reverses bundle ordering,
# whatever the feeder's flow axis (a horizontal-flow feeder is
# the true-serpentine fold, an LR row folding into an RL row).
reversed_secs.add(sec_id)

# BFS: propagate positive_fan through vertical chains.
Expand Down
Loading
Loading