0
mirror of https://github.com/bbenchoff/OrthoRoute.git synced 2026-08-21 06:00:57 +00:00
Files
OrthoRoute/tests/test_synthetic_boards.py
Brian Benchoff a17e67f668 Fix the CPU convergence death spiral: four stacked engine bugs
Measured on synthetic backplanes (benchmarks/run_benchmark.py), the
engine diverged on trivially-routable boards: 8 straight-across nets hit
overuse=199 at the 250-iteration cap. Root causes, in the order found:

1. Ownership-as-wall: _filter_roi_by_ownership REMOVED foreign-owned
   nodes from the ROI - including a net's own endpoints when a
   neighbor's barrel claimed them ('BUG: src not in ROI'), making
   barrel conflicts unresolvable. Replaced with ownership-as-cost:
   solvers take an optional ROI-local node_penalty; entering a
   foreign-owned node costs owner_penalty_base * pres_fac, so barrels
   are negotiated like any other congestion. Also fixed global_to_roi
   being rebuilt E-sized instead of N-sized.

2. Escape planner placed GUARANTEED-colliding portals: pads 6 grid
   steps apart (2.54mm headers) escaping toward each other with
   min_steps=3 land both portal vias on the SAME cell, and the
   collision resolver can only shorten (already at min). Fixed
   direction policy (away from single neighbors, uniform on ties) and
   added a hard one-portal-per-cell guarantee in _try_create_portal.

3. _retarget_portals_for_net had a sign bug: it flipped direction and
   then applied the flip formula with the already-flipped sign, moving
   the via 2*delta FURTHER on the original side - off the lattice for
   edge pads (IndexError in ROI extraction). Rewritten with correct
   math, bounds check, cell-uniqueness check, and deduped src/dst copy.

4. Planner/graph layer contract mismatch: portal entry_layer was drawn
   from range(1, layers) which includes B.Cu - a layer build_graph
   gives ZERO edges on >2-layer boards, so those portals seeded
   disconnected nodes and their nets could never route.

Results (routed / overuse / iterations):
  2x4 neighbor:  3/4, 40, cap(250)  ->  4/4, 0, 1
  2x8 neighbor:  8/8, 199, cap      ->  8/8, 0, 51
  2x16 pairs:   14/16, 12, cap      -> 16/16, 0, 47
  4x40 pairs 6L: untested           -> 80/80, 0, 12

The metrics-harness test now asserts full completion and zero overuse
on the trivial board - the convergence scoreboard.
2026-07-22 16:22:19 -07:00

110 lines
4.3 KiB
Python

"""Tests for the synthetic backplane generator and metrics harness.
Also the convergence scoreboard: the 2x4 neighbor board MUST fully route
with zero overuse (it does so in 1 iteration since the ownership-as-cost /
escape-planner fixes; before them it stranded a net and hit the iteration
cap). If the completeness/overuse assertions here regress, a negotiation
change broke convergence.
"""
import pytest
from benchmarks.metrics import collect_route_metrics
from benchmarks.synthetic_boards import make_backplane
class TestGenerator:
def test_pairs_pattern(self):
board = make_backplane(connectors=2, pins_per=16, layers=4, pattern="pairs")
assert board.layer_count == 4
assert len(board.components) == 2
pads = [p for n in board.nets for p in n.pads]
assert len(board.nets) == 16 # 32 pads / 2 per net
assert len(pads) == 32
assert all(len(n.pads) == 2 for n in board.nets)
def test_neighbor_pattern(self):
board = make_backplane(connectors=2, pins_per=8, layers=4, pattern="neighbor")
assert len(board.nets) == 8
for net in board.nets:
a, b = net.pads
# Straight across: same pin slot, different connector
assert a.component_id != b.component_id
assert a.position.y == b.position.y
def test_bus_pattern(self):
board = make_backplane(connectors=4, pins_per=8, layers=6, pattern="bus")
assert len(board.nets) == 8
assert all(len(n.pads) == 4 for n in board.nets)
def test_every_pad_on_exactly_one_net(self):
board = make_backplane(connectors=3, pins_per=10, layers=4,
pattern="pairs", rows=2)
seen = set()
for net in board.nets:
for pad in net.pads:
assert pad.id not in seen, f"pad {pad.id} on two nets"
assert pad.net_id == net.id
seen.add(pad.id)
assert len(seen) == 30
def test_pairs_deterministic(self):
b1 = make_backplane(connectors=2, pins_per=12, layers=4, seed=7)
b2 = make_backplane(connectors=2, pins_per=12, layers=4, seed=7)
pairing1 = [[p.id for p in n.pads] for n in b1.nets]
pairing2 = [[p.id for p in n.pads] for n in b2.nets]
assert pairing1 == pairing2
def test_rejects_bad_params(self):
with pytest.raises(ValueError):
make_backplane(pins_per=7, rows=2) # doesn't divide
with pytest.raises(ValueError):
make_backplane(pattern="starburst")
with pytest.raises(ValueError):
make_backplane(connectors=3, pattern="neighbor") # odd
@pytest.fixture(scope="module")
def routed():
from orthoroute.algorithms.manhattan.unified_pathfinder import (
PathFinderConfig, UnifiedPathFinder,
)
board = make_backplane(connectors=2, pins_per=4, layers=4,
pattern="neighbor")
config = PathFinderConfig()
config.portal_x_snap_max = 0.75
config.max_iterations = 12 # bound runtime; convergence not asserted
pf = UnifiedPathFinder(config=config, use_gpu=False)
pf.initialize_graph(board)
pf.map_all_pads(board)
pf.precompute_all_pad_escapes(board)
pf.prepare_routing_runtime()
pf.route_multiple_nets(board.nets)
pf.emit_geometry(board)
return pf, board
class TestMetricsHarness:
def test_metrics_structure(self, routed):
pf, board = routed
m = collect_route_metrics(pf, board, timings={"route": 1.0})
assert m["board"]["nets"] == 4
assert m["board"]["pads"] == 8
assert m["lattice"]["layers"] == 4
assert m["completion"]["total_nets"] == 4
assert m["completion"]["routed_nets"] == 4, "trivial board must fully route"
assert m["completion"]["complete"] is True
assert m["convergence"]["overuse_total"] == 0, "trivial board must converge"
assert m["convergence"]["iterations"] is not None
assert m["copper"]["wirelength_mm"] > 0
assert m["copper"]["layers_used"], "no layer carried copper"
assert all(int(z) < 4 for z in m["copper"]["lateral_steps_per_layer"])
assert m["timings_s"]["route"] == 1.0
def test_metrics_json_serializable(self, routed):
import json
pf, board = routed
json.dumps(collect_route_metrics(pf, board))