You've already forked OrthoRoute
mirror of
https://github.com/bbenchoff/OrthoRoute.git
synced 2026-08-21 22:09:17 +00:00
860 lines
32 KiB
Python
860 lines
32 KiB
Python
import gzip
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import pickle
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import traceback
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import cupy as cp
|
|
|
|
|
|
repo_root = Path(__file__).resolve().parents[2]
|
|
if str(repo_root) not in sys.path:
|
|
sys.path.insert(0, str(repo_root))
|
|
|
|
from benchmarks.metrics import collect_route_metrics
|
|
from benchmarks.experiment_config import effective_pathfinder_config
|
|
import orthoroute.algorithms.manhattan.unified_pathfinder as pathfinder_module
|
|
from orthoroute.algorithms.manhattan.unified_pathfinder import (
|
|
PathFinderConfig,
|
|
UnifiedPathFinder,
|
|
)
|
|
from orthoroute.algorithms.manhattan.hdi_stack import (
|
|
pcbway_elic_stack,
|
|
pcbway_mechanical_stack,
|
|
)
|
|
from orthoroute.algorithms.manhattan.layer_peeling import (
|
|
build_peel_plan,
|
|
rebuild_selected_portals,
|
|
remap_selected_portals,
|
|
remap_surviving_paths,
|
|
)
|
|
from orthoroute.infrastructure.kicad.file_parser import KiCadFileParser
|
|
|
|
|
|
source_board_value = os.getenv("ORTHO_SOURCE_BOARD")
|
|
if not source_board_value:
|
|
raise RuntimeError(
|
|
"ORTHO_SOURCE_BOARD must name the KiCad board to route"
|
|
)
|
|
source_board = Path(source_board_value).expanduser().resolve()
|
|
output_dir = Path(os.getenv(
|
|
"ORTHO_OUTPUT_DIR",
|
|
str(source_board.parent / "OrthoRoute-results"),
|
|
)).expanduser().resolve()
|
|
output_dir.mkdir(exist_ok=True)
|
|
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
diagnostic_max_iterations = os.getenv("ORTHO_DIAGNOSTIC_MAX_ITERATIONS")
|
|
diagnostic_net_limit = int(os.getenv("ORTHO_DIAGNOSTIC_NET_LIMIT", "0"))
|
|
diagnostic_layer_limit = int(os.getenv(
|
|
"ORTHO_DIAGNOSTIC_LAYER_LIMIT", "0"
|
|
))
|
|
diagnostic_via_scale = os.getenv("ORTHO_DIAGNOSTIC_VIA_SCALE")
|
|
diagnostic_owner_penalty = os.getenv("ORTHO_DIAGNOSTIC_OWNER_PENALTY")
|
|
diagnostic_path_node_penalty = os.getenv(
|
|
"ORTHO_DIAGNOSTIC_PATH_NODE_PENALTY"
|
|
)
|
|
fabrication_profile = os.getenv("ORTHO_FAB_PROFILE", "").strip()
|
|
direction_mode = os.getenv("ORTHO_DIRECTION_MODE", "strict").strip()
|
|
wrong_way_multiplier = float(os.getenv(
|
|
"ORTHO_WRONG_WAY_MULTIPLIER", "4.0"
|
|
))
|
|
layer_directions = os.getenv("ORTHO_LAYER_DIRECTIONS", "").strip()
|
|
layer_depth_bias = float(os.getenv("ORTHO_LAYER_DEPTH_BIAS", "0.0"))
|
|
hdi_stack_mode = os.getenv("ORTHO_HDI_STACK", "").strip()
|
|
grid_pitch = float(os.getenv("ORTHO_GRID_PITCH", "0.4"))
|
|
warm_start_paths_value = os.getenv("ORTHO_WARM_START_PATHS", "").strip()
|
|
run_tags = []
|
|
if diagnostic_net_limit:
|
|
run_tags.append(f"{diagnostic_net_limit}N")
|
|
if diagnostic_layer_limit:
|
|
run_tags.append(f"{diagnostic_layer_limit}L")
|
|
if diagnostic_via_scale is not None:
|
|
run_tags.append(f"V{diagnostic_via_scale}")
|
|
if diagnostic_owner_penalty is not None:
|
|
run_tags.append(f"O{diagnostic_owner_penalty}")
|
|
if diagnostic_path_node_penalty is not None:
|
|
run_tags.append(f"P{diagnostic_path_node_penalty}")
|
|
if diagnostic_max_iterations is not None:
|
|
run_tags.append(f"I{diagnostic_max_iterations}")
|
|
if fabrication_profile:
|
|
run_tags.append(f"FAB-{fabrication_profile}")
|
|
if direction_mode != "strict":
|
|
run_tags.append(
|
|
"BIDIR" if direction_mode == "bidirectional"
|
|
else f"GUIDED-{wrong_way_multiplier:g}"
|
|
)
|
|
if layer_depth_bias:
|
|
run_tags.append(f"DEPTH-{layer_depth_bias:g}")
|
|
if hdi_stack_mode:
|
|
run_tags.append(f"HDI-{hdi_stack_mode}")
|
|
if grid_pitch != 0.4:
|
|
run_tags.append(f"GRID-{grid_pitch:g}")
|
|
if warm_start_paths_value:
|
|
run_tags.append("PEEL")
|
|
tag = f"-{'-'.join(run_tags)}" if run_tags else ""
|
|
run_layers = diagnostic_layer_limit or 32
|
|
run_name = f"Backplane-{run_layers}L{tag}-{stamp}"
|
|
progress_path = output_dir / f"{run_name}-progress.json"
|
|
metrics_path = output_dir / f"{run_name}-metrics.json"
|
|
geometry_path = output_dir / f"{run_name}-geometry.json"
|
|
paths_path = output_dir / f"{run_name}-paths.pkl.gz"
|
|
|
|
|
|
def atomic_json(path, value):
|
|
temporary = path.with_suffix(path.suffix + ".tmp")
|
|
temporary.write_text(json.dumps(value, indent=2), encoding="utf-8")
|
|
temporary.replace(path)
|
|
|
|
|
|
def memory_snapshot():
|
|
free, total = cp.cuda.runtime.memGetInfo()
|
|
pool = cp.get_default_memory_pool()
|
|
return {
|
|
"device_free_bytes": int(free),
|
|
"device_total_bytes": int(total),
|
|
"pool_used_bytes": int(pool.used_bytes()),
|
|
"pool_total_bytes": int(pool.total_bytes()),
|
|
}
|
|
|
|
|
|
logging.basicConfig(
|
|
level=getattr(
|
|
logging,
|
|
os.getenv("ORTHO_LOG_LEVEL", "WARNING").upper(),
|
|
logging.WARNING,
|
|
),
|
|
format="%(asctime)s %(levelname)s %(message)s",
|
|
)
|
|
os.environ["USE_GPU"] = "1"
|
|
|
|
started = time.perf_counter()
|
|
source_sha = hashlib.sha256(source_board.read_bytes()).hexdigest()
|
|
progress = {
|
|
"status": "starting",
|
|
"run_name": run_name,
|
|
"source_board": str(source_board),
|
|
"source_sha256": source_sha,
|
|
"started": datetime.now().isoformat(timespec="seconds"),
|
|
"git_sha": subprocess.check_output(
|
|
["git", "rev-parse", "HEAD"],
|
|
cwd=repo_root,
|
|
text=True,
|
|
).strip(),
|
|
"iterations": [],
|
|
"fabrication_profile": fabrication_profile or None,
|
|
}
|
|
atomic_json(progress_path, progress)
|
|
|
|
timings = {}
|
|
memory = {"start": memory_snapshot()}
|
|
router = None
|
|
board = None
|
|
result = None
|
|
|
|
try:
|
|
phase_started = time.perf_counter()
|
|
board = KiCadFileParser().load_board(str(source_board))
|
|
if board is None:
|
|
raise RuntimeError("monster board parse failed")
|
|
if fabrication_profile:
|
|
if fabrication_profile != "pcbway_advanced_hdi":
|
|
raise ValueError(
|
|
"ORTHO_FAB_PROFILE must be pcbway_advanced_hdi"
|
|
)
|
|
# Conservative advanced-HDI envelope. PCBWay advertises local
|
|
# 2/2 mil features, but 4/4 mil is the broadly manufacturable
|
|
# baseline. The 0.15 mm mechanical blind/buried drill plus a
|
|
# 3 mil annular ring gives the existing 0.3024 mm finished pad.
|
|
board._design_rules = dict(board._design_rules)
|
|
board._design_rules.update({
|
|
"default_track_width": 0.1016,
|
|
"default_clearance": 0.1016,
|
|
"default_via_drill": 0.15,
|
|
"default_via_diameter": 0.3024,
|
|
"min_via_annular_width": 0.0762,
|
|
"min_hole_to_hole": 0.2794,
|
|
"min_hole_clearance": 0.1524,
|
|
})
|
|
if diagnostic_layer_limit:
|
|
if not 4 <= diagnostic_layer_limit <= len(board.layers):
|
|
raise ValueError(
|
|
"ORTHO_DIAGNOSTIC_LAYER_LIMIT must be between 4 "
|
|
f"and {len(board.layers)}"
|
|
)
|
|
# Retain F.Cu, the lowest requested internal layers, and B.Cu.
|
|
# This measures whether negotiated packing can stay shallow while
|
|
# preserving a valid outer-copper stack for geometry/export.
|
|
board.layers = (
|
|
board.layers[:diagnostic_layer_limit - 1]
|
|
+ [board.layers[-1]]
|
|
)
|
|
board.layer_count = diagnostic_layer_limit
|
|
for position, layer in enumerate(board.layers):
|
|
layer.stackup_position = position
|
|
board._build_indexes()
|
|
timings["parse_board"] = time.perf_counter() - phase_started
|
|
memory["parse_board"] = memory_snapshot()
|
|
|
|
config = PathFinderConfig()
|
|
config.grid_pitch = grid_pitch
|
|
if hdi_stack_mode:
|
|
stack_factories = {
|
|
"pcbway_elic": pcbway_elic_stack,
|
|
"pcbway_mechanical": pcbway_mechanical_stack,
|
|
}
|
|
if hdi_stack_mode not in stack_factories:
|
|
raise ValueError(
|
|
"ORTHO_HDI_STACK must be pcbway_elic or "
|
|
"pcbway_mechanical"
|
|
)
|
|
config.hdi_stack = stack_factories[hdi_stack_mode](
|
|
board.layer_count
|
|
)
|
|
if direction_mode == "strict":
|
|
config.wrong_way_cost_multiplier = float("inf")
|
|
elif direction_mode == "bidirectional":
|
|
config.wrong_way_cost_multiplier = 1.0
|
|
elif direction_mode == "guided":
|
|
config.wrong_way_cost_multiplier = wrong_way_multiplier
|
|
else:
|
|
raise ValueError(
|
|
"ORTHO_DIRECTION_MODE must be strict, guided, or "
|
|
"bidirectional"
|
|
)
|
|
if layer_directions:
|
|
config.preferred_layer_directions = [
|
|
axis.strip().lower()
|
|
for axis in layer_directions.split(",")
|
|
]
|
|
config.layer_depth_bias = layer_depth_bias
|
|
if diagnostic_via_scale is not None:
|
|
config.adjacent_via_step_scale = float(diagnostic_via_scale)
|
|
if diagnostic_owner_penalty is not None:
|
|
config.owner_penalty_base = float(diagnostic_owner_penalty)
|
|
if diagnostic_path_node_penalty is not None:
|
|
config.path_node_penalty_base = float(
|
|
diagnostic_path_node_penalty
|
|
)
|
|
# Preserve the actual experiment knobs in the journal. Zero-valued
|
|
# options intentionally disappear from the compact run name, and source
|
|
# ancestry alone should not be required to reconstruct a long GPU run.
|
|
progress["experiment_config"] = {
|
|
"stage": "requested_before_board_derivation",
|
|
"layer_count": int(board.layer_count),
|
|
"net_limit": int(diagnostic_net_limit),
|
|
"max_iterations_override": (
|
|
None
|
|
if diagnostic_max_iterations is None else
|
|
int(diagnostic_max_iterations)
|
|
),
|
|
"grid_pitch": float(config.grid_pitch),
|
|
"fabrication_profile": fabrication_profile or None,
|
|
"hdi_stack": hdi_stack_mode or None,
|
|
"direction_mode": direction_mode,
|
|
"wrong_way_multiplier": float(wrong_way_multiplier),
|
|
"preferred_layer_directions": (
|
|
list(config.preferred_layer_directions)
|
|
if config.preferred_layer_directions is not None else
|
|
None
|
|
),
|
|
"layer_depth_bias": float(config.layer_depth_bias),
|
|
"owner_penalty_base": float(config.owner_penalty_base),
|
|
"path_node_penalty_base": float(
|
|
config.path_node_penalty_base
|
|
),
|
|
"ordinary_hotset_cap": int(config.hotset_cap),
|
|
"slow_progress_hotset_cap": int(
|
|
config.slow_progress_hotset_cap
|
|
),
|
|
"slow_progress_hotset_cap_max": int(
|
|
config.slow_progress_hotset_cap_max
|
|
),
|
|
"slow_progress_pressure_after": int(
|
|
config.slow_progress_pressure_after
|
|
),
|
|
"slow_progress_pres_fac_max": float(
|
|
config.slow_progress_pres_fac_max
|
|
),
|
|
"slow_progress_window": int(config.slow_progress_window),
|
|
"slow_progress_min_fraction": float(
|
|
config.slow_progress_min_fraction
|
|
),
|
|
"slow_progress_min_overuse": int(
|
|
config.slow_progress_min_overuse
|
|
),
|
|
}
|
|
atomic_json(progress_path, progress)
|
|
if diagnostic_max_iterations is not None:
|
|
original_apply_derived_parameters = (
|
|
pathfinder_module.apply_derived_parameters
|
|
)
|
|
|
|
def apply_diagnostic_iteration_limit(config, derived):
|
|
original_apply_derived_parameters(config, derived)
|
|
config.max_iterations = int(diagnostic_max_iterations)
|
|
|
|
pathfinder_module.apply_derived_parameters = (
|
|
apply_diagnostic_iteration_limit
|
|
)
|
|
router = UnifiedPathFinder(config=config, use_gpu=True)
|
|
|
|
route_nets = [
|
|
net for net in board.nets
|
|
if len(getattr(net, "pads", ())) >= 2
|
|
]
|
|
if diagnostic_net_limit:
|
|
route_nets = route_nets[:diagnostic_net_limit]
|
|
|
|
phase_started = time.perf_counter()
|
|
router.initialize_graph(board)
|
|
cp.cuda.Stream.null.synchronize()
|
|
timings["initialize_graph"] = time.perf_counter() - phase_started
|
|
memory["initialize_graph"] = memory_snapshot()
|
|
|
|
phase_started = time.perf_counter()
|
|
router.precompute_all_pad_escapes(board, route_nets)
|
|
cp.cuda.Stream.null.synchronize()
|
|
timings["pad_escapes"] = time.perf_counter() - phase_started
|
|
memory["pad_escapes"] = memory_snapshot()
|
|
|
|
all_route_nets = list(route_nets)
|
|
route_requests = list(route_nets)
|
|
peel_plan = None
|
|
if warm_start_paths_value:
|
|
warm_start_path = Path(
|
|
warm_start_paths_value
|
|
).expanduser().resolve()
|
|
with gzip.open(warm_start_path, "rb") as stream:
|
|
warm_start = pickle.load(stream)
|
|
if warm_start.get("source_sha256") != source_sha:
|
|
raise ValueError(
|
|
"warm-start source hash differs from the board source"
|
|
)
|
|
source_shape = tuple(map(
|
|
int, warm_start["lattice"]["shape"]
|
|
))
|
|
target_shape = (
|
|
int(router.lattice.x_steps),
|
|
int(router.lattice.y_steps),
|
|
int(router.lattice.layers),
|
|
)
|
|
if source_shape[:2] != target_shape[:2]:
|
|
raise ValueError(
|
|
"warm-start and reduced lattices have different X/Y shapes"
|
|
)
|
|
if source_shape[2] != target_shape[2] + 2:
|
|
raise ValueError(
|
|
"a warm peel must reduce exactly two copper layers"
|
|
)
|
|
warm_paths = {
|
|
str(net_id): list(path)
|
|
for net_id, path in warm_start["net_paths"].items()
|
|
}
|
|
peel_plan = build_peel_plan(warm_paths, source_shape)
|
|
if peel_plan.target_layers != target_shape[2]:
|
|
raise ValueError("peel plan target does not match live router")
|
|
|
|
# Populate deterministic net/pad identities before installing the
|
|
# committed survivors. Only displaced nets are subsequently passed
|
|
# to negotiation.
|
|
router._parse_requests(all_route_nets)
|
|
survivors = remap_surviving_paths(
|
|
warm_paths,
|
|
peel_plan,
|
|
source_shape,
|
|
config.hdi_stack,
|
|
)
|
|
serialized_portals = warm_start.get("net_selected_portals")
|
|
if serialized_portals is not None:
|
|
selected_portals, portal_layers = remap_selected_portals(
|
|
serialized_portals,
|
|
survivors,
|
|
target_shape,
|
|
source_shape[2],
|
|
)
|
|
else:
|
|
selected_portals, portal_layers = rebuild_selected_portals(
|
|
router,
|
|
survivors,
|
|
target_shape,
|
|
)
|
|
router._restore_routing_state({
|
|
"paths": survivors,
|
|
"selected_portals": selected_portals,
|
|
"portal_layers": portal_layers,
|
|
})
|
|
displaced = set(peel_plan.displaced_nets)
|
|
route_requests = [
|
|
net for net in all_route_nets
|
|
if str(getattr(net, "name", "")) in displaced
|
|
]
|
|
if len(route_requests) != len(displaced):
|
|
found = {
|
|
str(getattr(net, "name", ""))
|
|
for net in route_requests
|
|
}
|
|
missing = sorted(displaced - found)
|
|
raise ValueError(
|
|
f"warm peel cannot map displaced nets: {missing[:10]}"
|
|
)
|
|
progress["warm_start"] = {
|
|
"paths": str(warm_start_path),
|
|
**peel_plan.as_dict(),
|
|
"committed_survivor_count": len(survivors),
|
|
"reroute_net_count": len(route_requests),
|
|
}
|
|
atomic_json(progress_path, progress)
|
|
|
|
def progress_callback(iteration, total, message):
|
|
routed = sum(bool(path) for path in router.net_paths.values())
|
|
# PathFinder applies board-derived tuning at negotiation start, after
|
|
# the requested configuration above was journaled. Record the
|
|
# authoritative live values on every snapshot so a terminal journal
|
|
# remains reproducible even if a pressure event changes the ceiling.
|
|
effective = effective_pathfinder_config(config, router)
|
|
if "effective_after_board_derivation" not in progress:
|
|
effective["initial_pressure_ceiling"] = float(
|
|
getattr(router, "_pres_fac_max_now", 0.0)
|
|
)
|
|
progress["effective_after_board_derivation"] = effective
|
|
progress["effective_live_config"] = effective
|
|
def overuse_sum(use, capacity):
|
|
if use is None or capacity is None:
|
|
return 0
|
|
over = cp.maximum(use - capacity, 0)
|
|
return int(cp.asnumpy(over.sum()))
|
|
|
|
edge_over = cp.maximum(
|
|
router.accounting.present
|
|
- router.accounting.capacity,
|
|
0,
|
|
)
|
|
canonical_edge_mask = cp.asarray(
|
|
router._canonical_edge_resource_mask()
|
|
)
|
|
edge_overuse = int(cp.asnumpy(
|
|
edge_over[canonical_edge_mask].sum()
|
|
))
|
|
via_column_overuse = overuse_sum(
|
|
getattr(router, "via_col_use", None),
|
|
getattr(router, "via_col_cap", None),
|
|
)
|
|
via_segment_overuse = overuse_sum(
|
|
getattr(router, "via_seg_use", None),
|
|
getattr(router, "via_seg_cap", None),
|
|
)
|
|
(
|
|
path_node_overuse,
|
|
path_node_overuse_count,
|
|
) = router._compute_path_node_overuse()
|
|
graph_overuse = (
|
|
edge_overuse + via_column_overuse
|
|
+ via_segment_overuse
|
|
)
|
|
plane_size = (
|
|
router.lattice.x_steps * router.lattice.y_steps
|
|
)
|
|
layers_used = sorted({
|
|
int(node) // plane_size
|
|
for path in router.net_paths.values()
|
|
for node in path
|
|
if 0 < int(node) // plane_size < router.lattice.layers - 1
|
|
})
|
|
progress["status"] = "routing"
|
|
progress["updated"] = datetime.now().isoformat(timespec="seconds")
|
|
progress["elapsed_seconds"] = round(
|
|
time.perf_counter() - started, 3
|
|
)
|
|
progress["iteration"] = iteration
|
|
progress["max_iterations"] = total
|
|
progress["routed_nets"] = routed
|
|
progress["excluded_nets"] = sorted(
|
|
getattr(router, "_excluded_nets", ())
|
|
)
|
|
progress["pres_fac"] = float(
|
|
getattr(router, "_pres_fac_now", 0.0)
|
|
)
|
|
progress["memory"] = memory_snapshot()
|
|
physical_conflicts = int(getattr(
|
|
router, "_last_barrel_conflict_count", 0
|
|
))
|
|
capture_conflict_details = physical_conflicts <= 5_000
|
|
escape_pair_options = []
|
|
if capture_conflict_details:
|
|
for first, second in sorted(getattr(
|
|
router, "_escape_conflict_pairs", ()
|
|
)):
|
|
entry = {}
|
|
for label, identity in (
|
|
("first", first),
|
|
("second", second),
|
|
):
|
|
net_name, pad_id = identity
|
|
candidates = router.portal_candidates.get(
|
|
pad_id, ()
|
|
)
|
|
conflicts = [
|
|
router._escape_candidate_conflicts(
|
|
net_name, pad_id, portal
|
|
)
|
|
for portal in candidates
|
|
]
|
|
entry[f"{label}_net"] = net_name
|
|
entry[f"{label}_pad"] = pad_id
|
|
entry[f"{label}_candidates"] = len(candidates)
|
|
entry[f"{label}_clean_candidates"] = sum(
|
|
count == 0 for count in conflicts
|
|
)
|
|
entry[f"{label}_min_conflicts"] = (
|
|
min(conflicts) if conflicts else None
|
|
)
|
|
escape_pair_options.append(entry)
|
|
progress["iterations"].append({
|
|
"iteration": iteration,
|
|
"elapsed_seconds": progress["elapsed_seconds"],
|
|
"routed_nets": routed,
|
|
"excluded_nets": len(progress["excluded_nets"]),
|
|
"pres_fac": progress["pres_fac"],
|
|
"pres_fac_max": float(getattr(
|
|
router, "_pres_fac_max_now", 0.0
|
|
)),
|
|
"pressure_work_scale": float(getattr(
|
|
router, "_last_pressure_work_scale", 1.0
|
|
)),
|
|
"slow_progress_events": int(getattr(
|
|
router, "_slow_progress_event_count", 0
|
|
)),
|
|
"slow_progress_fraction": getattr(
|
|
router, "_last_slow_progress_fraction", None
|
|
),
|
|
"pressure_trial_reference_ceiling": getattr(
|
|
router, "_pressure_trial_reference_ceiling", None
|
|
),
|
|
"pressure_trial_reference_fraction": getattr(
|
|
router, "_pressure_trial_reference_fraction", None
|
|
),
|
|
"pressure_trial_underperform_count": int(getattr(
|
|
router, "_pressure_trial_underperform_count", 0
|
|
)),
|
|
"pressure_backoff_count": int(getattr(
|
|
router, "_pressure_backoff_count", 0
|
|
)),
|
|
"pressure_rejected_ceiling": getattr(
|
|
router, "_pressure_rejected_ceiling", None
|
|
),
|
|
"adaptive_pressure_limit": float(getattr(
|
|
router, "_adaptive_pressure_limit", 0.0
|
|
)),
|
|
"hotset_rate_boost_until": int(getattr(
|
|
router, "_hotset_rate_boost_until", 0
|
|
)),
|
|
"overuse_total": graph_overuse,
|
|
"edge_overuse": edge_overuse,
|
|
"via_column_overuse": via_column_overuse,
|
|
"via_segment_overuse": via_segment_overuse,
|
|
"path_node_overuse_total": path_node_overuse,
|
|
"path_node_overuse_count": path_node_overuse_count,
|
|
"path_node_layers": router._path_node_layer_metrics(),
|
|
"negotiated_overuse_total": (
|
|
graph_overuse + path_node_overuse
|
|
),
|
|
"barrel_conflicts": physical_conflicts,
|
|
"exact_barrel_conflicts": int(getattr(
|
|
router, "_last_exact_barrel_conflict_count", 0
|
|
)),
|
|
"path_node_conflicts": int(getattr(
|
|
router, "_last_path_node_conflict_count", 0
|
|
)),
|
|
"escape_conflicts": int(getattr(
|
|
router, "_last_escape_conflict_count", 0
|
|
)),
|
|
"portal_grid_conflicts": int(getattr(
|
|
router, "_last_portal_grid_conflict_count", 0
|
|
)),
|
|
"portal_grid_pairs": [
|
|
{
|
|
"owner": identity[0],
|
|
"pad": identity[1],
|
|
"x_idx": int(identity[2]),
|
|
"y_idx": int(identity[3]),
|
|
"victim": victim,
|
|
"kind": kind,
|
|
}
|
|
for identity, victim, kind in sorted(getattr(
|
|
router, "_portal_grid_pairs", ()
|
|
))
|
|
] if capture_conflict_details else [],
|
|
"conflict_details_truncated": (
|
|
not capture_conflict_details
|
|
),
|
|
"portal_cleanup_movable_nets": sorted(getattr(
|
|
router, "_portal_cleanup_movable_nets", ()
|
|
)),
|
|
"stagnation_recovery_count": int(getattr(
|
|
router, "stagnation_counter", 0
|
|
)),
|
|
"last_stagnation_victims": list(getattr(
|
|
router, "_last_stagnation_victims", ()
|
|
)),
|
|
"hotset_size": int(getattr(
|
|
router, "_last_hotset_size", 0
|
|
)),
|
|
"hotset_cap": int(getattr(
|
|
router, "_last_hotset_cap", 0
|
|
)),
|
|
"hotset_offender_count": int(getattr(
|
|
router, "_last_hotset_offender_count", 0
|
|
)),
|
|
"hotset_exploration_fraction": float(getattr(
|
|
router,
|
|
"_last_hotset_exploration_fraction",
|
|
0.0,
|
|
)),
|
|
"hotset_conflict_aware": bool(getattr(
|
|
router, "_last_hotset_conflict_aware", False
|
|
)),
|
|
"hotset_conflict_pair_count": int(getattr(
|
|
router, "_last_hotset_conflict_pair_count", 0
|
|
)),
|
|
"hotset_conflict_pairs_covered": int(getattr(
|
|
router, "_last_hotset_conflict_pairs_covered", 0
|
|
)),
|
|
"hotset_conflict_pair_coverage_fraction": float(getattr(
|
|
router,
|
|
"_last_hotset_conflict_pair_coverage_fraction",
|
|
0.0,
|
|
)),
|
|
"via_pool_conflict_nets": len(getattr(
|
|
router, "_via_pool_conflict_nets", ()
|
|
)),
|
|
"via_pool_keeper_resources": len(getattr(
|
|
router, "_via_pool_keepers", {}
|
|
)),
|
|
"portal_cleanup_targets": [
|
|
{
|
|
"net": identity[0],
|
|
"pad": identity[1],
|
|
"x_idx": int(portal.x_idx),
|
|
"y_idx": int(portal.y_idx),
|
|
}
|
|
for identity, portal in sorted(getattr(
|
|
router, "_portal_cleanup_target_portals", {}
|
|
).items())
|
|
],
|
|
"escape_conflict_pairs": [
|
|
{
|
|
"first_net": first[0],
|
|
"first_pad": first[1],
|
|
"second_net": second[0],
|
|
"second_pad": second[1],
|
|
}
|
|
for first, second in sorted(getattr(
|
|
router, "_escape_conflict_pairs", ()
|
|
))
|
|
] if capture_conflict_details else [],
|
|
"escape_pair_options": (
|
|
escape_pair_options
|
|
if capture_conflict_details else []
|
|
),
|
|
"exact_barrel_pairs": [
|
|
{
|
|
"first_net": first,
|
|
"second_net": second,
|
|
}
|
|
for first, second in sorted(getattr(
|
|
router, "_exact_barrel_pairs", ()
|
|
))
|
|
] if capture_conflict_details else [],
|
|
"barrel_details": (
|
|
list(getattr(
|
|
router, "_last_exact_barrel_details", ()
|
|
))
|
|
if capture_conflict_details else []
|
|
),
|
|
"layers_used": layers_used,
|
|
"memory": progress["memory"],
|
|
})
|
|
atomic_json(progress_path, progress)
|
|
|
|
phase_started = time.perf_counter()
|
|
result = router.route_multiple_nets(
|
|
route_requests,
|
|
progress_cb=progress_callback,
|
|
)
|
|
cp.cuda.Stream.null.synchronize()
|
|
timings["route"] = time.perf_counter() - phase_started
|
|
memory["route"] = memory_snapshot()
|
|
|
|
phase_started = time.perf_counter()
|
|
router.emit_geometry(board)
|
|
timings["emit_geometry"] = time.perf_counter() - phase_started
|
|
|
|
payload = router.get_geometry_payload()
|
|
atomic_json(geometry_path, {
|
|
"run_name": run_name,
|
|
"source_board": str(source_board),
|
|
"source_sha256": source_sha,
|
|
"tracks": payload.tracks,
|
|
"vias": payload.vias,
|
|
})
|
|
|
|
with gzip.open(paths_path, "wb", compresslevel=3) as stream:
|
|
pickle.dump({
|
|
"run_name": run_name,
|
|
"source_board": str(source_board),
|
|
"source_sha256": source_sha,
|
|
"lattice": {
|
|
"shape": (
|
|
router.lattice.x_steps,
|
|
router.lattice.y_steps,
|
|
router.lattice.layers,
|
|
),
|
|
"pitch": router.lattice.pitch,
|
|
"layer_directions": list(
|
|
router.lattice.layer_dir
|
|
),
|
|
},
|
|
"net_paths": router.net_paths,
|
|
"net_portal_layers": router.net_portal_layers,
|
|
"net_selected_portals": router.net_selected_portals,
|
|
"peel_plan": (
|
|
peel_plan.as_dict()
|
|
if peel_plan is not None else None
|
|
),
|
|
"result": {
|
|
key: value for key, value in result.items()
|
|
if key != "paths"
|
|
},
|
|
}, stream, protocol=pickle.HIGHEST_PROTOCOL)
|
|
|
|
timings["total"] = time.perf_counter() - started
|
|
metrics = collect_route_metrics(router, board, timings)
|
|
selected_ids = {
|
|
str(getattr(net, "name", None) or getattr(net, "id", ""))
|
|
for net in all_route_nets
|
|
}
|
|
routed_ids = {
|
|
str(net_id)
|
|
for net_id, path in router.net_paths.items()
|
|
if path and str(net_id) in selected_ids
|
|
}
|
|
excluded_ids = sorted(
|
|
str(net_id)
|
|
for net_id in getattr(router, "_excluded_nets", ())
|
|
if str(net_id) in selected_ids
|
|
)
|
|
metrics["completion"] = {
|
|
"routed_nets": len(routed_ids),
|
|
"trivial_nets": 0,
|
|
"completed_nets": len(routed_ids),
|
|
"total_nets": len(selected_ids),
|
|
"excluded_nets": len(excluded_ids),
|
|
"excluded_net_ids": excluded_ids,
|
|
"unrouted_net_ids": sorted(selected_ids - routed_ids),
|
|
"complete": (
|
|
routed_ids == selected_ids
|
|
and not excluded_ids
|
|
and metrics["convergence"]["overuse_total"] == 0
|
|
and metrics["convergence"]["barrel_conflicts"] == 0
|
|
),
|
|
}
|
|
# The repository may advance while a multi-hour route is running.
|
|
# Preserve the exact source revision captured at process start.
|
|
metrics["git_sha"] = progress["git_sha"]
|
|
metrics["params"] = {
|
|
"backend": "gpu",
|
|
"source_board": str(source_board),
|
|
"source_sha256": source_sha,
|
|
"timestamp": datetime.now().isoformat(timespec="seconds"),
|
|
"effective_max_iterations": config.max_iterations,
|
|
"selected_routable_nets": len(all_route_nets),
|
|
"negotiated_routable_nets": len(route_requests),
|
|
"warm_start_paths": (
|
|
str(Path(warm_start_paths_value).expanduser().resolve())
|
|
if warm_start_paths_value else None
|
|
),
|
|
"peel_plan": (
|
|
peel_plan.as_dict()
|
|
if peel_plan is not None else None
|
|
),
|
|
"layer_limit": diagnostic_layer_limit or board.layer_count,
|
|
"adjacent_via_step_scale": config.adjacent_via_step_scale,
|
|
"owner_penalty_base": config.owner_penalty_base,
|
|
"path_node_penalty_base": config.path_node_penalty_base,
|
|
"fabrication_profile": fabrication_profile or None,
|
|
"direction_mode": direction_mode,
|
|
"wrong_way_cost_multiplier": (
|
|
config.wrong_way_cost_multiplier
|
|
),
|
|
"preferred_layer_directions": list(
|
|
router.lattice.layer_dir
|
|
),
|
|
"configured_layer_directions": (
|
|
config.preferred_layer_directions
|
|
),
|
|
"layer_depth_bias": config.layer_depth_bias,
|
|
"grid_pitch": config.grid_pitch,
|
|
"hdi_stack": (
|
|
config.hdi_stack.name
|
|
if config.hdi_stack is not None else None
|
|
),
|
|
"design_rules": dict(board._design_rules),
|
|
}
|
|
metrics["environment"] = {
|
|
"cupy": cp.__version__,
|
|
"cuda_runtime": cp.cuda.runtime.runtimeGetVersion(),
|
|
"cuda_driver": cp.cuda.runtime.driverGetVersion(),
|
|
"device": cp.cuda.runtime.getDeviceProperties(0)["name"].decode(),
|
|
}
|
|
metrics["memory"] = memory
|
|
metrics["route_result"] = {
|
|
key: value for key, value in result.items()
|
|
if key != "paths"
|
|
}
|
|
metrics["artifacts"] = {
|
|
"progress": str(progress_path),
|
|
"geometry": str(geometry_path),
|
|
"paths": str(paths_path),
|
|
}
|
|
atomic_json(metrics_path, metrics)
|
|
|
|
progress["status"] = (
|
|
"complete" if metrics["completion"]["complete"]
|
|
and metrics["convergence"]["overuse_total"] == 0
|
|
else "incomplete"
|
|
)
|
|
progress["updated"] = datetime.now().isoformat(timespec="seconds")
|
|
progress["elapsed_seconds"] = round(timings["total"], 3)
|
|
progress["metrics"] = str(metrics_path)
|
|
progress["geometry"] = str(geometry_path)
|
|
progress["paths"] = str(paths_path)
|
|
progress["completion"] = metrics["completion"]
|
|
progress["convergence"] = metrics["convergence"]
|
|
atomic_json(progress_path, progress)
|
|
|
|
print(json.dumps({
|
|
"status": progress["status"],
|
|
"metrics": str(metrics_path),
|
|
"geometry": str(geometry_path),
|
|
"paths": str(paths_path),
|
|
"completion": metrics["completion"],
|
|
"convergence": metrics["convergence"],
|
|
"copper": metrics["copper"],
|
|
"timings_s": metrics["timings_s"],
|
|
}, indent=2), flush=True)
|
|
except Exception as exc:
|
|
progress["status"] = "failed"
|
|
progress["updated"] = datetime.now().isoformat(timespec="seconds")
|
|
progress["elapsed_seconds"] = round(time.perf_counter() - started, 3)
|
|
progress["error"] = f"{type(exc).__name__}: {exc}"
|
|
progress["traceback"] = traceback.format_exc()
|
|
atomic_json(progress_path, progress)
|
|
raise
|