Skip to content

Commit 4fae82b

Browse files
authored
Merge pull request #1567 from seqeralabs/fix/symfan-terminus-half-pitch
fix(layout): seat an orphaned half-pitch branch on a full grid row
2 parents 9fcbb58 + 2983c55 commit 4fae82b

11 files changed

Lines changed: 322 additions & 109 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
%%metro title: Paired-track demo
2+
%%metro diamond_style: symmetric
3+
%%metro style: dark
4+
%%metro legend: bl
5+
%%metro files: pairs_in | FILE | Input set
6+
%%metro files: out_a1 | OUT | Track A
7+
%%metro file: out_a2 | OUT | Output A2
8+
%%metro file: out_a3 | OUT | Output three
9+
%%metro file: out_b | OUT | Probe value
10+
%%metro files: out_c | OUT | Chain out
11+
%%metro line: track_a | Track A | #0570b0
12+
%%metro line: track_b | Track B | #d94801
13+
14+
graph LR
15+
subgraph input_sec [Inputs]
16+
pairs_in[ ]
17+
convert_a[Convert one\ntrack a]
18+
convert_b[Convert two\ntrack b]
19+
20+
pairs_in -->|track_a| convert_a
21+
pairs_in -->|track_b| convert_b
22+
end
23+
24+
subgraph branch_sec [Branching stage]
25+
stage_a1[Upstream\nprimary stage]
26+
stage_a2[Filter step]
27+
stage_a3[Fork\nannotation]
28+
stage_a4[Terminal\nsegment]
29+
out_a1[ ]
30+
out_a2[ ]
31+
out_a3[ ]
32+
33+
pairs_in -->|track_a| stage_a1
34+
pairs_in -->|track_b| stage_a1
35+
stage_a1 -->|track_a,track_b| stage_a2
36+
stage_a2 -->|track_a,track_b| out_a1
37+
stage_a2 -->|track_a,track_b| stage_a3
38+
stage_a3 -->|track_a,track_b| out_a2
39+
stage_a3 -->|track_a,track_b| stage_a4
40+
stage_a4 -->|track_a,track_b| out_a3
41+
end
42+
43+
subgraph probe_sec [Hub]
44+
stage_b1[merge node]
45+
out_b[ ]
46+
47+
convert_a -->|track_a| stage_b1
48+
convert_b -->|track_b| stage_b1
49+
stage_b1 -->|track_a,track_b| out_b
50+
end
51+
52+
subgraph chain_sec [Downstream chain]
53+
split_a[Region split\ntrack a]
54+
split_b[Region split\ntrack b]
55+
stage_c1[Assemble\nstage]
56+
stage_c2[Collate\nmerge step]
57+
stage_c3[Fit and rank\nstage]
58+
stage_c4[Adjust to\nscale]
59+
out_c[ ]
60+
61+
convert_a -->|track_a| split_a
62+
convert_b -->|track_b| split_b
63+
split_a -->|track_a| stage_c1
64+
split_b -->|track_b| stage_c1
65+
stage_c1 -->|track_a,track_b| stage_c2
66+
stage_c2 -->|track_a,track_b| stage_c3
67+
stage_c3 -->|track_a,track_b| stage_c4
68+
stage_c4 -->|track_a,track_b| out_c
69+
end

examples/topologies/tn_wes_input_fan_somatic_tree.mmd

Lines changed: 0 additions & 69 deletions
This file was deleted.

scripts/gallery.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,13 +1833,13 @@ gallery:
18331833
station. The output's branch peels off between the two rather than
18341834
overrunning the successor's column, so the icon clears the downstream
18351835
stations and their labels."
1836-
- id: tn_wes_input_fan_somatic_tree
1836+
- id: paired_input_fan_branch_tree
18371837
source_dir: examples/topologies
18381838
category: Feature Showcase
18391839
description:
1840-
"A tumour/normal WES map: the input source fans to two CRAM->BAM
1841-
converters and threads the germline+tumour bundle cleanly between them to
1842-
the somatic caller, whose dead-end VCF outputs peel off without crowding
1840+
"A paired-input map: the source fans to two stacked converters and
1841+
threads the two-track bundle cleanly between them to the branch tree,
1842+
whose dead-end outputs peel off onto full grid rows without crowding
18431843
their neighbours."
18441844
- id: single_line_dual_source_stacked_exit
18451845
source_dir: examples/topologies

src/nf_metro/layout/CONTRACT.md

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,10 @@ pass:
247247
grid snap (6.4).
248248
- `graph.half_grid_station_ids` - written by Stage 6.3 (`center_ports` only)
249249
and Stage 6.17 (`diamond_style='symmetric'`); read by the Stage 6.4 grid
250-
snap, which must skip these half-pitch stations. Stage 6.17 runs after the
251-
last snap, so its writes mark branches for the invariant tests / straddle
252-
guard rather than feeding a later snap.
250+
snap, which must skip these half-pitch stations. Stage 6.18 both reads the
251+
set and clears the marking off any station it seats back on a full row, so
252+
the post-layout readers (the straddle guard, the co-fanned drop-clearance
253+
rule in `routing/intra_handlers.py`) see only stations still at half pitch.
253254
- `graph.symfan_trunk_station_ids` - written by Stage 6.3 (`center_ports` only);
254255
read by the Stage 6.4 grid snap, which must skip these source/trunk stations
255256
so they stay on the symfan's local frame instead of snapping to a rowspan
@@ -878,7 +879,8 @@ in pipeline order.
878879
- **Related tests**: `test_symfan_pairs_share_y`.
879880
- **Lifecycle:** invariant - 2-branch symfan pairs keep their half-pitch
880881
offsets at the final boundary (Stage 6.4 skips
881-
`graph.half_grid_station_ids`).
882+
`graph.half_grid_station_ids`); only Stage 6.18 may seat one on a full
883+
row, and only once its straddling partner has moved away.
882884

883885
### Stage 6.4: snap all Y to grid (engine.py)
884886
- **Purpose**: Final pass snapping every station and port Y to the
@@ -959,7 +961,9 @@ in pipeline order.
959961
are temporarily broken; both are restored before leaving the
960962
`if center_ports:` block.
961963
- **Lifecycle:** invariant - full-bundle columns are symmetric around
962-
the row's final trunk Y at the boundary; no later stage re-fans them.
964+
the row's final trunk Y at the boundary; no later stage re-fans them,
965+
though Stage 6.18 seats a half-pitch member on a full row once its
966+
straddling partner has moved away.
963967
*liftable:* no - one-shot, order-dependent (computes against the final
964968
trunk Y, so a premature run is wrong).
965969

@@ -1223,10 +1227,10 @@ in pipeline order.
12231227
Per-diamond, so a diamond compacts even when it shares a section with a
12241228
wider fan (which keeps its full-pitch slots) and regardless of
12251229
`center_ports`. Records the branches on
1226-
`MetroGraph.half_grid_station_ids`. Runs last, after every
1227-
trunk-settling pass, so the branches straddle the section trunk's final
1228-
Y exactly; the compaction only moves them inward toward the trunk, so it
1229-
never breaks bbox containment.
1230+
`MetroGraph.half_grid_station_ids`. Runs after every trunk-settling
1231+
pass, so the branches straddle the section trunk's final Y exactly; the
1232+
compaction only moves them inward toward the trunk, so it never breaks
1233+
bbox containment.
12301234
- **Helper**: `_apply_half_grid_symmetric_diamonds`.
12311235
- **Precondition**: Trunk Ys settled (post-6.16); `diamond_style`
12321236
is `symmetric`.
@@ -1239,7 +1243,41 @@ in pipeline order.
12391243
`test_symmetric_diamond_both_branches_deviate`,
12401244
`_guard_symmetric_diamond_branches_straddle_trunk`.
12411245
- **Lifecycle:** invariant - symmetric diamond branches keep their
1242-
half-pitch offsets at the final boundary (no later Y mutation).
1246+
half-pitch offsets at the final boundary; only Stage 6.18 may move one,
1247+
and only when its straddling partner is gone.
1248+
1249+
### Stage 6.18: orphaned half-pitch expansion (engine.py)
1250+
- **Purpose**: A half-pitch offset encodes one side of a symmetric pair
1251+
straddling the section trunk, so the pair reads as one compact grid
1252+
unit. Stage 6.10's `_align_terminus_to_upstream` may pull a terminus
1253+
member onto its producer's trunk Y, leaving the other member holding an
1254+
offset that straddles nothing and rendering as a branch stranded
1255+
between two grid rows. `_straddles_nothing` mirrors each marked
1256+
station's offset about the section's LR/RL port anchor; with no station
1257+
at the mirrored slot, the branch is seated one full row from the anchor
1258+
on the side it already sits, its half-grid marking cleared, and the
1259+
section bbox grown over the moved branch alone. Stations marked
1260+
half-grid whose settled Y is already a whole number of rows from the
1261+
anchor are left alone.
1262+
- **Helper**: `_expand_orphaned_half_grid_stations`
1263+
(`phases/fan_bundles.py`), sharing `_half_grid_frame` /
1264+
`_straddles_nothing` with the invariant test.
1265+
- **Precondition**: Every pass that places or dissolves a half-pitch pair
1266+
has run (post-6.17), so the half-grid marks are final.
1267+
- **Postcondition**: No station in `graph.half_grid_station_ids` sits half
1268+
a pitch off its section's LR/RL port anchor with the mirrored slot
1269+
empty.
1270+
- **Invariants preserved**: Trunk station Y, ports, bbox containment.
1271+
Deliberately not preserved: the half-grid marker set (the seated
1272+
station's id is discarded, so the post-layout readers see only stations
1273+
still at half pitch) and the section bbox extent, which grows over the
1274+
seated branch. No runtime `_guard_*` arms this postcondition:
1275+
`test_half_grid_stations_straddle_in_pairs` covers it across the corpus
1276+
without the abort risk a live guard would add to novel input.
1277+
- **Related tests**: `test_half_grid_stations_straddle_in_pairs`.
1278+
- **Lifecycle:** invariant - the expanded branch keeps its full-row Y at
1279+
the final boundary (no later Y mutation). The cleared marker reaches the
1280+
next `_layout_once` pass, which re-derives the marks from scratch.
12431281

12441282
## Unclear / structural-debt signals
12451283

src/nf_metro/layout/engine.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
_center_lr_entry_ports_on_fork,
111111
_convergence_source_ys,
112112
_divergence_target_ys,
113+
_expand_orphaned_half_grid_stations,
113114
_iter_symmetric_diamonds,
114115
_recenter_full_bundle_columns,
115116
_redistribute_fanout_siblings,
@@ -2007,6 +2008,13 @@ def _finalize_layout(
20072008
_apply_half_grid_symmetric_diamonds(graph, y_spacing)
20082009
_snap(graph, "6.17")
20092010

2011+
# Stage 6.18: Seat orphaned half-pitch stations on a full grid row (see
2012+
# _expand_orphaned_half_grid_stations and CONTRACT.md Stage 6.18). Must
2013+
# follow every pass that places or dissolves a half-pitch pair, so the
2014+
# half-grid marks are final.
2015+
_expand_orphaned_half_grid_stations(graph, y_spacing, section_y_padding)
2016+
_snap(graph, "6.18")
2017+
20102018
if validate:
20112019
if graph._defer_final_guards:
20122020
graph._after_final_deferred = True

src/nf_metro/layout/phase_state.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"4.1", "4.2", "4.3", "4.4", "4.5", "4.6", "4.7", "4.8", "4.9", "4.10",
6464
"5.1", "5.2", "5.3", "5.4", "5.5",
6565
"6.1", "6.2", "6.3", "6.4", "6.5", "6.6", "6.7", "6.8", "6.9", "6.10",
66-
"6.11", "6.12", "6.13", "6.14", "6.15a", "6.15", "6.16", "6.17",
66+
"6.11", "6.12", "6.13", "6.14", "6.15a", "6.15", "6.16", "6.17", "6.18",
6767
) # fmt: skip
6868

6969

@@ -106,23 +106,24 @@ class PhaseFieldSpec:
106106
"_row_y_grid_info": PhaseFieldSpec(
107107
name="_row_y_grid_info",
108108
writer_stage="1.2",
109-
reader_stages=("4.2", "6.3", "6.4"),
109+
reader_stages=("4.2", "6.3", "6.4", "6.18"),
110110
enforcement=FieldEnforcement.REQUIRE_WRITER,
111111
why=(
112112
"row-grid metadata from Stage 1.2's _align_row_y_grids; the grid-group "
113-
"port snap, fan re-centre, and grid snap read it to group same-row "
114-
"sections onto a shared pitch"
113+
"port snap, fan re-centre, grid snap and half-pitch expansion read it "
114+
"to group same-row sections onto a shared pitch"
115115
),
116116
),
117117
"half_grid_station_ids": PhaseFieldSpec(
118118
name="half_grid_station_ids",
119119
writer_stage="6.3",
120-
reader_stages=("6.4",),
120+
reader_stages=("6.4", "6.18"),
121121
enforcement=FieldEnforcement.REQUIRE_WRITER,
122122
why=(
123123
"2-branch symfan stations Stage 6.3 places at half-pitch; Stage 6.4's "
124124
"grid snap must skip them or it snaps their intentional half-grid Y to "
125-
"the full grid"
125+
"the full grid, and Stage 6.18 reads the set to find members whose "
126+
"pair partner moved to a full row"
126127
),
127128
run_condition_attr="center_ports",
128129
),

0 commit comments

Comments
 (0)