0
mirror of https://github.com/bbenchoff/OrthoRoute.git synced 2026-08-22 06:09:32 +00:00
Files
OrthoRoute/tests/test_layer_peeling.py
2026-07-28 09:00:38 -07:00

258 lines
6.7 KiB
Python

"""Warm-start layer-pair peeling regressions."""
from types import SimpleNamespace
import pytest
from orthoroute.algorithms.manhattan.hdi_stack import (
pcbway_mechanical_stack,
)
from orthoroute.algorithms.manhattan.layer_peeling import (
build_peel_plan,
coord_to_node,
infer_terminal_entry_layers,
node_to_coord,
planar_node_occupancy,
rebuild_selected_portals,
remap_selected_portals,
remap_surviving_path,
remap_surviving_paths,
stack_symmetric_internal_pairs,
validate_reduced_path,
)
from orthoroute.algorithms.manhattan.pad_escape_planner import Portal
SHAPE = (4, 3, 8)
def node(x, y, z):
return coord_to_node(x, y, z, SHAPE)
def test_node_coordinate_round_trip():
for z in range(SHAPE[2]):
for y in range(SHAPE[1]):
for x in range(SHAPE[0]):
assert node_to_coord(node(x, y, z), SHAPE) == (x, y, z)
def test_symmetric_pairs_exclude_outer_copper():
assert stack_symmetric_internal_pairs(8) == (
(1, 6),
(2, 5),
(3, 4),
)
def test_planar_occupancy_counts_distinct_per_net_node_uses():
paths = {
"A": [node(0, 0, 1), node(1, 0, 1), node(2, 0, 1)],
"B": [node(1, 0, 1), node(2, 0, 1)],
"VIA": [node(3, 2, 0), node(3, 2, 1), node(3, 2, 2)],
}
occupancy = planar_node_occupancy(paths, SHAPE)
assert occupancy[1] == 5
assert sum(occupancy) == 5
def test_peel_plan_chooses_least_occupied_symmetric_pair_and_displaced_set():
paths = {
"L1": [node(0, 0, 1), node(1, 0, 1)],
"L6": [node(0, 0, 6), node(1, 0, 6)],
"L2": [node(0, 1, 2), node(1, 1, 2)],
"VERTICAL_ONLY": [
node(3, 2, z) for z in range(SHAPE[2])
],
}
plan = build_peel_plan(paths, SHAPE)
assert plan.removed_layers == (3, 4)
assert plan.removed_planar_occupancy == 0
assert plan.displaced_nets == ()
assert plan.as_dict()["target_layers"] == 6
def test_displaced_nets_are_exactly_planar_users_of_removed_layers():
paths = {
"L1": [node(0, 0, 1), node(1, 0, 1)],
"L6": [node(0, 0, 6), node(1, 0, 6)],
"L2": [node(0, 1, 2), node(1, 1, 2)],
"L5": [node(0, 1, 5), node(1, 1, 5)],
"VERTICAL_ONLY": [
node(3, 2, z) for z in range(SHAPE[2])
],
}
plan = build_peel_plan(paths, SHAPE)
assert plan.removed_layers == (3, 4)
assert plan.displaced_nets == ()
paths["L3"] = [node(0, 2, 3), node(1, 2, 3)]
paths["L4"] = [node(0, 2, 4), node(1, 2, 4)]
plan = build_peel_plan(paths, SHAPE)
assert plan.removed_layers == (1, 6)
assert plan.displaced_nets == ("L1", "L6")
def test_via_chain_compresses_across_removed_layers():
path = [node(2, 1, z) for z in range(SHAPE[2])]
remapped = remap_surviving_path(path, SHAPE, (2, 5))
coords = [
node_to_coord(item, (SHAPE[0], SHAPE[1], 6))
for item in remapped
]
assert coords == [(2, 1, z) for z in range(6)]
validate_reduced_path(
remapped,
(SHAPE[0], SHAPE[1], 6),
pcbway_mechanical_stack(6),
)
def test_terminal_entry_layers_stop_at_first_planar_step():
path = [
node(0, 1, 0),
node(0, 1, 1),
node(0, 1, 2),
node(1, 1, 2),
node(1, 1, 3),
node(1, 1, 4),
node(1, 1, 5),
node(2, 1, 5),
node(2, 1, 6),
node(2, 1, 7),
]
assert infer_terminal_entry_layers(path, SHAPE) == (2, 5)
def test_selected_portals_rebuild_from_committed_terminal_coordinates():
source_portal = Portal(
x_idx=0, y_idx=1, pad_layer=0, delta_steps=3,
direction=1, pad_x=0.0, pad_y=0.0,
)
target_portal = Portal(
x_idx=2, y_idx=1, pad_layer=7, delta_steps=3,
direction=-1, pad_x=1.0, pad_y=1.0,
)
router = SimpleNamespace(
net_pad_ids={"NET": ("P1", "P2")},
portal_candidates={
"P1": [source_portal],
"P2": [target_portal],
},
portals={"P1": source_portal, "P2": target_portal},
)
path = [
node(0, 1, 0),
node(0, 1, 1),
node(0, 1, 2),
node(1, 1, 2),
node(2, 1, 2),
node(2, 1, 3),
node(2, 1, 4),
node(2, 1, 5),
node(2, 1, 6),
node(2, 1, 7),
]
selected, layers = rebuild_selected_portals(
router, {"NET": path}, SHAPE
)
assert layers["NET"] == (2, 2)
assert selected["NET"][0].entry_layer == 2
assert selected["NET"][1].entry_layer == 2
assert selected["NET"][0] is not source_portal
def test_serialized_back_portal_moves_to_new_outer_layer():
front = Portal(
x_idx=0, y_idx=1, pad_layer=0, delta_steps=3,
direction=1, pad_x=0.0, pad_y=0.0,
)
back = Portal(
x_idx=2, y_idx=1, pad_layer=9, delta_steps=3,
direction=-1, pad_x=1.0, pad_y=1.0,
)
shape = (4, 3, 8)
path = [
coord_to_node(0, 1, 0, shape),
coord_to_node(0, 1, 1, shape),
coord_to_node(1, 1, 1, shape),
coord_to_node(2, 1, 1, shape),
coord_to_node(2, 1, 2, shape),
coord_to_node(2, 1, 3, shape),
coord_to_node(2, 1, 4, shape),
coord_to_node(2, 1, 5, shape),
coord_to_node(2, 1, 6, shape),
coord_to_node(2, 1, 7, shape),
]
selected, layers = remap_selected_portals(
{"NET": (front, back)},
{"NET": path},
shape,
source_layer_count=10,
)
assert layers["NET"] == (1, 1)
assert selected["NET"][0].pad_layer == 0
assert selected["NET"][1].pad_layer == 7
def test_remap_rejects_nonadjacent_original_walk():
bad = [node(0, 0, 1), node(3, 0, 1)]
with pytest.raises(ValueError, match="not lattice-adjacent"):
validate_reduced_path(
bad,
SHAPE,
pcbway_mechanical_stack(8),
)
def test_bulk_remap_excludes_displaced_and_validates_survivors():
paths = {
"DISPLACED_A": [node(0, 0, 1), node(1, 0, 1)],
"DISPLACED_B": [node(0, 0, 6), node(1, 0, 6)],
"BUSY_2": [
node(0, 1, 2), node(1, 1, 2), node(2, 1, 2)
],
"BUSY_5": [
node(0, 1, 5), node(1, 1, 5), node(2, 1, 5)
],
"BUSY_3": [
node(0, 2, 3), node(1, 2, 3), node(2, 2, 3)
],
"BUSY_4": [
node(0, 2, 4), node(1, 2, 4), node(2, 2, 4)
],
"SURVIVOR": [node(2, 1, z) for z in range(SHAPE[2])],
"UNROUTED": [],
}
plan = build_peel_plan(paths, SHAPE)
survivors = remap_surviving_paths(
paths,
plan,
SHAPE,
pcbway_mechanical_stack(6),
)
assert set(survivors) == {
"BUSY_2",
"BUSY_3",
"BUSY_4",
"BUSY_5",
"SURVIVOR",
}