Each round routes every connection that has failed so far ahead of the rest, which keep their order. The promoted set only grows, so two groups cannot take turns failing; the best round is kept.
99 lines
3.8 KiB
Python
99 lines
3.8 KiB
Python
"""Route one board in several connection orders at once and keep the best.
|
|
|
|
Which connection routes first moves the result on a crowded board by several
|
|
connections either way, and no single order wins everywhere. Routing a few
|
|
orders in parallel processes and keeping the one that leaves the fewest
|
|
connections unrouted costs cores, not wall time.
|
|
|
|
Without a seed the candidates are the default order and seeds 0 to N-2, so the
|
|
result is never worse than a plain run. With seed S they are seeds S to S+N-1.
|
|
Ties go to the earlier candidate, so the choice is deterministic.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from concurrent.futures import ProcessPoolExecutor
|
|
import os
|
|
|
|
from freeroute.dsn import parse_dsn
|
|
from freeroute.ses import RoutingResult
|
|
|
|
from .pipeline import build_exact_routing_result, build_routing_result
|
|
|
|
|
|
def candidate_seeds(orders: int, seed: int | None = None) -> list[int | None]:
|
|
orders = max(int(orders), 1)
|
|
if seed is None:
|
|
return [None, *range(orders - 1)]
|
|
return list(range(seed, seed + orders))
|
|
|
|
|
|
def score(result: RoutingResult) -> tuple[int, int]:
|
|
"""Connections left open, then those left to a zone fill: lower is better."""
|
|
missing = sum(1 for u in result.unrouted if not u[3])
|
|
return missing, len(result.unrouted) - missing
|
|
|
|
|
|
def route_once(dsn_text: str, engine: str, options: dict, seed: int | None) -> RoutingResult:
|
|
dsn = parse_dsn(dsn_text)
|
|
if engine == "exact":
|
|
return build_exact_routing_result(dsn, seed=seed, **options)
|
|
if engine == "grid":
|
|
return build_routing_result(dsn, seed=seed, **options)
|
|
raise ValueError(f"routing orders are not supported by the {engine} engine")
|
|
|
|
|
|
def best_of_orders(
|
|
dsn_text: str,
|
|
engine: str,
|
|
options: dict,
|
|
*,
|
|
orders: int,
|
|
seed: int | None = None,
|
|
jobs: int | None = None,
|
|
) -> tuple[RoutingResult, int | None, list[tuple[int | None, tuple[int, int]]]]:
|
|
"""Route ``orders`` orders and return the best result, the seed that made
|
|
it (None for the default order), and every candidate's seed and score."""
|
|
seeds = candidate_seeds(orders, seed)
|
|
if len(seeds) == 1:
|
|
result = route_once(dsn_text, engine, options, seeds[0])
|
|
return result, seeds[0], [(seeds[0], score(result))]
|
|
jobs = max(1, min(jobs or os.cpu_count() or 1, len(seeds)))
|
|
with ProcessPoolExecutor(jobs) as pool:
|
|
futures = [pool.submit(route_once, dsn_text, engine, options, s) for s in seeds]
|
|
results = [f.result() for f in futures]
|
|
scores = [score(r) for r in results]
|
|
best = min(range(len(seeds)), key=lambda i: (scores[i], i))
|
|
return results[best], seeds[best], list(zip(seeds, scores, strict=True))
|
|
|
|
|
|
def promote_failures(
|
|
dsn_text: str,
|
|
engine: str,
|
|
options: dict,
|
|
*,
|
|
rounds: int,
|
|
seed: int | None = None,
|
|
) -> tuple[RoutingResult, list[tuple[int, int]]]:
|
|
"""Route, then route again with every connection that has failed so far
|
|
moved to the front, keeping the rest in their order. Returns the best
|
|
round's result and every round's score.
|
|
|
|
The promoted set only grows, so a connection that failed once keeps its
|
|
early place and two groups cannot take turns failing. Stops early when a
|
|
round routes everything or fails nothing new."""
|
|
numbers = {n.name: i + 1 for i, n in enumerate(parse_dsn(dsn_text).nets)}
|
|
first: frozenset = frozenset()
|
|
best: RoutingResult | None = None
|
|
scores: list[tuple[int, int]] = []
|
|
for _ in range(max(int(rounds), 1)):
|
|
result = route_once(dsn_text, engine, {**options, "route_first": first}, seed)
|
|
scores.append(score(result))
|
|
if best is None or scores[-1] < score(best):
|
|
best = result
|
|
failed = {(numbers.get(name), a, b) for name, a, b, zone in result.unrouted if not zone}
|
|
if not failed or failed <= first:
|
|
break
|
|
first = first | failed
|
|
return best, scores
|