"""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