0
mirror of https://github.com/bbenchoff/OrthoRoute.git synced 2026-08-21 22:09:17 +00:00
Files
OrthoRoute/benchmarks/metrics.py
2026-07-27 18:16:41 -07:00

233 lines
8.6 KiB
Python

"""Routing metrics collection.
Turns a routed PathFinderRouter into a JSON-serializable dict answering
the questions that matter for the speed / fewer-layers work: which layers
actually carry copper, how much wirelength and how many vias, did every
net route, how many iterations did convergence take, and how long each
phase cost. Run-over-run comparison is the whole point - keep keys stable.
"""
import subprocess
from collections import Counter
from typing import Dict, Optional
def collect_route_metrics(pf, board, timings: Optional[Dict[str, float]] = None) -> Dict:
"""Collect metrics from a routed PathFinderRouter instance.
Args:
pf: Router after route_multiple_nets (and ideally emit_geometry).
board: The routed domain Board.
timings: Optional {phase_name: seconds} measured by the caller.
Returns:
JSON-serializable metrics dict.
"""
plane = pf.lattice.x_steps * pf.lattice.y_steps
pitch = pf.lattice.pitch
preferred_axis = getattr(
pf.lattice,
"get_legal_axis",
lambda layer: "h" if layer % 2 == 1 else "v",
)
lateral_steps_per_layer: Dict[int, int] = {}
preferred_steps_per_layer: Dict[int, int] = {}
wrong_way_steps_per_layer: Dict[int, int] = {}
via_transitions = 0
via_layer_steps = 0
routed_path_ids = set()
trivial_path_ids = set()
for net_id, path in pf.net_paths.items():
if len(path) == 1:
trivial_path_ids.add(str(net_id))
continue
if len(path) < 2:
continue
routed_path_ids.add(str(net_id))
in_vertical_run = False
vertical_xy = None
vertical_direction = 0
for a, b in zip(path, path[1:]):
za, zb = a // plane, b // plane
if za == zb:
lateral_steps_per_layer[za] = lateral_steps_per_layer.get(za, 0) + 1
xa = (a % plane) % pf.lattice.x_steps
xb = (b % plane) % pf.lattice.x_steps
axis = "h" if xa != xb else "v"
counter = (
preferred_steps_per_layer
if axis == preferred_axis(za)
else wrong_way_steps_per_layer
)
counter[za] = counter.get(za, 0) + 1
in_vertical_run = False
vertical_xy = None
vertical_direction = 0
else:
via_layer_steps += abs(zb - za)
xy = a % plane
direction = 1 if zb > za else -1
if (
not in_vertical_run
or xy != vertical_xy
or direction != vertical_direction
):
via_transitions += 1
in_vertical_run = True
vertical_xy = xy
vertical_direction = direction
raw_nets = list(board.nets)
routable_ids = {
str(getattr(net, "name", None) or getattr(net, "id", ""))
for net in raw_nets
if len(getattr(net, "pads", ())) >= 2
}
completed_ids = (routed_path_ids | trivial_path_ids) & routable_ids
unrouted_ids = sorted(routable_ids - completed_ids)
excluded_ids = {
str(net_id) for net_id in getattr(pf, "_excluded_nets", ())
}
total_lateral = sum(lateral_steps_per_layer.values())
overuse_total, overuse_count = pf.accounting.compute_overuse(pf)
barrel_conflicts = int(
getattr(pf, "_last_barrel_conflict_count", 0)
)
exact_barrel_conflicts = int(
getattr(pf, "_last_exact_barrel_conflict_count", 0)
)
path_node_conflicts = int(
getattr(pf, "_last_path_node_conflict_count", 0)
)
if hasattr(pf, "_compute_path_node_overuse"):
path_node_overuse, path_node_overuse_count = (
pf._compute_path_node_overuse()
)
else:
path_node_overuse = int(getattr(
pf, "_last_path_node_overuse_total", 0
))
path_node_overuse_count = int(getattr(
pf, "_last_path_node_overuse_count", 0
))
negotiated_overuse = int(overuse_total) + path_node_overuse
escape_conflicts = int(
getattr(pf, "_last_escape_conflict_count", 0)
)
portal_grid_conflicts = int(
getattr(pf, "_last_portal_grid_conflict_count", 0)
)
hdi_stack = getattr(getattr(pf, "config", None), "hdi_stack", None)
hdi_metrics = None
if hdi_stack is not None:
geometry = pf.get_provisional_geometry()
process_counts = Counter(
str(via.get("via_process", "unspecified"))
for via in geometry.vias
)
span_counts = Counter(
f"{via.get('from_layer')}->{via.get('to_layer')}"
for via in geometry.vias
)
nonadjacent = 0
for via in geometry.vias:
start = pf._layer_name_to_index(via.get("from_layer"))
end = pf._layer_name_to_index(via.get("to_layer"))
if start is None or end is None or abs(end - start) != 1:
nonadjacent += 1
hdi_metrics = {
"name": hdi_stack.name,
"notation": hdi_stack.notation,
"core_pair": list(hdi_stack.core_pair),
"allowed_via_spans": [
list(pair) for pair in sorted(hdi_stack.allowed_via_spans)
],
"via_process_counts": dict(sorted(process_counts.items())),
"via_span_counts": dict(sorted(span_counts.items())),
"nonadjacent_emitted_vias": nonadjacent,
"topology_valid": nonadjacent == 0,
}
metrics = {
"board": {
"name": board.name,
"layer_count": board.layer_count,
"nets": len(raw_nets),
"routable_nets": len(routable_ids),
"singleton_nets": len(raw_nets) - len(routable_ids),
"pads": sum(len(getattr(n, "pads", [])) for n in raw_nets),
},
"lattice": {
"x_steps": pf.lattice.x_steps,
"y_steps": pf.lattice.y_steps,
"layers": pf.lattice.layers,
"nodes": pf.lattice.num_nodes,
"pitch_mm": pitch,
},
"completion": {
"routed_nets": len(routed_path_ids & routable_ids),
"trivial_nets": len(trivial_path_ids & routable_ids),
"completed_nets": len(completed_ids),
"total_nets": len(routable_ids),
"excluded_nets": len(excluded_ids),
"excluded_net_ids": sorted(excluded_ids),
"unrouted_net_ids": unrouted_ids,
"complete": (
len(completed_ids) == len(routable_ids)
and not excluded_ids
and int(overuse_total) == 0
and barrel_conflicts == 0
and path_node_conflicts == 0
),
},
"convergence": {
"iterations": getattr(pf, "iteration", None),
"overuse_total": int(overuse_total),
"overuse_count": int(overuse_count),
"path_node_overuse_total": path_node_overuse,
"path_node_overuse_count": path_node_overuse_count,
"negotiated_overuse_total": negotiated_overuse,
"barrel_conflicts": barrel_conflicts,
"exact_barrel_conflicts": exact_barrel_conflicts,
"path_node_conflicts": path_node_conflicts,
"escape_conflicts": escape_conflicts,
"portal_grid_conflicts": portal_grid_conflicts,
},
"copper": {
"wirelength_mm": round(total_lateral * pitch, 3),
"via_transitions": via_transitions,
"via_layer_steps": via_layer_steps,
"lateral_steps_per_layer": {
str(z): n for z, n in sorted(lateral_steps_per_layer.items())
},
"preferred_steps_per_layer": {
str(z): n
for z, n in sorted(preferred_steps_per_layer.items())
},
"wrong_way_steps_per_layer": {
str(z): n
for z, n in sorted(wrong_way_steps_per_layer.items())
},
"wrong_way_steps": sum(wrong_way_steps_per_layer.values()),
"layers_used": sorted(lateral_steps_per_layer.keys()),
"layers_used_count": len(lateral_steps_per_layer),
},
"timings_s": {k: round(v, 3) for k, v in (timings or {}).items()},
"git_sha": _git_sha(),
}
if hdi_metrics is not None:
metrics["hdi_stack"] = hdi_metrics
return metrics
def _git_sha() -> Optional[str]:
try:
return subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
capture_output=True, text=True, timeout=5,
).stdout.strip() or None
except Exception:
return None