From e2b7700154f673e3eece89c26dfd3d3ded94d35c Mon Sep 17 00:00:00 2001 From: seveibar Date: Wed, 22 Jul 2026 22:38:30 -0700 Subject: [PATCH 01/22] feat: add Pipeline9 preloaded trace graph routing --- benchmark.sh | 4 +- ...-pipeline-solver9-preloaded-trace-graph.ts | 61 +++++ .../preloaded-trace-graph-solver.ts | 212 ++++++++++++++++++ ...ute-json-without-trace-obstacles-solver.ts | 23 ++ lib/autorouter-pipelines/index.ts | 1 + lib/index.ts | 1 + lib/testing/AutoroutingPipelineDebugger.tsx | 2 + lib/testing/AutoroutingPipelineMenuBar.tsx | 4 + scripts/run-sample.ts | 23 +- .../pipeline9-preloaded-trace-graph.test.ts | 40 ++++ .../preloaded-trace-graph-solver.test.ts | 121 ++++++++++ 11 files changed, 485 insertions(+), 7 deletions(-) create mode 100644 lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts create mode 100644 lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts create mode 100644 lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts create mode 100644 tests/features/pipeline9-preloaded-trace-graph.test.ts create mode 100644 tests/features/preloaded-trace-graph-solver.test.ts diff --git a/benchmark.sh b/benchmark.sh index c26fe0ca2..c9f9f4456 100755 --- a/benchmark.sh +++ b/benchmark.sh @@ -20,6 +20,7 @@ resolve_pipeline_solver_name() { 5) echo "AutoroutingPipelineSolver5" ;; 6) echo "AutoroutingPipelineSolver6" ;; 7) echo "AutoroutingPipelineSolver7_MultiGraph" ;; + 9) echo "AutoroutingPipelineSolver9_PreloadedTraceGraph" ;; krt|KRT) echo "KrtAutoroutingPipelineSolver" ;; *) echo "Unknown pipeline: $1" >&2 @@ -80,7 +81,7 @@ Usage: Options: --solver NAME Run only one solver (same as first positional arg) - --pipeline ID Run a pipeline alias (1-7 or krt) + --pipeline ID Run a pipeline alias (1-7, 9, or krt) --scenario-limit N Run only first N scenarios (same as second positional arg) --concurrency N Number of Bun workers used per solver, or "auto" --effort N Override scenario effort multiplier @@ -107,6 +108,7 @@ Examples: ./benchmark.sh --pipeline 5 ./benchmark.sh --pipeline 6 ./benchmark.sh --pipeline 7 + ./benchmark.sh --pipeline 9 ./benchmark.sh --pipeline krt ./benchmark.sh --solver AutoroutingPipelineSolver7_MultiGraph --dataset srj05 --scenario-limit 20 ./benchmark.sh --dataset 11 --scenario-limit 20 diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts new file mode 100644 index 000000000..e86df5378 --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts @@ -0,0 +1,61 @@ +import { BaseSolver } from "lib/solvers/BaseSolver" +import type { SimpleRouteJson } from "lib/types" +import { + AutoroutingPipelineSolver7_MultiGraph, + type AutoroutingPipelineSolverOptions, +} from "../AutoroutingPipeline7_MultiGraph/AutoroutingPipelineSolver7_MultiGraph" +import { PreloadedTraceGraphSolver } from "./preloaded-trace-graph-solver" +import { PreprocessSimpleRouteJsonWithoutTraceObstaclesSolver } from "./preprocess-simple-route-json-without-trace-obstacles-solver" + +type PipelineStep = { + solverName: string + solverClass: new (...args: any[]) => BaseSolver + getConstructorParams: ( + instance: AutoroutingPipelineSolver7_MultiGraph, + ) => any[] + onSolved?: (instance: AutoroutingPipelineSolver7_MultiGraph) => void +} + +export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends AutoroutingPipelineSolver7_MultiGraph { + preloadedTraceGraphSolver?: PreloadedTraceGraphSolver + + constructor( + srj: SimpleRouteJson, + opts: AutoroutingPipelineSolverOptions = {}, + ) { + super(srj, opts) + const pipelineDef = this.pipelineDef as PipelineStep[] + const preprocessStep = pipelineDef.find( + (step) => step.solverName === "preprocessSimpleRouteJsonSolver", + ) + if (!preprocessStep) { + throw new Error("Pipeline9 could not find the preprocessing stage") + } + preprocessStep.solverClass = + PreprocessSimpleRouteJsonWithoutTraceObstaclesSolver + + const subdivisionStageIndex = pipelineDef.findIndex( + (step) => step.solverName === "nodeDimensionSubdivisionSolver", + ) + if (subdivisionStageIndex === -1) { + throw new Error("Pipeline9 could not find the node subdivision stage") + } + pipelineDef.splice(subdivisionStageIndex + 1, 0, { + solverName: "preloadedTraceGraphSolver", + solverClass: PreloadedTraceGraphSolver, + getConstructorParams: (pipeline) => [ + pipeline.capacityNodes!, + pipeline.originalSrj, + ], + onSolved: (pipeline) => { + const pipeline9 = + pipeline as AutoroutingPipelineSolver9_PreloadedTraceGraph + pipeline9.capacityNodes = pipeline9.preloadedTraceGraphSolver!.getOutput() + }, + }) + } + + override getSolverName(): string { + return "AutoroutingPipelineSolver9_PreloadedTraceGraph" + } +} diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts new file mode 100644 index 000000000..f86907b82 --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts @@ -0,0 +1,212 @@ +import { RbushIndex } from "lib/data-structures/RbushIndex" +import { BaseSolver } from "lib/solvers/BaseSolver" +import type { + CapacityMeshNode, + Obstacle, + SimpleRouteJson, +} from "lib/types" +import { getObstaclesFromSrjTraces } from "lib/utils/convertSrjTracesToObstacles" +import { getUniqueValidZLayersFromLayerNames } from "lib/utils/mapLayerNameToZ" + +type PreloadedTraceShape = { + connectionName: string + obstacle: Obstacle + zLayers: number[] +} + +type RotatedRectGeometry = { + center: { x: number; y: number } + halfWidth: number + halfHeight: number + unitX: { x: number; y: number } + unitY: { x: number; y: number } + bounds: { minX: number; minY: number; maxX: number; maxY: number } +} + +const GEOMETRIC_TOLERANCE = 1e-6 + +const getRotatedRectGeometry = ( + obstacle: Obstacle, +): RotatedRectGeometry => { + const radians = ((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180 + const cos = Math.cos(radians) + const sin = Math.sin(radians) + const halfWidth = obstacle.width / 2 + const halfHeight = obstacle.height / 2 + const extentX = Math.abs(cos) * halfWidth + Math.abs(sin) * halfHeight + const extentY = Math.abs(sin) * halfWidth + Math.abs(cos) * halfHeight + + return { + center: obstacle.center, + halfWidth, + halfHeight, + unitX: { x: cos, y: sin }, + unitY: { x: -sin, y: cos }, + bounds: { + minX: obstacle.center.x - extentX, + minY: obstacle.center.y - extentY, + maxX: obstacle.center.x + extentX, + maxY: obstacle.center.y + extentY, + }, + } +} + +const doesRotatedRectOverlapNode = ( + rect: RotatedRectGeometry, + node: CapacityMeshNode, +): boolean => { + const nodeHalfWidth = node.width / 2 + const nodeHalfHeight = node.height / 2 + const deltaX = rect.center.x - node.center.x + const deltaY = rect.center.y - node.center.y + const projectedOnRectX = Math.abs( + deltaX * rect.unitX.x + deltaY * rect.unitX.y, + ) + const projectedOnRectY = Math.abs( + deltaX * rect.unitY.x + deltaY * rect.unitY.y, + ) + + return ( + Math.abs(deltaX) <= + nodeHalfWidth + + rect.halfWidth * Math.abs(rect.unitX.x) + + rect.halfHeight * Math.abs(rect.unitY.x) + + GEOMETRIC_TOLERANCE && + Math.abs(deltaY) <= + nodeHalfHeight + + rect.halfWidth * Math.abs(rect.unitX.y) + + rect.halfHeight * Math.abs(rect.unitY.y) + + GEOMETRIC_TOLERANCE && + projectedOnRectX <= + rect.halfWidth + + nodeHalfWidth * Math.abs(rect.unitX.x) + + nodeHalfHeight * Math.abs(rect.unitX.y) + + GEOMETRIC_TOLERANCE && + projectedOnRectY <= + rect.halfHeight + + nodeHalfWidth * Math.abs(rect.unitY.x) + + nodeHalfHeight * Math.abs(rect.unitY.y) + + GEOMETRIC_TOLERANCE + ) +} + +const getPreloadedTraceShapes = ( + srj: SimpleRouteJson, +): PreloadedTraceShape[] => { + const shapes: PreloadedTraceShape[] = [] + + for (const trace of srj.traces ?? []) { + if (!trace.connection_name) { + throw new Error( + `Preloaded trace "${trace.pcb_trace_id}" is missing a connection name`, + ) + } + + const obstacles = getObstaclesFromSrjTraces({ + ...srj, + traces: [trace], + }) + for (const obstacle of obstacles) { + const zLayers = getUniqueValidZLayersFromLayerNames( + obstacle.layers, + srj.layerCount, + ) + if (zLayers.length === 0) { + throw new Error( + `Preloaded trace shape "${obstacle.obstacleId ?? trace.pcb_trace_id}" has no valid board layers`, + ) + } + shapes.push({ + connectionName: trace.connection_name, + obstacle, + zLayers, + }) + } + } + + return shapes +} + +/** + * Projects exact preloaded wire/via geometry onto the final capacity mesh. + * buildHyperGraph canonicalizes each `_connectedTo` value into a fixed net + * assignment, so old copper participates in pathing without fragmenting the + * RectDiff topology. + */ +export class PreloadedTraceGraphSolver extends BaseSolver { + private readonly outputNodes: CapacityMeshNode[] + private readonly nodeIndex = new RbushIndex() + private readonly traceShapes: PreloadedTraceShape[] + + constructor( + capacityMeshNodes: CapacityMeshNode[], + private readonly srj: SimpleRouteJson, + ) { + super() + this.MAX_ITERATIONS = 1 + this.outputNodes = capacityMeshNodes.map((node) => ({ + ...node, + availableZ: [...node.availableZ], + _connectedTo: + node._connectedTo === undefined ? undefined : [...node._connectedTo], + })) + this.nodeIndex.bulkLoad( + this.outputNodes.map((node) => ({ + item: node, + minX: node.center.x - node.width / 2, + minY: node.center.y - node.height / 2, + maxX: node.center.x + node.width / 2, + maxY: node.center.y + node.height / 2, + })), + ) + this.traceShapes = getPreloadedTraceShapes(srj) + } + + override getSolverName(): string { + return "PreloadedTraceGraphSolver" + } + + override _step(): void { + const projectedNodeIds = new Set() + let traceRegionAssignmentCount = 0 + + for (const shape of this.traceShapes) { + const geometry = getRotatedRectGeometry(shape.obstacle) + const candidateNodes = this.nodeIndex.search( + geometry.bounds.minX, + geometry.bounds.minY, + geometry.bounds.maxX, + geometry.bounds.maxY, + ) + + for (const node of candidateNodes) { + if (!node.availableZ.some((z) => shape.zLayers.includes(z))) continue + if (!doesRotatedRectOverlapNode(geometry, node)) continue + + const connectedTo = new Set(node._connectedTo ?? []) + const previousConnectionCount = connectedTo.size + connectedTo.add(shape.connectionName) + node._connectedTo = [...connectedTo] + projectedNodeIds.add(node.capacityMeshNodeId) + if (connectedTo.size > previousConnectionCount) { + traceRegionAssignmentCount++ + } + } + } + + this.stats = { + preloadedTraceCount: this.srj.traces?.length ?? 0, + preloadedTraceShapeCount: this.traceShapes.length, + projectedNodeCount: projectedNodeIds.size, + traceRegionAssignmentCount, + } + this.solved = true + } + + getOutput(): CapacityMeshNode[] { + if (!this.solved) { + throw new Error("PreloadedTraceGraphSolver has not solved yet") + } + return this.outputNodes + } +} diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts new file mode 100644 index 000000000..1857f1abb --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts @@ -0,0 +1,23 @@ +import type { SimpleRouteJson } from "lib/types" +import { addApproximatingRectsToSrj } from "lib/utils/addApproximatingRectsToSrj" +import { createSrjWithBoardValidObstacleLayers } from "lib/utils/create-srj-with-board-valid-obstacle-layers" +import { filterObstaclesOutsideBoard } from "lib/utils/filterObstaclesOutsideBoard" +import { PreprocessSimpleRouteJsonSolver } from "../AutoroutingPipeline4_TinyHypergraph/PreprocessSimpleRouteJsonSolver" + +export class PreprocessSimpleRouteJsonWithoutTraceObstaclesSolver extends PreprocessSimpleRouteJsonSolver { + override _step(): void { + const { traces, ...inputSrjWithoutTraces } = this.inputSrj + const srjWithBoardValidObstacleLayers = + createSrjWithBoardValidObstacleLayers(inputSrjWithoutTraces) + const srjWithApproximatingRects = addApproximatingRectsToSrj( + filterObstaclesOutsideBoard(srjWithBoardValidObstacleLayers), + ) + const outputSrj = createSrjWithBoardValidObstacleLayers( + srjWithApproximatingRects, + ) + + this.outputSrj = + traces === undefined ? outputSrj : { ...outputSrj, traces } + this.solved = true + } +} diff --git a/lib/autorouter-pipelines/index.ts b/lib/autorouter-pipelines/index.ts index ca03321cb..3712c09bd 100644 --- a/lib/autorouter-pipelines/index.ts +++ b/lib/autorouter-pipelines/index.ts @@ -21,3 +21,4 @@ export { AutoroutingPipelineSolver7_MultiGraph as AutoroutingPipelineSolver, } from "./AutoroutingPipeline7_MultiGraph/AutoroutingPipelineSolver7_MultiGraph" export { AutoroutingPipelineSolver8 } from "./AutoroutingPipeline8/AutoroutingPipelineSolver8" +export { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "./AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph" diff --git a/lib/index.ts b/lib/index.ts index d8d7a6174..46e219b90 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -23,6 +23,7 @@ export { AutoroutingPipelineSolver7_MultiGraph as AutoroutingPipelineSolver, } from "./autorouter-pipelines/AutoroutingPipeline7_MultiGraph/AutoroutingPipelineSolver7_MultiGraph" export { AutoroutingPipelineSolver8 } from "./autorouter-pipelines/AutoroutingPipeline8/AutoroutingPipelineSolver8" +export { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "./autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph" export { PolyHighDensitySolver } from "./autorouter-pipelines/AutoroutingPipeline6_PolyHypergraph/PolyHighDensitySolver" export { PolySingleIntraNodeSolver } from "./autorouter-pipelines/AutoroutingPipeline6_PolyHypergraph/PolySingleIntraNodeSolver" export { PolyIntraNodeSolver } from "./autorouter-pipelines/AutoroutingPipeline6_PolyHypergraph/PolyIntraNodeSolver" diff --git a/lib/testing/AutoroutingPipelineDebugger.tsx b/lib/testing/AutoroutingPipelineDebugger.tsx index 794a76c96..b25d0dcac 100644 --- a/lib/testing/AutoroutingPipelineDebugger.tsx +++ b/lib/testing/AutoroutingPipelineDebugger.tsx @@ -15,6 +15,7 @@ import { AutoroutingPipelineSolver5 } from "lib/autorouter-pipelines/Autorouting import { AutoroutingPipelineSolver6 } from "lib/autorouter-pipelines/AutoroutingPipeline6_PolyHypergraph/AutoroutingPipelineSolver6_PolyHypergraph" import { AutoroutingPipelineSolver7_MultiGraph } from "lib/autorouter-pipelines/AutoroutingPipeline7_MultiGraph/AutoroutingPipelineSolver7_MultiGraph" import { AutoroutingPipelineSolver8 } from "lib/autorouter-pipelines/AutoroutingPipeline8/AutoroutingPipelineSolver8" +import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph" import { AutoroutingPipelineSolver2_PortPointPathing, CapacityMeshSolver, @@ -60,6 +61,7 @@ const PIPELINE_SOLVERS = { AutoroutingPipelineSolver6, AutoroutingPipelineSolver7_MultiGraph, AutoroutingPipelineSolver8, + AutoroutingPipelineSolver9_PreloadedTraceGraph, AssignableAutoroutingPipeline1Solver, AssignableAutoroutingPipeline2, AssignableAutoroutingPipeline3, diff --git a/lib/testing/AutoroutingPipelineMenuBar.tsx b/lib/testing/AutoroutingPipelineMenuBar.tsx index cab5b7e68..fe604b193 100644 --- a/lib/testing/AutoroutingPipelineMenuBar.tsx +++ b/lib/testing/AutoroutingPipelineMenuBar.tsx @@ -61,6 +61,10 @@ export const PIPELINE_OPTIONS = [ id: "AutoroutingPipelineSolver8", label: "Pipeline8 Preplaced Vias (Fab)", }, + { + id: "AutoroutingPipelineSolver9_PreloadedTraceGraph", + label: "Pipeline9 Preloaded Trace Graph", + }, { id: "AssignableAutoroutingPipeline1Solver", label: "Assignable Pipeline 1", diff --git a/scripts/run-sample.ts b/scripts/run-sample.ts index a0a9844d2..1c4625011 100644 --- a/scripts/run-sample.ts +++ b/scripts/run-sample.ts @@ -19,6 +19,7 @@ import { AutoroutingPipelineSolver3_HgPortPointPathing, AutoroutingPipelineSolver4, AutoroutingPipelineSolver7_MultiGraph, + AutoroutingPipelineSolver9_PreloadedTraceGraph, } from "../lib" import { PipelineStageDebugRunner, @@ -37,7 +38,7 @@ import { toSimpleRouteJson, } from "./benchmark/scenarios" -type PipelineId = 1 | 2 | 3 | 4 | 7 +type PipelineId = 1 | 2 | 3 | 4 | 7 | 9 type SolverOptions = { effort?: number @@ -95,6 +96,10 @@ const PIPELINE_SOLVERS: Record< solverName: "AutoroutingPipelineSolver7_MultiGraph", SolverConstructor: AutoroutingPipelineSolver7_MultiGraph, }, + 9: { + solverName: "AutoroutingPipelineSolver9_PreloadedTraceGraph", + SolverConstructor: AutoroutingPipelineSolver9_PreloadedTraceGraph, + }, } const printHelp = () => { @@ -105,7 +110,7 @@ const printHelp = () => { " ./run-sample.sh [--pipeline 7] --sample 1 [--dataset dataset01]", "", "Options:", - " --pipeline N Pipeline to run (1, 2, 3, 4, or 7; defaults to 7)", + " --pipeline N Pipeline to run (1, 2, 3, 4, 7, or 9; defaults to 7)", " --srj-path PATH Path to a SimpleRouteJson file", " --sample N 1-based sample index from the benchmark dataset order", ` --dataset NAME Dataset used with --sample (${DATASET_OPTIONS_LABEL}, defaults to dataset01)`, @@ -113,7 +118,7 @@ const printHelp = () => { " --png-size N Square PNG size in pixels, min 1024 (default: 1536)", " --stop-after-stage NAME Stop after capturing a pipeline stage", " --ai-visuals Also write SVG, GraphicsObject JSON, and per-step PNGs", - " --net-colors Pipeline 7: color every visualization by net and write aggregate artifacts (implies --ai-visuals)", + " --net-colors Pipelines 7/9: color every visualization by net and write aggregate artifacts (implies --ai-visuals)", " --effort N Override solver effort", " -h, --help Show this help", "", @@ -303,7 +308,7 @@ const parseArgs = (): RunSampleOptions => { if (arg === "--pipeline") { const pipelineId = parsePositiveInt(args[i + 1] ?? "", "--pipeline") if (!(pipelineId in PIPELINE_SOLVERS)) { - throw new Error("--pipeline must be one of 1, 2, 3, 4, or 7") + throw new Error("--pipeline must be one of 1, 2, 3, 4, 7, or 9") } options.pipeline = pipelineId as PipelineId i += 1 @@ -396,8 +401,14 @@ const parseArgs = (): RunSampleOptions => { throw new Error("--png-size must be at least 1024") } - if (options.netColors && options.pipeline !== 7) { - throw new Error("--net-colors is currently supported by pipeline 7 only") + if ( + options.netColors && + options.pipeline !== 7 && + options.pipeline !== 9 + ) { + throw new Error( + "--net-colors is currently supported by pipelines 7 and 9 only", + ) } return options diff --git a/tests/features/pipeline9-preloaded-trace-graph.test.ts b/tests/features/pipeline9-preloaded-trace-graph.test.ts new file mode 100644 index 000000000..41dde266e --- /dev/null +++ b/tests/features/pipeline9-preloaded-trace-graph.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from "bun:test" +import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph" +import type { SimpleRouteJson } from "lib/types" +import scenario from "./preexisting-connected-traces/srj/preexisting-connected-traces06.srj.json" with { + type: "json", +} + +test("Pipeline9 projects preloaded copper into hypergraph regions without topology obstacles", () => { + const srj = structuredClone(scenario) as SimpleRouteJson + const preloadedTrace = srj.traces?.[0] + if (!preloadedTrace) { + throw new Error("Expected the Pipeline9 fixture to contain a trace") + } + + const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph(srj, { + targetMinCapacity: 0.75, + maxNodeDimension: 3, + effort: 0.5, + }) + solver.solve() + + const preprocessedSrj = + solver.preprocessSimpleRouteJsonSolver?.getOutputSimpleRouteJson() + expect( + preprocessedSrj?.obstacles.some((obstacle) => + obstacle.obstacleId?.startsWith("trace_obstacle_"), + ), + ).toBe(false) + expect(preprocessedSrj?.traces).toEqual(srj.traces) + expect(solver.preloadedTraceGraphSolver?.stats).toMatchObject({ + preloadedTraceCount: 1, + preloadedTraceShapeCount: 1, + }) + expect( + solver.capacityNodes?.some((node) => + node._connectedTo?.includes(preloadedTrace.connection_name), + ), + ).toBe(true) + expect(solver.getOutputSimplifiedPcbTraces()).toHaveLength(1) +}) diff --git a/tests/features/preloaded-trace-graph-solver.test.ts b/tests/features/preloaded-trace-graph-solver.test.ts new file mode 100644 index 000000000..aada96057 --- /dev/null +++ b/tests/features/preloaded-trace-graph-solver.test.ts @@ -0,0 +1,121 @@ +import { expect, test } from "bun:test" +import { PreloadedTraceGraphSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver" +import type { CapacityMeshNode, SimpleRouteJson } from "lib/types" + +test("preloaded trace projection follows rotated wires and via layers", () => { + const capacityMeshNodes: CapacityMeshNode[] = [ + { + capacityMeshNodeId: "diagonal-top", + center: { x: 0, y: 0 }, + width: 0.2, + height: 0.2, + layer: "top", + availableZ: [0], + }, + { + capacityMeshNodeId: "off-diagonal-top", + center: { x: 0, y: 1 }, + width: 0.2, + height: 0.2, + layer: "top", + availableZ: [0], + }, + { + capacityMeshNodeId: "via-bottom", + center: { x: 2, y: 2 }, + width: 0.2, + height: 0.2, + layer: "bottom", + availableZ: [1], + }, + { + capacityMeshNodeId: "wire-bottom", + center: { x: 0, y: 2 }, + width: 0.2, + height: 0.2, + layer: "bottom", + availableZ: [1], + }, + { + capacityMeshNodeId: "unrelated-bottom", + center: { x: 0, y: 0 }, + width: 0.2, + height: 0.2, + layer: "bottom", + availableZ: [1], + }, + ] + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + minViaPadDiameter: 0.6, + minViaHoleDiameter: 0.3, + bounds: { minX: -3, minY: -3, maxX: 3, maxY: 3 }, + obstacles: [], + connections: [], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "preloaded-diagonal", + connection_name: "net1", + route: [ + { + route_type: "wire", + x: -2, + y: -2, + width: 0.1, + layer: "top", + }, + { + route_type: "wire", + x: 2, + y: 2, + width: 0.1, + layer: "top", + }, + { + route_type: "via", + x: 2, + y: 2, + from_layer: "top", + to_layer: "bottom", + via_diameter: 0.6, + via_hole_diameter: 0.3, + }, + { + route_type: "wire", + x: 2, + y: 2, + width: 0.1, + layer: "bottom", + }, + { + route_type: "wire", + x: -2, + y: 2, + width: 0.1, + layer: "bottom", + }, + ], + }, + ], + } + + const solver = new PreloadedTraceGraphSolver(capacityMeshNodes, srj) + solver.solve() + const connectedNodeIds = solver + .getOutput() + .filter((node) => node._connectedTo?.includes("net1")) + .map((node) => node.capacityMeshNodeId) + + expect(connectedNodeIds).toEqual([ + "diagonal-top", + "via-bottom", + "wire-bottom", + ]) + expect(solver.stats).toMatchObject({ + preloadedTraceShapeCount: 3, + projectedNodeCount: 3, + traceRegionAssignmentCount: 3, + }) +}) From 271eee6c49d86ff086d2034ddd5d065fb78b3333 Mon Sep 17 00:00:00 2001 From: seveibar Date: Wed, 22 Jul 2026 22:42:31 -0700 Subject: [PATCH 02/22] chore: satisfy Pipeline9 format check --- ...torouting-pipeline-solver9-preloaded-trace-graph.ts | 3 ++- .../preloaded-trace-graph-solver.ts | 10 ++-------- ...simple-route-json-without-trace-obstacles-solver.ts | 3 +-- scripts/run-sample.ts | 6 +----- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts index e86df5378..c7cb3141c 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts @@ -50,7 +50,8 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends AutoroutingP onSolved: (pipeline) => { const pipeline9 = pipeline as AutoroutingPipelineSolver9_PreloadedTraceGraph - pipeline9.capacityNodes = pipeline9.preloadedTraceGraphSolver!.getOutput() + pipeline9.capacityNodes = + pipeline9.preloadedTraceGraphSolver!.getOutput() }, }) } diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts index f86907b82..623977a0b 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts @@ -1,10 +1,6 @@ import { RbushIndex } from "lib/data-structures/RbushIndex" import { BaseSolver } from "lib/solvers/BaseSolver" -import type { - CapacityMeshNode, - Obstacle, - SimpleRouteJson, -} from "lib/types" +import type { CapacityMeshNode, Obstacle, SimpleRouteJson } from "lib/types" import { getObstaclesFromSrjTraces } from "lib/utils/convertSrjTracesToObstacles" import { getUniqueValidZLayersFromLayerNames } from "lib/utils/mapLayerNameToZ" @@ -25,9 +21,7 @@ type RotatedRectGeometry = { const GEOMETRIC_TOLERANCE = 1e-6 -const getRotatedRectGeometry = ( - obstacle: Obstacle, -): RotatedRectGeometry => { +const getRotatedRectGeometry = (obstacle: Obstacle): RotatedRectGeometry => { const radians = ((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180 const cos = Math.cos(radians) const sin = Math.sin(radians) diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts index 1857f1abb..e1f200778 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts @@ -16,8 +16,7 @@ export class PreprocessSimpleRouteJsonWithoutTraceObstaclesSolver extends Prepro srjWithApproximatingRects, ) - this.outputSrj = - traces === undefined ? outputSrj : { ...outputSrj, traces } + this.outputSrj = traces === undefined ? outputSrj : { ...outputSrj, traces } this.solved = true } } diff --git a/scripts/run-sample.ts b/scripts/run-sample.ts index 1c4625011..2a7b8ce2c 100644 --- a/scripts/run-sample.ts +++ b/scripts/run-sample.ts @@ -401,11 +401,7 @@ const parseArgs = (): RunSampleOptions => { throw new Error("--png-size must be at least 1024") } - if ( - options.netColors && - options.pipeline !== 7 && - options.pipeline !== 9 - ) { + if (options.netColors && options.pipeline !== 7 && options.pipeline !== 9) { throw new Error( "--net-colors is currently supported by pipelines 7 and 9 only", ) From b83758f9f8bd2cd1a0b9ad0fc803e8b9e5a661bd Mon Sep 17 00:00:00 2001 From: seveibar Date: Thu, 23 Jul 2026 17:28:00 -0700 Subject: [PATCH 03/22] fix Pipeline9 preloaded trace routing --- ...-implicit-bounds-to-connected-obstacles.ts | 26 ++++ .../infer-preloaded-trace-connectivity.ts | 114 ++++++++++++++++++ .../preloaded-trace-graph-solver.ts | 45 +++---- ...ute-json-without-trace-obstacles-solver.ts | 13 +- ...infer-preloaded-trace-connectivity.test.ts | 77 ++++++++++++ .../pipeline9-srj23-routing-bounds.test.ts | 24 ++++ .../preloaded-trace-graph-solver.test.ts | 22 +++- 7 files changed, 289 insertions(+), 32 deletions(-) create mode 100644 lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/expand-implicit-bounds-to-connected-obstacles.ts create mode 100644 lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/infer-preloaded-trace-connectivity.ts create mode 100644 tests/features/infer-preloaded-trace-connectivity.test.ts create mode 100644 tests/features/pipeline9-srj23-routing-bounds.test.ts diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/expand-implicit-bounds-to-connected-obstacles.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/expand-implicit-bounds-to-connected-obstacles.ts new file mode 100644 index 000000000..f3744ba7d --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/expand-implicit-bounds-to-connected-obstacles.ts @@ -0,0 +1,26 @@ +import { getBoundingBox } from "@tscircuit/math-utils" +import type { SimpleRouteJson } from "lib/types" + +export const expandImplicitBoundsToConnectedObstacles = ( + srj: SimpleRouteJson, +): SimpleRouteJson => { + if (srj.outline && srj.outline.length >= 3) { + return srj + } + + const bounds = { ...srj.bounds } + for (const obstacle of srj.obstacles) { + const hasConnectivity = + (obstacle.connectedTo?.length ?? 0) > 0 || + (obstacle.offBoardConnectsTo?.length ?? 0) > 0 || + obstacle.netIsAssignable === true + if (!hasConnectivity) continue + const obstacleBounds = getBoundingBox(obstacle) + bounds.minX = Math.min(bounds.minX, obstacleBounds.minX) + bounds.maxX = Math.max(bounds.maxX, obstacleBounds.maxX) + bounds.minY = Math.min(bounds.minY, obstacleBounds.minY) + bounds.maxY = Math.max(bounds.maxY, obstacleBounds.maxY) + } + + return { ...srj, bounds } +} diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/infer-preloaded-trace-connectivity.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/infer-preloaded-trace-connectivity.ts new file mode 100644 index 000000000..d8b0d49a5 --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/infer-preloaded-trace-connectivity.ts @@ -0,0 +1,114 @@ +import type { + ConnectionPoint, + SimpleRouteJson, + SimplifiedPcbTrace, +} from "lib/types" +import { getConnectionPointLayers } from "lib/types" + +type TraceEndpoint = { + x: number + y: number + layers: string[] +} + +const ENDPOINT_MATCH_TOLERANCE = 1e-3 + +const getTraceEndpoint = ( + trace: SimplifiedPcbTrace, + endpointIndex: 0 | 1, +): TraceEndpoint | null => { + const routePoint = + endpointIndex === 0 ? trace.route[0] : trace.route[trace.route.length - 1] + if (!routePoint || !("x" in routePoint) || !("y" in routePoint)) { + return null + } + + const layers = + routePoint.route_type === "via" + ? [routePoint.from_layer, routePoint.to_layer] + : routePoint.route_type === "wire" + ? [routePoint.layer] + : [] + return { x: routePoint.x, y: routePoint.y, layers } +} + +const doesConnectionPointMatchEndpoint = ( + point: ConnectionPoint, + endpoint: TraceEndpoint, + connectionName: string, + srj: SimpleRouteJson, +): boolean => { + if ( + Math.abs(point.x - endpoint.x) > ENDPOINT_MATCH_TOLERANCE || + Math.abs(point.y - endpoint.y) > ENDPOINT_MATCH_TOLERANCE + ) { + return false + } + + const pointIds = [point.pointId, point.pcb_port_id].filter( + (pointId): pointId is string => pointId !== undefined, + ) + const pointLayers = new Set(getConnectionPointLayers(point)) + for (const obstacle of srj.obstacles) { + if ( + Math.abs(obstacle.center.x - point.x) > ENDPOINT_MATCH_TOLERANCE || + Math.abs(obstacle.center.y - point.y) > ENDPOINT_MATCH_TOLERANCE + ) { + continue + } + const connectedTo = obstacle.connectedTo ?? [] + if ( + !connectedTo.includes(connectionName) && + !pointIds.some((pointId) => connectedTo.includes(pointId)) + ) { + continue + } + for (const layer of obstacle.layers) { + pointLayers.add(layer) + } + } + + return endpoint.layers.some((layer) => pointLayers.has(layer)) +} + +export const inferPreloadedTraceConnectivity = ( + srj: SimpleRouteJson, +): SimpleRouteJson => { + if (srj.traces === undefined) { + return srj + } + + const connectionsByName = new Map( + srj.connections.map((connection) => [connection.name, connection]), + ) + const traces = srj.traces.map((trace) => { + const connection = connectionsByName.get(trace.connection_name) + const endpoints = [ + getTraceEndpoint(trace, 0), + getTraceEndpoint(trace, 1), + ].filter((endpoint): endpoint is TraceEndpoint => endpoint !== null) + const inferredPointIds = + connection?.pointsToConnect + .filter((point) => + endpoints.some((endpoint) => + doesConnectionPointMatchEndpoint( + point, + endpoint, + trace.connection_name, + srj, + ), + ), + ) + .map((point) => point.pointId) + .filter((pointId): pointId is string => pointId !== undefined) ?? [] + + return { + ...trace, + connectsTo: [ + ...new Set([...(trace.connectsTo ?? []), ...inferredPointIds]), + ], + } + }) + + return { ...srj, traces } +} diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts index 623977a0b..a2616f8d3 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts @@ -45,7 +45,7 @@ const getRotatedRectGeometry = (obstacle: Obstacle): RotatedRectGeometry => { } } -const doesRotatedRectOverlapNode = ( +const doesRotatedRectContainNode = ( rect: RotatedRectGeometry, node: CapacityMeshNode, ): boolean => { @@ -60,27 +60,18 @@ const doesRotatedRectOverlapNode = ( deltaX * rect.unitY.x + deltaY * rect.unitY.y, ) + const nodeExtentOnRectX = + nodeHalfWidth * Math.abs(rect.unitX.x) + + nodeHalfHeight * Math.abs(rect.unitX.y) + const nodeExtentOnRectY = + nodeHalfWidth * Math.abs(rect.unitY.x) + + nodeHalfHeight * Math.abs(rect.unitY.y) + return ( - Math.abs(deltaX) <= - nodeHalfWidth + - rect.halfWidth * Math.abs(rect.unitX.x) + - rect.halfHeight * Math.abs(rect.unitY.x) + - GEOMETRIC_TOLERANCE && - Math.abs(deltaY) <= - nodeHalfHeight + - rect.halfWidth * Math.abs(rect.unitX.y) + - rect.halfHeight * Math.abs(rect.unitY.y) + - GEOMETRIC_TOLERANCE && - projectedOnRectX <= - rect.halfWidth + - nodeHalfWidth * Math.abs(rect.unitX.x) + - nodeHalfHeight * Math.abs(rect.unitX.y) + - GEOMETRIC_TOLERANCE && - projectedOnRectY <= - rect.halfHeight + - nodeHalfWidth * Math.abs(rect.unitY.x) + - nodeHalfHeight * Math.abs(rect.unitY.y) + - GEOMETRIC_TOLERANCE + projectedOnRectX + nodeExtentOnRectX <= + rect.halfWidth + GEOMETRIC_TOLERANCE && + projectedOnRectY + nodeExtentOnRectY <= + rect.halfHeight + GEOMETRIC_TOLERANCE ) } @@ -122,10 +113,10 @@ const getPreloadedTraceShapes = ( } /** - * Projects exact preloaded wire/via geometry onto the final capacity mesh. - * buildHyperGraph canonicalizes each `_connectedTo` value into a fixed net - * assignment, so old copper participates in pathing without fragmenting the - * RectDiff topology. + * Conservatively projects preloaded copper onto the final capacity mesh. + * A region is assigned only when one trace shape covers its full area and + * every available layer. The hypergraph has region-level ownership, so + * assigning on partial overlap would block free space or an unused layer. */ export class PreloadedTraceGraphSolver extends BaseSolver { private readonly outputNodes: CapacityMeshNode[] @@ -174,8 +165,8 @@ export class PreloadedTraceGraphSolver extends BaseSolver { ) for (const node of candidateNodes) { - if (!node.availableZ.some((z) => shape.zLayers.includes(z))) continue - if (!doesRotatedRectOverlapNode(geometry, node)) continue + if (!node.availableZ.every((z) => shape.zLayers.includes(z))) continue + if (!doesRotatedRectContainNode(geometry, node)) continue const connectedTo = new Set(node._connectedTo ?? []) const previousConnectionCount = connectedTo.size diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts index e1f200778..cdf383c57 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts @@ -3,14 +3,23 @@ import { addApproximatingRectsToSrj } from "lib/utils/addApproximatingRectsToSrj import { createSrjWithBoardValidObstacleLayers } from "lib/utils/create-srj-with-board-valid-obstacle-layers" import { filterObstaclesOutsideBoard } from "lib/utils/filterObstaclesOutsideBoard" import { PreprocessSimpleRouteJsonSolver } from "../AutoroutingPipeline4_TinyHypergraph/PreprocessSimpleRouteJsonSolver" +import { expandImplicitBoundsToConnectedObstacles } from "./expand-implicit-bounds-to-connected-obstacles" +import { inferPreloadedTraceConnectivity } from "./infer-preloaded-trace-connectivity" export class PreprocessSimpleRouteJsonWithoutTraceObstaclesSolver extends PreprocessSimpleRouteJsonSolver { override _step(): void { - const { traces, ...inputSrjWithoutTraces } = this.inputSrj + const inputSrjWithTraceConnectivity = inferPreloadedTraceConnectivity( + this.inputSrj, + ) + const { traces, ...inputSrjWithoutTraces } = inputSrjWithTraceConnectivity const srjWithBoardValidObstacleLayers = createSrjWithBoardValidObstacleLayers(inputSrjWithoutTraces) + const srjWithExpandedImplicitBounds = + expandImplicitBoundsToConnectedObstacles( + filterObstaclesOutsideBoard(srjWithBoardValidObstacleLayers), + ) const srjWithApproximatingRects = addApproximatingRectsToSrj( - filterObstaclesOutsideBoard(srjWithBoardValidObstacleLayers), + srjWithExpandedImplicitBounds, ) const outputSrj = createSrjWithBoardValidObstacleLayers( srjWithApproximatingRects, diff --git a/tests/features/infer-preloaded-trace-connectivity.test.ts b/tests/features/infer-preloaded-trace-connectivity.test.ts new file mode 100644 index 000000000..27cbaa39e --- /dev/null +++ b/tests/features/infer-preloaded-trace-connectivity.test.ts @@ -0,0 +1,77 @@ +import { expect, test } from "bun:test" +import { inferPreloadedTraceConnectivity } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/infer-preloaded-trace-connectivity" +import type { SimpleRouteJson } from "lib/types" + +test("infers quantized endpoints through multilayer connected pads", () => { + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -3, minY: -3, maxX: 3, maxY: 3 }, + obstacles: [ + { + type: "rect", + center: { x: 1, y: 1 }, + width: 0.6, + height: 0.6, + layers: ["top", "bottom"], + connectedTo: ["through-hole", "net1"], + }, + ], + connections: [ + { + name: "net1", + pointsToConnect: [ + { + x: 1, + y: 1, + layer: "top", + pointId: "through-hole", + pcb_port_id: "through-hole", + }, + { + x: 2, + y: 2, + layer: "top", + pointId: "quantized-top", + }, + { + x: 0, + y: 0, + layer: "top", + pointId: "unrelated", + }, + ], + }, + ], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "preloaded-net1", + connection_name: "net1", + connectsTo: ["existing"], + route: [ + { + route_type: "wire", + x: 1.0005, + y: 1.0005, + width: 0.1, + layer: "bottom", + }, + { + route_type: "wire", + x: 2.0007, + y: 2.0007, + width: 0.1, + layer: "top", + }, + ], + }, + ], + } + + expect(inferPreloadedTraceConnectivity(srj).traces?.[0]?.connectsTo).toEqual([ + "existing", + "through-hole", + "quantized-top", + ]) +}) diff --git a/tests/features/pipeline9-srj23-routing-bounds.test.ts b/tests/features/pipeline9-srj23-routing-bounds.test.ts new file mode 100644 index 000000000..33a7810c2 --- /dev/null +++ b/tests/features/pipeline9-srj23-routing-bounds.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from "bun:test" +import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib" +import { loadScenarioBySampleNumber } from "../../scripts/benchmark/scenarios" + +test("Pipeline9 routes srj23 samples with connected pads beyond implicit bounds", async () => { + for (const sampleNumber of [101, 107]) { + const { scenario } = await loadScenarioBySampleNumber( + "srj23", + sampleNumber, + ) + const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph(scenario, { + cacheProvider: null, + effort: 0.1, + }) + solver.solve() + + expect(solver.failed).toBe(false) + expect(solver.solved).toBe(true) + expect( + solver.preprocessSimpleRouteJsonSolver?.getOutputSimpleRouteJson().bounds, + ).not.toEqual(scenario.bounds) + expect(solver.originalSrj.bounds).toEqual(scenario.bounds) + } +}) diff --git a/tests/features/preloaded-trace-graph-solver.test.ts b/tests/features/preloaded-trace-graph-solver.test.ts index aada96057..e9b2664dc 100644 --- a/tests/features/preloaded-trace-graph-solver.test.ts +++ b/tests/features/preloaded-trace-graph-solver.test.ts @@ -2,16 +2,32 @@ import { expect, test } from "bun:test" import { PreloadedTraceGraphSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver" import type { CapacityMeshNode, SimpleRouteJson } from "lib/types" -test("preloaded trace projection follows rotated wires and via layers", () => { +test("preloaded trace projection only reserves fully covered layers", () => { const capacityMeshNodes: CapacityMeshNode[] = [ { capacityMeshNodeId: "diagonal-top", center: { x: 0, y: 0 }, + width: 0.05, + height: 0.05, + layer: "top", + availableZ: [0], + }, + { + capacityMeshNodeId: "coarse-diagonal-top", + center: { x: 0, y: 0 }, width: 0.2, height: 0.2, layer: "top", availableZ: [0], }, + { + capacityMeshNodeId: "multilayer-diagonal", + center: { x: 0, y: 0 }, + width: 0.05, + height: 0.05, + layer: "z0,1", + availableZ: [0, 1], + }, { capacityMeshNodeId: "off-diagonal-top", center: { x: 0, y: 1 }, @@ -31,8 +47,8 @@ test("preloaded trace projection follows rotated wires and via layers", () => { { capacityMeshNodeId: "wire-bottom", center: { x: 0, y: 2 }, - width: 0.2, - height: 0.2, + width: 0.05, + height: 0.05, layer: "bottom", availableZ: [1], }, From 1b9803d254fdd0a1c7027dd186553f4c63fc2a0b Mon Sep 17 00:00:00 2001 From: seveibar Date: Thu, 23 Jul 2026 17:31:33 -0700 Subject: [PATCH 04/22] fix Pipeline9 regression test formatting --- .../pipeline9-srj23-routing-bounds.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/features/pipeline9-srj23-routing-bounds.test.ts b/tests/features/pipeline9-srj23-routing-bounds.test.ts index 33a7810c2..59021fdad 100644 --- a/tests/features/pipeline9-srj23-routing-bounds.test.ts +++ b/tests/features/pipeline9-srj23-routing-bounds.test.ts @@ -4,14 +4,14 @@ import { loadScenarioBySampleNumber } from "../../scripts/benchmark/scenarios" test("Pipeline9 routes srj23 samples with connected pads beyond implicit bounds", async () => { for (const sampleNumber of [101, 107]) { - const { scenario } = await loadScenarioBySampleNumber( - "srj23", - sampleNumber, + const { scenario } = await loadScenarioBySampleNumber("srj23", sampleNumber) + const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph( + scenario, + { + cacheProvider: null, + effort: 0.1, + }, ) - const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph(scenario, { - cacheProvider: null, - effort: 0.1, - }) solver.solve() expect(solver.failed).toBe(false) From 602c6427bb99fbc059e276f851c7dc0f3df19f07 Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 24 Jul 2026 12:19:38 -0700 Subject: [PATCH 05/22] fix Pipeline9 relaxed DRC on srj23 --- ...-pipeline-solver9-preloaded-trace-graph.ts | 106 ++- .../pipeline9-exact-drc-repair-solver.ts | 643 ++++++++++++++++++ lib/testing/evaluate-relaxed-drc.ts | 1 + lib/testing/utils/convertToCircuitJson.ts | 161 +++-- .../pipeline9-srj23-relaxed-drc.test.ts | 38 ++ tests/pipeline7-relaxed-drc-evaluator.test.ts | 2 +- ...ircuitJson-root-alias-connectivity.test.ts | 106 +++ 7 files changed, 1009 insertions(+), 48 deletions(-) create mode 100644 lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts create mode 100644 tests/features/pipeline9-srj23-relaxed-drc.test.ts create mode 100644 tests/utils/convertToCircuitJson-root-alias-connectivity.test.ts diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts index c7cb3141c..c9c0aea29 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts @@ -1,10 +1,12 @@ +import type { DrcEvaluator } from "high-density-repair03/lib" import { BaseSolver } from "lib/solvers/BaseSolver" -import type { SimpleRouteJson } from "lib/types" +import type { Obstacle, SimpleRouteJson } from "lib/types" import { AutoroutingPipelineSolver7_MultiGraph, type AutoroutingPipelineSolverOptions, } from "../AutoroutingPipeline7_MultiGraph/AutoroutingPipelineSolver7_MultiGraph" import { PreloadedTraceGraphSolver } from "./preloaded-trace-graph-solver" +import { Pipeline9ExactDrcRepairSolver } from "./pipeline9-exact-drc-repair-solver" import { PreprocessSimpleRouteJsonWithoutTraceObstaclesSolver } from "./preprocess-simple-route-json-without-trace-obstacles-solver" type PipelineStep = { @@ -16,6 +18,75 @@ type PipelineStep = { onSolved?: (instance: AutoroutingPipelineSolver7_MultiGraph) => void } +const getAxisAlignedRepairObstacle = (obstacle: Obstacle): Obstacle => { + const radians = ((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180 + if (Math.abs(radians) < 1e-9) return obstacle + + return { + ...obstacle, + width: + Math.abs(obstacle.width * Math.cos(radians)) + + Math.abs(obstacle.height * Math.sin(radians)), + height: + Math.abs(obstacle.width * Math.sin(radians)) + + Math.abs(obstacle.height * Math.cos(radians)), + ccwRotationDegrees: 0, + } +} + +const createPadCenteredDrcEvaluator = ( + evaluator: DrcEvaluator | undefined, + originalObstacles: Obstacle[], +): DrcEvaluator | undefined => { + if (!evaluator) return undefined + + const physicalObstacleById = new Map() + for (const obstacle of originalObstacles) { + const physicalIds = [ + obstacle.connectedTo.find((id) => id.startsWith("pcb_smtpad_")), + obstacle.connectedTo.find((id) => id.startsWith("pcb_plated_hole_")), + ] + for (const physicalId of physicalIds) { + if (physicalId && !physicalObstacleById.has(physicalId)) { + physicalObstacleById.set(physicalId, obstacle) + } + } + } + const addAccuratePadCenters = ( + errors: Array>, + ): Array> => + errors.map((error) => { + const errorType = + typeof error.error_type === "string" + ? error.error_type + : typeof error.type === "string" + ? error.type + : undefined + if ( + errorType !== "pcb_pad_trace_clearance_error" || + typeof error.pcb_pad_id !== "string" + ) { + return error + } + const obstacle = physicalObstacleById.get(error.pcb_pad_id) + return obstacle ? { ...error, center: obstacle.center } : error + }) + + return (input) => { + const result = evaluator(input) + if (Array.isArray(result)) { + return addAccuratePadCenters(result) + } + return { + ...result, + errors: addAccuratePadCenters(result.errors), + errorsWithCenters: addAccuratePadCenters( + result.errorsWithCenters ?? result.errors, + ), + } + } +} + export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends AutoroutingPipelineSolver7_MultiGraph { preloadedTraceGraphSolver?: PreloadedTraceGraphSolver @@ -54,6 +125,39 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends AutoroutingP pipeline9.preloadedTraceGraphSolver!.getOutput() }, }) + + const exactDrcStep = pipelineDef.find( + (step) => step.solverName === "exactGeometryDrcForceImproveSolver", + ) + if (!exactDrcStep) { + throw new Error("Pipeline9 could not find the exact DRC repair stage") + } + exactDrcStep.solverClass = Pipeline9ExactDrcRepairSolver + const getBaseExactDrcParams = exactDrcStep.getConstructorParams + exactDrcStep.getConstructorParams = (pipeline) => { + const [params] = getBaseExactDrcParams(pipeline) + const originalObstacles = pipeline.originalSrj.obstacles + const drcEvaluator = createPadCenteredDrcEvaluator( + params.drcEvaluator, + originalObstacles, + ) + const viaInPadDrcEvaluator = createPadCenteredDrcEvaluator( + params.viaInPadDrcEvaluator, + originalObstacles, + ) + return [ + { + ...params, + drcEvaluator, + viaInPadDrcEvaluator, + originalObstacles, + srj: { + ...params.srj, + obstacles: originalObstacles.map(getAxisAlignedRepairObstacle), + }, + }, + ] + } } override getSolverName(): string { diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts new file mode 100644 index 000000000..5cfd1edc6 --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts @@ -0,0 +1,643 @@ +import { + GlobalDrcBranchPortfolioSolver, + type GlobalDrcBranchPortfolioSolverParams, +} from "high-density-repair03/lib" +import { + applyDrcErrorForces, + cloneRoutes, + getDrcSnapshot, + materializeRoutes, +} from "high-density-repair03/lib/solvers/GlobalDrcForceImproveSolver/solverHelpers" +import type { Obstacle } from "lib/types" +import type { HighDensityRoute } from "lib/types/high-density-types" +import { mapZToLayerName } from "lib/utils/mapZToLayerName" + +type Pipeline9ExactDrcRepairSolverParams = + GlobalDrcBranchPortfolioSolverParams & { + originalObstacles: Obstacle[] + } + +type DrcError = Record + +type TerminalConstraint = { + routeIndex: number + endpoint: "start" | "end" + originalPoint: HighDensityRoute["route"][number] + traceRadius: number + owningObstacles: Obstacle[] +} + +const POSITION_EPSILON = 1e-6 +const ENDPOINT_SLIDE_RADII = [ + 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.2, 0.25, 0.3, +] as const +const BATCHED_TRACE_FORCE_SCALES = [4, 2, 1, 0.5, 0.25] as const +const MAX_BATCHED_TRACE_FORCE_PASSES = 20 +const MAX_CLEANUP_PASSES = 8 +const MAX_LOCAL_LAYER_DETOUR_EXPANSION = 6 + +const getErrorType = (error: DrcError): string | undefined => + typeof error.error_type === "string" + ? error.error_type + : typeof error.type === "string" + ? error.type + : undefined + +const getPhysicalPadIdFromError = (error: DrcError): string | undefined => { + if (typeof error.pcb_pad_id === "string") return error.pcb_pad_id + if (typeof error.pcb_trace_error_id !== "string") return undefined + return error.pcb_trace_error_id.match(/(pcb_(?:smtpad|plated_hole)_.+)$/)?.[1] +} + +const getPointDistance = ( + left: Pick, + right: Pick, +): number => Math.hypot(left.x - right.x, left.y - right.y) + +const obstacleAppliesToLayer = ( + obstacle: Obstacle, + z: number, + layerCount: number, +): boolean => { + if (obstacle.__zLayers?.includes(z)) { + return true + } + return obstacle.layers.includes(mapZToLayerName(z, layerCount)) +} + +const pointFitsInsideObstacle = ( + point: Pick, + obstacle: Obstacle, + inset: number, +): boolean => { + const radians = -((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180 + const deltaX = point.x - obstacle.center.x + const deltaY = point.y - obstacle.center.y + const localX = deltaX * Math.cos(radians) - deltaY * Math.sin(radians) + const localY = deltaX * Math.sin(radians) + deltaY * Math.cos(radians) + const halfWidth = obstacle.width / 2 - inset + const halfHeight = obstacle.height / 2 - inset + if (halfWidth <= 0 || halfHeight <= 0) return false + + if (String(obstacle.type) === "oval") { + return ( + (localX * localX) / (halfWidth * halfWidth) + + (localY * localY) / (halfHeight * halfHeight) <= + 1 + POSITION_EPSILON + ) + } + + return ( + Math.abs(localX) <= halfWidth + POSITION_EPSILON && + Math.abs(localY) <= halfHeight + POSITION_EPSILON + ) +} + +const obstacleSharesRouteNet = ( + obstacle: Obstacle, + route: HighDensityRoute, +): boolean => + obstacle.connectedTo.includes( + route.rootConnectionName ?? route.connectionName, + ) || obstacle.connectedTo.includes(route.connectionName) + +const obstacleRepresentsPhysicalPad = ( + obstacle: Obstacle, + padId: string, +): boolean => + obstacle.connectedTo.find((id) => id.startsWith("pcb_smtpad_")) === padId || + obstacle.connectedTo.find((id) => id.startsWith("pcb_plated_hole_")) === padId + +const getOtherTraceId = ( + error: DrcError, + traceRouteIndexById: Map, +): string | undefined => { + const traceId = error.pcb_trace_id + const errorId = error.pcb_trace_error_id + if (typeof traceId !== "string" || typeof errorId !== "string") { + return undefined + } + const prefix = `overlap_${traceId}_` + if (!errorId.startsWith(prefix)) return undefined + const otherTraceId = errorId.slice(prefix.length) + return traceRouteIndexById.has(otherTraceId) ? otherTraceId : undefined +} + +const getPointToSegmentDistance = ( + point: { x: number; y: number }, + start: { x: number; y: number }, + end: { x: number; y: number }, +): number => { + const deltaX = end.x - start.x + const deltaY = end.y - start.y + const lengthSquared = deltaX * deltaX + deltaY * deltaY + if (lengthSquared <= POSITION_EPSILON) { + return getPointDistance(point, start) + } + const projection = Math.max( + 0, + Math.min( + 1, + ((point.x - start.x) * deltaX + (point.y - start.y) * deltaY) / + lengthSquared, + ), + ) + return getPointDistance(point, { + x: start.x + deltaX * projection, + y: start.y + deltaY * projection, + }) +} + +const getCoincidentTerminalPointIndexes = ( + route: HighDensityRoute, + endpointIndex: number, +): number[] => { + const endpoint = route.route[endpointIndex] + if (!endpoint) return [] + const indexes = [endpointIndex] + const direction = endpointIndex === 0 ? 1 : -1 + + for ( + let pointIndex = endpointIndex + direction; + pointIndex >= 0 && pointIndex < route.route.length; + pointIndex += direction + ) { + const point = route.route[pointIndex] + if (!point || getPointDistance(point, endpoint) > POSITION_EPSILON) break + indexes.push(pointIndex) + } + + return indexes +} + +export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolver { + private readonly originalObstacles: Obstacle[] + private readonly terminalConstraints: TerminalConstraint[] + private cleanupStarted = false + private cleanupCandidateAttempts = 0 + private cleanupCandidatesAccepted = 0 + + constructor(params: Pipeline9ExactDrcRepairSolverParams) { + super(params) + this.originalObstacles = params.originalObstacles + this.terminalConstraints = params.hdRoutes.flatMap((route, routeIndex) => + (["start", "end"] as const).flatMap((endpoint) => { + const point = endpoint === "start" ? route.route[0] : route.route.at(-1) + if (!point) return [] + const traceRadius = + (route.traceThickness ?? params.srj.minTraceWidth) / 2 + const owningObstacles = params.originalObstacles.filter( + (obstacle) => + obstacleSharesRouteNet(obstacle, route) && + obstacleAppliesToLayer(obstacle, point.z, params.srj.layerCount) && + pointFitsInsideObstacle(point, obstacle, 0), + ) + return [ + { + routeIndex, + endpoint, + originalPoint: { ...point }, + traceRadius, + owningObstacles, + }, + ] + }), + ) + } + + private getSnapshot(routes: HighDensityRoute[]) { + return getDrcSnapshot( + this.params.srj, + routes, + this.params.drcEvaluator, + this.params.connMap, + ) + } + + private unlockCleanupTerminals( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + return routes.map((route) => ({ + ...route, + route: route.route.map((point, pointIndex) => { + if (pointIndex !== 0 && pointIndex !== route.route.length - 1) { + return point + } + const { pcb_port_id: _pcbPortId, ...movablePoint } = point + return movablePoint + }), + })) + } + + private restoreTerminalIds(routes: HighDensityRoute[]): HighDensityRoute[] { + const markerByRouteAndEndpoint = new Map( + this.terminalConstraints + .filter( + ( + constraint, + ): constraint is TerminalConstraint & { + originalPoint: HighDensityRoute["route"][number] & { + pcb_port_id: string + } + } => typeof constraint.originalPoint.pcb_port_id === "string", + ) + .map((constraint) => [ + `${constraint.routeIndex}:${constraint.endpoint}`, + constraint.originalPoint.pcb_port_id, + ]), + ) + + return routes.map((route, routeIndex) => ({ + ...route, + route: route.route.map((point, pointIndex) => { + const endpoint = + pointIndex === 0 + ? "start" + : pointIndex === route.route.length - 1 + ? "end" + : undefined + if (!endpoint) return point + const pcbPortId = markerByRouteAndEndpoint.get( + `${routeIndex}:${endpoint}`, + ) + return pcbPortId ? { ...point, pcb_port_id: pcbPortId } : point + }), + })) + } + + private candidatePreservesTerminals(routes: HighDensityRoute[]): boolean { + return this.terminalConstraints.every((constraint) => { + const route = routes[constraint.routeIndex] + const point = + constraint.endpoint === "start" ? route?.route[0] : route?.route.at(-1) + if (!point || point.z !== constraint.originalPoint.z) return false + + const stayedAtOriginalPosition = + getPointDistance(point, constraint.originalPoint) <= POSITION_EPSILON + if (stayedAtOriginalPosition) return true + + return constraint.owningObstacles.some((obstacle) => + pointFitsInsideObstacle(point, obstacle, constraint.traceRadius), + ) + }) + } + + private candidateImprovesSnapshot( + candidateRoutes: HighDensityRoute[], + currentIssueCount: number, + ): boolean { + this.cleanupCandidateAttempts += 1 + if (!this.candidatePreservesTerminals(candidateRoutes)) return false + + const candidateSnapshot = this.getSnapshot(candidateRoutes) + if (candidateSnapshot.count >= currentIssueCount) return false + this.cleanupCandidatesAccepted += 1 + return true + } + + private tryEndpointSlide( + routes: HighDensityRoute[], + error: DrcError, + ): HighDensityRoute[] | undefined { + const errorType = getErrorType(error) + const padId = getPhysicalPadIdFromError(error) + if ( + !padId || + (errorType !== "pcb_pad_trace_clearance_error" && + errorType !== "pcb_trace_error") + ) { + return undefined + } + + const snapshot = this.getSnapshot(routes) + const traceId = error.pcb_trace_id + if (typeof traceId !== "string") return undefined + const routeIndex = snapshot.traceRouteIndexById.get(traceId) + if (routeIndex === undefined) return undefined + const route = routes[routeIndex] + const foreignObstacle = this.originalObstacles.find((obstacle) => + obstacleRepresentsPhysicalPad(obstacle, padId), + ) + if (!route || !foreignObstacle || route.route.length < 2) { + return undefined + } + + const endpointIndexes = [0, route.route.length - 1].sort((left, right) => { + const leftPoint = route.route[left]! + const rightPoint = route.route[right]! + return ( + getPointDistance(leftPoint, foreignObstacle.center) - + getPointDistance(rightPoint, foreignObstacle.center) + ) + }) + + for (const endpointIndex of endpointIndexes) { + const endpoint = route.route[endpointIndex]! + const traceRadius = + (endpoint.traceThickness ?? + route.traceThickness ?? + this.params.srj.minTraceWidth) / 2 + const ownObstacles = this.originalObstacles.filter( + (obstacle) => + obstacle !== foreignObstacle && + obstacleSharesRouteNet(obstacle, route) && + obstacleAppliesToLayer( + obstacle, + endpoint.z, + this.params.srj.layerCount, + ) && + pointFitsInsideObstacle(endpoint, obstacle, 0), + ) + if (ownObstacles.length === 0) continue + + const awayX = endpoint.x - foreignObstacle.center.x + const awayY = endpoint.y - foreignObstacle.center.y + const awayLength = Math.hypot(awayX, awayY) + const normalizedAway = + awayLength > POSITION_EPSILON + ? { x: awayX / awayLength, y: awayY / awayLength } + : { x: 1, y: 0 } + const directions = [ + normalizedAway, + { x: 1, y: 0 }, + { x: -1, y: 0 }, + { x: 0, y: 1 }, + { x: 0, y: -1 }, + { x: Math.SQRT1_2, y: Math.SQRT1_2 }, + { x: Math.SQRT1_2, y: -Math.SQRT1_2 }, + { x: -Math.SQRT1_2, y: Math.SQRT1_2 }, + { x: -Math.SQRT1_2, y: -Math.SQRT1_2 }, + ] + const coincidentIndexes = getCoincidentTerminalPointIndexes( + route, + endpointIndex, + ) + + for (const radius of ENDPOINT_SLIDE_RADII) { + for (const direction of directions) { + const candidatePoint = { + x: endpoint.x + direction.x * radius, + y: endpoint.y + direction.y * radius, + } + if ( + !ownObstacles.some((obstacle) => + pointFitsInsideObstacle(candidatePoint, obstacle, traceRadius), + ) + ) { + continue + } + + const candidateRoutes = cloneRoutes(routes) + const candidateRoute = candidateRoutes[routeIndex] + if (!candidateRoute) continue + for (const pointIndex of coincidentIndexes) { + const point = candidateRoute.route[pointIndex] + if (!point) continue + point.x = candidatePoint.x + point.y = candidatePoint.y + } + const materializedCandidate = materializeRoutes(candidateRoutes) + if ( + this.candidateImprovesSnapshot( + materializedCandidate, + snapshot.count, + ) + ) { + return materializedCandidate + } + } + } + } + + return undefined + } + + private tryLocalTraceLayerDetour( + routes: HighDensityRoute[], + error: DrcError, + ): HighDensityRoute[] | undefined { + const errorType = getErrorType(error) + if ( + errorType !== "pcb_trace_error" && + errorType !== "pcb_pad_trace_clearance_error" + ) { + return undefined + } + const center = error.center + if ( + !center || + typeof center !== "object" || + typeof (center as { x?: unknown }).x !== "number" || + typeof (center as { y?: unknown }).y !== "number" + ) { + return undefined + } + + const snapshot = this.getSnapshot(routes) + const primaryTraceId = error.pcb_trace_id + const otherTraceId = getOtherTraceId(error, snapshot.traceRouteIndexById) + const traceIds = [otherTraceId, primaryTraceId].filter( + (traceId): traceId is string => + typeof traceId === "string" && + snapshot.traceRouteIndexById.has(traceId), + ) + const errorCenter = center as { x: number; y: number } + + for (const traceId of traceIds) { + const routeIndex = snapshot.traceRouteIndexById.get(traceId)! + const route = routes[routeIndex] + if (!route || route.route.length < 2) continue + + let nearestSegmentIndex = -1 + let nearestSegmentDistance = Number.POSITIVE_INFINITY + for ( + let segmentIndex = 0; + segmentIndex < route.route.length - 1; + segmentIndex += 1 + ) { + const start = route.route[segmentIndex] + const end = route.route[segmentIndex + 1] + if (!start || !end || start.z !== end.z) continue + const segmentDistance = getPointToSegmentDistance( + errorCenter, + start, + end, + ) + if (segmentDistance < nearestSegmentDistance) { + nearestSegmentDistance = segmentDistance + nearestSegmentIndex = segmentIndex + } + } + if (nearestSegmentIndex < 0) continue + + const z = route.route[nearestSegmentIndex]!.z + let sameLayerStart = nearestSegmentIndex + let sameLayerEnd = nearestSegmentIndex + 1 + while (sameLayerStart > 0 && route.route[sameLayerStart - 1]?.z === z) { + sameLayerStart -= 1 + } + while ( + sameLayerEnd < route.route.length - 1 && + route.route[sameLayerEnd + 1]?.z === z + ) { + sameLayerEnd += 1 + } + + for ( + let expansion = 0; + expansion <= MAX_LOCAL_LAYER_DETOUR_EXPANSION; + expansion += 1 + ) { + const detourStart = Math.max( + sameLayerStart, + nearestSegmentIndex - expansion, + ) + const detourEnd = Math.min( + sameLayerEnd, + nearestSegmentIndex + 1 + expansion, + ) + if (detourEnd <= detourStart) continue + + for ( + let targetZ = 0; + targetZ < this.params.srj.layerCount; + targetZ += 1 + ) { + if (targetZ === z) continue + const candidateRoutes = cloneRoutes(routes) + const candidateRoute = candidateRoutes[routeIndex] + if (!candidateRoute) continue + const startPoint = candidateRoute.route[detourStart] + if (!startPoint || !candidateRoute.route[detourEnd]) continue + + const detourPoints = candidateRoute.route + .slice(detourStart, detourEnd + 1) + .map((point) => ({ + ...point, + z: targetZ, + pcb_port_id: undefined, + })) + candidateRoute.route.splice( + detourStart + 1, + detourEnd - detourStart - 1, + ...detourPoints, + ) + + const materializedCandidate = materializeRoutes(candidateRoutes) + if ( + this.candidateImprovesSnapshot( + materializedCandidate, + snapshot.count, + ) + ) { + return materializedCandidate + } + } + } + } + + return undefined + } + + private tryBatchedTraceForce( + routes: HighDensityRoute[], + error: DrcError, + ): HighDensityRoute[] | undefined { + const errorType = getErrorType(error) + if ( + errorType !== "pcb_trace_error" && + errorType !== "pcb_pad_trace_clearance_error" + ) { + return undefined + } + + const snapshot = this.getSnapshot(routes) + const primaryTraceId = error.pcb_trace_id + const otherTraceId = getOtherTraceId(error, snapshot.traceRouteIndexById) + const traceIds = [otherTraceId, primaryTraceId].filter( + (traceId): traceId is string => + typeof traceId === "string" && + snapshot.traceRouteIndexById.has(traceId), + ) + for (const traceId of traceIds) { + for (const scale of BATCHED_TRACE_FORCE_SCALES) { + let candidateRoutes = cloneRoutes(routes) + for (let pass = 0; pass < MAX_BATCHED_TRACE_FORCE_PASSES; pass += 1) { + const changed = applyDrcErrorForces( + this.params.srj, + candidateRoutes, + [{ ...error, pcb_trace_id: traceId }], + snapshot.traceRouteIndexById, + scale, + this.params.connMap, + ) + if (!changed) break + + const materializedCandidate = materializeRoutes(candidateRoutes) + if ( + this.candidateImprovesSnapshot( + materializedCandidate, + snapshot.count, + ) + ) { + return materializedCandidate + } + candidateRoutes = cloneRoutes(materializedCandidate) + } + } + } + + return undefined + } + + private runPipeline9Cleanup(routes: HighDensityRoute[]): HighDensityRoute[] { + let improvedRoutes = this.unlockCleanupTerminals(routes) + + for (let pass = 0; pass < MAX_CLEANUP_PASSES; pass += 1) { + const snapshot = this.getSnapshot(improvedRoutes) + if (snapshot.count === 0) break + + let nextRoutes: HighDensityRoute[] | undefined + for (const error of snapshot.errors) { + nextRoutes = this.tryEndpointSlide(improvedRoutes, error) + if (nextRoutes) break + } + if (!nextRoutes) { + for (const error of snapshot.errors) { + nextRoutes = this.tryLocalTraceLayerDetour(improvedRoutes, error) + if (nextRoutes) break + } + } + if (!nextRoutes) { + for (const error of snapshot.errors) { + nextRoutes = this.tryBatchedTraceForce(improvedRoutes, error) + if (nextRoutes) break + } + } + if (!nextRoutes) break + improvedRoutes = nextRoutes + } + + return this.restoreTerminalIds(improvedRoutes) + } + + override _step(): void { + if (!this.cleanupStarted) { + super._step() + if (!this.solved) return + this.cleanupStarted = true + this.solved = false + } + + this.outputHdRoutes = this.runPipeline9Cleanup(this.outputHdRoutes) + const finalSnapshot = this.getSnapshot(this.outputHdRoutes) + this.stats = { + ...this.stats, + finalDrcIssueCount: finalSnapshot.count, + pipeline9DrcCleanupCandidateAttempts: this.cleanupCandidateAttempts, + pipeline9DrcCleanupCandidatesAccepted: this.cleanupCandidatesAccepted, + } + this.progress = 1 + this.solved = true + } +} diff --git a/lib/testing/evaluate-relaxed-drc.ts b/lib/testing/evaluate-relaxed-drc.ts index 120c4a7e0..b1ee76783 100644 --- a/lib/testing/evaluate-relaxed-drc.ts +++ b/lib/testing/evaluate-relaxed-drc.ts @@ -25,6 +25,7 @@ export const evaluateRelaxedDrc = ({ const circuitJson = convertToCircuitJson(srjWithPointPairs, traces, { minTraceWidth: inputSrj.minTraceWidth, minViaDiameter: inputSrj.minViaDiameter, + originalSrj: inputSrj, }) return { diff --git a/lib/testing/utils/convertToCircuitJson.ts b/lib/testing/utils/convertToCircuitJson.ts index 4e45ecf96..857cc0c1e 100644 --- a/lib/testing/utils/convertToCircuitJson.ts +++ b/lib/testing/utils/convertToCircuitJson.ts @@ -1,4 +1,3 @@ -import { pointToBoxDistance } from "@tscircuit/math-utils" import type { AnyCircuitElement, PcbTrace, PcbVia } from "circuit-json" import { Obstacle, SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" import { HighDensityRoute } from "lib/types/high-density-types" @@ -210,9 +209,6 @@ function convertHdRouteToCircuitJsonTraces( return traces } -const getObstacleConnectivityIds = (obstacles: Obstacle[]) => - Array.from(new Set(obstacles.flatMap((obstacle) => obstacle.connectedTo))) - /** * Create source_trace elements from the SimpleRouteJson connections * These represent the logical connections between points @@ -220,8 +216,94 @@ const getObstacleConnectivityIds = (obstacles: Obstacle[]) => function createSourceTraces( srj: SimpleRouteJson, hdRoutes: SimplifiedPcbTrace[] | HighDensityRoute[], + obstacleSrj: SimpleRouteJson = srj, ): AnyCircuitElement[] { const sourceTraces: AnyCircuitElement[] = [] + const sourceTraceById = new Map() + const endpointConnectivityByRouteName = new Map>() + + type RouteEndpoint = { x: number; y: number; layers: string[] } + const getRouteEndpoint = ( + route: SimplifiedPcbTrace | HighDensityRoute, + endpoint: "start" | "end", + ): RouteEndpoint | undefined => { + const segment = endpoint === "start" ? route.route[0] : route.route.at(-1) + if (!segment) return undefined + + if ("route_type" in segment) { + if (segment.route_type === "wire") { + return { x: segment.x, y: segment.y, layers: [segment.layer] } + } + if (segment.route_type === "via") { + return { + x: segment.x, + y: segment.y, + layers: [segment.from_layer, segment.to_layer], + } + } + const point = segment[endpoint] + return { + x: point.x, + y: point.y, + layers: [ + segment.route_type === "jumper" + ? segment.layer + : endpoint === "start" + ? segment.from_layer + : segment.to_layer, + ], + } + } + + return { + x: segment.x, + y: segment.y, + layers: [mapZToLayerName(segment.z, obstacleSrj.layerCount)], + } + } + const endpointIsInsideObstacle = ( + endpoint: RouteEndpoint, + obstacle: Obstacle, + ): boolean => { + if (!endpoint.layers.some((layer) => obstacle.layers.includes(layer))) { + return false + } + + const radians = -((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180 + const deltaX = endpoint.x - obstacle.center.x + const deltaY = endpoint.y - obstacle.center.y + const localX = deltaX * Math.cos(radians) - deltaY * Math.sin(radians) + const localY = deltaX * Math.sin(radians) + deltaY * Math.cos(radians) + return ( + Math.abs(localX) <= obstacle.width / 2 + 1e-9 && + Math.abs(localY) <= obstacle.height / 2 + 1e-9 + ) + } + + for (const hdRoute of hdRoutes) { + const routeName = + (hdRoute as SimplifiedPcbTrace).connection_name ?? + (hdRoute as HighDensityRoute).connectionName + if (!routeName || hdRoute.route.length === 0) continue + + const endpoints = [ + getRouteEndpoint(hdRoute, "start"), + getRouteEndpoint(hdRoute, "end"), + ].filter((endpoint): endpoint is RouteEndpoint => endpoint !== undefined) + const endpointConnectivity = + endpointConnectivityByRouteName.get(routeName) ?? new Set() + + for (const endpoint of endpoints) { + for (const obstacle of obstacleSrj.obstacles) { + if (endpointIsInsideObstacle(endpoint, obstacle)) { + for (const connectivityId of obstacle.connectedTo) { + endpointConnectivity.add(connectivityId) + } + } + } + } + endpointConnectivityByRouteName.set(routeName, endpointConnectivity) + } // Process each connection to create a source_trace srj.connections.forEach((connection) => { @@ -236,44 +318,30 @@ function createSourceTraces( connection.__netConnectionName || connection.__rootConnectionNames?.[0] || connection.name - - // Test for obstacles we're inside of - const obstaclesContainingEndpoints: Obstacle[] = [] - const hdRoute = hdRoutes.find( - (r) => - ((r as any).connection_name ?? (r as any).connectionName) === - connection.name, - ) - if (hdRoute) { - const getPointFromSegment = (segment: (typeof hdRoute.route)[0]) => { - if ("route_type" in segment && segment.route_type === "jumper") { - return segment.start - } - if ("x" in segment && "y" in segment) { - return { x: segment.x, y: segment.y } - } - return { x: 0, y: 0 } - } - - const endpoints = [ - getPointFromSegment(hdRoute.route[0]), - getPointFromSegment(hdRoute.route[hdRoute.route.length - 1]), + const connectionNames = new Set([ + connection.name, + connection.__netConnectionName, + ...(connection.__rootConnectionNames ?? []), + ]) + const connectedSourceNetIds = new Set( + [ + connection.__netConnectionName, + ...(connection.__rootConnectionNames ?? []), ] - - for (const endpoint of endpoints) { - for (const obstacle of srj.obstacles) { - if (pointToBoxDistance(endpoint, obstacle) <= 0) { - obstaclesContainingEndpoints.push(obstacle) - } - } + .filter((name): name is string => Boolean(name)) + .filter((name) => name !== netConnectionName), + ) + for (const connectionName of connectionNames) { + if (!connectionName) continue + for (const connectivityId of endpointConnectivityByRouteName.get( + connectionName, + ) ?? []) { + connectedSourceNetIds.add(connectivityId) } } // Check if this source_trace already exists - const existingSourceTrace = sourceTraces.find( - (st) => - st.type === "source_trace" && st.source_trace_id === netConnectionName, - ) + const existingSourceTrace = sourceTraceById.get(netConnectionName) if (existingSourceTrace) { // Add these port IDs to the existing source_trace @@ -287,19 +355,19 @@ function createSourceTraces( sourceTrace.connected_source_net_ids = [ ...new Set([ ...(sourceTrace.connected_source_net_ids ?? []), - ...getObstacleConnectivityIds(obstaclesContainingEndpoints), + ...connectedSourceNetIds, ]), ] } else { // Create a new source_trace for this connection - sourceTraces.push({ + const sourceTrace = { type: "source_trace", source_trace_id: netConnectionName, connected_source_port_ids: connectedPortIds, - connected_source_net_ids: getObstacleConnectivityIds( - obstaclesContainingEndpoints, - ), - }) + connected_source_net_ids: [...connectedSourceNetIds], + } as AnyCircuitElement + sourceTraces.push(sourceTrace) + sourceTraceById.set(netConnectionName, sourceTrace) } }) @@ -658,15 +726,16 @@ export function convertToCircuitJson( // Start with empty circuit JSON const circuitJson: AnyCircuitElement[] = [] + const circuitSrj = originalSrj ?? srjWithPointPairs // Add source traces from connection information - circuitJson.push(...createSourceTraces(srjWithPointPairs, routes)) + circuitJson.push(...createSourceTraces(srjWithPointPairs, routes, circuitSrj)) // Add PCB ports for connection points - circuitJson.push(...createPcbPorts(srjWithPointPairs)) + circuitJson.push(...createPcbPorts(circuitSrj)) // Add PCB pads / plated holes represented by SRJ obstacles - circuitJson.push(...createPcbPadElements(originalSrj ?? srjWithPointPairs)) + circuitJson.push(...createPcbPadElements(circuitSrj)) // Extract and add vias as independent pcb_via elements circuitJson.push( diff --git a/tests/features/pipeline9-srj23-relaxed-drc.test.ts b/tests/features/pipeline9-srj23-relaxed-drc.test.ts new file mode 100644 index 000000000..b8cad2b35 --- /dev/null +++ b/tests/features/pipeline9-srj23-relaxed-drc.test.ts @@ -0,0 +1,38 @@ +import { expect, test } from "bun:test" +import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib" +import { evaluateRelaxedDrc } from "lib/testing/evaluate-relaxed-drc" +import { loadScenarioBySampleNumber } from "../../scripts/benchmark/scenarios" + +test( + "Pipeline9 passes relaxed DRC on srj23 repair regressions", + async () => { + const failures: Array<{ sampleNumber: number; errors: unknown[] }> = [] + + for (const sampleNumber of [14, 21, 32, 55, 58, 62, 64, 107]) { + const { scenario } = await loadScenarioBySampleNumber( + "srj23", + sampleNumber, + ) + const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph( + scenario, + { + cacheProvider: null, + effort: 1, + }, + ) + solver.solve() + + const { errors } = evaluateRelaxedDrc({ + inputSrj: scenario, + srjWithPointPairs: solver.srjWithPointPairs!, + traces: solver.getOutputSimplifiedPcbTraces(), + }) + if (errors.length > 0) { + failures.push({ sampleNumber, errors }) + } + } + + expect(failures).toEqual([]) + }, + { timeout: 120_000 }, +) diff --git a/tests/pipeline7-relaxed-drc-evaluator.test.ts b/tests/pipeline7-relaxed-drc-evaluator.test.ts index c99f9b0cd..b15a10e20 100644 --- a/tests/pipeline7-relaxed-drc-evaluator.test.ts +++ b/tests/pipeline7-relaxed-drc-evaluator.test.ts @@ -88,7 +88,7 @@ test("Pipeline7 repair uses the benchmark relaxed DRC path", () => { benchmarkResult.errors.some( (error) => error.type === "pcb_pad_trace_clearance_error", ), - ).toBe(false) + ).toBe(true) expect( benchmarkResult.errors.some( (error) => diff --git a/tests/utils/convertToCircuitJson-root-alias-connectivity.test.ts b/tests/utils/convertToCircuitJson-root-alias-connectivity.test.ts new file mode 100644 index 000000000..cffd3bc97 --- /dev/null +++ b/tests/utils/convertToCircuitJson-root-alias-connectivity.test.ts @@ -0,0 +1,106 @@ +import { expect, test } from "bun:test" +import { getFullConnectivityMapFromCircuitJson } from "circuit-json-to-connectivity-map" +import { convertToCircuitJson } from "lib/testing/utils/convertToCircuitJson" +import type { SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" + +test("connects every merged root alias to the routed trace and its pads", () => { + const originalSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -1, maxX: 3, minY: -1, maxY: 1 }, + obstacles: [ + { + type: "rect", + layers: ["top"], + center: { x: 0, y: 0 }, + width: 0.4, + height: 0.4, + connectedTo: [ + "pcb_smtpad_0", + "pcb_port_0", + "root_a", + "endpoint_alias_a", + ], + }, + { + type: "rect", + layers: ["top"], + center: { x: 2, y: 0 }, + width: 0.4, + height: 0.4, + connectedTo: ["pcb_smtpad_1", "pcb_port_1", "root_b"], + }, + { + type: "rect", + layers: ["bottom"], + center: { x: 0, y: 0 }, + width: 0.4, + height: 0.4, + connectedTo: ["bottom_only_alias"], + }, + { + type: "rect", + layers: ["top"], + center: { x: -0.6, y: -0.6 }, + width: 2, + height: 0.2, + ccwRotationDegrees: 45, + connectedTo: ["rotated_endpoint_alias"], + }, + ], + connections: [ + { + name: "root_a", + pointsToConnect: [ + { x: 0, y: 0, layer: "top", pcb_port_id: "pcb_port_0" }, + { x: 1, y: 0, layer: "top" }, + ], + }, + { + name: "root_b", + pointsToConnect: [ + { x: 1, y: 0, layer: "top" }, + { x: 2, y: 0, layer: "top", pcb_port_id: "pcb_port_1" }, + ], + }, + ], + } + const srjWithPointPairs: SimpleRouteJson = { + ...originalSrj, + connections: [ + { + name: "merged_mst0", + __rootConnectionNames: ["root_a", "root_b"], + pointsToConnect: [ + { x: 0, y: 0, layer: "top", pcb_port_id: "pcb_port_0" }, + { x: 2, y: 0, layer: "top", pcb_port_id: "pcb_port_1" }, + ], + }, + ], + } + const traces: SimplifiedPcbTrace[] = [ + { + type: "pcb_trace", + pcb_trace_id: "pcb_trace_0", + connection_name: "merged_mst0", + route: [ + { route_type: "wire", x: 0, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 2, y: 0, width: 0.1, layer: "top" }, + ], + }, + ] + + const circuitJson = convertToCircuitJson(srjWithPointPairs, traces, { + minTraceWidth: originalSrj.minTraceWidth, + originalSrj, + }) + const connMap = getFullConnectivityMapFromCircuitJson(circuitJson) + + expect(connMap.areIdsConnected("root_a", "root_b")).toBe(true) + expect(connMap.areIdsConnected("root_b", "pcb_trace_0")).toBe(true) + expect(connMap.areIdsConnected("root_b", "pcb_smtpad_0")).toBe(true) + expect(connMap.areIdsConnected("root_a", "pcb_smtpad_1")).toBe(true) + expect(connMap.areIdsConnected("root_b", "endpoint_alias_a")).toBe(true) + expect(connMap.areIdsConnected("root_b", "rotated_endpoint_alias")).toBe(true) + expect(connMap.areIdsConnected("root_a", "bottom_only_alias")).toBe(false) +}) From 8137e3b9cf7acc4892489d72eaa22f595079a071 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sat, 25 Jul 2026 12:46:08 -0700 Subject: [PATCH 06/22] fix Pipeline9 prerouted trace DRC and repair --- .../create-pipeline7-relaxed-drc-evaluator.ts | 13 +- ...-pipeline-solver9-preloaded-trace-graph.ts | 112 +- .../infer-preloaded-trace-connectivity.ts | 21 +- .../lock-pipeline9-terminal-layers.ts | 121 + .../pipeline9-exact-drc-repair-solver.ts | 3524 ++++++++++++++++- .../pipeline9-ijump-rerouter.ts | 737 ++++ .../preloaded-trace-graph-solver.ts | 593 ++- ...tiTargetNecessaryCrampedPortPointSolver.ts | 22 +- .../getRegionNetIdByRegionId.ts | 68 +- .../MultipleHighDensityRouteStitchSolver3.ts | 35 +- .../SingleHighDensityRouteStitchSolver3.ts | 84 +- .../routeStitchingEndpointHelpers.ts | 41 + lib/testing/evaluate-relaxed-drc.ts | 1127 +++++- lib/testing/getDrcErrors.ts | 70 + lib/testing/utils/convertToCircuitJson.ts | 352 +- lib/types/capacity-mesh-types.ts | 8 + lib/utils/convertHdRouteToSimplifiedRoute.ts | 8 +- lib/utils/convertSrjTracesToObstacles.ts | 103 +- .../resolvePreloadedTraceCanonicalNetIds.ts | 215 + package.json | 3 +- tests/drc-via-spacing.test.ts | 57 +- ...laxed-drc-connectivity-regressions.test.ts | 244 ++ ...-drc-preloaded-trace-intersections.test.ts | 1572 ++++++++ ...infer-preloaded-trace-connectivity.test.ts | 59 + ...eline9-error-owned-cluster-rebuild.test.ts | 468 +++ ...ine9-final-continuity-terminal-via.test.ts | 248 ++ ...line9-final-endpoint-slide-cleanup.test.ts | 192 + .../pipeline9-final-error-owner-sweep.test.ts | 347 ++ ...ine9-fixed-copper-composite-repair.test.ts | 201 + .../features/pipeline9-ijump-rerouter.test.ts | 44 + .../pipeline9-lock-terminal-layers.test.ts | 55 + ...eline9-post-final-composite-repair.test.ts | 283 ++ ...ne9-post-repair-same-net-via-merge.test.ts | 156 + .../pipeline9-preloaded-trace-graph.test.ts | 2 +- ...9-shared-terminal-composite-repair.test.ts | 232 ++ .../pipeline9-srj23-relaxed-drc.test.ts | 4 +- ...e9-srj23-sample100-preloaded-graph.test.ts | 38 + ...ne9-srj23-sample32-preloaded-graph.test.ts | 40 + .../pipeline9-terminal-via-escape.test.ts | 172 + .../pipeline9-via-micro-shift.test.ts | 141 + ...reloaded-fixed-copper-cramped-port.test.ts | 102 + ...ed-trace-canonical-net-regressions.test.ts | 235 ++ .../preloaded-trace-graph-solver.test.ts | 530 ++- ...t-region-net-id-inactive-fixed-net.test.ts | 152 + tests/pipeline7-relaxed-drc-evaluator.test.ts | 78 +- ...nsity-route-distinct-terminal-snap.test.ts | 227 ++ .../convertHdRouteToSimplifiedRoute.test.ts | 29 + .../utils/convertSrjTracesToObstacles.test.ts | 77 +- 48 files changed, 13021 insertions(+), 221 deletions(-) create mode 100644 lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/lock-pipeline9-terminal-layers.ts create mode 100644 lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter.ts create mode 100644 lib/utils/resolvePreloadedTraceCanonicalNetIds.ts create mode 100644 tests/evaluate-relaxed-drc-connectivity-regressions.test.ts create mode 100644 tests/evaluate-relaxed-drc-preloaded-trace-intersections.test.ts create mode 100644 tests/features/pipeline9-error-owned-cluster-rebuild.test.ts create mode 100644 tests/features/pipeline9-final-continuity-terminal-via.test.ts create mode 100644 tests/features/pipeline9-final-endpoint-slide-cleanup.test.ts create mode 100644 tests/features/pipeline9-final-error-owner-sweep.test.ts create mode 100644 tests/features/pipeline9-fixed-copper-composite-repair.test.ts create mode 100644 tests/features/pipeline9-ijump-rerouter.test.ts create mode 100644 tests/features/pipeline9-lock-terminal-layers.test.ts create mode 100644 tests/features/pipeline9-post-final-composite-repair.test.ts create mode 100644 tests/features/pipeline9-post-repair-same-net-via-merge.test.ts create mode 100644 tests/features/pipeline9-shared-terminal-composite-repair.test.ts create mode 100644 tests/features/pipeline9-srj23-sample100-preloaded-graph.test.ts create mode 100644 tests/features/pipeline9-srj23-sample32-preloaded-graph.test.ts create mode 100644 tests/features/pipeline9-terminal-via-escape.test.ts create mode 100644 tests/features/pipeline9-via-micro-shift.test.ts create mode 100644 tests/features/preloaded-fixed-copper-cramped-port.test.ts create mode 100644 tests/features/preloaded-trace-canonical-net-regressions.test.ts create mode 100644 tests/get-region-net-id-inactive-fixed-net.test.ts create mode 100644 tests/stitch-solver/multiple-high-density-route-distinct-terminal-snap.test.ts diff --git a/lib/autorouter-pipelines/AutoroutingPipeline7_MultiGraph/create-pipeline7-relaxed-drc-evaluator.ts b/lib/autorouter-pipelines/AutoroutingPipeline7_MultiGraph/create-pipeline7-relaxed-drc-evaluator.ts index 61754941f..471b14dea 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline7_MultiGraph/create-pipeline7-relaxed-drc-evaluator.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline7_MultiGraph/create-pipeline7-relaxed-drc-evaluator.ts @@ -29,12 +29,15 @@ export const createPipeline7RelaxedDrcEvaluator = ( traces, }) + const centeredErrors = + errorsWithCenters.length === errors.length ? errorsWithCenters : errors + return { - errors: errors as unknown as Record[], - errorsWithCenters: errorsWithCenters as unknown as Record< - string, - unknown - >[], + // high-density-repair03 currently consumes `errors` from its custom + // evaluator snapshot. Put the center-enriched form there as well so + // via-only errors can participate in localized repair. + errors: centeredErrors as unknown as Record[], + errorsWithCenters: centeredErrors as unknown as Record[], } } } diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts index c9c0aea29..ceaf946df 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts @@ -1,12 +1,15 @@ import type { DrcEvaluator } from "high-density-repair03/lib" import { BaseSolver } from "lib/solvers/BaseSolver" import type { Obstacle, SimpleRouteJson } from "lib/types" +import { addApproximatingRectsToSrj } from "lib/utils/addApproximatingRectsToSrj" +import { getObstaclesFromSrjTraces } from "lib/utils/convertSrjTracesToObstacles" import { AutoroutingPipelineSolver7_MultiGraph, type AutoroutingPipelineSolverOptions, } from "../AutoroutingPipeline7_MultiGraph/AutoroutingPipelineSolver7_MultiGraph" -import { PreloadedTraceGraphSolver } from "./preloaded-trace-graph-solver" import { Pipeline9ExactDrcRepairSolver } from "./pipeline9-exact-drc-repair-solver" +import { lockPipeline9TerminalLayers } from "./lock-pipeline9-terminal-layers" +import { PreloadedTraceGraphSolver } from "./preloaded-trace-graph-solver" import { PreprocessSimpleRouteJsonWithoutTraceObstaclesSolver } from "./preprocess-simple-route-json-without-trace-obstacles-solver" type PipelineStep = { @@ -34,6 +37,24 @@ const getAxisAlignedRepairObstacle = (obstacle: Obstacle): Obstacle => { } } +const getPreloadedTraceGeometryObstacles = ( + srj: SimpleRouteJson, +): Obstacle[] => { + const traceObstacles = getObstaclesFromSrjTraces(srj, { + includeConnectionNameInConnectedTo: true, + includeSquareCaps: true, + modelJumperPads: true, + }) + if (traceObstacles.length === 0) return [] + + return addApproximatingRectsToSrj({ + ...srj, + obstacles: traceObstacles, + connections: [], + traces: undefined, + }).obstacles +} + const createPadCenteredDrcEvaluator = ( evaluator: DrcEvaluator | undefined, originalObstacles: Obstacle[], @@ -96,6 +117,9 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends AutoroutingP ) { super(srj, opts) const pipelineDef = this.pipelineDef as PipelineStep[] + const preloadedTraceGeometryObstacles = getPreloadedTraceGeometryObstacles( + this.originalSrj, + ) const preprocessStep = pipelineDef.find( (step) => step.solverName === "preprocessSimpleRouteJsonSolver", ) @@ -126,6 +150,83 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends AutoroutingP }, }) + for (const solverName of [ + "uniformPortDistributionSolver", + "highDensityRouteSolver", + "highDensityRepairSolver", + "traceSimplificationSolver", + "lengthMatchingSolver", + "traceWidthSolver", + ]) { + const step = pipelineDef.find( + (candidate) => candidate.solverName === solverName, + ) + if (!step) { + throw new Error(`Pipeline9 could not find the ${solverName} stage`) + } + const getBaseParams = step.getConstructorParams + step.getConstructorParams = (pipeline) => { + const [params, ...remainingParams] = getBaseParams(pipeline) + return [ + { + ...params, + obstacles: [ + ...(params.obstacles ?? []), + ...preloadedTraceGeometryObstacles, + ], + }, + ...remainingParams, + ] + } + } + + const stitchStep = pipelineDef.find( + (step) => step.solverName === "highDensityStitchSolver", + ) + if (!stitchStep) { + throw new Error("Pipeline9 could not find the route stitching stage") + } + const getBaseStitchParams = stitchStep.getConstructorParams + stitchStep.getConstructorParams = (pipeline) => { + const [params, ...remainingParams] = getBaseStitchParams(pipeline) + return [ + { + ...params, + preserveDistinctTerminalSnaps: true, + preserveTerminalLayers: true, + }, + ...remainingParams, + ] + } + + const globalDrcStep = pipelineDef.find( + (step) => step.solverName === "globalDrcForceImproveSolver", + ) + if (!globalDrcStep) { + throw new Error("Pipeline9 could not find the global DRC repair stage") + } + const getBaseGlobalDrcParams = globalDrcStep.getConstructorParams + globalDrcStep.getConstructorParams = (pipeline) => { + const [params] = getBaseGlobalDrcParams(pipeline) + return [ + { + ...params, + hdRoutes: lockPipeline9TerminalLayers( + params.hdRoutes, + pipeline.srjWithPointPairs!.connections, + pipeline.srj.layerCount, + ), + srj: { + ...params.srj, + obstacles: [ + ...params.srj.obstacles, + ...preloadedTraceGeometryObstacles, + ], + }, + }, + ] + } + const exactDrcStep = pipelineDef.find( (step) => step.solverName === "exactGeometryDrcForceImproveSolver", ) @@ -151,9 +252,16 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends AutoroutingP drcEvaluator, viaInPadDrcEvaluator, originalObstacles, + ijumpBaseObstacles: [ + ...originalObstacles, + ...preloadedTraceGeometryObstacles, + ], srj: { ...params.srj, - obstacles: originalObstacles.map(getAxisAlignedRepairObstacle), + obstacles: [ + ...originalObstacles.map(getAxisAlignedRepairObstacle), + ...preloadedTraceGeometryObstacles, + ], }, }, ] diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/infer-preloaded-trace-connectivity.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/infer-preloaded-trace-connectivity.ts index d8b0d49a5..1de214ca2 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/infer-preloaded-trace-connectivity.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/infer-preloaded-trace-connectivity.ts @@ -87,6 +87,13 @@ export const inferPreloadedTraceConnectivity = ( getTraceEndpoint(trace, 0), getTraceEndpoint(trace, 1), ].filter((endpoint): endpoint is TraceEndpoint => endpoint !== null) + const connectionPointIds = new Set( + connection?.pointsToConnect.flatMap((point) => + [point.pointId, point.pcb_port_id].filter( + (pointId): pointId is string => pointId !== undefined, + ), + ) ?? [], + ) const inferredPointIds = connection?.pointsToConnect .filter((point) => @@ -99,14 +106,18 @@ export const inferPreloadedTraceConnectivity = ( ), ), ) - .map((point) => point.pointId) - .filter((pointId): pointId is string => pointId !== undefined) ?? [] + .flatMap((point) => + [point.pointId, point.pcb_port_id].filter( + (pointId): pointId is string => pointId !== undefined, + ), + ) ?? [] + const preservedNonPointIds = (trace.connectsTo ?? []).filter( + (connectsTo) => !connectionPointIds.has(connectsTo), + ) return { ...trace, - connectsTo: [ - ...new Set([...(trace.connectsTo ?? []), ...inferredPointIds]), - ], + connectsTo: [...new Set([...preservedNonPointIds, ...inferredPointIds])], } }) diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/lock-pipeline9-terminal-layers.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/lock-pipeline9-terminal-layers.ts new file mode 100644 index 000000000..05abf9834 --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/lock-pipeline9-terminal-layers.ts @@ -0,0 +1,121 @@ +import type { SimpleRouteConnection } from "lib/types" +import type { HighDensityRoute } from "lib/types/high-density-types" +import { getConnectionPointLayer } from "lib/types/srj-types" +import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" + +const POSITION_EPSILON = 1e-6 + +const pointsCoincide = ( + left: { x: number; y: number }, + right: { x: number; y: number }, +): boolean => Math.hypot(left.x - right.x, left.y - right.y) <= POSITION_EPSILON + +/** + * Reasserts the authoritative PCB terminal layer after route simplification. + * A wrong-layer route island is joined to the terminal with a coincident via + * transition instead of silently changing the layer of a nonzero-length + * segment. + */ +export const lockPipeline9TerminalLayers = ( + hdRoutes: ReadonlyArray, + connections: ReadonlyArray, + layerCount: number, +): HighDensityRoute[] => { + const connectionByName = new Map( + connections.map((connection) => [connection.name, connection]), + ) + + return hdRoutes.map((hdRoute) => { + const connection = connectionByName.get(hdRoute.connectionName) + if (!connection || hdRoute.route.length === 0) return hdRoute + + const terminalByPcbPortId = new Map( + connection.pointsToConnect.flatMap((terminal) => + terminal.pcb_port_id + ? [ + [ + terminal.pcb_port_id, + { + ...terminal, + z: mapLayerNameToZ( + getConnectionPointLayer(terminal), + layerCount, + ), + }, + ] as const, + ] + : [], + ), + ) + const route = hdRoute.route.map((point) => ({ ...point })) + const vias = hdRoute.vias.map((via) => ({ ...via })) + const addVia = (point: { x: number; y: number }) => { + if (!vias.some((via) => pointsCoincide(via, point))) { + vias.push({ x: point.x, y: point.y }) + } + } + + const start = route[0] + const startTerminal = start?.pcb_port_id + ? terminalByPcbPortId.get(start.pcb_port_id) + : undefined + if (start && startTerminal && start.z !== startTerminal.z) { + const { + pcb_port_id: startPcbPortId, + toNextSegmentType, + ...startWithoutIdentity + } = start + route.splice( + 0, + 1, + { + ...startWithoutIdentity, + x: startTerminal.x, + y: startTerminal.y, + z: startTerminal.z, + pcb_port_id: startPcbPortId, + }, + { + ...startWithoutIdentity, + x: startTerminal.x, + y: startTerminal.y, + z: start.z, + ...(toNextSegmentType ? { toNextSegmentType } : {}), + }, + ) + addVia(startTerminal) + } + + const end = route.at(-1) + const endTerminal = end?.pcb_port_id + ? terminalByPcbPortId.get(end.pcb_port_id) + : undefined + if (end && endTerminal && end.z !== endTerminal.z) { + const { + pcb_port_id: endPcbPortId, + toNextSegmentType: _toNextSegmentType, + ...endWithoutIdentity + } = end + route.splice( + route.length - 1, + 1, + { + ...endWithoutIdentity, + x: endTerminal.x, + y: endTerminal.y, + z: end.z, + }, + { + ...endWithoutIdentity, + x: endTerminal.x, + y: endTerminal.y, + z: endTerminal.z, + pcb_port_id: endPcbPortId, + }, + ) + addVia(endTerminal) + } + + return { ...hdRoute, route, vias } + }) +} diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts index 5cfd1edc6..9b43fd3ee 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts @@ -8,16 +8,24 @@ import { getDrcSnapshot, materializeRoutes, } from "high-density-repair03/lib/solvers/GlobalDrcForceImproveSolver/solverHelpers" +import { SameNetViaMergerSolver } from "lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver" import type { Obstacle } from "lib/types" import type { HighDensityRoute } from "lib/types/high-density-types" +import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" import { mapZToLayerName } from "lib/utils/mapZToLayerName" +import { + type Pipeline9IjumpRerouteOptions, + Pipeline9IjumpRerouter, +} from "./pipeline9-ijump-rerouter" type Pipeline9ExactDrcRepairSolverParams = GlobalDrcBranchPortfolioSolverParams & { originalObstacles: Obstacle[] + ijumpBaseObstacles: Obstacle[] } type DrcError = Record +type DrcSnapshot = ReturnType type TerminalConstraint = { routeIndex: number @@ -27,14 +35,156 @@ type TerminalConstraint = { owningObstacles: Obstacle[] } +type ViaTransitionGroup = { + routeIndex: number + indexes: number[] + x: number + y: number + distanceToError: number +} + +type ErrorOwnedClusterPlan = { + routeIndexes: number[] + reverse: boolean + allowTerminalEscape: boolean +} + +type PostFinalCompositeWindow = { + startIndex: number + endIndex: number + terminalRooted: boolean +} + +type FixedCopperCompositePlan = { + routeIndex: number + targetErrorIdentity: string +} + +type ScopedSameNetViaMergeResult = { + routes?: HighDensityRoute[] + iterations: number + mergedViaCount: number +} + +type SharedTerminalCompositeGroup = { + fixedTraceId: string + canonicalNet: string + terminalPortId: string + branches: Array<{ + routeIndex: number + endpoint: "start" | "end" + }> + baselineErrorIds: Set +} + +type EndpointSlideBranch = { + routeIndex: number + endpoint: "start" | "end" + endpointIndex: number + coincidentIndexes: number[] + constraint: TerminalConstraint +} + +type FinalContinuityTerminalViaCandidate = { + routeIndex: number + endpoint: "start" | "end" + targetZ: number + distanceToError: number +} + const POSITION_EPSILON = 1e-6 +const PRELOADED_TERMINAL_MATCH_TOLERANCE = 1e-3 const ENDPOINT_SLIDE_RADII = [ 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.2, 0.25, 0.3, ] as const +const VIA_MICRO_SHIFT_RADII = ENDPOINT_SLIDE_RADII +const MAX_VIA_GROUPS_PER_ROUTE = 3 +const MAX_VIA_MICRO_SHIFT_DRC_EVALUATIONS_PER_SWEEP = 32 +const MAX_VIA_MICRO_SHIFT_SNAPSHOT_ISSUES = 8 const BATCHED_TRACE_FORCE_SCALES = [4, 2, 1, 0.5, 0.25] as const const MAX_BATCHED_TRACE_FORCE_PASSES = 20 const MAX_CLEANUP_PASSES = 8 +const DEFAULT_MAX_LOCAL_CLEANUP_DRC_EVALUATIONS = 500 +const HIGH_INITIAL_DRC_MAX_LOCAL_CLEANUP_DRC_EVALUATIONS = 300 +const HIGH_INITIAL_DRC_THRESHOLD = 20 +const DEFAULT_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES = + DEFAULT_MAX_LOCAL_CLEANUP_DRC_EVALUATIONS +const HIGH_INITIAL_DRC_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES = 64 const MAX_LOCAL_LAYER_DETOUR_EXPANSION = 6 +const MAX_IJUMP_FULL_ATTEMPTS_PER_ROUND = 18 +const MAX_IJUMP_INTERIOR_ATTEMPTS_PER_ROUND = 24 +const MAX_IJUMP_FIXED_ONLY_ATTEMPTS_PER_ROUND = 8 +const DEFAULT_MAX_IJUMP_TOTAL_ITERATIONS = 300_000 +const HIGH_INITIAL_DRC_MAX_IJUMP_TOTAL_ITERATIONS = 200_000 +const MAX_IJUMP_FULL_ITERATIONS = 30_000 +const MAX_IJUMP_INTERIOR_ITERATIONS = 10_000 +const MAX_IJUMP_PHASE_ROUNDS = 2 +const MAX_IJUMP_INTERIOR_EXPANSION = 6 +const MAX_ERROR_OWNED_CLUSTER_ITERATIONS = 75_000 +const MAX_ERROR_OWNED_CLUSTER_ROUTE_ITERATIONS = 15_000 +const MAX_ERROR_OWNED_CLUSTER_PASSES = 2 +const MAX_ERROR_OWNED_CLUSTER_TERMINAL_ESCAPE_CANDIDATES = 4 +const MAX_ERROR_OWNED_CLUSTER_TERMINAL_ESCAPE_ITERATIONS = 10_000 +const MAX_POST_CLUSTER_VIA_MICRO_SHIFT_DRC_EVALUATIONS = 32 +const MAX_FINAL_OWNER_IJUMP_ITERATIONS = 50_000 +const MAX_FINAL_OWNER_FULL_ROUTE_ITERATIONS = 25_000 +const MAX_FINAL_OWNER_LONG_ROUTE_ITERATIONS = 4_000 +const MAX_FINAL_OWNER_VARIANT_ITERATIONS = 10_000 +const MAX_FINAL_OWNER_INTERIOR_ITERATIONS = 10_000 +const MAX_FINAL_OWNER_FULL_VARIANT_ROUTE_POINTS = 24 +const FINAL_OWNER_INTERIOR_ITERATION_RESERVE = 8_000 +const MAX_FINAL_OWNER_FALLBACK_RESIDUAL = 2 +const MAX_POST_REPAIR_SAME_NET_VIA_MERGER_ITERATIONS = 8 +const MAX_SHARED_TERMINAL_COMPOSITE_RESIDUAL = 4 +const MAX_SHARED_TERMINAL_COMPOSITE_ATTEMPTS = 1 +const MAX_SHARED_TERMINAL_COMPOSITE_IJUMP_ITERATIONS = 12_500 +const MAX_SHARED_TERMINAL_COMPOSITE_DRC_EVALUATIONS = 2 +const MAX_POST_FINAL_COMPOSITE_IJUMP_ITERATIONS = 24_000 +const MAX_POST_FINAL_COMPOSITE_IJUMP_ITERATIONS_PER_ATTEMPT = 8_000 +const MAX_POST_FINAL_COMPOSITE_ATTEMPTS = 12 +const MAX_POST_FINAL_COMPOSITE_DRC_EVALUATIONS = 12 +const MAX_POST_FINAL_COMPOSITE_SAME_NET_VIA_MERGER_ITERATIONS = 32 +const MAX_POST_FINAL_COMPOSITE_SAME_NET_VIA_MERGER_ITERATIONS_PER_ATTEMPT = 8 +const MAX_FIXED_COPPER_COMPOSITE_RESIDUAL = 2 +const MAX_FIXED_COPPER_COMPOSITE_PRIMARY_ATTEMPTS = 4 +const MAX_FIXED_COPPER_COMPOSITE_FOLLOWUP_ATTEMPTS = 6 +const MAX_FIXED_COPPER_COMPOSITE_DRC_EVALUATIONS = 8 +const MAX_FIXED_COPPER_COMPOSITE_ITERATIONS = 24_000 +const MAX_FIXED_COPPER_COMPOSITE_ITERATIONS_PER_ATTEMPT = 8_000 +const MAX_FIXED_COPPER_COMPOSITE_EXPOSED_ISSUES = 4 +const MAX_FIXED_COPPER_COMPOSITE_FOLLOWUP_OWNERS = 2 +const MAX_FINAL_ENDPOINT_SLIDE_DRC_EVALUATIONS = 32 +const MAX_FINAL_CONTINUITY_TERMINAL_VIA_ATTEMPTS = 32 +const MAX_FINAL_CONTINUITY_TERMINAL_VIA_DRC_EVALUATIONS = 8 +const POST_FINAL_COMPOSITE_INTERIOR_EXPANSIONS = [4, 8] as const +const POST_FINAL_COMPOSITE_TERMINAL_PROXIMITY = 4 +const FULL_IJUMP_VARIANTS = [ + { reverse: false, shortenPath: true }, + { reverse: false, shortenPath: false }, + { reverse: true, shortenPath: false }, +] as const + +const INTERIOR_IJUMP_VARIANTS = [ + { reverse: false, shortenPath: false }, + { reverse: true, shortenPath: false }, +] as const + +const FIXED_ONLY_IJUMP_VARIANTS = [ + { reverse: false, shortenPath: false }, + { reverse: true, shortenPath: false }, +] as const + +const FINAL_OWNER_FULL_VARIANTS = [ + { reverse: false, shortenPath: false }, + { reverse: false, shortenPath: true }, + { reverse: true, shortenPath: false }, +] as const + +const FIXED_COPPER_COMPOSITE_PRIMARY_VARIANTS = [ + { reverse: false, shortenPath: true }, + { reverse: false, shortenPath: false }, + { reverse: true, shortenPath: false }, +] as const const getErrorType = (error: DrcError): string | undefined => typeof error.error_type === "string" @@ -123,6 +273,75 @@ const getOtherTraceId = ( return traceRouteIndexById.has(otherTraceId) ? otherTraceId : undefined } +const getRawOtherTraceId = (error: DrcError): string | undefined => { + const traceId = error.pcb_trace_id + const errorId = error.pcb_trace_error_id + if (typeof traceId !== "string" || typeof errorId !== "string") { + return undefined + } + const prefix = `overlap_${traceId}_` + return errorId.startsWith(prefix) ? errorId.slice(prefix.length) : undefined +} + +const getCandidateTraceIdsFromError = (error: DrcError): string[] => + Array.isArray(error.candidate_pcb_trace_ids) + ? error.candidate_pcb_trace_ids.filter( + (traceId): traceId is string => typeof traceId === "string", + ) + : [] + +const getUnitDirection = ( + deltaX: number, + deltaY: number, +): { x: number; y: number } | undefined => { + const length = Math.hypot(deltaX, deltaY) + if (length <= POSITION_EPSILON) return undefined + return { x: deltaX / length, y: deltaY / length } +} + +const getMicroShiftDirections = (preferredDirection?: { + x: number + y: number +}): Array<{ x: number; y: number }> => { + const directions: Array<{ x: number; y: number }> = [] + const seenDirections = new Set() + const addDirection = (direction: { x: number; y: number } | undefined) => { + if (!direction) return + const unitDirection = getUnitDirection(direction.x, direction.y) + if (!unitDirection) return + const key = `${unitDirection.x.toFixed(6)}:${unitDirection.y.toFixed(6)}` + if (seenDirections.has(key)) return + seenDirections.add(key) + directions.push(unitDirection) + } + + addDirection(preferredDirection) + for (const direction of [ + { x: 1, y: 0 }, + { x: -1, y: 0 }, + { x: 0, y: 1 }, + { x: 0, y: -1 }, + { x: Math.SQRT1_2, y: Math.SQRT1_2 }, + { x: Math.SQRT1_2, y: -Math.SQRT1_2 }, + { x: -Math.SQRT1_2, y: Math.SQRT1_2 }, + { x: -Math.SQRT1_2, y: -Math.SQRT1_2 }, + ]) { + addDirection(direction) + } + + return directions +} + +const routeHasValidLayerTransitions = (route: HighDensityRoute): boolean => + route.route.every((point, pointIndex) => { + const nextPoint = route.route[pointIndex + 1] + return ( + !nextPoint || + point.z === nextPoint.z || + getPointDistance(point, nextPoint) <= POSITION_EPSILON + ) + }) + const getPointToSegmentDistance = ( point: { x: number; y: number }, start: { x: number; y: number }, @@ -173,13 +392,79 @@ const getCoincidentTerminalPointIndexes = ( export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolver { private readonly originalObstacles: Obstacle[] private readonly terminalConstraints: TerminalConstraint[] + private readonly ijumpRerouter: Pipeline9IjumpRerouter private cleanupStarted = false private cleanupCandidateAttempts = 0 private cleanupCandidatesAccepted = 0 + private localCleanupDrcEvaluations = 0 + private selectedLocalCleanupDrcEvaluationLimit = + DEFAULT_MAX_LOCAL_CLEANUP_DRC_EVALUATIONS + private consecutiveLocalCleanupDrcMisses = 0 + private maxConsecutiveLocalCleanupDrcMisses = 0 + private selectedConsecutiveLocalCleanupDrcMissLimit = + DEFAULT_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES + private selectedIjumpIterationLimit = DEFAULT_MAX_IJUMP_TOTAL_ITERATIONS + private viaMicroShiftAttempts = 0 + private viaMicroShiftsAccepted = 0 + private ijumpFullAttempts = 0 + private ijumpInteriorAttempts = 0 + private ijumpFixedOnlyAttempts = 0 + private ijumpCandidatesAccepted = 0 + private ijumpIterations = 0 + private errorOwnedClusterOrderAttempts = 0 + private errorOwnedClusterRouteAttempts = 0 + private errorOwnedClusterDrcEvaluations = 0 + private errorOwnedClusterIterations = 0 + private errorOwnedClusterAccepted = 0 + private errorOwnedClusterTerminalEscapeAttempts = 0 + private errorOwnedClusterPostRouteAttempts = 0 + private errorOwnedClusterPostCandidatesAccepted = 0 + private postClusterViaMicroShiftDrcEvaluations = 0 + private finalOwnerFullAttempts = 0 + private finalOwnerInteriorAttempts = 0 + private finalOwnerDrcEvaluations = 0 + private finalOwnerCandidatesAccepted = 0 + private finalOwnerIterations = 0 + private postRepairSameNetViaMergeAttempts = 0 + private postRepairSameNetViaMergeDrcEvaluations = 0 + private postRepairSameNetViaMergeCandidatesAccepted = 0 + private postRepairSameNetViaMergeIterations = 0 + private sharedTerminalCompositeAttempts = 0 + private sharedTerminalCompositeRelocatedBranches = 0 + private sharedTerminalCompositeIjumpAttempts = 0 + private sharedTerminalCompositeDrcEvaluations = 0 + private sharedTerminalCompositeCandidatesAccepted = 0 + private sharedTerminalCompositeIterations = 0 + private postFinalCompositeAttempts = 0 + private postFinalCompositeForwardAttempts = 0 + private postFinalCompositeReverseAttempts = 0 + private postFinalCompositeTerminalRootedAttempts = 0 + private postFinalCompositeDrcEvaluations = 0 + private postFinalCompositeCandidatesAccepted = 0 + private postFinalCompositeIterations = 0 + private postFinalCompositeSameNetViaMergeIterations = 0 + private fixedCopperCompositePrimaryAttempts = 0 + private fixedCopperCompositeFollowupAttempts = 0 + private fixedCopperCompositeDrcEvaluations = 0 + private fixedCopperCompositeCandidatesAccepted = 0 + private fixedCopperCompositeIterations = 0 + private finalEndpointSlideAttempts = 0 + private finalEndpointSlideDrcEvaluations = 0 + private finalEndpointSlideCandidatesAccepted = 0 + private finalEndpointSlideRelocatedBranches = 0 + private finalContinuityTerminalViaAttempts = 0 + private finalContinuityTerminalViaDrcEvaluations = 0 + private finalContinuityTerminalViaCandidatesAccepted = 0 constructor(params: Pipeline9ExactDrcRepairSolverParams) { super(params) this.originalObstacles = params.originalObstacles + this.ijumpRerouter = new Pipeline9IjumpRerouter({ + srj: params.srj, + baseObstacles: params.ijumpBaseObstacles, + connMap: params.connMap, + viaHoleDiameter: params.viaHoleDiameter, + }) this.terminalConstraints = params.hdRoutes.flatMap((route, routeIndex) => (["start", "end"] as const).flatMap((endpoint) => { const point = endpoint === "start" ? route.route[0] : route.route.at(-1) @@ -285,20 +570,287 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private candidateImprovesSnapshot( candidateRoutes: HighDensityRoute[], currentIssueCount: number, + source: "local" | "ijump" = "local", ): boolean { this.cleanupCandidateAttempts += 1 if (!this.candidatePreservesTerminals(candidateRoutes)) return false + if ( + source === "local" && + this.localCleanupDrcEvaluations >= + this.selectedLocalCleanupDrcEvaluationLimit + ) { + return false + } + if (source === "local") this.localCleanupDrcEvaluations += 1 const candidateSnapshot = this.getSnapshot(candidateRoutes) - if (candidateSnapshot.count >= currentIssueCount) return false + if (candidateSnapshot.count >= currentIssueCount) { + if (source === "local") { + this.consecutiveLocalCleanupDrcMisses += 1 + this.maxConsecutiveLocalCleanupDrcMisses = Math.max( + this.maxConsecutiveLocalCleanupDrcMisses, + this.consecutiveLocalCleanupDrcMisses, + ) + } + return false + } + if (source === "local") this.consecutiveLocalCleanupDrcMisses = 0 this.cleanupCandidatesAccepted += 1 return true } + private hasLocalCleanupBudget(): boolean { + return ( + this.localCleanupDrcEvaluations < + this.selectedLocalCleanupDrcEvaluationLimit && + this.consecutiveLocalCleanupDrcMisses < + this.selectedConsecutiveLocalCleanupDrcMissLimit + ) + } + + private selectAdaptiveCleanupLimits(): void { + const initialDrcIssueCount = this.stats.initialDrcIssueCount + const useHighInitialDrcLimits = + typeof initialDrcIssueCount === "number" && + initialDrcIssueCount >= HIGH_INITIAL_DRC_THRESHOLD + this.selectedLocalCleanupDrcEvaluationLimit = useHighInitialDrcLimits + ? HIGH_INITIAL_DRC_MAX_LOCAL_CLEANUP_DRC_EVALUATIONS + : DEFAULT_MAX_LOCAL_CLEANUP_DRC_EVALUATIONS + this.selectedConsecutiveLocalCleanupDrcMissLimit = useHighInitialDrcLimits + ? HIGH_INITIAL_DRC_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES + : DEFAULT_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES + this.selectedIjumpIterationLimit = useHighInitialDrcLimits + ? HIGH_INITIAL_DRC_MAX_IJUMP_TOTAL_ITERATIONS + : DEFAULT_MAX_IJUMP_TOTAL_ITERATIONS + } + + private getCandidateRouteIndexesForError( + error: DrcError, + snapshot: DrcSnapshot, + ): number[] { + const primaryTraceId = + typeof error.pcb_trace_id === "string" ? error.pcb_trace_id : undefined + const otherTraceId = getOtherTraceId(error, snapshot.traceRouteIndexById) + const routeIndexes: number[] = [] + const seenRouteIndexes = new Set() + + for (const traceId of [ + ...getCandidateTraceIdsFromError(error), + otherTraceId, + primaryTraceId, + ]) { + if (!traceId) continue + const routeIndex = snapshot.traceRouteIndexById.get(traceId) + if (routeIndex === undefined || seenRouteIndexes.has(routeIndex)) { + continue + } + seenRouteIndexes.add(routeIndex) + routeIndexes.push(routeIndex) + } + + return routeIndexes + } + + private getViaTransitionGroups( + route: HighDensityRoute, + routeIndex: number, + errorCenter: { x: number; y: number }, + ): ViaTransitionGroup[] { + const groups: ViaTransitionGroup[] = [] + const seenGroups = new Set() + + for ( + let pointIndex = 0; + pointIndex < route.route.length - 1; + pointIndex += 1 + ) { + const point = route.route[pointIndex] + const nextPoint = route.route[pointIndex + 1] + if ( + !point || + !nextPoint || + point.z === nextPoint.z || + getPointDistance(point, nextPoint) > POSITION_EPSILON + ) { + continue + } + + let startIndex = pointIndex + let endIndex = pointIndex + 1 + while ( + startIndex > 0 && + getPointDistance(route.route[startIndex - 1]!, point) <= + POSITION_EPSILON + ) { + startIndex -= 1 + } + while ( + endIndex < route.route.length - 1 && + getPointDistance(route.route[endIndex + 1]!, point) <= POSITION_EPSILON + ) { + endIndex += 1 + } + + const groupKey = `${startIndex}:${endIndex}` + if (seenGroups.has(groupKey)) continue + seenGroups.add(groupKey) + groups.push({ + routeIndex, + indexes: Array.from( + { length: endIndex - startIndex + 1 }, + (_, indexOffset) => startIndex + indexOffset, + ), + x: point.x, + y: point.y, + distanceToError: getPointDistance(point, errorCenter), + }) + } + + return groups + .toSorted((left, right) => left.distanceToError - right.distanceToError) + .slice(0, MAX_VIA_GROUPS_PER_ROUTE) + } + + private tryViaMicroShift( + routes: HighDensityRoute[], + error: DrcError, + sweepBudget = { + remaining: MAX_VIA_MICRO_SHIFT_DRC_EVALUATIONS_PER_SWEEP, + }, + ): HighDensityRoute[] | undefined { + if (!this.hasLocalCleanupBudget() || sweepBudget.remaining <= 0) { + return undefined + } + const errorType = getErrorType(error) + if ( + errorType !== "pcb_via_clearance_error" && + errorType !== "pcb_via_trace_clearance_error" + ) { + return undefined + } + const errorCenter = this.getErrorCenter(error) + if (!errorCenter) return undefined + + const snapshot = this.getSnapshot(routes) + const viaGroups = this.getCandidateRouteIndexesForError(error, snapshot) + .flatMap((routeIndex) => { + const route = routes[routeIndex] + if (!route) return [] + return this.getViaTransitionGroups(route, routeIndex, errorCenter) + }) + .toSorted((left, right) => left.distanceToError - right.distanceToError) + if (viaGroups.length === 0) return undefined + + for (const viaGroup of viaGroups) { + const route = routes[viaGroup.routeIndex] + if (!route) continue + const nearestOtherVia = viaGroups + .filter( + (candidate) => + candidate !== viaGroup && + getPointDistance(candidate, viaGroup) > POSITION_EPSILON, + ) + .toSorted( + (left, right) => + getPointDistance(left, viaGroup) - + getPointDistance(right, viaGroup), + )[0] + const preferredDirection = nearestOtherVia + ? getUnitDirection( + viaGroup.x - nearestOtherVia.x, + viaGroup.y - nearestOtherVia.y, + ) + : getUnitDirection( + viaGroup.x - errorCenter.x, + viaGroup.y - errorCenter.y, + ) + + for (const direction of getMicroShiftDirections(preferredDirection)) { + for (const radius of VIA_MICRO_SHIFT_RADII) { + if (!this.hasLocalCleanupBudget() || sweepBudget.remaining <= 0) { + return undefined + } + const candidateX = viaGroup.x + direction.x * radius + const candidateY = viaGroup.y + direction.y * radius + const viaInset = route.viaDiameter / 2 + if ( + candidateX < + this.params.srj.bounds.minX + viaInset - POSITION_EPSILON || + candidateX > + this.params.srj.bounds.maxX - viaInset + POSITION_EPSILON || + candidateY < + this.params.srj.bounds.minY + viaInset - POSITION_EPSILON || + candidateY > + this.params.srj.bounds.maxY - viaInset + POSITION_EPSILON + ) { + continue + } + + const candidateRoutes = cloneRoutes(routes) + const candidateRoute = candidateRoutes[viaGroup.routeIndex] + if (!candidateRoute) continue + for (const pointIndex of viaGroup.indexes) { + const point = candidateRoute.route[pointIndex] + if (!point) continue + point.x = candidateX + point.y = candidateY + } + if (!routeHasValidLayerTransitions(candidateRoute)) continue + + this.viaMicroShiftAttempts += 1 + sweepBudget.remaining -= 1 + const materializedCandidate = materializeRoutes(candidateRoutes) + if ( + this.candidateImprovesSnapshot( + materializedCandidate, + snapshot.count, + ) + ) { + this.viaMicroShiftsAccepted += 1 + return materializedCandidate + } + } + } + } + + return undefined + } + + private runViaMicroShiftCleanup( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + const sweepBudget = { + remaining: MAX_VIA_MICRO_SHIFT_DRC_EVALUATIONS_PER_SWEEP, + } + + while (this.hasLocalCleanupBudget() && sweepBudget.remaining > 0) { + const snapshot = this.getSnapshot(improvedRoutes) + if (snapshot.count === 0) break + if (snapshot.count > MAX_VIA_MICRO_SHIFT_SNAPSHOT_ISSUES) break + let nextRoutes: HighDensityRoute[] | undefined + for (const error of snapshot.errors) { + nextRoutes = this.tryViaMicroShift(improvedRoutes, error, sweepBudget) + if ( + nextRoutes || + !this.hasLocalCleanupBudget() || + sweepBudget.remaining <= 0 + ) { + break + } + } + if (!nextRoutes) break + improvedRoutes = nextRoutes + } + + return improvedRoutes + } + private tryEndpointSlide( routes: HighDensityRoute[], error: DrcError, ): HighDensityRoute[] | undefined { + if (!this.hasLocalCleanupBudget()) return undefined const errorType = getErrorType(error) const padId = getPhysicalPadIdFromError(error) if ( @@ -375,6 +927,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve for (const radius of ENDPOINT_SLIDE_RADII) { for (const direction of directions) { + if (!this.hasLocalCleanupBudget()) return undefined const candidatePoint = { x: endpoint.x + direction.x * radius, y: endpoint.y + direction.y * radius, @@ -416,6 +969,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve routes: HighDensityRoute[], error: DrcError, ): HighDensityRoute[] | undefined { + if (!this.hasLocalCleanupBudget()) return undefined const errorType = getErrorType(error) if ( errorType !== "pcb_trace_error" && @@ -483,11 +1037,21 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve sameLayerEnd += 1 } - for ( - let expansion = 0; - expansion <= MAX_LOCAL_LAYER_DETOUR_EXPANSION; - expansion += 1 - ) { + const fullRunExpansion = Math.max( + nearestSegmentIndex - sameLayerStart, + sameLayerEnd - nearestSegmentIndex - 1, + ) + const expansionCandidates = [ + ...Array.from( + { length: MAX_LOCAL_LAYER_DETOUR_EXPANSION + 1 }, + (_, expansion) => expansion, + ), + ...(fullRunExpansion > MAX_LOCAL_LAYER_DETOUR_EXPANSION + ? [fullRunExpansion] + : []), + ] + + for (const expansion of expansionCandidates) { const detourStart = Math.max( sameLayerStart, nearestSegmentIndex - expansion, @@ -503,6 +1067,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve targetZ < this.params.srj.layerCount; targetZ += 1 ) { + if (!this.hasLocalCleanupBudget()) return undefined if (targetZ === z) continue const candidateRoutes = cloneRoutes(routes) const candidateRoute = candidateRoutes[routeIndex] @@ -543,6 +1108,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve routes: HighDensityRoute[], error: DrcError, ): HighDensityRoute[] | undefined { + if (!this.hasLocalCleanupBudget()) return undefined const errorType = getErrorType(error) if ( errorType !== "pcb_trace_error" && @@ -563,6 +1129,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve for (const scale of BATCHED_TRACE_FORCE_SCALES) { let candidateRoutes = cloneRoutes(routes) for (let pass = 0; pass < MAX_BATCHED_TRACE_FORCE_PASSES; pass += 1) { + if (!this.hasLocalCleanupBudget()) return undefined const changed = applyDrcErrorForces( this.params.srj, candidateRoutes, @@ -590,34 +1157,2825 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve return undefined } - private runPipeline9Cleanup(routes: HighDensityRoute[]): HighDensityRoute[] { - let improvedRoutes = this.unlockCleanupTerminals(routes) + private getRemainingIjumpIterations(): number { + return Math.max(0, this.selectedIjumpIterationLimit - this.ijumpIterations) + } - for (let pass = 0; pass < MAX_CLEANUP_PASSES; pass += 1) { - const snapshot = this.getSnapshot(improvedRoutes) - if (snapshot.count === 0) break + private tryIjumpCandidate( + routes: HighDensityRoute[], + snapshot: DrcSnapshot, + options: Omit, + maxIterations: number, + ): HighDensityRoute[] | undefined { + const iterationLimit = Math.min( + maxIterations, + this.getRemainingIjumpIterations(), + ) + if (iterationLimit <= 0) return undefined - let nextRoutes: HighDensityRoute[] | undefined - for (const error of snapshot.errors) { - nextRoutes = this.tryEndpointSlide(improvedRoutes, error) - if (nextRoutes) break - } - if (!nextRoutes) { - for (const error of snapshot.errors) { - nextRoutes = this.tryLocalTraceLayerDetour(improvedRoutes, error) - if (nextRoutes) break + const result = this.ijumpRerouter.tryReroute(routes, { + ...options, + maxIterations: iterationLimit, + }) + this.ijumpIterations += Math.max(0, result?.iterations ?? 0) + if (!result?.route) return undefined + + const candidateRoutes = cloneRoutes(routes) + candidateRoutes[options.routeIndex] = result.route + const materializedCandidate = materializeRoutes(candidateRoutes) + if ( + !this.candidateImprovesSnapshot( + materializedCandidate, + snapshot.count, + "ijump", + ) + ) { + return undefined + } + + this.ijumpCandidatesAccepted += 1 + return materializedCandidate + } + + private getOrderedIjumpRouteIndexes(snapshot: DrcSnapshot): number[] { + const orderedRouteIndexes: number[] = [] + const seenRouteIndexes = new Set() + const seenErrorGroups = new Set() + + for (const [errorIndex, error] of snapshot.errors.entries()) { + const primaryTraceId = error.pcb_trace_id + const errorGroup = + typeof primaryTraceId === "string" + ? `trace:${primaryTraceId}` + : `error:${errorIndex}` + if (seenErrorGroups.has(errorGroup)) continue + seenErrorGroups.add(errorGroup) + + const relatedErrors = + typeof primaryTraceId === "string" + ? snapshot.errors.filter( + (relatedError) => relatedError.pcb_trace_id === primaryTraceId, + ) + : [error] + for (const relatedError of relatedErrors) { + const otherTraceId = getOtherTraceId( + relatedError, + snapshot.traceRouteIndexById, + ) + for (const traceId of [ + ...getCandidateTraceIdsFromError(relatedError), + otherTraceId, + ]) { + if (!traceId || traceId === primaryTraceId) continue + const routeIndex = snapshot.traceRouteIndexById.get(traceId) + if (routeIndex !== undefined && !seenRouteIndexes.has(routeIndex)) { + seenRouteIndexes.add(routeIndex) + orderedRouteIndexes.push(routeIndex) + } } } - if (!nextRoutes) { - for (const error of snapshot.errors) { - nextRoutes = this.tryBatchedTraceForce(improvedRoutes, error) - if (nextRoutes) break + + const primaryRouteIndex = + typeof primaryTraceId === "string" + ? snapshot.traceRouteIndexById.get(primaryTraceId) + : undefined + if ( + primaryRouteIndex !== undefined && + !seenRouteIndexes.has(primaryRouteIndex) + ) { + seenRouteIndexes.add(primaryRouteIndex) + orderedRouteIndexes.push(primaryRouteIndex) + } + } + + return orderedRouteIndexes + } + + private runIjumpFullRouteCleanup( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let roundAttempts = 0 + + while ( + roundAttempts < MAX_IJUMP_FULL_ATTEMPTS_PER_ROUND && + this.getRemainingIjumpIterations() > 0 + ) { + const snapshot = this.getSnapshot(improvedRoutes) + if (snapshot.count === 0) break + let nextRoutes: HighDensityRoute[] | undefined + const routeIndexes = this.getOrderedIjumpRouteIndexes(snapshot) + + attemptLoop: for (const variant of FULL_IJUMP_VARIANTS) { + for (const routeIndex of routeIndexes) { + if ( + roundAttempts >= MAX_IJUMP_FULL_ATTEMPTS_PER_ROUND || + this.getRemainingIjumpIterations() <= 0 + ) { + break attemptLoop + } + roundAttempts += 1 + this.ijumpFullAttempts += 1 + nextRoutes = this.tryIjumpCandidate( + improvedRoutes, + snapshot, + { + routeIndex, + includeCandidateCopper: true, + ...variant, + }, + MAX_IJUMP_FULL_ITERATIONS, + ) + if (nextRoutes) break attemptLoop } } + if (!nextRoutes) break improvedRoutes = nextRoutes } + return improvedRoutes + } + + private getErrorCenter( + error: DrcError, + ): { x: number; y: number } | undefined { + const center = error.center ?? error.pcb_center + if (!center || typeof center !== "object") return undefined + const maybeCenter = center as Record + if ( + typeof maybeCenter.x !== "number" || + typeof maybeCenter.y !== "number" + ) { + return undefined + } + return { x: maybeCenter.x, y: maybeCenter.y } + } + + private getInteriorRerouteWindows( + route: HighDensityRoute, + center: { x: number; y: number }, + ): Array<{ startIndex: number; endIndex: number }> { + if (route.route.length < 4) return [] + + let nearestSegmentIndex = -1 + let nearestDistance = Number.POSITIVE_INFINITY + for ( + let segmentIndex = 0; + segmentIndex < route.route.length - 1; + segmentIndex += 1 + ) { + const start = route.route[segmentIndex] + const end = route.route[segmentIndex + 1] + if (!start || !end || start.z !== end.z) continue + const distance = getPointToSegmentDistance(center, start, end) + if (distance < nearestDistance) { + nearestDistance = distance + nearestSegmentIndex = segmentIndex + } + } + if (nearestSegmentIndex < 1) return [] + + const lastInteriorIndex = route.route.length - 2 + if (nearestSegmentIndex + 1 > lastInteriorIndex) return [] + + const windows: Array<{ startIndex: number; endIndex: number }> = [] + const seenWindows = new Set() + const addWindow = (startIndex: number, endIndex: number) => { + const boundedStart = Math.max(1, startIndex) + const boundedEnd = Math.min(lastInteriorIndex, endIndex) + if ( + boundedStart > nearestSegmentIndex || + boundedEnd < nearestSegmentIndex + 1 || + boundedStart >= boundedEnd + ) { + return + } + const key = `${boundedStart}:${boundedEnd}` + if (seenWindows.has(key)) return + seenWindows.add(key) + windows.push({ startIndex: boundedStart, endIndex: boundedEnd }) + } + + addWindow( + nearestSegmentIndex - MAX_IJUMP_INTERIOR_EXPANSION, + nearestSegmentIndex + 1, + ) + addWindow( + nearestSegmentIndex, + nearestSegmentIndex + 1 + MAX_IJUMP_INTERIOR_EXPANSION, + ) + addWindow( + nearestSegmentIndex - MAX_IJUMP_INTERIOR_EXPANSION, + nearestSegmentIndex + 1 + MAX_IJUMP_INTERIOR_EXPANSION, + ) + + for ( + let totalExpansion = 0; + totalExpansion <= MAX_IJUMP_INTERIOR_EXPANSION; + totalExpansion += 1 + ) { + for ( + let startExpansion = 0; + startExpansion <= totalExpansion; + startExpansion += 1 + ) { + const endExpansion = totalExpansion - startExpansion + addWindow( + nearestSegmentIndex - startExpansion, + nearestSegmentIndex + 1 + endExpansion, + ) + } + } + + return windows + } + + private runIjumpInteriorCleanup( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let roundAttempts = 0 + + while ( + roundAttempts < MAX_IJUMP_INTERIOR_ATTEMPTS_PER_ROUND && + this.getRemainingIjumpIterations() > 0 + ) { + const snapshot = this.getSnapshot(improvedRoutes) + if (snapshot.count === 0) break + let nextRoutes: HighDensityRoute[] | undefined + + const targets: Array<{ + routeIndex: number + windows: Array<{ startIndex: number; endIndex: number }> + }> = [] + const seenTargets = new Set() + + for (const error of snapshot.errors) { + const center = this.getErrorCenter(error) + if (!center) continue + const primaryTraceId = error.pcb_trace_id + const otherTraceId = getOtherTraceId( + error, + snapshot.traceRouteIndexById, + ) + const traceIds = [ + ...getCandidateTraceIdsFromError(error), + otherTraceId, + primaryTraceId, + ] + const seenTraceIds = new Set() + + for (const traceId of traceIds) { + if ( + typeof traceId !== "string" || + seenTraceIds.has(traceId) || + !snapshot.traceRouteIndexById.has(traceId) + ) { + continue + } + seenTraceIds.add(traceId) + const routeIndex = snapshot.traceRouteIndexById.get(traceId)! + const route = improvedRoutes[routeIndex] + if (!route) continue + const windows = this.getInteriorRerouteWindows(route, center) + if (windows.length === 0) continue + const targetKey = `${routeIndex}:${windows + .map(({ startIndex, endIndex }) => `${startIndex}-${endIndex}`) + .join(",")}` + if (seenTargets.has(targetKey)) continue + seenTargets.add(targetKey) + targets.push({ routeIndex, windows }) + } + } + + const maximumWindowCount = Math.max( + 0, + ...targets.map((target) => target.windows.length), + ) + attemptLoop: for ( + let windowIndex = 0; + windowIndex < maximumWindowCount; + windowIndex += 1 + ) { + for (const variant of INTERIOR_IJUMP_VARIANTS) { + for (const target of targets) { + const window = target.windows[windowIndex] + if (!window) continue + if ( + roundAttempts >= MAX_IJUMP_INTERIOR_ATTEMPTS_PER_ROUND || + this.getRemainingIjumpIterations() <= 0 + ) { + break attemptLoop + } + roundAttempts += 1 + this.ijumpInteriorAttempts += 1 + nextRoutes = this.tryIjumpCandidate( + improvedRoutes, + snapshot, + { + routeIndex: target.routeIndex, + ...window, + includeCandidateCopper: true, + ...variant, + }, + MAX_IJUMP_INTERIOR_ITERATIONS, + ) + if (nextRoutes) break attemptLoop + } + } + } + + if (!nextRoutes) break + improvedRoutes = nextRoutes + } + + return improvedRoutes + } + + private runIjumpFixedOnlyCleanup( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let roundAttempts = 0 + + while ( + roundAttempts < MAX_IJUMP_FIXED_ONLY_ATTEMPTS_PER_ROUND && + this.getRemainingIjumpIterations() > 0 + ) { + const snapshot = this.getSnapshot(improvedRoutes) + if (snapshot.count === 0) break + let nextRoutes: HighDensityRoute[] | undefined + const routeIndexes = this.getOrderedIjumpRouteIndexes(snapshot) + + attemptLoop: for (const variant of FIXED_ONLY_IJUMP_VARIANTS) { + for (const routeIndex of routeIndexes) { + if ( + roundAttempts >= MAX_IJUMP_FIXED_ONLY_ATTEMPTS_PER_ROUND || + this.getRemainingIjumpIterations() <= 0 + ) { + break attemptLoop + } + roundAttempts += 1 + this.ijumpFixedOnlyAttempts += 1 + nextRoutes = this.tryIjumpCandidate( + improvedRoutes, + snapshot, + { + routeIndex, + includeCandidateCopper: false, + ...variant, + }, + MAX_IJUMP_FULL_ITERATIONS, + ) + if (nextRoutes) break attemptLoop + } + } + + if (!nextRoutes) break + improvedRoutes = nextRoutes + } + + return improvedRoutes + } + + private getErrorOwnedClusterRouteIndexes( + error: DrcError, + snapshot: DrcSnapshot, + routes: HighDensityRoute[], + ): number[] { + const routeIndexes = new Set( + this.getCandidateRouteIndexesForError(error, snapshot), + ) + const referencedTraceIds = [ + typeof error.pcb_trace_id === "string" ? error.pcb_trace_id : undefined, + getRawOtherTraceId(error), + ].filter((traceId): traceId is string => Boolean(traceId)) + + // A preloaded trace is namespaced as `preloaded__`. + // Include the candidate counterpart, when present, because rebuilding the + // whole error-owned group can free the corridor used by its fixed copy. + for (const referencedTraceId of referencedTraceIds) { + let longestEmbeddedMatchLength = -1 + const embeddedRouteIndexes: number[] = [] + for (const [ + candidateTraceId, + routeIndex, + ] of snapshot.traceRouteIndexById) { + if ( + referencedTraceId !== candidateTraceId && + !referencedTraceId.endsWith(`_${candidateTraceId}`) + ) { + continue + } + if (candidateTraceId.length > longestEmbeddedMatchLength) { + longestEmbeddedMatchLength = candidateTraceId.length + embeddedRouteIndexes.length = 0 + } + if (candidateTraceId.length === longestEmbeddedMatchLength) { + embeddedRouteIndexes.push(routeIndex) + } + } + for (const routeIndex of embeddedRouteIndexes) { + routeIndexes.add(routeIndex) + } + } + + return [...routeIndexes].filter( + (routeIndex) => (routes[routeIndex]?.route.length ?? 0) >= 2, + ) + } + + private getErrorOwnedClusterOrders( + snapshot: DrcSnapshot, + routes: HighDensityRoute[], + ): number[][] { + const residualDegreeByRouteIndex = new Map() + for (const error of snapshot.errors) { + for (const routeIndex of this.getErrorOwnedClusterRouteIndexes( + error, + snapshot, + routes, + )) { + residualDegreeByRouteIndex.set( + routeIndex, + (residualDegreeByRouteIndex.get(routeIndex) ?? 0) + 1, + ) + } + } + + const degreeDescending = ( + left: number, + right: number, + indexDirection: 1 | -1, + ) => + (residualDegreeByRouteIndex.get(right) ?? 0) - + (residualDegreeByRouteIndex.get(left) ?? 0) || + (left - right) * indexDirection + const ascendingIndexOrder = [...residualDegreeByRouteIndex.keys()].sort( + (left, right) => degreeDescending(left, right, 1), + ) + if (ascendingIndexOrder.length <= 1) return [ascendingIndexOrder] + + const descendingIndexOrder = [...ascendingIndexOrder].sort((left, right) => + degreeDescending(left, right, -1), + ) + return [ascendingIndexOrder, descendingIndexOrder] + } + + private getErrorOwnedClusterPlans( + snapshot: DrcSnapshot, + routes: HighDensityRoute[], + ): ErrorOwnedClusterPlan[] { + const degreeDescendingOrders = this.getErrorOwnedClusterOrders( + snapshot, + routes, + ).filter((order) => order.length > 0) + return [ + ...degreeDescendingOrders.map((routeIndexes) => ({ + routeIndexes, + reverse: false, + allowTerminalEscape: routeIndexes.length <= 2, + })), + ...degreeDescendingOrders.map((routeIndexes) => ({ + routeIndexes: routeIndexes.toReversed(), + reverse: true, + allowTerminalEscape: true, + })), + ] + } + + private runIjumpErrorOwnedClusterRebuild( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + const baselineSnapshot = this.getSnapshot(routes) + if (baselineSnapshot.count === 0) return routes + + const plans = this.getErrorOwnedClusterPlans(baselineSnapshot, routes) + const seenPlans = new Set() + const failedFirstRouteAttempts = new Set() + + for (const plan of plans) { + const planKey = `${plan.reverse ? "reverse" : "forward"}:${plan.routeIndexes.join(",")}` + if ( + seenPlans.has(planKey) || + this.errorOwnedClusterIterations >= MAX_ERROR_OWNED_CLUSTER_ITERATIONS + ) { + continue + } + seenPlans.add(planKey) + this.errorOwnedClusterOrderAttempts += 1 + + const candidateRoutes = cloneRoutes(routes) + const pendingRouteIndexes = new Set(plan.routeIndexes) + const rebuiltRouteIndexes: number[] = [] + let completed = true + + for (const routeIndex of plan.routeIndexes) { + const firstRouteAttemptKey = [ + plan.reverse ? "reverse" : "forward", + routeIndex, + ...[...pendingRouteIndexes].sort((left, right) => left - right), + ].join(":") + if ( + rebuiltRouteIndexes.length === 0 && + failedFirstRouteAttempts.has(firstRouteAttemptKey) + ) { + completed = false + break + } + const remainingIterations = + MAX_ERROR_OWNED_CLUSTER_ITERATIONS - this.errorOwnedClusterIterations + if (remainingIterations <= 0) { + completed = false + break + } + + pendingRouteIndexes.delete(routeIndex) + this.errorOwnedClusterRouteAttempts += 1 + let result = this.ijumpRerouter.tryReroute(candidateRoutes, { + routeIndex, + omitCandidateRouteIndexes: pendingRouteIndexes, + includeCandidateCopper: true, + reverse: plan.reverse, + shortenPath: false, + maxIterations: Math.min( + MAX_ERROR_OWNED_CLUSTER_ROUTE_ITERATIONS, + remainingIterations, + ), + }) + this.errorOwnedClusterIterations += Math.max(0, result?.iterations ?? 0) + if ( + (!result?.route || !routeHasValidLayerTransitions(result.route)) && + plan.allowTerminalEscape + ) { + for (const candidate of this.ijumpRerouter + .getTerminalViaEscapeCandidates(candidateRoutes, routeIndex) + .slice(0, MAX_ERROR_OWNED_CLUSTER_TERMINAL_ESCAPE_CANDIDATES)) { + const terminalRemainingIterations = + MAX_ERROR_OWNED_CLUSTER_ITERATIONS - + this.errorOwnedClusterIterations + if (terminalRemainingIterations <= 0) break + + this.errorOwnedClusterTerminalEscapeAttempts += 1 + const terminalResult = + this.ijumpRerouter.tryRerouteWithTerminalViaEscape( + candidateRoutes, + { + routeIndex, + omitCandidateRouteIndexes: pendingRouteIndexes, + candidate, + includeCandidateCopper: true, + reverse: plan.reverse, + shortenPath: false, + maxIterations: Math.min( + MAX_ERROR_OWNED_CLUSTER_TERMINAL_ESCAPE_ITERATIONS, + terminalRemainingIterations, + ), + }, + ) + this.errorOwnedClusterIterations += Math.max( + 0, + terminalResult?.iterations ?? 0, + ) + if ( + terminalResult?.route && + routeHasValidLayerTransitions(terminalResult.route) + ) { + result = terminalResult + break + } + } + } + if (!result?.route || !routeHasValidLayerTransitions(result.route)) { + if (rebuiltRouteIndexes.length === 0) { + failedFirstRouteAttempts.add(firstRouteAttemptKey) + } + completed = false + break + } + + candidateRoutes[routeIndex] = result.route + rebuiltRouteIndexes.push(routeIndex) + } + + if (!completed) continue + const materializedCandidate = materializeRoutes(candidateRoutes) + if ( + !this.candidatePreservesTerminals(materializedCandidate) || + rebuiltRouteIndexes.some((routeIndex) => { + const route = materializedCandidate[routeIndex] + return !route || !routeHasValidLayerTransitions(route) + }) + ) { + continue + } + + this.errorOwnedClusterDrcEvaluations += 1 + this.cleanupCandidateAttempts += 1 + const candidateSnapshot = this.getSnapshot(materializedCandidate) + if (candidateSnapshot.count >= baselineSnapshot.count) continue + + let acceptedRoutes = materializedCandidate + let acceptedSnapshot = candidateSnapshot + const postClusterOrder = this.getErrorOwnedClusterOrders( + acceptedSnapshot, + acceptedRoutes, + )[0] + for (const routeIndex of postClusterOrder ?? []) { + if ( + acceptedSnapshot.count === 0 || + this.errorOwnedClusterIterations >= MAX_ERROR_OWNED_CLUSTER_ITERATIONS + ) { + break + } + + const remainingIterations = + MAX_ERROR_OWNED_CLUSTER_ITERATIONS - this.errorOwnedClusterIterations + this.errorOwnedClusterRouteAttempts += 1 + this.errorOwnedClusterPostRouteAttempts += 1 + const result = this.ijumpRerouter.tryReroute(acceptedRoutes, { + routeIndex, + includeCandidateCopper: true, + reverse: false, + shortenPath: false, + maxIterations: Math.min( + MAX_ERROR_OWNED_CLUSTER_ROUTE_ITERATIONS, + remainingIterations, + ), + }) + this.errorOwnedClusterIterations += Math.max(0, result?.iterations ?? 0) + if (!result?.route || !routeHasValidLayerTransitions(result.route)) { + continue + } + + const postCandidateRoutes = cloneRoutes(acceptedRoutes) + postCandidateRoutes[routeIndex] = result.route + const materializedPostCandidate = materializeRoutes(postCandidateRoutes) + if ( + !this.candidatePreservesTerminals(materializedPostCandidate) || + !routeHasValidLayerTransitions(materializedPostCandidate[routeIndex]!) + ) { + continue + } + + this.errorOwnedClusterDrcEvaluations += 1 + this.cleanupCandidateAttempts += 1 + const postCandidateSnapshot = this.getSnapshot( + materializedPostCandidate, + ) + if (postCandidateSnapshot.count >= acceptedSnapshot.count) continue + + acceptedRoutes = materializedPostCandidate + acceptedSnapshot = postCandidateSnapshot + this.errorOwnedClusterPostCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + } + + this.errorOwnedClusterAccepted += 1 + this.cleanupCandidatesAccepted += 1 + return acceptedRoutes + } + + return routes + } + + private runIjumpErrorOwnedClusterRebuildPasses( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + for (let pass = 0; pass < MAX_ERROR_OWNED_CLUSTER_PASSES; pass += 1) { + const issueCountBeforePass = this.getSnapshot(improvedRoutes).count + if ( + issueCountBeforePass === 0 || + this.errorOwnedClusterIterations >= MAX_ERROR_OWNED_CLUSTER_ITERATIONS + ) { + break + } + + const candidateRoutes = + this.runIjumpErrorOwnedClusterRebuild(improvedRoutes) + const issueCountAfterPass = this.getSnapshot(candidateRoutes).count + if (issueCountAfterPass >= issueCountBeforePass) break + improvedRoutes = candidateRoutes + } + return improvedRoutes + } + + private runPostClusterViaMicroShiftCleanup( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + const baseLimit = this.selectedLocalCleanupDrcEvaluationLimit + const evaluationsBeforeSweep = this.localCleanupDrcEvaluations + const consecutiveMissesBeforeSweep = this.consecutiveLocalCleanupDrcMisses + this.selectedLocalCleanupDrcEvaluationLimit = + Math.max(baseLimit, evaluationsBeforeSweep) + + MAX_POST_CLUSTER_VIA_MICRO_SHIFT_DRC_EVALUATIONS + // This sweep has its own explicit evaluation allowance, so a stalled + // earlier local phase must not consume it. + this.consecutiveLocalCleanupDrcMisses = 0 + try { + return this.runViaMicroShiftCleanup(routes) + } finally { + this.postClusterViaMicroShiftDrcEvaluations += + this.localCleanupDrcEvaluations - evaluationsBeforeSweep + this.selectedLocalCleanupDrcEvaluationLimit = baseLimit + this.consecutiveLocalCleanupDrcMisses = consecutiveMissesBeforeSweep + } + } + + private getRemainingFinalOwnerIterations(): number { + return Math.max( + 0, + MAX_FINAL_OWNER_IJUMP_ITERATIONS - this.finalOwnerIterations, + ) + } + + private getFinalOwnerErrorKey( + routeIndex: number, + snapshot: DrcSnapshot, + ): string { + return snapshot.errors + .filter((error) => + this.getCandidateRouteIndexesForError(error, snapshot).includes( + routeIndex, + ), + ) + .map((error) => + String( + error.pcb_trace_error_id ?? + error.pcb_via_trace_clearance_error_id ?? + error.pcb_pad_trace_clearance_error_id ?? + error.pcb_via_clearance_error_id ?? + error.error_type ?? + error.type, + ), + ) + .sort() + .join("|") + } + + private getFinalOwnerInteriorWindows( + route: HighDensityRoute, + center: { x: number; y: number }, + ): Array<{ startIndex: number; endIndex: number }> { + let nearestSegmentIndex = -1 + let nearestDistance = Number.POSITIVE_INFINITY + for ( + let segmentIndex = 0; + segmentIndex < route.route.length - 1; + segmentIndex += 1 + ) { + const start = route.route[segmentIndex] + const end = route.route[segmentIndex + 1] + if (!start || !end || start.z !== end.z) continue + const distance = getPointToSegmentDistance(center, start, end) + if (distance < nearestDistance) { + nearestDistance = distance + nearestSegmentIndex = segmentIndex + } + } + if (nearestSegmentIndex < 0) return [] + + return this.getInteriorRerouteWindows(route, center).toSorted( + (left, right) => + Math.abs( + (left.startIndex + left.endIndex - 1) / 2 - nearestSegmentIndex, + ) - + Math.abs( + (right.startIndex + right.endIndex - 1) / 2 - nearestSegmentIndex, + ) || + left.endIndex - left.startIndex - (right.endIndex - right.startIndex) || + left.startIndex - right.startIndex, + ) + } + + private tryFinalOwnerIjumpCandidate( + routes: HighDensityRoute[], + snapshot: DrcSnapshot, + options: Omit, + maxIterations: number, + kind: "full" | "interior", + ): + | { + routes: HighDensityRoute[] + snapshot: DrcSnapshot + } + | undefined { + const iterationLimit = Math.min( + maxIterations, + this.getRemainingFinalOwnerIterations(), + ) + if (iterationLimit <= 0) return undefined + + if (kind === "full") this.finalOwnerFullAttempts += 1 + else this.finalOwnerInteriorAttempts += 1 + const result = this.ijumpRerouter.tryReroute(routes, { + ...options, + maxIterations: iterationLimit, + }) + this.finalOwnerIterations += Math.max(0, result?.iterations ?? 0) + if (!result?.route || !routeHasValidLayerTransitions(result.route)) { + return undefined + } + + const candidateRoutes = cloneRoutes(routes) + candidateRoutes[options.routeIndex] = result.route + const materializedCandidate = materializeRoutes(candidateRoutes) + const materializedRoute = materializedCandidate[options.routeIndex] + if ( + !materializedRoute || + !routeHasValidLayerTransitions(materializedRoute) || + !this.candidatePreservesTerminals(materializedCandidate) + ) { + return undefined + } + + this.finalOwnerDrcEvaluations += 1 + this.cleanupCandidateAttempts += 1 + const candidateSnapshot = this.getSnapshot(materializedCandidate) + if (candidateSnapshot.count >= snapshot.count) return undefined + + this.finalOwnerCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + return { + routes: materializedCandidate, + snapshot: candidateSnapshot, + } + } + + private runIjumpFinalErrorOwnerSweep( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let snapshot = this.getSnapshot(improvedRoutes) + const failedFullAttempts = new Set() + const failedInteriorAttempts = new Set() + + repairLoop: while ( + snapshot.count > 0 && + this.getRemainingFinalOwnerIterations() > 0 + ) { + let acceptedCandidate: + | { + routes: HighDensityRoute[] + snapshot: DrcSnapshot + } + | undefined + const routeIndexes = this.getOrderedIjumpRouteIndexes(snapshot) + + fullLoop: for (const variant of FINAL_OWNER_FULL_VARIANTS) { + for (const routeIndex of routeIndexes) { + const route = improvedRoutes[routeIndex] + if (!route) continue + const isLongRoute = + route.route.length > MAX_FINAL_OWNER_FULL_VARIANT_ROUTE_POINTS + if (isLongRoute && (variant.reverse || variant.shortenPath)) continue + + const attemptKey = [ + routeIndex, + this.getFinalOwnerErrorKey(routeIndex, snapshot), + variant.reverse ? "reverse" : "forward", + variant.shortenPath ? "short" : "raw", + ].join(":") + if (failedFullAttempts.has(attemptKey)) continue + + const remainingForFullRoute = + this.getRemainingFinalOwnerIterations() - + FINAL_OWNER_INTERIOR_ITERATION_RESERVE + if (remainingForFullRoute <= 0) break fullLoop + const perAttemptLimit = isLongRoute + ? MAX_FINAL_OWNER_LONG_ROUTE_ITERATIONS + : variant.reverse || variant.shortenPath + ? MAX_FINAL_OWNER_VARIANT_ITERATIONS + : MAX_FINAL_OWNER_FULL_ROUTE_ITERATIONS + acceptedCandidate = this.tryFinalOwnerIjumpCandidate( + improvedRoutes, + snapshot, + { + routeIndex, + includeCandidateCopper: true, + ...variant, + }, + Math.min(perAttemptLimit, remainingForFullRoute), + "full", + ) + if (acceptedCandidate) break fullLoop + failedFullAttempts.add(attemptKey) + } + } + + if (acceptedCandidate) { + improvedRoutes = acceptedCandidate.routes + snapshot = acceptedCandidate.snapshot + // A successful reroute changes the candidate-copper obstacle field. + // Attempts that failed against the previous geometry must be eligible + // again even when their DRC error identifiers did not change. + failedFullAttempts.clear() + failedInteriorAttempts.clear() + continue + } + + interiorLoop: for (const error of snapshot.errors) { + const center = this.getErrorCenter(error) + if (!center) continue + for (const routeIndex of this.getCandidateRouteIndexesForError( + error, + snapshot, + )) { + const route = improvedRoutes[routeIndex] + if (!route) continue + for (const window of this.getFinalOwnerInteriorWindows( + route, + center, + )) { + const attemptKey = [ + routeIndex, + this.getFinalOwnerErrorKey(routeIndex, snapshot), + window.startIndex, + window.endIndex, + ].join(":") + if (failedInteriorAttempts.has(attemptKey)) continue + const remainingIterations = this.getRemainingFinalOwnerIterations() + if (remainingIterations <= 0) break repairLoop + + acceptedCandidate = this.tryFinalOwnerIjumpCandidate( + improvedRoutes, + snapshot, + { + routeIndex, + ...window, + includeCandidateCopper: true, + reverse: true, + shortenPath: false, + }, + Math.min( + MAX_FINAL_OWNER_INTERIOR_ITERATIONS, + remainingIterations, + ), + "interior", + ) + if (acceptedCandidate) break interiorLoop + failedInteriorAttempts.add(attemptKey) + } + } + } + + if (acceptedCandidate) { + improvedRoutes = acceptedCandidate.routes + snapshot = acceptedCandidate.snapshot + failedFullAttempts.clear() + failedInteriorAttempts.clear() + continue + } + + if (snapshot.count <= MAX_FINAL_OWNER_FALLBACK_RESIDUAL) { + fallbackLoop: for (const variant of FINAL_OWNER_FULL_VARIANTS) { + for (const routeIndex of this.getOrderedIjumpRouteIndexes(snapshot)) { + const remainingIterations = this.getRemainingFinalOwnerIterations() + if (remainingIterations <= 0) break fallbackLoop + acceptedCandidate = this.tryFinalOwnerIjumpCandidate( + improvedRoutes, + snapshot, + { + routeIndex, + includeCandidateCopper: true, + ...variant, + }, + remainingIterations, + "full", + ) + if (acceptedCandidate) break fallbackLoop + } + } + } + + if (!acceptedCandidate) break + improvedRoutes = acceptedCandidate.routes + snapshot = acceptedCandidate.snapshot + failedFullAttempts.clear() + failedInteriorAttempts.clear() + } + + return improvedRoutes + } + + private normalizeViaMetadataFromLayerTransitions( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + return materializeRoutes(routes).map((route) => { + const seenViaLocations = new Set() + const vias = route.route.flatMap((point, pointIndex) => { + const nextPoint = route.route[pointIndex + 1] + if ( + !nextPoint || + point.z === nextPoint.z || + point.x !== nextPoint.x || + point.y !== nextPoint.y + ) { + return [] + } + + const locationKey = `${point.x}:${point.y}` + if (seenViaLocations.has(locationKey)) return [] + seenViaLocations.add(locationKey) + return [{ x: point.x, y: point.y }] + }) + + return { ...route, vias } + }) + } + + private getCanonicalNetForRoute(route: HighDensityRoute): string | undefined { + return this.params.connMap?.getNetConnectedToId(route.connectionName) + } + + private tryScopedSameNetViaMerge( + routes: HighDensityRoute[], + ownerRouteIndex: number, + maxIterations: number, + ): ScopedSameNetViaMergeResult { + const connMap = this.params.connMap + const ownerRoute = routes[ownerRouteIndex] + const canonicalNet = ownerRoute && this.getCanonicalNetForRoute(ownerRoute) + if (!connMap || !ownerRoute || !canonicalNet || maxIterations <= 0) { + return { iterations: 0, mergedViaCount: 0 } + } + + const scopedRouteIndexes = routes.flatMap((route, routeIndex) => + this.getCanonicalNetForRoute(route) === canonicalNet ? [routeIndex] : [], + ) + if (scopedRouteIndexes.length === 0) { + return { iterations: 0, mergedViaCount: 0 } + } + + const scopedRoutes = this.normalizeViaMetadataFromLayerTransitions( + scopedRouteIndexes.map((routeIndex) => routes[routeIndex]!), + ) + let merger: SameNetViaMergerSolver | undefined + try { + merger = new SameNetViaMergerSolver({ + inputHdRoutes: scopedRoutes, + obstacles: this.params.srj.obstacles, + colorMap: {}, + layerCount: this.params.srj.layerCount, + connMap, + }) + while ( + !merger.solved && + !merger.failed && + merger.iterations < maxIterations + ) { + merger.step() + } + } catch { + return { + iterations: Math.min(maxIterations, merger?.iterations ?? 0), + mergedViaCount: 0, + } + } + + const iterations = Math.min(maxIterations, merger.iterations) + const mergedViaCount = Number(merger.stats.mergedViaCount) || 0 + const mergedScopedRoutes = merger.getMergedViaHdRoutes() + if ( + !merger.solved || + merger.failed || + mergedViaCount <= 0 || + !mergedScopedRoutes || + mergedScopedRoutes.length !== scopedRouteIndexes.length + ) { + return { iterations, mergedViaCount: 0 } + } + + const mergedRoutes = cloneRoutes(routes) + for (const [scopedIndex, routeIndex] of scopedRouteIndexes.entries()) { + mergedRoutes[routeIndex] = mergedScopedRoutes[scopedIndex]! + } + return { + routes: materializeRoutes(mergedRoutes), + iterations, + mergedViaCount, + } + } + + private runPostRepairSameNetViaMerge( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let baselineSnapshot = this.getSnapshot(improvedRoutes) + if ( + baselineSnapshot.count === 0 || + !baselineSnapshot.errors.some( + (error) => getErrorType(error) === "pcb_via_clearance_error", + ) + ) { + return routes + } + if (!this.params.connMap) return routes + + const orderedOwnerRouteIndexes: number[] = [] + const seenCanonicalNets = new Set() + const addOwner = (routeIndex: number) => { + const route = improvedRoutes[routeIndex] + const canonicalNet = route && this.getCanonicalNetForRoute(route) + if (!canonicalNet || seenCanonicalNets.has(canonicalNet)) return + seenCanonicalNets.add(canonicalNet) + orderedOwnerRouteIndexes.push(routeIndex) + } + + for (const error of baselineSnapshot.errors) { + if (getErrorType(error) !== "pcb_via_clearance_error") continue + for (const routeIndex of this.getCandidateRouteIndexesForError( + error, + baselineSnapshot, + )) { + addOwner(routeIndex) + } + } + for (const routeIndex of improvedRoutes.keys()) addOwner(routeIndex) + + for (const ownerRouteIndex of orderedOwnerRouteIndexes) { + const remainingIterations = + MAX_POST_REPAIR_SAME_NET_VIA_MERGER_ITERATIONS - + this.postRepairSameNetViaMergeIterations + if (remainingIterations <= 0) break + + this.postRepairSameNetViaMergeAttempts += 1 + const mergeResult = this.tryScopedSameNetViaMerge( + improvedRoutes, + ownerRouteIndex, + remainingIterations, + ) + this.postRepairSameNetViaMergeIterations += mergeResult.iterations + const materializedCandidate = mergeResult.routes + if ( + !materializedCandidate || + !this.candidatePreservesTerminals(materializedCandidate) || + materializedCandidate.some( + (route) => !routeHasValidLayerTransitions(route), + ) + ) { + continue + } + + this.postRepairSameNetViaMergeDrcEvaluations += 1 + this.cleanupCandidateAttempts += 1 + const candidateSnapshot = this.getSnapshot(materializedCandidate) + if (candidateSnapshot.count >= baselineSnapshot.count) continue + + this.postRepairSameNetViaMergeCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + improvedRoutes = materializedCandidate + baselineSnapshot = candidateSnapshot + if (baselineSnapshot.count === 0) break + } + + return improvedRoutes + } + + private getDrcErrorIdentifier(error: DrcError, errorIndex: number): string { + for (const key of [ + "pcb_trace_error_id", + "pcb_error_id", + "pcb_via_trace_clearance_error_id", + "pcb_pad_trace_clearance_error_id", + "pcb_via_clearance_error_id", + ]) { + const identifier = error[key] + if (typeof identifier === "string") return identifier + } + return `${getErrorType(error) ?? "unknown"}:${errorIndex}` + } + + private getSharedTerminalCompositeGroups( + routes: HighDensityRoute[], + snapshot: DrcSnapshot, + ): SharedTerminalCompositeGroup[] { + if ( + snapshot.count === 0 || + snapshot.count > MAX_SHARED_TERMINAL_COMPOSITE_RESIDUAL || + !this.params.connMap + ) { + return [] + } + + const candidatesByFixedTraceAndNet = new Map< + string, + { + fixedTraceId: string + canonicalNet: string + routeIndexes: Set + baselineErrorIds: Set + } + >() + for (const [errorIndex, error] of snapshot.errors.entries()) { + if ( + getErrorType(error) !== "pcb_trace_error" || + getPhysicalPadIdFromError(error) + ) { + continue + } + const candidateRouteIndexes = this.getCandidateRouteIndexesForError( + error, + snapshot, + ) + if (candidateRouteIndexes.length !== 1) continue + const routeIndex = candidateRouteIndexes[0]! + const route = routes[routeIndex] + const canonicalNet = route && this.getCanonicalNetForRoute(route) + if (!route || !canonicalNet) continue + + const referencedTraceIds = [ + typeof error.pcb_trace_id === "string" ? error.pcb_trace_id : undefined, + getRawOtherTraceId(error), + ].filter((traceId): traceId is string => Boolean(traceId)) + const fixedTraceIds = [ + ...new Set( + referencedTraceIds.filter( + (traceId) => !snapshot.traceRouteIndexById.has(traceId), + ), + ), + ] + if (fixedTraceIds.length !== 1) continue + const fixedTraceId = fixedTraceIds[0]! + const key = `${fixedTraceId}:${canonicalNet}` + const candidate = candidatesByFixedTraceAndNet.get(key) ?? { + fixedTraceId, + canonicalNet, + routeIndexes: new Set(), + baselineErrorIds: new Set(), + } + candidate.routeIndexes.add(routeIndex) + candidate.baselineErrorIds.add( + this.getDrcErrorIdentifier(error, errorIndex), + ) + candidatesByFixedTraceAndNet.set(key, candidate) + } + + const groups: SharedTerminalCompositeGroup[] = [] + for (const candidate of candidatesByFixedTraceAndNet.values()) { + const routeIndexes = [...candidate.routeIndexes] + if (routeIndexes.length < 2) continue + + let commonPortIds: Set | undefined + for (const routeIndex of routeIndexes) { + const routePortIds = new Set( + this.terminalConstraints.flatMap((constraint) => + constraint.routeIndex === routeIndex && + typeof constraint.originalPoint.pcb_port_id === "string" + ? [constraint.originalPoint.pcb_port_id] + : [], + ), + ) + commonPortIds = + commonPortIds === undefined + ? routePortIds + : new Set( + [...commonPortIds].filter((portId) => routePortIds.has(portId)), + ) + } + + for (const terminalPortId of [...(commonPortIds ?? [])].sort()) { + const branches = routeIndexes.flatMap((routeIndex) => { + const constraint = this.terminalConstraints.find( + (candidateConstraint) => + candidateConstraint.routeIndex === routeIndex && + candidateConstraint.originalPoint.pcb_port_id === terminalPortId, + ) + return constraint + ? [{ routeIndex, endpoint: constraint.endpoint }] + : [] + }) + if (branches.length !== routeIndexes.length) continue + groups.push({ + fixedTraceId: candidate.fixedTraceId, + canonicalNet: candidate.canonicalNet, + terminalPortId, + branches, + baselineErrorIds: new Set(candidate.baselineErrorIds), + }) + } + } + return groups + } + + private terminalCanHostRelocatedVia( + routes: HighDensityRoute[], + routeIndex: number, + endpoint: "start" | "end", + ): boolean { + const route = routes[routeIndex] + const terminal = + endpoint === "start" ? route?.route[0] : route?.route.at(-1) + const constraint = this.terminalConstraints.find( + (candidate) => + candidate.routeIndex === routeIndex && candidate.endpoint === endpoint, + ) + if (!route || !terminal || !constraint) return false + + const viaRadius = (route.viaDiameter ?? this.params.srj.minViaDiameter) / 2 + const bounds = this.params.srj.bounds + if ( + terminal.x < bounds.minX + viaRadius - POSITION_EPSILON || + terminal.x > bounds.maxX - viaRadius + POSITION_EPSILON || + terminal.y < bounds.minY + viaRadius - POSITION_EPSILON || + terminal.y > bounds.maxY - viaRadius + POSITION_EPSILON + ) { + return false + } + + return constraint.owningObstacles.some( + (obstacle) => + obstacle.connectedTo.some( + (id) => + id.startsWith("pcb_smtpad_") || id.startsWith("pcb_plated_hole_"), + ) && pointFitsInsideObstacle(terminal, obstacle, viaRadius), + ) + } + + private relocateNearestTransitionToTerminal( + route: HighDensityRoute, + endpoint: "start" | "end", + ): HighDensityRoute | undefined { + const terminal = endpoint === "start" ? route.route[0] : route.route.at(-1) + if (!terminal || route.route.length < 3) return undefined + + const transitionIndexes: number[] = [] + for ( + let pointIndex = 0; + pointIndex < route.route.length - 1; + pointIndex += 1 + ) { + const point = route.route[pointIndex] + const nextPoint = route.route[pointIndex + 1] + if ( + point && + nextPoint && + point.z !== nextPoint.z && + getPointDistance(point, nextPoint) <= POSITION_EPSILON + ) { + transitionIndexes.push(pointIndex) + } + } + const transitionIndex = + endpoint === "start" ? transitionIndexes[0] : transitionIndexes.at(-1) + if (transitionIndex === undefined) return undefined + + const transitionStart = route.route[transitionIndex]! + const transitionEnd = route.route[transitionIndex + 1]! + const oppositeZ = + terminal.z === transitionStart.z + ? transitionEnd.z + : terminal.z === transitionEnd.z + ? transitionStart.z + : undefined + if ( + oppositeZ === undefined || + oppositeZ === terminal.z || + oppositeZ < 0 || + oppositeZ >= this.params.srj.layerCount + ) { + return undefined + } + + const { pcb_port_id: _pcbPortId, ...terminalWithoutPortId } = terminal + const oppositeTerminal = { + ...terminalWithoutPortId, + z: oppositeZ, + } + const relocatedPoints = + endpoint === "start" + ? [ + { ...terminal }, + oppositeTerminal, + ...route.route.slice(transitionIndex + 2), + ] + : [ + ...route.route.slice(0, transitionIndex), + oppositeTerminal, + { ...terminal }, + ] + if (relocatedPoints.length < 2) return undefined + return { ...route, route: relocatedPoints } + } + + private runSharedTerminalCompositeRepair( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + const baselineSnapshot = this.getSnapshot(routes) + const groups = this.getSharedTerminalCompositeGroups( + routes, + baselineSnapshot, + ) + for (const group of groups) { + if ( + this.sharedTerminalCompositeAttempts >= + MAX_SHARED_TERMINAL_COMPOSITE_ATTEMPTS || + this.sharedTerminalCompositeDrcEvaluations >= + MAX_SHARED_TERMINAL_COMPOSITE_DRC_EVALUATIONS + ) { + break + } + if ( + group.branches.some( + ({ routeIndex, endpoint }) => + !this.terminalCanHostRelocatedVia(routes, routeIndex, endpoint), + ) + ) { + continue + } + + const relocatedRoutes = cloneRoutes(routes) + let relocationIsValid = true + for (const { routeIndex, endpoint } of group.branches) { + const relocatedRoute = this.relocateNearestTransitionToTerminal( + relocatedRoutes[routeIndex]!, + endpoint, + ) + if (!relocatedRoute) { + relocationIsValid = false + break + } + relocatedRoutes[routeIndex] = relocatedRoute + } + if (!relocationIsValid) continue + + this.sharedTerminalCompositeAttempts += 1 + this.sharedTerminalCompositeRelocatedBranches += group.branches.length + let atomicCandidate = + this.normalizeViaMetadataFromLayerTransitions(relocatedRoutes) + if ( + atomicCandidate.some( + (route) => !routeHasValidLayerTransitions(route), + ) || + !this.candidatePreservesTerminals(atomicCandidate) + ) { + continue + } + + this.sharedTerminalCompositeDrcEvaluations += 1 + this.cleanupCandidateAttempts += 1 + const relocatedSnapshot = this.getSnapshot(atomicCandidate) + if (relocatedSnapshot.count < baselineSnapshot.count) { + this.sharedTerminalCompositeCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + return atomicCandidate + } + if (relocatedSnapshot.count > baselineSnapshot.count) continue + + const relocatedErrorIds = new Set( + relocatedSnapshot.errors.map((error, errorIndex) => + this.getDrcErrorIdentifier(error, errorIndex), + ), + ) + if ( + [...group.baselineErrorIds].some((errorId) => + relocatedErrorIds.has(errorId), + ) + ) { + continue + } + + const branchRouteIndexes = new Set( + group.branches.map(({ routeIndex }) => routeIndex), + ) + const baselineErrorIds = new Set( + baselineSnapshot.errors.map((error, errorIndex) => + this.getDrcErrorIdentifier(error, errorIndex), + ), + ) + const exposedOwnerSets = relocatedSnapshot.errors.flatMap( + (error, errorIndex) => { + if ( + baselineErrorIds.has(this.getDrcErrorIdentifier(error, errorIndex)) + ) { + return [] + } + const routeIndexes = this.getCandidateRouteIndexesForError( + error, + relocatedSnapshot, + ) + if ( + !routeIndexes.some((routeIndex) => + branchRouteIndexes.has(routeIndex), + ) + ) { + return [] + } + const exposedRouteIndexes = routeIndexes.filter( + (routeIndex) => !branchRouteIndexes.has(routeIndex), + ) + return exposedRouteIndexes.length > 0 + ? [new Set(exposedRouteIndexes)] + : [] + }, + ) + if (exposedOwnerSets.length === 0) continue + const commonExposedOwners = new Set(exposedOwnerSets[0]) + for (const ownerSet of exposedOwnerSets.slice(1)) { + for (const routeIndex of commonExposedOwners) { + if (!ownerSet.has(routeIndex)) commonExposedOwners.delete(routeIndex) + } + } + if (commonExposedOwners.size !== 1) continue + const exposedOwnerRouteIndex = [...commonExposedOwners][0]! + + const remainingIterations = + MAX_SHARED_TERMINAL_COMPOSITE_IJUMP_ITERATIONS - + this.sharedTerminalCompositeIterations + if (remainingIterations <= 0) break + this.sharedTerminalCompositeIjumpAttempts += 1 + const rerouteResult = this.ijumpRerouter.tryReroute(atomicCandidate, { + routeIndex: exposedOwnerRouteIndex, + includeCandidateCopper: true, + reverse: false, + shortenPath: false, + maxIterations: remainingIterations, + }) + this.sharedTerminalCompositeIterations += Math.min( + remainingIterations, + Math.max(0, rerouteResult?.iterations ?? 0), + ) + if ( + !rerouteResult?.route || + !routeHasValidLayerTransitions(rerouteResult.route) + ) { + continue + } + + atomicCandidate = cloneRoutes(atomicCandidate) + atomicCandidate[exposedOwnerRouteIndex] = rerouteResult.route + atomicCandidate = + this.normalizeViaMetadataFromLayerTransitions(atomicCandidate) + if ( + atomicCandidate.some( + (route) => !routeHasValidLayerTransitions(route), + ) || + !this.candidatePreservesTerminals(atomicCandidate) || + this.sharedTerminalCompositeDrcEvaluations >= + MAX_SHARED_TERMINAL_COMPOSITE_DRC_EVALUATIONS + ) { + continue + } + + this.sharedTerminalCompositeDrcEvaluations += 1 + this.cleanupCandidateAttempts += 1 + const candidateSnapshot = this.getSnapshot(atomicCandidate) + if (candidateSnapshot.count >= baselineSnapshot.count) continue + + this.sharedTerminalCompositeCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + return atomicCandidate + } + + return routes + } + + private getPostFinalCompositeWindows( + route: HighDensityRoute, + center: { x: number; y: number }, + ): PostFinalCompositeWindow[] { + if (route.route.length < 3) return [] + + let nearestSegmentIndex = -1 + let nearestDistance = Number.POSITIVE_INFINITY + for ( + let segmentIndex = 0; + segmentIndex < route.route.length - 1; + segmentIndex += 1 + ) { + const start = route.route[segmentIndex] + const end = route.route[segmentIndex + 1] + if (!start || !end || start.z !== end.z) continue + const distance = getPointToSegmentDistance(center, start, end) + if (distance < nearestDistance) { + nearestDistance = distance + nearestSegmentIndex = segmentIndex + } + } + if (nearestSegmentIndex < 0) return [] + + const lastPointIndex = route.route.length - 1 + const lastSegmentIndex = lastPointIndex - 1 + const lastInteriorIndex = lastPointIndex - 1 + const windows: PostFinalCompositeWindow[] = [] + const seenWindows = new Set() + const addWindow = ( + startIndex: number, + endIndex: number, + terminalRooted: boolean, + ) => { + if ( + startIndex < 0 || + endIndex > lastPointIndex || + startIndex >= endIndex || + startIndex > nearestSegmentIndex || + endIndex < nearestSegmentIndex + 1 + ) { + return + } + const key = `${startIndex}:${endIndex}` + if (seenWindows.has(key)) return + seenWindows.add(key) + windows.push({ startIndex, endIndex, terminalRooted }) + } + + if (nearestSegmentIndex <= POST_FINAL_COMPOSITE_TERMINAL_PROXIMITY) { + let endIndex = Math.min( + lastPointIndex, + nearestSegmentIndex + + 1 + + POST_FINAL_COMPOSITE_INTERIOR_EXPANSIONS.at(-1)!, + ) + for ( + let pointIndex = nearestSegmentIndex; + pointIndex < lastPointIndex; + pointIndex += 1 + ) { + if (route.route[pointIndex]!.z === route.route[pointIndex + 1]!.z) { + continue + } + endIndex = Math.min(lastPointIndex, pointIndex + 2) + break + } + addWindow(0, endIndex, true) + } + + if ( + lastSegmentIndex - nearestSegmentIndex <= + POST_FINAL_COMPOSITE_TERMINAL_PROXIMITY + ) { + let startIndex = Math.max( + 0, + nearestSegmentIndex - POST_FINAL_COMPOSITE_INTERIOR_EXPANSIONS.at(-1)!, + ) + for ( + let pointIndex = nearestSegmentIndex; + pointIndex >= 0; + pointIndex -= 1 + ) { + if (route.route[pointIndex]!.z === route.route[pointIndex + 1]!.z) { + continue + } + startIndex = Math.max(0, pointIndex - 1) + break + } + addWindow(startIndex, lastPointIndex, true) + } + + for (const expansion of POST_FINAL_COMPOSITE_INTERIOR_EXPANSIONS) { + const startIndex = Math.max(1, nearestSegmentIndex - expansion) + const endIndex = Math.min( + lastInteriorIndex, + nearestSegmentIndex + 1 + expansion, + ) + addWindow(startIndex, endIndex, false) + } + + return windows + } + + private getRemainingPostFinalCompositeIterations(): number { + return Math.max( + 0, + MAX_POST_FINAL_COMPOSITE_IJUMP_ITERATIONS - + this.postFinalCompositeIterations, + ) + } + + private tryPostFinalCompositeCandidate( + routes: HighDensityRoute[], + snapshot: DrcSnapshot, + options: Omit, + terminalRooted: boolean, + ): + | { + routes: HighDensityRoute[] + snapshot: DrcSnapshot + } + | undefined { + const iterationLimit = Math.min( + MAX_POST_FINAL_COMPOSITE_IJUMP_ITERATIONS_PER_ATTEMPT, + this.getRemainingPostFinalCompositeIterations(), + ) + if ( + iterationLimit <= 0 || + this.postFinalCompositeAttempts >= MAX_POST_FINAL_COMPOSITE_ATTEMPTS || + this.postFinalCompositeDrcEvaluations >= + MAX_POST_FINAL_COMPOSITE_DRC_EVALUATIONS + ) { + return undefined + } + + this.postFinalCompositeAttempts += 1 + if (options.reverse) this.postFinalCompositeReverseAttempts += 1 + else this.postFinalCompositeForwardAttempts += 1 + if (terminalRooted) this.postFinalCompositeTerminalRootedAttempts += 1 + + const result = this.ijumpRerouter.tryReroute(routes, { + ...options, + maxIterations: iterationLimit, + }) + this.postFinalCompositeIterations += Math.min( + iterationLimit, + Math.max(0, result?.iterations ?? 0), + ) + if (!result?.route || !routeHasValidLayerTransitions(result.route)) { + return undefined + } + + const candidateRoutes = cloneRoutes(routes) + candidateRoutes[options.routeIndex] = result.route + const rawCandidate = materializeRoutes(candidateRoutes) + if ( + rawCandidate.some((route) => !routeHasValidLayerTransitions(route)) || + !this.candidatePreservesTerminals(rawCandidate) + ) { + return undefined + } + + this.postFinalCompositeDrcEvaluations += 1 + this.cleanupCandidateAttempts += 1 + const rawCandidateSnapshot = this.getSnapshot(rawCandidate) + if (rawCandidateSnapshot.count < snapshot.count) { + this.postFinalCompositeCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + return { routes: rawCandidate, snapshot: rawCandidateSnapshot } + } + if ( + this.postFinalCompositeDrcEvaluations >= + MAX_POST_FINAL_COMPOSITE_DRC_EVALUATIONS + ) { + return undefined + } + + const remainingViaMergeIterations = + MAX_POST_FINAL_COMPOSITE_SAME_NET_VIA_MERGER_ITERATIONS - + this.postFinalCompositeSameNetViaMergeIterations + if (remainingViaMergeIterations <= 0) return undefined + const mergeResult = this.tryScopedSameNetViaMerge( + rawCandidate, + options.routeIndex, + Math.min( + MAX_POST_FINAL_COMPOSITE_SAME_NET_VIA_MERGER_ITERATIONS_PER_ATTEMPT, + remainingViaMergeIterations, + ), + ) + this.postFinalCompositeSameNetViaMergeIterations += mergeResult.iterations + const atomicCandidate = mergeResult.routes + if (!atomicCandidate) return undefined + + if ( + atomicCandidate.some((route) => !routeHasValidLayerTransitions(route)) || + !this.candidatePreservesTerminals(atomicCandidate) + ) { + return undefined + } + + this.postFinalCompositeDrcEvaluations += 1 + this.cleanupCandidateAttempts += 1 + const atomicCandidateSnapshot = this.getSnapshot(atomicCandidate) + if (atomicCandidateSnapshot.count >= snapshot.count) return undefined + + this.postFinalCompositeCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + return { routes: atomicCandidate, snapshot: atomicCandidateSnapshot } + } + + private runPostFinalCompositeRepair( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let snapshot = this.getSnapshot(improvedRoutes) + + repairLoop: while ( + snapshot.count > 0 && + this.getRemainingPostFinalCompositeIterations() > 0 && + this.postFinalCompositeAttempts < MAX_POST_FINAL_COMPOSITE_ATTEMPTS && + this.postFinalCompositeDrcEvaluations < + MAX_POST_FINAL_COMPOSITE_DRC_EVALUATIONS + ) { + const errors = snapshot.errors + .map((error, errorIndex) => ({ error, errorIndex })) + .toSorted( + (left, right) => + Number(!getPhysicalPadIdFromError(left.error)) - + Number(!getPhysicalPadIdFromError(right.error)) || + left.errorIndex - right.errorIndex, + ) + + for (const { error } of errors) { + const center = this.getErrorCenter(error) + if (!center) continue + for (const routeIndex of this.getCandidateRouteIndexesForError( + error, + snapshot, + )) { + const route = improvedRoutes[routeIndex] + if (!route) continue + for (const window of this.getPostFinalCompositeWindows( + route, + center, + )) { + // Keep both search directions adjacent so neither is starved by + // later windows or owners. + for (const reverse of [false, true]) { + const candidate = this.tryPostFinalCompositeCandidate( + improvedRoutes, + snapshot, + { + routeIndex, + startIndex: window.startIndex, + endIndex: window.endIndex, + includeCandidateCopper: true, + reverse, + shortenPath: false, + }, + window.terminalRooted, + ) + if (!candidate) { + if ( + this.getRemainingPostFinalCompositeIterations() <= 0 || + this.postFinalCompositeAttempts >= + MAX_POST_FINAL_COMPOSITE_ATTEMPTS || + this.postFinalCompositeDrcEvaluations >= + MAX_POST_FINAL_COMPOSITE_DRC_EVALUATIONS + ) { + break repairLoop + } + continue + } + + improvedRoutes = candidate.routes + snapshot = candidate.snapshot + continue repairLoop + } + } + } + } + + break + } + + return improvedRoutes + } + + private getDrcErrorIdentity(error: DrcError): string { + for (const idKey of [ + "pcb_trace_error_id", + "pcb_via_trace_clearance_error_id", + "pcb_pad_trace_clearance_error_id", + "pcb_via_clearance_error_id", + "pcb_error_id", + ]) { + const id = error[idKey] + if (typeof id === "string") return `${getErrorType(error)}:${id}` + } + + const center = this.getErrorCenter(error) + return JSON.stringify([ + getErrorType(error), + error.pcb_trace_id, + error.pcb_via_id, + getCandidateTraceIdsFromError(error).toSorted(), + center?.x, + center?.y, + error.message, + ]) + } + + private getFixedCopperCompositePlans( + snapshot: DrcSnapshot, + ): FixedCopperCompositePlan[] { + if ( + snapshot.count === 0 || + snapshot.count > MAX_FIXED_COPPER_COMPOSITE_RESIDUAL + ) { + return [] + } + + const plans: FixedCopperCompositePlan[] = [] + const seenRouteIndexes = new Set() + for (const error of snapshot.errors) { + if ( + getErrorType(error) !== "pcb_trace_error" || + typeof error.pcb_trace_error_id !== "string" || + !error.pcb_trace_error_id.startsWith("overlap_") + ) { + continue + } + + const referencedTraceIds = [ + typeof error.pcb_trace_id === "string" ? error.pcb_trace_id : undefined, + getRawOtherTraceId(error), + ].filter((traceId): traceId is string => Boolean(traceId)) + const referencesFixedCopper = referencedTraceIds.some( + (traceId) => !snapshot.traceRouteIndexById.has(traceId), + ) + const candidateRouteIndexes = this.getCandidateRouteIndexesForError( + error, + snapshot, + ) + if ( + !referencesFixedCopper || + candidateRouteIndexes.length !== 1 || + seenRouteIndexes.has(candidateRouteIndexes[0]!) + ) { + continue + } + + const routeIndex = candidateRouteIndexes[0]! + seenRouteIndexes.add(routeIndex) + plans.push({ + routeIndex, + targetErrorIdentity: this.getDrcErrorIdentity(error), + }) + } + + return plans + } + + private getRemainingFixedCopperCompositeIterations(): number { + return Math.max( + 0, + MAX_FIXED_COPPER_COMPOSITE_ITERATIONS - + this.fixedCopperCompositeIterations, + ) + } + + private evaluateFixedCopperCompositeCandidate( + routes: HighDensityRoute[], + ): DrcSnapshot | undefined { + if ( + this.fixedCopperCompositeDrcEvaluations >= + MAX_FIXED_COPPER_COMPOSITE_DRC_EVALUATIONS + ) { + return undefined + } + this.fixedCopperCompositeDrcEvaluations += 1 + this.cleanupCandidateAttempts += 1 + return this.getSnapshot(routes) + } + + private getNewlyExposedFixedCopperCompositeOwners( + snapshot: DrcSnapshot, + baselineErrorIdentities: ReadonlySet, + primaryRouteIndex: number, + ): number[] { + const degreeByRouteIndex = new Map() + for (const error of snapshot.errors) { + if (baselineErrorIdentities.has(this.getDrcErrorIdentity(error))) { + continue + } + for (const routeIndex of this.getCandidateRouteIndexesForError( + error, + snapshot, + )) { + if (routeIndex === primaryRouteIndex) continue + degreeByRouteIndex.set( + routeIndex, + (degreeByRouteIndex.get(routeIndex) ?? 0) + 1, + ) + } + } + + return [...degreeByRouteIndex.keys()] + .toSorted( + (left, right) => + (degreeByRouteIndex.get(right) ?? 0) - + (degreeByRouteIndex.get(left) ?? 0) || left - right, + ) + .slice(0, MAX_FIXED_COPPER_COMPOSITE_FOLLOWUP_OWNERS) + } + + private acceptFixedCopperCompositeCandidate( + routes: HighDensityRoute[], + snapshot: DrcSnapshot, + ): { routes: HighDensityRoute[]; snapshot: DrcSnapshot } { + this.fixedCopperCompositeCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + return { routes, snapshot } + } + + private tryFixedCopperCompositePlan( + routes: HighDensityRoute[], + baselineSnapshot: DrcSnapshot, + plan: FixedCopperCompositePlan, + ): + | { + routes: HighDensityRoute[] + snapshot: DrcSnapshot + } + | undefined { + const baselineErrorIdentities = new Set( + baselineSnapshot.errors.map((error) => this.getDrcErrorIdentity(error)), + ) + + for (const primaryVariant of FIXED_COPPER_COMPOSITE_PRIMARY_VARIANTS) { + const primaryIterationLimit = Math.min( + MAX_FIXED_COPPER_COMPOSITE_ITERATIONS_PER_ATTEMPT, + this.getRemainingFixedCopperCompositeIterations(), + ) + if ( + primaryIterationLimit <= 0 || + this.fixedCopperCompositePrimaryAttempts >= + MAX_FIXED_COPPER_COMPOSITE_PRIMARY_ATTEMPTS || + this.fixedCopperCompositeDrcEvaluations >= + MAX_FIXED_COPPER_COMPOSITE_DRC_EVALUATIONS + ) { + return undefined + } + + this.fixedCopperCompositePrimaryAttempts += 1 + const primaryResult = this.ijumpRerouter.tryReroute(routes, { + routeIndex: plan.routeIndex, + includeCandidateCopper: false, + ...primaryVariant, + maxIterations: primaryIterationLimit, + }) + this.fixedCopperCompositeIterations += Math.min( + primaryIterationLimit, + Math.max(0, primaryResult?.iterations ?? 0), + ) + if ( + !primaryResult?.route || + !routeHasValidLayerTransitions(primaryResult.route) + ) { + continue + } + + const primaryRoutes = cloneRoutes(routes) + primaryRoutes[plan.routeIndex] = primaryResult.route + const materializedPrimary = materializeRoutes(primaryRoutes) + if ( + materializedPrimary.some( + (route) => !routeHasValidLayerTransitions(route), + ) || + !this.candidatePreservesTerminals(materializedPrimary) + ) { + continue + } + + const primarySnapshot = + this.evaluateFixedCopperCompositeCandidate(materializedPrimary) + if (!primarySnapshot) return undefined + if (primarySnapshot.count < baselineSnapshot.count) { + return this.acceptFixedCopperCompositeCandidate( + materializedPrimary, + primarySnapshot, + ) + } + if ( + primarySnapshot.count > MAX_FIXED_COPPER_COMPOSITE_EXPOSED_ISSUES || + primarySnapshot.errors.some( + (error) => + this.getDrcErrorIdentity(error) === plan.targetErrorIdentity, + ) + ) { + continue + } + + let workingRoutes = materializedPrimary + let workingSnapshot = primarySnapshot + const exposedOwnerRouteIndexes = + this.getNewlyExposedFixedCopperCompositeOwners( + workingSnapshot, + baselineErrorIdentities, + plan.routeIndex, + ) + if (exposedOwnerRouteIndexes.length === 0) continue + + for (const ownerRouteIndex of exposedOwnerRouteIndexes) { + let bestOwnerCandidate: + | { + routes: HighDensityRoute[] + snapshot: DrcSnapshot + } + | undefined + + for (const followupVariant of FINAL_OWNER_FULL_VARIANTS) { + const followupIterationLimit = Math.min( + MAX_FIXED_COPPER_COMPOSITE_ITERATIONS_PER_ATTEMPT, + this.getRemainingFixedCopperCompositeIterations(), + ) + if ( + followupIterationLimit <= 0 || + this.fixedCopperCompositeFollowupAttempts >= + MAX_FIXED_COPPER_COMPOSITE_FOLLOWUP_ATTEMPTS || + this.fixedCopperCompositeDrcEvaluations >= + MAX_FIXED_COPPER_COMPOSITE_DRC_EVALUATIONS + ) { + break + } + + this.fixedCopperCompositeFollowupAttempts += 1 + const followupResult = this.ijumpRerouter.tryReroute(workingRoutes, { + routeIndex: ownerRouteIndex, + includeCandidateCopper: true, + ...followupVariant, + maxIterations: followupIterationLimit, + }) + this.fixedCopperCompositeIterations += Math.min( + followupIterationLimit, + Math.max(0, followupResult?.iterations ?? 0), + ) + if ( + !followupResult?.route || + !routeHasValidLayerTransitions(followupResult.route) + ) { + continue + } + + const followupRoutes = cloneRoutes(workingRoutes) + followupRoutes[ownerRouteIndex] = followupResult.route + const materializedFollowup = materializeRoutes(followupRoutes) + if ( + materializedFollowup.some( + (route) => !routeHasValidLayerTransitions(route), + ) || + !this.candidatePreservesTerminals(materializedFollowup) + ) { + continue + } + + const followupSnapshot = + this.evaluateFixedCopperCompositeCandidate(materializedFollowup) + if (!followupSnapshot) break + if (followupSnapshot.count < baselineSnapshot.count) { + return this.acceptFixedCopperCompositeCandidate( + materializedFollowup, + followupSnapshot, + ) + } + if ( + followupSnapshot.count < workingSnapshot.count && + (!bestOwnerCandidate || + followupSnapshot.count < bestOwnerCandidate.snapshot.count) + ) { + bestOwnerCandidate = { + routes: materializedFollowup, + snapshot: followupSnapshot, + } + } + } + + if (!bestOwnerCandidate) continue + workingRoutes = bestOwnerCandidate.routes + workingSnapshot = bestOwnerCandidate.snapshot + } + + if (workingSnapshot.count < baselineSnapshot.count) { + return this.acceptFixedCopperCompositeCandidate( + workingRoutes, + workingSnapshot, + ) + } + } + + return undefined + } + + private runFixedCopperCompositeRepair( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let snapshot = this.getSnapshot(improvedRoutes) + + while ( + snapshot.count > 0 && + snapshot.count <= MAX_FIXED_COPPER_COMPOSITE_RESIDUAL && + this.getRemainingFixedCopperCompositeIterations() > 0 && + this.fixedCopperCompositePrimaryAttempts < + MAX_FIXED_COPPER_COMPOSITE_PRIMARY_ATTEMPTS && + this.fixedCopperCompositeDrcEvaluations < + MAX_FIXED_COPPER_COMPOSITE_DRC_EVALUATIONS + ) { + let acceptedCandidate: + | { + routes: HighDensityRoute[] + snapshot: DrcSnapshot + } + | undefined + for (const plan of this.getFixedCopperCompositePlans(snapshot)) { + acceptedCandidate = this.tryFixedCopperCompositePlan( + improvedRoutes, + snapshot, + plan, + ) + if (acceptedCandidate) break + } + if (!acceptedCandidate) break + + improvedRoutes = acceptedCandidate.routes + snapshot = acceptedCandidate.snapshot + } + + return improvedRoutes + } + + private getAtomicEndpointSlideBranches( + routes: HighDensityRoute[], + routeIndex: number, + endpoint: "start" | "end", + ): EndpointSlideBranch[] { + const anchorConstraint = this.terminalConstraints.find( + (constraint) => + constraint.routeIndex === routeIndex && + constraint.endpoint === endpoint, + ) + if (!anchorConstraint) return [] + + const terminalPortId = anchorConstraint.originalPoint.pcb_port_id + const constraints = + typeof terminalPortId === "string" + ? this.terminalConstraints.filter( + (constraint) => + constraint.originalPoint.pcb_port_id === terminalPortId, + ) + : [anchorConstraint] + const branches: EndpointSlideBranch[] = [] + const seenBranches = new Set() + + for (const constraint of constraints) { + const branchKey = `${constraint.routeIndex}:${constraint.endpoint}` + if (seenBranches.has(branchKey)) continue + seenBranches.add(branchKey) + const route = routes[constraint.routeIndex] + if (!route) continue + const endpointIndex = + constraint.endpoint === "start" ? 0 : route.route.length - 1 + if (!route.route[endpointIndex]) continue + branches.push({ + routeIndex: constraint.routeIndex, + endpoint: constraint.endpoint, + endpointIndex, + coincidentIndexes: getCoincidentTerminalPointIndexes( + route, + endpointIndex, + ), + constraint, + }) + } + + return branches + } + + private tryFinalEndpointSlideCandidate( + routes: HighDensityRoute[], + snapshot: DrcSnapshot, + error: DrcError, + ): + | { + routes: HighDensityRoute[] + snapshot: DrcSnapshot + } + | undefined { + if ( + this.finalEndpointSlideDrcEvaluations >= + MAX_FINAL_ENDPOINT_SLIDE_DRC_EVALUATIONS + ) { + return undefined + } + const errorType = getErrorType(error) + const padId = getPhysicalPadIdFromError(error) + if ( + !padId || + (errorType !== "pcb_pad_trace_clearance_error" && + errorType !== "pcb_trace_error") + ) { + return undefined + } + const foreignObstacle = this.originalObstacles.find((obstacle) => + obstacleRepresentsPhysicalPad(obstacle, padId), + ) + if (!foreignObstacle) return undefined + + for (const routeIndex of this.getCandidateRouteIndexesForError( + error, + snapshot, + )) { + const route = routes[routeIndex] + if (!route || route.route.length < 2) continue + const endpoints = (["start", "end"] as const).toSorted((left, right) => { + const leftPoint = + left === "start" ? route.route[0]! : route.route.at(-1)! + const rightPoint = + right === "start" ? route.route[0]! : route.route.at(-1)! + return ( + getPointDistance(leftPoint, foreignObstacle.center) - + getPointDistance(rightPoint, foreignObstacle.center) + ) + }) + + for (const endpoint of endpoints) { + const endpointPoint = + endpoint === "start" ? route.route[0] : route.route.at(-1) + if (!endpointPoint) continue + const branches = this.getAtomicEndpointSlideBranches( + routes, + routeIndex, + endpoint, + ) + if (branches.length === 0) continue + + const preferredDirection = getUnitDirection( + endpointPoint.x - foreignObstacle.center.x, + endpointPoint.y - foreignObstacle.center.y, + ) + for (const direction of getMicroShiftDirections(preferredDirection)) { + for (const radius of ENDPOINT_SLIDE_RADII) { + if ( + this.finalEndpointSlideDrcEvaluations >= + MAX_FINAL_ENDPOINT_SLIDE_DRC_EVALUATIONS + ) { + return undefined + } + const candidatePoint = { + x: endpointPoint.x + direction.x * radius, + y: endpointPoint.y + direction.y * radius, + } + const everyBranchFitsOwningPad = branches.every((branch) => { + const branchRoute = routes[branch.routeIndex] + const branchEndpoint = + branch.endpoint === "start" + ? branchRoute?.route[0] + : branchRoute?.route.at(-1) + return Boolean( + branchEndpoint && + branch.constraint.owningObstacles.some( + (obstacle) => + obstacleAppliesToLayer( + obstacle, + branchEndpoint.z, + this.params.srj.layerCount, + ) && + pointFitsInsideObstacle( + candidatePoint, + obstacle, + branch.constraint.traceRadius, + ), + ), + ) + }) + if (!everyBranchFitsOwningPad) continue + + const candidateRoutes = cloneRoutes(routes) + for (const branch of branches) { + const candidateRoute = candidateRoutes[branch.routeIndex] + if (!candidateRoute) continue + for (const pointIndex of branch.coincidentIndexes) { + const point = candidateRoute.route[pointIndex] + if (!point) continue + point.x = candidatePoint.x + point.y = candidatePoint.y + } + } + const materializedCandidate = materializeRoutes(candidateRoutes) + if ( + materializedCandidate.some( + (candidateRoute) => + !routeHasValidLayerTransitions(candidateRoute), + ) || + !this.candidatePreservesTerminals(materializedCandidate) + ) { + continue + } + + this.finalEndpointSlideAttempts += 1 + this.finalEndpointSlideDrcEvaluations += 1 + this.cleanupCandidateAttempts += 1 + const candidateSnapshot = this.getSnapshot(materializedCandidate) + if (candidateSnapshot.count >= snapshot.count) continue + + this.finalEndpointSlideCandidatesAccepted += 1 + this.finalEndpointSlideRelocatedBranches += branches.length + this.cleanupCandidatesAccepted += 1 + return { + routes: materializedCandidate, + snapshot: candidateSnapshot, + } + } + } + } + } + + return undefined + } + + private runFinalEndpointSlideCleanup( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let snapshot = this.getSnapshot(improvedRoutes) + + while ( + snapshot.count > 0 && + this.finalEndpointSlideDrcEvaluations < + MAX_FINAL_ENDPOINT_SLIDE_DRC_EVALUATIONS + ) { + let acceptedCandidate: + | { + routes: HighDensityRoute[] + snapshot: DrcSnapshot + } + | undefined + for (const error of snapshot.errors) { + acceptedCandidate = this.tryFinalEndpointSlideCandidate( + improvedRoutes, + snapshot, + error, + ) + if (acceptedCandidate) break + if ( + this.finalEndpointSlideDrcEvaluations >= + MAX_FINAL_ENDPOINT_SLIDE_DRC_EVALUATIONS + ) { + break + } + } + if (!acceptedCandidate) break + improvedRoutes = acceptedCandidate.routes + snapshot = acceptedCandidate.snapshot + } + + return improvedRoutes + } + + private getCombinedMissingConnectionCanonicalNet( + error: DrcError, + ): string | undefined { + if ( + getErrorType(error) !== "pcb_trace_error" || + typeof error.pcb_trace_error_id !== "string" || + !error.pcb_trace_error_id.startsWith("missing_connection_combined_") || + typeof error.source_trace_id !== "string" + ) { + return undefined + } + return ( + this.params.connMap?.getNetConnectedToId(error.source_trace_id) ?? + error.source_trace_id + ) + } + + private getPreloadedCopperLayersAtPoint( + canonicalNet: string, + point: { x: number; y: number }, + ): number[] { + const zLayers = new Set() + const addLayer = (layer: string) => { + const z = mapLayerNameToZ(layer, this.params.srj.layerCount) + if (Number.isInteger(z) && z >= 0 && z < this.params.srj.layerCount) { + zLayers.add(z) + } + } + for (const trace of this.params.srj.traces ?? []) { + const traceCanonicalNet = + this.params.connMap?.getNetConnectedToId(trace.connection_name) ?? + trace.connection_name + if (traceCanonicalNet !== canonicalNet) continue + + for ( + let pointIndex = 0; + pointIndex < trace.route.length; + pointIndex += 1 + ) { + const routePoint = trace.route[pointIndex]! + const nextPoint = trace.route[pointIndex + 1] + if (routePoint.route_type === "wire") { + if ( + getPointDistance(point, routePoint) <= + PRELOADED_TERMINAL_MATCH_TOLERANCE + ) { + addLayer(routePoint.layer) + } + if ( + nextPoint?.route_type === "wire" && + nextPoint.layer === routePoint.layer && + getPointToSegmentDistance(point, routePoint, nextPoint) <= + PRELOADED_TERMINAL_MATCH_TOLERANCE + ) { + addLayer(routePoint.layer) + } + } + } + } + + return [...zLayers].sort((left, right) => left - right) + } + + private routeAlreadyHasTransitionAtPoint( + route: HighDensityRoute, + point: { x: number; y: number }, + ): boolean { + if ( + route.vias.some( + (via) => + getPointDistance(via, point) <= PRELOADED_TERMINAL_MATCH_TOLERANCE, + ) + ) { + return true + } + return route.route.some((routePoint, pointIndex) => { + const nextPoint = route.route[pointIndex + 1] + return Boolean( + nextPoint && + routePoint.z !== nextPoint.z && + getPointDistance(routePoint, nextPoint) <= POSITION_EPSILON && + getPointDistance(routePoint, point) <= + PRELOADED_TERMINAL_MATCH_TOLERANCE, + ) + }) + } + + private getFinalContinuityTerminalViaCandidates( + routes: HighDensityRoute[], + snapshot: DrcSnapshot, + error: DrcError, + ): FinalContinuityTerminalViaCandidate[] { + const canonicalNet = this.getCombinedMissingConnectionCanonicalNet(error) + if (!canonicalNet) return [] + const errorCenter = this.getErrorCenter(error) + const candidates: FinalContinuityTerminalViaCandidate[] = [] + + for (const routeIndex of this.getCandidateRouteIndexesForError( + error, + snapshot, + )) { + const route = routes[routeIndex] + const routeCanonicalNet = + (route && this.getCanonicalNetForRoute(route)) ?? + route?.rootConnectionName ?? + route?.connectionName + if (!route || routeCanonicalNet !== canonicalNet) continue + + for (const endpoint of ["start", "end"] as const) { + const constraint = this.terminalConstraints.find( + (candidateConstraint) => + candidateConstraint.routeIndex === routeIndex && + candidateConstraint.endpoint === endpoint && + typeof candidateConstraint.originalPoint.pcb_port_id === "string", + ) + const terminal = + endpoint === "start" ? route.route[0] : route.route.at(-1) + if ( + !constraint || + !terminal || + terminal.z !== constraint.originalPoint.z || + this.routeAlreadyHasTransitionAtPoint(route, terminal) || + !this.terminalCanHostRelocatedVia(routes, routeIndex, endpoint) + ) { + continue + } + + for (const targetZ of this.getPreloadedCopperLayersAtPoint( + canonicalNet, + terminal, + )) { + if (targetZ === terminal.z) continue + candidates.push({ + routeIndex, + endpoint, + targetZ, + distanceToError: errorCenter + ? getPointDistance(terminal, errorCenter) + : 0, + }) + } + } + } + + return candidates.sort( + (left, right) => + left.distanceToError - right.distanceToError || + left.routeIndex - right.routeIndex || + (left.endpoint === right.endpoint + ? Math.abs(left.targetZ) - Math.abs(right.targetZ) + : left.endpoint === "start" + ? -1 + : 1), + ) + } + + private addFinalContinuityTerminalViaStub( + route: HighDensityRoute, + endpoint: "start" | "end", + targetZ: number, + ): HighDensityRoute | undefined { + const terminal = endpoint === "start" ? route.route[0] : route.route.at(-1) + if ( + !terminal || + targetZ === terminal.z || + targetZ < 0 || + targetZ >= this.params.srj.layerCount || + this.routeAlreadyHasTransitionAtPoint(route, terminal) + ) { + return undefined + } + + const { pcb_port_id: _pcbPortId, ...terminalWithoutIdentity } = terminal + const targetLayerPoint = { + ...terminalWithoutIdentity, + z: targetZ, + } + const routePoints = + endpoint === "start" + ? [ + { ...terminal }, + targetLayerPoint, + { ...terminalWithoutIdentity }, + ...route.route.slice(1), + ] + : [ + ...route.route.slice(0, -1), + { ...terminalWithoutIdentity }, + targetLayerPoint, + { ...terminal }, + ] + const materializedRoute = materializeRoutes([ + { ...route, route: routePoints }, + ])[0] + if ( + !materializedRoute || + !routeHasValidLayerTransitions(materializedRoute) + ) { + return undefined + } + + const transitionCountAtTerminal = materializedRoute.route.filter( + (routePoint, pointIndex) => { + const nextPoint = materializedRoute.route[pointIndex + 1] + return Boolean( + nextPoint && + routePoint.z !== nextPoint.z && + getPointDistance(routePoint, nextPoint) <= POSITION_EPSILON && + getPointDistance(routePoint, terminal) <= POSITION_EPSILON, + ) + }, + ).length + const viaCountAtTerminal = materializedRoute.vias.filter( + (via) => + getPointDistance(via, terminal) <= PRELOADED_TERMINAL_MATCH_TOLERANCE, + ).length + if (transitionCountAtTerminal !== 2 || viaCountAtTerminal !== 1) { + return undefined + } + return materializedRoute + } + + private runFinalContinuityTerminalViaBridge( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let snapshot = this.getSnapshot(improvedRoutes) + + while ( + snapshot.count > 0 && + this.finalContinuityTerminalViaAttempts < + MAX_FINAL_CONTINUITY_TERMINAL_VIA_ATTEMPTS && + this.finalContinuityTerminalViaDrcEvaluations < + MAX_FINAL_CONTINUITY_TERMINAL_VIA_DRC_EVALUATIONS + ) { + let acceptedCandidate: + | { + routes: HighDensityRoute[] + snapshot: DrcSnapshot + } + | undefined + + errorLoop: for (const error of snapshot.errors) { + for (const candidate of this.getFinalContinuityTerminalViaCandidates( + improvedRoutes, + snapshot, + error, + )) { + if ( + this.finalContinuityTerminalViaAttempts >= + MAX_FINAL_CONTINUITY_TERMINAL_VIA_ATTEMPTS || + this.finalContinuityTerminalViaDrcEvaluations >= + MAX_FINAL_CONTINUITY_TERMINAL_VIA_DRC_EVALUATIONS + ) { + break errorLoop + } + this.finalContinuityTerminalViaAttempts += 1 + + const candidateRoutes = cloneRoutes(improvedRoutes) + const bridgedRoute = this.addFinalContinuityTerminalViaStub( + candidateRoutes[candidate.routeIndex]!, + candidate.endpoint, + candidate.targetZ, + ) + if (!bridgedRoute) continue + candidateRoutes[candidate.routeIndex] = bridgedRoute + const materializedCandidate = materializeRoutes(candidateRoutes) + if ( + materializedCandidate.some( + (candidateRoute) => + !routeHasValidLayerTransitions(candidateRoute), + ) || + !this.candidatePreservesTerminals(materializedCandidate) + ) { + continue + } + + this.finalContinuityTerminalViaDrcEvaluations += 1 + this.cleanupCandidateAttempts += 1 + const candidateSnapshot = this.getSnapshot(materializedCandidate) + if (candidateSnapshot.count >= snapshot.count) continue + + this.finalContinuityTerminalViaCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + acceptedCandidate = { + routes: materializedCandidate, + snapshot: candidateSnapshot, + } + break errorLoop + } + } + + if (!acceptedCandidate) break + improvedRoutes = acceptedCandidate.routes + snapshot = acceptedCandidate.snapshot + } + + return improvedRoutes + } + + private runPipeline9Cleanup(routes: HighDensityRoute[]): HighDensityRoute[] { + this.selectAdaptiveCleanupLimits() + let improvedRoutes = this.unlockCleanupTerminals(routes) + improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) + + for (let pass = 0; pass < MAX_CLEANUP_PASSES; pass += 1) { + const snapshot = this.getSnapshot(improvedRoutes) + if (snapshot.count === 0) break + + let nextRoutes: HighDensityRoute[] | undefined + for (const error of snapshot.errors) { + if (!this.hasLocalCleanupBudget()) break + nextRoutes = this.tryEndpointSlide(improvedRoutes, error) + if (nextRoutes) break + } + if (!nextRoutes) { + for (const error of snapshot.errors) { + if (!this.hasLocalCleanupBudget()) break + nextRoutes = this.tryLocalTraceLayerDetour(improvedRoutes, error) + if (nextRoutes) break + } + } + if (!nextRoutes) { + for (const error of snapshot.errors) { + if (!this.hasLocalCleanupBudget()) break + nextRoutes = this.tryBatchedTraceForce(improvedRoutes, error) + if (nextRoutes) break + } + } + if (!nextRoutes) break + improvedRoutes = nextRoutes + } + improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) + + for (let round = 0; round < MAX_IJUMP_PHASE_ROUNDS; round += 1) { + const issueCountBeforeRound = this.getSnapshot(improvedRoutes).count + if (issueCountBeforeRound === 0) break + + let issueCountBeforePhase = issueCountBeforeRound + improvedRoutes = this.runIjumpFullRouteCleanup(improvedRoutes) + let issueCountAfterPhase = this.getSnapshot(improvedRoutes).count + if (issueCountAfterPhase < issueCountBeforePhase) { + improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) + } + + issueCountBeforePhase = this.getSnapshot(improvedRoutes).count + improvedRoutes = this.runIjumpInteriorCleanup(improvedRoutes) + issueCountAfterPhase = this.getSnapshot(improvedRoutes).count + if (issueCountAfterPhase < issueCountBeforePhase) { + improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) + } + + issueCountBeforePhase = this.getSnapshot(improvedRoutes).count + improvedRoutes = this.runIjumpFixedOnlyCleanup(improvedRoutes) + issueCountAfterPhase = this.getSnapshot(improvedRoutes).count + if (issueCountAfterPhase < issueCountBeforePhase) { + improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) + } + + if (this.getSnapshot(improvedRoutes).count >= issueCountBeforeRound) { + break + } + } + + improvedRoutes = this.runIjumpErrorOwnedClusterRebuildPasses(improvedRoutes) + improvedRoutes = this.runPostClusterViaMicroShiftCleanup(improvedRoutes) + improvedRoutes = this.runIjumpFinalErrorOwnerSweep(improvedRoutes) + improvedRoutes = this.runPostRepairSameNetViaMerge(improvedRoutes) + improvedRoutes = this.runSharedTerminalCompositeRepair(improvedRoutes) + improvedRoutes = this.runPostFinalCompositeRepair(improvedRoutes) + improvedRoutes = this.runFixedCopperCompositeRepair(improvedRoutes) + improvedRoutes = this.runFinalEndpointSlideCleanup(improvedRoutes) + improvedRoutes = this.runFinalContinuityTerminalViaBridge(improvedRoutes) + return this.restoreTerminalIds(improvedRoutes) } @@ -636,6 +3994,124 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve finalDrcIssueCount: finalSnapshot.count, pipeline9DrcCleanupCandidateAttempts: this.cleanupCandidateAttempts, pipeline9DrcCleanupCandidatesAccepted: this.cleanupCandidatesAccepted, + pipeline9LocalCleanupDrcEvaluations: this.localCleanupDrcEvaluations, + pipeline9SelectedLocalCleanupDrcEvaluationLimit: + this.selectedLocalCleanupDrcEvaluationLimit, + pipeline9ConsecutiveLocalCleanupDrcMisses: + this.consecutiveLocalCleanupDrcMisses, + pipeline9MaxConsecutiveLocalCleanupDrcMisses: + this.maxConsecutiveLocalCleanupDrcMisses, + pipeline9SelectedConsecutiveLocalCleanupDrcMissLimit: + this.selectedConsecutiveLocalCleanupDrcMissLimit, + pipeline9ViaMicroShiftAttempts: this.viaMicroShiftAttempts, + pipeline9ViaMicroShiftsAccepted: this.viaMicroShiftsAccepted, + pipeline9IjumpFullAttempts: this.ijumpFullAttempts, + pipeline9IjumpInteriorAttempts: this.ijumpInteriorAttempts, + pipeline9IjumpFixedOnlyAttempts: this.ijumpFixedOnlyAttempts, + pipeline9IjumpCandidatesAccepted: this.ijumpCandidatesAccepted, + pipeline9IjumpIterations: this.ijumpIterations, + pipeline9SelectedIjumpIterationLimit: this.selectedIjumpIterationLimit, + pipeline9ErrorOwnedClusterOrderAttempts: + this.errorOwnedClusterOrderAttempts, + pipeline9ErrorOwnedClusterRouteAttempts: + this.errorOwnedClusterRouteAttempts, + pipeline9ErrorOwnedClusterDrcEvaluations: + this.errorOwnedClusterDrcEvaluations, + pipeline9ErrorOwnedClusterIterations: this.errorOwnedClusterIterations, + pipeline9ErrorOwnedClusterAccepted: this.errorOwnedClusterAccepted, + pipeline9ErrorOwnedClusterTerminalEscapeAttempts: + this.errorOwnedClusterTerminalEscapeAttempts, + pipeline9ErrorOwnedClusterPostRouteAttempts: + this.errorOwnedClusterPostRouteAttempts, + pipeline9ErrorOwnedClusterPostCandidatesAccepted: + this.errorOwnedClusterPostCandidatesAccepted, + pipeline9PostClusterViaMicroShiftDrcEvaluations: + this.postClusterViaMicroShiftDrcEvaluations, + pipeline9FinalOwnerFullAttempts: this.finalOwnerFullAttempts, + pipeline9FinalOwnerInteriorAttempts: this.finalOwnerInteriorAttempts, + pipeline9FinalOwnerDrcEvaluations: this.finalOwnerDrcEvaluations, + pipeline9FinalOwnerCandidatesAccepted: this.finalOwnerCandidatesAccepted, + pipeline9FinalOwnerIterations: this.finalOwnerIterations, + pipeline9FinalOwnerIterationLimit: MAX_FINAL_OWNER_IJUMP_ITERATIONS, + pipeline9PostRepairSameNetViaMergeAttempts: + this.postRepairSameNetViaMergeAttempts, + pipeline9PostRepairSameNetViaMergeDrcEvaluations: + this.postRepairSameNetViaMergeDrcEvaluations, + pipeline9PostRepairSameNetViaMergeCandidatesAccepted: + this.postRepairSameNetViaMergeCandidatesAccepted, + pipeline9PostRepairSameNetViaMergeIterations: + this.postRepairSameNetViaMergeIterations, + pipeline9PostRepairSameNetViaMergeIterationLimit: + MAX_POST_REPAIR_SAME_NET_VIA_MERGER_ITERATIONS, + pipeline9SharedTerminalCompositeAttempts: + this.sharedTerminalCompositeAttempts, + pipeline9SharedTerminalCompositeRelocatedBranches: + this.sharedTerminalCompositeRelocatedBranches, + pipeline9SharedTerminalCompositeIjumpAttempts: + this.sharedTerminalCompositeIjumpAttempts, + pipeline9SharedTerminalCompositeDrcEvaluations: + this.sharedTerminalCompositeDrcEvaluations, + pipeline9SharedTerminalCompositeCandidatesAccepted: + this.sharedTerminalCompositeCandidatesAccepted, + pipeline9SharedTerminalCompositeIterations: + this.sharedTerminalCompositeIterations, + pipeline9SharedTerminalCompositeIterationLimit: + MAX_SHARED_TERMINAL_COMPOSITE_IJUMP_ITERATIONS, + pipeline9PostFinalCompositeAttempts: this.postFinalCompositeAttempts, + pipeline9PostFinalCompositeForwardAttempts: + this.postFinalCompositeForwardAttempts, + pipeline9PostFinalCompositeReverseAttempts: + this.postFinalCompositeReverseAttempts, + pipeline9PostFinalCompositeTerminalRootedAttempts: + this.postFinalCompositeTerminalRootedAttempts, + pipeline9PostFinalCompositeDrcEvaluations: + this.postFinalCompositeDrcEvaluations, + pipeline9PostFinalCompositeCandidatesAccepted: + this.postFinalCompositeCandidatesAccepted, + pipeline9PostFinalCompositeIterations: this.postFinalCompositeIterations, + pipeline9PostFinalCompositeIterationLimit: + MAX_POST_FINAL_COMPOSITE_IJUMP_ITERATIONS, + pipeline9PostFinalCompositeSameNetViaMergeIterations: + this.postFinalCompositeSameNetViaMergeIterations, + pipeline9PostFinalCompositeSameNetViaMergeIterationLimit: + MAX_POST_FINAL_COMPOSITE_SAME_NET_VIA_MERGER_ITERATIONS, + pipeline9FixedCopperCompositePrimaryAttempts: + this.fixedCopperCompositePrimaryAttempts, + pipeline9FixedCopperCompositeFollowupAttempts: + this.fixedCopperCompositeFollowupAttempts, + pipeline9FixedCopperCompositeDrcEvaluations: + this.fixedCopperCompositeDrcEvaluations, + pipeline9FixedCopperCompositeCandidatesAccepted: + this.fixedCopperCompositeCandidatesAccepted, + pipeline9FixedCopperCompositeIterations: + this.fixedCopperCompositeIterations, + pipeline9FixedCopperCompositePrimaryAttemptLimit: + MAX_FIXED_COPPER_COMPOSITE_PRIMARY_ATTEMPTS, + pipeline9FixedCopperCompositeFollowupAttemptLimit: + MAX_FIXED_COPPER_COMPOSITE_FOLLOWUP_ATTEMPTS, + pipeline9FixedCopperCompositeDrcEvaluationLimit: + MAX_FIXED_COPPER_COMPOSITE_DRC_EVALUATIONS, + pipeline9FixedCopperCompositeIterationLimit: + MAX_FIXED_COPPER_COMPOSITE_ITERATIONS, + pipeline9FinalEndpointSlideAttempts: this.finalEndpointSlideAttempts, + pipeline9FinalEndpointSlideDrcEvaluations: + this.finalEndpointSlideDrcEvaluations, + pipeline9FinalEndpointSlideCandidatesAccepted: + this.finalEndpointSlideCandidatesAccepted, + pipeline9FinalEndpointSlideRelocatedBranches: + this.finalEndpointSlideRelocatedBranches, + pipeline9FinalEndpointSlideDrcEvaluationLimit: + MAX_FINAL_ENDPOINT_SLIDE_DRC_EVALUATIONS, + pipeline9FinalContinuityTerminalViaAttempts: + this.finalContinuityTerminalViaAttempts, + pipeline9FinalContinuityTerminalViaDrcEvaluations: + this.finalContinuityTerminalViaDrcEvaluations, + pipeline9FinalContinuityTerminalViaCandidatesAccepted: + this.finalContinuityTerminalViaCandidatesAccepted, + pipeline9FinalContinuityTerminalViaAttemptLimit: + MAX_FINAL_CONTINUITY_TERMINAL_VIA_ATTEMPTS, + pipeline9FinalContinuityTerminalViaDrcEvaluationLimit: + MAX_FINAL_CONTINUITY_TERMINAL_VIA_DRC_EVALUATIONS, } this.progress = 1 this.solved = true diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter.ts new file mode 100644 index 000000000..447505f1f --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter.ts @@ -0,0 +1,737 @@ +import { MultilayerIjump } from "@tscircuit/infgrid-ijump-astar" +import type { ConnectivityMap } from "circuit-json-to-connectivity-map" +import { isObstacleConnectedToRoute } from "lib/solvers/TraceWidthSolver/isObstacleConnectedToRoute" +import type { Obstacle, SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" +import type { HighDensityRoute } from "lib/types/high-density-types" +import { addApproximatingRectsToSrj } from "lib/utils/addApproximatingRectsToSrj" +import { convertHdRouteToSimplifiedRoute } from "lib/utils/convertHdRouteToSimplifiedRoute" +import { getObstaclesFromSrjTraces } from "lib/utils/convertSrjTracesToObstacles" +import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" +import { mapZToLayerName } from "lib/utils/mapZToLayerName" + +type Pipeline9IjumpRerouterParams = { + srj: SimpleRouteJson + baseObstacles: Obstacle[] + connMap?: ConnectivityMap + viaHoleDiameter?: number +} + +export type Pipeline9IjumpRerouteOptions = { + routeIndex: number + startIndex?: number + endIndex?: number + /** + * Candidate routes that are intentionally absent while a related cluster + * is rebuilt. Fixed/preloaded copper is never omitted. + */ + omitCandidateRouteIndexes?: ReadonlySet + includeCandidateCopper: boolean + reverse: boolean + shortenPath: boolean + maxIterations: number +} + +export type Pipeline9IjumpRerouteResult = { + route?: HighDensityRoute + iterations: number +} + +export type Pipeline9TerminalViaEscapeCandidate = { + alternateZ: number + startVia: { x: number; y: number } + endVia: { x: number; y: number } +} + +export type Pipeline9TerminalViaEscapeOptions = Omit< + Pipeline9IjumpRerouteOptions, + "startIndex" | "endIndex" +> & { + candidate: Pipeline9TerminalViaEscapeCandidate +} + +const SAME_POINT_EPSILON = 1e-9 +const RELAXED_TRACE_CLEARANCE = 0.1 +const IJUMP_GRID_STEP = 0.05 +const MAX_TERMINAL_VIA_ESCAPE_CANDIDATES = 64 + +const getFailedAttemptIterationCost = ( + solverIterations: number | undefined, + maxIterations: number, +): number => { + const boundedMaxIterations = Math.max(0, Math.floor(maxIterations)) + if (boundedMaxIterations === 0) return 0 + if (solverIterations === undefined || !Number.isFinite(solverIterations)) { + return boundedMaxIterations + } + + // MultilayerIjump starts at -1 and resets to zero only once a connection + // search begins. An exception during its connection setup must still consume + // budget, otherwise exact repair can retry a broken setup for free. + return Math.min( + boundedMaxIterations, + Math.max(1, Math.floor(solverIterations)), + ) +} + +const pointsAreEqual = ( + left: HighDensityRoute["route"][number] | undefined, + right: HighDensityRoute["route"][number], +) => + Boolean(left) && + Math.abs(left!.x - right.x) <= SAME_POINT_EPSILON && + Math.abs(left!.y - right.y) <= SAME_POINT_EPSILON && + left!.z === right.z + +const addCanonicalConnectivity = ( + obstacle: Obstacle, + connMap?: ConnectivityMap, +): Obstacle => { + if (!connMap) return obstacle + + const connectedTo = new Set(obstacle.connectedTo) + for (const connectionId of obstacle.connectedTo) { + const netId = connMap.getNetConnectedToId(connectionId) + if (netId) connectedTo.add(netId) + } + return { ...obstacle, connectedTo: [...connectedTo] } +} + +const reverseSimplifiedRoute = ( + route: SimplifiedPcbTrace["route"], +): SimplifiedPcbTrace["route"] => + route.toReversed().map((point) => + point.route_type === "via" + ? { + ...point, + from_layer: point.to_layer, + to_layer: point.from_layer, + } + : point, + ) + +const convertSimplifiedRouteToHdPoints = ( + simplifiedRoute: SimplifiedPcbTrace["route"], + layerCount: number, + traceThickness: number, +): HighDensityRoute["route"] | undefined => { + const points: HighDensityRoute["route"] = [] + const pushPoint = (point: HighDensityRoute["route"][number]) => { + if (!pointsAreEqual(points.at(-1), point)) points.push(point) + } + + for (const point of simplifiedRoute) { + if (point.route_type === "wire") { + const z = mapLayerNameToZ(point.layer, layerCount) + if ( + !Number.isFinite(point.x) || + !Number.isFinite(point.y) || + !Number.isInteger(z) || + z < 0 || + z >= layerCount + ) { + return undefined + } + pushPoint({ + x: point.x, + y: point.y, + z, + traceThickness, + }) + continue + } + + if (point.route_type === "via") { + const fromZ = mapLayerNameToZ(point.from_layer, layerCount) + const toZ = mapLayerNameToZ(point.to_layer, layerCount) + if ( + !Number.isFinite(point.x) || + !Number.isFinite(point.y) || + !Number.isInteger(fromZ) || + !Number.isInteger(toZ) || + fromZ < 0 || + fromZ >= layerCount || + toZ < 0 || + toZ >= layerCount + ) { + return undefined + } + pushPoint({ x: point.x, y: point.y, z: fromZ, traceThickness }) + pushPoint({ x: point.x, y: point.y, z: toZ, traceThickness }) + continue + } + + return undefined + } + + if (points.length < 2) return undefined + for (let index = 0; index < points.length - 1; index += 1) { + const current = points[index]! + const next = points[index + 1]! + if ( + current.z !== next.z && + (Math.abs(current.x - next.x) > SAME_POINT_EPSILON || + Math.abs(current.y - next.y) > SAME_POINT_EPSILON) + ) { + return undefined + } + } + return points +} + +const routeStaysInsideBounds = ( + points: HighDensityRoute["route"], + route: HighDensityRoute, + bounds: SimpleRouteJson["bounds"], +): boolean => + points.every((point, pointIndex) => { + const isTerminal = pointIndex === 0 || pointIndex === points.length - 1 + const previous = points[pointIndex - 1] + const next = points[pointIndex + 1] + const isVia = + (previous?.z !== undefined && previous.z !== point.z) || + (next?.z !== undefined && next.z !== point.z) + const inset = isTerminal + ? 0 + : isVia + ? route.viaDiameter / 2 + : route.traceThickness / 2 + + return ( + point.x >= bounds.minX + inset - SAME_POINT_EPSILON && + point.x <= bounds.maxX - inset + SAME_POINT_EPSILON && + point.y >= bounds.minY + inset - SAME_POINT_EPSILON && + point.y <= bounds.maxY - inset + SAME_POINT_EPSILON + ) + }) + +const obstacleAppliesToLayer = ( + obstacle: Obstacle, + z: number, + layerCount: number, +): boolean => + obstacle.__zLayers?.includes(z) === true || + obstacle.layers.includes(mapZToLayerName(z, layerCount)) + +const pointIsInsideObstacle = ( + point: { x: number; y: number }, + obstacle: Obstacle, +): boolean => { + const radians = -((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180 + const deltaX = point.x - obstacle.center.x + const deltaY = point.y - obstacle.center.y + const localX = deltaX * Math.cos(radians) - deltaY * Math.sin(radians) + const localY = deltaX * Math.sin(radians) + deltaY * Math.cos(radians) + + return ( + Math.abs(localX) <= obstacle.width / 2 + SAME_POINT_EPSILON && + Math.abs(localY) <= obstacle.height / 2 + SAME_POINT_EPSILON + ) +} + +const pointFitsInsideBounds = ( + point: { x: number; y: number }, + bounds: SimpleRouteJson["bounds"], + inset: number, +): boolean => + point.x >= bounds.minX + inset - SAME_POINT_EPSILON && + point.x <= bounds.maxX - inset + SAME_POINT_EPSILON && + point.y >= bounds.minY + inset - SAME_POINT_EPSILON && + point.y <= bounds.maxY - inset + SAME_POINT_EPSILON + +const obstacleRepresentsPhysicalPad = (obstacle: Obstacle): boolean => + obstacle.connectedTo.some( + (id) => id.startsWith("pcb_smtpad_") || id.startsWith("pcb_plated_hole_"), + ) + +const snapToIjumpGrid = (value: number): number => { + const snapped = Math.round(value / IJUMP_GRID_STEP) * IJUMP_GRID_STEP + return Math.abs(snapped) <= SAME_POINT_EPSILON ? 0 : snapped +} + +const getPointKey = (point: { x: number; y: number }): string => + `${point.x.toFixed(9)}:${point.y.toFixed(9)}` + +export class Pipeline9IjumpRerouter { + private readonly srj: SimpleRouteJson + private readonly connMap?: ConnectivityMap + private readonly viaHoleDiameter?: number + private readonly terminalObstacles: Obstacle[] + private readonly baseObstacles: Obstacle[] + private readonly candidateObstacleCache = new WeakMap< + HighDensityRoute[], + Map + >() + + constructor(params: Pipeline9IjumpRerouterParams) { + this.srj = params.srj + this.connMap = params.connMap + this.viaHoleDiameter = params.viaHoleDiameter + this.terminalObstacles = params.baseObstacles.map((obstacle) => + addCanonicalConnectivity(obstacle, params.connMap), + ) + this.baseObstacles = addApproximatingRectsToSrj({ + ...params.srj, + obstacles: this.terminalObstacles, + connections: [], + traces: undefined, + }).obstacles + } + + private getOwningTerminalObstacles( + route: HighDensityRoute, + point: HighDensityRoute["route"][number], + ): Obstacle[] { + const owningObstacles = this.terminalObstacles.filter( + (obstacle) => + obstacleRepresentsPhysicalPad(obstacle) && + obstacleAppliesToLayer(obstacle, point.z, this.srj.layerCount) && + isObstacleConnectedToRoute(obstacle, route, this.connMap) && + pointIsInsideObstacle(point, obstacle), + ) + + if (!point.pcb_port_id) return owningObstacles + const matchingPortObstacles = owningObstacles.filter((obstacle) => + obstacle.connectedTo.includes(point.pcb_port_id!), + ) + return matchingPortObstacles.length > 0 + ? matchingPortObstacles + : owningObstacles + } + + private getTerminalAccessPoints( + route: HighDensityRoute, + point: HighDensityRoute["route"][number], + ): Array<{ x: number; y: number }> { + const owningObstacles = this.getOwningTerminalObstacles(route, point) + if (owningObstacles.length === 0) return [] + + const points: Array<{ x: number; y: number }> = [] + const seenPoints = new Set() + const addPoint = (candidate: { x: number; y: number }) => { + if ( + !Number.isFinite(candidate.x) || + !Number.isFinite(candidate.y) || + !pointFitsInsideBounds( + candidate, + this.srj.bounds, + route.viaDiameter / 2, + ) || + !owningObstacles.some((obstacle) => + pointIsInsideObstacle(candidate, obstacle), + ) + ) { + return + } + const key = getPointKey(candidate) + if (seenPoints.has(key)) return + seenPoints.add(key) + points.push(candidate) + } + + // Keep the exact routed terminal as the first choice. Moving to one of the + // pad access points below is only necessary when several branches share a + // terminal pad and their terminal vias need to be separated. + addPoint({ x: point.x, y: point.y }) + + for (const obstacle of owningObstacles) { + const longAxisIsX = obstacle.width >= obstacle.height + const longAxisLength = longAxisIsX ? obstacle.width : obstacle.height + const maximumOffset = Math.max( + 0, + longAxisLength / 2 - route.viaDiameter / 2, + ) + const rotationRadians = + ((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180 + + for (const offset of [-maximumOffset, maximumOffset, 0]) { + const localX = longAxisIsX ? offset : 0 + const localY = longAxisIsX ? 0 : offset + addPoint({ + x: snapToIjumpGrid( + obstacle.center.x + + localX * Math.cos(rotationRadians) - + localY * Math.sin(rotationRadians), + ), + y: snapToIjumpGrid( + obstacle.center.y + + localX * Math.sin(rotationRadians) + + localY * Math.cos(rotationRadians), + ), + }) + } + } + + return points + } + + /** + * Produces a small, deterministic set of full-route escapes. Every access + * point remains in the physical pad that owns the corresponding terminal, + * and every possible routing layer is considered without using net- or + * board-specific coordinates. + */ + getTerminalViaEscapeCandidates( + routes: HighDensityRoute[], + routeIndex: number, + ): Pipeline9TerminalViaEscapeCandidate[] { + const route = routes[routeIndex] + const start = route?.route[0] + const end = route?.route.at(-1) + if (!route || !start || !end || route.route.length < 2) return [] + + const startAccessPoints = this.getTerminalAccessPoints(route, start) + const endAccessPoints = this.getTerminalAccessPoints(route, end) + if (startAccessPoints.length === 0 || endAccessPoints.length === 0) { + return [] + } + + const alternateZs = Array.from( + { length: this.srj.layerCount }, + (_, alternateZ) => alternateZ, + ).filter((alternateZ) => alternateZ !== start.z || alternateZ !== end.z) + const candidates: Pipeline9TerminalViaEscapeCandidate[] = [] + const accessPointPairCount = + startAccessPoints.length * endAccessPoints.length + + // Interleave layers before advancing to the next pair of pad access + // points. The exact-repair caller intentionally tries only a small prefix, + // so layer-major ordering would starve every later board layer. + for ( + let accessPointPairIndex = 0; + accessPointPairIndex < accessPointPairCount; + accessPointPairIndex += 1 + ) { + const startVia = + startAccessPoints[ + Math.floor(accessPointPairIndex / endAccessPoints.length) + ]! + const endVia = + endAccessPoints[accessPointPairIndex % endAccessPoints.length]! + for (const alternateZ of alternateZs) { + candidates.push({ alternateZ, startVia, endVia }) + if (candidates.length >= MAX_TERMINAL_VIA_ESCAPE_CANDIDATES) { + return candidates + } + } + } + + return candidates + } + + private terminalViaEscapeCandidateIsValid( + route: HighDensityRoute, + candidate: Pipeline9TerminalViaEscapeCandidate, + ): boolean { + const start = route.route[0] + const end = route.route.at(-1) + if ( + !start || + !end || + !Number.isInteger(candidate.alternateZ) || + candidate.alternateZ < 0 || + candidate.alternateZ >= this.srj.layerCount || + (candidate.alternateZ === start.z && candidate.alternateZ === end.z) + ) { + return false + } + + const startOwningObstacles = this.getOwningTerminalObstacles(route, start) + const endOwningObstacles = this.getOwningTerminalObstacles(route, end) + return ( + Number.isFinite(candidate.startVia.x) && + Number.isFinite(candidate.startVia.y) && + Number.isFinite(candidate.endVia.x) && + Number.isFinite(candidate.endVia.y) && + pointFitsInsideBounds( + candidate.startVia, + this.srj.bounds, + candidate.alternateZ === start.z + ? route.traceThickness / 2 + : route.viaDiameter / 2, + ) && + pointFitsInsideBounds( + candidate.endVia, + this.srj.bounds, + candidate.alternateZ === end.z + ? route.traceThickness / 2 + : route.viaDiameter / 2, + ) && + startOwningObstacles.some((obstacle) => + pointIsInsideObstacle(candidate.startVia, obstacle), + ) && + endOwningObstacles.some((obstacle) => + pointIsInsideObstacle(candidate.endVia, obstacle), + ) + ) + } + + tryRerouteWithTerminalViaEscape( + routes: HighDensityRoute[], + options: Pipeline9TerminalViaEscapeOptions, + ): Pipeline9IjumpRerouteResult | undefined { + const targetRoute = routes[options.routeIndex] + const originalStart = targetRoute?.route[0] + const originalEnd = targetRoute?.route.at(-1) + if ( + !targetRoute || + !originalStart || + !originalEnd || + targetRoute.route.length < 2 || + !this.terminalViaEscapeCandidateIsValid(targetRoute, options.candidate) + ) { + return undefined + } + + const { candidate } = options + const routingRoutes = routes.map((route, routeIndex) => + routeIndex === options.routeIndex + ? { + ...route, + route: [ + { + x: candidate.startVia.x, + y: candidate.startVia.y, + z: candidate.alternateZ, + traceThickness: route.traceThickness, + }, + { + x: candidate.endVia.x, + y: candidate.endVia.y, + z: candidate.alternateZ, + traceThickness: route.traceThickness, + }, + ], + } + : route, + ) + const result = this.tryReroute(routingRoutes, { + routeIndex: options.routeIndex, + omitCandidateRouteIndexes: options.omitCandidateRouteIndexes, + includeCandidateCopper: options.includeCandidateCopper, + reverse: options.reverse, + shortenPath: options.shortenPath, + maxIterations: options.maxIterations, + }) + if (!result?.route) return result + + const rebuiltPoints: HighDensityRoute["route"] = [] + const pushPoint = (point: HighDensityRoute["route"][number]) => { + if (!pointsAreEqual(rebuiltPoints.at(-1), point)) { + rebuiltPoints.push(point) + } + } + const makeAccessPoint = (point: { x: number; y: number }, z: number) => ({ + x: point.x, + y: point.y, + z, + traceThickness: targetRoute.traceThickness, + }) + + pushPoint(originalStart) + pushPoint(makeAccessPoint(candidate.startVia, originalStart.z)) + pushPoint(makeAccessPoint(candidate.startVia, candidate.alternateZ)) + for (const routedPoint of result.route.route) { + const { pcb_port_id: _pcbPortId, ...point } = routedPoint + pushPoint(point) + } + pushPoint(makeAccessPoint(candidate.endVia, candidate.alternateZ)) + pushPoint(makeAccessPoint(candidate.endVia, originalEnd.z)) + pushPoint(originalEnd) + + if (!routeStaysInsideBounds(rebuiltPoints, targetRoute, this.srj.bounds)) { + return { iterations: result.iterations } + } + + return { + route: { + ...targetRoute, + route: rebuiltPoints, + vias: [], + }, + iterations: result.iterations, + } + } + + private getCandidateObstacles( + routes: HighDensityRoute[], + targetRouteIndex: number, + omitCandidateRouteIndexes?: ReadonlySet, + ): Obstacle[] { + const omittedRouteIndexes = [...(omitCandidateRouteIndexes ?? [])] + .filter((routeIndex) => routeIndex !== targetRouteIndex) + .sort((left, right) => left - right) + const cacheKey = `${targetRouteIndex}:${omittedRouteIndexes.join(",")}` + const cachedForRoutes = this.candidateObstacleCache.get(routes) + const cachedObstacles = cachedForRoutes?.get(cacheKey) + if (cachedObstacles) return cachedObstacles + + const omittedRouteIndexSet = new Set(omittedRouteIndexes) + const traces = routes.flatMap((route, routeIndex) => { + if ( + routeIndex === targetRouteIndex || + omittedRouteIndexSet.has(routeIndex) || + route.route.length < 2 + ) { + return [] + } + return [ + { + type: "pcb_trace" as const, + pcb_trace_id: `pipeline9_candidate_${routeIndex}`, + connection_name: route.connectionName, + route: convertHdRouteToSimplifiedRoute(route, this.srj.layerCount, { + defaultViaHoleDiameter: this.viaHoleDiameter, + obstacles: this.baseObstacles, + connMap: this.connMap, + }), + }, + ] + }) + const traceObstacles = getObstaclesFromSrjTraces( + { + ...this.srj, + obstacles: [], + connections: [], + traces, + }, + { + includeConnectionNameInConnectedTo: true, + includeSquareCaps: true, + modelJumperPads: true, + }, + ).map((obstacle) => addCanonicalConnectivity(obstacle, this.connMap)) + + const candidateObstacles = addApproximatingRectsToSrj({ + ...this.srj, + obstacles: traceObstacles, + connections: [], + traces: undefined, + }).obstacles + const obstaclesByTarget = cachedForRoutes ?? new Map() + obstaclesByTarget.set(cacheKey, candidateObstacles) + if (!cachedForRoutes) { + this.candidateObstacleCache.set(routes, obstaclesByTarget) + } + + return candidateObstacles + } + + tryReroute( + routes: HighDensityRoute[], + options: Pipeline9IjumpRerouteOptions, + ): Pipeline9IjumpRerouteResult | undefined { + const targetRoute = routes[options.routeIndex] + if (!targetRoute || targetRoute.route.length < 2) return undefined + + const startIndex = options.startIndex ?? 0 + const endIndex = options.endIndex ?? targetRoute.route.length - 1 + const startPoint = targetRoute.route[startIndex] + const endPoint = targetRoute.route[endIndex] + if ( + !startPoint || + !endPoint || + startIndex < 0 || + endIndex >= targetRoute.route.length || + startIndex >= endIndex + ) { + return undefined + } + + const start = { + x: startPoint.x, + y: startPoint.y, + layer: mapZToLayerName(startPoint.z, this.srj.layerCount), + } + const end = { + x: endPoint.x, + y: endPoint.y, + layer: mapZToLayerName(endPoint.z, this.srj.layerCount), + } + const pointsToConnect = options.reverse ? [end, start] : [start, end] + const candidateObstacles = options.includeCandidateCopper + ? this.getCandidateObstacles( + routes, + options.routeIndex, + options.omitCandidateRouteIndexes, + ) + : [] + const input = { + ...this.srj, + obstacles: [...this.baseObstacles, ...candidateObstacles], + connections: [ + { + name: targetRoute.connectionName, + pointsToConnect, + }, + ], + traces: undefined, + } + + let solver: MultilayerIjump | undefined + try { + solver = new MultilayerIjump({ + input: input as never, + connMap: this.connMap, + GRID_STEP: IJUMP_GRID_STEP, + OBSTACLE_MARGIN: + RELAXED_TRACE_CLEARANCE + targetRoute.traceThickness / 2, + MAX_ITERATIONS: options.maxIterations, + VIA_COST: 4, + isRemovePathLoopsEnabled: true, + isShortenPathWithShortcutsEnabled: options.shortenPath, + optimizeWithGoalBoxes: false, + }) + solver.VIA_DIAMETER = targetRoute.viaDiameter + solver.GOAL_RUSH_FACTOR = 1.1 + + const [rawTrace] = solver.solveAndMapToTraces() + if (!rawTrace) return { iterations: solver.iterations } + const simplifiedRoute = options.reverse + ? reverseSimplifiedRoute( + (rawTrace as unknown as SimplifiedPcbTrace).route, + ) + : (rawTrace as unknown as SimplifiedPcbTrace).route + const reroutedPoints = convertSimplifiedRouteToHdPoints( + simplifiedRoute, + this.srj.layerCount, + targetRoute.traceThickness, + ) + if (!reroutedPoints) return { iterations: solver.iterations } + + reroutedPoints[0] = { ...reroutedPoints[0]!, ...startPoint } + reroutedPoints[reroutedPoints.length - 1] = { + ...reroutedPoints.at(-1)!, + ...endPoint, + } + if ( + !routeStaysInsideBounds(reroutedPoints, targetRoute, this.srj.bounds) + ) { + return { iterations: solver.iterations } + } + const route = [ + ...targetRoute.route.slice(0, startIndex), + ...reroutedPoints, + ...targetRoute.route.slice(endIndex + 1), + ].filter( + (point, pointIndex, allPoints) => + !pointsAreEqual(allPoints[pointIndex - 1], point), + ) + + return { + route: { + ...targetRoute, + route, + vias: [], + }, + iterations: solver.iterations, + } + } catch { + return { + iterations: getFailedAttemptIterationCost( + solver?.iterations, + options.maxIterations, + ), + } + } + } +} diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts index a2616f8d3..7a3f7b4ec 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts @@ -1,15 +1,34 @@ -import { RbushIndex } from "lib/data-structures/RbushIndex" import { BaseSolver } from "lib/solvers/BaseSolver" -import type { CapacityMeshNode, Obstacle, SimpleRouteJson } from "lib/types" +import { PriorityQueue } from "lib/data-structures/PriorityQueue" +import { RbushIndex } from "lib/data-structures/RbushIndex" +import type { + CapacityMeshNode, + Obstacle, + SimpleRouteConnection, + SimpleRouteJson, +} from "lib/types" +import { getConnectionPointLayers } from "lib/utils/connection-point-utils" import { getObstaclesFromSrjTraces } from "lib/utils/convertSrjTracesToObstacles" import { getUniqueValidZLayersFromLayerNames } from "lib/utils/mapLayerNameToZ" +import { resolvePreloadedTraceCanonicalNetIds } from "lib/utils/resolvePreloadedTraceCanonicalNetIds" type PreloadedTraceShape = { - connectionName: string - obstacle: Obstacle + fixedNetId: string zLayers: number[] + geometry: RotatedRectGeometry + refinementCellDimension: number +} + +type PendingRefinement = { + f: number + node: CapacityMeshNode + candidateShapes: PreloadedTraceShape[] + depth: number + childPath: string } +type RefinementAxis = "x" | "y" + type RotatedRectGeometry = { center: { x: number; y: number } halfWidth: number @@ -20,6 +39,17 @@ type RotatedRectGeometry = { } const GEOMETRIC_TOLERANCE = 1e-6 +const REFINEMENT_CELL_FACTOR = 0.5 +const MAX_REFINEMENT_DEPTH = 16 +const BASE_MAX_COMPENSATED_OUTPUT_NODE_COUNT = 3_000 +const MIN_REFINEMENT_WORST_CASE_ALLOWANCE = 2_050 + +const getCanonicalSimpleRouteConnectionName = ( + connection: SimpleRouteConnection, +) => + connection.__netConnectionName ?? + connection.__rootConnectionNames?.[0] ?? + connection.name const getRotatedRectGeometry = (obstacle: Obstacle): RotatedRectGeometry => { const radians = ((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180 @@ -51,15 +81,14 @@ const doesRotatedRectContainNode = ( ): boolean => { const nodeHalfWidth = node.width / 2 const nodeHalfHeight = node.height / 2 - const deltaX = rect.center.x - node.center.x - const deltaY = rect.center.y - node.center.y + const deltaX = node.center.x - rect.center.x + const deltaY = node.center.y - rect.center.y const projectedOnRectX = Math.abs( deltaX * rect.unitX.x + deltaY * rect.unitX.y, ) const projectedOnRectY = Math.abs( deltaX * rect.unitY.x + deltaY * rect.unitY.y, ) - const nodeExtentOnRectX = nodeHalfWidth * Math.abs(rect.unitX.x) + nodeHalfHeight * Math.abs(rect.unitX.y) @@ -75,10 +104,61 @@ const doesRotatedRectContainNode = ( ) } +const doesRotatedRectIntersectNode = ( + rect: RotatedRectGeometry, + node: CapacityMeshNode, +): boolean => { + const nodeHalfWidth = node.width / 2 + const nodeHalfHeight = node.height / 2 + const nodeMinX = node.center.x - nodeHalfWidth + const nodeMaxX = node.center.x + nodeHalfWidth + const nodeMinY = node.center.y - nodeHalfHeight + const nodeMaxY = node.center.y + nodeHalfHeight + if ( + nodeMaxX < rect.bounds.minX - GEOMETRIC_TOLERANCE || + nodeMinX > rect.bounds.maxX + GEOMETRIC_TOLERANCE || + nodeMaxY < rect.bounds.minY - GEOMETRIC_TOLERANCE || + nodeMinY > rect.bounds.maxY + GEOMETRIC_TOLERANCE + ) { + return false + } + + const deltaX = node.center.x - rect.center.x + const deltaY = node.center.y - rect.center.y + const projectedOnRectX = Math.abs( + deltaX * rect.unitX.x + deltaY * rect.unitX.y, + ) + const projectedOnRectY = Math.abs( + deltaX * rect.unitY.x + deltaY * rect.unitY.y, + ) + const nodeExtentOnRectX = + nodeHalfWidth * Math.abs(rect.unitX.x) + + nodeHalfHeight * Math.abs(rect.unitX.y) + const nodeExtentOnRectY = + nodeHalfWidth * Math.abs(rect.unitY.x) + + nodeHalfHeight * Math.abs(rect.unitY.y) + + return ( + projectedOnRectX <= + rect.halfWidth + nodeExtentOnRectX + GEOMETRIC_TOLERANCE && + projectedOnRectY <= + rect.halfHeight + nodeExtentOnRectY + GEOMETRIC_TOLERANCE + ) +} + const getPreloadedTraceShapes = ( srj: SimpleRouteJson, + containmentCompensation = srj.minTraceWidth / 2, + refinementCellFactor = REFINEMENT_CELL_FACTOR, ): PreloadedTraceShape[] => { const shapes: PreloadedTraceShape[] = [] + const canonicalNetIdByTraceId = resolvePreloadedTraceCanonicalNetIds(srj) + const requestedKeepoutMargin = srj.defaultObstacleMargin ?? 0.15 + // Nodes are reserved only when the full axis-aligned cell fits inside the + // rotated keepout. Compensate by one candidate trace radius so that this + // containment approximation does not systematically shrink fixed copper. + const projectedKeepoutMargin = + requestedKeepoutMargin + containmentCompensation for (const trace of srj.traces ?? []) { if (!trace.connection_name) { @@ -87,13 +167,30 @@ const getPreloadedTraceShapes = ( ) } - const obstacles = getObstaclesFromSrjTraces({ - ...srj, - traces: [trace], - }) + const obstacles = getObstaclesFromSrjTraces( + { + ...srj, + traces: [trace], + }, + { + includeConnectionNameInConnectedTo: true, + includeSquareCaps: true, + modelJumperPads: true, + }, + ) for (const obstacle of obstacles) { + const keepoutObstacle = { + ...obstacle, + width: obstacle.width + projectedKeepoutMargin * 2, + height: obstacle.height + projectedKeepoutMargin * 2, + } + const refinementCellDimension = + Math.min( + obstacle.width + requestedKeepoutMargin * 2, + obstacle.height + requestedKeepoutMargin * 2, + ) * refinementCellFactor const zLayers = getUniqueValidZLayersFromLayerNames( - obstacle.layers, + keepoutObstacle.layers, srj.layerCount, ) if (zLayers.length === 0) { @@ -102,9 +199,12 @@ const getPreloadedTraceShapes = ( ) } shapes.push({ - connectionName: trace.connection_name, - obstacle, + fixedNetId: + canonicalNetIdByTraceId.get(trace.pcb_trace_id) ?? + trace.connection_name, zLayers, + geometry: getRotatedRectGeometry(keepoutObstacle), + refinementCellDimension, }) } } @@ -113,77 +213,468 @@ const getPreloadedTraceShapes = ( } /** - * Conservatively projects preloaded copper onto the final capacity mesh. - * A region is assigned only when one trace shape covers its full area and - * every available layer. The hypergraph has region-level ownership, so - * assigning on partial overlap would block free space or an unused layer. + * Projects preloaded copper onto the final capacity mesh. Spatial cells are + * refined until trace keepouts fully contain the affected cells. When a trace + * occupies only some of a cell's layers, the cell is cloned into layer groups + * so the hypergraph reserves only the copper's actual layers. */ export class PreloadedTraceGraphSolver extends BaseSolver { + private readonly inputNodes: CapacityMeshNode[] private readonly outputNodes: CapacityMeshNode[] - private readonly nodeIndex = new RbushIndex() private readonly traceShapes: PreloadedTraceShape[] + private readonly traceShapeIndex = new RbushIndex() constructor( capacityMeshNodes: CapacityMeshNode[], private readonly srj: SimpleRouteJson, + private readonly maxCompensatedOutputNodeCount?: number, ) { super() this.MAX_ITERATIONS = 1 - this.outputNodes = capacityMeshNodes.map((node) => ({ + this.inputNodes = capacityMeshNodes.map((node) => ({ ...node, availableZ: [...node.availableZ], _connectedTo: node._connectedTo === undefined ? undefined : [...node._connectedTo], + _preloadedFixedNetIds: + node._preloadedFixedNetIds === undefined + ? undefined + : [...node._preloadedFixedNetIds], })) - this.nodeIndex.bulkLoad( - this.outputNodes.map((node) => ({ - item: node, - minX: node.center.x - node.width / 2, - minY: node.center.y - node.height / 2, - maxX: node.center.x + node.width / 2, - maxY: node.center.y + node.height / 2, + this.outputNodes = [] + this.traceShapes = getPreloadedTraceShapes(srj) + this.traceShapeIndex.bulkLoad( + this.traceShapes.map((shape) => ({ + item: shape, + ...shape.geometry.bounds, })), ) - this.traceShapes = getPreloadedTraceShapes(srj) } override getSolverName(): string { return "PreloadedTraceGraphSolver" } - override _step(): void { - const projectedNodeIds = new Set() - let traceRegionAssignmentCount = 0 - - for (const shape of this.traceShapes) { - const geometry = getRotatedRectGeometry(shape.obstacle) - const candidateNodes = this.nodeIndex.search( - geometry.bounds.minX, - geometry.bounds.minY, - geometry.bounds.maxX, - geometry.bounds.maxY, + private reserveNodeByTraceLayerConnectivity( + node: CapacityMeshNode, + reservingShapes: PreloadedTraceShape[], + ): CapacityMeshNode[] { + const existingConnections = [...(node._connectedTo ?? [])].sort() + const existingFixedNetIds = [ + ...new Set(node._preloadedFixedNetIds ?? []), + ].sort() + const layerGroups = new Map< + string, + { availableZ: number[]; fixedNetIds: string[] } + >() + for (const z of node.availableZ) { + const fixedNetIds = reservingShapes + .filter((shape) => shape.zLayers.includes(z)) + .map((shape) => shape.fixedNetId) + const combinedFixedNetIds = [ + ...new Set([...existingFixedNetIds, ...fixedNetIds]), + ].sort() + const groupKey = JSON.stringify(combinedFixedNetIds) + const group = layerGroups.get(groupKey) ?? { + availableZ: [], + fixedNetIds: combinedFixedNetIds, + } + group.availableZ.push(z) + layerGroups.set(groupKey, group) + } + + if (layerGroups.size <= 1) { + const group = [...layerGroups.values()][0] + return [ + { + ...node, + availableZ: [...node.availableZ], + _connectedTo: + existingConnections.length === 0 + ? undefined + : [...existingConnections], + _preloadedFixedNetIds: + group && group.fixedNetIds.length > 0 + ? [...group.fixedNetIds] + : undefined, + }, + ] + } + + return [...layerGroups.values()].map(({ availableZ, fixedNetIds }) => ({ + ...node, + capacityMeshNodeId: `${node.capacityMeshNodeId}__preloaded_z${availableZ.join("_")}`, + layer: `z${availableZ.join(",")}`, + availableZ: [...availableZ], + _connectedTo: + existingConnections.length === 0 ? undefined : [...existingConnections], + _preloadedFixedNetIds: + fixedNetIds.length === 0 ? undefined : [...fixedNetIds], + })) + } + + private splitRefinementTask( + task: PendingRefinement, + intersectingShapes: PreloadedTraceShape[], + splitAxis: RefinementAxis, + ): PendingRefinement[] { + const { node, depth, childPath } = task + const splitAlongX = splitAxis === "x" + const childWidth = splitAlongX ? node.width / 2 : node.width + const childHeight = splitAlongX ? node.height : node.height / 2 + const offsetX = splitAlongX ? childWidth / 2 : 0 + const offsetY = splitAlongX ? 0 : childHeight / 2 + const children = [-1, 1].map((direction, index) => ({ + ...node, + capacityMeshNodeId: `${node.capacityMeshNodeId}__preloaded_${childPath}${index}`, + center: { + x: node.center.x + direction * offsetX, + y: node.center.y + direction * offsetY, + }, + width: childWidth, + height: childHeight, + availableZ: [...node.availableZ], + _connectedTo: + node._connectedTo === undefined ? undefined : [...node._connectedTo], + _preloadedFixedNetIds: + node._preloadedFixedNetIds === undefined + ? undefined + : [...node._preloadedFixedNetIds], + })) + + return children.map((child, index) => ({ + f: -(child.width * child.height), + node: child, + candidateShapes: intersectingShapes, + depth: depth + 1, + childPath: `${childPath}${index}`, + })) + } + + private getRefinementSplitAxis( + node: CapacityMeshNode, + intersectingShapes: PreloadedTraceShape[], + ): RefinementAxis { + const getUnresolvedPartialArea = (splitAxis: RefinementAxis) => { + const splitAlongX = splitAxis === "x" + const childWidth = splitAlongX ? node.width / 2 : node.width + const childHeight = splitAlongX ? node.height : node.height / 2 + const offsetX = splitAlongX ? childWidth / 2 : 0 + const offsetY = splitAlongX ? 0 : childHeight / 2 + let unresolvedPartialArea = 0 + + for (const direction of [-1, 1]) { + const child: CapacityMeshNode = { + ...node, + center: { + x: node.center.x + direction * offsetX, + y: node.center.y + direction * offsetY, + }, + width: childWidth, + height: childHeight, + } + for (const shape of intersectingShapes) { + if ( + !doesRotatedRectIntersectNode(shape.geometry, child) || + doesRotatedRectContainNode(shape.geometry, child) + ) { + continue + } + unresolvedPartialArea += childWidth * childHeight + } + } + + return unresolvedPartialArea + } + + const getContainmentBenefit = (splitAxis: RefinementAxis) => { + let benefit = 0 + for (const shape of intersectingShapes) { + const rect = shape.geometry + const deltaX = node.center.x - rect.center.x + const deltaY = node.center.y - rect.center.y + const projectedOnRectX = Math.abs( + deltaX * rect.unitX.x + deltaY * rect.unitX.y, + ) + const projectedOnRectY = Math.abs( + deltaX * rect.unitY.x + deltaY * rect.unitY.y, + ) + const overflowOnRectX = Math.max( + 0, + projectedOnRectX + + (node.width / 2) * Math.abs(rect.unitX.x) + + (node.height / 2) * Math.abs(rect.unitX.y) - + rect.halfWidth, + ) + const overflowOnRectY = Math.max( + 0, + projectedOnRectY + + (node.width / 2) * Math.abs(rect.unitY.x) + + (node.height / 2) * Math.abs(rect.unitY.y) - + rect.halfHeight, + ) + const splitExtentReduction = + splitAxis === "x" ? node.width / 4 : node.height / 4 + const rectXReduction = + splitExtentReduction * + Math.abs(splitAxis === "x" ? rect.unitX.x : rect.unitX.y) + const rectYReduction = + splitExtentReduction * + Math.abs(splitAxis === "x" ? rect.unitY.x : rect.unitY.y) + benefit += + Math.min(overflowOnRectX, rectXReduction) + + Math.min(overflowOnRectY, rectYReduction) + } + return benefit + } + + const xPartialArea = getUnresolvedPartialArea("x") + const yPartialArea = getUnresolvedPartialArea("y") + if (xPartialArea + GEOMETRIC_TOLERANCE < yPartialArea) { + return "x" + } + if (yPartialArea + GEOMETRIC_TOLERANCE < xPartialArea) { + return "y" + } + const xContainmentBenefit = getContainmentBenefit("x") + const yContainmentBenefit = getContainmentBenefit("y") + if (xContainmentBenefit > yContainmentBenefit + GEOMETRIC_TOLERANCE) { + return "x" + } + if (yContainmentBenefit > xContainmentBenefit + GEOMETRIC_TOLERANCE) { + return "y" + } + return node.width >= node.height ? "x" : "y" + } + + private getIndexedTraceShapesForNode( + node: CapacityMeshNode, + ): PreloadedTraceShape[] { + return this.traceShapeIndex.search( + node.center.x - node.width / 2, + node.center.y - node.height / 2, + node.center.x + node.width / 2, + node.center.y + node.height / 2, + ) + } + + private getSemanticNodeReservationShapes( + node: CapacityMeshNode, + intersectingShapes: PreloadedTraceShape[], + containingShapes: PreloadedTraceShape[], + ): PreloadedTraceShape[] { + const containingShapeSet = new Set(containingShapes) + + return intersectingShapes.filter( + (shape) => + containingShapeSet.has(shape) || + this.doesShapeMatchContainedSemanticTarget(node, shape), + ) + } + + private doesShapeMatchContainedSemanticTarget( + node: CapacityMeshNode, + shape: PreloadedTraceShape, + ): boolean { + if (node._targetConnectionName) { + const targetConnection = this.srj.connections.find((connection) => { + const aliases = new Set([ + connection.name, + connection.__netConnectionName, + ...(connection.__rootConnectionNames ?? []), + ]) + return aliases.has(node._targetConnectionName!) + }) + const canonicalTargetNet = targetConnection + ? getCanonicalSimpleRouteConnectionName(targetConnection) + : node._targetConnectionName + return canonicalTargetNet === shape.fixedNetId + } + + const minX = node.center.x - node.width / 2 - GEOMETRIC_TOLERANCE + const maxX = node.center.x + node.width / 2 + GEOMETRIC_TOLERANCE + const minY = node.center.y - node.height / 2 - GEOMETRIC_TOLERANCE + const maxY = node.center.y + node.height / 2 + GEOMETRIC_TOLERANCE + + return this.srj.connections.some( + (connection) => + getCanonicalSimpleRouteConnectionName(connection) === + shape.fixedNetId && + connection.pointsToConnect.some((point) => { + if ( + point.x < minX || + point.x > maxX || + point.y < minY || + point.y > maxY + ) { + return false + } + const pointZLayers = getUniqueValidZLayersFromLayerNames( + getConnectionPointLayers(point), + this.srj.layerCount, + ) + return pointZLayers.some( + (z) => node.availableZ.includes(z) && shape.zLayers.includes(z), + ) + }), + ) + } + + private projectNodesWithBoundedRefinement(): { + refinementBudgetExhausted: boolean + refinementSplitCount: number + refinementWorstCaseOutputNodeCount: number + effectiveMaxOutputNodeCount: number + minimumLayerSplitNodeCount: number + minimumRefinementWorstCaseAllowance: number + } { + const minimumLayerSplitNodeCount = this.inputNodes.reduce( + (count, node) => count + Math.max(1, node.availableZ.length), + 0, + ) + const minimumRefinementWorstCaseAllowance = + this.maxCompensatedOutputNodeCount === undefined + ? MIN_REFINEMENT_WORST_CASE_ALLOWANCE + : 0 + const requestedMaxOutputNodeCount = + this.maxCompensatedOutputNodeCount ?? + Math.max( + BASE_MAX_COMPENSATED_OUTPUT_NODE_COUNT, + minimumLayerSplitNodeCount + minimumRefinementWorstCaseAllowance, ) + const effectiveMaxOutputNodeCount = Math.max( + requestedMaxOutputNodeCount, + minimumLayerSplitNodeCount, + ) + let refinementWorstCaseOutputNodeCount = minimumLayerSplitNodeCount + let refinementBudgetExhausted = false + let refinementSplitCount = 0 + // Refine the largest unresolved cells first. A hard cap necessarily leaves + // some partial intersections conservatively reserved; prioritizing by area + // prevents input ordering from turning a grazing trace into an arbitrarily + // large blocked region. + const pending = new PriorityQueue( + this.inputNodes.map((node) => ({ + f: -(node.width * node.height), + node, + candidateShapes: this.getIndexedTraceShapesForNode(node), + depth: 0, + childPath: "", + })), + effectiveMaxOutputNodeCount + this.inputNodes.length, + ) - for (const node of candidateNodes) { - if (!node.availableZ.every((z) => shape.zLayers.includes(z))) continue - if (!doesRotatedRectContainNode(geometry, node)) continue - - const connectedTo = new Set(node._connectedTo ?? []) - const previousConnectionCount = connectedTo.size - connectedTo.add(shape.connectionName) - node._connectedTo = [...connectedTo] - projectedNodeIds.add(node.capacityMeshNodeId) - if (connectedTo.size > previousConnectionCount) { - traceRegionAssignmentCount++ + while (!pending.isEmpty()) { + const task = pending.dequeue()! + const { node, candidateShapes, depth } = task + const intersectingShapes = candidateShapes.filter( + (shape) => + node.availableZ.some((z) => shape.zLayers.includes(z)) && + doesRotatedRectIntersectNode(shape.geometry, node), + ) + if (intersectingShapes.length === 0) { + this.outputNodes.push(node) + continue + } + + const containingShapes = intersectingShapes.filter((shape) => + doesRotatedRectContainNode(shape.geometry, node), + ) + if (containingShapes.length === intersectingShapes.length) { + this.outputNodes.push( + ...this.reserveNodeByTraceLayerConnectivity(node, containingShapes), + ) + continue + } + + const targetCellDimension = Math.min( + ...intersectingShapes.map((shape) => shape.refinementCellDimension), + ) + const splitAxis = this.getRefinementSplitAxis(node, intersectingShapes) + const reachedRefinementFloor = + depth >= MAX_REFINEMENT_DEPTH || + (splitAxis === "x" + ? node.width <= targetCellDimension + : node.height <= targetCellDimension) + const layerSplitCost = Math.max(1, node.availableZ.length) + const canSplitWithinBudget = + refinementWorstCaseOutputNodeCount + layerSplitCost <= + effectiveMaxOutputNodeCount + + if (node._containsTarget || node._isComponentTopologyNode) { + // Target/component nodes are semantic routing regions, not ordinary + // geometric cells. They can intentionally span a large component + // area, so promoting a nearby trace's partial overlap to ownership of + // the whole node can steal an unrelated terminal. Keep full coverage + // and same-net partial coverage; later geometric routing stages still + // receive the exact preloaded trace obstacles. + this.outputNodes.push( + ...this.reserveNodeByTraceLayerConnectivity( + node, + this.getSemanticNodeReservationShapes( + node, + intersectingShapes, + containingShapes, + ), + ), + ) + continue + } + + if (reachedRefinementFloor || !canSplitWithinBudget) { + if (!canSplitWithinBudget && !reachedRefinementFloor) { + refinementBudgetExhausted = true } + // A partially intersecting cell cannot be left free: at the + // refinement floor (or budget), reserve it conservatively. + this.outputNodes.push( + ...this.reserveNodeByTraceLayerConnectivity(node, intersectingShapes), + ) + continue + } + + refinementWorstCaseOutputNodeCount += layerSplitCost + refinementSplitCount++ + for (const childTask of this.splitRefinementTask( + task, + intersectingShapes, + splitAxis, + )) { + pending.enqueue(childTask) } } + return { + refinementBudgetExhausted, + refinementSplitCount, + refinementWorstCaseOutputNodeCount, + effectiveMaxOutputNodeCount, + minimumLayerSplitNodeCount, + minimumRefinementWorstCaseAllowance, + } + } + + override _step(): void { + const refinementStats = this.projectNodesWithBoundedRefinement() + const projectedNodes = this.outputNodes.filter( + (node) => (node._preloadedFixedNetIds?.length ?? 0) > 0, + ) + const traceRegionAssignmentCount = projectedNodes.reduce( + (count, node) => count + (node._preloadedFixedNetIds?.length ?? 0), + 0, + ) + this.stats = { preloadedTraceCount: this.srj.traces?.length ?? 0, preloadedTraceShapeCount: this.traceShapes.length, - projectedNodeCount: projectedNodeIds.size, + inputNodeCount: this.inputNodes.length, + outputNodeCount: this.outputNodes.length, + refinedNodeCount: this.outputNodes.length - this.inputNodes.length, + projectedNodeCount: projectedNodes.length, traceRegionAssignmentCount, + usedContainmentCompensation: true, + compensatedOutputNodeCount: this.outputNodes.length, + ...refinementStats, } this.solved = true } diff --git a/lib/solvers/NecessaryCrampedPortPointSolver/MultiTargetNecessaryCrampedPortPointSolver.ts b/lib/solvers/NecessaryCrampedPortPointSolver/MultiTargetNecessaryCrampedPortPointSolver.ts index 13311b379..15ee5b543 100644 --- a/lib/solvers/NecessaryCrampedPortPointSolver/MultiTargetNecessaryCrampedPortPointSolver.ts +++ b/lib/solvers/NecessaryCrampedPortPointSolver/MultiTargetNecessaryCrampedPortPointSolver.ts @@ -1,4 +1,6 @@ +import { pointToBoxDistance } from "@tscircuit/math-utils" import { BaseSolver } from "@tscircuit/solver-utils" +import { GraphicsObject, mergeGraphics } from "graphics-debug" import { CapacityMeshNode, CapacityMeshNodeId, @@ -8,12 +10,10 @@ import { SegmentPortPoint, SharedEdgeSegment, } from "../AvailableSegmentPointSolver/AvailableSegmentPointSolver" -import { GraphicsObject, mergeGraphics } from "graphics-debug" -import { isAllCandidatesBlockedByObstacles } from "./isAllCandidatesBlockedByObstacles" +import { SingleTargetNecessaryCrampedPortPointSolver } from "./SingleTargetNecessaryCrampedPortPointSolver" import { costFunction } from "./costFunction" +import { isAllCandidatesBlockedByObstacles } from "./isAllCandidatesBlockedByObstacles" import { ExploredPortPoint } from "./types" -import { pointToBoxDistance } from "@tscircuit/math-utils" -import { SingleTargetNecessaryCrampedPortPointSolver } from "./SingleTargetNecessaryCrampedPortPointSolver" const CRAMPED_NON_NECESSARY_PORT_PENALTY = 1_000 @@ -158,7 +158,7 @@ export class MultiTargetNecessaryCrampedPortPointSolver extends BaseSolver { return } - let crampedCandidates = this.candidatesAtDepth.filter((candidate) => { + const crampedCandidates = this.candidatesAtDepth.filter((candidate) => { const port = candidate.port const capacityMeshNodes = port.nodeIds.map((nodeId) => { const cmNode = this.nodeMap.get(nodeId) @@ -242,7 +242,10 @@ export class MultiTargetNecessaryCrampedPortPointSolver extends BaseSolver { return [portPoint] } - if (this.isMultilayerEscapePort(portPoint)) { + if ( + this.isMultilayerEscapePort(portPoint) || + this.isPreloadedFixedCopperPort(portPoint) + ) { return [ { ...portPoint, @@ -263,6 +266,13 @@ export class MultiTargetNecessaryCrampedPortPointSolver extends BaseSolver { ) } + private isPreloadedFixedCopperPort(portPoint: SegmentPortPoint): boolean { + return portPoint.nodeIds.some( + (nodeId) => + (this.nodeMap.get(nodeId)?._preloadedFixedNetIds?.length ?? 0) > 0, + ) + } + override visualize(): GraphicsObject { const graphics: GraphicsObject = { rects: [], diff --git a/lib/solvers/PortPointPathingSolver/tinyhypergraph/getRegionNetIdByRegionId.ts b/lib/solvers/PortPointPathingSolver/tinyhypergraph/getRegionNetIdByRegionId.ts index 2b8b993a9..95b2c35be 100644 --- a/lib/solvers/PortPointPathingSolver/tinyhypergraph/getRegionNetIdByRegionId.ts +++ b/lib/solvers/PortPointPathingSolver/tinyhypergraph/getRegionNetIdByRegionId.ts @@ -1,10 +1,15 @@ +import { checkIfConnectionPointIsInRegion } from "../hgportpointpathingsolver/checkIfConnectionPointIsInRegion" import type { ConnectionHgWithSimpleRouteConnection, HgPortPointPathingSolverParams, } from "../hgportpointpathingsolver/types" -import { checkIfConnectionPointIsInRegion } from "../hgportpointpathingsolver/checkIfConnectionPointIsInRegion" import type { TinyRouteNetIndexer } from "./createTinyRouteNetIndexer" +export const BLOCKED_REGION_NET_ID = -2 + +const getFixedNetIndexerId = (fixedNetId: string) => + `__tscircuit_preloaded_fixed_net__:${JSON.stringify(fixedNetId)}` + export function getRegionNetIdByRegionId(input: { params: Omit & { connections: ConnectionHgWithSimpleRouteConnection[] @@ -13,6 +18,7 @@ export function getRegionNetIdByRegionId(input: { }): Map { const regionNetCandidates = new Map>() const netIndexByConnectionAlias = new Map() + const netIndexByOriginalCanonicalName = new Map() for (const connection of input.params.connections) { const netId = connection.mutuallyConnectedNetworkId const routeNetIndex = input.getNetIndex({ @@ -22,6 +28,16 @@ export function getRegionNetIdByRegionId(input: { for (const connectionAlias of [connection.connectionId, netId]) { netIndexByConnectionAlias.set(connectionAlias, routeNetIndex) } + const originalCanonicalNames = [ + connection.simpleRouteConnection.__netConnectionName, + ...(connection.simpleRouteConnection.__rootConnectionNames ?? []), + ].filter((name): name is string => Boolean(name)) + if (originalCanonicalNames.length === 0) { + originalCanonicalNames.push(connection.simpleRouteConnection.name) + } + for (const originalCanonicalName of originalCanonicalNames) { + netIndexByOriginalCanonicalName.set(originalCanonicalName, routeNetIndex) + } for (const point of connection.simpleRouteConnection.pointsToConnect) { for (const region of input.params.graph.regions) { if ( @@ -44,10 +60,43 @@ export function getRegionNetIdByRegionId(input: { } } + const fixedNetIndexByCanonicalNetId = new Map() for (const region of input.params.graph.regions) { + const fixedNetCandidates = new Set() + const hasPreloadedFixedCopper = + (region.d._preloadedFixedNetIds?.length ?? 0) > 0 for (const connectionName of region.d._connectedTo ?? []) { - const routeNetIndex = netIndexByConnectionAlias.get(connectionName) - if (routeNetIndex === undefined) continue + let routeNetIndex = netIndexByConnectionAlias.get(connectionName) + if (routeNetIndex === undefined) { + if (!hasPreloadedFixedCopper) continue + routeNetIndex = input.getNetIndex({ + connectionId: connectionName, + mutuallyConnectedNetworkId: connectionName, + }) + netIndexByConnectionAlias.set(connectionName, routeNetIndex) + } + + let netCandidates = regionNetCandidates.get(region.regionId) + if (!netCandidates) { + netCandidates = new Set() + regionNetCandidates.set(region.regionId, netCandidates) + } + netCandidates.add(routeNetIndex) + } + + for (const fixedNetId of region.d._preloadedFixedNetIds ?? []) { + let routeNetIndex = netIndexByOriginalCanonicalName.get(fixedNetId) + if (routeNetIndex === undefined) { + routeNetIndex = fixedNetIndexByCanonicalNetId.get(fixedNetId) + } + if (routeNetIndex === undefined) { + const fixedNetIndexerId = getFixedNetIndexerId(fixedNetId) + routeNetIndex = input.getNetIndex({ + connectionId: fixedNetIndexerId, + mutuallyConnectedNetworkId: fixedNetIndexerId, + }) + fixedNetIndexByCanonicalNetId.set(fixedNetId, routeNetIndex) + } let netCandidates = regionNetCandidates.get(region.regionId) if (!netCandidates) { @@ -55,6 +104,19 @@ export function getRegionNetIdByRegionId(input: { regionNetCandidates.set(region.regionId, netCandidates) } netCandidates.add(routeNetIndex) + fixedNetCandidates.add(routeNetIndex) + } + + const netCandidates = regionNetCandidates.get(region.regionId) + if ( + netCandidates && + fixedNetCandidates.size > 0 && + netCandidates.size > 1 + ) { + // A region occupied by multiple fixed nets (or by fixed copper plus an + // unrelated active net) is not a free ambiguous region. Reserve it for + // every route so no net can incorrectly traverse the conflict. + regionNetCandidates.set(region.regionId, new Set([BLOCKED_REGION_NET_ID])) } } diff --git a/lib/solvers/RouteStitchingSolver/MultipleHighDensityRouteStitchSolver3.ts b/lib/solvers/RouteStitchingSolver/MultipleHighDensityRouteStitchSolver3.ts index c97f4f51c..6ef7dc64d 100644 --- a/lib/solvers/RouteStitchingSolver/MultipleHighDensityRouteStitchSolver3.ts +++ b/lib/solvers/RouteStitchingSolver/MultipleHighDensityRouteStitchSolver3.ts @@ -15,6 +15,7 @@ import { selectIslandEndpoints, selectRoutesAlongEndpointPath, snapIslandEndpointToNearestTerminal, + snapIslandEndpointsToDistinctTerminals, } from "./routeStitchingEndpointHelpers" import { compareRoutes, @@ -41,6 +42,8 @@ export class MultipleHighDensityRouteStitchSolver3 extends BaseSolver { defaultViaDiameter: number allowedLayerTransitionPointKeys?: Set preserveTerminalPcbPortIds: boolean + preserveTerminalLayers: boolean + preserveDistinctTerminalSnaps: boolean private endpointIndex = new EndpointClusterIndex() private canStitchBetweenTerminals(params: { @@ -59,6 +62,7 @@ export class MultipleHighDensityRouteStitchSolver3 extends BaseSolver { defaultViaDiameter: this.defaultViaDiameter, allowedLayerTransitionPointKeys: this.allowedLayerTransitionPointKeys, preserveTerminalPcbPortIds: this.preserveTerminalPcbPortIds, + preserveTerminalLayers: this.preserveTerminalLayers, }) while ( @@ -139,12 +143,17 @@ export class MultipleHighDensityRouteStitchSolver3 extends BaseSolver { defaultViaDiameter?: number allowedLayerTransitionPointKeys?: Set preserveTerminalPcbPortIds?: boolean + preserveTerminalLayers?: boolean + preserveDistinctTerminalSnaps?: boolean }) { super() this.colorMap = params.colorMap ?? {} this.allowedLayerTransitionPointKeys = params.allowedLayerTransitionPointKeys this.preserveTerminalPcbPortIds = params.preserveTerminalPcbPortIds ?? false + this.preserveTerminalLayers = params.preserveTerminalLayers ?? false + this.preserveDistinctTerminalSnaps = + params.preserveDistinctTerminalSnaps ?? false const canonicalHdRoutes = [...params.hdRoutes].sort(compareRoutes) @@ -258,15 +267,22 @@ export class MultipleHighDensityRouteStitchSolver3 extends BaseSolver { ) { ;[start, end] = [end, start] } - - start = snapIslandEndpointToNearestTerminal({ - islandEndpoint: start, - terminals: [globalStart, globalEnd], - }) - end = snapIslandEndpointToNearestTerminal({ - islandEndpoint: end, - terminals: [globalStart, globalEnd], - }) + if (this.preserveDistinctTerminalSnaps) { + ;({ start, end } = snapIslandEndpointsToDistinctTerminals({ + start, + end, + terminals: [globalStart, globalEnd], + })) + } else { + start = snapIslandEndpointToNearestTerminal({ + islandEndpoint: start, + terminals: [globalStart, globalEnd], + }) + end = snapIslandEndpointToNearestTerminal({ + islandEndpoint: end, + terminals: [globalStart, globalEnd], + }) + } } else { start = { ...connection.pointsToConnect[0], @@ -422,6 +438,7 @@ export class MultipleHighDensityRouteStitchSolver3 extends BaseSolver { defaultViaDiameter: this.defaultViaDiameter, allowedLayerTransitionPointKeys: this.allowedLayerTransitionPointKeys, preserveTerminalPcbPortIds: this.preserveTerminalPcbPortIds, + preserveTerminalLayers: this.preserveTerminalLayers, }) } diff --git a/lib/solvers/RouteStitchingSolver/SingleHighDensityRouteStitchSolver3.ts b/lib/solvers/RouteStitchingSolver/SingleHighDensityRouteStitchSolver3.ts index 5c5c19072..1997c7651 100644 --- a/lib/solvers/RouteStitchingSolver/SingleHighDensityRouteStitchSolver3.ts +++ b/lib/solvers/RouteStitchingSolver/SingleHighDensityRouteStitchSolver3.ts @@ -52,6 +52,7 @@ export class SingleHighDensityRouteStitchSolver3 extends BaseSolver { end: StitchTerminal colorMap: Record allowedLayerTransitionPointKeys?: Set + preserveTerminalLayers: boolean constructor(opts: { connectionName: string @@ -63,12 +64,14 @@ export class SingleHighDensityRouteStitchSolver3 extends BaseSolver { defaultViaDiameter?: number allowedLayerTransitionPointKeys?: Set preserveTerminalPcbPortIds?: boolean + preserveTerminalLayers?: boolean }) { super() const canonicalHdRoutes = [...opts.hdRoutes].sort(compareRoutes) this.remainingHdRoutes = canonicalHdRoutes this.colorMap = opts.colorMap ?? {} this.allowedLayerTransitionPointKeys = opts.allowedLayerTransitionPointKeys + this.preserveTerminalLayers = opts.preserveTerminalLayers ?? false if (canonicalHdRoutes.length === 0) { this.start = opts.start @@ -219,6 +222,38 @@ export class SingleHighDensityRouteStitchSolver3 extends BaseSolver { ) } + const initialRoutePoints: RoutePoint[] = [ + { + x: this.start.x, + y: this.start.y, + z: this.preserveTerminalLayers + ? this.start.z + : closestFirstRoutePoint.z, + }, + ] + const initialVias: Array<{ x: number; y: number }> = [] + if ( + this.preserveTerminalLayers && + this.start.z !== closestFirstRoutePoint.z + ) { + if ( + this.allowedLayerTransitionPointKeys && + !this.allowedLayerTransitionPointKeys.has(getXyPointKey(this.start)) + ) { + this.failed = true + this.error = `Layer transition at ${getXyPointKey( + this.start, + )} is not allowed` + return + } + initialRoutePoints.push({ + x: this.start.x, + y: this.start.y, + z: closestFirstRoutePoint.z, + }) + initialVias.push({ x: this.start.x, y: this.start.y }) + } + this.mergedHdRoute = { connectionName: opts.connectionName, ...(opts.preserveTerminalPcbPortIds && this.start.pcb_port_id @@ -228,14 +263,8 @@ export class SingleHighDensityRouteStitchSolver3 extends BaseSolver { ? { endPcbPortId: this.end.pcb_port_id } : {}), rootConnectionName: firstRoute.rootConnectionName, - route: [ - { - x: this.start.x, - y: this.start.y, - z: closestFirstRoutePoint.z, - }, - ], - vias: [], + route: initialRoutePoints, + vias: initialVias, jumpers: [], viaDiameter: firstRoute.viaDiameter, traceThickness: firstRoute.traceThickness, @@ -274,19 +303,50 @@ export class SingleHighDensityRouteStitchSolver3 extends BaseSolver { _step() { if (this.remainingHdRoutes.length === 0) { - const lastMergedPoint = + let lastMergedPoint = this.mergedHdRoute.route[this.mergedHdRoute.route.length - 1] + const distanceToEnd = distance(lastMergedPoint, this.end) if ( - distance(lastMergedPoint, this.end) > GEOMETRIC_TOLERANCE && - distance(lastMergedPoint, this.end) <= - MAX_TERMINAL_STITCH_GAP_DISTANCE_3 + distanceToEnd > GEOMETRIC_TOLERANCE && + distanceToEnd <= MAX_TERMINAL_STITCH_GAP_DISTANCE_3 ) { this.mergedHdRoute.route.push({ x: this.end.x, y: this.end.y, z: lastMergedPoint.z, }) + lastMergedPoint = + this.mergedHdRoute.route[this.mergedHdRoute.route.length - 1] + } + + if ( + this.preserveTerminalLayers && + distance(lastMergedPoint, this.end) <= GEOMETRIC_TOLERANCE && + lastMergedPoint.z !== this.end.z + ) { + if ( + this.allowedLayerTransitionPointKeys && + !this.allowedLayerTransitionPointKeys.has(getXyPointKey(this.end)) + ) { + this.failed = true + this.error = `Layer transition at ${getXyPointKey( + this.end, + )} is not allowed` + return + } + this.mergedHdRoute.route.push({ + x: this.end.x, + y: this.end.y, + z: this.end.z, + }) + if ( + !this.mergedHdRoute.vias.some( + (via) => distance(via, this.end) <= GEOMETRIC_TOLERANCE, + ) + ) { + this.mergedHdRoute.vias.push({ x: this.end.x, y: this.end.y }) + } } this.solved = true diff --git a/lib/solvers/RouteStitchingSolver/routeStitchingEndpointHelpers.ts b/lib/solvers/RouteStitchingSolver/routeStitchingEndpointHelpers.ts index 4c77588a7..0f4ec8d75 100644 --- a/lib/solvers/RouteStitchingSolver/routeStitchingEndpointHelpers.ts +++ b/lib/solvers/RouteStitchingSolver/routeStitchingEndpointHelpers.ts @@ -195,6 +195,47 @@ export const snapIslandEndpointToNearestTerminal = (params: { : params.islandEndpoint } +/** + * Snaps both island endpoints without collapsing a partial island onto one + * physical terminal. When both endpoints choose the same terminal, keep the + * closer snap and leave the farther island endpoint available for stitching. + */ +export const snapIslandEndpointsToDistinctTerminals = (params: { + start: Point3 + end: Point3 + terminals: Point3[] +}) => { + const snappedStart = snapIslandEndpointToNearestTerminal({ + islandEndpoint: params.start, + terminals: params.terminals, + }) + const snappedEnd = snapIslandEndpointToNearestTerminal({ + islandEndpoint: params.end, + terminals: params.terminals, + }) + const getSnappedTerminalIdentity = (point: Point3) => { + const terminal = params.terminals.find( + (candidateTerminal) => candidateTerminal === point, + ) as (Point3 & { pcb_port_id?: string; pointId?: string }) | undefined + if (!terminal) return undefined + if (terminal.pcb_port_id) return `pcb_port:${terminal.pcb_port_id}` + if (terminal.pointId) return `point:${terminal.pointId}` + return `position:${terminal.x}:${terminal.y}:${terminal.z}` + } + const startTerminalIdentity = getSnappedTerminalIdentity(snappedStart) + const endTerminalIdentity = getSnappedTerminalIdentity(snappedEnd) + + if (startTerminalIdentity && startTerminalIdentity === endTerminalIdentity) { + const startSnapDistance = distance(params.start, snappedStart) + const endSnapDistance = distance(params.end, snappedEnd) + return startSnapDistance <= endSnapDistance + ? { start: snappedStart, end: params.end } + : { start: params.start, end: snappedEnd } + } + + return { start: snappedStart, end: snappedEnd } +} + /** * Returns the route islands on the deterministic endpoint path between the * chosen terminals. If the subset cannot actually stitch to both terminals, diff --git a/lib/testing/evaluate-relaxed-drc.ts b/lib/testing/evaluate-relaxed-drc.ts index b1ee76783..93711f97e 100644 --- a/lib/testing/evaluate-relaxed-drc.ts +++ b/lib/testing/evaluate-relaxed-drc.ts @@ -1,8 +1,28 @@ +import { + pointToSegmentClosestPoint, + pointToSegmentDistance, + segmentToSegmentMinDistance, +} from "@tscircuit/math-utils" import type { AnyCircuitElement } from "circuit-json" -import type { SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" +import { + type Obstacle, + type SimpleRouteJson, + type SimplifiedPcbTrace, + getConnectionPointLayers, +} from "lib/types" +import { getUniqueValidZLayersFromLayerNames } from "lib/utils/mapLayerNameToZ" +import { mapZToLayerName } from "lib/utils/mapZToLayerName" +import { resolvePreloadedTraceCanonicalNetIds } from "lib/utils/resolvePreloadedTraceCanonicalNetIds" import { RELAXED_DRC_OPTIONS } from "./drcPresets" -import { getDrcErrors, type GetDrcErrorsResult } from "./getDrcErrors" -import { convertToCircuitJson } from "./utils/convertToCircuitJson" +import { + type GetDrcErrorsResult, + VIA_OUTER_CLEARANCE_ERROR_PREFIX, + getDrcErrors, +} from "./getDrcErrors" +import { + type PcbViaWithContributingTraceIds, + convertToCircuitJson, +} from "./utils/convertToCircuitJson" /** Inputs used by the benchmark's relaxed DRC evaluation. */ export interface EvaluateRelaxedDrcInput { @@ -16,20 +36,1115 @@ export interface EvaluateRelaxedDrcResult extends GetDrcErrorsResult { circuitJson: AnyCircuitElement[] } -/** Converts routed traces and evaluates them using the benchmark relaxed DRC. */ +type CandidateViaOwnership = { + outerDiameterTraceIds: string[] + holeDiameterTraceIds: string[] +} + +type Point = { x: number; y: number } + +type LinearCopperPrimitive = { + kind: "wire" | "through_pad" + start: Point + end: Point + radius: number + layers: Set +} + +type CopperPrimitive = + | LinearCopperPrimitive + | { + kind: "via" + center: Point + radius: number + layers: Set + } + +type PhysicalTrace = { + pcbTraceId: string + canonicalNet: string + primitives: CopperPrimitive[] +} + +const VIA_DIMENSION_EPSILON = 1e-9 +const PHYSICAL_CONNECTIVITY_EPSILON = 1e-6 + +const getViaLayerSet = ( + layerNames: readonly string[], + layerCount: number, +): Set => { + const zLayers = getUniqueValidZLayersFromLayerNames(layerNames, layerCount) + if (zLayers.length === 0) return new Set() + const minimumZ = Math.min(...zLayers) + const maximumZ = Math.max(...zLayers) + return new Set( + Array.from({ length: maximumZ - minimumZ + 1 }, (_, index) => + mapZToLayerName(minimumZ + index, layerCount), + ), + ) +} + +const getPhysicalTraces = ( + circuitJson: AnyCircuitElement[], + layerCount: number, +): PhysicalTrace[] => { + const viaPrimitivesByTraceId = new Map() + for (const element of circuitJson) { + if (element.type !== "pcb_via") continue + const via = element as PcbViaWithContributingTraceIds + const contributingTraceIds = [ + ...new Set([ + ...(via.contributing_pcb_trace_ids ?? []), + ...(via.contributing_pcb_trace_via_dimensions ?? []).map( + (contribution) => contribution.pcb_trace_id, + ), + ...(typeof via.pcb_trace_id === "string" ? [via.pcb_trace_id] : []), + ]), + ] + const primitive: CopperPrimitive = { + kind: "via", + center: { x: via.x, y: via.y }, + radius: via.outer_diameter / 2, + layers: getViaLayerSet(via.layers, layerCount), + } + for (const traceId of contributingTraceIds) { + const primitives = viaPrimitivesByTraceId.get(traceId) ?? [] + primitives.push(primitive) + viaPrimitivesByTraceId.set(traceId, primitives) + } + } + + return circuitJson.flatMap((element): PhysicalTrace[] => { + if ( + element.type !== "pcb_trace" || + typeof element.pcb_trace_id !== "string" || + typeof element.source_trace_id !== "string" + ) { + return [] + } + + const primitives: CopperPrimitive[] = [ + ...(viaPrimitivesByTraceId.get(element.pcb_trace_id) ?? []), + ] + for (const routePoint of element.route) { + if (routePoint.route_type === "wire") { + primitives.push({ + kind: "wire", + start: { x: routePoint.x, y: routePoint.y }, + end: { x: routePoint.x, y: routePoint.y }, + radius: routePoint.width / 2, + layers: new Set([routePoint.layer]), + }) + } else if (routePoint.route_type === "through_pad") { + primitives.push({ + kind: "through_pad", + start: routePoint.start, + end: routePoint.end, + radius: routePoint.width / 2, + layers: getViaLayerSet( + [routePoint.start_layer, routePoint.end_layer], + layerCount, + ), + }) + } + } + for (let index = 0; index < element.route.length - 1; index += 1) { + const start = element.route[index] + const end = element.route[index + 1] + if ( + start?.route_type !== "wire" || + end?.route_type !== "wire" || + start.layer !== end.layer + ) { + continue + } + primitives.push({ + kind: "wire", + start: { x: start.x, y: start.y }, + end: { x: end.x, y: end.y }, + radius: Math.max(start.width, end.width) / 2, + layers: new Set([start.layer]), + }) + } + + return [ + { + pcbTraceId: element.pcb_trace_id, + canonicalNet: element.source_trace_id, + primitives, + }, + ] + }) +} + +const layersOverlap = (a: Set, b: Set): boolean => + [...a].some((layer) => b.has(layer)) + +const isLinearCopperPrimitive = ( + primitive: CopperPrimitive, +): primitive is LinearCopperPrimitive => primitive.kind !== "via" + +const primitivesTouch = ( + a: CopperPrimitive, + b: CopperPrimitive, + additionalClearance = 0, +): boolean => { + if (!layersOverlap(a.layers, b.layers)) return false + const maximumDistance = + a.radius + b.radius + additionalClearance + PHYSICAL_CONNECTIVITY_EPSILON + if (isLinearCopperPrimitive(a) && isLinearCopperPrimitive(b)) { + return ( + segmentToSegmentMinDistance(a.start, a.end, b.start, b.end) <= + maximumDistance + ) + } + if (isLinearCopperPrimitive(a) && b.kind === "via") { + return pointToSegmentDistance(b.center, a.start, a.end) <= maximumDistance + } + if (a.kind === "via" && isLinearCopperPrimitive(b)) { + return pointToSegmentDistance(a.center, b.start, b.end) <= maximumDistance + } + if (a.kind === "via" && b.kind === "via") { + return ( + Math.hypot(a.center.x - b.center.x, a.center.y - b.center.y) <= + maximumDistance + ) + } + return false +} + +const getLinearSegmentIntersection = ( + a: LinearCopperPrimitive, + b: LinearCopperPrimitive, +): Point | undefined => { + const aDelta = { x: a.end.x - a.start.x, y: a.end.y - a.start.y } + const bDelta = { x: b.end.x - b.start.x, y: b.end.y - b.start.y } + const denominator = aDelta.x * bDelta.y - aDelta.y * bDelta.x + if (Math.abs(denominator) <= PHYSICAL_CONNECTIVITY_EPSILON) return undefined + const startDelta = { + x: b.start.x - a.start.x, + y: b.start.y - a.start.y, + } + const aFraction = + (startDelta.x * bDelta.y - startDelta.y * bDelta.x) / denominator + const bFraction = + (startDelta.x * aDelta.y - startDelta.y * aDelta.x) / denominator + if ( + aFraction < -PHYSICAL_CONNECTIVITY_EPSILON || + aFraction > 1 + PHYSICAL_CONNECTIVITY_EPSILON || + bFraction < -PHYSICAL_CONNECTIVITY_EPSILON || + bFraction > 1 + PHYSICAL_CONNECTIVITY_EPSILON + ) { + return undefined + } + return { + x: a.start.x + aFraction * aDelta.x, + y: a.start.y + aFraction * aDelta.y, + } +} + +const getCopperOverlapCenter = ( + a: CopperPrimitive, + b: CopperPrimitive, +): Point => { + if (isLinearCopperPrimitive(a) && isLinearCopperPrimitive(b)) { + const intersection = getLinearSegmentIntersection(a, b) + if (intersection) return intersection + const closestPairs = [ + [a.start, pointToSegmentClosestPoint(a.start, b.start, b.end)], + [a.end, pointToSegmentClosestPoint(a.end, b.start, b.end)], + [pointToSegmentClosestPoint(b.start, a.start, a.end), b.start], + [pointToSegmentClosestPoint(b.end, a.start, a.end), b.end], + ] as const + const closestPair = closestPairs.reduce((best, pair) => + Math.hypot(pair[0].x - pair[1].x, pair[0].y - pair[1].y) < + Math.hypot(best[0].x - best[1].x, best[0].y - best[1].y) + ? pair + : best, + ) + return { + x: (closestPair[0].x + closestPair[1].x) / 2, + y: (closestPair[0].y + closestPair[1].y) / 2, + } + } + if (isLinearCopperPrimitive(a) && b.kind === "via") { + const closest = pointToSegmentClosestPoint(b.center, a.start, a.end) + return { x: (closest.x + b.center.x) / 2, y: (closest.y + b.center.y) / 2 } + } + if (a.kind === "via" && isLinearCopperPrimitive(b)) { + return getCopperOverlapCenter(b, a) + } + if (a.kind === "via" && b.kind === "via") { + return { + x: (a.center.x + b.center.x) / 2, + y: (a.center.y + b.center.y) / 2, + } + } + throw new Error("Unsupported copper primitive pair") +} + +const getThroughPadCopperOverlapErrors = ({ + circuitJson, + layerCount, + rawErrors, +}: { + circuitJson: AnyCircuitElement[] + layerCount: number + rawErrors: GetDrcErrorsResult["errors"] +}): GetDrcErrorsResult["errors"] => { + const hasThroughPadCopper = circuitJson.some( + (element) => + element.type === "pcb_trace" && + element.route.some( + (routePoint) => routePoint.route_type === "through_pad", + ), + ) + if (!hasThroughPadCopper) return [] + + const physicalTraces = getPhysicalTraces(circuitJson, layerCount) + const rawOverlapIds = new Set( + rawErrors.flatMap((error) => + "pcb_trace_error_id" in error && + typeof error.pcb_trace_error_id === "string" + ? [error.pcb_trace_error_id] + : [], + ), + ) + const errors: GetDrcErrorsResult["errors"] = [] + + for ( + let traceAIndex = 0; + traceAIndex < physicalTraces.length; + traceAIndex += 1 + ) { + const traceA = physicalTraces[traceAIndex]! + for ( + let traceBIndex = traceAIndex + 1; + traceBIndex < physicalTraces.length; + traceBIndex += 1 + ) { + const traceB = physicalTraces[traceBIndex]! + if ( + traceA.pcbTraceId === traceB.pcbTraceId || + traceA.canonicalNet === traceB.canonicalNet + ) { + continue + } + const overlapId = `overlap_${traceA.pcbTraceId}_${traceB.pcbTraceId}` + const reverseOverlapId = `overlap_${traceB.pcbTraceId}_${traceA.pcbTraceId}` + if (rawOverlapIds.has(overlapId) || rawOverlapIds.has(reverseOverlapId)) { + continue + } + + let overlappingPrimitives: [CopperPrimitive, CopperPrimitive] | undefined + for (const primitiveA of traceA.primitives) { + for (const primitiveB of traceB.primitives) { + if ( + primitiveA.kind !== "through_pad" && + primitiveB.kind !== "through_pad" + ) { + continue + } + if ( + primitivesTouch( + primitiveA, + primitiveB, + RELAXED_DRC_OPTIONS.traceClearance ?? 0, + ) + ) { + overlappingPrimitives = [primitiveA, primitiveB] + break + } + } + if (overlappingPrimitives) break + } + if (!overlappingPrimitives) continue + + errors.push({ + type: "pcb_trace_error", + error_type: "pcb_trace_error", + pcb_trace_error_id: overlapId, + pcb_trace_id: traceA.pcbTraceId, + source_trace_id: traceA.canonicalNet, + message: `PCB trace ${traceA.pcbTraceId} overlaps with ${traceB.pcbTraceId} through-pad copper (accidental contact)`, + center: getCopperOverlapCenter(...overlappingPrimitives), + pcb_component_ids: [], + pcb_port_ids: [], + }) + } + } + + return errors +} + +const rotatePointIntoObstacleCoordinates = ( + point: Point, + obstacle: Obstacle, +): Point => { + const radians = -((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180 + const deltaX = point.x - obstacle.center.x + const deltaY = point.y - obstacle.center.y + return { + x: deltaX * Math.cos(radians) - deltaY * Math.sin(radians), + y: deltaX * Math.sin(radians) + deltaY * Math.cos(radians), + } +} + +const pointIsInsideObstacle = (point: Point, obstacle: Obstacle): boolean => { + const localPoint = rotatePointIntoObstacleCoordinates(point, obstacle) + return ( + Math.abs(localPoint.x) <= obstacle.width / 2 + 1e-9 && + Math.abs(localPoint.y) <= obstacle.height / 2 + 1e-9 + ) +} + +const primitiveTouchesObstacle = ( + primitive: CopperPrimitive, + obstacle: Obstacle, +): boolean => { + if (![...primitive.layers].some((layer) => obstacle.layers.includes(layer))) { + return false + } + const halfWidth = obstacle.width / 2 + const halfHeight = obstacle.height / 2 + + if (primitive.kind === "via") { + const localCenter = rotatePointIntoObstacleCoordinates( + primitive.center, + obstacle, + ) + const deltaX = Math.max(Math.abs(localCenter.x) - halfWidth, 0) + const deltaY = Math.max(Math.abs(localCenter.y) - halfHeight, 0) + return ( + Math.hypot(deltaX, deltaY) <= + primitive.radius + PHYSICAL_CONNECTIVITY_EPSILON + ) + } + + const localStart = rotatePointIntoObstacleCoordinates( + primitive.start, + obstacle, + ) + const localEnd = rotatePointIntoObstacleCoordinates(primitive.end, obstacle) + if ( + (Math.abs(localStart.x) <= halfWidth && + Math.abs(localStart.y) <= halfHeight) || + (Math.abs(localEnd.x) <= halfWidth && Math.abs(localEnd.y) <= halfHeight) + ) { + return true + } + const corners = [ + { x: -halfWidth, y: -halfHeight }, + { x: halfWidth, y: -halfHeight }, + { x: halfWidth, y: halfHeight }, + { x: -halfWidth, y: halfHeight }, + ] + return corners.some((corner, index) => { + const nextCorner = corners[(index + 1) % corners.length]! + return ( + segmentToSegmentMinDistance(localStart, localEnd, corner, nextCorner) <= + primitive.radius + PHYSICAL_CONNECTIVITY_EPSILON + ) + }) +} + +const primitiveTouchesTerminalPoint = ( + primitive: CopperPrimitive, + point: Point, + pointLayers: Set, +): boolean => { + if (!layersOverlap(primitive.layers, pointLayers)) return false + if (isLinearCopperPrimitive(primitive)) { + return ( + pointToSegmentDistance(point, primitive.start, primitive.end) <= + primitive.radius + PHYSICAL_CONNECTIVITY_EPSILON + ) + } + return ( + Math.hypot(primitive.center.x - point.x, primitive.center.y - point.y) <= + primitive.radius + PHYSICAL_CONNECTIVITY_EPSILON + ) +} + +const getCanonicalConnectionName = ( + connection: SimpleRouteJson["connections"][number], +): string => + connection.__netConnectionName ?? + connection.__rootConnectionNames?.[0] ?? + connection.name + +const getPrimitiveCenter = (primitive: CopperPrimitive): Point => + primitive.kind === "via" + ? primitive.center + : { + x: (primitive.start.x + primitive.end.x) / 2, + y: (primitive.start.y + primitive.end.y) / 2, + } + +const getPrimitiveDistanceToPoint = ( + primitive: CopperPrimitive, + point: Point, +): number => + primitive.kind === "via" + ? Math.max( + 0, + Math.hypot(primitive.center.x - point.x, primitive.center.y - point.y) - + primitive.radius, + ) + : Math.max( + 0, + pointToSegmentDistance(point, primitive.start, primitive.end) - + primitive.radius, + ) + +const getCombinedCopperContinuityErrors = ({ + inputSrj, + srjWithPointPairs, + circuitJson, + candidateTraceIds, + canonicalNetByTraceId, +}: { + inputSrj: SimpleRouteJson + srjWithPointPairs: SimpleRouteJson + circuitJson: AnyCircuitElement[] + candidateTraceIds: Set + canonicalNetByTraceId: Map +}): GetDrcErrorsResult["errors"] => { + const candidateTraceIdsByCanonicalNet = new Map() + for (const traceId of candidateTraceIds) { + const canonicalNet = canonicalNetByTraceId.get(traceId) + if (!canonicalNet) continue + const owners = candidateTraceIdsByCanonicalNet.get(canonicalNet) ?? [] + owners.push(traceId) + candidateTraceIdsByCanonicalNet.set(canonicalNet, owners) + } + + const pointsByCanonicalNet = new Map< + string, + Array<{ + point: SimpleRouteJson["connections"][number]["pointsToConnect"][number] + connection: SimpleRouteJson["connections"][number] + }> + >() + for (const connection of inputSrj.connections) { + const canonicalNet = getCanonicalConnectionName(connection) + const points = pointsByCanonicalNet.get(canonicalNet) ?? [] + points.push( + ...connection.pointsToConnect.map((point) => ({ point, connection })), + ) + pointsByCanonicalNet.set(canonicalNet, points) + } + const expectedCanonicalNets = new Set() + for (const connection of srjWithPointPairs.connections) { + const canonicalNet = getCanonicalConnectionName(connection) + expectedCanonicalNets.add(canonicalNet) + if (!pointsByCanonicalNet.has(canonicalNet)) { + pointsByCanonicalNet.set( + canonicalNet, + connection.pointsToConnect.map((point) => ({ point, connection })), + ) + } + } + const physicalTraces = getPhysicalTraces(circuitJson, inputSrj.layerCount) + const errors: GetDrcErrorsResult["errors"] = [] + for (const canonicalNet of expectedCanonicalNets) { + const ownerTraceIds = + candidateTraceIdsByCanonicalNet.get(canonicalNet) ?? [] + const requiredPoints = pointsByCanonicalNet.get(canonicalNet) ?? [] + if (requiredPoints.length < 2) continue + const netTraces = physicalTraces.filter( + (trace) => trace.canonicalNet === canonicalNet, + ) + + const primitiveNodes = netTraces.flatMap((trace) => + trace.primitives.map((primitive) => ({ + primitive, + pcbTraceId: trace.pcbTraceId, + })), + ) + const parentByPrimitiveIndex = primitiveNodes.map((_, index) => index) + const findRoot = (primitiveIndex: number): number => { + const parent = parentByPrimitiveIndex[primitiveIndex] ?? primitiveIndex + if (parent === primitiveIndex) return primitiveIndex + const root = findRoot(parent) + parentByPrimitiveIndex[primitiveIndex] = root + return root + } + const union = (primitiveAIndex: number, primitiveBIndex: number) => { + const rootA = findRoot(primitiveAIndex) + const rootB = findRoot(primitiveBIndex) + if (rootA !== rootB) parentByPrimitiveIndex[rootB] = rootA + } + + for ( + let primitiveAIndex = 0; + primitiveAIndex < primitiveNodes.length; + primitiveAIndex += 1 + ) { + for ( + let primitiveBIndex = primitiveAIndex + 1; + primitiveBIndex < primitiveNodes.length; + primitiveBIndex += 1 + ) { + if ( + primitivesTouch( + primitiveNodes[primitiveAIndex]!.primitive, + primitiveNodes[primitiveBIndex]!.primitive, + ) + ) { + union(primitiveAIndex, primitiveBIndex) + } + } + } + + const touchedPrimitiveIndicesByRequiredPoint = requiredPoints.map( + ({ point, connection }) => { + const pointLayers = new Set(getConnectionPointLayers(point)) + const pointIdentityIds = new Set( + [point.pointId, point.pcb_port_id].filter( + (id): id is string => typeof id === "string", + ), + ) + const connectionIdentityIds = new Set([ + connection.name, + connection.__netConnectionName, + ...(connection.__rootConnectionNames ?? []), + canonicalNet, + ]) + const terminalObstacles = inputSrj.obstacles.filter((obstacle) => { + if ( + !obstacle.layers.some((layer) => pointLayers.has(layer)) || + !pointIsInsideObstacle(point, obstacle) + ) { + return false + } + return obstacle.connectedTo.some( + (id) => pointIdentityIds.has(id) || connectionIdentityIds.has(id), + ) + }) + const touchedPrimitiveIndices = primitiveNodes.flatMap( + ({ primitive }, primitiveIndex) => + primitiveTouchesTerminalPoint(primitive, point, pointLayers) || + terminalObstacles.some((obstacle) => + primitiveTouchesObstacle(primitive, obstacle), + ) + ? [primitiveIndex] + : [], + ) + for (const primitiveIndex of touchedPrimitiveIndices.slice(1)) { + union(touchedPrimitiveIndices[0]!, primitiveIndex) + } + return touchedPrimitiveIndices + }, + ) + + const referencePrimitiveIndex = touchedPrimitiveIndicesByRequiredPoint.find( + (primitiveIndices) => primitiveIndices.length > 0, + )?.[0] + const disconnectedPointIndex = + touchedPrimitiveIndicesByRequiredPoint.findIndex( + (primitiveIndices) => + primitiveIndices.length === 0 || + (referencePrimitiveIndex !== undefined && + findRoot(primitiveIndices[0]!) !== + findRoot(referencePrimitiveIndex)), + ) + + let disconnectedCandidatePrimitiveIndex = -1 + if (referencePrimitiveIndex !== undefined && disconnectedPointIndex < 0) { + const referenceRoot = findRoot(referencePrimitiveIndex) + disconnectedCandidatePrimitiveIndex = primitiveNodes.findIndex( + (node, primitiveIndex) => + candidateTraceIds.has(node.pcbTraceId) && + findRoot(primitiveIndex) !== referenceRoot, + ) + } + if ( + referencePrimitiveIndex !== undefined && + disconnectedPointIndex < 0 && + disconnectedCandidatePrimitiveIndex < 0 + ) { + continue + } + + const disconnectedPoint = + requiredPoints[Math.max(disconnectedPointIndex, 0)]?.point ?? + requiredPoints[0]!.point + let candidateOwnerTraceIds: string[] = [] + let errorCenter: Point = { + x: disconnectedPoint.x, + y: disconnectedPoint.y, + } + let message = `Combined routed and preloaded copper for ${canonicalNet} is not physically contiguous` + + if (disconnectedCandidatePrimitiveIndex >= 0) { + const disconnectedRoot = findRoot(disconnectedCandidatePrimitiveIndex) + candidateOwnerTraceIds = [ + ...new Set( + primitiveNodes.flatMap((node, primitiveIndex) => + candidateTraceIds.has(node.pcbTraceId) && + findRoot(primitiveIndex) === disconnectedRoot + ? [node.pcbTraceId] + : [], + ), + ), + ] + errorCenter = getPrimitiveCenter( + primitiveNodes[disconnectedCandidatePrimitiveIndex]!.primitive, + ) + message = `Candidate copper for ${canonicalNet} contains a fragment disconnected from its required terminals` + } else if (primitiveNodes.length > 0) { + const referenceRoot = + referencePrimitiveIndex === undefined + ? undefined + : findRoot(referencePrimitiveIndex) + candidateOwnerTraceIds = [ + ...new Set( + primitiveNodes.flatMap((node, primitiveIndex) => + candidateTraceIds.has(node.pcbTraceId) && + (referenceRoot === undefined || + findRoot(primitiveIndex) !== referenceRoot) + ? [node.pcbTraceId] + : [], + ), + ), + ] + if (candidateOwnerTraceIds.length === 0) { + const nearestCandidateNode = primitiveNodes + .filter((node) => candidateTraceIds.has(node.pcbTraceId)) + .map((node) => ({ + node, + distance: getPrimitiveDistanceToPoint( + node.primitive, + disconnectedPoint, + ), + })) + .sort((left, right) => left.distance - right.distance)[0]?.node + if (nearestCandidateNode) { + candidateOwnerTraceIds = [nearestCandidateNode.pcbTraceId] + } + } + } + + errors.push({ + type: "pcb_trace_error", + error_type: "pcb_trace_error", + pcb_trace_error_id: `missing_connection_combined_${canonicalNet}`, + pcb_trace_id: + candidateOwnerTraceIds[0] ?? ownerTraceIds[0] ?? canonicalNet, + source_trace_id: canonicalNet, + message, + center: errorCenter, + pcb_component_ids: [], + pcb_port_ids: requiredPoints.flatMap(({ point }) => + point.pcb_port_id ? [point.pcb_port_id] : [], + ), + candidate_pcb_trace_ids: candidateOwnerTraceIds, + } as GetDrcErrorsResult["errors"][number]) + } + + return errors +} + +const getCandidateViaOwnership = ( + via: PcbViaWithContributingTraceIds, + candidateTraceIds: Set, +): CandidateViaOwnership => { + const contributions = + via.contributing_pcb_trace_via_dimensions ?? + (typeof via.pcb_trace_id === "string" + ? [ + { + pcb_trace_id: via.pcb_trace_id, + outer_diameter: via.outer_diameter, + hole_diameter: via.hole_diameter, + }, + ] + : []) + const fixedContributions = contributions.filter( + (contribution) => !candidateTraceIds.has(contribution.pcb_trace_id), + ) + const maximumFixedOuterDiameter = Math.max( + Number.NEGATIVE_INFINITY, + ...fixedContributions.map((contribution) => contribution.outer_diameter), + ) + const maximumFixedHoleDiameter = Math.max( + Number.NEGATIVE_INFINITY, + ...fixedContributions.map((contribution) => contribution.hole_diameter), + ) + const candidateContributions = contributions.filter((contribution) => + candidateTraceIds.has(contribution.pcb_trace_id), + ) + + return { + outerDiameterTraceIds: [ + ...new Set( + candidateContributions + .filter( + (contribution) => + contribution.outer_diameter >= + via.outer_diameter - VIA_DIMENSION_EPSILON && + contribution.outer_diameter > + maximumFixedOuterDiameter + VIA_DIMENSION_EPSILON, + ) + .map((contribution) => contribution.pcb_trace_id), + ), + ], + holeDiameterTraceIds: [ + ...new Set( + candidateContributions + .filter( + (contribution) => + contribution.hole_diameter >= + via.hole_diameter - VIA_DIMENSION_EPSILON && + contribution.hole_diameter > + maximumFixedHoleDiameter + VIA_DIMENSION_EPSILON, + ) + .map((contribution) => contribution.pcb_trace_id), + ), + ], + } +} + +const getCandidateContinuityOwnerTraceIds = ( + error: Record, + canonicalNetByTraceId: Map, + candidateTraceIdsByCanonicalNet: Map, +): string[] => { + if (Array.isArray(error.candidate_pcb_trace_ids)) { + return error.candidate_pcb_trace_ids.filter( + (traceId): traceId is string => typeof traceId === "string", + ) + } + if ( + typeof error.pcb_trace_error_id !== "string" || + !error.pcb_trace_error_id.startsWith("missing_connection_") + ) { + return [] + } + + const canonicalNet = + typeof error.source_trace_id === "string" + ? error.source_trace_id + : typeof error.pcb_trace_id === "string" + ? canonicalNetByTraceId.get(error.pcb_trace_id) + : undefined + return canonicalNet + ? (candidateTraceIdsByCanonicalNet.get(canonicalNet) ?? []) + : [] +} + +const isRawTraceContinuityError = (error: object): boolean => { + const errorRecord = error as Record + return ( + typeof errorRecord.pcb_trace_error_id === "string" && + (errorRecord.pcb_trace_error_id.startsWith("missing_connection_") || + errorRecord.pcb_trace_error_id.startsWith("disconnected_endpoint_")) + ) +} + +const isCombinedContinuityError = (error: Record): boolean => + typeof error.pcb_trace_error_id === "string" && + error.pcb_trace_error_id.startsWith("missing_connection_combined_") + +const getOtherCopperIdFromOverlapError = ( + error: Record, +): string | undefined => { + if ( + typeof error.pcb_trace_id !== "string" || + typeof error.pcb_trace_error_id !== "string" + ) { + return undefined + } + const prefix = `overlap_${error.pcb_trace_id}_` + return error.pcb_trace_error_id.startsWith(prefix) + ? error.pcb_trace_error_id.slice(prefix.length) + : undefined +} + +const createPreloadedTraceId = ( + originalId: string, + index: number, + usedTraceIds: Set, +): string => { + const baseId = `preloaded_${index}_${originalId}` + let traceId = baseId + let suffix = 1 + + while (usedTraceIds.has(traceId)) { + traceId = `${baseId}_${suffix}` + suffix += 1 + } + + usedTraceIds.add(traceId) + return traceId +} + +/** + * Adds fixed copper to the candidate traces while keeping Circuit JSON ids + * unique. Fixed traces come first so a same-net coincident via retains fixed + * ownership when Circuit JSON merges duplicate via geometry. + */ +export const createRelaxedDrcTraceSet = ( + inputSrj: SimpleRouteJson, + traces: SimplifiedPcbTrace[], +): SimplifiedPcbTrace[] => { + const usedTraceIds = new Set(traces.map((trace) => trace.pcb_trace_id)) + const originalPreloadedTraces = inputSrj.traces ?? [] + const originalTraceIdCounts = new Map() + for (const trace of originalPreloadedTraces) { + originalTraceIdCounts.set( + trace.pcb_trace_id, + (originalTraceIdCounts.get(trace.pcb_trace_id) ?? 0) + 1, + ) + } + const renamedPreloadedTraceIds = originalPreloadedTraces.map((trace, index) => + createPreloadedTraceId(trace.pcb_trace_id, index, usedTraceIds), + ) + const canonicalNetIdByOriginalTraceId = + resolvePreloadedTraceCanonicalNetIds(inputSrj) + const preloadedTraceIdByOriginalId = new Map() + for (const [index, trace] of originalPreloadedTraces.entries()) { + if (originalTraceIdCounts.get(trace.pcb_trace_id) !== 1) continue + preloadedTraceIdByOriginalId.set( + trace.pcb_trace_id, + renamedPreloadedTraceIds[index]!, + ) + } + const preloadedTraces = originalPreloadedTraces.map((trace, index) => ({ + ...trace, + connection_name: + canonicalNetIdByOriginalTraceId.get(trace.pcb_trace_id) ?? + trace.connection_name, + pcb_trace_id: renamedPreloadedTraceIds[index]!, + ...(trace.connectsTo + ? { + connectsTo: trace.connectsTo.map( + (connectedId) => + preloadedTraceIdByOriginalId.get(connectedId) ?? connectedId, + ), + } + : {}), + })) + + return [...preloadedTraces, ...traces] +} + +const getCandidateViaPairOwnerTraceIds = ( + error: Record, + candidateViaOwnershipByViaId: Map, +): string[] => { + if (!Array.isArray(error.pcb_via_ids)) return [] + const useOuterDiameter = + typeof error.pcb_error_id === "string" && + error.pcb_error_id.startsWith(VIA_OUTER_CLEARANCE_ERROR_PREFIX) + return [ + ...new Set( + error.pcb_via_ids.flatMap((viaId) => { + if (typeof viaId !== "string") return [] + const ownership = candidateViaOwnershipByViaId.get(viaId) + return useOuterDiameter + ? (ownership?.outerDiameterTraceIds ?? []) + : (ownership?.holeDiameterTraceIds ?? []) + }), + ), + ] +} + +const errorInvolvesCandidateCopper = ( + error: Record, + candidateTraceIds: Set, + candidateViaOwnershipByViaId: Map, + canonicalNetByTraceId: Map, + candidateTraceIdsByCanonicalNet: Map, +): boolean => { + if (isCombinedContinuityError(error)) { + return true + } + if ( + typeof error.pcb_trace_id === "string" && + candidateTraceIds.has(error.pcb_trace_id) + ) { + return true + } + if ( + getCandidateContinuityOwnerTraceIds( + error, + canonicalNetByTraceId, + candidateTraceIdsByCanonicalNet, + ).length > 0 + ) { + return true + } + if ( + typeof error.pcb_via_id === "string" && + (candidateViaOwnershipByViaId.get(error.pcb_via_id)?.outerDiameterTraceIds + .length ?? 0) > 0 + ) { + return true + } + if ( + getCandidateViaPairOwnerTraceIds(error, candidateViaOwnershipByViaId) + .length > 0 + ) { + return true + } + + const otherCopperId = getOtherCopperIdFromOverlapError(error) + return Boolean( + otherCopperId && + (candidateTraceIds.has(otherCopperId) || + (candidateViaOwnershipByViaId.get(otherCopperId)?.outerDiameterTraceIds + .length ?? 0) > 0), + ) +} + +/** Converts routed and preloaded traces and evaluates the combined copper. */ export const evaluateRelaxedDrc = ({ inputSrj, srjWithPointPairs, traces, }: EvaluateRelaxedDrcInput): EvaluateRelaxedDrcResult => { - const circuitJson = convertToCircuitJson(srjWithPointPairs, traces, { + const combinedTraces = createRelaxedDrcTraceSet(inputSrj, traces) + const candidateTraceIds = new Set(traces.map((trace) => trace.pcb_trace_id)) + const preloadedTraceIds = new Set( + combinedTraces + .filter((trace) => !candidateTraceIds.has(trace.pcb_trace_id)) + .map((trace) => trace.pcb_trace_id), + ) + const circuitJson = convertToCircuitJson(srjWithPointPairs, combinedTraces, { minTraceWidth: inputSrj.minTraceWidth, minViaDiameter: inputSrj.minViaDiameter, originalSrj: inputSrj, + preloadedTraceIds, }) + const canonicalNetByTraceId = new Map() + const candidateTraceIdsByCanonicalNet = new Map() + for (const element of circuitJson) { + if ( + element.type !== "pcb_trace" || + typeof element.pcb_trace_id !== "string" || + typeof element.source_trace_id !== "string" + ) { + continue + } + canonicalNetByTraceId.set(element.pcb_trace_id, element.source_trace_id) + if (!candidateTraceIds.has(element.pcb_trace_id)) continue + const candidateOwners = + candidateTraceIdsByCanonicalNet.get(element.source_trace_id) ?? [] + candidateOwners.push(element.pcb_trace_id) + candidateTraceIdsByCanonicalNet.set( + element.source_trace_id, + candidateOwners, + ) + } + const candidateViaOwnershipByViaId = new Map() + for (const element of circuitJson) { + if (element.type !== "pcb_via") continue + const via = element as PcbViaWithContributingTraceIds + const ownership = getCandidateViaOwnership(via, candidateTraceIds) + if ( + ownership.outerDiameterTraceIds.length > 0 || + ownership.holeDiameterTraceIds.length > 0 + ) { + candidateViaOwnershipByViaId.set(via.pcb_via_id, ownership) + } + } + const drcResult = getDrcErrors(circuitJson, RELAXED_DRC_OPTIONS) + const throughPadCopperOverlapErrors = getThroughPadCopperOverlapErrors({ + circuitJson, + layerCount: inputSrj.layerCount, + rawErrors: drcResult.errors, + }) + const combinedCopperContinuityErrors = getCombinedCopperContinuityErrors({ + inputSrj, + srjWithPointPairs, + circuitJson, + candidateTraceIds, + canonicalNetByTraceId, + }) + const shouldIncludeError = (error: object) => + errorInvolvesCandidateCopper( + error as Record, + candidateTraceIds, + candidateViaOwnershipByViaId, + canonicalNetByTraceId, + candidateTraceIdsByCanonicalNet, + ) + const annotateCandidateTraceOwnership = (error: T): T => { + const errorRecord = error as Record + const ownerTraceIds = new Set() + if ( + typeof errorRecord.pcb_trace_id === "string" && + candidateTraceIds.has(errorRecord.pcb_trace_id) + ) { + ownerTraceIds.add(errorRecord.pcb_trace_id) + } + for (const ownerTraceId of getCandidateContinuityOwnerTraceIds( + errorRecord, + canonicalNetByTraceId, + candidateTraceIdsByCanonicalNet, + )) { + ownerTraceIds.add(ownerTraceId) + } + if (typeof errorRecord.pcb_via_id === "string") { + for (const ownerTraceId of candidateViaOwnershipByViaId.get( + errorRecord.pcb_via_id, + )?.outerDiameterTraceIds ?? []) { + ownerTraceIds.add(ownerTraceId) + } + } + for (const ownerTraceId of getCandidateViaPairOwnerTraceIds( + errorRecord, + candidateViaOwnershipByViaId, + )) { + ownerTraceIds.add(ownerTraceId) + } + const otherCopperId = getOtherCopperIdFromOverlapError(errorRecord) + if (otherCopperId) { + if (candidateTraceIds.has(otherCopperId)) { + ownerTraceIds.add(otherCopperId) + } + for (const ownerTraceId of candidateViaOwnershipByViaId.get(otherCopperId) + ?.outerDiameterTraceIds ?? []) { + ownerTraceIds.add(ownerTraceId) + } + } + + return ownerTraceIds.size > 0 + ? ({ + ...error, + candidate_pcb_trace_ids: [...ownerTraceIds], + } as T) + : error + } return { circuitJson, - ...getDrcErrors(circuitJson, RELAXED_DRC_OPTIONS), + errors: [ + ...drcResult.errors.filter((error) => !isRawTraceContinuityError(error)), + ...throughPadCopperOverlapErrors, + ...combinedCopperContinuityErrors, + ] + .filter(shouldIncludeError) + .map(annotateCandidateTraceOwnership), + errorsWithCenters: [ + ...drcResult.errorsWithCenters.filter( + (error) => !isRawTraceContinuityError(error), + ), + ...(throughPadCopperOverlapErrors as GetDrcErrorsResult["errorsWithCenters"]), + ...(combinedCopperContinuityErrors as GetDrcErrorsResult["errorsWithCenters"]), + ] + .filter(shouldIncludeError) + .map(annotateCandidateTraceOwnership), + locationAwareErrors: [ + ...drcResult.locationAwareErrors.filter( + (error) => !isRawTraceContinuityError(error), + ), + ...(throughPadCopperOverlapErrors as GetDrcErrorsResult["locationAwareErrors"]), + ...(combinedCopperContinuityErrors as GetDrcErrorsResult["locationAwareErrors"]), + ] + .filter(shouldIncludeError) + .map(annotateCandidateTraceOwnership), } } diff --git a/lib/testing/getDrcErrors.ts b/lib/testing/getDrcErrors.ts index 5eb3dcb98..e9095a903 100644 --- a/lib/testing/getDrcErrors.ts +++ b/lib/testing/getDrcErrors.ts @@ -26,6 +26,7 @@ type PcbViaWithTraceId = CircuitJsonElement & { pcb_via_id: string pcb_trace_id: string } +type PcbViaElement = Extract type DrcError = | PcbTraceError @@ -53,6 +54,70 @@ export interface GetDrcErrorsOptions { includeTypedTraceClearance?: boolean } +export const VIA_OUTER_CLEARANCE_ERROR_PREFIX = + "different_net_vias_outer_clearance_" + +const getLayerName = (layer: PcbViaElement["layers"][number]): string => layer + +const viasShareLayer = (viaA: PcbViaElement, viaB: PcbViaElement): boolean => { + const viaALayers = new Set(viaA.layers.map(getLayerName)) + return viaB.layers.some((layer) => viaALayers.has(getLayerName(layer))) +} + +/** + * The manufacturing via-spacing checks use drill-hole edges. Different nets + * also need copper-to-copper clearance between their outer annuli. + */ +const checkDifferentNetViaOuterClearance = ( + circuitJson: CircuitJson, + connMap: ConnectivityMap, + minimumClearance: number, +): PcbViaClearanceError[] => { + const vias = circuitJson.filter( + (element): element is PcbViaElement => element.type === "pcb_via", + ) + const errors: PcbViaClearanceError[] = [] + + for (let viaAIndex = 0; viaAIndex < vias.length; viaAIndex += 1) { + const viaA = vias[viaAIndex]! + for ( + let viaBIndex = viaAIndex + 1; + viaBIndex < vias.length; + viaBIndex += 1 + ) { + const viaB = vias[viaBIndex]! + if ( + !viasShareLayer(viaA, viaB) || + connMap.areIdsConnected(viaA.pcb_via_id, viaB.pcb_via_id) + ) { + continue + } + const actualClearance = + Math.hypot(viaA.x - viaB.x, viaA.y - viaB.y) - + viaA.outer_diameter / 2 - + viaB.outer_diameter / 2 + if (actualClearance + 1e-9 >= minimumClearance) continue + + const sortedViaIds = [viaA.pcb_via_id, viaB.pcb_via_id].sort() + errors.push({ + type: "pcb_via_clearance_error", + error_type: "pcb_via_clearance_error", + pcb_error_id: `${VIA_OUTER_CLEARANCE_ERROR_PREFIX}${sortedViaIds.join("_")}`, + pcb_via_ids: [viaA.pcb_via_id, viaB.pcb_via_id], + minimum_clearance: minimumClearance, + actual_clearance: actualClearance, + pcb_center: { + x: (viaA.x + viaB.x) / 2, + y: (viaA.y + viaB.y) / 2, + }, + message: `Vias ${viaA.pcb_via_id} and ${viaB.pcb_via_id} from different nets have ${actualClearance.toFixed(3)}mm outer-copper clearance (minimum: ${minimumClearance.toFixed(3)}mm)`, + }) + } + } + + return errors +} + const createDrcConnectivityMap = ( circuitJson: CircuitJson, ): ConnectivityMap => { @@ -104,6 +169,11 @@ export const getDrcErrors = ( connMap, minClearance: viaClearance, }), + ...checkDifferentNetViaOuterClearance( + circuitJson, + connMap, + options.traceClearance ?? MIN_VIA_TO_VIA_CLEARANCE, + ), ] const errors: DrcError[] = [ diff --git a/lib/testing/utils/convertToCircuitJson.ts b/lib/testing/utils/convertToCircuitJson.ts index 857cc0c1e..d488aad0a 100644 --- a/lib/testing/utils/convertToCircuitJson.ts +++ b/lib/testing/utils/convertToCircuitJson.ts @@ -3,8 +3,26 @@ import { Obstacle, SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" import { HighDensityRoute } from "lib/types/high-density-types" import { getConnectionPointLayers } from "lib/types/srj-types" import { getViaDimensions } from "lib/utils/getViaDimensions" +import { getUniqueValidZLayersFromLayerNames } from "lib/utils/mapLayerNameToZ" import type { LayerName } from "lib/utils/mapZToLayerName" import { mapZToLayerName } from "lib/utils/mapZToLayerName" +import { resolvePreloadedTraceCanonicalNetIds } from "lib/utils/resolvePreloadedTraceCanonicalNetIds" + +export type PcbViaTraceContribution = { + pcb_trace_id: string + outer_diameter: number + hole_diameter: number +} + +export type PcbViaWithContributingTraceIds = PcbVia & { + contributing_pcb_trace_ids?: string[] + contributing_pcb_trace_via_dimensions?: PcbViaTraceContribution[] +} + +type ResolveMappedConnectionName = ( + connectionName: string, + pcbTraceId: string, +) => string | undefined /** * Convert a simplified PCB trace from the autorouter to a circuit-json compatible PCB trace @@ -54,8 +72,23 @@ function convertSimplifiedPcbTraceToCircuitJson( from_layer: segment.from_layer, to_layer: segment.to_layer, } + } else if (segment.route_type === "through_obstacle") { + if ( + !isLayerName(segment.from_layer) || + !isLayerName(segment.to_layer) + ) { + return null + } + return { + route_type: "through_pad" as const, + start: segment.start, + end: segment.end, + width: segment.width, + start_layer: segment.from_layer, + end_layer: segment.to_layer, + } } else { - // jumper/through_obstacle - skip for now as circuit-json doesn't support these route types + // Jumpers are represented by separate off-board components, not PCB copper. return null } }) @@ -217,6 +250,8 @@ function createSourceTraces( srj: SimpleRouteJson, hdRoutes: SimplifiedPcbTrace[] | HighDensityRoute[], obstacleSrj: SimpleRouteJson = srj, + originalSrj?: SimpleRouteJson, + resolveMappedConnectionName: ResolveMappedConnectionName = () => undefined, ): AnyCircuitElement[] { const sourceTraces: AnyCircuitElement[] = [] const sourceTraceById = new Map() @@ -280,18 +315,25 @@ function createSourceTraces( ) } - for (const hdRoute of hdRoutes) { + for (const [routeIndex, hdRoute] of hdRoutes.entries()) { const routeName = (hdRoute as SimplifiedPcbTrace).connection_name ?? (hdRoute as HighDensityRoute).connectionName if (!routeName || hdRoute.route.length === 0) continue + const pcbTraceId = + "type" in hdRoute && hdRoute.type === "pcb_trace" + ? hdRoute.pcb_trace_id + : `trace_${routeIndex}` + const canonicalRouteName = + resolveMappedConnectionName(routeName, pcbTraceId) ?? routeName const endpoints = [ getRouteEndpoint(hdRoute, "start"), getRouteEndpoint(hdRoute, "end"), ].filter((endpoint): endpoint is RouteEndpoint => endpoint !== undefined) const endpointConnectivity = - endpointConnectivityByRouteName.get(routeName) ?? new Set() + endpointConnectivityByRouteName.get(canonicalRouteName) ?? + new Set() for (const endpoint of endpoints) { for (const obstacle of obstacleSrj.obstacles) { @@ -302,11 +344,21 @@ function createSourceTraces( } } } - endpointConnectivityByRouteName.set(routeName, endpointConnectivity) + endpointConnectivityByRouteName.set( + canonicalRouteName, + endpointConnectivity, + ) } + // Include original connections because point-pair conversion can omit nets + // that are already represented by preloaded copper. + const sourceConnections = [ + ...srj.connections, + ...(originalSrj?.connections ?? []), + ] + // Process each connection to create a source_trace - srj.connections.forEach((connection) => { + sourceConnections.forEach((connection) => { // Extract port IDs from the connection points const connectedPortIds = connection.pointsToConnect .filter((point) => point.pcb_port_id) @@ -318,8 +370,8 @@ function createSourceTraces( connection.__netConnectionName || connection.__rootConnectionNames?.[0] || connection.name - const connectionNames = new Set([ - connection.name, + const canonicalConnectionNames = new Set([ + netConnectionName, connection.__netConnectionName, ...(connection.__rootConnectionNames ?? []), ]) @@ -331,7 +383,7 @@ function createSourceTraces( .filter((name): name is string => Boolean(name)) .filter((name) => name !== netConnectionName), ) - for (const connectionName of connectionNames) { + for (const connectionName of canonicalConnectionNames) { if (!connectionName) continue for (const connectivityId of endpointConnectivityByRouteName.get( connectionName, @@ -452,6 +504,23 @@ const layerNames = new Set([ const isLayerName = (layer: string): layer is LayerName => layerNames.has(layer) +const getLayerNamesBetween = ( + fromLayer: string, + toLayer: string, + layerCount: number, +): LayerName[] => { + const zLayers = getUniqueValidZLayersFromLayerNames( + [fromLayer, toLayer], + layerCount, + ) + if (zLayers.length === 0) return [] + const minimumZ = Math.min(...zLayers) + const maximumZ = Math.max(...zLayers) + return Array.from({ length: maximumZ - minimumZ + 1 }, (_, index) => + mapZToLayerName(minimumZ + index, layerCount), + ) +} + /** * Create pad-like circuit-json elements from SRJ obstacles. * Multi-layer obstacles represent plated holes and must not be deduped away @@ -611,9 +680,93 @@ function extractViasFromRoutes( layerCount: number, minViaDiameter = 0.3, minViaHoleDiameter = minViaDiameter * 0.5, + resolveMappedConnectionName: ResolveMappedConnectionName = () => undefined, + reservedCircuitElementIds: Set = new Set(), ): PcbVia[] { - const vias: PcbVia[] = [] - const viaLocations = new Set() // Track unique via locations + const vias: PcbViaWithContributingTraceIds[] = [] + const viaIndexByLocation = new Map() + let nextViaId = 0 + const createPcbViaId = () => { + let pcbViaId = `via_${nextViaId}` + while (reservedCircuitElementIds.has(pcbViaId)) { + nextViaId += 1 + pcbViaId = `via_${nextViaId}` + } + nextViaId += 1 + reservedCircuitElementIds.add(pcbViaId) + return pcbViaId + } + const addOrMergeVia = ( + locationKey: string, + via: Omit, + ) => { + const existingViaIndex = viaIndexByLocation.get(locationKey) + if (existingViaIndex === undefined) { + const pcbVia = { + ...via, + pcb_via_id: createPcbViaId(), + ...(typeof via.pcb_trace_id === "string" + ? { + contributing_pcb_trace_via_dimensions: [ + { + pcb_trace_id: via.pcb_trace_id, + outer_diameter: via.outer_diameter, + hole_diameter: via.hole_diameter, + }, + ], + } + : {}), + } as PcbViaWithContributingTraceIds + viaIndexByLocation.set(locationKey, vias.length) + vias.push(pcbVia) + return + } + + const existingVia = vias[existingViaIndex] + if (!existingVia) return + if (typeof via.pcb_trace_id === "string") { + const contributions = + existingVia.contributing_pcb_trace_via_dimensions ?? [] + const existingContribution = contributions.find( + (contribution) => contribution.pcb_trace_id === via.pcb_trace_id, + ) + if (existingContribution) { + existingContribution.outer_diameter = Math.max( + existingContribution.outer_diameter, + via.outer_diameter, + ) + existingContribution.hole_diameter = Math.max( + existingContribution.hole_diameter, + via.hole_diameter, + ) + } else { + contributions.push({ + pcb_trace_id: via.pcb_trace_id, + outer_diameter: via.outer_diameter, + hole_diameter: via.hole_diameter, + }) + } + existingVia.contributing_pcb_trace_via_dimensions = contributions + } + existingVia.outer_diameter = Math.max( + existingVia.outer_diameter, + via.outer_diameter, + ) + existingVia.hole_diameter = Math.max( + existingVia.hole_diameter, + via.hole_diameter, + ) + if (typeof via.pcb_trace_id === "string") { + const existingContributorTraceIds = + existingVia.contributing_pcb_trace_ids ?? + (typeof existingVia.pcb_trace_id === "string" + ? [existingVia.pcb_trace_id] + : []) + existingVia.contributing_pcb_trace_ids = [ + ...new Set([...existingContributorTraceIds, via.pcb_trace_id]), + ] + } + } if (routes.length > 0) { if ("type" in routes[0] && routes[0].type === "pcb_trace") { @@ -630,20 +783,28 @@ function extractViasFromRoutes( const viaDiameter = segment.via_diameter ?? minViaDiameter const viaHoleDiameter = segment.via_hole_diameter ?? minViaHoleDiameter - const locationKey = `${segment.x},${segment.y},${segment.from_layer},${segment.to_layer}` - if (!viaLocations.has(locationKey)) { - vias.push({ - type: "pcb_via", - pcb_via_id: `via_${vias.length}`, - pcb_trace_id: trace.pcb_trace_id, - x: segment.x, - y: segment.y, - outer_diameter: viaDiameter, - hole_diameter: viaHoleDiameter, - layers: [segment.from_layer, segment.to_layer], - }) - viaLocations.add(locationKey) - } + const routeNetId = + resolveMappedConnectionName( + trace.connection_name, + trace.pcb_trace_id, + ) ?? trace.connection_name + const viaLayerKey = [segment.from_layer, segment.to_layer] + .sort() + .join(",") + const locationKey = `${routeNetId},${segment.x},${segment.y},${viaLayerKey}` + addOrMergeVia(locationKey, { + type: "pcb_via", + pcb_trace_id: trace.pcb_trace_id, + x: segment.x, + y: segment.y, + outer_diameter: viaDiameter, + hole_diameter: viaHoleDiameter, + layers: getLayerNamesBetween( + segment.from_layer, + segment.to_layer, + layerCount, + ), + }) } }) }) @@ -665,21 +826,23 @@ function extractViasFromRoutes( ) { const fromLayer = mapZToLayerName(prevPoint.z, layerCount) const toLayer = mapZToLayerName(currPoint.z, layerCount) - const locationKey = `${currPoint.x},${currPoint.y},${fromLayer},${toLayer}` - - if (!viaLocations.has(locationKey)) { - vias.push({ - type: "pcb_via", - pcb_via_id: `via_${vias.length}`, - pcb_trace_id: traceId, - x: currPoint.x, - y: currPoint.y, - outer_diameter: viaDiameter, - hole_diameter: viaHoleDiameter, - layers: [fromLayer, toLayer], - }) - viaLocations.add(locationKey) - } + const routeNetId = + resolveMappedConnectionName(route.connectionName, traceId) ?? + route.rootConnectionName ?? + route.connectionName ?? + traceId + const viaLayerKey = [fromLayer, toLayer].sort().join(",") + const locationKey = `${routeNetId},${currPoint.x},${currPoint.y},${viaLayerKey}` + + addOrMergeVia(locationKey, { + type: "pcb_via", + pcb_trace_id: traceId, + x: currPoint.x, + y: currPoint.y, + outer_diameter: viaDiameter, + hole_diameter: viaHoleDiameter, + layers: getLayerNamesBetween(fromLayer, toLayer, layerCount), + }) } } }) @@ -699,6 +862,7 @@ export type ConvertToCircuitJsonOptions = { minViaDiameter?: number minViaHoleDiameter?: number originalSrj?: SimpleRouteJson + preloadedTraceIds?: ReadonlySet } export function convertToCircuitJson( @@ -711,6 +875,7 @@ export function convertToCircuitJson( minViaDiameter, minViaHoleDiameter, originalSrj, + preloadedTraceIds = new Set(), } = options const viaDimensions = getViaDimensions(srjWithPointPairs) const resolvedMinViaDiameter = minViaDiameter ?? viaDimensions.padDiameter @@ -727,9 +892,61 @@ export function convertToCircuitJson( // Start with empty circuit JSON const circuitJson: AnyCircuitElement[] = [] const circuitSrj = originalSrj ?? srjWithPointPairs + const createConnectionMap = (connectionSrj: SimpleRouteJson) => { + const connectionMap = new Map() + connectionSrj.connections.forEach((conn) => { + connectionMap.set( + conn.name, + conn.__netConnectionName || + conn.__rootConnectionNames?.[0] || + conn.name, + ) + }) + return connectionMap + } + const originalConnectionMap = originalSrj + ? createConnectionMap(originalSrj) + : new Map() + const pointPairConnectionMap = createConnectionMap(srjWithPointPairs) + const inferredPreloadedConnectionByTraceId = new Map() + if ( + originalSrj && + routes.length > 0 && + "type" in routes[0] && + routes[0].type === "pcb_trace" + ) { + const preloadedTraces = (routes as SimplifiedPcbTrace[]).filter((trace) => + preloadedTraceIds.has(trace.pcb_trace_id), + ) + for (const [traceId, canonicalName] of resolvePreloadedTraceCanonicalNetIds( + { + ...originalSrj, + traces: preloadedTraces, + }, + )) { + inferredPreloadedConnectionByTraceId.set(traceId, canonicalName) + } + } + const resolveMappedConnectionName = ( + connectionName: string, + pcbTraceId: string, + ): string | undefined => + preloadedTraceIds.has(pcbTraceId) + ? (originalConnectionMap.get(connectionName) ?? + inferredPreloadedConnectionByTraceId.get(pcbTraceId)) + : (pointPairConnectionMap.get(connectionName) ?? + originalConnectionMap.get(connectionName)) // Add source traces from connection information - circuitJson.push(...createSourceTraces(srjWithPointPairs, routes, circuitSrj)) + circuitJson.push( + ...createSourceTraces( + srjWithPointPairs, + routes, + circuitSrj, + originalSrj, + resolveMappedConnectionName, + ), + ) // Add PCB ports for connection points circuitJson.push(...createPcbPorts(circuitSrj)) @@ -737,35 +954,19 @@ export function convertToCircuitJson( // Add PCB pads / plated holes represented by SRJ obstacles circuitJson.push(...createPcbPadElements(circuitSrj)) - // Extract and add vias as independent pcb_via elements - circuitJson.push( - ...extractViasFromRoutes( - routes, - srjWithPointPairs.layerCount, - resolvedMinViaDiameter, - resolvedMinViaHoleDiameter, - ), - ) - - // Build a map of connection names to simplify lookups - const connectionMap = new Map() - srjWithPointPairs.connections.forEach((conn) => { - connectionMap.set( - conn.name, - conn.__netConnectionName || conn.__rootConnectionNames?.[0] || conn.name, - ) - }) - - // Process routes based on their type + // Convert PCB traces before allocating via ids so generated ids can avoid + // every id already present in the resulting Circuit JSON. + const convertedPcbTraces: AnyCircuitElement[] = [] if (routes.length > 0) { if ("type" in routes[0] && routes[0].type === "pcb_trace") { // Handle SimplifiedPcbTraces ;(routes as SimplifiedPcbTrace[]).forEach((trace) => { const connectionName = trace.connection_name - circuitJson.push( + convertedPcbTraces.push( convertSimplifiedPcbTraceToCircuitJson( trace, - connectionMap.get(connectionName) || connectionName, + resolveMappedConnectionName(connectionName, trace.pcb_trace_id) ?? + connectionName, ) as AnyCircuitElement, ) }) @@ -776,14 +977,37 @@ export function convertToCircuitJson( const traces = convertHdRouteToCircuitJsonTraces( route, `trace_${index}`, - connectionMap.get(connectionName) || connectionName, + resolveMappedConnectionName(connectionName, `trace_${index}`) ?? + connectionName, srjWithPointPairs.layerCount, minTraceWidth, ) - circuitJson.push(...(traces as AnyCircuitElement[])) + convertedPcbTraces.push(...(traces as AnyCircuitElement[])) }) } } + const reservedCircuitElementIds = new Set() + for (const element of [...circuitJson, ...convertedPcbTraces]) { + for (const [propertyName, value] of Object.entries(element)) { + if (propertyName.endsWith("_id") && typeof value === "string") { + reservedCircuitElementIds.add(value) + } + } + } + + // Extract and add vias as independent pcb_via elements. + circuitJson.push( + ...extractViasFromRoutes( + routes, + srjWithPointPairs.layerCount, + resolvedMinViaDiameter, + resolvedMinViaHoleDiameter, + resolveMappedConnectionName, + reservedCircuitElementIds, + ), + ) + circuitJson.push(...convertedPcbTraces) + return circuitJson } diff --git a/lib/types/capacity-mesh-types.ts b/lib/types/capacity-mesh-types.ts index da99c6073..348a974f6 100644 --- a/lib/types/capacity-mesh-types.ts +++ b/lib/types/capacity-mesh-types.ts @@ -34,6 +34,14 @@ export interface CapacityMeshNode { _soicRegionType?: "center" | "pad" | "pad-gap" _isComponentTopologyNode?: boolean _connectedTo?: string[] + /** + * Canonical net ids for fixed, preloaded copper occupying this node. + * + * This is kept separate from `_connectedTo` because those ids are resolved + * through the active point-pair connectivity map. A generated point-pair + * name can collide with an unrelated original fixed-net name. + */ + _preloadedFixedNetIds?: string[] _parent?: CapacityMeshNode } diff --git a/lib/utils/convertHdRouteToSimplifiedRoute.ts b/lib/utils/convertHdRouteToSimplifiedRoute.ts index 1073f9323..3ba07110d 100644 --- a/lib/utils/convertHdRouteToSimplifiedRoute.ts +++ b/lib/utils/convertHdRouteToSimplifiedRoute.ts @@ -240,6 +240,7 @@ export const convertHdRouteToSimplifiedRoute = ( let currentLayerPoints: Point[] = [] let currentZ = hdRoute.route[0].z + const emittedViaTransitions = new Set() // Add all points to their respective layer segments for (let i = 0; i < hdRoute.route.length; i++) { @@ -281,9 +282,14 @@ export const convertHdRouteToSimplifiedRoute = ( Math.abs(via.x - point.x) < 0.001 && Math.abs(via.y - point.y) < 0.001, ) + const viaTransitionKey = `${point.x}:${point.y}:${Math.min( + currentZ, + point.z, + )}:${Math.max(currentZ, point.z)}` // Add a via if one exists - if (viaExists) { + if (viaExists && !emittedViaTransitions.has(viaTransitionKey)) { + emittedViaTransitions.add(viaTransitionKey) result.push({ route_type: "via", x: point.x, diff --git a/lib/utils/convertSrjTracesToObstacles.ts b/lib/utils/convertSrjTracesToObstacles.ts index 80e8751a3..0acc2c501 100644 --- a/lib/utils/convertSrjTracesToObstacles.ts +++ b/lib/utils/convertSrjTracesToObstacles.ts @@ -1,17 +1,26 @@ import type { Obstacle, SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" import { getViaDimensions } from "lib/utils/getViaDimensions" +import { JUMPER_DIMENSIONS } from "lib/utils/jumperSizes" import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" import { mapZToLayerName } from "lib/utils/mapZToLayerName" type RoutePoint = SimplifiedPcbTrace["route"][number] type WireRoutePoint = Extract type ViaRoutePoint = Extract +type JumperRoutePoint = Extract type ThroughObstacleRoutePoint = Extract< RoutePoint, { route_type: "through_obstacle" } > const MIN_OBSTACLE_DIMENSION = 0.001 +const JUMPER_ENDPOINT_TOLERANCE = 0.01 + +type TraceObstacleOptions = { + includeSquareCaps?: boolean + includeConnectionNameInConnectedTo?: boolean + modelJumperPads?: boolean +} const isWireRoutePoint = (point: RoutePoint): point is WireRoutePoint => point.route_type === "wire" @@ -19,6 +28,9 @@ const isWireRoutePoint = (point: RoutePoint): point is WireRoutePoint => const isViaRoutePoint = (point: RoutePoint): point is ViaRoutePoint => point.route_type === "via" +const isJumperRoutePoint = (point: RoutePoint): point is JumperRoutePoint => + point.route_type === "jumper" + const isThroughObstacleRoutePoint = ( point: RoutePoint, ): point is ThroughObstacleRoutePoint => point.route_type === "through_obstacle" @@ -45,6 +57,7 @@ const createSegmentObstacle = ({ width, layer, connectedTo, + includeSquareCaps, }: { obstacleId: string start: { x: number; y: number } @@ -52,6 +65,7 @@ const createSegmentObstacle = ({ width: number layer: string connectedTo: string[] + includeSquareCaps: boolean }): Obstacle | null => { const dx = end.x - start.x const dy = end.y - start.y @@ -67,15 +81,70 @@ const createSegmentObstacle = ({ x: (start.x + end.x) / 2, y: (start.y + end.y) / 2, }, - width: length, + // Pipeline9 uses square-cap rectangles so the projected hypergraph + // reservation includes one trace radius beyond each route point. Other + // pipelines retain their legacy centerline obstacle geometry. + width: + length + + (includeSquareCaps ? Math.max(width, MIN_OBSTACLE_DIMENSION) : 0), height: Math.max(width, MIN_OBSTACLE_DIMENSION), ccwRotationDegrees: (Math.atan2(dy, dx) * 180) / Math.PI, connectedTo, } } +const pointsMatch = ( + left: { x: number; y: number }, + right: { x: number; y: number }, +): boolean => + Math.abs(left.x - right.x) < JUMPER_ENDPOINT_TOLERANCE && + Math.abs(left.y - right.y) < JUMPER_ENDPOINT_TOLERANCE + +const wireSegmentSpansJumper = ( + start: WireRoutePoint, + end: WireRoutePoint, + jumpers: JumperRoutePoint[], +): boolean => + jumpers.some( + (jumper) => + (pointsMatch(start, jumper.start) && pointsMatch(end, jumper.end)) || + (pointsMatch(start, jumper.end) && pointsMatch(end, jumper.start)), + ) + +const createJumperPadObstacles = ({ + traceId, + traceIndex, + jumper, + pointIndex, + connectedTo, +}: { + traceId: string + traceIndex: number + jumper: JumperRoutePoint + pointIndex: number + connectedTo: string[] +}): Obstacle[] => { + const dimensions = + JUMPER_DIMENSIONS[jumper.footprint] ?? JUMPER_DIMENSIONS["0603"] + const dx = jumper.end.x - jumper.start.x + const dy = jumper.end.y - jumper.start.y + const rotationDegrees = (Math.atan2(dy, dx) * 180) / Math.PI + + return [jumper.start, jumper.end].map((center, padIndex) => ({ + obstacleId: `trace_obstacle_${traceId}_${traceIndex}_${pointIndex}_jumper_pad_${padIndex}`, + type: "rect", + layers: [jumper.layer], + center: { ...center }, + width: Math.max(dimensions.padLength, MIN_OBSTACLE_DIMENSION), + height: Math.max(dimensions.padWidth, MIN_OBSTACLE_DIMENSION), + ccwRotationDegrees: rotationDegrees, + connectedTo, + })) +} + export const getObstaclesFromSrjTraces = ( srj: SimpleRouteJson | null | undefined, + options: TraceObstacleOptions = {}, ): Obstacle[] => { if (!srj) return [] @@ -83,7 +152,15 @@ export const getObstaclesFromSrjTraces = ( const viaDimensions = getViaDimensions(srj) for (const [traceIndex, trace] of (srj.traces ?? []).entries()) { - const connectedTo = trace.connectsTo ?? [] + const connectedTo = [ + ...new Set([ + ...(options.includeConnectionNameInConnectedTo + ? [trace.connection_name] + : []), + ...(trace.connectsTo ?? []), + ]), + ] + const jumpers = trace.route.filter(isJumperRoutePoint) for (let pointIndex = 0; pointIndex < trace.route.length; pointIndex++) { const routePoint = trace.route[pointIndex]! @@ -106,6 +183,19 @@ export const getObstaclesFromSrjTraces = ( continue } + if (isJumperRoutePoint(routePoint) && options.modelJumperPads) { + traceObstacles.push( + ...createJumperPadObstacles({ + traceId: trace.pcb_trace_id, + traceIndex, + jumper: routePoint, + pointIndex, + connectedTo, + }), + ) + continue + } + if (isThroughObstacleRoutePoint(routePoint)) { const obstacle = createSegmentObstacle({ obstacleId: `trace_obstacle_${trace.pcb_trace_id}_${traceIndex}_${pointIndex}_through`, @@ -114,6 +204,7 @@ export const getObstaclesFromSrjTraces = ( width: routePoint.width, layer: routePoint.from_layer, connectedTo, + includeSquareCaps: options.includeSquareCaps ?? false, }) if (obstacle) { @@ -138,7 +229,9 @@ export const getObstaclesFromSrjTraces = ( if ( !isWireRoutePoint(routePoint) || !isWireRoutePoint(nextRoutePoint) || - routePoint.layer !== nextRoutePoint.layer + routePoint.layer !== nextRoutePoint.layer || + (options.modelJumperPads && + wireSegmentSpansJumper(routePoint, nextRoutePoint, jumpers)) ) { continue } @@ -150,6 +243,7 @@ export const getObstaclesFromSrjTraces = ( width: routePoint.width, layer: routePoint.layer, connectedTo, + includeSquareCaps: options.includeSquareCaps ?? false, }) if (obstacle) traceObstacles.push(obstacle) @@ -161,10 +255,11 @@ export const getObstaclesFromSrjTraces = ( export function convertSrjTracesToObstacles( srj: SimpleRouteJson | null | undefined, + options: TraceObstacleOptions = {}, ): SimpleRouteJson | null | undefined { if (!srj) return srj - const traceObstacles = getObstaclesFromSrjTraces(srj) + const traceObstacles = getObstaclesFromSrjTraces(srj, options) if (traceObstacles.length === 0) return srj diff --git a/lib/utils/resolvePreloadedTraceCanonicalNetIds.ts b/lib/utils/resolvePreloadedTraceCanonicalNetIds.ts new file mode 100644 index 000000000..1bd10378b --- /dev/null +++ b/lib/utils/resolvePreloadedTraceCanonicalNetIds.ts @@ -0,0 +1,215 @@ +import type { + SimpleRouteConnection, + SimpleRouteJson, + SimplifiedPcbTrace, +} from "lib/types" + +const getCanonicalConnectionName = ( + connection: SimpleRouteConnection, +): string => + connection.__netConnectionName ?? + connection.__rootConnectionNames?.[0] ?? + connection.name + +const getConnectionIdentityIds = ( + connection: SimpleRouteConnection, +): Set => + new Set( + [ + connection.name, + connection.__netConnectionName, + ...(connection.__rootConnectionNames ?? []), + ...connection.pointsToConnect.flatMap((point) => [ + point.pointId, + point.pcb_port_id, + ]), + ].filter((id): id is string => typeof id === "string"), + ) + +const getSimplifiedTraceEndpoints = ( + trace: SimplifiedPcbTrace, +): Array<{ x: number; y: number; layers: string[] }> => + [ + { routePoint: trace.route[0], endpoint: "start" as const }, + { routePoint: trace.route.at(-1), endpoint: "end" as const }, + ].flatMap(({ routePoint, endpoint }) => { + if (!routePoint) return [] + if (routePoint.route_type === "wire") { + return [{ x: routePoint.x, y: routePoint.y, layers: [routePoint.layer] }] + } + if (routePoint.route_type === "via") { + return [ + { + x: routePoint.x, + y: routePoint.y, + layers: [routePoint.from_layer, routePoint.to_layer], + }, + ] + } + return [ + { + x: routePoint[endpoint].x, + y: routePoint[endpoint].y, + layers: [ + routePoint.route_type === "jumper" + ? routePoint.layer + : endpoint === "start" + ? routePoint.from_layer + : routePoint.to_layer, + ], + }, + ] + }) + +const endpointIsInsideObstacle = ( + endpoint: { x: number; y: number; layers: string[] }, + obstacle: SimpleRouteJson["obstacles"][number], +): boolean => { + if (!endpoint.layers.some((layer) => obstacle.layers.includes(layer))) { + return false + } + + const radians = -((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180 + const deltaX = endpoint.x - obstacle.center.x + const deltaY = endpoint.y - obstacle.center.y + const localX = deltaX * Math.cos(radians) - deltaY * Math.sin(radians) + const localY = deltaX * Math.sin(radians) + deltaY * Math.cos(radians) + return ( + Math.abs(localX) <= obstacle.width / 2 + 1e-9 && + Math.abs(localY) <= obstacle.height / 2 + 1e-9 + ) +} + +const getDirectCanonicalNetEvidence = ( + srj: SimpleRouteJson, + trace: SimplifiedPcbTrace, + traceIndexByUniqueId: Map, +): Set => { + const canonicalNetIds = new Set() + const directlyNamedConnection = srj.connections.find((connection) => + getConnectionIdentityIds(connection).has(trace.connection_name), + ) + if (directlyNamedConnection) { + canonicalNetIds.add(getCanonicalConnectionName(directlyNamedConnection)) + } + + const traceConnectedIds = new Set(trace.connectsTo ?? []) + for (const connection of srj.connections) { + if ( + [...getConnectionIdentityIds(connection)].some( + (connectionId) => + !traceIndexByUniqueId.has(connectionId) && + traceConnectedIds.has(connectionId), + ) + ) { + canonicalNetIds.add(getCanonicalConnectionName(connection)) + } + } + + const traceIdentityIds = new Set([ + trace.pcb_trace_id, + trace.connection_name, + ...traceConnectedIds, + ]) + const endpoints = getSimplifiedTraceEndpoints(trace) + for (const obstacle of srj.obstacles) { + if ( + !obstacle.connectedTo.some((id) => traceIdentityIds.has(id)) || + !endpoints.some((endpoint) => + endpointIsInsideObstacle(endpoint, obstacle), + ) + ) { + continue + } + const obstacleConnectedIds = new Set(obstacle.connectedTo) + for (const connection of srj.connections) { + if ( + [...getConnectionIdentityIds(connection)].some((id) => + obstacleConnectedIds.has(id), + ) + ) { + canonicalNetIds.add(getCanonicalConnectionName(connection)) + } + } + } + + return canonicalNetIds +} + +/** + * Resolves fixed traces by explicit terminal/net evidence, then propagates a + * unique canonical net through trace-id links. Ambiguous components retain + * only per-trace unambiguous evidence and otherwise fall back to the raw name. + */ +export const resolvePreloadedTraceCanonicalNetIds = ( + srj: SimpleRouteJson, +): Map => { + const traces = srj.traces ?? [] + const traceIdCounts = new Map() + for (const trace of traces) { + traceIdCounts.set( + trace.pcb_trace_id, + (traceIdCounts.get(trace.pcb_trace_id) ?? 0) + 1, + ) + } + const traceIndexByUniqueId = new Map() + for (const [traceIndex, trace] of traces.entries()) { + if (traceIdCounts.get(trace.pcb_trace_id) === 1) { + traceIndexByUniqueId.set(trace.pcb_trace_id, traceIndex) + } + } + + const parentByTraceIndex = traces.map((_, traceIndex) => traceIndex) + const findRoot = (traceIndex: number): number => { + const parent = parentByTraceIndex[traceIndex] ?? traceIndex + if (parent === traceIndex) return traceIndex + const root = findRoot(parent) + parentByTraceIndex[traceIndex] = root + return root + } + const union = (leftTraceIndex: number, rightTraceIndex: number) => { + const leftRoot = findRoot(leftTraceIndex) + const rightRoot = findRoot(rightTraceIndex) + if (leftRoot !== rightRoot) parentByTraceIndex[rightRoot] = leftRoot + } + for (const [traceIndex, trace] of traces.entries()) { + for (const connectedId of trace.connectsTo ?? []) { + const connectedTraceIndex = traceIndexByUniqueId.get(connectedId) + if (connectedTraceIndex !== undefined) { + union(traceIndex, connectedTraceIndex) + } + } + } + + const directEvidenceByTraceIndex = traces.map((trace) => + getDirectCanonicalNetEvidence(srj, trace, traceIndexByUniqueId), + ) + const canonicalNetIdsByRoot = new Map>() + for (const [ + traceIndex, + canonicalNetIds, + ] of directEvidenceByTraceIndex.entries()) { + const root = findRoot(traceIndex) + const componentNetIds = canonicalNetIdsByRoot.get(root) ?? new Set() + for (const canonicalNetId of canonicalNetIds) { + componentNetIds.add(canonicalNetId) + } + canonicalNetIdsByRoot.set(root, componentNetIds) + } + + const canonicalNetIdByTraceId = new Map() + for (const [traceIndex, trace] of traces.entries()) { + if (traceIdCounts.get(trace.pcb_trace_id) !== 1) continue + const componentNetIds = + canonicalNetIdsByRoot.get(findRoot(traceIndex)) ?? new Set() + const directNetIds = directEvidenceByTraceIndex[traceIndex]! + const canonicalNetId = + componentNetIds.size === 1 + ? (componentNetIds.values().next().value ?? trace.connection_name) + : directNetIds.size === 1 + ? (directNetIds.values().next().value ?? trace.connection_name) + : trace.connection_name + canonicalNetIdByTraceId.set(trace.pcb_trace_id, canonicalNetId) + } + return canonicalNetIdByTraceId +} diff --git a/package.json b/package.json index 14a85a89b..e93bbf113 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "dependencies": { "fast-json-stable-stringify": "^2.1.0", "object-hash": "^3.0.0", - "stack-svgs": "^0.0.1" + "stack-svgs": "^0.0.1", + "@tscircuit/infgrid-ijump-astar": "0.0.33" } } diff --git a/tests/drc-via-spacing.test.ts b/tests/drc-via-spacing.test.ts index aabf4dbc0..6e09ae2bc 100644 --- a/tests/drc-via-spacing.test.ts +++ b/tests/drc-via-spacing.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "bun:test" import { MIN_VIA_TO_VIA_CLEARANCE, + VIA_OUTER_CLEARANCE_ERROR_PREFIX, getDrcErrors, } from "lib/testing/getDrcErrors" @@ -36,15 +37,48 @@ test("getDrcErrors reports different-net vias that are too close", () => { viaClearance: 0.1, }) + expect(errors).toHaveLength(2) + expect(errors).toContainEqual( + expect.objectContaining({ + type: "pcb_via_clearance_error", + error_type: "pcb_via_clearance_error", + pcb_error_id: "different_net_vias_close_via_a_via_b", + pcb_via_ids: ["via_a", "via_b"], + }), + ) + expect(errors).toContainEqual( + expect.objectContaining({ + type: "pcb_via_clearance_error", + error_type: "pcb_via_clearance_error", + pcb_error_id: `${VIA_OUTER_CLEARANCE_ERROR_PREFIX}via_a_via_b`, + pcb_via_ids: ["via_a", "via_b"], + }), + ) + expect(locationAwareErrors).toHaveLength(2) + expect(locationAwareErrors[0].center).toEqual({ x: 0.12, y: 0 }) + expect(locationAwareErrors[1].center).toEqual({ x: 0.12, y: 0 }) +}) + +test("getDrcErrors reports outer-via clearance independently of hole spacing", () => { + const centerDistance = VIA_OUTER_DIAMETER + 0.05 + const { errors } = getDrcErrors(createViaPair(centerDistance)) + expect(errors).toHaveLength(1) expect(errors[0]).toMatchObject({ type: "pcb_via_clearance_error", error_type: "pcb_via_clearance_error", - pcb_error_id: "different_net_vias_close_via_a_via_b", + pcb_error_id: `${VIA_OUTER_CLEARANCE_ERROR_PREFIX}via_a_via_b`, pcb_via_ids: ["via_a", "via_b"], + minimum_clearance: 0.1, }) - expect(locationAwareErrors).toHaveLength(1) - expect(locationAwareErrors[0].center).toEqual({ x: 0.12, y: 0 }) + const outerClearanceError = errors.find( + (error) => error.type === "pcb_via_clearance_error", + ) + expect( + outerClearanceError?.type === "pcb_via_clearance_error" + ? outerClearanceError.actual_clearance + : undefined, + ).toBeCloseTo(0.05) }) test("getDrcErrors enforces 0.1 minimum via-to-via clearance", () => { @@ -53,15 +87,18 @@ test("getDrcErrors enforces 0.1 minimum via-to-via clearance", () => { viaClearance: 0.05, }) - expect(errors).toHaveLength(1) - expect(errors[0]).toMatchObject({ - type: "pcb_via_clearance_error", - pcb_via_ids: ["via_a", "via_b"], - }) + expect(errors).toHaveLength(2) + expect( + errors.every( + (error) => + error.type === "pcb_via_clearance_error" && + error.pcb_via_ids.join(",") === "via_a,via_b", + ), + ).toBe(true) }) -test("getDrcErrors allows vias at 0.1 clearance", () => { - const centerDistance = VIA_HOLE_DIAMETER + MIN_VIA_TO_VIA_CLEARANCE +test("getDrcErrors allows vias at 0.1 hole and outer-copper clearance", () => { + const centerDistance = VIA_OUTER_DIAMETER + MIN_VIA_TO_VIA_CLEARANCE const { errors } = getDrcErrors(createViaPair(centerDistance)) expect(errors).toHaveLength(0) diff --git a/tests/evaluate-relaxed-drc-connectivity-regressions.test.ts b/tests/evaluate-relaxed-drc-connectivity-regressions.test.ts new file mode 100644 index 000000000..cf9f37ed4 --- /dev/null +++ b/tests/evaluate-relaxed-drc-connectivity-regressions.test.ts @@ -0,0 +1,244 @@ +import { expect, test } from "bun:test" +import { evaluateRelaxedDrc } from "lib/testing/evaluate-relaxed-drc" +import type { Obstacle, SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" + +const makeTerminalObstacle = ({ + x, + y = 0, + portId, + connectionName, +}: { + x: number + y?: number + portId: string + connectionName: string +}): Obstacle => ({ + type: "rect", + layers: ["top"], + center: { x, y }, + width: 0.2, + height: 0.2, + connectedTo: [connectionName, portId, `pcb_smtpad_${portId}`], +}) + +const getContinuityErrorIds = ( + result: ReturnType, +): string[] => + result.errors.flatMap((error) => { + if (!("pcb_trace_error_id" in error)) return [] + return error.pcb_trace_error_id.startsWith("missing_connection_") || + error.pcb_trace_error_id.startsWith("disconnected_endpoint_") + ? [error.pcb_trace_error_id] + : [] + }) + +test("relaxed DRC accepts a terminal contacted by the interior of combined same-net copper", () => { + const connectionName = "shared" + const fixedTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "fixed", + connection_name: connectionName, + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 1, y: 0, width: 0.1, layer: "top" }, + ], + } + const candidateTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "candidate", + connection_name: `${connectionName}_mst0`, + route: [ + { route_type: "wire", x: 1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 2, y: 0, width: 0.1, layer: "top" }, + ], + } + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -1, maxX: 3, maxY: 1 }, + obstacles: [ + makeTerminalObstacle({ + x: -1, + portId: "pcb_port_a", + connectionName, + }), + makeTerminalObstacle({ + x: 0, + portId: "pcb_port_b", + connectionName, + }), + makeTerminalObstacle({ + x: 1, + portId: "pcb_port_c", + connectionName, + }), + makeTerminalObstacle({ + x: 2, + portId: "pcb_port_d", + connectionName, + }), + ], + connections: [ + { + name: connectionName, + pointsToConnect: [ + { x: -1, y: 0, layer: "top", pcb_port_id: "pcb_port_a" }, + { x: 0, y: 0, layer: "top", pcb_port_id: "pcb_port_b" }, + ], + }, + ], + traces: [fixedTrace], + } + const srjWithPointPairs: SimpleRouteJson = { + ...inputSrj, + connections: [ + { + name: `${connectionName}_mst0`, + __rootConnectionNames: [connectionName], + pointsToConnect: [ + { x: 1, y: 0, layer: "top", pcb_port_id: "pcb_port_c" }, + { x: 2, y: 0, layer: "top", pcb_port_id: "pcb_port_d" }, + ], + }, + ], + } + + const result = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [candidateTrace], + }) + + expect(getContinuityErrorIds(result)).toEqual([]) +}) + +test("relaxed DRC reports an entirely missing required point-pair route", () => { + const connectionName = "net" + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -1, maxX: 2, maxY: 1 }, + obstacles: [ + makeTerminalObstacle({ + x: -1, + portId: "pcb_port_a", + connectionName, + }), + makeTerminalObstacle({ + x: 1, + portId: "pcb_port_b", + connectionName, + }), + ], + connections: [ + { + name: connectionName, + pointsToConnect: [ + { x: -1, y: 0, layer: "top", pcb_port_id: "pcb_port_a" }, + { x: 1, y: 0, layer: "top", pcb_port_id: "pcb_port_b" }, + ], + }, + ], + traces: [], + } + const srjWithPointPairs: SimpleRouteJson = { + ...inputSrj, + connections: [ + { + name: `${connectionName}_mst0`, + __rootConnectionNames: [connectionName], + pointsToConnect: inputSrj.connections[0]!.pointsToConnect, + }, + ], + } + + const result = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [], + }) + + expect(getContinuityErrorIds(result)).toContain( + `missing_connection_combined_${connectionName}`, + ) +}) + +test("relaxed DRC reports a same-net candidate fragment disconnected from the routed net", () => { + const connectionName = "net" + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -1, maxX: 2, maxY: 2 }, + obstacles: [ + makeTerminalObstacle({ + x: -1, + portId: "pcb_port_a", + connectionName, + }), + makeTerminalObstacle({ + x: 1, + portId: "pcb_port_b", + connectionName, + }), + ], + connections: [ + { + name: connectionName, + pointsToConnect: [ + { x: -1, y: 0, layer: "top", pcb_port_id: "pcb_port_a" }, + { x: 1, y: 0, layer: "top", pcb_port_id: "pcb_port_b" }, + ], + }, + ], + traces: [], + } + const srjWithPointPairs: SimpleRouteJson = { + ...inputSrj, + connections: [ + { + name: `${connectionName}_mst0`, + __rootConnectionNames: [connectionName], + pointsToConnect: inputSrj.connections[0]!.pointsToConnect, + }, + ], + } + const mainTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "main", + connection_name: `${connectionName}_mst0`, + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 1, y: 0, width: 0.1, layer: "top" }, + ], + } + const orphanTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "orphan", + connection_name: `${connectionName}_mst0`, + route: [ + { route_type: "wire", x: 0, y: 1, width: 0.1, layer: "top" }, + { route_type: "wire", x: 1, y: 1, width: 0.1, layer: "top" }, + ], + } + + const result = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [mainTrace, orphanTrace], + }) + const orphanContinuityError = result.errors.find((error) => { + if ( + !("pcb_trace_error_id" in error) || + (!error.pcb_trace_error_id.startsWith("missing_connection_") && + !error.pcb_trace_error_id.startsWith("disconnected_endpoint_")) + ) { + return false + } + const owners = ( + error as typeof error & { candidate_pcb_trace_ids?: string[] } + ).candidate_pcb_trace_ids + return owners?.includes(orphanTrace.pcb_trace_id) ?? false + }) + + expect(orphanContinuityError).toBeDefined() +}) diff --git a/tests/evaluate-relaxed-drc-preloaded-trace-intersections.test.ts b/tests/evaluate-relaxed-drc-preloaded-trace-intersections.test.ts new file mode 100644 index 000000000..32ad96b61 --- /dev/null +++ b/tests/evaluate-relaxed-drc-preloaded-trace-intersections.test.ts @@ -0,0 +1,1572 @@ +import { expect, test } from "bun:test" +import { RELAXED_DRC_OPTIONS } from "lib/testing/drcPresets" +import { evaluateRelaxedDrc } from "lib/testing/evaluate-relaxed-drc" +import { + VIA_OUTER_CLEARANCE_ERROR_PREFIX, + getDrcErrors, +} from "lib/testing/getDrcErrors" +import type { SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" +import preexistingConnectedTraceScenario from "./features/preexisting-connected-traces/srj/preexisting-connected-traces06.srj.json" with { + type: "json", +} + +test("relaxed DRC checks preloaded-to-routed crossings with root connectivity", () => { + const preloadedTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "shared_trace_id", + connection_name: "fixed_net", + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 1, y: 0, width: 0.1, layer: "top" }, + ], + } + const routedTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "shared_trace_id", + connection_name: "new_net_mst0", + route: [ + { route_type: "wire", x: 0, y: -1, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 1, width: 0.1, layer: "top" }, + ], + } + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [ + { + name: "fixed_net", + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: 1, y: 0, layer: "top" }, + ], + }, + { + name: "new_net", + pointsToConnect: [ + { x: 0, y: -1, layer: "top" }, + { x: 0, y: 1, layer: "top" }, + ], + }, + ], + traces: [preloadedTrace], + } + const srjWithPointPairs: SimpleRouteJson = { + ...inputSrj, + connections: [ + { + name: "new_net_mst0", + __rootConnectionNames: ["new_net"], + pointsToConnect: [ + { x: 0, y: -1, layer: "top" }, + { x: 0, y: 1, layer: "top" }, + ], + }, + ], + } + + const differentNetResult = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [routedTrace], + }) + const pcbTraces = differentNetResult.circuitJson.filter( + (element) => element.type === "pcb_trace", + ) + const overlapErrorIds = differentNetResult.errors.flatMap((error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id.startsWith("overlap_") + ? [error.pcb_trace_error_id] + : [], + ) + + expect(pcbTraces.map((trace) => trace.pcb_trace_id)).toEqual([ + "preloaded_0_shared_trace_id", + "shared_trace_id", + ]) + expect(overlapErrorIds).toHaveLength(1) + expect(overlapErrorIds[0]).toContain("shared_trace_id") + expect(overlapErrorIds[0]).toContain("preloaded_0_shared_trace_id") + const crossingError = differentNetResult.errors.find( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id === overlapErrorIds[0], + ) + expect( + ( + crossingError as typeof crossingError & { + candidate_pcb_trace_ids?: string[] + } + )?.candidate_pcb_trace_ids, + ).toEqual(["shared_trace_id"]) + + const sameNetResult = evaluateRelaxedDrc({ + inputSrj: { + ...inputSrj, + connections: [ + { + name: "shared_net", + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: 1, y: 0, layer: "top" }, + { x: 0, y: -1, layer: "top" }, + { x: 0, y: 1, layer: "top" }, + ], + }, + ], + traces: [{ ...preloadedTrace, connection_name: "shared_net" }], + }, + srjWithPointPairs: { + ...srjWithPointPairs, + connections: [ + { + name: "shared_net_mst0", + __rootConnectionNames: ["shared_net"], + pointsToConnect: [ + { x: 0, y: -1, layer: "top" }, + { x: 0, y: 1, layer: "top" }, + ], + }, + ], + }, + traces: [{ ...routedTrace, connection_name: "shared_net_mst0" }], + }) + const sameNetPcbTraces = sameNetResult.circuitJson.filter( + (element) => element.type === "pcb_trace", + ) + + expect(sameNetPcbTraces.map((trace) => trace.source_trace_id)).toEqual([ + "shared_net", + "shared_net", + ]) + expect( + sameNetResult.errors.filter( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id.startsWith("overlap_"), + ), + ).toHaveLength(0) +}) + +test("relaxed DRC retains coincident vias from different nets", () => { + const createViaTrace = ( + pcbTraceId: string, + connectionName: string, + viaX = 0, + ): SimplifiedPcbTrace => ({ + type: "pcb_trace", + pcb_trace_id: pcbTraceId, + connection_name: connectionName, + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: viaX, y: 0, width: 0.1, layer: "top" }, + { + route_type: "via", + x: viaX, + y: 0, + from_layer: "top", + to_layer: "bottom", + via_diameter: 0.3, + via_hole_diameter: 0.15, + }, + { + route_type: "wire", + x: viaX, + y: 0, + width: 0.1, + layer: "bottom", + }, + { route_type: "wire", x: 1, y: 0, width: 0.1, layer: "bottom" }, + ], + }) + const fixedTrace = createViaTrace("fixed_trace", "fixed_net") + const routedTrace = createViaTrace("routed_trace", "new_net_mst0", 0.05) + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + minViaPadDiameter: 0.3, + minViaHoleDiameter: 0.15, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [ + { + name: "fixed_net", + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: 1, y: 0, layer: "bottom" }, + ], + }, + { + name: "new_net", + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: 1, y: 0, layer: "bottom" }, + ], + }, + ], + traces: [fixedTrace], + } + const srjWithPointPairs: SimpleRouteJson = { + ...inputSrj, + connections: [ + { + name: "new_net_mst0", + __rootConnectionNames: ["new_net"], + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: 1, y: 0, layer: "bottom" }, + ], + }, + ], + } + + const differentNetResult = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [routedTrace], + }) + expect( + differentNetResult.circuitJson.filter( + (element) => element.type === "pcb_via", + ), + ).toHaveLength(2) + const viaClearanceError = differentNetResult.errors.find( + (error) => error.type === "pcb_via_clearance_error", + ) + expect(viaClearanceError).toBeDefined() + expect( + ( + viaClearanceError as typeof viaClearanceError & { + candidate_pcb_trace_ids?: string[] + } + )?.candidate_pcb_trace_ids, + ).toEqual(["routed_trace"]) + const fixedTraceToCandidateViaError = differentNetResult.errors.find( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id.startsWith( + "overlap_preloaded_0_fixed_trace_via_", + ), + ) + expect( + ( + fixedTraceToCandidateViaError as typeof fixedTraceToCandidateViaError & { + candidate_pcb_trace_ids?: string[] + } + )?.candidate_pcb_trace_ids, + ).toEqual(["routed_trace"]) + + const sameNetResult = evaluateRelaxedDrc({ + inputSrj: { + ...inputSrj, + connections: [ + { + name: "shared_net", + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: 1, y: 0, layer: "bottom" }, + ], + }, + ], + traces: [ + { + ...fixedTrace, + connection_name: "shared_net", + route: fixedTrace.route.map((point) => + point.route_type === "via" + ? { + ...point, + from_layer: "bottom", + to_layer: "top", + via_diameter: 0.4, + } + : point, + ), + }, + ], + }, + srjWithPointPairs: { + ...srjWithPointPairs, + connections: [ + { + name: "shared_net_mst0", + __rootConnectionNames: ["shared_net"], + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: 1, y: 0, layer: "bottom" }, + ], + }, + ], + }, + traces: [createViaTrace("routed_trace", "shared_net_mst0")], + }) + const sameNetVias = sameNetResult.circuitJson.filter( + (element) => element.type === "pcb_via", + ) + expect(sameNetVias).toHaveLength(1) + expect(sameNetVias[0]?.outer_diameter).toBe(0.4) + expect(sameNetVias[0]?.pcb_trace_id).toBe("preloaded_0_fixed_trace") + expect( + sameNetResult.errors.filter( + (error) => error.type === "pcb_via_clearance_error", + ), + ).toHaveLength(0) +}) + +test("relaxed DRC attributes enlarged merged via copper to the candidate", () => { + const createSharedViaTrace = ( + pcbTraceId: string, + connectionName: string, + viaDiameter: number, + ): SimplifiedPcbTrace => ({ + type: "pcb_trace", + pcb_trace_id: pcbTraceId, + connection_name: connectionName, + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 0, width: 0.1, layer: "top" }, + { + route_type: "via", + x: 0, + y: 0, + from_layer: "top", + to_layer: "bottom", + via_diameter: viaDiameter, + via_hole_diameter: viaDiameter / 2, + }, + { + route_type: "wire", + x: 0, + y: 0, + width: 0.1, + layer: "bottom", + }, + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "bottom" }, + ], + }) + const fixedSharedTrace = createSharedViaTrace("fixed_shared", "shared", 0.3) + const fixedForeignTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "fixed_foreign", + connection_name: "foreign", + route: [ + { + route_type: "wire", + x: 0.34, + y: -0.7, + width: 0.1, + layer: "top", + }, + { + route_type: "wire", + x: 0.34, + y: 0.7, + width: 0.1, + layer: "top", + }, + ], + } + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [ + { + name: "shared", + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: -1, y: 0, layer: "bottom" }, + ], + }, + { + name: "foreign", + pointsToConnect: [ + { x: 0.34, y: -0.7, layer: "top" }, + { x: 0.34, y: 0.7, layer: "top" }, + ], + }, + ], + traces: [fixedSharedTrace, fixedForeignTrace], + } + const srjWithPointPairs: SimpleRouteJson = { + ...inputSrj, + connections: [ + { + name: "shared_mst0", + __rootConnectionNames: ["shared"], + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: -1, y: 0, layer: "bottom" }, + ], + }, + ], + } + const evaluateCandidateVia = (viaDiameter: number) => + evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [ + createSharedViaTrace("candidate_shared", "shared_mst0", viaDiameter), + ], + }) + const getFixedTraceToMergedViaError = ( + result: ReturnType, + ) => + result.errors.find( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id.startsWith( + "overlap_preloaded_1_fixed_foreign_via_", + ), + ) + + expect(getFixedTraceToMergedViaError(evaluateCandidateVia(0.3))).toBe( + undefined, + ) + + const enlargedResult = evaluateCandidateVia(0.6) + const mergedVia = enlargedResult.circuitJson.find( + (element) => element.type === "pcb_via", + ) as + | (Extract< + (typeof enlargedResult.circuitJson)[number], + { type: "pcb_via" } + > & { + contributing_pcb_trace_ids?: string[] + }) + | undefined + expect(mergedVia?.outer_diameter).toBe(0.6) + expect(mergedVia?.pcb_trace_id).toBe("preloaded_0_fixed_shared") + expect(mergedVia?.contributing_pcb_trace_ids).toEqual([ + "preloaded_0_fixed_shared", + "candidate_shared", + ]) + + const enlargedViaError = getFixedTraceToMergedViaError(enlargedResult) + expect(enlargedViaError).toBeDefined() + expect( + ( + enlargedViaError as typeof enlargedViaError & { + candidate_pcb_trace_ids?: string[] + } + )?.candidate_pcb_trace_ids, + ).toEqual(["candidate_shared"]) +}) + +test("relaxed DRC keeps colliding original and point-pair names on their own nets", () => { + const preloadedTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "fixed_trace", + connection_name: "route_mst0", + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 1, y: 0, width: 0.1, layer: "top" }, + ], + } + const candidateTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "candidate_trace", + connection_name: "route_mst0", + route: [ + { route_type: "wire", x: 0, y: -1, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 1, width: 0.1, layer: "top" }, + ], + } + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [ + { + type: "rect", + layers: ["top"], + center: { x: -1, y: 0 }, + width: 0.3, + height: 0.3, + connectedTo: [ + "pcb_smtpad_fixed_start", + "pcb_port_fixed_start", + "route_mst0", + ], + }, + { + type: "rect", + layers: ["top"], + center: { x: 1, y: 0 }, + width: 0.3, + height: 0.3, + connectedTo: [ + "pcb_smtpad_fixed_end", + "pcb_port_fixed_end", + "route_mst0", + ], + }, + { + type: "rect", + layers: ["top"], + center: { x: 0, y: -1 }, + width: 0.3, + height: 0.3, + connectedTo: [ + "pcb_smtpad_candidate_start", + "pcb_port_candidate_start", + "route", + ], + }, + { + type: "rect", + layers: ["top"], + center: { x: 0, y: 1 }, + width: 0.3, + height: 0.3, + connectedTo: [ + "pcb_smtpad_candidate_end", + "pcb_port_candidate_end", + "route", + ], + }, + ], + connections: [ + { + name: "route_mst0", + pointsToConnect: [ + { + x: -1, + y: 0, + layer: "top", + pcb_port_id: "pcb_port_fixed_start", + }, + { + x: 1, + y: 0, + layer: "top", + pcb_port_id: "pcb_port_fixed_end", + }, + ], + }, + { + name: "route", + pointsToConnect: [ + { + x: 0, + y: -1, + layer: "top", + pcb_port_id: "pcb_port_candidate_start", + }, + { + x: 0, + y: 1, + layer: "top", + pcb_port_id: "pcb_port_candidate_end", + }, + ], + }, + ], + traces: [preloadedTrace], + } + const srjWithPointPairs: SimpleRouteJson = { + ...inputSrj, + connections: [ + { + name: "route_mst0", + __rootConnectionNames: ["route"], + pointsToConnect: [ + { + x: 0, + y: -1, + layer: "top", + pcb_port_id: "pcb_port_candidate_start", + }, + { + x: 0, + y: 1, + layer: "top", + pcb_port_id: "pcb_port_candidate_end", + }, + ], + }, + ], + } + + const result = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [candidateTrace], + }) + expect( + result.circuitJson + .filter((element) => element.type === "pcb_trace") + .map((trace) => trace.source_trace_id), + ).toEqual(["route_mst0", "route"]) + const sourceTraceById = new Map( + result.circuitJson + .filter((element) => element.type === "source_trace") + .map((sourceTrace) => [sourceTrace.source_trace_id, sourceTrace]), + ) + expect( + sourceTraceById + .get("route_mst0") + ?.connected_source_net_ids?.includes("pcb_smtpad_fixed_start"), + ).toBe(true) + expect( + sourceTraceById + .get("route_mst0") + ?.connected_source_net_ids?.includes("pcb_smtpad_candidate_start"), + ).toBe(false) + expect( + sourceTraceById + .get("route") + ?.connected_source_net_ids?.includes("pcb_smtpad_candidate_start"), + ).toBe(true) + expect( + sourceTraceById + .get("route") + ?.connected_source_net_ids?.includes("pcb_smtpad_fixed_start"), + ).toBe(false) + + const crossingError = result.errors.find( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id === + "overlap_preloaded_0_fixed_trace_candidate_trace", + ) + expect(crossingError).toBeDefined() + expect( + ( + crossingError as typeof crossingError & { + candidate_pcb_trace_ids?: string[] + } + )?.candidate_pcb_trace_ids, + ).toEqual(["candidate_trace"]) +}) + +test("relaxed DRC does not claim an unknown preloaded name as a point-pair alias", () => { + const preloadedTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "fixed_trace", + connection_name: "route_mst0", + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 1, y: 0, width: 0.1, layer: "top" }, + ], + } + const candidateTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "candidate_trace", + connection_name: "route_mst0", + route: [ + { route_type: "wire", x: 0, y: -1, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 1, width: 0.1, layer: "top" }, + ], + } + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [ + { + name: "route", + pointsToConnect: [ + { x: 0, y: -1, layer: "top" }, + { x: 0, y: 1, layer: "top" }, + ], + }, + ], + traces: [preloadedTrace], + } + const srjWithPointPairs: SimpleRouteJson = { + ...inputSrj, + connections: [ + { + name: "route_mst0", + __rootConnectionNames: ["route"], + pointsToConnect: [ + { x: 0, y: -1, layer: "top" }, + { x: 0, y: 1, layer: "top" }, + ], + }, + ], + } + + const result = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [candidateTrace], + }) + expect( + result.circuitJson + .filter((element) => element.type === "pcb_trace") + .map((trace) => trace.source_trace_id), + ).toEqual(["route_mst0", "route"]) + expect( + result.errors.some( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id === + "overlap_preloaded_0_fixed_trace_candidate_trace", + ), + ).toBe(true) +}) + +test("relaxed DRC makes combined physical continuity authoritative", () => { + const fixedTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "fixed", + connection_name: "shared", + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 0, width: 0.1, layer: "top" }, + ], + } + const candidateTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "candidate", + connection_name: "shared_mst0", + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0.5, y: 0, width: 0.1, layer: "top" }, + ], + } + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -1, maxX: 2, maxY: 1 }, + obstacles: [ + { + type: "rect", + layers: ["top"], + center: { x: -1, y: 0 }, + width: 0.2, + height: 0.2, + connectedTo: ["shared", "pcb_port_a", "pcb_smtpad_a"], + }, + { + type: "rect", + layers: ["top"], + center: { x: 1, y: 0 }, + width: 0.2, + height: 0.2, + connectedTo: ["shared", "pcb_port_b", "pcb_smtpad_b"], + }, + ], + connections: [ + { + name: "shared", + pointsToConnect: [ + { x: -1, y: 0, layer: "top", pcb_port_id: "pcb_port_a" }, + { x: 1, y: 0, layer: "top", pcb_port_id: "pcb_port_b" }, + ], + }, + ], + traces: [fixedTrace], + } + const srjWithPointPairs: SimpleRouteJson = { + ...inputSrj, + connections: [ + { + name: "shared_mst0", + __rootConnectionNames: ["shared"], + pointsToConnect: inputSrj.connections[0]!.pointsToConnect, + }, + ], + } + const getMissingConnectionErrors = ( + errors: ReturnType["errors"], + ) => + errors.filter( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id.startsWith("missing_connection_"), + ) + + const fixedOnlyResult = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [], + }) + expect( + getMissingConnectionErrors( + getDrcErrors(fixedOnlyResult.circuitJson, RELAXED_DRC_OPTIONS).errors, + ), + ).toHaveLength(1) + const [fixedOnlyContinuityError] = getMissingConnectionErrors( + fixedOnlyResult.errors, + ) + expect( + fixedOnlyContinuityError && "pcb_trace_error_id" in fixedOnlyContinuityError + ? fixedOnlyContinuityError.pcb_trace_error_id + : undefined, + ).toBe("missing_connection_combined_shared") + expect( + ( + fixedOnlyContinuityError as typeof fixedOnlyContinuityError & { + candidate_pcb_trace_ids?: string[] + } + )?.candidate_pcb_trace_ids, + ).toEqual([]) + + const candidateResult = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [candidateTrace], + }) + const [continuityError] = getMissingConnectionErrors(candidateResult.errors) + expect( + continuityError && "pcb_trace_id" in continuityError + ? continuityError.pcb_trace_id + : undefined, + ).toBe("candidate") + expect( + ( + continuityError as typeof continuityError & { + candidate_pcb_trace_ids?: string[] + } + )?.candidate_pcb_trace_ids, + ).toEqual(["candidate"]) +}) + +test("relaxed DRC does not transfer fixed-only via errors to a contained candidate via", () => { + const makeViaTrace = ( + pcbTraceId: string, + connectionName: string, + x: number, + viaDiameter: number, + ): SimplifiedPcbTrace => ({ + type: "pcb_trace", + pcb_trace_id: pcbTraceId, + connection_name: connectionName, + route: [ + { route_type: "wire", x, y: -1, width: 0.1, layer: "top" }, + { route_type: "wire", x, y: 0, width: 0.1, layer: "top" }, + { + route_type: "via", + x, + y: 0, + from_layer: "top", + to_layer: "bottom", + via_diameter: viaDiameter, + via_hole_diameter: viaDiameter / 2, + }, + { route_type: "wire", x, y: 0, width: 0.1, layer: "bottom" }, + { route_type: "wire", x, y: 1, width: 0.1, layer: "bottom" }, + ], + }) + const fixedShared = makeViaTrace("fixed_shared", "shared", 0, 0.4) + const fixedForeign = makeViaTrace("fixed_foreign", "foreign", 0.25, 0.3) + const candidate = makeViaTrace("candidate", "shared_mst0", 0, 0.1) + const makeTerminalObstacle = ( + net: string, + portId: string, + x: number, + y: number, + layer: "top" | "bottom", + ) => ({ + type: "rect" as const, + layers: [layer], + center: { x, y }, + width: 0.2, + height: 0.2, + connectedTo: [net, portId, `pcb_smtpad_${portId}`], + }) + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + minViaPadDiameter: 0.3, + minViaHoleDiameter: 0.15, + bounds: { minX: -1, minY: -2, maxX: 1.4, maxY: 2 }, + obstacles: [ + makeTerminalObstacle("shared", "pcb_port_shared_top", 0, -1, "top"), + makeTerminalObstacle("shared", "pcb_port_shared_bottom", 0, 1, "bottom"), + makeTerminalObstacle("foreign", "pcb_port_foreign_top", 0.25, -1, "top"), + makeTerminalObstacle( + "foreign", + "pcb_port_foreign_bottom", + 0.25, + 1, + "bottom", + ), + ], + connections: [ + { + name: "shared", + pointsToConnect: [ + { + x: 0, + y: -1, + layer: "top", + pcb_port_id: "pcb_port_shared_top", + }, + { + x: 0, + y: 1, + layer: "bottom", + pcb_port_id: "pcb_port_shared_bottom", + }, + ], + }, + { + name: "foreign", + pointsToConnect: [ + { + x: 0.25, + y: -1, + layer: "top", + pcb_port_id: "pcb_port_foreign_top", + }, + { + x: 0.25, + y: 1, + layer: "bottom", + pcb_port_id: "pcb_port_foreign_bottom", + }, + ], + }, + ], + traces: [fixedShared, fixedForeign], + } + const srjWithPointPairs: SimpleRouteJson = { + ...inputSrj, + connections: [ + { + name: "shared_mst0", + __rootConnectionNames: ["shared"], + pointsToConnect: inputSrj.connections[0]!.pointsToConnect, + }, + ], + } + const result = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [candidate], + }) + const rawViaErrors = getDrcErrors( + result.circuitJson, + RELAXED_DRC_OPTIONS, + ).errors.filter((error) => error.type === "pcb_via_clearance_error") + + expect(rawViaErrors).toHaveLength(2) + expect( + result.errors.filter((error) => error.type === "pcb_via_clearance_error"), + ).toHaveLength(0) +}) + +test("relaxed DRC attributes candidate-to-candidate crossings to both traces", () => { + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [ + { + name: "horizontal", + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: 1, y: 0, layer: "top" }, + ], + }, + { + name: "vertical", + pointsToConnect: [ + { x: 0, y: -1, layer: "top" }, + { x: 0, y: 1, layer: "top" }, + ], + }, + ], + } + const traces: SimplifiedPcbTrace[] = [ + { + type: "pcb_trace", + pcb_trace_id: "candidate_horizontal", + connection_name: "horizontal", + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 1, y: 0, width: 0.1, layer: "top" }, + ], + }, + { + type: "pcb_trace", + pcb_trace_id: "candidate_vertical", + connection_name: "vertical", + route: [ + { route_type: "wire", x: 0, y: -1, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 1, width: 0.1, layer: "top" }, + ], + }, + ] + const result = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs: inputSrj, + traces, + }) + const crossingError = result.errors.find( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id.startsWith("overlap_"), + ) as + | (ReturnType["errors"][number] & { + candidate_pcb_trace_ids?: string[] + }) + | undefined + + expect(crossingError?.candidate_pcb_trace_ids?.toSorted()).toEqual([ + "candidate_horizontal", + "candidate_vertical", + ]) +}) + +test("relaxed DRC canonicalizes explicit preloaded trace connectivity without claiming raw aliases", () => { + for (const retainConnectsTo of [true, false]) { + const inputSrj = structuredClone( + preexistingConnectedTraceScenario, + ) as SimpleRouteJson + const fixedTrace = inputSrj.traces?.[0] + if (!retainConnectsTo && fixedTrace) { + inputSrj.traces = [ + { ...fixedTrace, connectsTo: undefined }, + ...(inputSrj.traces?.slice(1) ?? []), + ] + } + const mainConnection = inputSrj.connections[0]! + const srjWithPointPairs: SimpleRouteJson = { + ...inputSrj, + connections: [ + { + name: `${mainConnection.name}_mst0`, + __rootConnectionNames: [mainConnection.name], + pointsToConnect: mainConnection.pointsToConnect.slice(1), + }, + ], + } + const candidateTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "candidate", + connection_name: `${mainConnection.name}_mst0`, + route: [ + { route_type: "wire", x: -2.75, y: 0.635, width: 0.1, layer: "top" }, + { route_type: "wire", x: 2.175, y: 1.4, width: 0.1, layer: "top" }, + ], + } + + const result = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [candidateTrace], + }) + expect( + result.circuitJson + .filter((element) => element.type === "pcb_trace") + .map((trace) => trace.source_trace_id), + ).toEqual([mainConnection.name, mainConnection.name]) + expect( + result.errors.filter( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id.startsWith("overlap_"), + ), + ).toHaveLength(0) + } +}) + +test("relaxed DRC rejects disjoint same-net fragments that separately reach the terminals", () => { + const makeScenario = (): SimpleRouteJson => ({ + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -1, maxX: 2, maxY: 1 }, + obstacles: [ + { + type: "rect", + layers: ["top"], + center: { x: -1, y: 0 }, + width: 0.2, + height: 0.2, + connectedTo: ["shared", "pcb_port_a", "pcb_smtpad_a"], + }, + { + type: "rect", + layers: ["top"], + center: { x: 1, y: 0 }, + width: 0.2, + height: 0.2, + connectedTo: ["shared", "pcb_port_b", "pcb_smtpad_b"], + }, + ], + connections: [ + { + name: "shared", + pointsToConnect: [ + { x: -1, y: 0, layer: "top", pcb_port_id: "pcb_port_a" }, + { x: 1, y: 0, layer: "top", pcb_port_id: "pcb_port_b" }, + ], + }, + ], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "fixed", + connection_name: "shared", + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 0, width: 0.1, layer: "top" }, + ], + }, + ], + }) + const inputSrj = makeScenario() + const srjWithPointPairs: SimpleRouteJson = { + ...inputSrj, + connections: [ + { + name: "shared_mst0", + __rootConnectionNames: ["shared"], + pointsToConnect: inputSrj.connections[0]!.pointsToConnect, + }, + ], + } + const evaluateCandidate = (startX: number, layer: "top" | "bottom" = "top") => + evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "candidate", + connection_name: "shared_mst0", + route: [ + { + route_type: "wire", + x: startX, + y: 0, + width: 0.1, + layer, + }, + { + route_type: "wire", + x: 1, + y: 0, + width: 0.1, + layer, + }, + ], + }, + ], + }) + const getCombinedContinuityErrors = ( + result: ReturnType, + ) => + result.errors.filter( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id.startsWith("missing_connection_combined_"), + ) + + const disjointResult = evaluateCandidate(0.5) + expect(getCombinedContinuityErrors(disjointResult)).toHaveLength(1) + expect( + ( + getCombinedContinuityErrors(disjointResult)[0] as + | (ReturnType["errors"][number] & { + candidate_pcb_trace_ids?: string[] + }) + | undefined + )?.candidate_pcb_trace_ids, + ).toEqual(["candidate"]) + + expect(getCombinedContinuityErrors(evaluateCandidate(0))).toHaveLength(0) + expect( + getCombinedContinuityErrors(evaluateCandidate(0, "bottom")), + ).toHaveLength(1) + + const brokenSingleTraceResult = evaluateRelaxedDrc({ + inputSrj: { ...inputSrj, traces: [] }, + srjWithPointPairs, + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "candidate", + connection_name: "shared_mst0", + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0.5, y: 0, width: 0.1, layer: "bottom" }, + { route_type: "wire", x: 0.5, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 1, y: 0, width: 0.1, layer: "top" }, + ], + }, + ], + }) + expect(getCombinedContinuityErrors(brokenSingleTraceResult)).toHaveLength(1) +}) + +test("relaxed DRC reserves candidate trace ids when allocating via ids", () => { + const fixedTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "fixed", + connection_name: "fixed_net", + route: [ + { route_type: "wire", x: 0, y: 0, width: 0.1, layer: "top" }, + { + route_type: "via", + x: 0, + y: 0, + from_layer: "top", + to_layer: "bottom", + via_diameter: 0.3, + via_hole_diameter: 0.15, + }, + { route_type: "wire", x: 0, y: 0, width: 0.1, layer: "bottom" }, + { route_type: "wire", x: 1, y: 0, width: 0.1, layer: "bottom" }, + ], + } + const candidateTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "via_0", + connection_name: "candidate_net", + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 1, y: 0, width: 0.1, layer: "top" }, + ], + } + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -1, maxX: 2, maxY: 1 }, + obstacles: [], + connections: [ + { + name: "fixed_net", + pointsToConnect: [ + { x: 0, y: 0, layer: "top" }, + { x: 1, y: 0, layer: "bottom" }, + ], + }, + { + name: "candidate_net", + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: 1, y: 0, layer: "top" }, + ], + }, + ], + traces: [fixedTrace], + } + + const result = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs: inputSrj, + traces: [candidateTrace], + }) + const via = result.circuitJson.find((element) => element.type === "pcb_via") + expect(via?.pcb_via_id).not.toBe("via_0") + expect( + result.errors.some( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id === + `overlap_via_0_${via?.pcb_via_id ?? "missing_via"}`, + ), + ).toBe(true) +}) + +test("relaxed DRC preserves preloaded through-obstacle copper and checks clearance", () => { + const fixedTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "fixed_through", + connection_name: "fixed_net", + route: [ + { + route_type: "through_obstacle", + start: { x: -1, y: 0 }, + end: { x: 1, y: 0 }, + from_layer: "top", + to_layer: "bottom", + width: 0.1, + }, + ], + } + const candidateTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "candidate", + connection_name: "candidate_net_mst0", + route: [ + { route_type: "wire", x: 0, y: 0.15, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 1, width: 0.1, layer: "top" }, + ], + } + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [ + { + name: "fixed_net", + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: 1, y: 0, layer: "bottom" }, + ], + }, + { + name: "candidate_net", + pointsToConnect: [ + { x: 0, y: 0.15, layer: "top" }, + { x: 0, y: 1, layer: "top" }, + ], + }, + ], + traces: [fixedTrace], + } + const result = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs: { + ...inputSrj, + connections: [ + { + name: "candidate_net_mst0", + __rootConnectionNames: ["candidate_net"], + pointsToConnect: inputSrj.connections[1]!.pointsToConnect, + }, + ], + }, + traces: [candidateTrace], + }) + const convertedFixedTrace = result.circuitJson.find( + (element) => + element.type === "pcb_trace" && + element.pcb_trace_id === "preloaded_0_fixed_through", + ) + expect( + convertedFixedTrace?.type === "pcb_trace" + ? convertedFixedTrace.route + : undefined, + ).toEqual([ + { + route_type: "through_pad", + start: { x: -1, y: 0 }, + end: { x: 1, y: 0 }, + width: 0.1, + start_layer: "top", + end_layer: "bottom", + }, + ]) + + const overlapError = result.errors.find( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id === + "overlap_preloaded_0_fixed_through_candidate", + ) + expect(overlapError).toBeDefined() + expect( + ( + overlapError as + | (typeof overlapError & { candidate_pcb_trace_ids?: string[] }) + | undefined + )?.candidate_pcb_trace_ids, + ).toEqual(["candidate"]) +}) + +test("relaxed DRC rejects different-net via outer-copper overlap", () => { + const makeViaTrace = ( + pcbTraceId: string, + connectionName: string, + viaX: number, + terminalDirection: -1 | 1, + ): SimplifiedPcbTrace => ({ + type: "pcb_trace", + pcb_trace_id: pcbTraceId, + connection_name: connectionName, + route: [ + { + route_type: "wire", + x: viaX + terminalDirection, + y: 0, + width: 0.1, + layer: "top", + }, + { route_type: "wire", x: viaX, y: 0, width: 0.1, layer: "top" }, + { + route_type: "via", + x: viaX, + y: 0, + from_layer: "top", + to_layer: "bottom", + via_diameter: 0.4, + via_hole_diameter: 0.1, + }, + { route_type: "wire", x: viaX, y: 0, width: 0.1, layer: "bottom" }, + { + route_type: "wire", + x: viaX + terminalDirection, + y: 0, + width: 0.1, + layer: "bottom", + }, + ], + }) + const fixedTrace = makeViaTrace("fixed", "fixed_net", 0, -1) + const candidateTrace = makeViaTrace( + "candidate", + "candidate_net_mst0", + 0.37, + 1, + ) + const inputSrj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.4, + bounds: { minX: -2, minY: -1, maxX: 2, maxY: 1 }, + obstacles: [], + connections: [ + { + name: "fixed_net", + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: -1, y: 0, layer: "bottom" }, + ], + }, + { + name: "candidate_net", + pointsToConnect: [ + { x: 1.37, y: 0, layer: "top" }, + { x: 1.37, y: 0, layer: "bottom" }, + ], + }, + ], + traces: [fixedTrace], + } + const result = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs: { + ...inputSrj, + connections: [ + { + name: "candidate_net_mst0", + __rootConnectionNames: ["candidate_net"], + pointsToConnect: inputSrj.connections[1]!.pointsToConnect, + }, + ], + }, + traces: [candidateTrace], + }) + const outerClearanceError = result.errors.find( + (error) => + error.type === "pcb_via_clearance_error" && + error.pcb_error_id.startsWith(VIA_OUTER_CLEARANCE_ERROR_PREFIX), + ) + + expect(outerClearanceError).toBeDefined() + expect( + outerClearanceError?.type === "pcb_via_clearance_error" + ? outerClearanceError.actual_clearance + : undefined, + ).toBeCloseTo(-0.03) + expect( + outerClearanceError?.type === "pcb_via_clearance_error" + ? outerClearanceError.minimum_clearance + : undefined, + ).toBe(0.1) + expect( + ( + outerClearanceError as + | (typeof outerClearanceError & { + candidate_pcb_trace_ids?: string[] + }) + | undefined + )?.candidate_pcb_trace_ids, + ).toEqual(["candidate"]) +}) + +test("relaxed DRC expands through-vias onto intermediate layers", () => { + const fixedTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "fixed", + connection_name: "fixed_net", + route: [ + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 0, width: 0.1, layer: "top" }, + { + route_type: "via", + x: 0, + y: 0, + from_layer: "top", + to_layer: "bottom", + via_diameter: 0.4, + via_hole_diameter: 0.1, + }, + { route_type: "wire", x: 0, y: 0, width: 0.1, layer: "bottom" }, + { route_type: "wire", x: -1, y: 0, width: 0.1, layer: "bottom" }, + ], + } + const candidateTrace: SimplifiedPcbTrace = { + type: "pcb_trace", + pcb_trace_id: "candidate", + connection_name: "candidate_net_mst0", + route: [ + { route_type: "wire", x: 0, y: -1, width: 0.1, layer: "inner1" }, + { route_type: "wire", x: 0, y: 1, width: 0.1, layer: "inner1" }, + ], + } + const inputSrj: SimpleRouteJson = { + layerCount: 4, + minTraceWidth: 0.1, + minViaDiameter: 0.4, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [ + { + name: "fixed_net", + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: -1, y: 0, layer: "bottom" }, + ], + }, + { + name: "candidate_net", + pointsToConnect: [ + { x: 0, y: -1, layer: "inner1" }, + { x: 0, y: 1, layer: "inner1" }, + ], + }, + ], + traces: [fixedTrace], + } + const result = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs: { + ...inputSrj, + connections: [ + { + name: "candidate_net_mst0", + __rootConnectionNames: ["candidate_net"], + pointsToConnect: inputSrj.connections[1]!.pointsToConnect, + }, + ], + }, + traces: [candidateTrace], + }) + const via = result.circuitJson.find((element) => element.type === "pcb_via") + expect(via?.type === "pcb_via" ? via.layers : undefined).toEqual([ + "top", + "inner1", + "inner2", + "bottom", + ]) + + const overlapError = result.errors.find( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id === + `overlap_candidate_${via?.type === "pcb_via" ? via.pcb_via_id : "missing"}`, + ) + expect(overlapError).toBeDefined() + expect( + ( + overlapError as + | (typeof overlapError & { candidate_pcb_trace_ids?: string[] }) + | undefined + )?.candidate_pcb_trace_ids, + ).toEqual(["candidate"]) +}) diff --git a/tests/features/infer-preloaded-trace-connectivity.test.ts b/tests/features/infer-preloaded-trace-connectivity.test.ts index 27cbaa39e..cfe0f0fb9 100644 --- a/tests/features/infer-preloaded-trace-connectivity.test.ts +++ b/tests/features/infer-preloaded-trace-connectivity.test.ts @@ -75,3 +75,62 @@ test("infers quantized endpoints through multilayer connected pads", () => { "quantized-top", ]) }) + +test("removes stale known port metadata that retained copper no longer reaches", () => { + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -3, minY: -3, maxX: 3, maxY: 3 }, + obstacles: [], + connections: [ + { + name: "net1", + pointsToConnect: [ + { + x: -2, + y: 0, + layer: "top", + pointId: "port-a", + pcb_port_id: "port-a", + }, + { + x: 2, + y: 0, + layer: "top", + pointId: "port-b", + pcb_port_id: "port-b", + }, + ], + }, + ], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "partial-net1", + connection_name: "net1", + connectsTo: ["port-a", "port-b", "non-port-alias"], + route: [ + { + route_type: "wire", + x: -2, + y: 0, + width: 0.1, + layer: "top", + }, + { + route_type: "wire", + x: 0, + y: 0, + width: 0.1, + layer: "top", + }, + ], + }, + ], + } + + expect(inferPreloadedTraceConnectivity(srj).traces?.[0]?.connectsTo).toEqual([ + "non-port-alias", + "port-a", + ]) +}) diff --git a/tests/features/pipeline9-error-owned-cluster-rebuild.test.ts b/tests/features/pipeline9-error-owned-cluster-rebuild.test.ts new file mode 100644 index 000000000..5b4779c01 --- /dev/null +++ b/tests/features/pipeline9-error-owned-cluster-rebuild.test.ts @@ -0,0 +1,468 @@ +import { expect, test } from "bun:test" +import type { + DrcEvaluator, + HighDensityRoute, + SimpleRouteJson, +} from "high-density-repair03/lib" +import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" +import type { + Pipeline9IjumpRerouteOptions, + Pipeline9IjumpRerouteResult, + Pipeline9TerminalViaEscapeOptions, +} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" + +const srj: SimpleRouteJson = { + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + obstacles: [], + connections: ["A", "B", "C"].map((name, index) => ({ + name, + pointsToConnect: [ + { x: -1, y: index - 1, layer: "top", pointId: `${name}_start` }, + { x: 1, y: index - 1, layer: "top", pointId: `${name}_end` }, + ], + })), +} + +const hdRoutes: HighDensityRoute[] = ["A", "B", "C"].map( + (connectionName, index) => ({ + connectionName, + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { + x: -1, + y: index - 1, + z: 0, + pcb_port_id: `${connectionName}_start`, + }, + { + x: 1, + y: index - 1, + z: 0, + pcb_port_id: `${connectionName}_end`, + }, + ], + }), +) + +const drcEvaluator: DrcEvaluator = ({ routes }) => { + if (routes?.every((route) => route.route.length >= 3)) { + if ((routes[0]?.route.length ?? 0) >= 4) return [] + const postClusterError = { + type: "pcb_via_trace_clearance_error", + error_type: "pcb_via_trace_clearance_error", + pcb_trace_id: "B_0", + candidate_pcb_trace_ids: ["A_0"], + center: { x: 0, y: -0.5 }, + } + return { + errors: [postClusterError], + errorsWithCenters: [postClusterError], + } + } + const errors = [ + { + type: "pcb_trace_error", + error_type: "pcb_trace_error", + pcb_trace_id: "A_0", + pcb_trace_error_id: "overlap_A_0_B_0", + candidate_pcb_trace_ids: ["A_0", "B_0"], + center: { x: 0, y: -0.5 }, + }, + { + type: "pcb_trace_error", + error_type: "pcb_trace_error", + pcb_trace_id: "A_0", + pcb_trace_error_id: "overlap_A_0_C_0", + candidate_pcb_trace_ids: ["A_0", "C_0"], + center: { x: 0, y: 0.5 }, + }, + ] + return { errors, errorsWithCenters: errors } +} + +test("Pipeline9 atomically rebuilds error-owned routes in residual-degree order", () => { + const solver = new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator, + originalObstacles: [], + ijumpBaseObstacles: [], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + const attempts: Array<{ + routeIndex: number + omittedRouteIndexes: number[] + maxIterations: number + }> = [] + const stubRerouter = { + tryReroute: ( + routes: HighDensityRoute[], + options: Pipeline9IjumpRerouteOptions, + ): Pipeline9IjumpRerouteResult | undefined => { + const target = routes[options.routeIndex] + const start = target?.route[0] + const end = target?.route.at(-1) + if (!target || !start || !end) return undefined + attempts.push({ + routeIndex: options.routeIndex, + omittedRouteIndexes: [ + ...(options.omitCandidateRouteIndexes ?? []), + ].sort((left, right) => left - right), + maxIterations: options.maxIterations, + }) + return { + route: { + ...target, + route: + target.route.length >= 3 + ? [ + start, + { + x: -0.2, + y: start.y + 0.2, + z: start.z, + traceThickness: target.traceThickness, + }, + { + x: 0.2, + y: start.y + 0.2, + z: start.z, + traceThickness: target.traceThickness, + }, + end, + ] + : [ + start, + { + x: 0, + y: start.y + 0.2, + z: start.z, + traceThickness: target.traceThickness, + }, + end, + ], + vias: [], + }, + iterations: 100, + } + }, + } + const privateSolver = solver as unknown as { + ijumpRerouter: typeof stubRerouter + runIjumpErrorOwnedClusterRebuild: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + errorOwnedClusterAccepted: number + errorOwnedClusterIterations: number + } + privateSolver.ijumpRerouter = stubRerouter + + const output = privateSolver.runIjumpErrorOwnedClusterRebuild(hdRoutes) + + expect(output.map((route) => route.route.length)).toEqual([4, 3, 3]) + expect(attempts).toEqual([ + { + routeIndex: 0, + omittedRouteIndexes: [1, 2], + maxIterations: 15_000, + }, + { + routeIndex: 1, + omittedRouteIndexes: [2], + maxIterations: 15_000, + }, + { + routeIndex: 2, + omittedRouteIndexes: [], + maxIterations: 15_000, + }, + { + routeIndex: 0, + omittedRouteIndexes: [], + maxIterations: 15_000, + }, + ]) + expect(privateSolver.errorOwnedClusterAccepted).toBe(1) + expect(privateSolver.errorOwnedClusterIterations).toBe(400) + expect(output[0]?.route[0]).toEqual(hdRoutes[0]?.route[0]) + expect(output[0]?.route.at(-1)).toEqual(hdRoutes[0]?.route.at(-1)) +}) + +test("Pipeline9 falls back to a low-degree-first reverse cluster with a bounded terminal escape", () => { + const solver = new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator, + originalObstacles: [], + ijumpBaseObstacles: [], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + const routeAttempts: Array<{ routeIndex: number; reverse: boolean }> = [] + const terminalAttempts: number[] = [] + const rebuildRoute = ( + routes: HighDensityRoute[], + routeIndex: number, + ): HighDensityRoute | undefined => { + const target = routes[routeIndex] + const start = target?.route[0] + const end = target?.route.at(-1) + if (!target || !start || !end) return undefined + return { + ...target, + route: [ + start, + { + x: 0, + y: start.y + 0.2, + z: start.z, + }, + end, + ], + vias: [], + } + } + const stubRerouter = { + tryReroute: ( + routes: HighDensityRoute[], + options: Pipeline9IjumpRerouteOptions, + ): Pipeline9IjumpRerouteResult => { + routeAttempts.push({ + routeIndex: options.routeIndex, + reverse: options.reverse, + }) + if (!options.reverse || options.routeIndex === 0) { + return { iterations: 100 } + } + return { + route: rebuildRoute(routes, options.routeIndex), + iterations: 100, + } + }, + getTerminalViaEscapeCandidates: () => [ + { + alternateZ: 1, + startVia: { x: -1, y: -1 }, + endVia: { x: 1, y: -1 }, + }, + ], + tryRerouteWithTerminalViaEscape: ( + routes: HighDensityRoute[], + options: Pipeline9TerminalViaEscapeOptions, + ): Pipeline9IjumpRerouteResult => { + terminalAttempts.push(options.routeIndex) + return { + route: rebuildRoute(routes, options.routeIndex), + iterations: 100, + } + }, + } + const privateSolver = solver as unknown as { + ijumpRerouter: typeof stubRerouter + runIjumpErrorOwnedClusterRebuild: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + errorOwnedClusterAccepted: number + errorOwnedClusterIterations: number + errorOwnedClusterTerminalEscapeAttempts: number + } + privateSolver.ijumpRerouter = stubRerouter + + const output = privateSolver.runIjumpErrorOwnedClusterRebuild(hdRoutes) + + expect(output.every((route) => route.route.length === 3)).toBe(true) + expect(routeAttempts.some((attempt) => attempt.reverse)).toBe(true) + expect( + routeAttempts + .filter((attempt) => attempt.reverse) + .map((attempt) => attempt.routeIndex), + ).toEqual([2, 1, 0]) + expect(terminalAttempts).toEqual([0]) + expect(privateSolver.errorOwnedClusterAccepted).toBe(1) + expect(privateSolver.errorOwnedClusterIterations).toBe(700) + expect(privateSolver.errorOwnedClusterTerminalEscapeAttempts).toBe(1) +}) + +test.each([ + { + initialDrcIssueCount: 19, + expectedLocalLimit: 500, + expectedConsecutiveMissLimit: 500, + expectedIjumpLimit: 300_000, + }, + { + initialDrcIssueCount: 20, + expectedLocalLimit: 300, + expectedConsecutiveMissLimit: 64, + expectedIjumpLimit: 200_000, + }, +])( + "Pipeline9 selects adaptive cleanup limits for $initialDrcIssueCount initial errors", + ({ + initialDrcIssueCount, + expectedLocalLimit, + expectedConsecutiveMissLimit, + expectedIjumpLimit, + }) => { + const solver = new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator, + originalObstacles: [], + ijumpBaseObstacles: [], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + const privateSolver = solver as unknown as { + stats: Record + localCleanupDrcEvaluations: number + consecutiveLocalCleanupDrcMisses: number + ijumpIterations: number + selectedLocalCleanupDrcEvaluationLimit: number + selectedConsecutiveLocalCleanupDrcMissLimit: number + selectedIjumpIterationLimit: number + selectAdaptiveCleanupLimits: () => void + hasLocalCleanupBudget: () => boolean + getRemainingIjumpIterations: () => number + } + privateSolver.stats = { initialDrcIssueCount } + privateSolver.selectAdaptiveCleanupLimits() + + expect(privateSolver.selectedLocalCleanupDrcEvaluationLimit).toBe( + expectedLocalLimit, + ) + expect(privateSolver.selectedConsecutiveLocalCleanupDrcMissLimit).toBe( + expectedConsecutiveMissLimit, + ) + expect(privateSolver.selectedIjumpIterationLimit).toBe(expectedIjumpLimit) + privateSolver.consecutiveLocalCleanupDrcMisses = + expectedConsecutiveMissLimit + expect(privateSolver.hasLocalCleanupBudget()).toBe(false) + privateSolver.ijumpIterations = expectedIjumpLimit - 123 + expect(privateSolver.getRemainingIjumpIterations()).toBe(123) + }, +) + +test("Pipeline9 bounds consecutive fruitless local DRC evaluations and resets after improvement", () => { + let reportedIssueCount = 2 + const boundedDrcEvaluator: DrcEvaluator = () => { + const errors = Array.from({ length: reportedIssueCount }, (_, index) => ({ + type: "pcb_trace_error", + error_type: "pcb_trace_error", + pcb_trace_id: `candidate_${index}`, + })) + return { errors, errorsWithCenters: errors } + } + const solver = new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator: boundedDrcEvaluator, + originalObstacles: [], + ijumpBaseObstacles: [], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + const privateSolver = solver as unknown as { + consecutiveLocalCleanupDrcMisses: number + maxConsecutiveLocalCleanupDrcMisses: number + selectedLocalCleanupDrcEvaluationLimit: number + selectedConsecutiveLocalCleanupDrcMissLimit: number + candidateImprovesSnapshot: ( + routes: HighDensityRoute[], + currentIssueCount: number, + source: "local", + ) => boolean + hasLocalCleanupBudget: () => boolean + } + privateSolver.selectedLocalCleanupDrcEvaluationLimit = 10 + privateSolver.selectedConsecutiveLocalCleanupDrcMissLimit = 2 + + expect(privateSolver.candidateImprovesSnapshot(hdRoutes, 2, "local")).toBe( + false, + ) + expect(privateSolver.consecutiveLocalCleanupDrcMisses).toBe(1) + + reportedIssueCount = 1 + expect(privateSolver.candidateImprovesSnapshot(hdRoutes, 2, "local")).toBe( + true, + ) + expect(privateSolver.consecutiveLocalCleanupDrcMisses).toBe(0) + + reportedIssueCount = 2 + expect(privateSolver.candidateImprovesSnapshot(hdRoutes, 2, "local")).toBe( + false, + ) + expect(privateSolver.hasLocalCleanupBudget()).toBe(true) + expect(privateSolver.candidateImprovesSnapshot(hdRoutes, 2, "local")).toBe( + false, + ) + expect(privateSolver.hasLocalCleanupBudget()).toBe(false) + expect(privateSolver.maxConsecutiveLocalCleanupDrcMisses).toBe(2) +}) + +test("Pipeline9 reserves a bounded post-cluster via micro-shift sweep after the base local budget", () => { + const solver = new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator, + originalObstacles: [], + ijumpBaseObstacles: [], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + const privateSolver = solver as unknown as { + localCleanupDrcEvaluations: number + consecutiveLocalCleanupDrcMisses: number + selectedLocalCleanupDrcEvaluationLimit: number + selectedConsecutiveLocalCleanupDrcMissLimit: number + postClusterViaMicroShiftDrcEvaluations: number + hasLocalCleanupBudget: () => boolean + runViaMicroShiftCleanup: (routes: HighDensityRoute[]) => HighDensityRoute[] + runPostClusterViaMicroShiftCleanup: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + } + privateSolver.selectedLocalCleanupDrcEvaluationLimit = 300 + privateSolver.localCleanupDrcEvaluations = 300 + privateSolver.selectedConsecutiveLocalCleanupDrcMissLimit = 64 + privateSolver.consecutiveLocalCleanupDrcMisses = 64 + privateSolver.runViaMicroShiftCleanup = (routes) => { + expect(privateSolver.hasLocalCleanupBudget()).toBe(true) + privateSolver.localCleanupDrcEvaluations += 32 + return routes + } + + const output = privateSolver.runPostClusterViaMicroShiftCleanup(hdRoutes) + + expect(output).toBe(hdRoutes) + expect(privateSolver.postClusterViaMicroShiftDrcEvaluations).toBe(32) + expect(privateSolver.selectedLocalCleanupDrcEvaluationLimit).toBe(300) + expect(privateSolver.consecutiveLocalCleanupDrcMisses).toBe(64) + expect(privateSolver.hasLocalCleanupBudget()).toBe(false) +}) diff --git a/tests/features/pipeline9-final-continuity-terminal-via.test.ts b/tests/features/pipeline9-final-continuity-terminal-via.test.ts new file mode 100644 index 000000000..44509209f --- /dev/null +++ b/tests/features/pipeline9-final-continuity-terminal-via.test.ts @@ -0,0 +1,248 @@ +import { expect, test } from "bun:test" +import { ConnectivityMap } from "circuit-json-to-connectivity-map" +import type { DrcEvaluator, SimpleRouteJson } from "high-density-repair03/lib" +import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" +import type { Obstacle } from "lib/types" +import type { HighDensityRoute } from "lib/types/high-density-types" +import { convertHdRouteToSimplifiedRoute } from "lib/utils/convertHdRouteToSimplifiedRoute" + +const terminal = { x: 13.9125, y: -6 } +const terminalPortId = "pcb_port_16" + +const srj: SimpleRouteJson = { + bounds: { minX: 9, minY: -9, maxX: 16, maxY: -3 }, + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + obstacles: [], + connections: [ + { + name: "source_net_1_mst0", + netConnectionName: "source_net_1", + pointsToConnect: [ + { + x: 10, + y: -8, + layer: "bottom", + pointId: "pcb_port_start", + pcb_port_id: "pcb_port_start", + }, + { + ...terminal, + layer: "bottom", + pointId: terminalPortId, + pcb_port_id: terminalPortId, + }, + ], + }, + ], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "preloaded_source_net_1", + connection_name: "source_net_1", + route: [ + { + route_type: "wire", + x: 15, + y: -5, + width: 0.1, + layer: "top", + }, + { + route_type: "wire", + ...terminal, + width: 0.1, + layer: "top", + }, + ], + }, + ], +} + +const terminalPad: Obstacle = { + type: "rect", + layers: ["bottom"], + center: terminal, + width: 1, + height: 0.6, + connectedTo: [ + "pcb_smtpad_16", + terminalPortId, + "source_net_1", + "source_net_1_mst0", + ], +} + +const startPad: Obstacle = { + type: "rect", + layers: ["bottom"], + center: { x: 10, y: -8 }, + width: 1, + height: 0.6, + connectedTo: [ + "pcb_smtpad_start", + "pcb_port_start", + "source_net_1", + "source_net_1_mst0", + ], +} + +const route: HighDensityRoute = { + connectionName: "source_net_1_mst0", + rootConnectionName: "source_net_1", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: 10, y: -8, z: 1, pcb_port_id: "pcb_port_start" }, + { x: 12, y: -6, z: 1 }, + { ...terminal, z: 1, pcb_port_id: terminalPortId }, + ], +} + +const missingConnectionError = { + type: "pcb_trace_error", + error_type: "pcb_trace_error", + pcb_trace_error_id: "missing_connection_combined_source_net_1", + pcb_trace_id: "source_net_1_mst0_0", + source_trace_id: "source_net_1", + candidate_pcb_trace_ids: ["source_net_1_mst0_0"], + center: terminal, +} + +test("Pipeline9 bridges a bottom terminal to same-net preloaded top copper", () => { + const drcEvaluator: DrcEvaluator = ({ routes }) => { + const candidateRoute = routes?.[0] + const hasTerminalVia = + candidateRoute?.route.some((point, pointIndex) => { + const nextPoint = candidateRoute.route[pointIndex + 1] + return ( + nextPoint !== undefined && + point.x === terminal.x && + point.y === terminal.y && + nextPoint.x === terminal.x && + nextPoint.y === terminal.y && + point.z !== nextPoint.z + ) + }) ?? false + const errors = hasTerminalVia ? [] : [missingConnectionError] + return { errors, errorsWithCenters: errors } + } + const solver = new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes: [route], + drcEvaluator, + connMap: new ConnectivityMap({ + connectivity_net16: ["source_net_1", "source_net_1_mst0"], + }), + originalObstacles: [startPad, terminalPad], + ijumpBaseObstacles: [startPad, terminalPad], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + const privateSolver = solver as unknown as { + localCleanupDrcEvaluations: number + selectedLocalCleanupDrcEvaluationLimit: number + runFinalContinuityTerminalViaBridge: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + getSnapshot: (routes: HighDensityRoute[]) => { + count: number + errors: Array> + traceRouteIndexById: Map + } + getFinalContinuityTerminalViaCandidates: ( + routes: HighDensityRoute[], + snapshot: { + count: number + errors: Array> + traceRouteIndexById: Map + }, + error: Record, + ) => Array<{ + routeIndex: number + endpoint: "start" | "end" + targetZ: number + }> + addFinalContinuityTerminalViaStub: ( + route: HighDensityRoute, + endpoint: "start" | "end", + targetZ: number, + ) => HighDensityRoute | undefined + candidatePreservesTerminals: (routes: HighDensityRoute[]) => boolean + finalContinuityTerminalViaAttempts: number + finalContinuityTerminalViaDrcEvaluations: number + finalContinuityTerminalViaCandidatesAccepted: number + } + privateSolver.localCleanupDrcEvaluations = 300 + privateSolver.selectedLocalCleanupDrcEvaluationLimit = 300 + + const baselineSnapshot = privateSolver.getSnapshot([route]) + expect( + privateSolver + .getFinalContinuityTerminalViaCandidates( + [route], + baselineSnapshot, + baselineSnapshot.errors[0]!, + ) + .map(({ routeIndex, endpoint, targetZ }) => ({ + routeIndex, + endpoint, + targetZ, + })), + ).toEqual([{ routeIndex: 0, endpoint: "end", targetZ: 0 }]) + const directBridge = privateSolver.addFinalContinuityTerminalViaStub( + route, + "end", + 0, + ) + expect(directBridge).toBeDefined() + expect( + directBridge + ? privateSolver.candidatePreservesTerminals([directBridge]) + : false, + ).toBe(true) + + const output = privateSolver.runFinalContinuityTerminalViaBridge([route]) + const outputRoute = output[0]! + const simplified = convertHdRouteToSimplifiedRoute(outputRoute, 2) + const layerTransitions = outputRoute.route.flatMap((point, pointIndex) => { + const nextPoint = outputRoute.route[pointIndex + 1] + return nextPoint && point.z !== nextPoint.z ? [{ point, nextPoint }] : [] + }) + + expect(privateSolver.finalContinuityTerminalViaAttempts).toBe(1) + expect(privateSolver.finalContinuityTerminalViaDrcEvaluations).toBe(1) + expect(privateSolver.finalContinuityTerminalViaCandidatesAccepted).toBe(1) + expect(privateSolver.getSnapshot(output).count).toBe(0) + expect(outputRoute.route.at(-1)).toMatchObject({ + ...terminal, + z: 1, + pcb_port_id: terminalPortId, + }) + expect(outputRoute.vias).toHaveLength(1) + expect(outputRoute.vias[0]!.x).toBeCloseTo(terminal.x, 3) + expect(outputRoute.vias[0]!.y).toBe(terminal.y) + expect(layerTransitions).toHaveLength(2) + expect( + layerTransitions.every( + ({ point, nextPoint }) => + point.x === nextPoint.x && point.y === nextPoint.y, + ), + ).toBe(true) + expect(simplified.filter((segment) => segment.route_type === "via")).toEqual([ + { + route_type: "via", + ...terminal, + from_layer: "bottom", + to_layer: "top", + via_diameter: 0.3, + }, + ]) + expect(privateSolver.localCleanupDrcEvaluations).toBe(300) +}) diff --git a/tests/features/pipeline9-final-endpoint-slide-cleanup.test.ts b/tests/features/pipeline9-final-endpoint-slide-cleanup.test.ts new file mode 100644 index 000000000..dc724d9f5 --- /dev/null +++ b/tests/features/pipeline9-final-endpoint-slide-cleanup.test.ts @@ -0,0 +1,192 @@ +import { expect, test } from "bun:test" +import { ConnectivityMap } from "circuit-json-to-connectivity-map" +import type { + DrcEvaluator, + HighDensityRoute, + SimpleRouteJson, +} from "high-density-repair03/lib" +import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" +import type { Obstacle } from "lib/types" + +const terminal = { x: -15.85, y: -14.175 } +const foreignPadCenter = { x: -15.81, y: -15 } +const sharedPortId = "pcb_port_76" +const clearTerminalY = terminal.y + 0.07 + +const srj: SimpleRouteJson = { + bounds: { minX: -18, minY: -18, maxX: -12, maxY: -11 }, + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + obstacles: [], + connections: [ + { + name: "source_net_33_mst0", + pointsToConnect: [ + { + ...terminal, + layer: "top", + pointId: sharedPortId, + pcb_port_id: sharedPortId, + }, + { + x: -13, + y: -13, + layer: "top", + pointId: "branch_0_end", + }, + ], + }, + { + name: "source_net_33_mst1", + pointsToConnect: [ + { + x: -13, + y: -14, + layer: "top", + pointId: "branch_1_start", + }, + { + ...terminal, + layer: "top", + pointId: sharedPortId, + pcb_port_id: sharedPortId, + }, + ], + }, + ], +} + +const ownPad: Obstacle = { + type: "rect", + layers: ["top"], + center: { x: -15.85, y: -14.175 }, + width: 1, + height: 0.6, + connectedTo: [ + "pcb_smtpad_76", + sharedPortId, + "source_net_33", + "source_net_33_mst0", + "source_net_33_mst1", + ], +} + +const foreignPad: Obstacle = { + type: "rect", + layers: ["top", "bottom"], + center: foreignPadCenter, + width: 1.5, + height: 1.5, + connectedTo: ["pcb_plated_hole_24", "foreign_net"], +} + +const hdRoutes: HighDensityRoute[] = [ + { + connectionName: "source_net_33_mst0", + rootConnectionName: "source_net_33", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { ...terminal, z: 0, pcb_port_id: sharedPortId }, + { x: -15, y: -13.7, z: 0 }, + { x: -13, y: -13, z: 0, pcb_port_id: "branch_0_end" }, + ], + }, + { + connectionName: "source_net_33_mst1", + rootConnectionName: "source_net_33", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: -13, y: -14, z: 0, pcb_port_id: "branch_1_start" }, + { x: -15, y: -13.8, z: 0 }, + { ...terminal, z: 0, pcb_port_id: sharedPortId }, + ], + }, +] + +const makePadTraceError = (traceId: string) => ({ + type: "pcb_pad_trace_clearance_error", + error_type: "pcb_pad_trace_clearance_error", + pcb_trace_id: traceId, + pcb_pad_id: "pcb_plated_hole_24", + pcb_pad_trace_clearance_error_id: `clearance_${traceId}_pcb_plated_hole_24`, + candidate_pcb_trace_ids: [traceId], + center: foreignPadCenter, +}) + +test("Pipeline9 atomically slides a shared terminal after local cleanup is exhausted", () => { + const evaluatedTerminalPairs: Array<{ + branch0: { x: number; y: number } + branch1: { x: number; y: number } + }> = [] + const drcEvaluator: DrcEvaluator = ({ routes }) => { + const branch0 = routes?.[0]?.route[0] ?? terminal + const branch1 = routes?.[1]?.route.at(-1) ?? terminal + evaluatedTerminalPairs.push({ + branch0: { x: branch0.x, y: branch0.y }, + branch1: { x: branch1.x, y: branch1.y }, + }) + const errors = [ + ...(branch0.y < clearTerminalY + ? [makePadTraceError("source_net_33_mst0_0")] + : []), + ...(branch1.y < clearTerminalY + ? [makePadTraceError("source_net_33_mst1_0")] + : []), + ] + return { errors, errorsWithCenters: errors } + } + const solver = new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator, + connMap: new ConnectivityMap({ + source_net_33: ["source_net_33_mst0", "source_net_33_mst1"], + foreign_net: ["foreign_net"], + }), + originalObstacles: [ownPad, foreignPad], + ijumpBaseObstacles: [ownPad, foreignPad], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + const privateSolver = solver as unknown as { + localCleanupDrcEvaluations: number + selectedLocalCleanupDrcEvaluationLimit: number + runFinalEndpointSlideCleanup: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + getSnapshot: (routes: HighDensityRoute[]) => { count: number } + finalEndpointSlideAttempts: number + finalEndpointSlideDrcEvaluations: number + finalEndpointSlideCandidatesAccepted: number + finalEndpointSlideRelocatedBranches: number + } + privateSolver.localCleanupDrcEvaluations = 300 + privateSolver.selectedLocalCleanupDrcEvaluationLimit = 300 + + const output = privateSolver.runFinalEndpointSlideCleanup(hdRoutes) + + expect(privateSolver.getSnapshot(output).count).toBe(0) + expect(output[0]!.route[0]!.y).toBeGreaterThanOrEqual(clearTerminalY) + expect(output[1]!.route.at(-1)!.y).toBe(output[0]!.route[0]!.y) + expect(output[1]!.route.at(-1)!.x).toBe(output[0]!.route[0]!.x) + expect( + evaluatedTerminalPairs.every( + ({ branch0, branch1 }) => + branch0.x === branch1.x && branch0.y === branch1.y, + ), + ).toBe(true) + expect(privateSolver.localCleanupDrcEvaluations).toBe(300) + expect(privateSolver.finalEndpointSlideAttempts).toBeGreaterThan(0) + expect(privateSolver.finalEndpointSlideDrcEvaluations).toBeLessThanOrEqual(32) + expect(privateSolver.finalEndpointSlideCandidatesAccepted).toBe(1) + expect(privateSolver.finalEndpointSlideRelocatedBranches).toBe(2) +}) diff --git a/tests/features/pipeline9-final-error-owner-sweep.test.ts b/tests/features/pipeline9-final-error-owner-sweep.test.ts new file mode 100644 index 000000000..2f0b4e64b --- /dev/null +++ b/tests/features/pipeline9-final-error-owner-sweep.test.ts @@ -0,0 +1,347 @@ +import { expect, test } from "bun:test" +import type { + DrcEvaluator, + HighDensityRoute, + SimpleRouteJson, +} from "high-density-repair03/lib" +import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" +import type { + Pipeline9IjumpRerouteOptions, + Pipeline9IjumpRerouteResult, +} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" + +const makeSrj = (names: string[]): SimpleRouteJson => ({ + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + obstacles: [], + connections: names.map((name, index) => ({ + name, + pointsToConnect: [ + { x: -1, y: index * 0.5, layer: "top", pointId: `${name}_start` }, + { x: 1, y: index * 0.5, layer: "top", pointId: `${name}_end` }, + ], + })), +}) + +const makeSolver = ( + srj: SimpleRouteJson, + hdRoutes: HighDensityRoute[], + drcEvaluator: DrcEvaluator, +) => + new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator, + originalObstacles: [], + ijumpBaseObstacles: [], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + +const makeError = (traceId: string, center: { x: number; y: number }) => ({ + type: "pcb_trace_error", + error_type: "pcb_trace_error", + pcb_trace_id: traceId, + pcb_trace_error_id: `overlap_${traceId}_fixed_trace`, + candidate_pcb_trace_ids: [traceId], + center, +}) + +test("Pipeline9 final-owner sweep handles a 1,046-iteration single owner", () => { + const srj = makeSrj(["A"]) + const hdRoutes: HighDensityRoute[] = [ + { + connectionName: "A", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: -1, y: 0, z: 0, pcb_port_id: "A_start" }, + { x: 1, y: 0, z: 0, pcb_port_id: "A_end" }, + ], + }, + ] + const drcEvaluator: DrcEvaluator = ({ routes }) => { + if ((routes?.[0]?.route.length ?? 0) >= 3) return [] + const error = makeError("A_0", { x: 0, y: 0 }) + return { errors: [error], errorsWithCenters: [error] } + } + const solver = makeSolver(srj, hdRoutes, drcEvaluator) + const stubRerouter = { + tryReroute: ( + routes: HighDensityRoute[], + options: Pipeline9IjumpRerouteOptions, + ): Pipeline9IjumpRerouteResult => { + const target = routes[options.routeIndex]! + return { + route: { + ...target, + route: [ + target.route[0]!, + { x: 0, y: 0.2, z: 0 }, + target.route.at(-1)!, + ], + }, + iterations: 1_046, + } + }, + } + const privateSolver = solver as unknown as { + ijumpRerouter: typeof stubRerouter + runIjumpFinalErrorOwnerSweep: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + finalOwnerFullAttempts: number + finalOwnerCandidatesAccepted: number + finalOwnerIterations: number + } + privateSolver.ijumpRerouter = stubRerouter + + const output = privateSolver.runIjumpFinalErrorOwnerSweep(hdRoutes) + + expect(drcEvaluator({ traces: [], routes: output })).toEqual([]) + expect(privateSolver.finalOwnerFullAttempts).toBe(1) + expect(privateSolver.finalOwnerCandidatesAccepted).toBe(1) + expect(privateSolver.finalOwnerIterations).toBe(1_046) +}) + +test("Pipeline9 recomputes final owners and falls back to a nearest-error interior window", () => { + const srj = makeSrj(["A", "B"]) + const hdRoutes: HighDensityRoute[] = [ + { + connectionName: "A", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: -1, y: 0, z: 0, pcb_port_id: "A_start" }, + { x: 1, y: 0, z: 0, pcb_port_id: "A_end" }, + ], + }, + { + connectionName: "B", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: -1, y: 0.5, z: 0, pcb_port_id: "B_start" }, + { x: -0.25, y: 0.4, z: 0 }, + { x: 0.25, y: 0.4, z: 0 }, + { x: 1, y: 0.5, z: 0, pcb_port_id: "B_end" }, + ], + }, + ] + const drcEvaluator: DrcEvaluator = ({ routes }) => { + const errors: Array> = [] + if ((routes?.[0]?.route.length ?? 0) < 3) { + errors.push(makeError("A_0", { x: 0, y: 0 })) + } + if ( + !routes?.[1]?.route.some((point) => point.y > 0.9 && !point.pcb_port_id) + ) { + errors.push(makeError("B_0", { x: 0, y: 0.4 })) + } + return errors.length === 0 ? [] : { errors, errorsWithCenters: errors } + } + const solver = makeSolver(srj, hdRoutes, drcEvaluator) + const attemptedRouteIndexes: number[] = [] + const stubRerouter = { + tryReroute: ( + routes: HighDensityRoute[], + options: Pipeline9IjumpRerouteOptions, + ): Pipeline9IjumpRerouteResult => { + attemptedRouteIndexes.push(options.routeIndex) + const target = routes[options.routeIndex]! + if (options.routeIndex === 0) { + return { + route: { + ...target, + route: [ + target.route[0]!, + { x: 0, y: 0.2, z: 0 }, + target.route.at(-1)!, + ], + }, + iterations: 100, + } + } + if (options.startIndex === undefined) return { iterations: 100 } + return { + route: { + ...target, + route: [ + target.route[0]!, + { x: -0.25, y: 1.1, z: 0 }, + { x: 0.25, y: 1.1, z: 0 }, + target.route.at(-1)!, + ], + }, + iterations: 100, + } + }, + } + const privateSolver = solver as unknown as { + ijumpRerouter: typeof stubRerouter + runIjumpFinalErrorOwnerSweep: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + finalOwnerFullAttempts: number + finalOwnerInteriorAttempts: number + finalOwnerCandidatesAccepted: number + } + privateSolver.ijumpRerouter = stubRerouter + + const output = privateSolver.runIjumpFinalErrorOwnerSweep(hdRoutes) + + expect(drcEvaluator({ traces: [], routes: output })).toEqual([]) + expect(attemptedRouteIndexes[0]).toBe(0) + expect(attemptedRouteIndexes.slice(1)).toContain(1) + expect(privateSolver.finalOwnerFullAttempts).toBe(4) + expect(privateSolver.finalOwnerInteriorAttempts).toBe(1) + expect(privateSolver.finalOwnerCandidatesAccepted).toBe(2) +}) + +test("Pipeline9 retries a failed final owner after accepted copper changes", () => { + const srj = makeSrj(["A", "B"]) + const hdRoutes: HighDensityRoute[] = [ + { + connectionName: "A", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: -1, y: 0, z: 0, pcb_port_id: "A_start" }, + { x: 1, y: 0, z: 0, pcb_port_id: "A_end" }, + ], + }, + { + connectionName: "B", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: -1, y: 0.5, z: 0, pcb_port_id: "B_start" }, + { x: 1, y: 0.5, z: 0, pcb_port_id: "B_end" }, + ], + }, + ] + const fixedErrors = ["fixed_0", "fixed_1"].map((traceId, index) => ({ + type: "pcb_trace_error", + error_type: "pcb_trace_error", + pcb_trace_id: traceId, + pcb_trace_error_id: `overlap_${traceId}_preloaded`, + center: { x: index * 0.1, y: 1.5 }, + })) + const drcEvaluator: DrcEvaluator = ({ routes }) => { + const errors: Array> = [...fixedErrors] + if ((routes?.[0]?.route.length ?? 0) < 3) { + errors.unshift(makeError("A_0", { x: 0, y: 0 })) + } + if ((routes?.[1]?.route.length ?? 0) < 3) { + errors.splice( + errors.length - fixedErrors.length, + 0, + makeError("B_0", { + x: 0, + y: 0.5, + }), + ) + } + return { errors, errorsWithCenters: errors } + } + const solver = makeSolver(srj, hdRoutes, drcEvaluator) + const rawForwardAttempts: number[] = [] + const stubRerouter = { + tryReroute: ( + routes: HighDensityRoute[], + options: Pipeline9IjumpRerouteOptions, + ): Pipeline9IjumpRerouteResult => { + if (!options.reverse && !options.shortenPath) { + rawForwardAttempts.push(options.routeIndex) + } + if (options.reverse || options.shortenPath) { + return { iterations: 10 } + } + const target = routes[options.routeIndex]! + if (options.routeIndex === 0 && (routes[1]?.route.length ?? 0) < 3) { + return { iterations: 10 } + } + return { + route: { + ...target, + route: [ + target.route[0]!, + { x: 0, y: options.routeIndex === 0 ? 0.2 : 0.8, z: 0 }, + target.route.at(-1)!, + ], + }, + iterations: 10, + } + }, + } + const privateSolver = solver as unknown as { + ijumpRerouter: typeof stubRerouter + runIjumpFinalErrorOwnerSweep: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + finalOwnerCandidatesAccepted: number + } + privateSolver.ijumpRerouter = stubRerouter + + const output = privateSolver.runIjumpFinalErrorOwnerSweep(hdRoutes) + + expect(output[0]?.route).toHaveLength(3) + expect(output[1]?.route).toHaveLength(3) + expect( + rawForwardAttempts.filter((routeIndex) => routeIndex === 0), + ).toHaveLength(2) + expect(privateSolver.finalOwnerCandidatesAccepted).toBe(2) +}) + +test("Pipeline9 final-owner sweep never exceeds its separate 50k budget", () => { + const srj = makeSrj(["A"]) + const hdRoutes: HighDensityRoute[] = [ + { + connectionName: "A", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: -1, y: 0, z: 0, pcb_port_id: "A_start" }, + { x: 1, y: 0, z: 0, pcb_port_id: "A_end" }, + ], + }, + ] + const error = makeError("A_0", { x: 0, y: 0 }) + const drcEvaluator: DrcEvaluator = () => ({ + errors: [error], + errorsWithCenters: [error], + }) + const solver = makeSolver(srj, hdRoutes, drcEvaluator) + const stubRerouter = { + tryReroute: ( + _routes: HighDensityRoute[], + options: Pipeline9IjumpRerouteOptions, + ): Pipeline9IjumpRerouteResult => ({ + iterations: options.maxIterations, + }), + } + const privateSolver = solver as unknown as { + ijumpRerouter: typeof stubRerouter + runIjumpFinalErrorOwnerSweep: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + finalOwnerIterations: number + } + privateSolver.ijumpRerouter = stubRerouter + + privateSolver.runIjumpFinalErrorOwnerSweep(hdRoutes) + + expect(privateSolver.finalOwnerIterations).toBe(50_000) +}) diff --git a/tests/features/pipeline9-fixed-copper-composite-repair.test.ts b/tests/features/pipeline9-fixed-copper-composite-repair.test.ts new file mode 100644 index 000000000..3297235a8 --- /dev/null +++ b/tests/features/pipeline9-fixed-copper-composite-repair.test.ts @@ -0,0 +1,201 @@ +import { expect, test } from "bun:test" +import type { + DrcEvaluator, + HighDensityRoute, + SimpleRouteJson, +} from "high-density-repair03/lib" +import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" +import type { + Pipeline9IjumpRerouteOptions, + Pipeline9IjumpRerouteResult, +} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" + +const srj: SimpleRouteJson = { + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + obstacles: [], + connections: ["A", "B"].map((name, index) => ({ + name, + pointsToConnect: [ + { + x: -1, + y: index * 0.5, + layer: "top", + pointId: `${name}_start`, + }, + { + x: 1, + y: index * 0.5, + layer: "top", + pointId: `${name}_end`, + }, + ], + })), +} + +const hdRoutes: HighDensityRoute[] = ["A", "B"].map( + (connectionName, index) => ({ + connectionName, + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { + x: -1, + y: index * 0.5, + z: 0, + pcb_port_id: `${connectionName}_start`, + }, + { + x: 1, + y: index * 0.5, + z: 0, + pcb_port_id: `${connectionName}_end`, + }, + ], + }), +) + +test("Pipeline9 atomically repairs a fixed-copper escape and its newly exposed owner", () => { + let sawWorseIntermediate = false + const fixedError = { + type: "pcb_trace_error", + error_type: "pcb_trace_error", + pcb_trace_id: "preloaded_0_fixed_trace", + pcb_trace_error_id: "overlap_preloaded_0_fixed_trace_A_0", + candidate_pcb_trace_ids: ["A_0"], + center: { x: 0, y: 0 }, + } + const exposedErrors = [ + { + type: "pcb_trace_error", + error_type: "pcb_trace_error", + pcb_trace_id: "B_0", + pcb_trace_error_id: "overlap_B_0_A_0", + candidate_pcb_trace_ids: ["B_0", "A_0"], + center: { x: 0.25, y: 0.25 }, + }, + { + type: "pcb_via_trace_clearance_error", + error_type: "pcb_via_trace_clearance_error", + pcb_via_trace_clearance_error_id: "clearance_B_0_A_0", + pcb_trace_id: "B_0", + candidate_pcb_trace_ids: ["B_0", "A_0"], + center: { x: 0.5, y: 0.25 }, + }, + ] + const drcEvaluator: DrcEvaluator = ({ routes }) => { + if (routes?.[0]?.rootConnectionName !== "fixed-only-primary") { + return { errors: [fixedError], errorsWithCenters: [fixedError] } + } + if (routes[1]?.rootConnectionName !== "candidate-aware-followup") { + sawWorseIntermediate = true + return { errors: exposedErrors, errorsWithCenters: exposedErrors } + } + return [] + } + const solver = new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator, + originalObstacles: [], + ijumpBaseObstacles: [], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + const attempts: Array<{ + routeIndex: number + includeCandidateCopper: boolean + shortenPath: boolean + }> = [] + const stubRerouter = { + tryReroute: ( + routes: HighDensityRoute[], + options: Pipeline9IjumpRerouteOptions, + ): Pipeline9IjumpRerouteResult | undefined => { + attempts.push({ + routeIndex: options.routeIndex, + includeCandidateCopper: options.includeCandidateCopper, + shortenPath: options.shortenPath, + }) + const targetRoute = routes[options.routeIndex] + if (!targetRoute) return undefined + + if ( + options.routeIndex === 0 && + !options.includeCandidateCopper && + options.shortenPath + ) { + return { + route: { + ...targetRoute, + rootConnectionName: "fixed-only-primary", + }, + iterations: 300, + } + } + if ( + options.routeIndex === 1 && + options.includeCandidateCopper && + routes[0]?.rootConnectionName === "fixed-only-primary" + ) { + return { + route: { + ...targetRoute, + rootConnectionName: "candidate-aware-followup", + }, + iterations: 200, + } + } + return { iterations: 50 } + }, + } + const privateSolver = solver as unknown as { + ijumpRerouter: typeof stubRerouter + runFixedCopperCompositeRepair: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + getSnapshot: (routes: HighDensityRoute[]) => { count: number } + fixedCopperCompositePrimaryAttempts: number + fixedCopperCompositeFollowupAttempts: number + fixedCopperCompositeDrcEvaluations: number + fixedCopperCompositeCandidatesAccepted: number + fixedCopperCompositeIterations: number + } + privateSolver.ijumpRerouter = stubRerouter + + expect(privateSolver.getSnapshot(hdRoutes).count).toBe(1) + const output = privateSolver.runFixedCopperCompositeRepair(hdRoutes) + + expect(sawWorseIntermediate).toBe(true) + expect(privateSolver.getSnapshot(output).count).toBe(0) + expect(output[0]?.rootConnectionName).toBe("fixed-only-primary") + expect(output[1]?.rootConnectionName).toBe("candidate-aware-followup") + expect(attempts).toEqual([ + { + routeIndex: 0, + includeCandidateCopper: false, + shortenPath: true, + }, + { + routeIndex: 1, + includeCandidateCopper: true, + shortenPath: false, + }, + ]) + expect(privateSolver.fixedCopperCompositePrimaryAttempts).toBe(1) + expect(privateSolver.fixedCopperCompositeFollowupAttempts).toBe(1) + expect(privateSolver.fixedCopperCompositeDrcEvaluations).toBe(2) + expect(privateSolver.fixedCopperCompositeCandidatesAccepted).toBe(1) + expect(privateSolver.fixedCopperCompositeIterations).toBe(500) + expect(output[0]?.route[0]).toEqual(hdRoutes[0]?.route[0]) + expect(output[0]?.route.at(-1)).toEqual(hdRoutes[0]?.route.at(-1)) + expect(output[1]?.route[0]).toEqual(hdRoutes[1]?.route[0]) + expect(output[1]?.route.at(-1)).toEqual(hdRoutes[1]?.route.at(-1)) +}) diff --git a/tests/features/pipeline9-ijump-rerouter.test.ts b/tests/features/pipeline9-ijump-rerouter.test.ts new file mode 100644 index 000000000..ea153a4fa --- /dev/null +++ b/tests/features/pipeline9-ijump-rerouter.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "bun:test" +import type { ConnectivityMap } from "circuit-json-to-connectivity-map" +import { Pipeline9IjumpRerouter } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" +import type { SimpleRouteJson } from "lib/types" +import type { HighDensityRoute } from "lib/types/high-density-types" + +test("Pipeline9 charges bounded budget when iJump throws before search initialization", () => { + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [], + } + const route: HighDensityRoute = { + connectionName: "missing_from_connectivity_map", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: -1, y: 0, z: 0 }, + { x: 1, y: 0, z: 0 }, + ], + } + const connMap = { + getNetConnectedToId: () => undefined, + } as unknown as ConnectivityMap + const rerouter = new Pipeline9IjumpRerouter({ + srj, + baseObstacles: [], + connMap, + }) + + const result = rerouter.tryReroute([route], { + routeIndex: 0, + includeCandidateCopper: false, + reverse: false, + shortenPath: false, + maxIterations: 17, + }) + + expect(result).toEqual({ iterations: 1 }) +}) diff --git a/tests/features/pipeline9-lock-terminal-layers.test.ts b/tests/features/pipeline9-lock-terminal-layers.test.ts new file mode 100644 index 000000000..b8eb34335 --- /dev/null +++ b/tests/features/pipeline9-lock-terminal-layers.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from "bun:test" +import { lockPipeline9TerminalLayers } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/lock-pipeline9-terminal-layers" +import type { HighDensityRoute } from "lib/types/high-density-types" + +test("Pipeline9 restores terminal layers after simplification", () => { + const route: HighDensityRoute = { + connectionName: "conn", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: 0, y: 0, z: 1, pcb_port_id: "start" }, + { x: 1, y: 0, z: 1 }, + { x: 2, y: 0, z: 1, pcb_port_id: "end" }, + ], + } + + const [locked] = lockPipeline9TerminalLayers( + [route], + [ + { + name: "conn", + pointsToConnect: [ + { + x: 0, + y: 0, + layer: "top", + pointId: "start", + pcb_port_id: "start", + }, + { + x: 2, + y: 0, + layer: "top", + pointId: "end", + pcb_port_id: "end", + }, + ], + }, + ], + 2, + ) + + expect(locked?.route).toEqual([ + { x: 0, y: 0, z: 0, pcb_port_id: "start" }, + { x: 0, y: 0, z: 1 }, + { x: 1, y: 0, z: 1 }, + { x: 2, y: 0, z: 1 }, + { x: 2, y: 0, z: 0, pcb_port_id: "end" }, + ]) + expect(locked?.vias).toEqual([ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ]) +}) diff --git a/tests/features/pipeline9-post-final-composite-repair.test.ts b/tests/features/pipeline9-post-final-composite-repair.test.ts new file mode 100644 index 000000000..b1edd864f --- /dev/null +++ b/tests/features/pipeline9-post-final-composite-repair.test.ts @@ -0,0 +1,283 @@ +import { expect, test } from "bun:test" +import { ConnectivityMap } from "circuit-json-to-connectivity-map" +import type { + DrcEvaluator, + HighDensityRoute, + SimpleRouteJson, +} from "high-density-repair03/lib" +import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" +import type { + Pipeline9IjumpRerouteOptions, + Pipeline9IjumpRerouteResult, +} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" + +const makeSolver = ({ + srj, + hdRoutes, + drcEvaluator, + connMap, +}: { + srj: SimpleRouteJson + hdRoutes: HighDensityRoute[] + drcEvaluator: DrcEvaluator + connMap?: ConnectivityMap +}) => + new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator, + connMap, + originalObstacles: [], + ijumpBaseObstacles: [], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + +const makeTraceError = (traceId: string, center: { x: number; y: number }) => ({ + type: "pcb_trace_error", + error_type: "pcb_trace_error", + pcb_trace_id: traceId, + pcb_trace_error_id: `overlap_${traceId}_fixed_trace`, + candidate_pcb_trace_ids: [traceId], + center, +}) + +const getViaPosition = (route: HighDensityRoute) => { + for ( + let pointIndex = 0; + pointIndex < route.route.length - 1; + pointIndex += 1 + ) { + const point = route.route[pointIndex]! + const nextPoint = route.route[pointIndex + 1]! + if (point.z !== nextPoint.z) return { x: point.x, y: point.y } + } + throw new Error(`Route "${route.connectionName}" has no via transition`) +} + +test("Pipeline9 fairly schedules both directions for a terminal-rooted window", () => { + const srj: SimpleRouteJson = { + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + obstacles: [], + connections: [ + { + name: "A", + pointsToConnect: [ + { x: -1, y: 0, layer: "top", pointId: "A_start" }, + { x: 1, y: 0, layer: "bottom", pointId: "A_end" }, + ], + }, + ], + } + const hdRoutes: HighDensityRoute[] = [ + { + connectionName: "A", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [{ x: 0, y: 0 }], + route: [ + { x: -1, y: 0, z: 0, pcb_port_id: "A_start" }, + { x: -0.8, y: 0, z: 0 }, + { x: -0.6, y: 0, z: 0 }, + { x: -0.4, y: 0, z: 0 }, + { x: -0.2, y: 0, z: 0 }, + { x: 0, y: 0, z: 0 }, + { x: 0, y: 0, z: 1 }, + { x: 0.2, y: 0, z: 1 }, + { x: 0.6, y: 0, z: 1 }, + { x: 1, y: 0, z: 1, pcb_port_id: "A_end" }, + ], + }, + ] + const error = makeTraceError("A_0", { x: -0.5, y: 0 }) + const drcEvaluator: DrcEvaluator = ({ routes }) => + routes?.[0]?.rootConnectionName === "repaired" + ? [] + : { errors: [error], errorsWithCenters: [error] } + const solver = makeSolver({ srj, hdRoutes, drcEvaluator }) + const attempts: Pipeline9IjumpRerouteOptions[] = [] + const stubRerouter = { + tryReroute: ( + routes: HighDensityRoute[], + options: Pipeline9IjumpRerouteOptions, + ): Pipeline9IjumpRerouteResult => { + attempts.push({ ...options }) + if (!options.reverse) return { iterations: 100 } + return { + route: { + ...routes[options.routeIndex]!, + rootConnectionName: "repaired", + }, + iterations: 100, + } + }, + } + const privateSolver = solver as unknown as { + ijumpRerouter: typeof stubRerouter + runPostFinalCompositeRepair: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + getSnapshot: (routes: HighDensityRoute[]) => { count: number } + postFinalCompositeForwardAttempts: number + postFinalCompositeReverseAttempts: number + postFinalCompositeTerminalRootedAttempts: number + postFinalCompositeCandidatesAccepted: number + } + privateSolver.ijumpRerouter = stubRerouter + + const output = privateSolver.runPostFinalCompositeRepair(hdRoutes) + + expect(privateSolver.getSnapshot(output).count).toBe(0) + expect( + attempts.slice(0, 2).map(({ startIndex, endIndex, reverse }) => ({ + startIndex, + endIndex, + reverse, + })), + ).toEqual([ + { startIndex: 0, endIndex: 7, reverse: false }, + { startIndex: 0, endIndex: 7, reverse: true }, + ]) + expect(privateSolver.postFinalCompositeForwardAttempts).toBe(1) + expect(privateSolver.postFinalCompositeReverseAttempts).toBe(1) + expect(privateSolver.postFinalCompositeTerminalRootedAttempts).toBe(2) + expect(privateSolver.postFinalCompositeCandidatesAccepted).toBe(1) +}) + +test("Pipeline9 atomically merges only the rerouted owner's canonical net", () => { + const connectionYs = [-0.75, -0.25, 0.25, 0.75] + const names = ["A", "A2", "B", "B2"] + const srj: SimpleRouteJson = { + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + obstacles: [], + connections: names.map((name, index) => ({ + name, + pointsToConnect: [ + { + x: -1, + y: connectionYs[index]!, + layer: "top", + pointId: `${name}_start`, + }, + { + x: 1, + y: connectionYs[index]!, + layer: "bottom", + pointId: `${name}_end`, + }, + ], + })), + } + const makeViaRoute = ( + connectionName: string, + terminalY: number, + via: { x: number; y: number }, + ): HighDensityRoute => ({ + connectionName, + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [{ ...via }], + route: [ + { + x: -1, + y: terminalY, + z: 0, + pcb_port_id: `${connectionName}_start`, + }, + { x: via.x, y: via.y, z: 0 }, + { x: via.x, y: via.y, z: 1 }, + { + x: 1, + y: terminalY, + z: 1, + pcb_port_id: `${connectionName}_end`, + }, + ], + }) + const hdRoutes: HighDensityRoute[] = [ + makeViaRoute("A", connectionYs[0]!, { x: 0, y: 0 }), + makeViaRoute("A2", connectionYs[1]!, { x: 0.18, y: 0 }), + makeViaRoute("B", connectionYs[2]!, { x: 0, y: 1 }), + makeViaRoute("B2", connectionYs[3]!, { x: 0.18, y: 1 }), + ] + const traceError = makeTraceError("A_0", { x: -0.5, y: -0.375 }) + const drcEvaluator: DrcEvaluator = ({ routes }) => { + const routeA = routes?.[0] + const routeA2 = routes?.[1] + if (!routeA || !routeA2) return [] + if (routeA.rootConnectionName !== "rerouted") { + return { errors: [traceError], errorsWithCenters: [traceError] } + } + + const viaA = getViaPosition(routeA) + const viaA2 = getViaPosition(routeA2) + if (Math.hypot(viaA.x - viaA2.x, viaA.y - viaA2.y) <= 1e-6) { + return [] + } + const viaError = { + type: "pcb_via_clearance_error", + error_type: "pcb_via_clearance_error", + message: "The raw reroute replaced one error with a same-net via error", + center: { + x: (viaA.x + viaA2.x) / 2, + y: (viaA.y + viaA2.y) / 2, + }, + } + return { errors: [viaError], errorsWithCenters: [viaError] } + } + const solver = makeSolver({ + srj, + hdRoutes, + drcEvaluator, + connMap: new ConnectivityMap({ + target_net: ["A", "A2"], + unrelated_net: ["B", "B2"], + }), + }) + const stubRerouter = { + tryReroute: ( + routes: HighDensityRoute[], + options: Pipeline9IjumpRerouteOptions, + ): Pipeline9IjumpRerouteResult => ({ + route: { + ...routes[options.routeIndex]!, + rootConnectionName: "rerouted", + }, + iterations: 50, + }), + } + const privateSolver = solver as unknown as { + ijumpRerouter: typeof stubRerouter + runPostFinalCompositeRepair: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + getSnapshot: (routes: HighDensityRoute[]) => { count: number } + postFinalCompositeDrcEvaluations: number + postFinalCompositeCandidatesAccepted: number + postFinalCompositeSameNetViaMergeIterations: number + } + privateSolver.ijumpRerouter = stubRerouter + + expect(privateSolver.getSnapshot(hdRoutes).count).toBe(1) + const output = privateSolver.runPostFinalCompositeRepair(hdRoutes) + + expect(privateSolver.getSnapshot(output).count).toBe(0) + expect(getViaPosition(output[0]!)).toEqual(getViaPosition(output[1]!)) + expect(output[2]).toEqual(hdRoutes[2]) + expect(output[3]).toEqual(hdRoutes[3]) + expect(privateSolver.postFinalCompositeDrcEvaluations).toBe(2) + expect(privateSolver.postFinalCompositeCandidatesAccepted).toBe(1) + expect( + privateSolver.postFinalCompositeSameNetViaMergeIterations, + ).toBeGreaterThan(0) +}) diff --git a/tests/features/pipeline9-post-repair-same-net-via-merge.test.ts b/tests/features/pipeline9-post-repair-same-net-via-merge.test.ts new file mode 100644 index 000000000..066a3c87e --- /dev/null +++ b/tests/features/pipeline9-post-repair-same-net-via-merge.test.ts @@ -0,0 +1,156 @@ +import { expect, test } from "bun:test" +import { ConnectivityMap } from "circuit-json-to-connectivity-map" +import type { + DrcEvaluator, + HighDensityRoute, + SimpleRouteJson, +} from "high-density-repair03/lib" +import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" + +const getViaPosition = (route: HighDensityRoute) => { + for ( + let pointIndex = 0; + pointIndex < route.route.length - 1; + pointIndex += 1 + ) { + const point = route.route[pointIndex]! + const nextPoint = route.route[pointIndex + 1]! + if (point.z !== nextPoint.z) return { x: point.x, y: point.y } + } + throw new Error(`Route "${route.connectionName}" has no via transition`) +} + +test("Pipeline9 coalesces post-repair same-net vias with stale via metadata", () => { + const srj: SimpleRouteJson = { + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + obstacles: [], + connections: [ + { + name: "A", + pointsToConnect: [ + { x: -1, y: -0.5, layer: "top", pointId: "a_start" }, + { x: 1, y: -0.5, layer: "bottom", pointId: "a_end" }, + ], + }, + { + name: "B", + pointsToConnect: [ + { x: -1, y: 0.5, layer: "top", pointId: "b_start" }, + { x: 1, y: 0.5, layer: "bottom", pointId: "b_end" }, + ], + }, + ], + } + const hdRoutes: HighDensityRoute[] = [ + { + connectionName: "A", + traceThickness: 0.1, + viaDiameter: 0.3, + // Deliberately rounded metadata: the exact transition is at x=0.0005. + vias: [{ x: 0, y: 0 }], + route: [ + { x: -1, y: -0.5, z: 0, pcb_port_id: "a_start" }, + { x: 0.0005, y: 0, z: 0 }, + { x: 0.0005, y: 0, z: 1 }, + { x: 1, y: -0.5, z: 1, pcb_port_id: "a_end" }, + ], + }, + { + connectionName: "B", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [{ x: 0.18, y: 0 }], + route: [ + { x: -1, y: 0.5, z: 0, pcb_port_id: "b_start" }, + { x: 0.18, y: 0, z: 0 }, + { x: 0.18, y: 0, z: 1 }, + { x: 1, y: 0.5, z: 1, pcb_port_id: "b_end" }, + ], + }, + ] + const drcEvaluator: DrcEvaluator = ({ routes }) => { + const routeA = routes?.[0] + const routeB = routes?.[1] + if (!routeA || !routeB) return [] + const viaA = getViaPosition(routeA) + const viaB = getViaPosition(routeB) + const distance = Math.hypot(viaA.x - viaB.x, viaA.y - viaB.y) + if (distance <= 1e-6 || distance >= 0.3) return [] + const error = { + type: "pcb_via_clearance_error", + error_type: "pcb_via_clearance_error", + message: "Same-net vias are too close", + pcb_via_ids: ["via_a", "via_b"], + center: { + x: (viaA.x + viaB.x) / 2, + y: (viaA.y + viaB.y) / 2, + }, + } + return { errors: [error], errorsWithCenters: [error] } + } + const solver = new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator, + connMap: new ConnectivityMap({ same_net: ["A", "B"] }), + originalObstacles: [], + ijumpBaseObstacles: [], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + const privateSolver = solver as unknown as { + runPostRepairSameNetViaMerge: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + getSnapshot: (routes: HighDensityRoute[]) => { count: number } + postRepairSameNetViaMergeAttempts: number + postRepairSameNetViaMergeDrcEvaluations: number + postRepairSameNetViaMergeCandidatesAccepted: number + postRepairSameNetViaMergeIterations: number + } + + expect(privateSolver.getSnapshot(hdRoutes).count).toBe(1) + const output = privateSolver.runPostRepairSameNetViaMerge(hdRoutes) + + expect(privateSolver.getSnapshot(output).count).toBe(0) + expect(getViaPosition(output[0]!)).toEqual(getViaPosition(output[1]!)) + expect(output[0]?.route[0]).toEqual(hdRoutes[0]?.route[0]) + expect(output[0]?.route.at(-1)).toEqual(hdRoutes[0]?.route.at(-1)) + expect(output[1]?.route[0]).toEqual(hdRoutes[1]?.route[0]) + expect(output[1]?.route.at(-1)).toEqual(hdRoutes[1]?.route.at(-1)) + expect(privateSolver.postRepairSameNetViaMergeAttempts).toBe(1) + expect(privateSolver.postRepairSameNetViaMergeDrcEvaluations).toBe(1) + expect(privateSolver.postRepairSameNetViaMergeCandidatesAccepted).toBe(1) + expect(privateSolver.postRepairSameNetViaMergeIterations).toBeLessThanOrEqual( + 8, + ) + + const incompleteConnectivitySolver = new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator, + connMap: new ConnectivityMap({ incomplete_net: ["A"] }), + originalObstacles: [], + ijumpBaseObstacles: [], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) as unknown as { + runPostRepairSameNetViaMerge: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + } + expect( + incompleteConnectivitySolver.runPostRepairSameNetViaMerge(hdRoutes), + ).toBe(hdRoutes) +}) diff --git a/tests/features/pipeline9-preloaded-trace-graph.test.ts b/tests/features/pipeline9-preloaded-trace-graph.test.ts index 41dde266e..b05cf3068 100644 --- a/tests/features/pipeline9-preloaded-trace-graph.test.ts +++ b/tests/features/pipeline9-preloaded-trace-graph.test.ts @@ -33,7 +33,7 @@ test("Pipeline9 projects preloaded copper into hypergraph regions without topolo }) expect( solver.capacityNodes?.some((node) => - node._connectedTo?.includes(preloadedTrace.connection_name), + node._preloadedFixedNetIds?.includes(srj.connections[0]!.name), ), ).toBe(true) expect(solver.getOutputSimplifiedPcbTraces()).toHaveLength(1) diff --git a/tests/features/pipeline9-shared-terminal-composite-repair.test.ts b/tests/features/pipeline9-shared-terminal-composite-repair.test.ts new file mode 100644 index 000000000..0def5807a --- /dev/null +++ b/tests/features/pipeline9-shared-terminal-composite-repair.test.ts @@ -0,0 +1,232 @@ +import { expect, test } from "bun:test" +import { ConnectivityMap } from "circuit-json-to-connectivity-map" +import type { + DrcEvaluator, + HighDensityRoute, + SimpleRouteJson, +} from "high-density-repair03/lib" +import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" +import type { + Pipeline9IjumpRerouteOptions, + Pipeline9IjumpRerouteResult, +} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" +import type { Obstacle } from "lib/types" + +const srj: SimpleRouteJson = { + bounds: { minX: -1, minY: -1, maxX: 3, maxY: 2 }, + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + obstacles: [], + connections: [ + { + name: "branchA", + pointsToConnect: [ + { x: 0, y: 0, layer: "top", pointId: "shared_port" }, + { x: 2, y: 0, layer: "bottom", pointId: "branch_a_end" }, + ], + }, + { + name: "branchB", + pointsToConnect: [ + { x: 2, y: 1, layer: "bottom", pointId: "branch_b_start" }, + { x: 0, y: 0, layer: "top", pointId: "shared_port" }, + ], + }, + { + name: "blocker", + pointsToConnect: [ + { x: -0.5, y: 0.5, layer: "top", pointId: "blocker_start" }, + { x: 2.5, y: 0.5, layer: "top", pointId: "blocker_end" }, + ], + }, + ], +} + +const sharedPad: Obstacle = { + type: "rect", + layers: ["top"], + center: { x: 0, y: 0 }, + width: 1, + height: 1, + connectedTo: ["pcb_smtpad_shared", "shared_port", "branchA", "branchB"], +} + +const hdRoutes: HighDensityRoute[] = [ + { + connectionName: "branchA", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [{ x: 0.8, y: 0 }], + route: [ + { x: 0, y: 0, z: 0, pcb_port_id: "shared_port" }, + { x: 0.8, y: 0, z: 0 }, + { x: 0.8, y: 0, z: 1 }, + { x: 2, y: 0, z: 1, pcb_port_id: "branch_a_end" }, + ], + }, + { + connectionName: "branchB", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [{ x: 0.8, y: 1 }], + route: [ + { x: 2, y: 1, z: 1, pcb_port_id: "branch_b_start" }, + { x: 0.8, y: 1, z: 1 }, + { x: 0.8, y: 1, z: 0 }, + { x: 0, y: 0, z: 0, pcb_port_id: "shared_port" }, + ], + }, + { + connectionName: "blocker", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: -0.5, y: 0.5, z: 0, pcb_port_id: "blocker_start" }, + { x: 2.5, y: 0.5, z: 0, pcb_port_id: "blocker_end" }, + ], + }, +] + +const makeTraceError = ( + primaryTraceId: string, + otherTraceId: string, + candidateTraceId: string, +) => ({ + type: "pcb_trace_error", + error_type: "pcb_trace_error", + pcb_trace_id: primaryTraceId, + pcb_trace_error_id: `overlap_${primaryTraceId}_${otherTraceId}`, + candidate_pcb_trace_ids: [candidateTraceId], + center: { x: 0.25, y: 0.25 }, +}) + +const transitionIsAtSharedTerminal = ( + route: HighDensityRoute | undefined, +): boolean => + Boolean( + route?.route.some( + (point, pointIndex) => + point.x === 0 && + point.y === 0 && + route.route[pointIndex + 1]?.x === 0 && + route.route[pointIndex + 1]?.y === 0 && + route.route[pointIndex + 1]?.z !== point.z, + ), + ) + +const drcEvaluator: DrcEvaluator = ({ routes }) => { + const branchARelocated = transitionIsAtSharedTerminal(routes?.[0]) + const branchBRelocated = transitionIsAtSharedTerminal(routes?.[1]) + if (!branchARelocated || !branchBRelocated) { + const errors = [ + makeTraceError("preloaded_fixed_0", "branchA_0", "branchA_0"), + makeTraceError("preloaded_fixed_0", "branchB_0", "branchB_0"), + ] + return { errors, errorsWithCenters: errors } + } + if (routes?.[2]?.rootConnectionName === "repaired") return [] + + const errors = [ + makeTraceError("blocker_0", "branchA_0", "blocker_0"), + makeTraceError("blocker_0", "branchB_0", "blocker_0"), + ] + return { errors, errorsWithCenters: errors } +} + +const makeSolver = () => + new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator, + connMap: new ConnectivityMap({ + branch_net: ["branchA", "branchB"], + blocker_net: ["blocker"], + }), + originalObstacles: [sharedPad], + ijumpBaseObstacles: [sharedPad], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + +test("Pipeline9 atomically relocates shared-terminal vias and reroutes the exposed owner", () => { + const solver = makeSolver() + const attempts: Pipeline9IjumpRerouteOptions[] = [] + const stubRerouter = { + tryReroute: ( + routes: HighDensityRoute[], + options: Pipeline9IjumpRerouteOptions, + ): Pipeline9IjumpRerouteResult => { + attempts.push({ ...options }) + return { + route: { + ...routes[options.routeIndex]!, + rootConnectionName: "repaired", + }, + iterations: 2_181, + } + }, + } + const privateSolver = solver as unknown as { + ijumpRerouter: typeof stubRerouter + runSharedTerminalCompositeRepair: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + getSnapshot: (routes: HighDensityRoute[]) => { count: number } + sharedTerminalCompositeAttempts: number + sharedTerminalCompositeRelocatedBranches: number + sharedTerminalCompositeIjumpAttempts: number + sharedTerminalCompositeDrcEvaluations: number + sharedTerminalCompositeCandidatesAccepted: number + sharedTerminalCompositeIterations: number + } + privateSolver.ijumpRerouter = stubRerouter + + const output = privateSolver.runSharedTerminalCompositeRepair(hdRoutes) + + expect(privateSolver.getSnapshot(output).count).toBe(0) + expect(transitionIsAtSharedTerminal(output[0])).toBe(true) + expect(transitionIsAtSharedTerminal(output[1])).toBe(true) + expect(attempts).toEqual([ + { + routeIndex: 2, + includeCandidateCopper: true, + reverse: false, + shortenPath: false, + maxIterations: 12_500, + }, + ]) + expect(privateSolver.sharedTerminalCompositeAttempts).toBe(1) + expect(privateSolver.sharedTerminalCompositeRelocatedBranches).toBe(2) + expect(privateSolver.sharedTerminalCompositeIjumpAttempts).toBe(1) + expect(privateSolver.sharedTerminalCompositeDrcEvaluations).toBe(2) + expect(privateSolver.sharedTerminalCompositeCandidatesAccepted).toBe(1) + expect(privateSolver.sharedTerminalCompositeIterations).toBe(2_181) +}) + +test("Pipeline9 rolls back a shared-terminal composite that exhausts its hard budget", () => { + const solver = makeSolver() + const stubRerouter = { + tryReroute: (): Pipeline9IjumpRerouteResult => ({ iterations: 12_500 }), + } + const privateSolver = solver as unknown as { + ijumpRerouter: typeof stubRerouter + runSharedTerminalCompositeRepair: ( + routes: HighDensityRoute[], + ) => HighDensityRoute[] + sharedTerminalCompositeCandidatesAccepted: number + sharedTerminalCompositeIterations: number + } + privateSolver.ijumpRerouter = stubRerouter + + const output = privateSolver.runSharedTerminalCompositeRepair(hdRoutes) + + expect(output).toBe(hdRoutes) + expect(privateSolver.sharedTerminalCompositeCandidatesAccepted).toBe(0) + expect(privateSolver.sharedTerminalCompositeIterations).toBe(12_500) +}) diff --git a/tests/features/pipeline9-srj23-relaxed-drc.test.ts b/tests/features/pipeline9-srj23-relaxed-drc.test.ts index b8cad2b35..617e20beb 100644 --- a/tests/features/pipeline9-srj23-relaxed-drc.test.ts +++ b/tests/features/pipeline9-srj23-relaxed-drc.test.ts @@ -8,7 +8,7 @@ test( async () => { const failures: Array<{ sampleNumber: number; errors: unknown[] }> = [] - for (const sampleNumber of [14, 21, 32, 55, 58, 62, 64, 107]) { + for (const sampleNumber of [8, 14, 21, 32, 55, 58, 62, 64, 107]) { const { scenario } = await loadScenarioBySampleNumber( "srj23", sampleNumber, @@ -34,5 +34,5 @@ test( expect(failures).toEqual([]) }, - { timeout: 120_000 }, + { timeout: 300_000 }, ) diff --git a/tests/features/pipeline9-srj23-sample100-preloaded-graph.test.ts b/tests/features/pipeline9-srj23-sample100-preloaded-graph.test.ts new file mode 100644 index 000000000..27c02467b --- /dev/null +++ b/tests/features/pipeline9-srj23-sample100-preloaded-graph.test.ts @@ -0,0 +1,38 @@ +import { expect, test } from "bun:test" +import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib" +import { evaluateRelaxedDrc } from "lib/testing/evaluate-relaxed-drc" +import { loadScenarioBySampleNumber } from "../../scripts/benchmark/scenarios" + +test( + "Pipeline9 completes srj23 sample 100 with the bounded preloaded graph", + async () => { + const { scenario } = await loadScenarioBySampleNumber("srj23", 100) + const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph( + scenario, + { + cacheProvider: null, + effort: 1, + }, + ) + + solver.solve() + + expect(solver.failed).toBe(false) + expect(solver.solved).toBe(true) + expect(solver.preloadedTraceGraphSolver?.stats).toMatchObject({ + refinementBudgetExhausted: true, + effectiveMaxOutputNodeCount: 3_000, + }) + expect( + solver.preloadedTraceGraphSolver?.stats.outputNodeCount, + ).toBeLessThan(3_000) + expect( + evaluateRelaxedDrc({ + inputSrj: scenario, + srjWithPointPairs: solver.srjWithPointPairs!, + traces: solver.getOutputSimplifiedPcbTraces(), + }).errors, + ).toEqual([]) + }, + { timeout: 30_000 }, +) diff --git a/tests/features/pipeline9-srj23-sample32-preloaded-graph.test.ts b/tests/features/pipeline9-srj23-sample32-preloaded-graph.test.ts new file mode 100644 index 000000000..12eac5fd1 --- /dev/null +++ b/tests/features/pipeline9-srj23-sample32-preloaded-graph.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from "bun:test" +import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib" +import { loadScenarioBySampleNumber } from "../../scripts/benchmark/scenarios" + +test( + "Pipeline9 gives dense srj23 inputs a baseline-relative refinement budget", + async () => { + const { scenario } = await loadScenarioBySampleNumber("srj23", 32) + const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph( + scenario, + { + cacheProvider: null, + effort: 1, + }, + ) + + while (!solver.failed && !solver.portPointPathingSolver?.solved) { + solver.step() + } + + expect(solver.failed).toBe(false) + expect(solver.portPointPathingSolver?.solved).toBe(true) + const stats = solver.preloadedTraceGraphSolver?.stats + if (!stats) { + throw new Error("Expected Pipeline9 preloaded graph stats") + } + expect(stats.minimumRefinementWorstCaseAllowance).toBe(2_050) + expect(stats.effectiveMaxOutputNodeCount).toBe( + Math.max( + 3_000, + stats.minimumLayerSplitNodeCount + + stats.minimumRefinementWorstCaseAllowance, + ), + ) + expect(stats.outputNodeCount).toBeLessThan( + stats.effectiveMaxOutputNodeCount, + ) + }, + { timeout: 15_000 }, +) diff --git a/tests/features/pipeline9-terminal-via-escape.test.ts b/tests/features/pipeline9-terminal-via-escape.test.ts new file mode 100644 index 000000000..7d35de536 --- /dev/null +++ b/tests/features/pipeline9-terminal-via-escape.test.ts @@ -0,0 +1,172 @@ +import { expect, test } from "bun:test" +import { + Pipeline9IjumpRerouter, + type Pipeline9TerminalViaEscapeCandidate, +} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" +import type { Obstacle, SimpleRouteJson } from "lib/types" +import type { HighDensityRoute } from "lib/types/high-density-types" + +const startPad: Obstacle = { + type: "rect", + layers: ["top"], + center: { x: -2, y: 0 }, + width: 0.8, + height: 0.28, + connectedTo: ["pcb_smtpad_start", "pcb_port_start", "test_net"], +} + +const endPad: Obstacle = { + type: "rect", + layers: ["top"], + center: { x: 2, y: 0 }, + width: 0.8, + height: 0.28, + connectedTo: ["pcb_smtpad_end", "pcb_port_end", "test_net"], +} + +const blockingTopCopper: Obstacle = { + type: "rect", + layers: ["top"], + center: { x: 0, y: 0 }, + width: 2, + height: 0.5, + connectedTo: ["fixed_net"], +} + +const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + bounds: { minX: -3, minY: -2, maxX: 3, maxY: 2 }, + obstacles: [startPad, endPad, blockingTopCopper], + connections: [], +} + +const route: HighDensityRoute = { + connectionName: "test_net", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: -2, y: 0, z: 0, pcb_port_id: "pcb_port_start" }, + { x: 2, y: 0, z: 0, pcb_port_id: "pcb_port_end" }, + ], +} + +const getLayerTransitions = (candidateRoute: HighDensityRoute) => + candidateRoute.route.flatMap((point, pointIndex) => { + const nextPoint = candidateRoute.route[pointIndex + 1] + return nextPoint && nextPoint.z !== point.z + ? [{ x: point.x, y: point.y, fromZ: point.z, toZ: nextPoint.z }] + : [] + }) + +test("Pipeline9 escapes a blocked terminal pad with bounded generated vias", () => { + const rerouter = new Pipeline9IjumpRerouter({ + srj, + baseObstacles: srj.obstacles, + }) + const routes = [route] + const candidates = rerouter.getTerminalViaEscapeCandidates(routes, 0) + + expect(candidates.length).toBeGreaterThan(0) + expect(candidates.length).toBeLessThanOrEqual(64) + expect( + candidates.every( + (candidate) => + candidate.startVia.x >= srj.bounds.minX + route.viaDiameter / 2 && + candidate.startVia.x <= srj.bounds.maxX - route.viaDiameter / 2 && + candidate.endVia.x >= srj.bounds.minX + route.viaDiameter / 2 && + candidate.endVia.x <= srj.bounds.maxX - route.viaDiameter / 2, + ), + ).toBe(true) + + const endViaXs = candidates.map((candidate) => candidate.endVia.x) + expect(endViaXs).toContain(1.75) + expect(endViaXs).toContain(2.25) + + const escapeCandidate = candidates.find( + (candidate) => + candidate.alternateZ === 1 && + candidate.startVia.x === route.route[0]!.x && + candidate.startVia.y === route.route[0]!.y && + candidate.endVia.x === 1.75 && + candidate.endVia.y === 0, + ) + expect(escapeCandidate).toBeDefined() + if (!escapeCandidate) throw new Error("Expected a generated pad escape") + + const result = rerouter.tryRerouteWithTerminalViaEscape(routes, { + routeIndex: 0, + candidate: escapeCandidate, + includeCandidateCopper: false, + reverse: true, + shortenPath: false, + maxIterations: 50_000, + }) + + expect(result?.route).toBeDefined() + if (!result?.route) throw new Error("Expected terminal via escape to route") + expect(result.route.route[0]).toEqual(route.route[0]) + expect(result.route.route.at(-1)).toEqual(route.route.at(-1)) + expect(getLayerTransitions(result.route)).toEqual( + expect.arrayContaining([ + { x: -2, y: 0, fromZ: 0, toZ: 1 }, + { x: 1.75, y: 0, fromZ: 1, toZ: 0 }, + ]), + ) + expect( + result.route.route.some((point, pointIndex) => { + const nextPoint = result.route?.route[pointIndex + 1] + return ( + nextPoint?.z === 1 && + point.z === 1 && + Math.min(point.x, nextPoint.x) < -1 && + Math.max(point.x, nextPoint.x) > 1 + ) + }), + ).toBe(true) +}) + +test("Pipeline9 rejects a terminal escape access point outside its pad", () => { + const rerouter = new Pipeline9IjumpRerouter({ + srj, + baseObstacles: srj.obstacles, + }) + const invalidCandidate: Pipeline9TerminalViaEscapeCandidate = { + alternateZ: 1, + startVia: { x: -2, y: 0 }, + endVia: { x: 0, y: 0 }, + } + + expect( + rerouter.tryRerouteWithTerminalViaEscape([route], { + routeIndex: 0, + candidate: invalidCandidate, + includeCandidateCopper: false, + reverse: true, + shortenPath: false, + maxIterations: 50_000, + }), + ).toBeUndefined() +}) + +test("Pipeline9 interleaves terminal escape layers within the bounded prefix", () => { + const multilayerSrj: SimpleRouteJson = { + ...srj, + layerCount: 4, + } + const rerouter = new Pipeline9IjumpRerouter({ + srj: multilayerSrj, + baseObstacles: multilayerSrj.obstacles, + }) + + const candidates = rerouter.getTerminalViaEscapeCandidates([route], 0) + + expect(candidates.slice(0, 3).map(({ alternateZ }) => alternateZ)).toEqual([ + 1, 2, 3, + ]) + expect( + new Set(candidates.slice(0, 4).map(({ alternateZ }) => alternateZ)), + ).toEqual(new Set([1, 2, 3])) +}) diff --git a/tests/features/pipeline9-via-micro-shift.test.ts b/tests/features/pipeline9-via-micro-shift.test.ts new file mode 100644 index 000000000..dd3879cc3 --- /dev/null +++ b/tests/features/pipeline9-via-micro-shift.test.ts @@ -0,0 +1,141 @@ +import { expect, test } from "bun:test" +import type { + DrcEvaluator, + HighDensityRoute, + SimpleRouteJson, +} from "high-density-repair03/lib" +import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" + +const getViaPosition = (route: HighDensityRoute) => { + for (let pointIndex = 0; pointIndex < route.route.length - 1; pointIndex++) { + const point = route.route[pointIndex]! + const nextPoint = route.route[pointIndex + 1]! + if (point.z !== nextPoint.z) return { x: point.x, y: point.y } + } + throw new Error(`Route "${route.connectionName}" has no via transition`) +} + +test("Pipeline9 micro-shifts a candidate via when strict DRC improves", () => { + const srj: SimpleRouteJson = { + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + obstacles: [], + connections: [ + { + name: "A", + pointsToConnect: [ + { x: -1, y: 0, layer: "top", pointId: "a_start" }, + { x: 1, y: 0, layer: "bottom", pointId: "a_end" }, + ], + }, + { + name: "B", + pointsToConnect: [ + { x: -1, y: 0.18, layer: "top", pointId: "b_start" }, + { x: 1, y: 0.18, layer: "bottom", pointId: "b_end" }, + ], + }, + ], + } + const hdRoutes: HighDensityRoute[] = [ + { + connectionName: "A", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [{ x: 0, y: 0 }], + route: [ + { x: -1, y: 0, z: 0, pcb_port_id: "a_start" }, + { x: 0, y: 0, z: 0 }, + { x: 0, y: 0, z: 1 }, + { x: 1, y: 0, z: 1, pcb_port_id: "a_end" }, + ], + }, + { + connectionName: "B", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [{ x: 0, y: 0.18 }], + route: [ + { x: -1, y: 0.18, z: 0, pcb_port_id: "b_start" }, + { x: 0, y: 0.18, z: 0 }, + { x: 0, y: 0.18, z: 1 }, + { x: 1, y: 0.18, z: 1, pcb_port_id: "b_end" }, + ], + }, + ] + const drcEvaluator: DrcEvaluator = ({ routes }) => { + const routeA = routes?.[0] + const routeB = routes?.[1] + if (!routeA || !routeB) return [] + const viaA = getViaPosition(routeA) + const viaB = getViaPosition(routeB) + if (Math.hypot(viaA.x - viaB.x, viaA.y - viaB.y) >= 0.3) { + return [] + } + const error = { + type: "pcb_via_clearance_error", + error_type: "pcb_via_clearance_error", + message: "Candidate vias are too close", + pcb_via_ids: ["via_a", "via_b"], + candidate_pcb_trace_ids: ["A_0", "B_0"], + center: { + x: (viaA.x + viaB.x) / 2, + y: (viaA.y + viaB.y) / 2, + }, + } + return { errors: [error], errorsWithCenters: [error] } + } + const solver = new Pipeline9ExactDrcRepairSolver({ + srj, + hdRoutes, + drcEvaluator, + originalObstacles: [], + ijumpBaseObstacles: [], + viaHoleDiameter: 0.15, + maxIterations: 1, + enableLargeBoardBroadFallback: false, + enablePostSolveClearanceRelaxation: false, + broadMaxIterations: 1, + broadPassMultiplier: 1, + }) + + const output = ( + solver as unknown as { + tryViaMicroShift: ( + routes: HighDensityRoute[], + error: Record, + ) => HighDensityRoute[] | undefined + } + ).tryViaMicroShift(hdRoutes, { + type: "pcb_via_clearance_error", + error_type: "pcb_via_clearance_error", + message: "Candidate vias are too close", + pcb_via_ids: ["via_a", "via_b"], + candidate_pcb_trace_ids: ["A_0", "B_0"], + center: { x: 0, y: 0.09 }, + }) + + expect(output).toBeDefined() + if (!output) throw new Error("Expected the via micro-shift to improve DRC") + expect(drcEvaluator({ traces: [], routes: output })).toEqual([]) + expect(output[0]?.route[0]).toMatchObject({ + x: -1, + y: 0, + z: 0, + pcb_port_id: "a_start", + }) + expect(output[0]?.route.at(-1)).toMatchObject({ + x: 1, + y: 0, + z: 1, + pcb_port_id: "a_end", + }) + expect( + Math.hypot( + getViaPosition(output[0]!).x - getViaPosition(output[1]!).x, + getViaPosition(output[0]!).y - getViaPosition(output[1]!).y, + ), + ).toBeGreaterThanOrEqual(0.3) +}) diff --git a/tests/features/preloaded-fixed-copper-cramped-port.test.ts b/tests/features/preloaded-fixed-copper-cramped-port.test.ts new file mode 100644 index 000000000..371e8c467 --- /dev/null +++ b/tests/features/preloaded-fixed-copper-cramped-port.test.ts @@ -0,0 +1,102 @@ +import { expect, test } from "bun:test" +import type { + SegmentPortPoint, + SharedEdgeSegment, +} from "lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver" +import { MultiTargetNecessaryCrampedPortPointSolver } from "lib/solvers/NecessaryCrampedPortPointSolver/MultiTargetNecessaryCrampedPortPointSolver" +import type { CapacityMeshNode, SimpleRouteJson } from "lib/types" + +const createCrampedPort = ( + id: string, + nodeIds: [string, string], +): SegmentPortPoint => ({ + segmentPortPointId: id, + x: 0, + y: 0, + availableZ: [0], + nodeIds, + edgeId: `edge-${id}`, + connectionName: null, + distToCentermostPortOnZ: 0, + cramped: true, +}) + +const createSegment = ( + id: string, + nodeIds: [string, string], +): SharedEdgeSegment => ({ + edgeId: `edge-${id}`, + nodeIds, + start: { x: 0, y: -0.05 }, + end: { x: 0, y: 0.05 }, + availableZ: [0], + portPoints: [createCrampedPort(id, nodeIds)], +}) + +test("preloaded fixed copper keeps incident cramped graph ports", () => { + const capacityMeshNodes: CapacityMeshNode[] = [ + { + capacityMeshNodeId: "fixed", + center: { x: -0.05, y: 0 }, + width: 0.1, + height: 0.1, + layer: "top", + availableZ: [0], + _preloadedFixedNetIds: ["fixed-net"], + }, + { + capacityMeshNodeId: "free-neighbor", + center: { x: 0.05, y: 0 }, + width: 0.1, + height: 0.1, + layer: "top", + availableZ: [0], + }, + { + capacityMeshNodeId: "unrelated-free-a", + center: { x: 1, y: 0 }, + width: 0.1, + height: 0.1, + layer: "top", + availableZ: [0], + }, + { + capacityMeshNodeId: "unrelated-free-b", + center: { x: 1.1, y: 0 }, + width: 0.1, + height: 0.1, + layer: "top", + availableZ: [0], + }, + ] + const sharedEdgeSegments = [ + createSegment("fixed-escape", ["fixed", "free-neighbor"]), + createSegment("unrelated", ["unrelated-free-a", "unrelated-free-b"]), + ] + const simpleRouteJson: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [], + } + const solver = new MultiTargetNecessaryCrampedPortPointSolver({ + sharedEdgeSegments, + capacityMeshNodes, + simpleRouteJson, + numberOfCrampedPortPointsToKeep: 1, + }) + + solver.solve() + + const outputByEdgeId = new Map( + solver.getOutput().map((segment) => [segment.edgeId, segment]), + ) + expect(outputByEdgeId.get("edge-fixed-escape")?.portPoints).toEqual([ + expect.objectContaining({ + segmentPortPointId: "fixed-escape", + tinyHypergraphPortPenalty: 1_000, + }), + ]) + expect(outputByEdgeId.get("edge-unrelated")?.portPoints).toEqual([]) +}) diff --git a/tests/features/preloaded-trace-canonical-net-regressions.test.ts b/tests/features/preloaded-trace-canonical-net-regressions.test.ts new file mode 100644 index 000000000..39622b5af --- /dev/null +++ b/tests/features/preloaded-trace-canonical-net-regressions.test.ts @@ -0,0 +1,235 @@ +import { expect, test } from "bun:test" +import { PreloadedTraceGraphSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver" +import { + createRelaxedDrcTraceSet, + evaluateRelaxedDrc, +} from "lib/testing/evaluate-relaxed-drc" +import { convertToCircuitJson } from "lib/testing/utils/convertToCircuitJson" +import type { + CapacityMeshNode, + SimpleRouteJson, + SimplifiedPcbTrace, +} from "lib/types" +import { resolvePreloadedTraceCanonicalNetIds } from "lib/utils/resolvePreloadedTraceCanonicalNetIds" + +const makePreloadedTraceChain = (): SimpleRouteJson => ({ + layerCount: 2, + minTraceWidth: 0.1, + defaultObstacleMargin: 0, + bounds: { minX: -3, minY: -2, maxX: 3, maxY: 2 }, + connections: [ + { + name: "root-net", + pointsToConnect: [ + { + x: -2, + y: 0, + layer: "top", + pointId: "point-a", + pcb_port_id: "pcb_port_a", + }, + { + x: 2, + y: 0, + layer: "top", + pointId: "point-c", + pcb_port_id: "pcb_port_c", + }, + ], + }, + ], + obstacles: [ + { + type: "rect", + obstacleId: "pad-a", + center: { x: -2, y: 0 }, + width: 0.4, + height: 0.4, + layers: ["top"], + connectedTo: ["fixed-a", "root-net", "pcb_port_a", "pcb_smtpad_a"], + }, + ], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "fixed-a", + connection_name: "local-a", + connectsTo: ["fixed-mid"], + route: [ + { route_type: "wire", x: -2, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: -0.5, y: 0, width: 0.1, layer: "top" }, + ], + }, + { + type: "pcb_trace", + pcb_trace_id: "fixed-mid", + connection_name: "local-mid", + connectsTo: ["fixed-a", "fixed-c"], + route: [ + { route_type: "wire", x: -0.5, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0.5, y: 0, width: 0.1, layer: "top" }, + ], + }, + { + type: "pcb_trace", + pcb_trace_id: "fixed-c", + connection_name: "local-c", + connectsTo: ["fixed-mid"], + route: [ + { route_type: "wire", x: 0.5, y: 0, width: 0.1, layer: "top" }, + { route_type: "wire", x: 2, y: 0, width: 0.1, layer: "top" }, + ], + }, + ], +}) + +const makeCrossingCandidate = (connectionName: string): SimplifiedPcbTrace => ({ + type: "pcb_trace", + // Force a collision with the generated id for fixed-mid. + pcb_trace_id: "preloaded_1_fixed-mid", + connection_name: `${connectionName}_mst0`, + route: [ + { route_type: "wire", x: 0, y: -1, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 1, width: 0.1, layer: "top" }, + ], +}) + +const withCrossingPointPair = ( + srj: SimpleRouteJson, + connectionName: string, +): SimpleRouteJson => ({ + ...srj, + connections: [ + { + name: `${connectionName}_mst0`, + __rootConnectionNames: [connectionName], + pointsToConnect: [ + { + x: 0, + y: -1, + layer: "top", + pcb_port_id: `pcb_port_${connectionName}_start`, + }, + { + x: 0, + y: 1, + layer: "top", + pcb_port_id: `pcb_port_${connectionName}_end`, + }, + ], + }, + ], +}) + +test("shared resolver propagates obstacle-only evidence through an A-MID-C trace chain", () => { + const canonicalNetByTraceId = resolvePreloadedTraceCanonicalNetIds( + makePreloadedTraceChain(), + ) + + expect(Object.fromEntries(canonicalNetByTraceId)).toEqual({ + "fixed-a": "root-net", + "fixed-mid": "root-net", + "fixed-c": "root-net", + }) +}) + +test("preloaded graph assigns the canonical net to a middle-only cell", () => { + const middleOnlyNode: CapacityMeshNode = { + capacityMeshNodeId: "middle-only", + center: { x: 0, y: 0 }, + width: 0.02, + height: 0.02, + layer: "top", + availableZ: [0], + } + const solver = new PreloadedTraceGraphSolver( + [middleOnlyNode], + makePreloadedTraceChain(), + ) + + solver.solve() + + expect(solver.getOutput()).toHaveLength(1) + expect(solver.getOutput()[0]?._preloadedFixedNetIds).toEqual(["root-net"]) +}) + +test("relaxed DRC namespaces trace links before canonical conversion", () => { + const inputSrj = makePreloadedTraceChain() + const sameNetCandidate = makeCrossingCandidate("root-net") + const combinedTraces = createRelaxedDrcTraceSet(inputSrj, [sameNetCandidate]) + const fixedTraces = combinedTraces.slice(0, 3) + + expect(fixedTraces.map((trace) => trace.pcb_trace_id)).toEqual([ + "preloaded_0_fixed-a", + "preloaded_1_fixed-mid_1", + "preloaded_2_fixed-c", + ]) + expect(fixedTraces.map((trace) => trace.connection_name)).toEqual([ + "root-net", + "root-net", + "root-net", + ]) + expect(fixedTraces.map((trace) => trace.connectsTo)).toEqual([ + ["preloaded_1_fixed-mid_1"], + ["preloaded_0_fixed-a", "preloaded_2_fixed-c"], + ["preloaded_1_fixed-mid_1"], + ]) + + const srjWithPointPairs = withCrossingPointPair(inputSrj, "root-net") + const preloadedTraceIds = new Set( + fixedTraces.map((trace) => trace.pcb_trace_id), + ) + const circuitJson = convertToCircuitJson(srjWithPointPairs, combinedTraces, { + originalSrj: inputSrj, + preloadedTraceIds, + }) + const sourceTraceIdByPcbTraceId = new Map( + circuitJson + .filter((element) => element.type === "pcb_trace") + .map((trace) => [trace.pcb_trace_id, trace.source_trace_id]), + ) + + expect(Object.fromEntries(sourceTraceIdByPcbTraceId)).toEqual({ + "preloaded_0_fixed-a": "root-net", + "preloaded_1_fixed-mid_1": "root-net", + "preloaded_2_fixed-c": "root-net", + "preloaded_1_fixed-mid": "root-net", + }) + + const sameNetResult = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs, + traces: [sameNetCandidate], + }) + expect( + sameNetResult.errors.filter( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id.startsWith("overlap_"), + ), + ).toHaveLength(0) + + const foreignNetCandidate = makeCrossingCandidate("foreign-net") + const foreignNetResult = evaluateRelaxedDrc({ + inputSrj, + srjWithPointPairs: withCrossingPointPair(inputSrj, "foreign-net"), + traces: [foreignNetCandidate], + }) + const overlapErrors = foreignNetResult.errors.filter( + (error) => + "pcb_trace_error_id" in error && + error.pcb_trace_error_id.startsWith("overlap_"), + ) + + expect(overlapErrors).toHaveLength(1) + expect( + overlapErrors[0] && + "pcb_trace_error_id" in overlapErrors[0] && + overlapErrors[0].pcb_trace_error_id, + ).toContain("preloaded_1_fixed-mid_1") + expect( + overlapErrors[0] && + "pcb_trace_error_id" in overlapErrors[0] && + overlapErrors[0].pcb_trace_error_id, + ).toContain("preloaded_1_fixed-mid") +}) diff --git a/tests/features/preloaded-trace-graph-solver.test.ts b/tests/features/preloaded-trace-graph-solver.test.ts index e9b2664dc..74fa5277f 100644 --- a/tests/features/preloaded-trace-graph-solver.test.ts +++ b/tests/features/preloaded-trace-graph-solver.test.ts @@ -2,7 +2,7 @@ import { expect, test } from "bun:test" import { PreloadedTraceGraphSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver" import type { CapacityMeshNode, SimpleRouteJson } from "lib/types" -test("preloaded trace projection only reserves fully covered layers", () => { +test("preloaded trace projection refines narrow single-layer regions", () => { const capacityMeshNodes: CapacityMeshNode[] = [ { capacityMeshNodeId: "diagonal-top", @@ -28,6 +28,14 @@ test("preloaded trace projection only reserves fully covered layers", () => { layer: "z0,1", availableZ: [0, 1], }, + { + capacityMeshNodeId: "candidate-radius-compensation-top", + center: { x: 0, y: 0.08 }, + width: 0.02, + height: 0.02, + layer: "top", + availableZ: [0], + }, { capacityMeshNodeId: "off-diagonal-top", center: { x: 0, y: 1 }, @@ -64,6 +72,7 @@ test("preloaded trace projection only reserves fully covered layers", () => { const srj: SimpleRouteJson = { layerCount: 2, minTraceWidth: 0.1, + defaultObstacleMargin: 0, minViaPadDiameter: 0.6, minViaHoleDiameter: 0.3, bounds: { minX: -3, minY: -3, maxX: 3, maxY: 3 }, @@ -121,17 +130,520 @@ test("preloaded trace projection only reserves fully covered layers", () => { solver.solve() const connectedNodeIds = solver .getOutput() - .filter((node) => node._connectedTo?.includes("net1")) + .filter((node) => node._preloadedFixedNetIds?.includes("net1")) .map((node) => node.capacityMeshNodeId) - expect(connectedNodeIds).toEqual([ - "diagonal-top", - "via-bottom", - "wire-bottom", - ]) + expect(connectedNodeIds).toContain("diagonal-top") + expect( + connectedNodeIds.some((nodeId) => nodeId.startsWith("via-bottom")), + ).toBe(true) + expect(connectedNodeIds).toContain("wire-bottom") + expect( + connectedNodeIds.some((nodeId) => + nodeId.startsWith("coarse-diagonal-top__preloaded_"), + ), + ).toBe(true) + expect( + connectedNodeIds.some( + (nodeId) => + nodeId.startsWith("off-diagonal-top") || + nodeId.startsWith("unrelated-bottom"), + ), + ).toBe(false) + const multilayerTraceNodes = solver + .getOutput() + .filter((node) => node.capacityMeshNodeId.startsWith("multilayer-diagonal")) + expect(multilayerTraceNodes).toHaveLength(2) + expect( + multilayerTraceNodes.find((node) => node.availableZ.includes(0)) + ?._preloadedFixedNetIds, + ).toContain("net1") + expect( + multilayerTraceNodes.find((node) => node.availableZ.includes(1)) + ?._preloadedFixedNetIds ?? [], + ).not.toContain("net1") + expect(connectedNodeIds).toContain("candidate-radius-compensation-top") + const reservedCoarseChildren = solver + .getOutput() + .filter( + (node) => + node.capacityMeshNodeId.startsWith("coarse-diagonal-top__preloaded_") && + node._preloadedFixedNetIds?.includes("net1"), + ) + expect(reservedCoarseChildren.length).toBeGreaterThan(0) + expect( + reservedCoarseChildren.every( + (node) => node.width <= 0.1 && node.height <= 0.1, + ), + ).toBe(true) expect(solver.stats).toMatchObject({ preloadedTraceShapeCount: 3, - projectedNodeCount: 3, - traceRegionAssignmentCount: 3, + inputNodeCount: capacityMeshNodes.length, + usedContainmentCompensation: true, + }) + expect(solver.stats.outputNodeCount).toBeGreaterThan(capacityMeshNodes.length) + + const boundedSolver = new PreloadedTraceGraphSolver(capacityMeshNodes, srj, 1) + boundedSolver.solve() + expect(boundedSolver.stats).toMatchObject({ + usedContainmentCompensation: true, + refinementBudgetExhausted: true, + }) + expect( + boundedSolver + .getOutput() + .find( + (node) => + node.capacityMeshNodeId === "candidate-radius-compensation-top", + )?._preloadedFixedNetIds ?? [], + ).toContain("net1") + expect(boundedSolver.stats.outputNodeCount).toBeLessThanOrEqual( + boundedSolver.stats.effectiveMaxOutputNodeCount, + ) +}) + +test("preloaded trace projection conservatively reserves boundary cells", () => { + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + defaultObstacleMargin: 0.15, + bounds: { minX: -3, minY: -3, maxX: 3, maxY: 3 }, + obstacles: [], + connections: [], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "fixed-diagonal", + connection_name: "fixed-net", + route: [ + { + route_type: "wire", + x: -2, + y: -2, + width: 0.1, + layer: "top", + }, + { + route_type: "wire", + x: 2, + y: 2, + width: 0.1, + layer: "top", + }, + ], + }, + ], + } + const solver = new PreloadedTraceGraphSolver( + [ + { + capacityMeshNodeId: "coarse", + center: { x: 0, y: 0 }, + width: 4, + height: 4, + layer: "top", + availableZ: [0], + }, + ], + srj, + ) + + solver.solve() + + const nodesInsideRequiredClearance = solver + .getOutput() + .filter( + (node) => + Math.abs(node.center.y - node.center.x) / Math.SQRT2 < 0.2 - 1e-9 && + Math.abs(node.center.x) < 1.8 && + Math.abs(node.center.y) < 1.8, + ) + expect(nodesInsideRequiredClearance.length).toBeGreaterThan(0) + expect( + nodesInsideRequiredClearance.every((node) => + node._preloadedFixedNetIds?.includes("fixed-net"), + ), + ).toBe(true) +}) + +test("preloaded trace projection reserves square-cap segment endpoints", () => { + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + defaultObstacleMargin: 0, + bounds: { minX: -1, minY: -1, maxX: 2, maxY: 1 }, + obstacles: [], + connections: [], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "fixed-horizontal", + connection_name: "fixed-net", + route: [ + { + route_type: "wire", + x: 0, + y: 0, + width: 0.1, + layer: "top", + }, + { + route_type: "wire", + x: 1, + y: 0, + width: 0.1, + layer: "top", + }, + ], + }, + ], + } + const solver = new PreloadedTraceGraphSolver( + [ + { + capacityMeshNodeId: "inside-endcap-clearance", + center: { x: 1.08, y: 0 }, + width: 0.01, + height: 0.01, + layer: "top", + availableZ: [0], + }, + { + capacityMeshNodeId: "outside-endcap-clearance", + center: { x: 1.12, y: 0 }, + width: 0.01, + height: 0.01, + layer: "top", + availableZ: [0], + }, + ], + srj, + ) + + solver.solve() + + expect( + solver + .getOutput() + .find((node) => node.capacityMeshNodeId === "inside-endcap-clearance") + ?._preloadedFixedNetIds, + ).toEqual(["fixed-net"]) + expect( + solver + .getOutput() + .find((node) => node.capacityMeshNodeId === "outside-endcap-clearance") + ?._preloadedFixedNetIds, + ).toBeUndefined() +}) + +test("axial preloaded traces refine into long conservative strips", () => { + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + defaultObstacleMargin: 0.15, + bounds: { minX: -3, minY: -3, maxX: 3, maxY: 3 }, + obstacles: [], + connections: [], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "fixed-horizontal", + connection_name: "fixed-net", + route: [ + { + route_type: "wire", + x: -2, + y: 0, + width: 0.1, + layer: "top", + }, + { + route_type: "wire", + x: 2, + y: 0, + width: 0.1, + layer: "top", + }, + ], + }, + ], + } + const solver = new PreloadedTraceGraphSolver( + [ + { + capacityMeshNodeId: "coarse", + center: { x: 0, y: 0 }, + width: 4, + height: 4, + layer: "top", + availableZ: [0], + }, + ], + srj, + ) + + solver.solve() + + const reservedNodes = solver + .getOutput() + .filter((node) => node._preloadedFixedNetIds?.includes("fixed-net")) + expect(reservedNodes.length).toBeGreaterThan(0) + expect( + reservedNodes.some((node) => node.width === 4 && node.height <= 0.25), + ).toBe(true) + expect(solver.stats.refinementBudgetExhausted).toBe(false) + expect(solver.stats.outputNodeCount).toBeLessThan(20) +}) + +test("separate same-net layer shapes keep one canonical fixed reservation", () => { + const createLayerTrace = (pcbTraceId: string, layer: "top" | "bottom") => ({ + type: "pcb_trace" as const, + pcb_trace_id: pcbTraceId, + connection_name: "shared-child", + route: [ + { + route_type: "wire" as const, + x: -1, + y: 0, + width: 0.1, + layer, + }, + { + route_type: "wire" as const, + x: 1, + y: 0, + width: 0.1, + layer, + }, + ], + }) + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [ + { + name: "shared-child", + __rootConnectionNames: ["shared-root"], + pointsToConnect: [ + { x: -1, y: 0, layer: "top" }, + { x: 1, y: 0, layer: "bottom" }, + ], + }, + ], + traces: [ + createLayerTrace("fixed-top", "top"), + createLayerTrace("fixed-bottom", "bottom"), + ], + } + const solver = new PreloadedTraceGraphSolver( + [ + { + capacityMeshNodeId: "both-layers", + center: { x: 0, y: 0 }, + width: 0.02, + height: 0.02, + layer: "z0,1", + availableZ: [0, 1], + }, + ], + srj, + ) + + solver.solve() + + expect(solver.getOutput()).toEqual([ + expect.objectContaining({ + capacityMeshNodeId: "both-layers", + availableZ: [0, 1], + _preloadedFixedNetIds: ["shared-root"], + }), + ]) +}) + +test("semantic target nodes ignore unrelated partial trace ownership", () => { + const createTrace = ( + pcbTraceId: string, + connectionName: string, + route: Array<{ x: number; y: number }>, + ) => ({ + type: "pcb_trace" as const, + pcb_trace_id: pcbTraceId, + connection_name: connectionName, + route: route.map(({ x, y }) => ({ + route_type: "wire" as const, + x, + y, + width: 0.1, + layer: "top" as const, + })), }) + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + defaultObstacleMargin: 0.15, + bounds: { minX: -5, minY: -5, maxX: 5, maxY: 5 }, + obstacles: [], + connections: [ + { + name: "active-net", + pointsToConnect: [ + { x: 0, y: 0, layer: "top" }, + { x: 4, y: 4, layer: "top" }, + ], + }, + { + name: "fixed-other", + pointsToConnect: [ + { x: 0.5, y: 0, layer: "top" }, + { x: 4, y: 0, layer: "top" }, + ], + }, + ], + traces: [ + createTrace("same-net-partial", "active-net", [ + { x: -1, y: 0.85 }, + { x: 1, y: 0.85 }, + ]), + createTrace("other-net-partial", "fixed-other", [ + { x: -1, y: 0.85 }, + { x: 1, y: 0.85 }, + ]), + createTrace("other-net-full", "fixed-other", [ + { x: 2, y: 0 }, + { x: 4, y: 0 }, + ]), + ], + } + const solver = new PreloadedTraceGraphSolver( + [ + { + capacityMeshNodeId: "coarse-semantic-target", + center: { x: 0, y: 0 }, + width: 1.5, + height: 1.5, + layer: "top", + availableZ: [0], + _containsTarget: true, + _targetConnectionName: "active-net", + _connectedTo: ["active-net", "fixed-other"], + }, + { + capacityMeshNodeId: "fully-covered-semantic-target", + center: { x: 3, y: 0 }, + width: 0.02, + height: 0.02, + layer: "top", + availableZ: [0], + _containsTarget: true, + _connectedTo: ["active-net"], + }, + ], + srj, + ) + + solver.solve() + + expect( + solver + .getOutput() + .find((node) => node.capacityMeshNodeId === "coarse-semantic-target") + ?._preloadedFixedNetIds, + ).toEqual(["active-net"]) + expect( + solver + .getOutput() + .find( + (node) => node.capacityMeshNodeId === "fully-covered-semantic-target", + )?._preloadedFixedNetIds, + ).toEqual(["fixed-other"]) +}) + +test("bounded refinement prioritizes large partial cells independent of input order", () => { + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + defaultObstacleMargin: 0, + bounds: { minX: -1, minY: -5, maxX: 15, maxY: 5 }, + obstacles: [], + connections: [], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "small-cell-trace", + connection_name: "small-net", + route: [ + { + route_type: "wire", + x: -0.5, + y: 0, + width: 0.1, + layer: "top", + }, + { + route_type: "wire", + x: 0.5, + y: 0, + width: 0.1, + layer: "top", + }, + ], + }, + { + type: "pcb_trace", + pcb_trace_id: "large-cell-trace", + connection_name: "large-net", + route: [ + { + route_type: "wire", + x: 6.1, + y: -3, + width: 0.1, + layer: "top", + }, + { + route_type: "wire", + x: 6.1, + y: 3, + width: 0.1, + layer: "top", + }, + ], + }, + ], + } + const solver = new PreloadedTraceGraphSolver( + [ + { + capacityMeshNodeId: "small-first", + center: { x: 0, y: 0 }, + width: 1, + height: 1, + layer: "top", + availableZ: [0], + }, + { + capacityMeshNodeId: "large-second", + center: { x: 10, y: 0 }, + width: 8, + height: 8, + layer: "top", + availableZ: [0], + }, + ], + srj, + 3, + ) + + solver.solve() + + const largestReservedArea = Math.max( + ...solver + .getOutput() + .filter((node) => node._preloadedFixedNetIds?.length) + .map((node) => node.width * node.height), + ) + expect(solver.stats.refinementBudgetExhausted).toBe(true) + expect(largestReservedArea).toBeLessThanOrEqual(32) }) diff --git a/tests/get-region-net-id-inactive-fixed-net.test.ts b/tests/get-region-net-id-inactive-fixed-net.test.ts new file mode 100644 index 000000000..869084f69 --- /dev/null +++ b/tests/get-region-net-id-inactive-fixed-net.test.ts @@ -0,0 +1,152 @@ +import { expect, test } from "bun:test" +import type { + ConnectionHgWithSimpleRouteConnection, + HyperGraphHg, + RegionHg, +} from "lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/types" +import { createTinyRouteNetIndexer } from "lib/solvers/PortPointPathingSolver/tinyhypergraph/createTinyRouteNetIndexer" +import { + BLOCKED_REGION_NET_ID, + getRegionNetIdByRegionId, +} from "lib/solvers/PortPointPathingSolver/tinyhypergraph/getRegionNetIdByRegionId" +import type { CapacityMeshNode } from "lib/types" + +const createRegion = ( + regionId: string, + x: number, + connectedTo?: string[], + preloadedFixedNetIds?: string[], +): RegionHg => ({ + regionId, + d: { + capacityMeshNodeId: regionId, + center: { x, y: 0 }, + width: 1, + height: 1, + layer: "top", + availableZ: [0], + _connectedTo: connectedTo, + _preloadedFixedNetIds: preloadedFixedNetIds, + } satisfies CapacityMeshNode, + ports: [], +}) + +test("inactive fixed nets receive distinct hypergraph reservation ids", () => { + const activeStartRegion = createRegion("active-start", 0) + const activeEndRegion = createRegion("active-end", 2) + const sameNetFixedRegion = createRegion( + "same-net-fixed-region", + 8, + undefined, + ["active-net"], + ) + const fixedRegion = createRegion("fixed-region", 10, undefined, ["fixed-net"]) + const graph: HyperGraphHg = { + regions: [ + activeStartRegion, + activeEndRegion, + sameNetFixedRegion, + fixedRegion, + ], + ports: [], + } + const activeConnection: ConnectionHgWithSimpleRouteConnection = { + connectionId: "active-connection", + mutuallyConnectedNetworkId: "active-net", + startRegion: activeStartRegion, + endRegion: activeEndRegion, + simpleRouteConnection: { + name: "active-connection", + __rootConnectionNames: ["active-net"], + pointsToConnect: [ + { x: 0, y: 0, layer: "top" }, + { x: 2, y: 0, layer: "top" }, + ], + }, + } + type RegionNetParams = Parameters< + typeof getRegionNetIdByRegionId + >[0]["params"] + const getNetIndex = createTinyRouteNetIndexer() + + const regionNetIds = getRegionNetIdByRegionId({ + params: { + graph, + connections: [activeConnection], + layerCount: 2, + } as RegionNetParams, + getNetIndex, + }) + + expect(regionNetIds.get("active-start")).toBe(0) + expect(regionNetIds.get("active-end")).toBe(0) + expect(regionNetIds.get("same-net-fixed-region")).toBe(0) + expect(regionNetIds.get("fixed-region")).toBe(1) +}) + +test("fixed canonical names do not alias unrelated point-pair ids", () => { + const activeStartRegion = createRegion("active-start", 0) + const activeEndRegion = createRegion("active-end", 2) + const fixedRegion = createRegion("fixed-region", 10, undefined, [ + "route_mst0", + ]) + const graph: HyperGraphHg = { + regions: [activeStartRegion, activeEndRegion, fixedRegion], + ports: [], + } + const activeConnection: ConnectionHgWithSimpleRouteConnection = { + connectionId: "route_mst0", + mutuallyConnectedNetworkId: "connectivity_net0", + startRegion: activeStartRegion, + endRegion: activeEndRegion, + simpleRouteConnection: { + name: "route_mst0", + __rootConnectionNames: ["route"], + pointsToConnect: [ + { x: 0, y: 0, layer: "top" }, + { x: 2, y: 0, layer: "top" }, + ], + }, + } + type RegionNetParams = Parameters< + typeof getRegionNetIdByRegionId + >[0]["params"] + + const regionNetIds = getRegionNetIdByRegionId({ + params: { + graph, + connections: [activeConnection], + layerCount: 2, + } as RegionNetParams, + getNetIndex: createTinyRouteNetIndexer(), + }) + + expect(regionNetIds.get("active-start")).toBe(0) + expect(regionNetIds.get("active-end")).toBe(0) + expect(regionNetIds.get("fixed-region")).toBe(1) +}) + +test("regions occupied by multiple fixed nets are blocked", () => { + const fixedConflictRegion = createRegion("fixed-conflict", 0, undefined, [ + "fixed-a", + "fixed-b", + ]) + const graph: HyperGraphHg = { + regions: [fixedConflictRegion], + ports: [], + } + type RegionNetParams = Parameters< + typeof getRegionNetIdByRegionId + >[0]["params"] + + const regionNetIds = getRegionNetIdByRegionId({ + params: { + graph, + connections: [], + layerCount: 2, + } as unknown as RegionNetParams, + getNetIndex: createTinyRouteNetIndexer(), + }) + + expect(regionNetIds.get("fixed-conflict")).toBe(BLOCKED_REGION_NET_ID) +}) diff --git a/tests/pipeline7-relaxed-drc-evaluator.test.ts b/tests/pipeline7-relaxed-drc-evaluator.test.ts index b15a10e20..9aa5be382 100644 --- a/tests/pipeline7-relaxed-drc-evaluator.test.ts +++ b/tests/pipeline7-relaxed-drc-evaluator.test.ts @@ -92,7 +92,81 @@ test("Pipeline7 repair uses the benchmark relaxed DRC path", () => { expect( benchmarkResult.errors.some( (error) => - "error_type" in error && error.error_type === "pcb_trace_error", + "pcb_trace_error_id" in error && + error.pcb_trace_error_id.startsWith("missing_connection_combined_"), ), - ).toBe(true) + ).toBe(false) +}) + +test("Pipeline7 repair receives location-aware via errors", () => { + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [ + { + name: "a", + pointsToConnect: [ + { x: -1, y: 0, layer: "top", pointId: "a_start" }, + { x: 1, y: 0, layer: "bottom", pointId: "a_end" }, + ], + }, + { + name: "b", + pointsToConnect: [ + { x: -1, y: 0.35, layer: "top", pointId: "b_start" }, + { x: 1, y: 0.35, layer: "bottom", pointId: "b_end" }, + ], + }, + ], + } + const routes: HighDensityRoute[] = [ + { + connectionName: "a", + route: [ + { x: -1, y: 0, z: 0 }, + { x: 0, y: 0, z: 0 }, + { x: 0, y: 0, z: 1 }, + { x: 1, y: 0, z: 1 }, + ], + vias: [{ x: 0, y: 0 }], + traceThickness: 0.1, + viaDiameter: 0.3, + }, + { + connectionName: "b", + route: [ + { x: -1, y: 0.35, z: 0 }, + { x: 0, y: 0.35, z: 0 }, + { x: 0, y: 0.35, z: 1 }, + { x: 1, y: 0.35, z: 1 }, + ], + vias: [{ x: 0, y: 0.35 }], + traceThickness: 0.1, + viaDiameter: 0.3, + }, + ] + const evaluator = createPipeline7RelaxedDrcEvaluator({ + connections: srj.connections, + originalConnections: srj.connections, + layerCount: srj.layerCount, + obstacles: srj.obstacles, + defaultViaHoleDiameter: 0.15, + connMap: getConnectivityMapFromSimpleRouteJson(srj), + srjWithPointPairs: srj, + originalSrj: srj, + }) + + const result = evaluator({ traces: [], routes }) + if (Array.isArray(result)) { + throw new Error("Exact DRC evaluator returned errors without centers") + } + + const viaError = result.errors.find( + (error) => error.type === "pcb_via_clearance_error", + ) + expect(viaError).toBeDefined() + expect(viaError?.center).toEqual({ x: 0, y: 0.175 }) + expect(isDeepStrictEqual(result.errors, result.errorsWithCenters)).toBe(true) }) diff --git a/tests/stitch-solver/multiple-high-density-route-distinct-terminal-snap.test.ts b/tests/stitch-solver/multiple-high-density-route-distinct-terminal-snap.test.ts new file mode 100644 index 000000000..25d862776 --- /dev/null +++ b/tests/stitch-solver/multiple-high-density-route-distinct-terminal-snap.test.ts @@ -0,0 +1,227 @@ +import { expect, test } from "bun:test" +import { MultipleHighDensityRouteStitchSolver3 } from "lib/solvers/RouteStitchingSolver/MultipleHighDensityRouteStitchSolver3" +import { snapIslandEndpointsToDistinctTerminals } from "lib/solvers/RouteStitchingSolver/routeStitchingEndpointHelpers" +import type { HighDensityIntraNodeRoute } from "lib/types/high-density-types" + +const makeRoute = ( + points: Array<{ x: number; y: number; z: number }>, +): HighDensityIntraNodeRoute => ({ + connectionName: "conn", + traceThickness: 0.15, + viaDiameter: 0.3, + route: points, + vias: [], + jumpers: [], +}) + +test("partial island endpoints do not both snap to the same PCB terminal", () => { + const solver = new MultipleHighDensityRouteStitchSolver3({ + connections: [ + { + name: "conn", + pointsToConnect: [ + { + x: -10, + y: 0, + layer: "top", + pointId: "pcb_port_far", + pcb_port_id: "pcb_port_far", + }, + { + x: 0, + y: 0, + layer: "top", + pointId: "pcb_port_near", + pcb_port_id: "pcb_port_near", + }, + ], + }, + ], + hdRoutes: [ + makeRoute([ + { x: 0, y: 0, z: 0 }, + { x: 0.4, y: 0, z: 0 }, + ]), + makeRoute([ + { x: 0.4, y: 0, z: 0 }, + { x: 0.8, y: 0, z: 0 }, + ]), + ], + layerCount: 2, + preserveTerminalPcbPortIds: true, + preserveDistinctTerminalSnaps: true, + }) + + expect(solver.unsolvedRoutes).toHaveLength(1) + const [unsolvedRoute] = solver.unsolvedRoutes + const startPcbPortId = ( + unsolvedRoute?.start as { pcb_port_id?: string } | undefined + )?.pcb_port_id + const endPcbPortId = ( + unsolvedRoute?.end as { pcb_port_id?: string } | undefined + )?.pcb_port_id + expect(startPcbPortId).not.toBe(endPcbPortId) + + solver.solve() + + expect(solver.failed).toBe(false) + expect(solver.solved).toBe(true) +}) + +test("distinct terminal snapping remains opt-in", () => { + const solver = new MultipleHighDensityRouteStitchSolver3({ + connections: [ + { + name: "conn", + pointsToConnect: [ + { + x: -10, + y: 0, + layer: "top", + pointId: "pcb_port_far", + pcb_port_id: "pcb_port_far", + }, + { + x: 0, + y: 0, + layer: "top", + pointId: "pcb_port_near", + pcb_port_id: "pcb_port_near", + }, + ], + }, + ], + hdRoutes: [ + makeRoute([ + { x: 0, y: 0, z: 0 }, + { x: 0.4, y: 0, z: 0 }, + ]), + makeRoute([ + { x: 0.4, y: 0, z: 0 }, + { x: 0.8, y: 0, z: 0 }, + ]), + ], + layerCount: 2, + preserveTerminalPcbPortIds: true, + }) + + expect(solver.unsolvedRoutes).toHaveLength(1) + const [unsolvedRoute] = solver.unsolvedRoutes + expect( + (unsolvedRoute?.start as { pcb_port_id?: string } | undefined)?.pcb_port_id, + ).toBe("pcb_port_near") + expect( + (unsolvedRoute?.end as { pcb_port_id?: string } | undefined)?.pcb_port_id, + ).toBe("pcb_port_near") +}) + +test("pointId-only island endpoints do not both snap to one terminal", () => { + const nearTerminal = { + x: 0, + y: 0, + z: 0, + pointId: "near_terminal", + } + const result = snapIslandEndpointsToDistinctTerminals({ + start: { x: 0, y: 0, z: 0 }, + end: { x: 0.4, y: 0, z: 0 }, + terminals: [{ x: -10, y: 0, z: 0, pointId: "far_terminal" }, nearTerminal], + }) + + expect(result.start).toBe(nearTerminal) + expect(result.end).toEqual({ x: 0.4, y: 0, z: 0 }) +}) + +test("terminal layer preservation adds endpoint vias around a wrong-layer island", () => { + const solver = new MultipleHighDensityRouteStitchSolver3({ + connections: [ + { + name: "conn", + pointsToConnect: [ + { + x: 0, + y: 0, + layer: "top", + pointId: "start", + pcb_port_id: "start", + }, + { + x: 2, + y: 0, + layer: "top", + pointId: "end", + pcb_port_id: "end", + }, + ], + }, + ], + hdRoutes: [ + makeRoute([ + { x: 0, y: 0, z: 1 }, + { x: 2, y: 0, z: 1 }, + ]), + ], + layerCount: 2, + preserveTerminalPcbPortIds: true, + preserveTerminalLayers: true, + }) + + solver.solve() + + expect(solver.failed).toBe(false) + expect(solver.mergedHdRoutes).toHaveLength(1) + expect(solver.mergedHdRoutes[0]?.route).toEqual([ + { x: 0, y: 0, z: 0 }, + { x: 0, y: 0, z: 1 }, + { x: 2, y: 0, z: 1 }, + { x: 2, y: 0, z: 0 }, + ]) + expect(solver.mergedHdRoutes[0]?.vias).toEqual([ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ]) +}) + +test("terminal layer preservation remains opt-in", () => { + const solver = new MultipleHighDensityRouteStitchSolver3({ + connections: [ + { + name: "conn", + pointsToConnect: [ + { + x: 0, + y: 0, + layer: "top", + pointId: "start", + pcb_port_id: "start", + }, + { + x: 2, + y: 0, + layer: "top", + pointId: "end", + pcb_port_id: "end", + }, + ], + }, + ], + hdRoutes: [ + makeRoute([ + { x: 0, y: 0, z: 1 }, + { x: 2, y: 0, z: 1 }, + ]), + ], + layerCount: 2, + preserveTerminalPcbPortIds: true, + }) + + solver.solve() + + expect(solver.failed).toBe(false) + expect(solver.mergedHdRoutes).toHaveLength(1) + expect(solver.mergedHdRoutes[0]?.route).toEqual([ + { x: 0, y: 0, z: 1 }, + { x: 2, y: 0, z: 1 }, + ]) + expect(solver.mergedHdRoutes[0]?.vias).toEqual([]) +}) diff --git a/tests/utils/convertHdRouteToSimplifiedRoute.test.ts b/tests/utils/convertHdRouteToSimplifiedRoute.test.ts index 38662290b..09b874632 100644 --- a/tests/utils/convertHdRouteToSimplifiedRoute.test.ts +++ b/tests/utils/convertHdRouteToSimplifiedRoute.test.ts @@ -428,4 +428,33 @@ describe("convertHdRouteToSimplifiedRoute", () => { ] `) }) + + test("serializes a coincident terminal via stub as one fabrication via", () => { + const input: HighDensityIntraNodeRoute = { + connectionName: "terminal-via-stub", + traceThickness: 0.1, + viaDiameter: 0.3, + route: [ + { x: 0, y: 0, z: 1 }, + { x: 1, y: 0, z: 1 }, + { x: 1, y: 0, z: 0 }, + { x: 1, y: 0, z: 1, pcb_port_id: "pcb_port_16" }, + ], + vias: [{ x: 1, y: 0 }], + } + + const result = convertHdRouteToSimplifiedRoute(input, 2) + const vias = result.filter((segment) => segment.route_type === "via") + + expect(vias).toEqual([ + { + route_type: "via", + x: 1, + y: 0, + from_layer: "bottom", + to_layer: "top", + via_diameter: 0.3, + }, + ]) + }) }) diff --git a/tests/utils/convertSrjTracesToObstacles.test.ts b/tests/utils/convertSrjTracesToObstacles.test.ts index 715c3d4de..1692bc8a5 100644 --- a/tests/utils/convertSrjTracesToObstacles.test.ts +++ b/tests/utils/convertSrjTracesToObstacles.test.ts @@ -60,7 +60,10 @@ const baseSrj: SimpleRouteJson = { } test("getObstaclesFromSrjTraces converts wire segments and vias to obstacles", () => { - const traceObstacles = getObstaclesFromSrjTraces(baseSrj) + const traceObstacles = getObstaclesFromSrjTraces(baseSrj, { + includeConnectionNameInConnectedTo: true, + includeSquareCaps: true, + }) expect(traceObstacles).toHaveLength(2) expect(traceObstacles[0]).toMatchObject({ @@ -69,15 +72,81 @@ test("getObstaclesFromSrjTraces converts wire segments and vias to obstacles", ( center: { x: 2, y: 0 }, width: 0.4, height: 0.4, - connectedTo: [], + connectedTo: ["net.GND"], }) expect(traceObstacles[1]).toMatchObject({ type: "rect", layers: ["top"], center: { x: 1, y: 0 }, - width: 2, + width: 2.15, height: 0.15, ccwRotationDegrees: 0, - connectedTo: [], + connectedTo: ["net.GND"], }) }) + +test("jumper gaps reserve only the physical jumper pads", () => { + const srj: SimpleRouteJson = { + ...baseSrj, + obstacles: [], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "trace_with_jumper", + connection_name: "jumper-net", + route: [ + { + route_type: "wire", + x: -1, + y: 0, + width: 0.15, + layer: "top", + }, + { + route_type: "wire", + x: 1, + y: 0, + width: 0.15, + layer: "top", + }, + { + route_type: "jumper", + start: { x: -1, y: 0 }, + end: { x: 1, y: 0 }, + footprint: "1206", + layer: "top", + }, + ], + }, + ], + } + + const traceObstacles = getObstaclesFromSrjTraces(srj, { + includeConnectionNameInConnectedTo: true, + modelJumperPads: true, + }) + + expect(traceObstacles).toHaveLength(2) + expect(traceObstacles).toEqual([ + expect.objectContaining({ + center: { x: -1, y: 0 }, + width: 0.6, + height: 1.6, + ccwRotationDegrees: 0, + connectedTo: ["jumper-net"], + }), + expect.objectContaining({ + center: { x: 1, y: 0 }, + width: 0.6, + height: 1.6, + ccwRotationDegrees: 0, + connectedTo: ["jumper-net"], + }), + ]) + expect( + convertSrjTracesToObstacles(srj, { + includeConnectionNameInConnectedTo: true, + modelJumperPads: true, + })?.obstacles, + ).toHaveLength(2) +}) From 42758dc225c2f59f4aa345c2b4bc32a1d8bd1c88 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sat, 25 Jul 2026 17:57:22 -0700 Subject: [PATCH 07/22] feat: replace Pipeline9 ijump repair with B01 --- ...-pipeline-solver9-preloaded-trace-graph.ts | 5 +- ...-rerouter.ts => pipeline9-b01-rerouter.ts} | 609 +++++++++++------- .../pipeline9-exact-drc-repair-solver.ts | 243 ++++--- package.json | 6 +- ...test.ts => pipeline9-b01-rerouter.test.ts} | 24 +- ...eline9-error-owned-cluster-rebuild.test.ts | 60 +- ...ine9-final-continuity-terminal-via.test.ts | 2 +- ...line9-final-endpoint-slide-cleanup.test.ts | 2 +- .../pipeline9-final-error-owner-sweep.test.ts | 56 +- ...ine9-fixed-copper-composite-repair.test.ts | 16 +- ...eline9-post-final-composite-repair.test.ts | 26 +- ...ne9-post-repair-same-net-via-merge.test.ts | 4 +- ...9-shared-terminal-composite-repair.test.ts | 28 +- .../pipeline9-srj23-routing-bounds.test.ts | 46 +- .../pipeline9-terminal-via-escape.test.ts | 10 +- .../pipeline9-via-micro-shift.test.ts | 2 +- 16 files changed, 661 insertions(+), 478 deletions(-) rename lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/{pipeline9-ijump-rerouter.ts => pipeline9-b01-rerouter.ts} (57%) rename tests/features/{pipeline9-ijump-rerouter.test.ts => pipeline9-b01-rerouter.test.ts} (53%) diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts index ceaf946df..5d070512e 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts @@ -252,10 +252,7 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends AutoroutingP drcEvaluator, viaInPadDrcEvaluator, originalObstacles, - ijumpBaseObstacles: [ - ...originalObstacles, - ...preloadedTraceGeometryObstacles, - ], + b01BaseObstacles: originalObstacles, srj: { ...params.srj, obstacles: [ diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts similarity index 57% rename from lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter.ts rename to lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts index 447505f1f..f1e03c357 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts @@ -1,22 +1,24 @@ -import { MultilayerIjump } from "@tscircuit/infgrid-ijump-astar" +import { + HighDensitySolverB01, + type HighDensityObstacle, + type HighDensityRectObstacle, + type HighDensityRouteObstacle, + type NodeWithPortPoints, +} from "@tscircuit/high-density-b01" import type { ConnectivityMap } from "circuit-json-to-connectivity-map" import { isObstacleConnectedToRoute } from "lib/solvers/TraceWidthSolver/isObstacleConnectedToRoute" import type { Obstacle, SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" import type { HighDensityRoute } from "lib/types/high-density-types" -import { addApproximatingRectsToSrj } from "lib/utils/addApproximatingRectsToSrj" -import { convertHdRouteToSimplifiedRoute } from "lib/utils/convertHdRouteToSimplifiedRoute" -import { getObstaclesFromSrjTraces } from "lib/utils/convertSrjTracesToObstacles" import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" import { mapZToLayerName } from "lib/utils/mapZToLayerName" -type Pipeline9IjumpRerouterParams = { +type Pipeline9B01RerouterParams = { srj: SimpleRouteJson baseObstacles: Obstacle[] connMap?: ConnectivityMap - viaHoleDiameter?: number } -export type Pipeline9IjumpRerouteOptions = { +export type Pipeline9B01RerouteOptions = { routeIndex: number startIndex?: number endIndex?: number @@ -31,7 +33,7 @@ export type Pipeline9IjumpRerouteOptions = { maxIterations: number } -export type Pipeline9IjumpRerouteResult = { +export type Pipeline9B01RerouteResult = { route?: HighDensityRoute iterations: number } @@ -43,40 +45,25 @@ export type Pipeline9TerminalViaEscapeCandidate = { } export type Pipeline9TerminalViaEscapeOptions = Omit< - Pipeline9IjumpRerouteOptions, + Pipeline9B01RerouteOptions, "startIndex" | "endIndex" > & { candidate: Pipeline9TerminalViaEscapeCandidate } const SAME_POINT_EPSILON = 1e-9 -const RELAXED_TRACE_CLEARANCE = 0.1 -const IJUMP_GRID_STEP = 0.05 +const B01_TRACE_CLEARANCE = 0.1 +const B01_GRID_STEP = 0.05 +const B01_LOW_RESOLUTION_CELL_SIZE = 0.2 +const MAX_B01_ROUTING_WINDOW_SIZE = 15 +const B01_ROUTING_WINDOW_PADDING = 2 +const MIN_B01_ROUTING_WINDOW_SIZE = 2 const MAX_TERMINAL_VIA_ESCAPE_CANDIDATES = 64 -const getFailedAttemptIterationCost = ( - solverIterations: number | undefined, - maxIterations: number, -): number => { - const boundedMaxIterations = Math.max(0, Math.floor(maxIterations)) - if (boundedMaxIterations === 0) return 0 - if (solverIterations === undefined || !Number.isFinite(solverIterations)) { - return boundedMaxIterations - } - - // MultilayerIjump starts at -1 and resets to zero only once a connection - // search begins. An exception during its connection setup must still consume - // budget, otherwise exact repair can retry a broken setup for free. - return Math.min( - boundedMaxIterations, - Math.max(1, Math.floor(solverIterations)), - ) -} - const pointsAreEqual = ( left: HighDensityRoute["route"][number] | undefined, right: HighDensityRoute["route"][number], -) => +): boolean => Boolean(left) && Math.abs(left!.x - right.x) <= SAME_POINT_EPSILON && Math.abs(left!.y - right.y) <= SAME_POINT_EPSILON && @@ -96,86 +83,260 @@ const addCanonicalConnectivity = ( return { ...obstacle, connectedTo: [...connectedTo] } } -const reverseSimplifiedRoute = ( - route: SimplifiedPcbTrace["route"], -): SimplifiedPcbTrace["route"] => - route.toReversed().map((point) => - point.route_type === "via" - ? { - ...point, - from_layer: point.to_layer, - to_layer: point.from_layer, - } - : point, - ) +const getCanonicalRootConnectionName = ( + connectionName: string, + connMap?: ConnectivityMap, +): string => + connMap?.getNetConnectedToId(connectionName) ?? + connectionName.replace(/_mst\d+$/, "") -const convertSimplifiedRouteToHdPoints = ( - simplifiedRoute: SimplifiedPcbTrace["route"], +const convertRectObstacle = ( + obstacle: Obstacle, + obstacleIndex: number, layerCount: number, - traceThickness: number, -): HighDensityRoute["route"] | undefined => { - const points: HighDensityRoute["route"] = [] - const pushPoint = (point: HighDensityRoute["route"][number]) => { - if (!pointsAreEqual(points.at(-1), point)) points.push(point) + connMap?: ConnectivityMap, +): HighDensityRectObstacle => { + const connectionName = + obstacle.obstacleId ?? `pipeline9_base_obstacle_${obstacleIndex}` + const rootConnectionName = + obstacle.connectedTo + .map((connectedId) => connMap?.getNetConnectedToId(connectedId)) + .find((netId): netId is string => Boolean(netId)) ?? connectionName + return { + type: "rect", + connectionName, + rootConnectionName, + center: obstacle.center, + width: obstacle.width, + height: obstacle.height, + ccwRotationDegrees: obstacle.ccwRotationDegrees, + zLayers: + obstacle.__zLayers ?? + obstacle.layers.map((layer) => mapLayerNameToZ(layer, layerCount)), + } +} + +const getRouteVias = ( + route: HighDensityRoute["route"], +): Array<{ x: number; y: number }> => { + const vias: Array<{ x: number; y: number }> = [] + const seen = new Set() + for (let pointIndex = 1; pointIndex < route.length; pointIndex += 1) { + const previous = route[pointIndex - 1]! + const point = route[pointIndex]! + if (previous.z === point.z) continue + const key = `${point.x.toFixed(9)}:${point.y.toFixed(9)}` + if (seen.has(key)) continue + seen.add(key) + vias.push({ x: point.x, y: point.y }) } + return vias +} - for (const point of simplifiedRoute) { - if (point.route_type === "wire") { - const z = mapLayerNameToZ(point.layer, layerCount) - if ( - !Number.isFinite(point.x) || - !Number.isFinite(point.y) || - !Number.isInteger(z) || - z < 0 || - z >= layerCount - ) { - return undefined - } - pushPoint({ - x: point.x, - y: point.y, - z, - traceThickness, - }) - continue - } +const convertCandidateRouteToObstacle = ( + route: HighDensityRoute, + connMap?: ConnectivityMap, +): HighDensityRouteObstacle => ({ + type: "route", + connectionName: route.connectionName, + rootConnectionName: + connMap?.getNetConnectedToId(route.connectionName) ?? + route.rootConnectionName ?? + route.connectionName.replace(/_mst\d+$/, ""), + traceThickness: route.traceThickness, + viaDiameter: route.viaDiameter, + route: route.route.map(({ x, y, z }) => ({ x, y, z })), + vias: getRouteVias(route.route), +}) + +const convertPreloadedTraceToRouteObstacles = ( + trace: SimplifiedPcbTrace, + traceIndex: number, + layerCount: number, + defaultViaDiameter: number, + connMap?: ConnectivityMap, +): HighDensityRouteObstacle[] => { + const connectionName = trace.connection_name + const rootConnectionName = getCanonicalRootConnectionName( + connectionName, + connMap, + ) + const obstacles: HighDensityRouteObstacle[] = [] + const addObstacle = ( + route: HighDensityRouteObstacle["route"], + traceThickness: number, + viaDiameter = defaultViaDiameter, + vias: Array<{ x: number; y: number }> = [], + ): void => { + if (route.length < 2) return + obstacles.push({ + type: "route", + connectionName: `${connectionName}_fixed_${traceIndex}_${obstacles.length}`, + rootConnectionName, + traceThickness: Math.max(SAME_POINT_EPSILON, traceThickness), + viaDiameter: Math.max(SAME_POINT_EPSILON, viaDiameter), + route, + vias, + }) + } + for (let pointIndex = 0; pointIndex < trace.route.length; pointIndex += 1) { + const point = trace.route[pointIndex]! if (point.route_type === "via") { const fromZ = mapLayerNameToZ(point.from_layer, layerCount) const toZ = mapLayerNameToZ(point.to_layer, layerCount) - if ( - !Number.isFinite(point.x) || - !Number.isFinite(point.y) || - !Number.isInteger(fromZ) || - !Number.isInteger(toZ) || - fromZ < 0 || - fromZ >= layerCount || - toZ < 0 || - toZ >= layerCount - ) { - return undefined + addObstacle( + [ + { x: point.x, y: point.y, z: fromZ }, + { x: point.x, y: point.y, z: toZ }, + ], + SAME_POINT_EPSILON, + point.via_diameter ?? defaultViaDiameter, + [{ x: point.x, y: point.y }], + ) + continue + } + if (point.route_type === "through_obstacle") { + const fromZ = mapLayerNameToZ(point.from_layer, layerCount) + const toZ = mapLayerNameToZ(point.to_layer, layerCount) + const minZ = Math.min(fromZ, toZ) + const maxZ = Math.max(fromZ, toZ) + for (let z = minZ; z <= maxZ; z += 1) { + addObstacle( + [ + { ...point.start, z }, + { ...point.end, z }, + ], + point.width, + ) } - pushPoint({ x: point.x, y: point.y, z: fromZ, traceThickness }) - pushPoint({ x: point.x, y: point.y, z: toZ, traceThickness }) continue } - return undefined + const next = trace.route[pointIndex + 1] + if ( + point.route_type !== "wire" || + next?.route_type !== "wire" || + point.layer !== next.layer + ) { + continue + } + const z = mapLayerNameToZ(point.layer, layerCount) + addObstacle( + [ + { x: point.x, y: point.y, z }, + { x: next.x, y: next.y, z }, + ], + Math.max(point.width, next.width), + ) + } + + return obstacles +} + +const simplifyB01Route = ( + route: HighDensityRoute["route"], +): HighDensityRoute["route"] => { + const deduplicated = route.filter( + (point, pointIndex, allPoints) => + !pointsAreEqual(allPoints[pointIndex - 1], point), + ) + if (deduplicated.length < 3) return deduplicated + + const simplified = [deduplicated[0]!] + for (let pointIndex = 1; pointIndex < deduplicated.length - 1; pointIndex++) { + const previous = simplified.at(-1)! + const point = deduplicated[pointIndex]! + const next = deduplicated[pointIndex + 1]! + const crossProduct = + (point.x - previous.x) * (next.y - point.y) - + (point.y - previous.y) * (next.x - point.x) + if ( + previous.z === point.z && + point.z === next.z && + Math.abs(crossProduct) <= SAME_POINT_EPSILON + ) { + continue + } + simplified.push(point) } + simplified.push(deduplicated.at(-1)!) + return simplified +} - if (points.length < 2) return undefined - for (let index = 0; index < points.length - 1; index += 1) { - const current = points[index]! - const next = points[index + 1]! +const getB01RoutingWindow = ( + startPoint: HighDensityRoute["route"][number], + endPoint: HighDensityRoute["route"][number], + boardBounds: SimpleRouteJson["bounds"], +): + | { + center: { x: number; y: number } + width: number + height: number + } + | undefined => { + const getAxisWindow = ( + start: number, + end: number, + boardMin: number, + boardMax: number, + ): { min: number; max: number } | undefined => { + const endpointMin = Math.min(start, end) + const endpointMax = Math.max(start, end) + const endpointSpan = endpointMax - endpointMin + if (endpointSpan > MAX_B01_ROUTING_WINDOW_SIZE) return undefined + + const boardSize = boardMax - boardMin + if (boardSize <= 0) return undefined + const size = Math.min( + boardSize, + MAX_B01_ROUTING_WINDOW_SIZE, + Math.max( + MIN_B01_ROUTING_WINDOW_SIZE, + endpointSpan + B01_ROUTING_WINDOW_PADDING * 2, + ), + ) + let min = (start + end) / 2 - size / 2 + let max = min + size + if (min < boardMin) { + min = boardMin + max = min + size + } + if (max > boardMax) { + max = boardMax + min = max - size + } if ( - current.z !== next.z && - (Math.abs(current.x - next.x) > SAME_POINT_EPSILON || - Math.abs(current.y - next.y) > SAME_POINT_EPSILON) + endpointMin < min - SAME_POINT_EPSILON || + endpointMax > max + SAME_POINT_EPSILON ) { return undefined } + return { min, max } + } + + const xWindow = getAxisWindow( + startPoint.x, + endPoint.x, + boardBounds.minX, + boardBounds.maxX, + ) + const yWindow = getAxisWindow( + startPoint.y, + endPoint.y, + boardBounds.minY, + boardBounds.maxY, + ) + if (!xWindow || !yWindow) return undefined + + return { + center: { + x: (xWindow.min + xWindow.max) / 2, + y: (yWindow.min + yWindow.max) / 2, + }, + width: xWindow.max - xWindow.min, + height: yWindow.max - yWindow.min, } - return points } const routeStaysInsideBounds = ( @@ -243,38 +404,55 @@ const obstacleRepresentsPhysicalPad = (obstacle: Obstacle): boolean => (id) => id.startsWith("pcb_smtpad_") || id.startsWith("pcb_plated_hole_"), ) -const snapToIjumpGrid = (value: number): number => { - const snapped = Math.round(value / IJUMP_GRID_STEP) * IJUMP_GRID_STEP +const snapToB01Grid = (value: number): number => { + const snapped = Math.round(value / B01_GRID_STEP) * B01_GRID_STEP return Math.abs(snapped) <= SAME_POINT_EPSILON ? 0 : snapped } const getPointKey = (point: { x: number; y: number }): string => `${point.x.toFixed(9)}:${point.y.toFixed(9)}` -export class Pipeline9IjumpRerouter { +export class Pipeline9B01Rerouter { private readonly srj: SimpleRouteJson private readonly connMap?: ConnectivityMap - private readonly viaHoleDiameter?: number private readonly terminalObstacles: Obstacle[] - private readonly baseObstacles: Obstacle[] + private readonly fixedObstacles: HighDensityObstacle[] private readonly candidateObstacleCache = new WeakMap< HighDensityRoute[], - Map + Map >() - constructor(params: Pipeline9IjumpRerouterParams) { + constructor(params: Pipeline9B01RerouterParams) { this.srj = params.srj this.connMap = params.connMap - this.viaHoleDiameter = params.viaHoleDiameter this.terminalObstacles = params.baseObstacles.map((obstacle) => addCanonicalConnectivity(obstacle, params.connMap), ) - this.baseObstacles = addApproximatingRectsToSrj({ - ...params.srj, - obstacles: this.terminalObstacles, - connections: [], - traces: undefined, - }).obstacles + const defaultViaDiameter = + params.srj.minViaPadDiameter ?? + params.srj.min_via_pad_diameter ?? + params.srj.minViaDiameter ?? + 0.3 + const preloadedTraceObstacles = (params.srj.traces ?? []).flatMap( + (trace, traceIndex) => + convertPreloadedTraceToRouteObstacles( + trace, + traceIndex, + params.srj.layerCount, + defaultViaDiameter, + params.connMap, + ), + ) + const baseRectObstacles = this.terminalObstacles.map( + (obstacle, obstacleIndex) => + convertRectObstacle( + obstacle, + obstacleIndex, + params.srj.layerCount, + params.connMap, + ), + ) + this.fixedObstacles = [...baseRectObstacles, ...preloadedTraceObstacles] } private getOwningTerminalObstacles( @@ -347,12 +525,12 @@ export class Pipeline9IjumpRerouter { const localX = longAxisIsX ? offset : 0 const localY = longAxisIsX ? 0 : offset addPoint({ - x: snapToIjumpGrid( + x: snapToB01Grid( obstacle.center.x + localX * Math.cos(rotationRadians) - localY * Math.sin(rotationRadians), ), - y: snapToIjumpGrid( + y: snapToB01Grid( obstacle.center.y + localX * Math.sin(rotationRadians) + localY * Math.cos(rotationRadians), @@ -468,7 +646,7 @@ export class Pipeline9IjumpRerouter { tryRerouteWithTerminalViaEscape( routes: HighDensityRoute[], options: Pipeline9TerminalViaEscapeOptions, - ): Pipeline9IjumpRerouteResult | undefined { + ): Pipeline9B01RerouteResult | undefined { const targetRoute = routes[options.routeIndex] const originalStart = targetRoute?.route[0] const originalEnd = targetRoute?.route.at(-1) @@ -556,7 +734,7 @@ export class Pipeline9IjumpRerouter { routes: HighDensityRoute[], targetRouteIndex: number, omitCandidateRouteIndexes?: ReadonlySet, - ): Obstacle[] { + ): HighDensityRouteObstacle[] { const omittedRouteIndexes = [...(omitCandidateRouteIndexes ?? [])] .filter((routeIndex) => routeIndex !== targetRouteIndex) .sort((left, right) => left - right) @@ -566,7 +744,7 @@ export class Pipeline9IjumpRerouter { if (cachedObstacles) return cachedObstacles const omittedRouteIndexSet = new Set(omittedRouteIndexes) - const traces = routes.flatMap((route, routeIndex) => { + const candidateObstacles = routes.flatMap((route, routeIndex) => { if ( routeIndex === targetRouteIndex || omittedRouteIndexSet.has(routeIndex) || @@ -574,40 +752,10 @@ export class Pipeline9IjumpRerouter { ) { return [] } - return [ - { - type: "pcb_trace" as const, - pcb_trace_id: `pipeline9_candidate_${routeIndex}`, - connection_name: route.connectionName, - route: convertHdRouteToSimplifiedRoute(route, this.srj.layerCount, { - defaultViaHoleDiameter: this.viaHoleDiameter, - obstacles: this.baseObstacles, - connMap: this.connMap, - }), - }, - ] + return [convertCandidateRouteToObstacle(route, this.connMap)] }) - const traceObstacles = getObstaclesFromSrjTraces( - { - ...this.srj, - obstacles: [], - connections: [], - traces, - }, - { - includeConnectionNameInConnectedTo: true, - includeSquareCaps: true, - modelJumperPads: true, - }, - ).map((obstacle) => addCanonicalConnectivity(obstacle, this.connMap)) - - const candidateObstacles = addApproximatingRectsToSrj({ - ...this.srj, - obstacles: traceObstacles, - connections: [], - traces: undefined, - }).obstacles - const obstaclesByTarget = cachedForRoutes ?? new Map() + const obstaclesByTarget = + cachedForRoutes ?? new Map() obstaclesByTarget.set(cacheKey, candidateObstacles) if (!cachedForRoutes) { this.candidateObstacleCache.set(routes, obstaclesByTarget) @@ -618,8 +766,8 @@ export class Pipeline9IjumpRerouter { tryReroute( routes: HighDensityRoute[], - options: Pipeline9IjumpRerouteOptions, - ): Pipeline9IjumpRerouteResult | undefined { + options: Pipeline9B01RerouteOptions, + ): Pipeline9B01RerouteResult | undefined { const targetRoute = routes[options.routeIndex] if (!targetRoute || targetRoute.route.length < 2) return undefined @@ -637,17 +785,42 @@ export class Pipeline9IjumpRerouter { return undefined } + const routingWindow = getB01RoutingWindow( + startPoint, + endPoint, + this.srj.bounds, + ) + if (!routingWindow) { + return { iterations: 0 } + } + + const rootConnectionName = + this.connMap?.getNetConnectedToId(targetRoute.connectionName) ?? + targetRoute.rootConnectionName ?? + targetRoute.connectionName.replace(/_mst\d+$/, "") const start = { + connectionName: targetRoute.connectionName, + rootConnectionName, x: startPoint.x, y: startPoint.y, - layer: mapZToLayerName(startPoint.z, this.srj.layerCount), + z: startPoint.z, } const end = { + connectionName: targetRoute.connectionName, + rootConnectionName, x: endPoint.x, y: endPoint.y, - layer: mapZToLayerName(endPoint.z, this.srj.layerCount), + z: endPoint.z, + } + const portPoints = options.reverse ? [end, start] : [start, end] + const nodeWithPortPoints: NodeWithPortPoints = { + capacityMeshNodeId: `pipeline9_b01_${options.routeIndex}`, + center: routingWindow.center, + width: routingWindow.width, + height: routingWindow.height, + availableZ: Array.from({ length: this.srj.layerCount }, (_, z) => z), + portPoints, } - const pointsToConnect = options.reverse ? [end, start] : [start, end] const candidateObstacles = options.includeCandidateCopper ? this.getCandidateObstacles( routes, @@ -655,83 +828,75 @@ export class Pipeline9IjumpRerouter { options.omitCandidateRouteIndexes, ) : [] - const input = { - ...this.srj, - obstacles: [...this.baseObstacles, ...candidateObstacles], - connections: [ - { - name: targetRoute.connectionName, - pointsToConnect, - }, - ], - traces: undefined, + const solver = new HighDensitySolverB01({ + nodeWithPortPoints, + obstacles: [...this.fixedObstacles, ...candidateObstacles], + highResolutionCellSize: B01_GRID_STEP, + highResolutionCellThickness: 8, + lowResolutionCellSize: B01_LOW_RESOLUTION_CELL_SIZE, + traceThickness: targetRoute.traceThickness, + traceMargin: B01_TRACE_CLEARANCE, + obstacleClearanceMargin: B01_TRACE_CLEARANCE, + viaDiameter: targetRoute.viaDiameter, + viaMinDistFromBorder: 0, + maxCellCount: 500_000, + hyperParameters: { + shuffleSeed: options.reverse ? 1 : options.shortenPath ? 2 : 0, + viaBaseCost: 4, + sameRootObstacleCostMultiplier: 0.05, + }, + }) + solver.MAX_ITERATIONS = Math.max(1, Math.floor(options.maxIterations)) + solver.solve() + const [solvedRoute] = solver.getOutput() + if (!solver.solved || !solvedRoute) { + return { iterations: solver.iterations } } - let solver: MultilayerIjump | undefined - try { - solver = new MultilayerIjump({ - input: input as never, - connMap: this.connMap, - GRID_STEP: IJUMP_GRID_STEP, - OBSTACLE_MARGIN: - RELAXED_TRACE_CLEARANCE + targetRoute.traceThickness / 2, - MAX_ITERATIONS: options.maxIterations, - VIA_COST: 4, - isRemovePathLoopsEnabled: true, - isShortenPathWithShortcutsEnabled: options.shortenPath, - optimizeWithGoalBoxes: false, - }) - solver.VIA_DIAMETER = targetRoute.viaDiameter - solver.GOAL_RUSH_FACTOR = 1.1 - - const [rawTrace] = solver.solveAndMapToTraces() - if (!rawTrace) return { iterations: solver.iterations } - const simplifiedRoute = options.reverse - ? reverseSimplifiedRoute( - (rawTrace as unknown as SimplifiedPcbTrace).route, - ) - : (rawTrace as unknown as SimplifiedPcbTrace).route - const reroutedPoints = convertSimplifiedRouteToHdPoints( - simplifiedRoute, - this.srj.layerCount, - targetRoute.traceThickness, - ) - if (!reroutedPoints) return { iterations: solver.iterations } + let reroutedPoints: HighDensityRoute["route"] = solvedRoute.route.map( + ({ x, y, z }) => ({ + x, + y, + z, + traceThickness: targetRoute.traceThickness, + }), + ) + const firstPoint = reroutedPoints[0] + const lastPoint = reroutedPoints.at(-1) + if ( + firstPoint && + lastPoint && + Math.hypot(lastPoint.x - startPoint.x, lastPoint.y - startPoint.y) < + Math.hypot(firstPoint.x - startPoint.x, firstPoint.y - startPoint.y) + ) { + reroutedPoints.reverse() + } + reroutedPoints = simplifyB01Route(reroutedPoints) + if (reroutedPoints.length < 2) { + return { iterations: solver.iterations } + } + reroutedPoints[0] = { ...startPoint } + reroutedPoints[reroutedPoints.length - 1] = { ...endPoint } + if (!routeStaysInsideBounds(reroutedPoints, targetRoute, this.srj.bounds)) { + return { iterations: solver.iterations } + } - reroutedPoints[0] = { ...reroutedPoints[0]!, ...startPoint } - reroutedPoints[reroutedPoints.length - 1] = { - ...reroutedPoints.at(-1)!, - ...endPoint, - } - if ( - !routeStaysInsideBounds(reroutedPoints, targetRoute, this.srj.bounds) - ) { - return { iterations: solver.iterations } - } - const route = [ - ...targetRoute.route.slice(0, startIndex), - ...reroutedPoints, - ...targetRoute.route.slice(endIndex + 1), - ].filter( - (point, pointIndex, allPoints) => - !pointsAreEqual(allPoints[pointIndex - 1], point), - ) + const route = [ + ...targetRoute.route.slice(0, startIndex), + ...reroutedPoints, + ...targetRoute.route.slice(endIndex + 1), + ].filter( + (point, pointIndex, allPoints) => + !pointsAreEqual(allPoints[pointIndex - 1], point), + ) - return { - route: { - ...targetRoute, - route, - vias: [], - }, - iterations: solver.iterations, - } - } catch { - return { - iterations: getFailedAttemptIterationCost( - solver?.iterations, - options.maxIterations, - ), - } + return { + route: { + ...targetRoute, + route, + vias: getRouteVias(route), + }, + iterations: solver.iterations, } } } diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts index 9b43fd3ee..109897b44 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts @@ -14,14 +14,14 @@ import type { HighDensityRoute } from "lib/types/high-density-types" import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" import { mapZToLayerName } from "lib/utils/mapZToLayerName" import { - type Pipeline9IjumpRerouteOptions, - Pipeline9IjumpRerouter, -} from "./pipeline9-ijump-rerouter" + type Pipeline9B01RerouteOptions, + Pipeline9B01Rerouter, +} from "./pipeline9-b01-rerouter" type Pipeline9ExactDrcRepairSolverParams = GlobalDrcBranchPortfolioSolverParams & { originalObstacles: Obstacle[] - ijumpBaseObstacles: Obstacle[] + b01BaseObstacles: Obstacle[] } type DrcError = Record @@ -111,22 +111,22 @@ const DEFAULT_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES = DEFAULT_MAX_LOCAL_CLEANUP_DRC_EVALUATIONS const HIGH_INITIAL_DRC_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES = 64 const MAX_LOCAL_LAYER_DETOUR_EXPANSION = 6 -const MAX_IJUMP_FULL_ATTEMPTS_PER_ROUND = 18 -const MAX_IJUMP_INTERIOR_ATTEMPTS_PER_ROUND = 24 -const MAX_IJUMP_FIXED_ONLY_ATTEMPTS_PER_ROUND = 8 -const DEFAULT_MAX_IJUMP_TOTAL_ITERATIONS = 300_000 -const HIGH_INITIAL_DRC_MAX_IJUMP_TOTAL_ITERATIONS = 200_000 -const MAX_IJUMP_FULL_ITERATIONS = 30_000 -const MAX_IJUMP_INTERIOR_ITERATIONS = 10_000 -const MAX_IJUMP_PHASE_ROUNDS = 2 -const MAX_IJUMP_INTERIOR_EXPANSION = 6 +const MAX_B01_FULL_ATTEMPTS_PER_ROUND = 18 +const MAX_B01_INTERIOR_ATTEMPTS_PER_ROUND = 24 +const MAX_B01_FIXED_ONLY_ATTEMPTS_PER_ROUND = 8 +const DEFAULT_MAX_B01_TOTAL_ITERATIONS = 300_000 +const HIGH_INITIAL_DRC_MAX_B01_TOTAL_ITERATIONS = 200_000 +const MAX_B01_FULL_ITERATIONS = 30_000 +const MAX_B01_INTERIOR_ITERATIONS = 10_000 +const MAX_B01_PHASE_ROUNDS = 2 +const MAX_B01_INTERIOR_EXPANSION = 6 const MAX_ERROR_OWNED_CLUSTER_ITERATIONS = 75_000 const MAX_ERROR_OWNED_CLUSTER_ROUTE_ITERATIONS = 15_000 const MAX_ERROR_OWNED_CLUSTER_PASSES = 2 const MAX_ERROR_OWNED_CLUSTER_TERMINAL_ESCAPE_CANDIDATES = 4 const MAX_ERROR_OWNED_CLUSTER_TERMINAL_ESCAPE_ITERATIONS = 10_000 const MAX_POST_CLUSTER_VIA_MICRO_SHIFT_DRC_EVALUATIONS = 32 -const MAX_FINAL_OWNER_IJUMP_ITERATIONS = 50_000 +const MAX_FINAL_OWNER_B01_ITERATIONS = 50_000 const MAX_FINAL_OWNER_FULL_ROUTE_ITERATIONS = 25_000 const MAX_FINAL_OWNER_LONG_ROUTE_ITERATIONS = 4_000 const MAX_FINAL_OWNER_VARIANT_ITERATIONS = 10_000 @@ -137,10 +137,10 @@ const MAX_FINAL_OWNER_FALLBACK_RESIDUAL = 2 const MAX_POST_REPAIR_SAME_NET_VIA_MERGER_ITERATIONS = 8 const MAX_SHARED_TERMINAL_COMPOSITE_RESIDUAL = 4 const MAX_SHARED_TERMINAL_COMPOSITE_ATTEMPTS = 1 -const MAX_SHARED_TERMINAL_COMPOSITE_IJUMP_ITERATIONS = 12_500 +const MAX_SHARED_TERMINAL_COMPOSITE_B01_ITERATIONS = 12_500 const MAX_SHARED_TERMINAL_COMPOSITE_DRC_EVALUATIONS = 2 -const MAX_POST_FINAL_COMPOSITE_IJUMP_ITERATIONS = 24_000 -const MAX_POST_FINAL_COMPOSITE_IJUMP_ITERATIONS_PER_ATTEMPT = 8_000 +const MAX_POST_FINAL_COMPOSITE_B01_ITERATIONS = 24_000 +const MAX_POST_FINAL_COMPOSITE_B01_ITERATIONS_PER_ATTEMPT = 8_000 const MAX_POST_FINAL_COMPOSITE_ATTEMPTS = 12 const MAX_POST_FINAL_COMPOSITE_DRC_EVALUATIONS = 12 const MAX_POST_FINAL_COMPOSITE_SAME_NET_VIA_MERGER_ITERATIONS = 32 @@ -158,18 +158,18 @@ const MAX_FINAL_CONTINUITY_TERMINAL_VIA_ATTEMPTS = 32 const MAX_FINAL_CONTINUITY_TERMINAL_VIA_DRC_EVALUATIONS = 8 const POST_FINAL_COMPOSITE_INTERIOR_EXPANSIONS = [4, 8] as const const POST_FINAL_COMPOSITE_TERMINAL_PROXIMITY = 4 -const FULL_IJUMP_VARIANTS = [ +const FULL_B01_VARIANTS = [ { reverse: false, shortenPath: true }, { reverse: false, shortenPath: false }, { reverse: true, shortenPath: false }, ] as const -const INTERIOR_IJUMP_VARIANTS = [ +const INTERIOR_B01_VARIANTS = [ { reverse: false, shortenPath: false }, { reverse: true, shortenPath: false }, ] as const -const FIXED_ONLY_IJUMP_VARIANTS = [ +const FIXED_ONLY_B01_VARIANTS = [ { reverse: false, shortenPath: false }, { reverse: true, shortenPath: false }, ] as const @@ -392,7 +392,7 @@ const getCoincidentTerminalPointIndexes = ( export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolver { private readonly originalObstacles: Obstacle[] private readonly terminalConstraints: TerminalConstraint[] - private readonly ijumpRerouter: Pipeline9IjumpRerouter + private readonly b01Rerouter: Pipeline9B01Rerouter private cleanupStarted = false private cleanupCandidateAttempts = 0 private cleanupCandidatesAccepted = 0 @@ -403,14 +403,14 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private maxConsecutiveLocalCleanupDrcMisses = 0 private selectedConsecutiveLocalCleanupDrcMissLimit = DEFAULT_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES - private selectedIjumpIterationLimit = DEFAULT_MAX_IJUMP_TOTAL_ITERATIONS + private selectedB01IterationLimit = DEFAULT_MAX_B01_TOTAL_ITERATIONS private viaMicroShiftAttempts = 0 private viaMicroShiftsAccepted = 0 - private ijumpFullAttempts = 0 - private ijumpInteriorAttempts = 0 - private ijumpFixedOnlyAttempts = 0 - private ijumpCandidatesAccepted = 0 - private ijumpIterations = 0 + private b01FullAttempts = 0 + private b01InteriorAttempts = 0 + private b01FixedOnlyAttempts = 0 + private b01CandidatesAccepted = 0 + private b01Iterations = 0 private errorOwnedClusterOrderAttempts = 0 private errorOwnedClusterRouteAttempts = 0 private errorOwnedClusterDrcEvaluations = 0 @@ -431,7 +431,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private postRepairSameNetViaMergeIterations = 0 private sharedTerminalCompositeAttempts = 0 private sharedTerminalCompositeRelocatedBranches = 0 - private sharedTerminalCompositeIjumpAttempts = 0 + private sharedTerminalCompositeB01Attempts = 0 private sharedTerminalCompositeDrcEvaluations = 0 private sharedTerminalCompositeCandidatesAccepted = 0 private sharedTerminalCompositeIterations = 0 @@ -459,11 +459,10 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve constructor(params: Pipeline9ExactDrcRepairSolverParams) { super(params) this.originalObstacles = params.originalObstacles - this.ijumpRerouter = new Pipeline9IjumpRerouter({ + this.b01Rerouter = new Pipeline9B01Rerouter({ srj: params.srj, - baseObstacles: params.ijumpBaseObstacles, + baseObstacles: params.b01BaseObstacles, connMap: params.connMap, - viaHoleDiameter: params.viaHoleDiameter, }) this.terminalConstraints = params.hdRoutes.flatMap((route, routeIndex) => (["start", "end"] as const).flatMap((endpoint) => { @@ -570,7 +569,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private candidateImprovesSnapshot( candidateRoutes: HighDensityRoute[], currentIssueCount: number, - source: "local" | "ijump" = "local", + source: "local" | "b01" = "local", ): boolean { this.cleanupCandidateAttempts += 1 if (!this.candidatePreservesTerminals(candidateRoutes)) return false @@ -619,9 +618,9 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.selectedConsecutiveLocalCleanupDrcMissLimit = useHighInitialDrcLimits ? HIGH_INITIAL_DRC_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES : DEFAULT_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES - this.selectedIjumpIterationLimit = useHighInitialDrcLimits - ? HIGH_INITIAL_DRC_MAX_IJUMP_TOTAL_ITERATIONS - : DEFAULT_MAX_IJUMP_TOTAL_ITERATIONS + this.selectedB01IterationLimit = useHighInitialDrcLimits + ? HIGH_INITIAL_DRC_MAX_B01_TOTAL_ITERATIONS + : DEFAULT_MAX_B01_TOTAL_ITERATIONS } private getCandidateRouteIndexesForError( @@ -1157,27 +1156,27 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve return undefined } - private getRemainingIjumpIterations(): number { - return Math.max(0, this.selectedIjumpIterationLimit - this.ijumpIterations) + private getRemainingB01Iterations(): number { + return Math.max(0, this.selectedB01IterationLimit - this.b01Iterations) } - private tryIjumpCandidate( + private tryB01Candidate( routes: HighDensityRoute[], snapshot: DrcSnapshot, - options: Omit, + options: Omit, maxIterations: number, ): HighDensityRoute[] | undefined { const iterationLimit = Math.min( maxIterations, - this.getRemainingIjumpIterations(), + this.getRemainingB01Iterations(), ) if (iterationLimit <= 0) return undefined - const result = this.ijumpRerouter.tryReroute(routes, { + const result = this.b01Rerouter.tryReroute(routes, { ...options, maxIterations: iterationLimit, }) - this.ijumpIterations += Math.max(0, result?.iterations ?? 0) + this.b01Iterations += Math.max(0, result?.iterations ?? 0) if (!result?.route) return undefined const candidateRoutes = cloneRoutes(routes) @@ -1187,17 +1186,17 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve !this.candidateImprovesSnapshot( materializedCandidate, snapshot.count, - "ijump", + "b01", ) ) { return undefined } - this.ijumpCandidatesAccepted += 1 + this.b01CandidatesAccepted += 1 return materializedCandidate } - private getOrderedIjumpRouteIndexes(snapshot: DrcSnapshot): number[] { + private getOrderedB01RouteIndexes(snapshot: DrcSnapshot): number[] { const orderedRouteIndexes: number[] = [] const seenRouteIndexes = new Set() const seenErrorGroups = new Set() @@ -1251,32 +1250,32 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve return orderedRouteIndexes } - private runIjumpFullRouteCleanup( + private runB01FullRouteCleanup( routes: HighDensityRoute[], ): HighDensityRoute[] { let improvedRoutes = routes let roundAttempts = 0 while ( - roundAttempts < MAX_IJUMP_FULL_ATTEMPTS_PER_ROUND && - this.getRemainingIjumpIterations() > 0 + roundAttempts < MAX_B01_FULL_ATTEMPTS_PER_ROUND && + this.getRemainingB01Iterations() > 0 ) { const snapshot = this.getSnapshot(improvedRoutes) if (snapshot.count === 0) break let nextRoutes: HighDensityRoute[] | undefined - const routeIndexes = this.getOrderedIjumpRouteIndexes(snapshot) + const routeIndexes = this.getOrderedB01RouteIndexes(snapshot) - attemptLoop: for (const variant of FULL_IJUMP_VARIANTS) { + attemptLoop: for (const variant of FULL_B01_VARIANTS) { for (const routeIndex of routeIndexes) { if ( - roundAttempts >= MAX_IJUMP_FULL_ATTEMPTS_PER_ROUND || - this.getRemainingIjumpIterations() <= 0 + roundAttempts >= MAX_B01_FULL_ATTEMPTS_PER_ROUND || + this.getRemainingB01Iterations() <= 0 ) { break attemptLoop } roundAttempts += 1 - this.ijumpFullAttempts += 1 - nextRoutes = this.tryIjumpCandidate( + this.b01FullAttempts += 1 + nextRoutes = this.tryB01Candidate( improvedRoutes, snapshot, { @@ -1284,7 +1283,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve includeCandidateCopper: true, ...variant, }, - MAX_IJUMP_FULL_ITERATIONS, + MAX_B01_FULL_ITERATIONS, ) if (nextRoutes) break attemptLoop } @@ -1358,21 +1357,21 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve } addWindow( - nearestSegmentIndex - MAX_IJUMP_INTERIOR_EXPANSION, + nearestSegmentIndex - MAX_B01_INTERIOR_EXPANSION, nearestSegmentIndex + 1, ) addWindow( nearestSegmentIndex, - nearestSegmentIndex + 1 + MAX_IJUMP_INTERIOR_EXPANSION, + nearestSegmentIndex + 1 + MAX_B01_INTERIOR_EXPANSION, ) addWindow( - nearestSegmentIndex - MAX_IJUMP_INTERIOR_EXPANSION, - nearestSegmentIndex + 1 + MAX_IJUMP_INTERIOR_EXPANSION, + nearestSegmentIndex - MAX_B01_INTERIOR_EXPANSION, + nearestSegmentIndex + 1 + MAX_B01_INTERIOR_EXPANSION, ) for ( let totalExpansion = 0; - totalExpansion <= MAX_IJUMP_INTERIOR_EXPANSION; + totalExpansion <= MAX_B01_INTERIOR_EXPANSION; totalExpansion += 1 ) { for ( @@ -1391,15 +1390,15 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve return windows } - private runIjumpInteriorCleanup( + private runB01InteriorCleanup( routes: HighDensityRoute[], ): HighDensityRoute[] { let improvedRoutes = routes let roundAttempts = 0 while ( - roundAttempts < MAX_IJUMP_INTERIOR_ATTEMPTS_PER_ROUND && - this.getRemainingIjumpIterations() > 0 + roundAttempts < MAX_B01_INTERIOR_ATTEMPTS_PER_ROUND && + this.getRemainingB01Iterations() > 0 ) { const snapshot = this.getSnapshot(improvedRoutes) if (snapshot.count === 0) break @@ -1458,19 +1457,19 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve windowIndex < maximumWindowCount; windowIndex += 1 ) { - for (const variant of INTERIOR_IJUMP_VARIANTS) { + for (const variant of INTERIOR_B01_VARIANTS) { for (const target of targets) { const window = target.windows[windowIndex] if (!window) continue if ( - roundAttempts >= MAX_IJUMP_INTERIOR_ATTEMPTS_PER_ROUND || - this.getRemainingIjumpIterations() <= 0 + roundAttempts >= MAX_B01_INTERIOR_ATTEMPTS_PER_ROUND || + this.getRemainingB01Iterations() <= 0 ) { break attemptLoop } roundAttempts += 1 - this.ijumpInteriorAttempts += 1 - nextRoutes = this.tryIjumpCandidate( + this.b01InteriorAttempts += 1 + nextRoutes = this.tryB01Candidate( improvedRoutes, snapshot, { @@ -1479,7 +1478,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve includeCandidateCopper: true, ...variant, }, - MAX_IJUMP_INTERIOR_ITERATIONS, + MAX_B01_INTERIOR_ITERATIONS, ) if (nextRoutes) break attemptLoop } @@ -1493,32 +1492,32 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve return improvedRoutes } - private runIjumpFixedOnlyCleanup( + private runB01FixedOnlyCleanup( routes: HighDensityRoute[], ): HighDensityRoute[] { let improvedRoutes = routes let roundAttempts = 0 while ( - roundAttempts < MAX_IJUMP_FIXED_ONLY_ATTEMPTS_PER_ROUND && - this.getRemainingIjumpIterations() > 0 + roundAttempts < MAX_B01_FIXED_ONLY_ATTEMPTS_PER_ROUND && + this.getRemainingB01Iterations() > 0 ) { const snapshot = this.getSnapshot(improvedRoutes) if (snapshot.count === 0) break let nextRoutes: HighDensityRoute[] | undefined - const routeIndexes = this.getOrderedIjumpRouteIndexes(snapshot) + const routeIndexes = this.getOrderedB01RouteIndexes(snapshot) - attemptLoop: for (const variant of FIXED_ONLY_IJUMP_VARIANTS) { + attemptLoop: for (const variant of FIXED_ONLY_B01_VARIANTS) { for (const routeIndex of routeIndexes) { if ( - roundAttempts >= MAX_IJUMP_FIXED_ONLY_ATTEMPTS_PER_ROUND || - this.getRemainingIjumpIterations() <= 0 + roundAttempts >= MAX_B01_FIXED_ONLY_ATTEMPTS_PER_ROUND || + this.getRemainingB01Iterations() <= 0 ) { break attemptLoop } roundAttempts += 1 - this.ijumpFixedOnlyAttempts += 1 - nextRoutes = this.tryIjumpCandidate( + this.b01FixedOnlyAttempts += 1 + nextRoutes = this.tryB01Candidate( improvedRoutes, snapshot, { @@ -1526,7 +1525,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve includeCandidateCopper: false, ...variant, }, - MAX_IJUMP_FULL_ITERATIONS, + MAX_B01_FULL_ITERATIONS, ) if (nextRoutes) break attemptLoop } @@ -1645,7 +1644,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve ] } - private runIjumpErrorOwnedClusterRebuild( + private runB01ErrorOwnedClusterRebuild( routes: HighDensityRoute[], ): HighDensityRoute[] { const baselineSnapshot = this.getSnapshot(routes) @@ -1693,7 +1692,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve pendingRouteIndexes.delete(routeIndex) this.errorOwnedClusterRouteAttempts += 1 - let result = this.ijumpRerouter.tryReroute(candidateRoutes, { + let result = this.b01Rerouter.tryReroute(candidateRoutes, { routeIndex, omitCandidateRouteIndexes: pendingRouteIndexes, includeCandidateCopper: true, @@ -1709,7 +1708,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve (!result?.route || !routeHasValidLayerTransitions(result.route)) && plan.allowTerminalEscape ) { - for (const candidate of this.ijumpRerouter + for (const candidate of this.b01Rerouter .getTerminalViaEscapeCandidates(candidateRoutes, routeIndex) .slice(0, MAX_ERROR_OWNED_CLUSTER_TERMINAL_ESCAPE_CANDIDATES)) { const terminalRemainingIterations = @@ -1719,7 +1718,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.errorOwnedClusterTerminalEscapeAttempts += 1 const terminalResult = - this.ijumpRerouter.tryRerouteWithTerminalViaEscape( + this.b01Rerouter.tryRerouteWithTerminalViaEscape( candidateRoutes, { routeIndex, @@ -1794,7 +1793,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve MAX_ERROR_OWNED_CLUSTER_ITERATIONS - this.errorOwnedClusterIterations this.errorOwnedClusterRouteAttempts += 1 this.errorOwnedClusterPostRouteAttempts += 1 - const result = this.ijumpRerouter.tryReroute(acceptedRoutes, { + const result = this.b01Rerouter.tryReroute(acceptedRoutes, { routeIndex, includeCandidateCopper: true, reverse: false, @@ -1840,7 +1839,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve return routes } - private runIjumpErrorOwnedClusterRebuildPasses( + private runB01ErrorOwnedClusterRebuildPasses( routes: HighDensityRoute[], ): HighDensityRoute[] { let improvedRoutes = routes @@ -1854,7 +1853,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve } const candidateRoutes = - this.runIjumpErrorOwnedClusterRebuild(improvedRoutes) + this.runB01ErrorOwnedClusterRebuild(improvedRoutes) const issueCountAfterPass = this.getSnapshot(candidateRoutes).count if (issueCountAfterPass >= issueCountBeforePass) break improvedRoutes = candidateRoutes @@ -1887,7 +1886,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private getRemainingFinalOwnerIterations(): number { return Math.max( 0, - MAX_FINAL_OWNER_IJUMP_ITERATIONS - this.finalOwnerIterations, + MAX_FINAL_OWNER_B01_ITERATIONS - this.finalOwnerIterations, ) } @@ -1950,10 +1949,10 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve ) } - private tryFinalOwnerIjumpCandidate( + private tryFinalOwnerB01Candidate( routes: HighDensityRoute[], snapshot: DrcSnapshot, - options: Omit, + options: Omit, maxIterations: number, kind: "full" | "interior", ): @@ -1970,7 +1969,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve if (kind === "full") this.finalOwnerFullAttempts += 1 else this.finalOwnerInteriorAttempts += 1 - const result = this.ijumpRerouter.tryReroute(routes, { + const result = this.b01Rerouter.tryReroute(routes, { ...options, maxIterations: iterationLimit, }) @@ -2004,7 +2003,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve } } - private runIjumpFinalErrorOwnerSweep( + private runB01FinalErrorOwnerSweep( routes: HighDensityRoute[], ): HighDensityRoute[] { let improvedRoutes = routes @@ -2022,7 +2021,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve snapshot: DrcSnapshot } | undefined - const routeIndexes = this.getOrderedIjumpRouteIndexes(snapshot) + const routeIndexes = this.getOrderedB01RouteIndexes(snapshot) fullLoop: for (const variant of FINAL_OWNER_FULL_VARIANTS) { for (const routeIndex of routeIndexes) { @@ -2049,7 +2048,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve : variant.reverse || variant.shortenPath ? MAX_FINAL_OWNER_VARIANT_ITERATIONS : MAX_FINAL_OWNER_FULL_ROUTE_ITERATIONS - acceptedCandidate = this.tryFinalOwnerIjumpCandidate( + acceptedCandidate = this.tryFinalOwnerB01Candidate( improvedRoutes, snapshot, { @@ -2099,7 +2098,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve const remainingIterations = this.getRemainingFinalOwnerIterations() if (remainingIterations <= 0) break repairLoop - acceptedCandidate = this.tryFinalOwnerIjumpCandidate( + acceptedCandidate = this.tryFinalOwnerB01Candidate( improvedRoutes, snapshot, { @@ -2131,10 +2130,10 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve if (snapshot.count <= MAX_FINAL_OWNER_FALLBACK_RESIDUAL) { fallbackLoop: for (const variant of FINAL_OWNER_FULL_VARIANTS) { - for (const routeIndex of this.getOrderedIjumpRouteIndexes(snapshot)) { + for (const routeIndex of this.getOrderedB01RouteIndexes(snapshot)) { const remainingIterations = this.getRemainingFinalOwnerIterations() if (remainingIterations <= 0) break fallbackLoop - acceptedCandidate = this.tryFinalOwnerIjumpCandidate( + acceptedCandidate = this.tryFinalOwnerB01Candidate( improvedRoutes, snapshot, { @@ -2682,11 +2681,11 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve const exposedOwnerRouteIndex = [...commonExposedOwners][0]! const remainingIterations = - MAX_SHARED_TERMINAL_COMPOSITE_IJUMP_ITERATIONS - + MAX_SHARED_TERMINAL_COMPOSITE_B01_ITERATIONS - this.sharedTerminalCompositeIterations if (remainingIterations <= 0) break - this.sharedTerminalCompositeIjumpAttempts += 1 - const rerouteResult = this.ijumpRerouter.tryReroute(atomicCandidate, { + this.sharedTerminalCompositeB01Attempts += 1 + const rerouteResult = this.b01Rerouter.tryReroute(atomicCandidate, { routeIndex: exposedOwnerRouteIndex, includeCandidateCopper: true, reverse: false, @@ -2839,7 +2838,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private getRemainingPostFinalCompositeIterations(): number { return Math.max( 0, - MAX_POST_FINAL_COMPOSITE_IJUMP_ITERATIONS - + MAX_POST_FINAL_COMPOSITE_B01_ITERATIONS - this.postFinalCompositeIterations, ) } @@ -2847,7 +2846,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private tryPostFinalCompositeCandidate( routes: HighDensityRoute[], snapshot: DrcSnapshot, - options: Omit, + options: Omit, terminalRooted: boolean, ): | { @@ -2856,7 +2855,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve } | undefined { const iterationLimit = Math.min( - MAX_POST_FINAL_COMPOSITE_IJUMP_ITERATIONS_PER_ATTEMPT, + MAX_POST_FINAL_COMPOSITE_B01_ITERATIONS_PER_ATTEMPT, this.getRemainingPostFinalCompositeIterations(), ) if ( @@ -2873,7 +2872,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve else this.postFinalCompositeForwardAttempts += 1 if (terminalRooted) this.postFinalCompositeTerminalRootedAttempts += 1 - const result = this.ijumpRerouter.tryReroute(routes, { + const result = this.b01Rerouter.tryReroute(routes, { ...options, maxIterations: iterationLimit, }) @@ -3188,7 +3187,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve } this.fixedCopperCompositePrimaryAttempts += 1 - const primaryResult = this.ijumpRerouter.tryReroute(routes, { + const primaryResult = this.b01Rerouter.tryReroute(routes, { routeIndex: plan.routeIndex, includeCandidateCopper: false, ...primaryVariant, @@ -3270,7 +3269,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve } this.fixedCopperCompositeFollowupAttempts += 1 - const followupResult = this.ijumpRerouter.tryReroute(workingRoutes, { + const followupResult = this.b01Rerouter.tryReroute(workingRoutes, { routeIndex: ownerRouteIndex, includeCandidateCopper: true, ...followupVariant, @@ -3936,26 +3935,26 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve } improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) - for (let round = 0; round < MAX_IJUMP_PHASE_ROUNDS; round += 1) { + for (let round = 0; round < MAX_B01_PHASE_ROUNDS; round += 1) { const issueCountBeforeRound = this.getSnapshot(improvedRoutes).count if (issueCountBeforeRound === 0) break let issueCountBeforePhase = issueCountBeforeRound - improvedRoutes = this.runIjumpFullRouteCleanup(improvedRoutes) + improvedRoutes = this.runB01FullRouteCleanup(improvedRoutes) let issueCountAfterPhase = this.getSnapshot(improvedRoutes).count if (issueCountAfterPhase < issueCountBeforePhase) { improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) } issueCountBeforePhase = this.getSnapshot(improvedRoutes).count - improvedRoutes = this.runIjumpInteriorCleanup(improvedRoutes) + improvedRoutes = this.runB01InteriorCleanup(improvedRoutes) issueCountAfterPhase = this.getSnapshot(improvedRoutes).count if (issueCountAfterPhase < issueCountBeforePhase) { improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) } issueCountBeforePhase = this.getSnapshot(improvedRoutes).count - improvedRoutes = this.runIjumpFixedOnlyCleanup(improvedRoutes) + improvedRoutes = this.runB01FixedOnlyCleanup(improvedRoutes) issueCountAfterPhase = this.getSnapshot(improvedRoutes).count if (issueCountAfterPhase < issueCountBeforePhase) { improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) @@ -3966,9 +3965,9 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve } } - improvedRoutes = this.runIjumpErrorOwnedClusterRebuildPasses(improvedRoutes) + improvedRoutes = this.runB01ErrorOwnedClusterRebuildPasses(improvedRoutes) improvedRoutes = this.runPostClusterViaMicroShiftCleanup(improvedRoutes) - improvedRoutes = this.runIjumpFinalErrorOwnerSweep(improvedRoutes) + improvedRoutes = this.runB01FinalErrorOwnerSweep(improvedRoutes) improvedRoutes = this.runPostRepairSameNetViaMerge(improvedRoutes) improvedRoutes = this.runSharedTerminalCompositeRepair(improvedRoutes) improvedRoutes = this.runPostFinalCompositeRepair(improvedRoutes) @@ -4005,12 +4004,12 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.selectedConsecutiveLocalCleanupDrcMissLimit, pipeline9ViaMicroShiftAttempts: this.viaMicroShiftAttempts, pipeline9ViaMicroShiftsAccepted: this.viaMicroShiftsAccepted, - pipeline9IjumpFullAttempts: this.ijumpFullAttempts, - pipeline9IjumpInteriorAttempts: this.ijumpInteriorAttempts, - pipeline9IjumpFixedOnlyAttempts: this.ijumpFixedOnlyAttempts, - pipeline9IjumpCandidatesAccepted: this.ijumpCandidatesAccepted, - pipeline9IjumpIterations: this.ijumpIterations, - pipeline9SelectedIjumpIterationLimit: this.selectedIjumpIterationLimit, + pipeline9B01FullAttempts: this.b01FullAttempts, + pipeline9B01InteriorAttempts: this.b01InteriorAttempts, + pipeline9B01FixedOnlyAttempts: this.b01FixedOnlyAttempts, + pipeline9B01CandidatesAccepted: this.b01CandidatesAccepted, + pipeline9B01Iterations: this.b01Iterations, + pipeline9SelectedB01IterationLimit: this.selectedB01IterationLimit, pipeline9ErrorOwnedClusterOrderAttempts: this.errorOwnedClusterOrderAttempts, pipeline9ErrorOwnedClusterRouteAttempts: @@ -4032,7 +4031,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve pipeline9FinalOwnerDrcEvaluations: this.finalOwnerDrcEvaluations, pipeline9FinalOwnerCandidatesAccepted: this.finalOwnerCandidatesAccepted, pipeline9FinalOwnerIterations: this.finalOwnerIterations, - pipeline9FinalOwnerIterationLimit: MAX_FINAL_OWNER_IJUMP_ITERATIONS, + pipeline9FinalOwnerIterationLimit: MAX_FINAL_OWNER_B01_ITERATIONS, pipeline9PostRepairSameNetViaMergeAttempts: this.postRepairSameNetViaMergeAttempts, pipeline9PostRepairSameNetViaMergeDrcEvaluations: @@ -4047,8 +4046,8 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.sharedTerminalCompositeAttempts, pipeline9SharedTerminalCompositeRelocatedBranches: this.sharedTerminalCompositeRelocatedBranches, - pipeline9SharedTerminalCompositeIjumpAttempts: - this.sharedTerminalCompositeIjumpAttempts, + pipeline9SharedTerminalCompositeB01Attempts: + this.sharedTerminalCompositeB01Attempts, pipeline9SharedTerminalCompositeDrcEvaluations: this.sharedTerminalCompositeDrcEvaluations, pipeline9SharedTerminalCompositeCandidatesAccepted: @@ -4056,7 +4055,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve pipeline9SharedTerminalCompositeIterations: this.sharedTerminalCompositeIterations, pipeline9SharedTerminalCompositeIterationLimit: - MAX_SHARED_TERMINAL_COMPOSITE_IJUMP_ITERATIONS, + MAX_SHARED_TERMINAL_COMPOSITE_B01_ITERATIONS, pipeline9PostFinalCompositeAttempts: this.postFinalCompositeAttempts, pipeline9PostFinalCompositeForwardAttempts: this.postFinalCompositeForwardAttempts, @@ -4070,7 +4069,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.postFinalCompositeCandidatesAccepted, pipeline9PostFinalCompositeIterations: this.postFinalCompositeIterations, pipeline9PostFinalCompositeIterationLimit: - MAX_POST_FINAL_COMPOSITE_IJUMP_ITERATIONS, + MAX_POST_FINAL_COMPOSITE_B01_ITERATIONS, pipeline9PostFinalCompositeSameNetViaMergeIterations: this.postFinalCompositeSameNetViaMergeIterations, pipeline9PostFinalCompositeSameNetViaMergeIterationLimit: diff --git a/package.json b/package.json index e93bbf113..6b1fbbfdd 100644 --- a/package.json +++ b/package.json @@ -89,12 +89,12 @@ "high-density-repair01": "git+https://github.com/tscircuit/high-density-repair01.git#cefc7be547ff727bd31573ac096b850da847af46", "high-density-repair02": "https://codeload.github.com/tscircuit/high-density-repair02/tar.gz/2afc0cbba3bf2f7eb6b9cd33615d21e9ad9352d4", "high-density-repair03": "git+https://github.com/tscircuit/high-density-repair03.git#a469074c59c1fa4e3f6410089ce5a63608d0c0dc", - "@tscircuit/high-density-a01": "git+https://github.com/tscircuit/high-density-a01.git#9a3a3d" + "@tscircuit/high-density-a01": "git+https://github.com/tscircuit/high-density-a01.git#9a3a3d", + "@tscircuit/high-density-b01": "git+https://github.com/tscircuit/high-density-b01.git#df91662" }, "dependencies": { "fast-json-stable-stringify": "^2.1.0", "object-hash": "^3.0.0", - "stack-svgs": "^0.0.1", - "@tscircuit/infgrid-ijump-astar": "0.0.33" + "stack-svgs": "^0.0.1" } } diff --git a/tests/features/pipeline9-ijump-rerouter.test.ts b/tests/features/pipeline9-b01-rerouter.test.ts similarity index 53% rename from tests/features/pipeline9-ijump-rerouter.test.ts rename to tests/features/pipeline9-b01-rerouter.test.ts index ea153a4fa..d670bd6b3 100644 --- a/tests/features/pipeline9-ijump-rerouter.test.ts +++ b/tests/features/pipeline9-b01-rerouter.test.ts @@ -1,10 +1,10 @@ import { expect, test } from "bun:test" import type { ConnectivityMap } from "circuit-json-to-connectivity-map" -import { Pipeline9IjumpRerouter } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" +import { Pipeline9B01Rerouter } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter" import type { SimpleRouteJson } from "lib/types" import type { HighDensityRoute } from "lib/types/high-density-types" -test("Pipeline9 charges bounded budget when iJump throws before search initialization", () => { +test("Pipeline9 B01 loads pre-routed traces as fixed route obstacles", () => { const srj: SimpleRouteJson = { layerCount: 2, minTraceWidth: 0.1, @@ -12,6 +12,17 @@ test("Pipeline9 charges bounded budget when iJump throws before search initializ bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, obstacles: [], connections: [], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "fixed_wall", + connection_name: "fixed_net", + route: [ + { route_type: "wire", x: 0, y: -2, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 2, width: 0.1, layer: "top" }, + ], + }, + ], } const route: HighDensityRoute = { connectionName: "missing_from_connectivity_map", @@ -26,7 +37,7 @@ test("Pipeline9 charges bounded budget when iJump throws before search initializ const connMap = { getNetConnectedToId: () => undefined, } as unknown as ConnectivityMap - const rerouter = new Pipeline9IjumpRerouter({ + const rerouter = new Pipeline9B01Rerouter({ srj, baseObstacles: [], connMap, @@ -37,8 +48,11 @@ test("Pipeline9 charges bounded budget when iJump throws before search initializ includeCandidateCopper: false, reverse: false, shortenPath: false, - maxIterations: 17, + maxIterations: 50_000, }) - expect(result).toEqual({ iterations: 1 }) + expect(result?.route?.route[0]).toEqual(route.route[0]) + expect(result?.route?.route.at(-1)).toEqual(route.route.at(-1)) + expect(result?.route?.route.some((point) => point.z === 1)).toBe(true) + expect(result?.iterations).toBeLessThanOrEqual(50_000) }) diff --git a/tests/features/pipeline9-error-owned-cluster-rebuild.test.ts b/tests/features/pipeline9-error-owned-cluster-rebuild.test.ts index 5b4779c01..89d6daa6e 100644 --- a/tests/features/pipeline9-error-owned-cluster-rebuild.test.ts +++ b/tests/features/pipeline9-error-owned-cluster-rebuild.test.ts @@ -6,10 +6,10 @@ import type { } from "high-density-repair03/lib" import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" import type { - Pipeline9IjumpRerouteOptions, - Pipeline9IjumpRerouteResult, + Pipeline9B01RerouteOptions, + Pipeline9B01RerouteResult, Pipeline9TerminalViaEscapeOptions, -} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" +} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter" const srj: SimpleRouteJson = { bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, @@ -91,7 +91,7 @@ test("Pipeline9 atomically rebuilds error-owned routes in residual-degree order" hdRoutes, drcEvaluator, originalObstacles: [], - ijumpBaseObstacles: [], + b01BaseObstacles: [], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, @@ -107,8 +107,8 @@ test("Pipeline9 atomically rebuilds error-owned routes in residual-degree order" const stubRerouter = { tryReroute: ( routes: HighDensityRoute[], - options: Pipeline9IjumpRerouteOptions, - ): Pipeline9IjumpRerouteResult | undefined => { + options: Pipeline9B01RerouteOptions, + ): Pipeline9B01RerouteResult | undefined => { const target = routes[options.routeIndex] const start = target?.route[0] const end = target?.route.at(-1) @@ -158,16 +158,16 @@ test("Pipeline9 atomically rebuilds error-owned routes in residual-degree order" }, } const privateSolver = solver as unknown as { - ijumpRerouter: typeof stubRerouter - runIjumpErrorOwnedClusterRebuild: ( + b01Rerouter: typeof stubRerouter + runB01ErrorOwnedClusterRebuild: ( routes: HighDensityRoute[], ) => HighDensityRoute[] errorOwnedClusterAccepted: number errorOwnedClusterIterations: number } - privateSolver.ijumpRerouter = stubRerouter + privateSolver.b01Rerouter = stubRerouter - const output = privateSolver.runIjumpErrorOwnedClusterRebuild(hdRoutes) + const output = privateSolver.runB01ErrorOwnedClusterRebuild(hdRoutes) expect(output.map((route) => route.route.length)).toEqual([4, 3, 3]) expect(attempts).toEqual([ @@ -204,7 +204,7 @@ test("Pipeline9 falls back to a low-degree-first reverse cluster with a bounded hdRoutes, drcEvaluator, originalObstacles: [], - ijumpBaseObstacles: [], + b01BaseObstacles: [], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, @@ -239,8 +239,8 @@ test("Pipeline9 falls back to a low-degree-first reverse cluster with a bounded const stubRerouter = { tryReroute: ( routes: HighDensityRoute[], - options: Pipeline9IjumpRerouteOptions, - ): Pipeline9IjumpRerouteResult => { + options: Pipeline9B01RerouteOptions, + ): Pipeline9B01RerouteResult => { routeAttempts.push({ routeIndex: options.routeIndex, reverse: options.reverse, @@ -263,7 +263,7 @@ test("Pipeline9 falls back to a low-degree-first reverse cluster with a bounded tryRerouteWithTerminalViaEscape: ( routes: HighDensityRoute[], options: Pipeline9TerminalViaEscapeOptions, - ): Pipeline9IjumpRerouteResult => { + ): Pipeline9B01RerouteResult => { terminalAttempts.push(options.routeIndex) return { route: rebuildRoute(routes, options.routeIndex), @@ -272,17 +272,17 @@ test("Pipeline9 falls back to a low-degree-first reverse cluster with a bounded }, } const privateSolver = solver as unknown as { - ijumpRerouter: typeof stubRerouter - runIjumpErrorOwnedClusterRebuild: ( + b01Rerouter: typeof stubRerouter + runB01ErrorOwnedClusterRebuild: ( routes: HighDensityRoute[], ) => HighDensityRoute[] errorOwnedClusterAccepted: number errorOwnedClusterIterations: number errorOwnedClusterTerminalEscapeAttempts: number } - privateSolver.ijumpRerouter = stubRerouter + privateSolver.b01Rerouter = stubRerouter - const output = privateSolver.runIjumpErrorOwnedClusterRebuild(hdRoutes) + const output = privateSolver.runB01ErrorOwnedClusterRebuild(hdRoutes) expect(output.every((route) => route.route.length === 3)).toBe(true) expect(routeAttempts.some((attempt) => attempt.reverse)).toBe(true) @@ -302,13 +302,13 @@ test.each([ initialDrcIssueCount: 19, expectedLocalLimit: 500, expectedConsecutiveMissLimit: 500, - expectedIjumpLimit: 300_000, + expectedB01Limit: 300_000, }, { initialDrcIssueCount: 20, expectedLocalLimit: 300, expectedConsecutiveMissLimit: 64, - expectedIjumpLimit: 200_000, + expectedB01Limit: 200_000, }, ])( "Pipeline9 selects adaptive cleanup limits for $initialDrcIssueCount initial errors", @@ -316,14 +316,14 @@ test.each([ initialDrcIssueCount, expectedLocalLimit, expectedConsecutiveMissLimit, - expectedIjumpLimit, + expectedB01Limit, }) => { const solver = new Pipeline9ExactDrcRepairSolver({ srj, hdRoutes, drcEvaluator, originalObstacles: [], - ijumpBaseObstacles: [], + b01BaseObstacles: [], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, @@ -335,13 +335,13 @@ test.each([ stats: Record localCleanupDrcEvaluations: number consecutiveLocalCleanupDrcMisses: number - ijumpIterations: number + b01Iterations: number selectedLocalCleanupDrcEvaluationLimit: number selectedConsecutiveLocalCleanupDrcMissLimit: number - selectedIjumpIterationLimit: number + selectedB01IterationLimit: number selectAdaptiveCleanupLimits: () => void hasLocalCleanupBudget: () => boolean - getRemainingIjumpIterations: () => number + getRemainingB01Iterations: () => number } privateSolver.stats = { initialDrcIssueCount } privateSolver.selectAdaptiveCleanupLimits() @@ -352,12 +352,12 @@ test.each([ expect(privateSolver.selectedConsecutiveLocalCleanupDrcMissLimit).toBe( expectedConsecutiveMissLimit, ) - expect(privateSolver.selectedIjumpIterationLimit).toBe(expectedIjumpLimit) + expect(privateSolver.selectedB01IterationLimit).toBe(expectedB01Limit) privateSolver.consecutiveLocalCleanupDrcMisses = expectedConsecutiveMissLimit expect(privateSolver.hasLocalCleanupBudget()).toBe(false) - privateSolver.ijumpIterations = expectedIjumpLimit - 123 - expect(privateSolver.getRemainingIjumpIterations()).toBe(123) + privateSolver.b01Iterations = expectedB01Limit - 123 + expect(privateSolver.getRemainingB01Iterations()).toBe(123) }, ) @@ -376,7 +376,7 @@ test("Pipeline9 bounds consecutive fruitless local DRC evaluations and resets af hdRoutes, drcEvaluator: boundedDrcEvaluator, originalObstacles: [], - ijumpBaseObstacles: [], + b01BaseObstacles: [], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, @@ -428,7 +428,7 @@ test("Pipeline9 reserves a bounded post-cluster via micro-shift sweep after the hdRoutes, drcEvaluator, originalObstacles: [], - ijumpBaseObstacles: [], + b01BaseObstacles: [], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, diff --git a/tests/features/pipeline9-final-continuity-terminal-via.test.ts b/tests/features/pipeline9-final-continuity-terminal-via.test.ts index 44509209f..3aabb166a 100644 --- a/tests/features/pipeline9-final-continuity-terminal-via.test.ts +++ b/tests/features/pipeline9-final-continuity-terminal-via.test.ts @@ -137,7 +137,7 @@ test("Pipeline9 bridges a bottom terminal to same-net preloaded top copper", () connectivity_net16: ["source_net_1", "source_net_1_mst0"], }), originalObstacles: [startPad, terminalPad], - ijumpBaseObstacles: [startPad, terminalPad], + b01BaseObstacles: [startPad, terminalPad], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, diff --git a/tests/features/pipeline9-final-endpoint-slide-cleanup.test.ts b/tests/features/pipeline9-final-endpoint-slide-cleanup.test.ts index dc724d9f5..b1bc687e1 100644 --- a/tests/features/pipeline9-final-endpoint-slide-cleanup.test.ts +++ b/tests/features/pipeline9-final-endpoint-slide-cleanup.test.ts @@ -149,7 +149,7 @@ test("Pipeline9 atomically slides a shared terminal after local cleanup is exhau foreign_net: ["foreign_net"], }), originalObstacles: [ownPad, foreignPad], - ijumpBaseObstacles: [ownPad, foreignPad], + b01BaseObstacles: [ownPad, foreignPad], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, diff --git a/tests/features/pipeline9-final-error-owner-sweep.test.ts b/tests/features/pipeline9-final-error-owner-sweep.test.ts index 2f0b4e64b..84b4a9910 100644 --- a/tests/features/pipeline9-final-error-owner-sweep.test.ts +++ b/tests/features/pipeline9-final-error-owner-sweep.test.ts @@ -6,9 +6,9 @@ import type { } from "high-density-repair03/lib" import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" import type { - Pipeline9IjumpRerouteOptions, - Pipeline9IjumpRerouteResult, -} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" + Pipeline9B01RerouteOptions, + Pipeline9B01RerouteResult, +} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter" const makeSrj = (names: string[]): SimpleRouteJson => ({ bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, @@ -35,7 +35,7 @@ const makeSolver = ( hdRoutes, drcEvaluator, originalObstacles: [], - ijumpBaseObstacles: [], + b01BaseObstacles: [], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, @@ -76,8 +76,8 @@ test("Pipeline9 final-owner sweep handles a 1,046-iteration single owner", () => const stubRerouter = { tryReroute: ( routes: HighDensityRoute[], - options: Pipeline9IjumpRerouteOptions, - ): Pipeline9IjumpRerouteResult => { + options: Pipeline9B01RerouteOptions, + ): Pipeline9B01RerouteResult => { const target = routes[options.routeIndex]! return { route: { @@ -93,17 +93,17 @@ test("Pipeline9 final-owner sweep handles a 1,046-iteration single owner", () => }, } const privateSolver = solver as unknown as { - ijumpRerouter: typeof stubRerouter - runIjumpFinalErrorOwnerSweep: ( + b01Rerouter: typeof stubRerouter + runB01FinalErrorOwnerSweep: ( routes: HighDensityRoute[], ) => HighDensityRoute[] finalOwnerFullAttempts: number finalOwnerCandidatesAccepted: number finalOwnerIterations: number } - privateSolver.ijumpRerouter = stubRerouter + privateSolver.b01Rerouter = stubRerouter - const output = privateSolver.runIjumpFinalErrorOwnerSweep(hdRoutes) + const output = privateSolver.runB01FinalErrorOwnerSweep(hdRoutes) expect(drcEvaluator({ traces: [], routes: output })).toEqual([]) expect(privateSolver.finalOwnerFullAttempts).toBe(1) @@ -154,8 +154,8 @@ test("Pipeline9 recomputes final owners and falls back to a nearest-error interi const stubRerouter = { tryReroute: ( routes: HighDensityRoute[], - options: Pipeline9IjumpRerouteOptions, - ): Pipeline9IjumpRerouteResult => { + options: Pipeline9B01RerouteOptions, + ): Pipeline9B01RerouteResult => { attemptedRouteIndexes.push(options.routeIndex) const target = routes[options.routeIndex]! if (options.routeIndex === 0) { @@ -187,17 +187,17 @@ test("Pipeline9 recomputes final owners and falls back to a nearest-error interi }, } const privateSolver = solver as unknown as { - ijumpRerouter: typeof stubRerouter - runIjumpFinalErrorOwnerSweep: ( + b01Rerouter: typeof stubRerouter + runB01FinalErrorOwnerSweep: ( routes: HighDensityRoute[], ) => HighDensityRoute[] finalOwnerFullAttempts: number finalOwnerInteriorAttempts: number finalOwnerCandidatesAccepted: number } - privateSolver.ijumpRerouter = stubRerouter + privateSolver.b01Rerouter = stubRerouter - const output = privateSolver.runIjumpFinalErrorOwnerSweep(hdRoutes) + const output = privateSolver.runB01FinalErrorOwnerSweep(hdRoutes) expect(drcEvaluator({ traces: [], routes: output })).toEqual([]) expect(attemptedRouteIndexes[0]).toBe(0) @@ -260,8 +260,8 @@ test("Pipeline9 retries a failed final owner after accepted copper changes", () const stubRerouter = { tryReroute: ( routes: HighDensityRoute[], - options: Pipeline9IjumpRerouteOptions, - ): Pipeline9IjumpRerouteResult => { + options: Pipeline9B01RerouteOptions, + ): Pipeline9B01RerouteResult => { if (!options.reverse && !options.shortenPath) { rawForwardAttempts.push(options.routeIndex) } @@ -286,15 +286,15 @@ test("Pipeline9 retries a failed final owner after accepted copper changes", () }, } const privateSolver = solver as unknown as { - ijumpRerouter: typeof stubRerouter - runIjumpFinalErrorOwnerSweep: ( + b01Rerouter: typeof stubRerouter + runB01FinalErrorOwnerSweep: ( routes: HighDensityRoute[], ) => HighDensityRoute[] finalOwnerCandidatesAccepted: number } - privateSolver.ijumpRerouter = stubRerouter + privateSolver.b01Rerouter = stubRerouter - const output = privateSolver.runIjumpFinalErrorOwnerSweep(hdRoutes) + const output = privateSolver.runB01FinalErrorOwnerSweep(hdRoutes) expect(output[0]?.route).toHaveLength(3) expect(output[1]?.route).toHaveLength(3) @@ -327,21 +327,21 @@ test("Pipeline9 final-owner sweep never exceeds its separate 50k budget", () => const stubRerouter = { tryReroute: ( _routes: HighDensityRoute[], - options: Pipeline9IjumpRerouteOptions, - ): Pipeline9IjumpRerouteResult => ({ + options: Pipeline9B01RerouteOptions, + ): Pipeline9B01RerouteResult => ({ iterations: options.maxIterations, }), } const privateSolver = solver as unknown as { - ijumpRerouter: typeof stubRerouter - runIjumpFinalErrorOwnerSweep: ( + b01Rerouter: typeof stubRerouter + runB01FinalErrorOwnerSweep: ( routes: HighDensityRoute[], ) => HighDensityRoute[] finalOwnerIterations: number } - privateSolver.ijumpRerouter = stubRerouter + privateSolver.b01Rerouter = stubRerouter - privateSolver.runIjumpFinalErrorOwnerSweep(hdRoutes) + privateSolver.runB01FinalErrorOwnerSweep(hdRoutes) expect(privateSolver.finalOwnerIterations).toBe(50_000) }) diff --git a/tests/features/pipeline9-fixed-copper-composite-repair.test.ts b/tests/features/pipeline9-fixed-copper-composite-repair.test.ts index 3297235a8..05c2d8b7f 100644 --- a/tests/features/pipeline9-fixed-copper-composite-repair.test.ts +++ b/tests/features/pipeline9-fixed-copper-composite-repair.test.ts @@ -6,9 +6,9 @@ import type { } from "high-density-repair03/lib" import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" import type { - Pipeline9IjumpRerouteOptions, - Pipeline9IjumpRerouteResult, -} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" + Pipeline9B01RerouteOptions, + Pipeline9B01RerouteResult, +} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter" const srj: SimpleRouteJson = { bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, @@ -101,7 +101,7 @@ test("Pipeline9 atomically repairs a fixed-copper escape and its newly exposed o hdRoutes, drcEvaluator, originalObstacles: [], - ijumpBaseObstacles: [], + b01BaseObstacles: [], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, @@ -117,8 +117,8 @@ test("Pipeline9 atomically repairs a fixed-copper escape and its newly exposed o const stubRerouter = { tryReroute: ( routes: HighDensityRoute[], - options: Pipeline9IjumpRerouteOptions, - ): Pipeline9IjumpRerouteResult | undefined => { + options: Pipeline9B01RerouteOptions, + ): Pipeline9B01RerouteResult | undefined => { attempts.push({ routeIndex: options.routeIndex, includeCandidateCopper: options.includeCandidateCopper, @@ -157,7 +157,7 @@ test("Pipeline9 atomically repairs a fixed-copper escape and its newly exposed o }, } const privateSolver = solver as unknown as { - ijumpRerouter: typeof stubRerouter + b01Rerouter: typeof stubRerouter runFixedCopperCompositeRepair: ( routes: HighDensityRoute[], ) => HighDensityRoute[] @@ -168,7 +168,7 @@ test("Pipeline9 atomically repairs a fixed-copper escape and its newly exposed o fixedCopperCompositeCandidatesAccepted: number fixedCopperCompositeIterations: number } - privateSolver.ijumpRerouter = stubRerouter + privateSolver.b01Rerouter = stubRerouter expect(privateSolver.getSnapshot(hdRoutes).count).toBe(1) const output = privateSolver.runFixedCopperCompositeRepair(hdRoutes) diff --git a/tests/features/pipeline9-post-final-composite-repair.test.ts b/tests/features/pipeline9-post-final-composite-repair.test.ts index b1edd864f..55e678d60 100644 --- a/tests/features/pipeline9-post-final-composite-repair.test.ts +++ b/tests/features/pipeline9-post-final-composite-repair.test.ts @@ -7,9 +7,9 @@ import type { } from "high-density-repair03/lib" import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" import type { - Pipeline9IjumpRerouteOptions, - Pipeline9IjumpRerouteResult, -} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" + Pipeline9B01RerouteOptions, + Pipeline9B01RerouteResult, +} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter" const makeSolver = ({ srj, @@ -28,7 +28,7 @@ const makeSolver = ({ drcEvaluator, connMap, originalObstacles: [], - ijumpBaseObstacles: [], + b01BaseObstacles: [], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, @@ -102,12 +102,12 @@ test("Pipeline9 fairly schedules both directions for a terminal-rooted window", ? [] : { errors: [error], errorsWithCenters: [error] } const solver = makeSolver({ srj, hdRoutes, drcEvaluator }) - const attempts: Pipeline9IjumpRerouteOptions[] = [] + const attempts: Pipeline9B01RerouteOptions[] = [] const stubRerouter = { tryReroute: ( routes: HighDensityRoute[], - options: Pipeline9IjumpRerouteOptions, - ): Pipeline9IjumpRerouteResult => { + options: Pipeline9B01RerouteOptions, + ): Pipeline9B01RerouteResult => { attempts.push({ ...options }) if (!options.reverse) return { iterations: 100 } return { @@ -120,7 +120,7 @@ test("Pipeline9 fairly schedules both directions for a terminal-rooted window", }, } const privateSolver = solver as unknown as { - ijumpRerouter: typeof stubRerouter + b01Rerouter: typeof stubRerouter runPostFinalCompositeRepair: ( routes: HighDensityRoute[], ) => HighDensityRoute[] @@ -130,7 +130,7 @@ test("Pipeline9 fairly schedules both directions for a terminal-rooted window", postFinalCompositeTerminalRootedAttempts: number postFinalCompositeCandidatesAccepted: number } - privateSolver.ijumpRerouter = stubRerouter + privateSolver.b01Rerouter = stubRerouter const output = privateSolver.runPostFinalCompositeRepair(hdRoutes) @@ -247,8 +247,8 @@ test("Pipeline9 atomically merges only the rerouted owner's canonical net", () = const stubRerouter = { tryReroute: ( routes: HighDensityRoute[], - options: Pipeline9IjumpRerouteOptions, - ): Pipeline9IjumpRerouteResult => ({ + options: Pipeline9B01RerouteOptions, + ): Pipeline9B01RerouteResult => ({ route: { ...routes[options.routeIndex]!, rootConnectionName: "rerouted", @@ -257,7 +257,7 @@ test("Pipeline9 atomically merges only the rerouted owner's canonical net", () = }), } const privateSolver = solver as unknown as { - ijumpRerouter: typeof stubRerouter + b01Rerouter: typeof stubRerouter runPostFinalCompositeRepair: ( routes: HighDensityRoute[], ) => HighDensityRoute[] @@ -266,7 +266,7 @@ test("Pipeline9 atomically merges only the rerouted owner's canonical net", () = postFinalCompositeCandidatesAccepted: number postFinalCompositeSameNetViaMergeIterations: number } - privateSolver.ijumpRerouter = stubRerouter + privateSolver.b01Rerouter = stubRerouter expect(privateSolver.getSnapshot(hdRoutes).count).toBe(1) const output = privateSolver.runPostFinalCompositeRepair(hdRoutes) diff --git a/tests/features/pipeline9-post-repair-same-net-via-merge.test.ts b/tests/features/pipeline9-post-repair-same-net-via-merge.test.ts index 066a3c87e..571d09c58 100644 --- a/tests/features/pipeline9-post-repair-same-net-via-merge.test.ts +++ b/tests/features/pipeline9-post-repair-same-net-via-merge.test.ts @@ -97,7 +97,7 @@ test("Pipeline9 coalesces post-repair same-net vias with stale via metadata", () drcEvaluator, connMap: new ConnectivityMap({ same_net: ["A", "B"] }), originalObstacles: [], - ijumpBaseObstacles: [], + b01BaseObstacles: [], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, @@ -138,7 +138,7 @@ test("Pipeline9 coalesces post-repair same-net vias with stale via metadata", () drcEvaluator, connMap: new ConnectivityMap({ incomplete_net: ["A"] }), originalObstacles: [], - ijumpBaseObstacles: [], + b01BaseObstacles: [], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, diff --git a/tests/features/pipeline9-shared-terminal-composite-repair.test.ts b/tests/features/pipeline9-shared-terminal-composite-repair.test.ts index 0def5807a..75b5ab1e6 100644 --- a/tests/features/pipeline9-shared-terminal-composite-repair.test.ts +++ b/tests/features/pipeline9-shared-terminal-composite-repair.test.ts @@ -7,9 +7,9 @@ import type { } from "high-density-repair03/lib" import { Pipeline9ExactDrcRepairSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver" import type { - Pipeline9IjumpRerouteOptions, - Pipeline9IjumpRerouteResult, -} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" + Pipeline9B01RerouteOptions, + Pipeline9B01RerouteResult, +} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter" import type { Obstacle } from "lib/types" const srj: SimpleRouteJson = { @@ -145,7 +145,7 @@ const makeSolver = () => blocker_net: ["blocker"], }), originalObstacles: [sharedPad], - ijumpBaseObstacles: [sharedPad], + b01BaseObstacles: [sharedPad], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, @@ -156,12 +156,12 @@ const makeSolver = () => test("Pipeline9 atomically relocates shared-terminal vias and reroutes the exposed owner", () => { const solver = makeSolver() - const attempts: Pipeline9IjumpRerouteOptions[] = [] + const attempts: Pipeline9B01RerouteOptions[] = [] const stubRerouter = { tryReroute: ( routes: HighDensityRoute[], - options: Pipeline9IjumpRerouteOptions, - ): Pipeline9IjumpRerouteResult => { + options: Pipeline9B01RerouteOptions, + ): Pipeline9B01RerouteResult => { attempts.push({ ...options }) return { route: { @@ -173,19 +173,19 @@ test("Pipeline9 atomically relocates shared-terminal vias and reroutes the expos }, } const privateSolver = solver as unknown as { - ijumpRerouter: typeof stubRerouter + b01Rerouter: typeof stubRerouter runSharedTerminalCompositeRepair: ( routes: HighDensityRoute[], ) => HighDensityRoute[] getSnapshot: (routes: HighDensityRoute[]) => { count: number } sharedTerminalCompositeAttempts: number sharedTerminalCompositeRelocatedBranches: number - sharedTerminalCompositeIjumpAttempts: number + sharedTerminalCompositeB01Attempts: number sharedTerminalCompositeDrcEvaluations: number sharedTerminalCompositeCandidatesAccepted: number sharedTerminalCompositeIterations: number } - privateSolver.ijumpRerouter = stubRerouter + privateSolver.b01Rerouter = stubRerouter const output = privateSolver.runSharedTerminalCompositeRepair(hdRoutes) @@ -203,7 +203,7 @@ test("Pipeline9 atomically relocates shared-terminal vias and reroutes the expos ]) expect(privateSolver.sharedTerminalCompositeAttempts).toBe(1) expect(privateSolver.sharedTerminalCompositeRelocatedBranches).toBe(2) - expect(privateSolver.sharedTerminalCompositeIjumpAttempts).toBe(1) + expect(privateSolver.sharedTerminalCompositeB01Attempts).toBe(1) expect(privateSolver.sharedTerminalCompositeDrcEvaluations).toBe(2) expect(privateSolver.sharedTerminalCompositeCandidatesAccepted).toBe(1) expect(privateSolver.sharedTerminalCompositeIterations).toBe(2_181) @@ -212,17 +212,17 @@ test("Pipeline9 atomically relocates shared-terminal vias and reroutes the expos test("Pipeline9 rolls back a shared-terminal composite that exhausts its hard budget", () => { const solver = makeSolver() const stubRerouter = { - tryReroute: (): Pipeline9IjumpRerouteResult => ({ iterations: 12_500 }), + tryReroute: (): Pipeline9B01RerouteResult => ({ iterations: 12_500 }), } const privateSolver = solver as unknown as { - ijumpRerouter: typeof stubRerouter + b01Rerouter: typeof stubRerouter runSharedTerminalCompositeRepair: ( routes: HighDensityRoute[], ) => HighDensityRoute[] sharedTerminalCompositeCandidatesAccepted: number sharedTerminalCompositeIterations: number } - privateSolver.ijumpRerouter = stubRerouter + privateSolver.b01Rerouter = stubRerouter const output = privateSolver.runSharedTerminalCompositeRepair(hdRoutes) diff --git a/tests/features/pipeline9-srj23-routing-bounds.test.ts b/tests/features/pipeline9-srj23-routing-bounds.test.ts index 59021fdad..09a83afab 100644 --- a/tests/features/pipeline9-srj23-routing-bounds.test.ts +++ b/tests/features/pipeline9-srj23-routing-bounds.test.ts @@ -2,23 +2,31 @@ import { expect, test } from "bun:test" import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib" import { loadScenarioBySampleNumber } from "../../scripts/benchmark/scenarios" -test("Pipeline9 routes srj23 samples with connected pads beyond implicit bounds", async () => { - for (const sampleNumber of [101, 107]) { - const { scenario } = await loadScenarioBySampleNumber("srj23", sampleNumber) - const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph( - scenario, - { - cacheProvider: null, - effort: 0.1, - }, - ) - solver.solve() +test( + "Pipeline9 routes srj23 samples with connected pads beyond implicit bounds", + async () => { + for (const sampleNumber of [101, 107]) { + const { scenario } = await loadScenarioBySampleNumber( + "srj23", + sampleNumber, + ) + const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph( + scenario, + { + cacheProvider: null, + effort: 0.1, + }, + ) + solver.solve() - expect(solver.failed).toBe(false) - expect(solver.solved).toBe(true) - expect( - solver.preprocessSimpleRouteJsonSolver?.getOutputSimpleRouteJson().bounds, - ).not.toEqual(scenario.bounds) - expect(solver.originalSrj.bounds).toEqual(scenario.bounds) - } -}) + expect(solver.failed).toBe(false) + expect(solver.solved).toBe(true) + expect( + solver.preprocessSimpleRouteJsonSolver?.getOutputSimpleRouteJson() + .bounds, + ).not.toEqual(scenario.bounds) + expect(solver.originalSrj.bounds).toEqual(scenario.bounds) + } + }, + { timeout: 15_000 }, +) diff --git a/tests/features/pipeline9-terminal-via-escape.test.ts b/tests/features/pipeline9-terminal-via-escape.test.ts index 7d35de536..6e604c8e4 100644 --- a/tests/features/pipeline9-terminal-via-escape.test.ts +++ b/tests/features/pipeline9-terminal-via-escape.test.ts @@ -1,8 +1,8 @@ import { expect, test } from "bun:test" import { - Pipeline9IjumpRerouter, + Pipeline9B01Rerouter, type Pipeline9TerminalViaEscapeCandidate, -} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-ijump-rerouter" +} from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter" import type { Obstacle, SimpleRouteJson } from "lib/types" import type { HighDensityRoute } from "lib/types/high-density-types" @@ -62,7 +62,7 @@ const getLayerTransitions = (candidateRoute: HighDensityRoute) => }) test("Pipeline9 escapes a blocked terminal pad with bounded generated vias", () => { - const rerouter = new Pipeline9IjumpRerouter({ + const rerouter = new Pipeline9B01Rerouter({ srj, baseObstacles: srj.obstacles, }) @@ -129,7 +129,7 @@ test("Pipeline9 escapes a blocked terminal pad with bounded generated vias", () }) test("Pipeline9 rejects a terminal escape access point outside its pad", () => { - const rerouter = new Pipeline9IjumpRerouter({ + const rerouter = new Pipeline9B01Rerouter({ srj, baseObstacles: srj.obstacles, }) @@ -156,7 +156,7 @@ test("Pipeline9 interleaves terminal escape layers within the bounded prefix", ( ...srj, layerCount: 4, } - const rerouter = new Pipeline9IjumpRerouter({ + const rerouter = new Pipeline9B01Rerouter({ srj: multilayerSrj, baseObstacles: multilayerSrj.obstacles, }) diff --git a/tests/features/pipeline9-via-micro-shift.test.ts b/tests/features/pipeline9-via-micro-shift.test.ts index dd3879cc3..47db5ce15 100644 --- a/tests/features/pipeline9-via-micro-shift.test.ts +++ b/tests/features/pipeline9-via-micro-shift.test.ts @@ -92,7 +92,7 @@ test("Pipeline9 micro-shifts a candidate via when strict DRC improves", () => { hdRoutes, drcEvaluator, originalObstacles: [], - ijumpBaseObstacles: [], + b01BaseObstacles: [], viaHoleDiameter: 0.15, maxIterations: 1, enableLargeBoardBroadFallback: false, From c109fddcd307c69be24fa96d6ea3ddf2f27bb355 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sat, 25 Jul 2026 22:38:42 -0700 Subject: [PATCH 08/22] fix: complete Pipeline9 B01 DRC repair --- .../pipeline9-b01-rerouter.ts | 52 ++- .../pipeline9-exact-drc-repair-solver.ts | 364 ++++++++++++++++++ tests/features/pipeline9-b01-rerouter.test.ts | 46 +++ .../pipeline9-srj23-relaxed-drc.test.ts | 7 +- 4 files changed, 455 insertions(+), 14 deletions(-) diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts index f1e03c357..4753b57cf 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts @@ -56,8 +56,6 @@ const B01_TRACE_CLEARANCE = 0.1 const B01_GRID_STEP = 0.05 const B01_LOW_RESOLUTION_CELL_SIZE = 0.2 const MAX_B01_ROUTING_WINDOW_SIZE = 15 -const B01_ROUTING_WINDOW_PADDING = 2 -const MIN_B01_ROUTING_WINDOW_SIZE = 2 const MAX_TERMINAL_VIA_ESCAPE_CANDIDATES = 64 const pointsAreEqual = ( @@ -264,6 +262,41 @@ const simplifyB01Route = ( return simplified } +const normalizeEndpointLayerTransitions = ( + route: HighDensityRoute["route"], +): HighDensityRoute["route"] => { + if (route.length < 2) return route + const normalized = [...route] + const start = normalized[0]! + const next = normalized[1]! + if ( + start.z !== next.z && + Math.hypot(start.x - next.x, start.y - next.y) > SAME_POINT_EPSILON + ) { + normalized.splice(1, 0, { + x: start.x, + y: start.y, + z: next.z, + traceThickness: start.traceThickness, + }) + } + + const end = normalized.at(-1)! + const previous = normalized.at(-2)! + if ( + previous.z !== end.z && + Math.hypot(previous.x - end.x, previous.y - end.y) > SAME_POINT_EPSILON + ) { + normalized.splice(normalized.length - 1, 0, { + x: end.x, + y: end.y, + z: previous.z, + traceThickness: end.traceThickness, + }) + } + return normalized +} + const getB01RoutingWindow = ( startPoint: HighDensityRoute["route"][number], endPoint: HighDensityRoute["route"][number], @@ -288,14 +321,7 @@ const getB01RoutingWindow = ( const boardSize = boardMax - boardMin if (boardSize <= 0) return undefined - const size = Math.min( - boardSize, - MAX_B01_ROUTING_WINDOW_SIZE, - Math.max( - MIN_B01_ROUTING_WINDOW_SIZE, - endpointSpan + B01_ROUTING_WINDOW_PADDING * 2, - ), - ) + const size = Math.min(boardSize, MAX_B01_ROUTING_WINDOW_SIZE) let min = (start + end) / 2 - size / 2 let max = min + size if (min < boardMin) { @@ -848,10 +874,11 @@ export class Pipeline9B01Rerouter { }) solver.MAX_ITERATIONS = Math.max(1, Math.floor(options.maxIterations)) solver.solve() - const [solvedRoute] = solver.getOutput() - if (!solver.solved || !solvedRoute) { + if (!solver.solved) { return { iterations: solver.iterations } } + const [solvedRoute] = solver.getOutput() + if (!solvedRoute) return { iterations: solver.iterations } let reroutedPoints: HighDensityRoute["route"] = solvedRoute.route.map( ({ x, y, z }) => ({ @@ -877,6 +904,7 @@ export class Pipeline9B01Rerouter { } reroutedPoints[0] = { ...startPoint } reroutedPoints[reroutedPoints.length - 1] = { ...endPoint } + reroutedPoints = normalizeEndpointLayerTransitions(reroutedPoints) if (!routeStaysInsideBounds(reroutedPoints, targetRoute, this.srj.bounds)) { return { iterations: solver.iterations } } diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts index 109897b44..b0ca86204 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts @@ -60,6 +60,12 @@ type FixedCopperCompositePlan = { targetErrorIdentity: string } +type AnchoredFixedCopperWindow = { + routes: HighDensityRoute[] + startIndex: number + endIndex: number +} + type ScopedSameNetViaMergeResult = { routes?: HighDensityRoute[] iterations: number @@ -145,6 +151,11 @@ const MAX_POST_FINAL_COMPOSITE_ATTEMPTS = 12 const MAX_POST_FINAL_COMPOSITE_DRC_EVALUATIONS = 12 const MAX_POST_FINAL_COMPOSITE_SAME_NET_VIA_MERGER_ITERATIONS = 32 const MAX_POST_FINAL_COMPOSITE_SAME_NET_VIA_MERGER_ITERATIONS_PER_ATTEMPT = 8 +const ANCHORED_FIXED_COPPER_HALF_SPAN = 7 +const MAX_ANCHORED_FIXED_COPPER_ATTEMPTS = 96 +const MAX_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS = 48 +const MAX_ANCHORED_FIXED_COPPER_ITERATIONS = 480_000 +const MAX_ANCHORED_FIXED_COPPER_ITERATIONS_PER_ATTEMPT = 12_000 const MAX_FIXED_COPPER_COMPOSITE_RESIDUAL = 2 const MAX_FIXED_COPPER_COMPOSITE_PRIMARY_ATTEMPTS = 4 const MAX_FIXED_COPPER_COMPOSITE_FOLLOWUP_ATTEMPTS = 6 @@ -443,6 +454,10 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private postFinalCompositeCandidatesAccepted = 0 private postFinalCompositeIterations = 0 private postFinalCompositeSameNetViaMergeIterations = 0 + private anchoredFixedCopperAttempts = 0 + private anchoredFixedCopperDrcEvaluations = 0 + private anchoredFixedCopperCandidatesAccepted = 0 + private anchoredFixedCopperIterations = 0 private fixedCopperCompositePrimaryAttempts = 0 private fixedCopperCompositeFollowupAttempts = 0 private fixedCopperCompositeDrcEvaluations = 0 @@ -3020,6 +3035,341 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve return improvedRoutes } + private getAnchoredFixedCopperWindow( + routes: HighDensityRoute[], + routeIndex: number, + centers: ReadonlyArray<{ x: number; y: number }>, + ): AnchoredFixedCopperWindow | undefined { + const route = routes[routeIndex] + if (!route || route.route.length < 2 || centers.length === 0) { + return undefined + } + + const cumulativeDistances = [0] + for ( + let segmentIndex = 0; + segmentIndex < route.route.length - 1; + segmentIndex += 1 + ) { + cumulativeDistances.push( + cumulativeDistances[segmentIndex]! + + getPointDistance( + route.route[segmentIndex]!, + route.route[segmentIndex + 1]!, + ), + ) + } + const totalRouteDistance = cumulativeDistances.at(-1) ?? 0 + if (totalRouteDistance <= POSITION_EPSILON) return undefined + + const projectedRouteDistances: number[] = [] + for (const center of centers) { + let nearestSegmentIndex = -1 + let nearestSegmentProjection = 0 + let nearestDistance = Number.POSITIVE_INFINITY + for ( + let segmentIndex = 0; + segmentIndex < route.route.length - 1; + segmentIndex += 1 + ) { + const start = route.route[segmentIndex] + const end = route.route[segmentIndex + 1] + if (!start || !end || start.z !== end.z) continue + const segmentLength = getPointDistance(start, end) + if (segmentLength <= POSITION_EPSILON) continue + const distance = getPointToSegmentDistance(center, start, end) + if (distance < nearestDistance) { + const deltaX = end.x - start.x + const deltaY = end.y - start.y + nearestDistance = distance + nearestSegmentIndex = segmentIndex + nearestSegmentProjection = Math.max( + 0, + Math.min( + 1, + ((center.x - start.x) * deltaX + (center.y - start.y) * deltaY) / + (segmentLength * segmentLength), + ), + ) + } + } + if (nearestSegmentIndex < 0) continue + projectedRouteDistances.push( + cumulativeDistances[nearestSegmentIndex]! + + nearestSegmentProjection * + getPointDistance( + route.route[nearestSegmentIndex]!, + route.route[nearestSegmentIndex + 1]!, + ), + ) + } + if (projectedRouteDistances.length === 0) return undefined + + const startRouteDistance = Math.max( + 0, + Math.min(...projectedRouteDistances) - ANCHORED_FIXED_COPPER_HALF_SPAN, + ) + const endRouteDistance = Math.min( + totalRouteDistance, + Math.max(...projectedRouteDistances) + ANCHORED_FIXED_COPPER_HALF_SPAN, + ) + if (endRouteDistance - startRouteDistance <= POSITION_EPSILON) { + return undefined + } + + const getAnchorAtRouteDistance = ( + routeDistance: number, + ): + | { + segmentIndex: number + point: HighDensityRoute["route"][number] + } + | undefined => { + for ( + let segmentIndex = 0; + segmentIndex < route.route.length - 1; + segmentIndex += 1 + ) { + const segmentStartDistance = cumulativeDistances[segmentIndex]! + const segmentEndDistance = cumulativeDistances[segmentIndex + 1]! + const segmentLength = segmentEndDistance - segmentStartDistance + if ( + segmentLength <= POSITION_EPSILON || + routeDistance > segmentEndDistance + POSITION_EPSILON + ) { + continue + } + const segmentStart = route.route[segmentIndex]! + const segmentEnd = route.route[segmentIndex + 1]! + if (routeDistance <= segmentStartDistance + POSITION_EPSILON) { + return { segmentIndex, point: { ...segmentStart } } + } + if (routeDistance >= segmentEndDistance - POSITION_EPSILON) { + return { segmentIndex, point: { ...segmentEnd } } + } + const fraction = (routeDistance - segmentStartDistance) / segmentLength + return { + segmentIndex, + point: { + x: segmentStart.x + (segmentEnd.x - segmentStart.x) * fraction, + y: segmentStart.y + (segmentEnd.y - segmentStart.y) * fraction, + z: segmentStart.z, + traceThickness: route.traceThickness, + }, + } + } + return undefined + } + const startAnchor = getAnchorAtRouteDistance(startRouteDistance) + const endAnchor = getAnchorAtRouteDistance(endRouteDistance) + if (!startAnchor || !endAnchor) return undefined + + const anchoredPoints: HighDensityRoute["route"] = [] + const pushPoint = (point: HighDensityRoute["route"][number]): number => { + const previousPoint = anchoredPoints.at(-1) + if ( + previousPoint && + previousPoint.z === point.z && + getPointDistance(previousPoint, point) <= POSITION_EPSILON + ) { + anchoredPoints[anchoredPoints.length - 1] = { ...point } + return anchoredPoints.length - 1 + } + anchoredPoints.push({ ...point }) + return anchoredPoints.length - 1 + } + let startIndex = -1 + let endIndex = -1 + for ( + let segmentIndex = 0; + segmentIndex < route.route.length - 1; + segmentIndex += 1 + ) { + pushPoint(route.route[segmentIndex]!) + if (segmentIndex === startAnchor.segmentIndex) { + startIndex = pushPoint(startAnchor.point) + } + if (segmentIndex === endAnchor.segmentIndex) { + endIndex = pushPoint(endAnchor.point) + } + } + pushPoint(route.route.at(-1)!) + if (startIndex < 0 || endIndex < 0 || startIndex >= endIndex) { + return undefined + } + + const anchoredRoutes = cloneRoutes(routes) + anchoredRoutes[routeIndex] = { + ...route, + route: anchoredPoints, + } + return { routes: anchoredRoutes, startIndex, endIndex } + } + + private getRemainingAnchoredFixedCopperIterations(): number { + return Math.max( + 0, + MAX_ANCHORED_FIXED_COPPER_ITERATIONS - this.anchoredFixedCopperIterations, + ) + } + + private tryAnchoredFixedCopperCandidate( + routes: HighDensityRoute[], + snapshot: DrcSnapshot, + routeIndex: number, + centers: ReadonlyArray<{ x: number; y: number }>, + options: { + includeCandidateCopper: boolean + reverse: boolean + }, + ): + | { + routes: HighDensityRoute[] + snapshot: DrcSnapshot + } + | undefined { + const anchoredWindow = this.getAnchoredFixedCopperWindow( + routes, + routeIndex, + centers, + ) + if (!anchoredWindow) return undefined + const iterationLimit = Math.min( + MAX_ANCHORED_FIXED_COPPER_ITERATIONS_PER_ATTEMPT, + this.getRemainingAnchoredFixedCopperIterations(), + ) + if ( + iterationLimit <= 0 || + this.anchoredFixedCopperAttempts >= MAX_ANCHORED_FIXED_COPPER_ATTEMPTS || + this.anchoredFixedCopperDrcEvaluations >= + MAX_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS + ) { + return undefined + } + + this.anchoredFixedCopperAttempts += 1 + const result = this.b01Rerouter.tryReroute(anchoredWindow.routes, { + routeIndex, + startIndex: anchoredWindow.startIndex, + endIndex: anchoredWindow.endIndex, + includeCandidateCopper: options.includeCandidateCopper, + reverse: options.reverse, + shortenPath: false, + maxIterations: iterationLimit, + }) + this.anchoredFixedCopperIterations += Math.min( + iterationLimit, + Math.max(0, result?.iterations ?? 0), + ) + if (!result?.route || !routeHasValidLayerTransitions(result.route)) { + return undefined + } + + const candidateRoutes = cloneRoutes(anchoredWindow.routes) + candidateRoutes[routeIndex] = result.route + const materializedCandidate = materializeRoutes(candidateRoutes) + if ( + materializedCandidate.some( + (candidateRoute) => !routeHasValidLayerTransitions(candidateRoute), + ) || + !this.candidatePreservesTerminals(materializedCandidate) + ) { + return undefined + } + + this.anchoredFixedCopperDrcEvaluations += 1 + this.cleanupCandidateAttempts += 1 + const candidateSnapshot = this.getSnapshot(materializedCandidate) + if (candidateSnapshot.count >= snapshot.count) return undefined + + this.anchoredFixedCopperCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + return { + routes: materializedCandidate, + snapshot: candidateSnapshot, + } + } + + private runAnchoredFixedCopperRepair( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let snapshot = this.getSnapshot(improvedRoutes) + + repairLoop: while ( + snapshot.count > 0 && + this.getRemainingAnchoredFixedCopperIterations() > 0 && + this.anchoredFixedCopperAttempts < MAX_ANCHORED_FIXED_COPPER_ATTEMPTS && + this.anchoredFixedCopperDrcEvaluations < + MAX_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS + ) { + const centersByRouteIndex = new Map< + number, + Array<{ x: number; y: number }> + >() + for (const error of snapshot.errors) { + const center = this.getErrorCenter(error) + if (!center) continue + for (const routeIndex of this.getCandidateRouteIndexesForError( + error, + snapshot, + )) { + const centers = centersByRouteIndex.get(routeIndex) ?? [] + if ( + !centers.some( + (existingCenter) => + getPointDistance(existingCenter, center) <= POSITION_EPSILON, + ) + ) { + centers.push(center) + } + centersByRouteIndex.set(routeIndex, centers) + } + } + + const routeGroups = [...centersByRouteIndex.entries()].toSorted( + (left, right) => right[1].length - left[1].length || left[0] - right[0], + ) + for (const [routeIndex, centers] of routeGroups) { + const centerGroups = + centers.length > 1 + ? [centers, ...centers.map((center) => [center])] + : [centers] + for (const centerGroup of centerGroups) { + for (const includeCandidateCopper of [true, false]) { + for (const reverse of [false, true]) { + const candidate = this.tryAnchoredFixedCopperCandidate( + improvedRoutes, + snapshot, + routeIndex, + centerGroup, + { includeCandidateCopper, reverse }, + ) + if (!candidate) { + if ( + this.getRemainingAnchoredFixedCopperIterations() <= 0 || + this.anchoredFixedCopperAttempts >= + MAX_ANCHORED_FIXED_COPPER_ATTEMPTS || + this.anchoredFixedCopperDrcEvaluations >= + MAX_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS + ) { + break repairLoop + } + continue + } + improvedRoutes = candidate.routes + snapshot = candidate.snapshot + continue repairLoop + } + } + } + } + break + } + + return improvedRoutes + } + private getDrcErrorIdentity(error: DrcError): string { for (const idKey of [ "pcb_trace_error_id", @@ -3971,6 +4321,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve improvedRoutes = this.runPostRepairSameNetViaMerge(improvedRoutes) improvedRoutes = this.runSharedTerminalCompositeRepair(improvedRoutes) improvedRoutes = this.runPostFinalCompositeRepair(improvedRoutes) + improvedRoutes = this.runAnchoredFixedCopperRepair(improvedRoutes) improvedRoutes = this.runFixedCopperCompositeRepair(improvedRoutes) improvedRoutes = this.runFinalEndpointSlideCleanup(improvedRoutes) improvedRoutes = this.runFinalContinuityTerminalViaBridge(improvedRoutes) @@ -4074,6 +4425,19 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.postFinalCompositeSameNetViaMergeIterations, pipeline9PostFinalCompositeSameNetViaMergeIterationLimit: MAX_POST_FINAL_COMPOSITE_SAME_NET_VIA_MERGER_ITERATIONS, + pipeline9AnchoredFixedCopperAttempts: this.anchoredFixedCopperAttempts, + pipeline9AnchoredFixedCopperDrcEvaluations: + this.anchoredFixedCopperDrcEvaluations, + pipeline9AnchoredFixedCopperCandidatesAccepted: + this.anchoredFixedCopperCandidatesAccepted, + pipeline9AnchoredFixedCopperIterations: + this.anchoredFixedCopperIterations, + pipeline9AnchoredFixedCopperAttemptLimit: + MAX_ANCHORED_FIXED_COPPER_ATTEMPTS, + pipeline9AnchoredFixedCopperDrcEvaluationLimit: + MAX_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS, + pipeline9AnchoredFixedCopperIterationLimit: + MAX_ANCHORED_FIXED_COPPER_ITERATIONS, pipeline9FixedCopperCompositePrimaryAttempts: this.fixedCopperCompositePrimaryAttempts, pipeline9FixedCopperCompositeFollowupAttempts: diff --git a/tests/features/pipeline9-b01-rerouter.test.ts b/tests/features/pipeline9-b01-rerouter.test.ts index d670bd6b3..3d094b6b0 100644 --- a/tests/features/pipeline9-b01-rerouter.test.ts +++ b/tests/features/pipeline9-b01-rerouter.test.ts @@ -56,3 +56,49 @@ test("Pipeline9 B01 loads pre-routed traces as fixed route obstacles", () => { expect(result?.route?.route.some((point) => point.z === 1)).toBe(true) expect(result?.iterations).toBeLessThanOrEqual(50_000) }) + +test("Pipeline9 B01 keeps off-grid endpoint layer transitions vertical", () => { + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [], + } + const route: HighDensityRoute = { + connectionName: "off_grid_layer_change", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: -1.013, y: -0.017, z: 0 }, + { x: 1.027, y: 0.019, z: 1 }, + ], + } + const rerouter = new Pipeline9B01Rerouter({ + srj, + baseObstacles: [], + }) + + const result = rerouter.tryReroute([route], { + routeIndex: 0, + includeCandidateCopper: false, + reverse: false, + shortenPath: false, + maxIterations: 50_000, + }) + + expect(result?.route?.route[0]).toEqual(route.route[0]) + expect(result?.route?.route.at(-1)).toEqual(route.route.at(-1)) + expect( + result?.route?.route.every((point, pointIndex, points) => { + const nextPoint = points[pointIndex + 1] + return ( + !nextPoint || + point.z === nextPoint.z || + Math.hypot(point.x - nextPoint.x, point.y - nextPoint.y) <= 1e-9 + ) + }), + ).toBe(true) +}) diff --git a/tests/features/pipeline9-srj23-relaxed-drc.test.ts b/tests/features/pipeline9-srj23-relaxed-drc.test.ts index 617e20beb..27cbb3ffe 100644 --- a/tests/features/pipeline9-srj23-relaxed-drc.test.ts +++ b/tests/features/pipeline9-srj23-relaxed-drc.test.ts @@ -8,7 +8,10 @@ test( async () => { const failures: Array<{ sampleNumber: number; errors: unknown[] }> = [] - for (const sampleNumber of [8, 14, 21, 32, 55, 58, 62, 64, 107]) { + for (const sampleNumber of [ + 8, 14, 21, 23, 27, 29, 32, 51, 55, 57, 58, 62, 64, 68, 72, 74, 83, 94, 97, + 107, + ]) { const { scenario } = await loadScenarioBySampleNumber( "srj23", sampleNumber, @@ -34,5 +37,5 @@ test( expect(failures).toEqual([]) }, - { timeout: 300_000 }, + { timeout: 900_000 }, ) From c88b69dbf30425a48669922e52968a8c1cdd78cd Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 26 Jul 2026 17:54:23 -0700 Subject: [PATCH 09/22] fix: preload Pipeline9 traces onto graph ports --- ...-pipeline-solver9-preloaded-trace-graph.ts | 19 +- .../preloaded-trace-graph-solver.ts | 809 ++++-------------- .../AvailableSegmentPointSolver.ts | 2 + .../buildHyperGraph.ts | 1 + .../hgportpointpathingsolver/types.ts | 1 + .../TinyHypergraphPortPointPathingSolver.ts | 91 ++ lib/utils/convertSrjTracesToObstacles.ts | 6 +- .../pipeline9-preloaded-trace-graph.test.ts | 73 +- ...e9-srj23-sample100-preloaded-graph.test.ts | 54 +- ...ne9-srj23-sample32-preloaded-graph.test.ts | 60 +- ...ed-trace-canonical-net-regressions.test.ts | 33 +- .../preloaded-trace-graph-solver.test.ts | 715 +++------------- 12 files changed, 530 insertions(+), 1334 deletions(-) diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts index 5d070512e..037cf29e4 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts @@ -129,25 +129,20 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends AutoroutingP preprocessStep.solverClass = PreprocessSimpleRouteJsonWithoutTraceObstaclesSolver - const subdivisionStageIndex = pipelineDef.findIndex( - (step) => step.solverName === "nodeDimensionSubdivisionSolver", + const crampedPortStageIndex = pipelineDef.findIndex( + (step) => step.solverName === "necessaryCrampedPortPointSolver", ) - if (subdivisionStageIndex === -1) { - throw new Error("Pipeline9 could not find the node subdivision stage") + if (crampedPortStageIndex === -1) { + throw new Error("Pipeline9 could not find the cramped-port stage") } - pipelineDef.splice(subdivisionStageIndex + 1, 0, { + pipelineDef.splice(crampedPortStageIndex + 1, 0, { solverName: "preloadedTraceGraphSolver", solverClass: PreloadedTraceGraphSolver, getConstructorParams: (pipeline) => [ - pipeline.capacityNodes!, + pipeline.sharedEdgeSegmentsWithNecessaryCrampedPortPoints ?? + pipeline.necessaryCrampedPortPointSolver!.getOutput(), pipeline.originalSrj, ], - onSolved: (pipeline) => { - const pipeline9 = - pipeline as AutoroutingPipelineSolver9_PreloadedTraceGraph - pipeline9.capacityNodes = - pipeline9.preloadedTraceGraphSolver!.getOutput() - }, }) for (const solverName of [ diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts index 7a3f7b4ec..3f15f1ee1 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts @@ -1,164 +1,53 @@ +import { pointToSegmentDistance } from "@tscircuit/math-utils" import { BaseSolver } from "lib/solvers/BaseSolver" -import { PriorityQueue } from "lib/data-structures/PriorityQueue" -import { RbushIndex } from "lib/data-structures/RbushIndex" import type { - CapacityMeshNode, - Obstacle, - SimpleRouteConnection, - SimpleRouteJson, -} from "lib/types" -import { getConnectionPointLayers } from "lib/utils/connection-point-utils" -import { getObstaclesFromSrjTraces } from "lib/utils/convertSrjTracesToObstacles" -import { getUniqueValidZLayersFromLayerNames } from "lib/utils/mapLayerNameToZ" + SharedEdgeSegment, + SegmentPortPoint, +} from "lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver" +import type { SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" +import { getViaDimensions } from "lib/utils/getViaDimensions" +import { JUMPER_DIMENSIONS } from "lib/utils/jumperSizes" +import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" +import { minimumDistanceBetweenSegments } from "lib/utils/minimumDistanceBetweenSegments" import { resolvePreloadedTraceCanonicalNetIds } from "lib/utils/resolvePreloadedTraceCanonicalNetIds" -type PreloadedTraceShape = { +type Point = { x: number; y: number } + +type PreloadedTracePrimitive = { fixedNetId: string + connectionName: string zLayers: number[] - geometry: RotatedRectGeometry - refinementCellDimension: number -} - -type PendingRefinement = { - f: number - node: CapacityMeshNode - candidateShapes: PreloadedTraceShape[] - depth: number - childPath: string + start: Point + end: Point + radius: number } -type RefinementAxis = "x" | "y" - -type RotatedRectGeometry = { - center: { x: number; y: number } - halfWidth: number - halfHeight: number - unitX: { x: number; y: number } - unitY: { x: number; y: number } - bounds: { minX: number; minY: number; maxX: number; maxY: number } -} +type RoutePoint = SimplifiedPcbTrace["route"][number] +type WireRoutePoint = Extract const GEOMETRIC_TOLERANCE = 1e-6 -const REFINEMENT_CELL_FACTOR = 0.5 -const MAX_REFINEMENT_DEPTH = 16 -const BASE_MAX_COMPENSATED_OUTPUT_NODE_COUNT = 3_000 -const MIN_REFINEMENT_WORST_CASE_ALLOWANCE = 2_050 - -const getCanonicalSimpleRouteConnectionName = ( - connection: SimpleRouteConnection, -) => - connection.__netConnectionName ?? - connection.__rootConnectionNames?.[0] ?? - connection.name - -const getRotatedRectGeometry = (obstacle: Obstacle): RotatedRectGeometry => { - const radians = ((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180 - const cos = Math.cos(radians) - const sin = Math.sin(radians) - const halfWidth = obstacle.width / 2 - const halfHeight = obstacle.height / 2 - const extentX = Math.abs(cos) * halfWidth + Math.abs(sin) * halfHeight - const extentY = Math.abs(sin) * halfWidth + Math.abs(cos) * halfHeight - return { - center: obstacle.center, - halfWidth, - halfHeight, - unitX: { x: cos, y: sin }, - unitY: { x: -sin, y: cos }, - bounds: { - minX: obstacle.center.x - extentX, - minY: obstacle.center.y - extentY, - maxX: obstacle.center.x + extentX, - maxY: obstacle.center.y + extentY, - }, - } -} - -const doesRotatedRectContainNode = ( - rect: RotatedRectGeometry, - node: CapacityMeshNode, -): boolean => { - const nodeHalfWidth = node.width / 2 - const nodeHalfHeight = node.height / 2 - const deltaX = node.center.x - rect.center.x - const deltaY = node.center.y - rect.center.y - const projectedOnRectX = Math.abs( - deltaX * rect.unitX.x + deltaY * rect.unitX.y, - ) - const projectedOnRectY = Math.abs( - deltaX * rect.unitY.x + deltaY * rect.unitY.y, - ) - const nodeExtentOnRectX = - nodeHalfWidth * Math.abs(rect.unitX.x) + - nodeHalfHeight * Math.abs(rect.unitX.y) - const nodeExtentOnRectY = - nodeHalfWidth * Math.abs(rect.unitY.x) + - nodeHalfHeight * Math.abs(rect.unitY.y) - - return ( - projectedOnRectX + nodeExtentOnRectX <= - rect.halfWidth + GEOMETRIC_TOLERANCE && - projectedOnRectY + nodeExtentOnRectY <= - rect.halfHeight + GEOMETRIC_TOLERANCE - ) +const getLayersBetween = ( + fromLayer: string, + toLayer: string, + layerCount: number, +): number[] => { + const fromZ = mapLayerNameToZ(fromLayer, layerCount) + const toZ = mapLayerNameToZ(toLayer, layerCount) + const minZ = Math.min(fromZ, toZ) + const maxZ = Math.max(fromZ, toZ) + return Array.from({ length: maxZ - minZ + 1 }, (_, index) => minZ + index) } -const doesRotatedRectIntersectNode = ( - rect: RotatedRectGeometry, - node: CapacityMeshNode, -): boolean => { - const nodeHalfWidth = node.width / 2 - const nodeHalfHeight = node.height / 2 - const nodeMinX = node.center.x - nodeHalfWidth - const nodeMaxX = node.center.x + nodeHalfWidth - const nodeMinY = node.center.y - nodeHalfHeight - const nodeMaxY = node.center.y + nodeHalfHeight - if ( - nodeMaxX < rect.bounds.minX - GEOMETRIC_TOLERANCE || - nodeMinX > rect.bounds.maxX + GEOMETRIC_TOLERANCE || - nodeMaxY < rect.bounds.minY - GEOMETRIC_TOLERANCE || - nodeMinY > rect.bounds.maxY + GEOMETRIC_TOLERANCE - ) { - return false - } - - const deltaX = node.center.x - rect.center.x - const deltaY = node.center.y - rect.center.y - const projectedOnRectX = Math.abs( - deltaX * rect.unitX.x + deltaY * rect.unitX.y, - ) - const projectedOnRectY = Math.abs( - deltaX * rect.unitY.x + deltaY * rect.unitY.y, - ) - const nodeExtentOnRectX = - nodeHalfWidth * Math.abs(rect.unitX.x) + - nodeHalfHeight * Math.abs(rect.unitX.y) - const nodeExtentOnRectY = - nodeHalfWidth * Math.abs(rect.unitY.x) + - nodeHalfHeight * Math.abs(rect.unitY.y) +const isWireRoutePoint = (point: RoutePoint): point is WireRoutePoint => + point.route_type === "wire" - return ( - projectedOnRectX <= - rect.halfWidth + nodeExtentOnRectX + GEOMETRIC_TOLERANCE && - projectedOnRectY <= - rect.halfHeight + nodeExtentOnRectY + GEOMETRIC_TOLERANCE - ) -} - -const getPreloadedTraceShapes = ( +const getPreloadedTracePrimitives = ( srj: SimpleRouteJson, - containmentCompensation = srj.minTraceWidth / 2, - refinementCellFactor = REFINEMENT_CELL_FACTOR, -): PreloadedTraceShape[] => { - const shapes: PreloadedTraceShape[] = [] +): PreloadedTracePrimitive[] => { + const primitives: PreloadedTracePrimitive[] = [] const canonicalNetIdByTraceId = resolvePreloadedTraceCanonicalNetIds(srj) - const requestedKeepoutMargin = srj.defaultObstacleMargin ?? 0.15 - // Nodes are reserved only when the full axis-aligned cell fits inside the - // rotated keepout. Compensate by one candidate trace radius so that this - // containment approximation does not systematically shrink fixed copper. - const projectedKeepoutMargin = - requestedKeepoutMargin + containmentCompensation + const defaultViaDiameter = getViaDimensions(srj).padDiameter for (const trace of srj.traces ?? []) { if (!trace.connection_name) { @@ -166,523 +55,197 @@ const getPreloadedTraceShapes = ( `Preloaded trace "${trace.pcb_trace_id}" is missing a connection name`, ) } - - const obstacles = getObstaclesFromSrjTraces( - { - ...srj, - traces: [trace], - }, - { - includeConnectionNameInConnectedTo: true, - includeSquareCaps: true, - modelJumperPads: true, - }, - ) - for (const obstacle of obstacles) { - const keepoutObstacle = { - ...obstacle, - width: obstacle.width + projectedKeepoutMargin * 2, - height: obstacle.height + projectedKeepoutMargin * 2, - } - const refinementCellDimension = - Math.min( - obstacle.width + requestedKeepoutMargin * 2, - obstacle.height + requestedKeepoutMargin * 2, - ) * refinementCellFactor - const zLayers = getUniqueValidZLayersFromLayerNames( - keepoutObstacle.layers, - srj.layerCount, - ) - if (zLayers.length === 0) { - throw new Error( - `Preloaded trace shape "${obstacle.obstacleId ?? trace.pcb_trace_id}" has no valid board layers`, + const fixedNetId = + canonicalNetIdByTraceId.get(trace.pcb_trace_id) ?? trace.connection_name + + for (const routePoint of trace.route) { + if (routePoint.route_type === "via") { + primitives.push({ + fixedNetId, + connectionName: trace.connection_name, + zLayers: getLayersBetween( + routePoint.from_layer, + routePoint.to_layer, + srj.layerCount, + ), + start: routePoint, + end: routePoint, + radius: (routePoint.via_diameter ?? defaultViaDiameter) / 2, + }) + } else if (routePoint.route_type === "through_obstacle") { + primitives.push({ + fixedNetId, + connectionName: trace.connection_name, + zLayers: getLayersBetween( + routePoint.from_layer, + routePoint.to_layer, + srj.layerCount, + ), + start: routePoint.start, + end: routePoint.end, + radius: routePoint.width / 2, + }) + } else if (routePoint.route_type === "jumper") { + const dimensions = JUMPER_DIMENSIONS[routePoint.footprint] + const padRadius = Math.hypot( + dimensions.padLength / 2, + dimensions.padWidth / 2, ) + const z = mapLayerNameToZ(routePoint.layer, srj.layerCount) + for (const padCenter of [routePoint.start, routePoint.end]) { + primitives.push({ + fixedNetId, + connectionName: trace.connection_name, + zLayers: [z], + start: padCenter, + end: padCenter, + radius: padRadius, + }) + } } - shapes.push({ - fixedNetId: - canonicalNetIdByTraceId.get(trace.pcb_trace_id) ?? - trace.connection_name, - zLayers, - geometry: getRotatedRectGeometry(keepoutObstacle), - refinementCellDimension, + } + + for (let pointIndex = 0; pointIndex < trace.route.length - 1; pointIndex++) { + const start = trace.route[pointIndex]! + const end = trace.route[pointIndex + 1]! + if ( + !isWireRoutePoint(start) || + !isWireRoutePoint(end) || + start.layer !== end.layer + ) { + continue + } + primitives.push({ + fixedNetId, + connectionName: trace.connection_name, + zLayers: [mapLayerNameToZ(start.layer, srj.layerCount)], + start, + end, + radius: Math.max(start.width, end.width) / 2, }) } } - return shapes + return primitives +} + +const getClosestPortPoint = ( + segment: SharedEdgeSegment, + primitive: PreloadedTracePrimitive, + z: number, +): SegmentPortPoint | undefined => + segment.portPoints + .filter((portPoint) => portPoint.availableZ.includes(z)) + .map((portPoint) => ({ + portPoint, + distance: pointToSegmentDistance( + portPoint, + primitive.start, + primitive.end, + ), + })) + .sort( + (left, right) => + left.distance - right.distance || + left.portPoint.distToCentermostPortOnZ - + right.portPoint.distToCentermostPortOnZ || + left.portPoint.segmentPortPointId.localeCompare( + right.portPoint.segmentPortPointId, + ), + )[0]?.portPoint + +const preloadPort = ( + portPoint: SegmentPortPoint, + primitive: PreloadedTracePrimitive, +) => { + const fixedNetIds = [ + ...new Set([ + ...(portPoint._preloadedFixedNetIds ?? []), + primitive.fixedNetId, + ]), + ].sort() + portPoint._preloadedFixedNetIds = fixedNetIds + + if (portPoint.connectionName === null) { + portPoint.connectionName = primitive.connectionName + portPoint.rootConnectionName = primitive.fixedNetId + } } /** - * Projects preloaded copper onto the final capacity mesh. Spatial cells are - * refined until trace keepouts fully contain the affected cells. When a trace - * occupies only some of a cell's layers, the cell is cloned into layer groups - * so the hypergraph reserves only the copper's actual layers. + * Loads fixed copper onto the existing capacity-graph boundary ports. + * + * The capacity regions and their adjacency are intentionally untouched. A + * physical trace crossing is quantized to the closest already-existing port + * on that boundary and layer, then reserved for the trace's canonical net. */ export class PreloadedTraceGraphSolver extends BaseSolver { - private readonly inputNodes: CapacityMeshNode[] - private readonly outputNodes: CapacityMeshNode[] - private readonly traceShapes: PreloadedTraceShape[] - private readonly traceShapeIndex = new RbushIndex() + private readonly primitives: PreloadedTracePrimitive[] constructor( - capacityMeshNodes: CapacityMeshNode[], + private readonly sharedEdgeSegments: SharedEdgeSegment[], private readonly srj: SimpleRouteJson, - private readonly maxCompensatedOutputNodeCount?: number, ) { super() this.MAX_ITERATIONS = 1 - this.inputNodes = capacityMeshNodes.map((node) => ({ - ...node, - availableZ: [...node.availableZ], - _connectedTo: - node._connectedTo === undefined ? undefined : [...node._connectedTo], - _preloadedFixedNetIds: - node._preloadedFixedNetIds === undefined - ? undefined - : [...node._preloadedFixedNetIds], - })) - this.outputNodes = [] - this.traceShapes = getPreloadedTraceShapes(srj) - this.traceShapeIndex.bulkLoad( - this.traceShapes.map((shape) => ({ - item: shape, - ...shape.geometry.bounds, - })), - ) + this.primitives = getPreloadedTracePrimitives(srj) } override getSolverName(): string { return "PreloadedTraceGraphSolver" } - private reserveNodeByTraceLayerConnectivity( - node: CapacityMeshNode, - reservingShapes: PreloadedTraceShape[], - ): CapacityMeshNode[] { - const existingConnections = [...(node._connectedTo ?? [])].sort() - const existingFixedNetIds = [ - ...new Set(node._preloadedFixedNetIds ?? []), - ].sort() - const layerGroups = new Map< - string, - { availableZ: number[]; fixedNetIds: string[] } - >() - for (const z of node.availableZ) { - const fixedNetIds = reservingShapes - .filter((shape) => shape.zLayers.includes(z)) - .map((shape) => shape.fixedNetId) - const combinedFixedNetIds = [ - ...new Set([...existingFixedNetIds, ...fixedNetIds]), - ].sort() - const groupKey = JSON.stringify(combinedFixedNetIds) - const group = layerGroups.get(groupKey) ?? { - availableZ: [], - fixedNetIds: combinedFixedNetIds, - } - group.availableZ.push(z) - layerGroups.set(groupKey, group) - } - - if (layerGroups.size <= 1) { - const group = [...layerGroups.values()][0] - return [ - { - ...node, - availableZ: [...node.availableZ], - _connectedTo: - existingConnections.length === 0 - ? undefined - : [...existingConnections], - _preloadedFixedNetIds: - group && group.fixedNetIds.length > 0 - ? [...group.fixedNetIds] - : undefined, - }, - ] - } - - return [...layerGroups.values()].map(({ availableZ, fixedNetIds }) => ({ - ...node, - capacityMeshNodeId: `${node.capacityMeshNodeId}__preloaded_z${availableZ.join("_")}`, - layer: `z${availableZ.join(",")}`, - availableZ: [...availableZ], - _connectedTo: - existingConnections.length === 0 ? undefined : [...existingConnections], - _preloadedFixedNetIds: - fixedNetIds.length === 0 ? undefined : [...fixedNetIds], - })) - } - - private splitRefinementTask( - task: PendingRefinement, - intersectingShapes: PreloadedTraceShape[], - splitAxis: RefinementAxis, - ): PendingRefinement[] { - const { node, depth, childPath } = task - const splitAlongX = splitAxis === "x" - const childWidth = splitAlongX ? node.width / 2 : node.width - const childHeight = splitAlongX ? node.height : node.height / 2 - const offsetX = splitAlongX ? childWidth / 2 : 0 - const offsetY = splitAlongX ? 0 : childHeight / 2 - const children = [-1, 1].map((direction, index) => ({ - ...node, - capacityMeshNodeId: `${node.capacityMeshNodeId}__preloaded_${childPath}${index}`, - center: { - x: node.center.x + direction * offsetX, - y: node.center.y + direction * offsetY, - }, - width: childWidth, - height: childHeight, - availableZ: [...node.availableZ], - _connectedTo: - node._connectedTo === undefined ? undefined : [...node._connectedTo], - _preloadedFixedNetIds: - node._preloadedFixedNetIds === undefined - ? undefined - : [...node._preloadedFixedNetIds], - })) - - return children.map((child, index) => ({ - f: -(child.width * child.height), - node: child, - candidateShapes: intersectingShapes, - depth: depth + 1, - childPath: `${childPath}${index}`, - })) - } - - private getRefinementSplitAxis( - node: CapacityMeshNode, - intersectingShapes: PreloadedTraceShape[], - ): RefinementAxis { - const getUnresolvedPartialArea = (splitAxis: RefinementAxis) => { - const splitAlongX = splitAxis === "x" - const childWidth = splitAlongX ? node.width / 2 : node.width - const childHeight = splitAlongX ? node.height : node.height / 2 - const offsetX = splitAlongX ? childWidth / 2 : 0 - const offsetY = splitAlongX ? 0 : childHeight / 2 - let unresolvedPartialArea = 0 - - for (const direction of [-1, 1]) { - const child: CapacityMeshNode = { - ...node, - center: { - x: node.center.x + direction * offsetX, - y: node.center.y + direction * offsetY, - }, - width: childWidth, - height: childHeight, - } - for (const shape of intersectingShapes) { - if ( - !doesRotatedRectIntersectNode(shape.geometry, child) || - doesRotatedRectContainNode(shape.geometry, child) - ) { - continue - } - unresolvedPartialArea += childWidth * childHeight + override _step(): void { + for (const primitive of this.primitives) { + for (const segment of this.sharedEdgeSegments) { + if ( + minimumDistanceBetweenSegments( + primitive.start, + primitive.end, + segment.start, + segment.end, + ) > + primitive.radius + GEOMETRIC_TOLERANCE + ) { + continue } - } - - return unresolvedPartialArea - } - const getContainmentBenefit = (splitAxis: RefinementAxis) => { - let benefit = 0 - for (const shape of intersectingShapes) { - const rect = shape.geometry - const deltaX = node.center.x - rect.center.x - const deltaY = node.center.y - rect.center.y - const projectedOnRectX = Math.abs( - deltaX * rect.unitX.x + deltaY * rect.unitX.y, - ) - const projectedOnRectY = Math.abs( - deltaX * rect.unitY.x + deltaY * rect.unitY.y, - ) - const overflowOnRectX = Math.max( - 0, - projectedOnRectX + - (node.width / 2) * Math.abs(rect.unitX.x) + - (node.height / 2) * Math.abs(rect.unitX.y) - - rect.halfWidth, - ) - const overflowOnRectY = Math.max( - 0, - projectedOnRectY + - (node.width / 2) * Math.abs(rect.unitY.x) + - (node.height / 2) * Math.abs(rect.unitY.y) - - rect.halfHeight, - ) - const splitExtentReduction = - splitAxis === "x" ? node.width / 4 : node.height / 4 - const rectXReduction = - splitExtentReduction * - Math.abs(splitAxis === "x" ? rect.unitX.x : rect.unitX.y) - const rectYReduction = - splitExtentReduction * - Math.abs(splitAxis === "x" ? rect.unitY.x : rect.unitY.y) - benefit += - Math.min(overflowOnRectX, rectXReduction) + - Math.min(overflowOnRectY, rectYReduction) - } - return benefit - } - - const xPartialArea = getUnresolvedPartialArea("x") - const yPartialArea = getUnresolvedPartialArea("y") - if (xPartialArea + GEOMETRIC_TOLERANCE < yPartialArea) { - return "x" - } - if (yPartialArea + GEOMETRIC_TOLERANCE < xPartialArea) { - return "y" - } - const xContainmentBenefit = getContainmentBenefit("x") - const yContainmentBenefit = getContainmentBenefit("y") - if (xContainmentBenefit > yContainmentBenefit + GEOMETRIC_TOLERANCE) { - return "x" - } - if (yContainmentBenefit > xContainmentBenefit + GEOMETRIC_TOLERANCE) { - return "y" - } - return node.width >= node.height ? "x" : "y" - } - - private getIndexedTraceShapesForNode( - node: CapacityMeshNode, - ): PreloadedTraceShape[] { - return this.traceShapeIndex.search( - node.center.x - node.width / 2, - node.center.y - node.height / 2, - node.center.x + node.width / 2, - node.center.y + node.height / 2, - ) - } - - private getSemanticNodeReservationShapes( - node: CapacityMeshNode, - intersectingShapes: PreloadedTraceShape[], - containingShapes: PreloadedTraceShape[], - ): PreloadedTraceShape[] { - const containingShapeSet = new Set(containingShapes) - - return intersectingShapes.filter( - (shape) => - containingShapeSet.has(shape) || - this.doesShapeMatchContainedSemanticTarget(node, shape), - ) - } - - private doesShapeMatchContainedSemanticTarget( - node: CapacityMeshNode, - shape: PreloadedTraceShape, - ): boolean { - if (node._targetConnectionName) { - const targetConnection = this.srj.connections.find((connection) => { - const aliases = new Set([ - connection.name, - connection.__netConnectionName, - ...(connection.__rootConnectionNames ?? []), - ]) - return aliases.has(node._targetConnectionName!) - }) - const canonicalTargetNet = targetConnection - ? getCanonicalSimpleRouteConnectionName(targetConnection) - : node._targetConnectionName - return canonicalTargetNet === shape.fixedNetId - } - - const minX = node.center.x - node.width / 2 - GEOMETRIC_TOLERANCE - const maxX = node.center.x + node.width / 2 + GEOMETRIC_TOLERANCE - const minY = node.center.y - node.height / 2 - GEOMETRIC_TOLERANCE - const maxY = node.center.y + node.height / 2 + GEOMETRIC_TOLERANCE - - return this.srj.connections.some( - (connection) => - getCanonicalSimpleRouteConnectionName(connection) === - shape.fixedNetId && - connection.pointsToConnect.some((point) => { - if ( - point.x < minX || - point.x > maxX || - point.y < minY || - point.y > maxY - ) { - return false - } - const pointZLayers = getUniqueValidZLayersFromLayerNames( - getConnectionPointLayers(point), - this.srj.layerCount, - ) - return pointZLayers.some( - (z) => node.availableZ.includes(z) && shape.zLayers.includes(z), - ) - }), - ) - } - - private projectNodesWithBoundedRefinement(): { - refinementBudgetExhausted: boolean - refinementSplitCount: number - refinementWorstCaseOutputNodeCount: number - effectiveMaxOutputNodeCount: number - minimumLayerSplitNodeCount: number - minimumRefinementWorstCaseAllowance: number - } { - const minimumLayerSplitNodeCount = this.inputNodes.reduce( - (count, node) => count + Math.max(1, node.availableZ.length), - 0, - ) - const minimumRefinementWorstCaseAllowance = - this.maxCompensatedOutputNodeCount === undefined - ? MIN_REFINEMENT_WORST_CASE_ALLOWANCE - : 0 - const requestedMaxOutputNodeCount = - this.maxCompensatedOutputNodeCount ?? - Math.max( - BASE_MAX_COMPENSATED_OUTPUT_NODE_COUNT, - minimumLayerSplitNodeCount + minimumRefinementWorstCaseAllowance, - ) - const effectiveMaxOutputNodeCount = Math.max( - requestedMaxOutputNodeCount, - minimumLayerSplitNodeCount, - ) - let refinementWorstCaseOutputNodeCount = minimumLayerSplitNodeCount - let refinementBudgetExhausted = false - let refinementSplitCount = 0 - // Refine the largest unresolved cells first. A hard cap necessarily leaves - // some partial intersections conservatively reserved; prioritizing by area - // prevents input ordering from turning a grazing trace into an arbitrarily - // large blocked region. - const pending = new PriorityQueue( - this.inputNodes.map((node) => ({ - f: -(node.width * node.height), - node, - candidateShapes: this.getIndexedTraceShapesForNode(node), - depth: 0, - childPath: "", - })), - effectiveMaxOutputNodeCount + this.inputNodes.length, - ) - - while (!pending.isEmpty()) { - const task = pending.dequeue()! - const { node, candidateShapes, depth } = task - const intersectingShapes = candidateShapes.filter( - (shape) => - node.availableZ.some((z) => shape.zLayers.includes(z)) && - doesRotatedRectIntersectNode(shape.geometry, node), - ) - if (intersectingShapes.length === 0) { - this.outputNodes.push(node) - continue - } - - const containingShapes = intersectingShapes.filter((shape) => - doesRotatedRectContainNode(shape.geometry, node), - ) - if (containingShapes.length === intersectingShapes.length) { - this.outputNodes.push( - ...this.reserveNodeByTraceLayerConnectivity(node, containingShapes), - ) - continue - } - - const targetCellDimension = Math.min( - ...intersectingShapes.map((shape) => shape.refinementCellDimension), - ) - const splitAxis = this.getRefinementSplitAxis(node, intersectingShapes) - const reachedRefinementFloor = - depth >= MAX_REFINEMENT_DEPTH || - (splitAxis === "x" - ? node.width <= targetCellDimension - : node.height <= targetCellDimension) - const layerSplitCost = Math.max(1, node.availableZ.length) - const canSplitWithinBudget = - refinementWorstCaseOutputNodeCount + layerSplitCost <= - effectiveMaxOutputNodeCount - - if (node._containsTarget || node._isComponentTopologyNode) { - // Target/component nodes are semantic routing regions, not ordinary - // geometric cells. They can intentionally span a large component - // area, so promoting a nearby trace's partial overlap to ownership of - // the whole node can steal an unrelated terminal. Keep full coverage - // and same-net partial coverage; later geometric routing stages still - // receive the exact preloaded trace obstacles. - this.outputNodes.push( - ...this.reserveNodeByTraceLayerConnectivity( - node, - this.getSemanticNodeReservationShapes( - node, - intersectingShapes, - containingShapes, - ), - ), - ) - continue - } - - if (reachedRefinementFloor || !canSplitWithinBudget) { - if (!canSplitWithinBudget && !reachedRefinementFloor) { - refinementBudgetExhausted = true + for (const z of primitive.zLayers) { + if (!segment.availableZ.includes(z)) continue + const portPoint = getClosestPortPoint(segment, primitive, z) + if (portPoint) preloadPort(portPoint, primitive) } - // A partially intersecting cell cannot be left free: at the - // refinement floor (or budget), reserve it conservatively. - this.outputNodes.push( - ...this.reserveNodeByTraceLayerConnectivity(node, intersectingShapes), - ) - continue } - - refinementWorstCaseOutputNodeCount += layerSplitCost - refinementSplitCount++ - for (const childTask of this.splitRefinementTask( - task, - intersectingShapes, - splitAxis, - )) { - pending.enqueue(childTask) - } - } - - return { - refinementBudgetExhausted, - refinementSplitCount, - refinementWorstCaseOutputNodeCount, - effectiveMaxOutputNodeCount, - minimumLayerSplitNodeCount, - minimumRefinementWorstCaseAllowance, } - } - override _step(): void { - const refinementStats = this.projectNodesWithBoundedRefinement() - const projectedNodes = this.outputNodes.filter( - (node) => (node._preloadedFixedNetIds?.length ?? 0) > 0, + const portPoints = this.sharedEdgeSegments.flatMap( + (segment) => segment.portPoints, ) - const traceRegionAssignmentCount = projectedNodes.reduce( - (count, node) => count + (node._preloadedFixedNetIds?.length ?? 0), - 0, + const preloadedPortPoints = portPoints.filter( + (portPoint) => (portPoint._preloadedFixedNetIds?.length ?? 0) > 0, ) - this.stats = { preloadedTraceCount: this.srj.traces?.length ?? 0, - preloadedTraceShapeCount: this.traceShapes.length, - inputNodeCount: this.inputNodes.length, - outputNodeCount: this.outputNodes.length, - refinedNodeCount: this.outputNodes.length - this.inputNodes.length, - projectedNodeCount: projectedNodes.length, - traceRegionAssignmentCount, - usedContainmentCompensation: true, - compensatedOutputNodeCount: this.outputNodes.length, - ...refinementStats, + preloadedTraceShapeCount: this.primitives.length, + inputBoundaryCount: this.sharedEdgeSegments.length, + outputBoundaryCount: this.sharedEdgeSegments.length, + inputPortCount: portPoints.length, + outputPortCount: portPoints.length, + preloadedPortCount: preloadedPortPoints.length, + tracePortAssignmentCount: preloadedPortPoints.reduce( + (count, portPoint) => + count + (portPoint._preloadedFixedNetIds?.length ?? 0), + 0, + ), + topologyChanged: false, } this.solved = true } - getOutput(): CapacityMeshNode[] { + getOutput(): SharedEdgeSegment[] { if (!this.solved) { throw new Error("PreloadedTraceGraphSolver has not solved yet") } - return this.outputNodes + return this.sharedEdgeSegments } } diff --git a/lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver.ts b/lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver.ts index 88ef7a7a1..bd6e7de66 100644 --- a/lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver.ts +++ b/lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver.ts @@ -27,6 +27,8 @@ export interface SegmentPortPoint { cramped: boolean /** Extra tiny-hypergraph traversal cost for fallback ports. */ tinyHypergraphPortPenalty?: number + /** Canonical fixed-net ids loaded onto this existing graph port. */ + _preloadedFixedNetIds?: string[] } export interface SharedEdgeSegment { diff --git a/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/buildHyperGraph.ts b/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/buildHyperGraph.ts index 616e5b0e8..4ed17e37c 100644 --- a/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/buildHyperGraph.ts +++ b/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/buildHyperGraph.ts @@ -215,6 +215,7 @@ export function buildHyperGraph(params: { cramped: spp.cramped, regions: [region1, region2], tinyHypergraphPortPenalty: spp.tinyHypergraphPortPenalty, + _preloadedFixedNetIds: spp._preloadedFixedNetIds, } const hgPort: RegionPortHg = { portId: spp.segmentPortPointId, diff --git a/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/types.ts b/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/types.ts index 597c13955..e20e1a60e 100644 --- a/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/types.ts +++ b/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/types.ts @@ -23,6 +23,7 @@ export type RawPort = { cramped?: boolean regions: RegionHg[] tinyHypergraphPortPenalty?: number + _preloadedFixedNetIds?: string[] } export type RegionPortHg = Omit & { diff --git a/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts b/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts index 4bec9097e..682f4d494 100644 --- a/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts +++ b/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts @@ -79,6 +79,7 @@ type TinyPortMetadata = { _tinyTerminal?: boolean tinyHypergraphPortPenalty?: number duplicatedFromPortId?: string + _preloadedFixedNetIds?: string[] } type LoadedTinyGraph = { @@ -271,6 +272,7 @@ const toSerializedPortData = ( distToCentermostPortOnZ: port.d.distToCentermostPortOnZ, tinyHypergraphPortPenalty: port.d.tinyHypergraphPortPenalty, cramped: port.d.cramped, + _preloadedFixedNetIds: port.d._preloadedFixedNetIds, } } @@ -627,11 +629,91 @@ const applyMetadataPortPenalties = (loaded: LoadedTinyGraph) => { return metadataPortPenaltyCount } +const applyPreloadedPortReservations = ( + solver: TinyHyperGraphSolver & LoadedTinyGraph, +) => { + const netIndexByAlias = new Map() + let nextFixedNetIndex = 0 + + for ( + let routeIndex = 0; + routeIndex < solver.problem.routeNet.length; + routeIndex++ + ) { + const routeNetIndex = solver.problem.routeNet[routeIndex]! + nextFixedNetIndex = Math.max(nextFixedNetIndex, routeNetIndex + 1) + const routeMetadata = solver.problem.routeMetadata?.[routeIndex] + const simpleRouteConnection = routeMetadata?.simpleRouteConnection + for (const alias of [ + routeMetadata?.connectionId, + routeMetadata?.mutuallyConnectedNetworkId, + simpleRouteConnection?.name, + simpleRouteConnection?.__netConnectionName, + ...(simpleRouteConnection?.__rootConnectionNames ?? []), + ]) { + if (typeof alias === "string" && alias.length > 0) { + netIndexByAlias.set(alias, routeNetIndex) + } + } + } + for (const regionNetIndex of solver.problem.regionNetId) { + if (regionNetIndex >= 0) { + nextFixedNetIndex = Math.max(nextFixedNetIndex, regionNetIndex + 1) + } + } + + const fixedNetIndexById = new Map() + const getFixedNetIndex = (fixedNetId: string) => { + const activeNetIndex = netIndexByAlias.get(fixedNetId) + if (activeNetIndex !== undefined) return activeNetIndex + + let fixedNetIndex = fixedNetIndexById.get(fixedNetId) + if (fixedNetIndex === undefined) { + fixedNetIndex = nextFixedNetIndex++ + fixedNetIndexById.set(fixedNetId, fixedNetIndex) + } + return fixedNetIndex + } + + let preloadedPortCount = 0 + const problemSetup = solver.problemSetup + for (let portId = 0; portId < solver.topology.portCount; portId++) { + const fixedNetIds = + solver.topology.portMetadata?.[portId]?._preloadedFixedNetIds + if (!Array.isArray(fixedNetIds) || fixedNetIds.length === 0) continue + + preloadedPortCount++ + const reservedNetIds = [ + ...new Set(fixedNetIds.map(getFixedNetIndex)), + ].sort((left, right) => left - right) + const endpointNetIds = problemSetup.portEndpointNetIds[portId]! + for (const reservedNetId of reservedNetIds) { + endpointNetIds.add(reservedNetId) + } + + const existingReservation = + problemSetup.portEndpointReservationNetId[portId]! + const combinedReservations = new Set(reservedNetIds) + if (existingReservation >= 0) { + combinedReservations.add(existingReservation) + } else if (existingReservation === -2) { + combinedReservations.add(-2) + } + problemSetup.portEndpointReservationNetId[portId] = + combinedReservations.size === 1 + ? [...combinedReservations][0]! + : -2 + } + + return preloadedPortCount +} + class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSectionPipelineSolver { private configuredSolvers = new WeakSet() duplicatePortPenaltyCount = 0 metadataPortPenaltyCount = 0 crampedPortPenaltyCount = 0 + preloadedPortCount = 0 readonly crampedPortTraversalPenalty: number readonly useSelectiveReripRouting: boolean @@ -739,6 +821,14 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect applyMetadataPortPenalties(loadedSolver) applyTerminalRegionNetIds(loadedSolver) } + if (solver instanceof TinyHyperGraphSolver) { + this.preloadedPortCount = Math.max( + this.preloadedPortCount, + applyPreloadedPortReservations( + solver as TinyHyperGraphSolver & LoadedTinyGraph, + ), + ) + } this.configuredSolvers.add(solver) } @@ -932,6 +1022,7 @@ export class TinyHypergraphPortPointPathingSolver extends BaseSolver { this.tinyPipelineSolver.metadataPortPenaltyCount, crampedPortPenalty: this.tinyPipelineSolver.crampedPortTraversalPenalty, crampedPortPenaltyCount: this.tinyPipelineSolver.crampedPortPenaltyCount, + preloadedPortCount: this.tinyPipelineSolver.preloadedPortCount, duplicateCongestedPortError: this.duplicateCongestedPortError, ...(this.tinyPipelineSolver.stats ?? {}), ...(currentTinySolver?.stats ?? {}), diff --git a/lib/utils/convertSrjTracesToObstacles.ts b/lib/utils/convertSrjTracesToObstacles.ts index 0acc2c501..61dacfa6f 100644 --- a/lib/utils/convertSrjTracesToObstacles.ts +++ b/lib/utils/convertSrjTracesToObstacles.ts @@ -81,9 +81,9 @@ const createSegmentObstacle = ({ x: (start.x + end.x) / 2, y: (start.y + end.y) / 2, }, - // Pipeline9 uses square-cap rectangles so the projected hypergraph - // reservation includes one trace radius beyond each route point. Other - // pipelines retain their legacy centerline obstacle geometry. + // Pipeline9 uses square-cap rectangles for downstream physical-clearance + // and DRC stages. Hypergraph preloading is port-based and does not consume + // these rectangles or alter the capacity topology. width: length + (includeSquareCaps ? Math.max(width, MIN_OBSTACLE_DIMENSION) : 0), diff --git a/tests/features/pipeline9-preloaded-trace-graph.test.ts b/tests/features/pipeline9-preloaded-trace-graph.test.ts index b05cf3068..b2f79ff35 100644 --- a/tests/features/pipeline9-preloaded-trace-graph.test.ts +++ b/tests/features/pipeline9-preloaded-trace-graph.test.ts @@ -5,7 +5,7 @@ import scenario from "./preexisting-connected-traces/srj/preexisting-connected-t type: "json", } -test("Pipeline9 projects preloaded copper into hypergraph regions without topology obstacles", () => { +test("Pipeline9 loads preexisting copper into ports without changing capacity topology", () => { const srj = structuredClone(scenario) as SimpleRouteJson const preloadedTrace = srj.traces?.[0] if (!preloadedTrace) { @@ -17,6 +17,20 @@ test("Pipeline9 projects preloaded copper into hypergraph regions without topolo maxNodeDimension: 3, effort: 0.5, }) + const traceFreeSolver = new AutoroutingPipelineSolver9_PreloadedTraceGraph( + { ...structuredClone(srj), traces: undefined }, + { + targetMinCapacity: 0.75, + maxNodeDimension: 3, + effort: 0.5, + }, + ) + while ( + !traceFreeSolver.failed && + !traceFreeSolver.preloadedTraceGraphSolver?.solved + ) { + traceFreeSolver.step() + } solver.solve() const preprocessedSrj = @@ -30,11 +44,64 @@ test("Pipeline9 projects preloaded copper into hypergraph regions without topolo expect(solver.preloadedTraceGraphSolver?.stats).toMatchObject({ preloadedTraceCount: 1, preloadedTraceShapeCount: 1, + topologyChanged: false, }) + const getCapacityTopology = ( + pipeline: AutoroutingPipelineSolver9_PreloadedTraceGraph, + ) => + pipeline.capacityNodes?.map((node) => ({ + capacityMeshNodeId: node.capacityMeshNodeId, + center: node.center, + width: node.width, + height: node.height, + layer: node.layer, + availableZ: node.availableZ, + adjacentNodeIds: node._adjacentNodeIds, + })) + const getPortTopology = ( + pipeline: AutoroutingPipelineSolver9_PreloadedTraceGraph, + ) => + pipeline.preloadedTraceGraphSolver?.getOutput().map((segment) => ({ + edgeId: segment.edgeId, + nodeIds: segment.nodeIds, + start: segment.start, + end: segment.end, + availableZ: segment.availableZ, + ports: segment.portPoints.map((portPoint) => ({ + segmentPortPointId: portPoint.segmentPortPointId, + x: portPoint.x, + y: portPoint.y, + availableZ: portPoint.availableZ, + nodeIds: portPoint.nodeIds, + edgeId: portPoint.edgeId, + distToCentermostPortOnZ: portPoint.distToCentermostPortOnZ, + cramped: portPoint.cramped, + })), + })) + expect(getCapacityTopology(solver)).toEqual( + getCapacityTopology(traceFreeSolver), + ) + expect(getPortTopology(solver)).toEqual(getPortTopology(traceFreeSolver)) + expect( + solver.preloadedTraceGraphSolver + ?.getOutput() + .flatMap((segment) => segment.portPoints) + .some((portPoint) => + portPoint._preloadedFixedNetIds?.includes(srj.connections[0]!.name), + ), + ).toBe(true) + expect( + Number(solver.portPointPathingSolver?.stats.preloadedPortCount), + ).toBeGreaterThan(0) expect( solver.capacityNodes?.some((node) => - node._preloadedFixedNetIds?.includes(srj.connections[0]!.name), + node.capacityMeshNodeId.includes("__preloaded_"), ), - ).toBe(true) + ).toBe(false) + expect( + solver.capacityNodes?.some( + (node) => (node._preloadedFixedNetIds?.length ?? 0) > 0, + ), + ).toBe(false) expect(solver.getOutputSimplifiedPcbTraces()).toHaveLength(1) }) diff --git a/tests/features/pipeline9-srj23-sample100-preloaded-graph.test.ts b/tests/features/pipeline9-srj23-sample100-preloaded-graph.test.ts index 27c02467b..61a74b629 100644 --- a/tests/features/pipeline9-srj23-sample100-preloaded-graph.test.ts +++ b/tests/features/pipeline9-srj23-sample100-preloaded-graph.test.ts @@ -3,36 +3,28 @@ import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib" import { evaluateRelaxedDrc } from "lib/testing/evaluate-relaxed-drc" import { loadScenarioBySampleNumber } from "../../scripts/benchmark/scenarios" -test( - "Pipeline9 completes srj23 sample 100 with the bounded preloaded graph", - async () => { - const { scenario } = await loadScenarioBySampleNumber("srj23", 100) - const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph( - scenario, - { - cacheProvider: null, - effort: 1, - }, - ) +test("Pipeline9 completes srj23 sample 100 with preloaded ports", async () => { + const { scenario } = await loadScenarioBySampleNumber("srj23", 100) + const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph(scenario, { + cacheProvider: null, + effort: 1, + }) - solver.solve() + solver.solve() - expect(solver.failed).toBe(false) - expect(solver.solved).toBe(true) - expect(solver.preloadedTraceGraphSolver?.stats).toMatchObject({ - refinementBudgetExhausted: true, - effectiveMaxOutputNodeCount: 3_000, - }) - expect( - solver.preloadedTraceGraphSolver?.stats.outputNodeCount, - ).toBeLessThan(3_000) - expect( - evaluateRelaxedDrc({ - inputSrj: scenario, - srjWithPointPairs: solver.srjWithPointPairs!, - traces: solver.getOutputSimplifiedPcbTraces(), - }).errors, - ).toEqual([]) - }, - { timeout: 30_000 }, -) + expect(solver.failed).toBe(false) + expect(solver.solved).toBe(true) + expect(solver.preloadedTraceGraphSolver?.stats).toMatchObject({ + topologyChanged: false, + inputBoundaryCount: + solver.preloadedTraceGraphSolver?.stats.outputBoundaryCount, + inputPortCount: solver.preloadedTraceGraphSolver?.stats.outputPortCount, + }) + expect( + evaluateRelaxedDrc({ + inputSrj: scenario, + srjWithPointPairs: solver.srjWithPointPairs!, + traces: solver.getOutputSimplifiedPcbTraces(), + }).errors, + ).toEqual([]) +}) diff --git a/tests/features/pipeline9-srj23-sample32-preloaded-graph.test.ts b/tests/features/pipeline9-srj23-sample32-preloaded-graph.test.ts index 12eac5fd1..8dfc3f22a 100644 --- a/tests/features/pipeline9-srj23-sample32-preloaded-graph.test.ts +++ b/tests/features/pipeline9-srj23-sample32-preloaded-graph.test.ts @@ -2,39 +2,31 @@ import { expect, test } from "bun:test" import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib" import { loadScenarioBySampleNumber } from "../../scripts/benchmark/scenarios" -test( - "Pipeline9 gives dense srj23 inputs a baseline-relative refinement budget", - async () => { - const { scenario } = await loadScenarioBySampleNumber("srj23", 32) - const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph( - scenario, - { - cacheProvider: null, - effort: 1, - }, - ) +test("Pipeline9 preserves the srj23 sample 32 capacity topology", async () => { + const { scenario } = await loadScenarioBySampleNumber("srj23", 32) + const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph(scenario, { + cacheProvider: null, + effort: 1, + }) - while (!solver.failed && !solver.portPointPathingSolver?.solved) { - solver.step() - } + while (!solver.failed && !solver.portPointPathingSolver?.solved) { + solver.step() + } - expect(solver.failed).toBe(false) - expect(solver.portPointPathingSolver?.solved).toBe(true) - const stats = solver.preloadedTraceGraphSolver?.stats - if (!stats) { - throw new Error("Expected Pipeline9 preloaded graph stats") - } - expect(stats.minimumRefinementWorstCaseAllowance).toBe(2_050) - expect(stats.effectiveMaxOutputNodeCount).toBe( - Math.max( - 3_000, - stats.minimumLayerSplitNodeCount + - stats.minimumRefinementWorstCaseAllowance, - ), - ) - expect(stats.outputNodeCount).toBeLessThan( - stats.effectiveMaxOutputNodeCount, - ) - }, - { timeout: 15_000 }, -) + expect(solver.failed).toBe(false) + expect(solver.portPointPathingSolver?.solved).toBe(true) + expect(solver.preloadedTraceGraphSolver?.stats).toMatchObject({ + topologyChanged: false, + inputBoundaryCount: + solver.preloadedTraceGraphSolver?.stats.outputBoundaryCount, + inputPortCount: solver.preloadedTraceGraphSolver?.stats.outputPortCount, + }) + expect( + solver.capacityNodes?.some((node) => + node.capacityMeshNodeId.includes("__preloaded_"), + ), + ).toBe(false) + expect(solver.capacityNodes).toEqual( + solver.nodeDimensionSubdivisionSolver?.outputNodes ?? null, + ) +}) diff --git a/tests/features/preloaded-trace-canonical-net-regressions.test.ts b/tests/features/preloaded-trace-canonical-net-regressions.test.ts index 39622b5af..0f50971f9 100644 --- a/tests/features/preloaded-trace-canonical-net-regressions.test.ts +++ b/tests/features/preloaded-trace-canonical-net-regressions.test.ts @@ -6,7 +6,6 @@ import { } from "lib/testing/evaluate-relaxed-drc" import { convertToCircuitJson } from "lib/testing/utils/convertToCircuitJson" import type { - CapacityMeshNode, SimpleRouteJson, SimplifiedPcbTrace, } from "lib/types" @@ -133,24 +132,38 @@ test("shared resolver propagates obstacle-only evidence through an A-MID-C trace }) }) -test("preloaded graph assigns the canonical net to a middle-only cell", () => { - const middleOnlyNode: CapacityMeshNode = { - capacityMeshNodeId: "middle-only", - center: { x: 0, y: 0 }, - width: 0.02, - height: 0.02, - layer: "top", +test("preloaded graph assigns the canonical net to a middle trace port", () => { + const middlePort = { + segmentPortPointId: "middle-port", + x: 0, + y: 0, availableZ: [0], + nodeIds: ["left", "right"] as [string, string], + edgeId: "middle-edge", + connectionName: null, + distToCentermostPortOnZ: 0, + cramped: false, } const solver = new PreloadedTraceGraphSolver( - [middleOnlyNode], + [ + { + edgeId: "middle-edge", + nodeIds: ["left", "right"], + start: { x: 0, y: -1 }, + end: { x: 0, y: 1 }, + availableZ: [0], + portPoints: [middlePort], + }, + ], makePreloadedTraceChain(), ) solver.solve() expect(solver.getOutput()).toHaveLength(1) - expect(solver.getOutput()[0]?._preloadedFixedNetIds).toEqual(["root-net"]) + expect( + solver.getOutput()[0]?.portPoints[0]?._preloadedFixedNetIds, + ).toEqual(["root-net"]) }) test("relaxed DRC namespaces trace links before canonical conversion", () => { diff --git a/tests/features/preloaded-trace-graph-solver.test.ts b/tests/features/preloaded-trace-graph-solver.test.ts index 74fa5277f..a1e300a1c 100644 --- a/tests/features/preloaded-trace-graph-solver.test.ts +++ b/tests/features/preloaded-trace-graph-solver.test.ts @@ -1,649 +1,128 @@ import { expect, test } from "bun:test" import { PreloadedTraceGraphSolver } from "lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver" -import type { CapacityMeshNode, SimpleRouteJson } from "lib/types" +import type { + SegmentPortPoint, + SharedEdgeSegment, +} from "lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver" +import type { SimpleRouteJson } from "lib/types" + +const createPort = ( + id: string, + y: number, + z: number, +): SegmentPortPoint => ({ + segmentPortPointId: id, + x: 0, + y, + availableZ: [z], + nodeIds: ["left", "right"], + edgeId: "shared-edge", + connectionName: null, + distToCentermostPortOnZ: Math.abs(y), + cramped: false, +}) -test("preloaded trace projection refines narrow single-layer regions", () => { - const capacityMeshNodes: CapacityMeshNode[] = [ - { - capacityMeshNodeId: "diagonal-top", - center: { x: 0, y: 0 }, - width: 0.05, - height: 0.05, - layer: "top", - availableZ: [0], - }, +test("preloaded traces reserve existing graph ports without changing topology", () => { + const sharedEdgeSegments: SharedEdgeSegment[] = [ { - capacityMeshNodeId: "coarse-diagonal-top", - center: { x: 0, y: 0 }, - width: 0.2, - height: 0.2, - layer: "top", - availableZ: [0], - }, - { - capacityMeshNodeId: "multilayer-diagonal", - center: { x: 0, y: 0 }, - width: 0.05, - height: 0.05, - layer: "z0,1", + edgeId: "shared-edge", + nodeIds: ["left", "right"], + start: { x: 0, y: -1 }, + end: { x: 0, y: 1 }, availableZ: [0, 1], - }, - { - capacityMeshNodeId: "candidate-radius-compensation-top", - center: { x: 0, y: 0.08 }, - width: 0.02, - height: 0.02, - layer: "top", - availableZ: [0], - }, - { - capacityMeshNodeId: "off-diagonal-top", - center: { x: 0, y: 1 }, - width: 0.2, - height: 0.2, - layer: "top", - availableZ: [0], - }, - { - capacityMeshNodeId: "via-bottom", - center: { x: 2, y: 2 }, - width: 0.2, - height: 0.2, - layer: "bottom", - availableZ: [1], - }, - { - capacityMeshNodeId: "wire-bottom", - center: { x: 0, y: 2 }, - width: 0.05, - height: 0.05, - layer: "bottom", - availableZ: [1], - }, - { - capacityMeshNodeId: "unrelated-bottom", - center: { x: 0, y: 0 }, - width: 0.2, - height: 0.2, - layer: "bottom", - availableZ: [1], + portPoints: [ + createPort("top-low", -0.5, 0), + createPort("top-mid", 0, 0), + createPort("top-high", 0.5, 0), + createPort("bottom-high", 0.5, 1), + ], }, ] + const topologyBefore = sharedEdgeSegments.map((segment) => ({ + edgeId: segment.edgeId, + nodeIds: [...segment.nodeIds], + start: { ...segment.start }, + end: { ...segment.end }, + availableZ: [...segment.availableZ], + ports: segment.portPoints.map((portPoint) => ({ + id: portPoint.segmentPortPointId, + x: portPoint.x, + y: portPoint.y, + z: [...portPoint.availableZ], + })), + })) const srj: SimpleRouteJson = { layerCount: 2, minTraceWidth: 0.1, - defaultObstacleMargin: 0, - minViaPadDiameter: 0.6, - minViaHoleDiameter: 0.3, - bounds: { minX: -3, minY: -3, maxX: 3, maxY: 3 }, - obstacles: [], - connections: [], - traces: [ - { - type: "pcb_trace", - pcb_trace_id: "preloaded-diagonal", - connection_name: "net1", - route: [ - { - route_type: "wire", - x: -2, - y: -2, - width: 0.1, - layer: "top", - }, - { - route_type: "wire", - x: 2, - y: 2, - width: 0.1, - layer: "top", - }, - { - route_type: "via", - x: 2, - y: 2, - from_layer: "top", - to_layer: "bottom", - via_diameter: 0.6, - via_hole_diameter: 0.3, - }, - { - route_type: "wire", - x: 2, - y: 2, - width: 0.1, - layer: "bottom", - }, - { - route_type: "wire", - x: -2, - y: 2, - width: 0.1, - layer: "bottom", - }, - ], - }, - ], - } - - const solver = new PreloadedTraceGraphSolver(capacityMeshNodes, srj) - solver.solve() - const connectedNodeIds = solver - .getOutput() - .filter((node) => node._preloadedFixedNetIds?.includes("net1")) - .map((node) => node.capacityMeshNodeId) - - expect(connectedNodeIds).toContain("diagonal-top") - expect( - connectedNodeIds.some((nodeId) => nodeId.startsWith("via-bottom")), - ).toBe(true) - expect(connectedNodeIds).toContain("wire-bottom") - expect( - connectedNodeIds.some((nodeId) => - nodeId.startsWith("coarse-diagonal-top__preloaded_"), - ), - ).toBe(true) - expect( - connectedNodeIds.some( - (nodeId) => - nodeId.startsWith("off-diagonal-top") || - nodeId.startsWith("unrelated-bottom"), - ), - ).toBe(false) - const multilayerTraceNodes = solver - .getOutput() - .filter((node) => node.capacityMeshNodeId.startsWith("multilayer-diagonal")) - expect(multilayerTraceNodes).toHaveLength(2) - expect( - multilayerTraceNodes.find((node) => node.availableZ.includes(0)) - ?._preloadedFixedNetIds, - ).toContain("net1") - expect( - multilayerTraceNodes.find((node) => node.availableZ.includes(1)) - ?._preloadedFixedNetIds ?? [], - ).not.toContain("net1") - expect(connectedNodeIds).toContain("candidate-radius-compensation-top") - const reservedCoarseChildren = solver - .getOutput() - .filter( - (node) => - node.capacityMeshNodeId.startsWith("coarse-diagonal-top__preloaded_") && - node._preloadedFixedNetIds?.includes("net1"), - ) - expect(reservedCoarseChildren.length).toBeGreaterThan(0) - expect( - reservedCoarseChildren.every( - (node) => node.width <= 0.1 && node.height <= 0.1, - ), - ).toBe(true) - expect(solver.stats).toMatchObject({ - preloadedTraceShapeCount: 3, - inputNodeCount: capacityMeshNodes.length, - usedContainmentCompensation: true, - }) - expect(solver.stats.outputNodeCount).toBeGreaterThan(capacityMeshNodes.length) - - const boundedSolver = new PreloadedTraceGraphSolver(capacityMeshNodes, srj, 1) - boundedSolver.solve() - expect(boundedSolver.stats).toMatchObject({ - usedContainmentCompensation: true, - refinementBudgetExhausted: true, - }) - expect( - boundedSolver - .getOutput() - .find( - (node) => - node.capacityMeshNodeId === "candidate-radius-compensation-top", - )?._preloadedFixedNetIds ?? [], - ).toContain("net1") - expect(boundedSolver.stats.outputNodeCount).toBeLessThanOrEqual( - boundedSolver.stats.effectiveMaxOutputNodeCount, - ) -}) - -test("preloaded trace projection conservatively reserves boundary cells", () => { - const srj: SimpleRouteJson = { - layerCount: 2, - minTraceWidth: 0.1, - defaultObstacleMargin: 0.15, - bounds: { minX: -3, minY: -3, maxX: 3, maxY: 3 }, - obstacles: [], - connections: [], - traces: [ - { - type: "pcb_trace", - pcb_trace_id: "fixed-diagonal", - connection_name: "fixed-net", - route: [ - { - route_type: "wire", - x: -2, - y: -2, - width: 0.1, - layer: "top", - }, - { - route_type: "wire", - x: 2, - y: 2, - width: 0.1, - layer: "top", - }, - ], - }, - ], - } - const solver = new PreloadedTraceGraphSolver( - [ - { - capacityMeshNodeId: "coarse", - center: { x: 0, y: 0 }, - width: 4, - height: 4, - layer: "top", - availableZ: [0], - }, - ], - srj, - ) - - solver.solve() - - const nodesInsideRequiredClearance = solver - .getOutput() - .filter( - (node) => - Math.abs(node.center.y - node.center.x) / Math.SQRT2 < 0.2 - 1e-9 && - Math.abs(node.center.x) < 1.8 && - Math.abs(node.center.y) < 1.8, - ) - expect(nodesInsideRequiredClearance.length).toBeGreaterThan(0) - expect( - nodesInsideRequiredClearance.every((node) => - node._preloadedFixedNetIds?.includes("fixed-net"), - ), - ).toBe(true) -}) - -test("preloaded trace projection reserves square-cap segment endpoints", () => { - const srj: SimpleRouteJson = { - layerCount: 2, - minTraceWidth: 0.1, - defaultObstacleMargin: 0, - bounds: { minX: -1, minY: -1, maxX: 2, maxY: 1 }, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, obstacles: [], - connections: [], - traces: [ + connections: [ { - type: "pcb_trace", - pcb_trace_id: "fixed-horizontal", - connection_name: "fixed-net", - route: [ - { - route_type: "wire", - x: 0, - y: 0, - width: 0.1, - layer: "top", - }, - { - route_type: "wire", - x: 1, - y: 0, - width: 0.1, - layer: "top", - }, + name: "child-net", + __rootConnectionNames: ["root-net"], + pointsToConnect: [ + { x: -1, y: 0.42, layer: "top" }, + { x: 1, y: 0.42, layer: "top" }, ], }, ], - } - const solver = new PreloadedTraceGraphSolver( - [ - { - capacityMeshNodeId: "inside-endcap-clearance", - center: { x: 1.08, y: 0 }, - width: 0.01, - height: 0.01, - layer: "top", - availableZ: [0], - }, - { - capacityMeshNodeId: "outside-endcap-clearance", - center: { x: 1.12, y: 0 }, - width: 0.01, - height: 0.01, - layer: "top", - availableZ: [0], - }, - ], - srj, - ) - - solver.solve() - - expect( - solver - .getOutput() - .find((node) => node.capacityMeshNodeId === "inside-endcap-clearance") - ?._preloadedFixedNetIds, - ).toEqual(["fixed-net"]) - expect( - solver - .getOutput() - .find((node) => node.capacityMeshNodeId === "outside-endcap-clearance") - ?._preloadedFixedNetIds, - ).toBeUndefined() -}) - -test("axial preloaded traces refine into long conservative strips", () => { - const srj: SimpleRouteJson = { - layerCount: 2, - minTraceWidth: 0.1, - defaultObstacleMargin: 0.15, - bounds: { minX: -3, minY: -3, maxX: 3, maxY: 3 }, - obstacles: [], - connections: [], traces: [ { type: "pcb_trace", - pcb_trace_id: "fixed-horizontal", - connection_name: "fixed-net", + pcb_trace_id: "fixed-trace", + connection_name: "child-net", route: [ - { - route_type: "wire", - x: -2, - y: 0, - width: 0.1, - layer: "top", - }, - { - route_type: "wire", - x: 2, - y: 0, - width: 0.1, - layer: "top", - }, + { route_type: "wire", x: -1, y: 0.42, width: 0.1, layer: "top" }, + { route_type: "wire", x: 1, y: 0.42, width: 0.1, layer: "top" }, ], }, ], } - const solver = new PreloadedTraceGraphSolver( - [ - { - capacityMeshNodeId: "coarse", - center: { x: 0, y: 0 }, - width: 4, - height: 4, - layer: "top", - availableZ: [0], - }, - ], - srj, - ) + const solver = new PreloadedTraceGraphSolver(sharedEdgeSegments, srj) solver.solve() - const reservedNodes = solver - .getOutput() - .filter((node) => node._preloadedFixedNetIds?.includes("fixed-net")) - expect(reservedNodes.length).toBeGreaterThan(0) - expect( - reservedNodes.some((node) => node.width === 4 && node.height <= 0.25), - ).toBe(true) - expect(solver.stats.refinementBudgetExhausted).toBe(false) - expect(solver.stats.outputNodeCount).toBeLessThan(20) -}) - -test("separate same-net layer shapes keep one canonical fixed reservation", () => { - const createLayerTrace = (pcbTraceId: string, layer: "top" | "bottom") => ({ - type: "pcb_trace" as const, - pcb_trace_id: pcbTraceId, - connection_name: "shared-child", - route: [ - { - route_type: "wire" as const, - x: -1, - y: 0, - width: 0.1, - layer, - }, - { - route_type: "wire" as const, - x: 1, - y: 0, - width: 0.1, - layer, - }, - ], - }) - const srj: SimpleRouteJson = { - layerCount: 2, - minTraceWidth: 0.1, - bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, - obstacles: [], - connections: [ - { - name: "shared-child", - __rootConnectionNames: ["shared-root"], - pointsToConnect: [ - { x: -1, y: 0, layer: "top" }, - { x: 1, y: 0, layer: "bottom" }, - ], - }, - ], - traces: [ - createLayerTrace("fixed-top", "top"), - createLayerTrace("fixed-bottom", "bottom"), - ], - } - const solver = new PreloadedTraceGraphSolver( - [ - { - capacityMeshNodeId: "both-layers", - center: { x: 0, y: 0 }, - width: 0.02, - height: 0.02, - layer: "z0,1", - availableZ: [0, 1], - }, - ], - srj, - ) - - solver.solve() - - expect(solver.getOutput()).toEqual([ + const topologyAfter = solver.getOutput().map((segment) => ({ + edgeId: segment.edgeId, + nodeIds: [...segment.nodeIds], + start: { ...segment.start }, + end: { ...segment.end }, + availableZ: [...segment.availableZ], + ports: segment.portPoints.map((portPoint) => ({ + id: portPoint.segmentPortPointId, + x: portPoint.x, + y: portPoint.y, + z: [...portPoint.availableZ], + })), + })) + expect(topologyAfter).toEqual(topologyBefore) + expect(sharedEdgeSegments[0]!.portPoints).toEqual([ expect.objectContaining({ - capacityMeshNodeId: "both-layers", - availableZ: [0, 1], - _preloadedFixedNetIds: ["shared-root"], + segmentPortPointId: "top-low", + connectionName: null, + }), + expect.objectContaining({ + segmentPortPointId: "top-mid", + connectionName: null, + }), + expect.objectContaining({ + segmentPortPointId: "top-high", + connectionName: "child-net", + rootConnectionName: "root-net", + _preloadedFixedNetIds: ["root-net"], + }), + expect.objectContaining({ + segmentPortPointId: "bottom-high", + connectionName: null, }), ]) -}) - -test("semantic target nodes ignore unrelated partial trace ownership", () => { - const createTrace = ( - pcbTraceId: string, - connectionName: string, - route: Array<{ x: number; y: number }>, - ) => ({ - type: "pcb_trace" as const, - pcb_trace_id: pcbTraceId, - connection_name: connectionName, - route: route.map(({ x, y }) => ({ - route_type: "wire" as const, - x, - y, - width: 0.1, - layer: "top" as const, - })), + expect(solver.stats).toMatchObject({ + inputBoundaryCount: 1, + outputBoundaryCount: 1, + inputPortCount: 4, + outputPortCount: 4, + preloadedPortCount: 1, + tracePortAssignmentCount: 1, + topologyChanged: false, }) - const srj: SimpleRouteJson = { - layerCount: 2, - minTraceWidth: 0.1, - defaultObstacleMargin: 0.15, - bounds: { minX: -5, minY: -5, maxX: 5, maxY: 5 }, - obstacles: [], - connections: [ - { - name: "active-net", - pointsToConnect: [ - { x: 0, y: 0, layer: "top" }, - { x: 4, y: 4, layer: "top" }, - ], - }, - { - name: "fixed-other", - pointsToConnect: [ - { x: 0.5, y: 0, layer: "top" }, - { x: 4, y: 0, layer: "top" }, - ], - }, - ], - traces: [ - createTrace("same-net-partial", "active-net", [ - { x: -1, y: 0.85 }, - { x: 1, y: 0.85 }, - ]), - createTrace("other-net-partial", "fixed-other", [ - { x: -1, y: 0.85 }, - { x: 1, y: 0.85 }, - ]), - createTrace("other-net-full", "fixed-other", [ - { x: 2, y: 0 }, - { x: 4, y: 0 }, - ]), - ], - } - const solver = new PreloadedTraceGraphSolver( - [ - { - capacityMeshNodeId: "coarse-semantic-target", - center: { x: 0, y: 0 }, - width: 1.5, - height: 1.5, - layer: "top", - availableZ: [0], - _containsTarget: true, - _targetConnectionName: "active-net", - _connectedTo: ["active-net", "fixed-other"], - }, - { - capacityMeshNodeId: "fully-covered-semantic-target", - center: { x: 3, y: 0 }, - width: 0.02, - height: 0.02, - layer: "top", - availableZ: [0], - _containsTarget: true, - _connectedTo: ["active-net"], - }, - ], - srj, - ) - - solver.solve() - - expect( - solver - .getOutput() - .find((node) => node.capacityMeshNodeId === "coarse-semantic-target") - ?._preloadedFixedNetIds, - ).toEqual(["active-net"]) - expect( - solver - .getOutput() - .find( - (node) => node.capacityMeshNodeId === "fully-covered-semantic-target", - )?._preloadedFixedNetIds, - ).toEqual(["fixed-other"]) -}) - -test("bounded refinement prioritizes large partial cells independent of input order", () => { - const srj: SimpleRouteJson = { - layerCount: 2, - minTraceWidth: 0.1, - defaultObstacleMargin: 0, - bounds: { minX: -1, minY: -5, maxX: 15, maxY: 5 }, - obstacles: [], - connections: [], - traces: [ - { - type: "pcb_trace", - pcb_trace_id: "small-cell-trace", - connection_name: "small-net", - route: [ - { - route_type: "wire", - x: -0.5, - y: 0, - width: 0.1, - layer: "top", - }, - { - route_type: "wire", - x: 0.5, - y: 0, - width: 0.1, - layer: "top", - }, - ], - }, - { - type: "pcb_trace", - pcb_trace_id: "large-cell-trace", - connection_name: "large-net", - route: [ - { - route_type: "wire", - x: 6.1, - y: -3, - width: 0.1, - layer: "top", - }, - { - route_type: "wire", - x: 6.1, - y: 3, - width: 0.1, - layer: "top", - }, - ], - }, - ], - } - const solver = new PreloadedTraceGraphSolver( - [ - { - capacityMeshNodeId: "small-first", - center: { x: 0, y: 0 }, - width: 1, - height: 1, - layer: "top", - availableZ: [0], - }, - { - capacityMeshNodeId: "large-second", - center: { x: 10, y: 0 }, - width: 8, - height: 8, - layer: "top", - availableZ: [0], - }, - ], - srj, - 3, - ) - - solver.solve() - - const largestReservedArea = Math.max( - ...solver - .getOutput() - .filter((node) => node._preloadedFixedNetIds?.length) - .map((node) => node.width * node.height), - ) - expect(solver.stats.refinementBudgetExhausted).toBe(true) - expect(largestReservedArea).toBeLessThanOrEqual(32) }) From 83441a153f068709ab472492a1240a0f9e667cf5 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 26 Jul 2026 17:59:37 -0700 Subject: [PATCH 10/22] fix: keep duplicated graph ports unassigned --- .../TinyHypergraphPortPointPathingSolver.ts | 6 + .../preloaded-port-duplication.test.ts | 137 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 tests/features/preloaded-port-duplication.test.ts diff --git a/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts b/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts index 682f4d494..e28c48596 100644 --- a/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts +++ b/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts @@ -945,6 +945,12 @@ export class TinyHypergraphPortPointPathingSolver extends BaseSolver { } else { this.duplicateCongestedPortReport = duplicateCongestedPortSolver.report graphForTiny = duplicateCongestedPortSolver.getOutput() + for (const port of graphForTiny.ports) { + const metadata = port.d as TinyPortMetadata + if (typeof metadata.duplicatedFromPortId === "string") { + delete metadata._preloadedFixedNetIds + } + } } } else { this.duplicateCongestedPortError = `Skipped for ${connections.length} connections` diff --git a/tests/features/preloaded-port-duplication.test.ts b/tests/features/preloaded-port-duplication.test.ts new file mode 100644 index 000000000..2e1293fe3 --- /dev/null +++ b/tests/features/preloaded-port-duplication.test.ts @@ -0,0 +1,137 @@ +import { expect, test } from "bun:test" +import { ConnectivityMap } from "circuit-json-to-connectivity-map" +import { buildHyperGraph } from "lib/solvers/PortPointPathingSolver/hgportpointpathingsolver" +import { TinyHypergraphPortPointPathingSolver } from "lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver" +import type { + CapacityMeshNode, + SimpleRouteConnection, +} from "lib/types" + +test("congestion duplicates do not inherit preloaded port ownership", () => { + const capacityMeshNodes: CapacityMeshNode[] = [ + { + capacityMeshNodeId: "left", + center: { x: -2, y: 0 }, + width: 2, + height: 2, + layer: "top", + availableZ: [0], + }, + { + capacityMeshNodeId: "middle", + center: { x: 0, y: 0 }, + width: 2, + height: 2, + layer: "top", + availableZ: [0], + }, + { + capacityMeshNodeId: "right", + center: { x: 2, y: 0 }, + width: 2, + height: 2, + layer: "top", + availableZ: [0], + }, + ] + const simpleRouteJsonConnections: SimpleRouteConnection[] = [ + { + name: "fixed-route", + __rootConnectionNames: ["fixed-root"], + pointsToConnect: [ + { x: -2, y: 0.2, layer: "top" }, + { x: 2, y: 0.2, layer: "top" }, + ], + }, + { + name: "foreign-route", + __rootConnectionNames: ["foreign-root"], + pointsToConnect: [ + { x: -2, y: -0.2, layer: "top" }, + { x: 2, y: -0.2, layer: "top" }, + ], + }, + ] + const connectivityMap = new ConnectivityMap({}) + connectivityMap.addConnections([["fixed-route", "fixed-root"]]) + connectivityMap.addConnections([["foreign-route", "foreign-root"]]) + const { graph, connections } = buildHyperGraph({ + capacityMeshNodes, + segmentPortPoints: [ + { + segmentPortPointId: "left-middle", + x: -1, + y: 0, + availableZ: [0], + nodeIds: ["left", "middle"], + edgeId: "left-middle-edge", + connectionName: "fixed-route", + rootConnectionName: "fixed-root", + distToCentermostPortOnZ: 0, + cramped: false, + _preloadedFixedNetIds: ["fixed-root"], + }, + { + segmentPortPointId: "middle-right", + x: 1, + y: 0, + availableZ: [0], + nodeIds: ["middle", "right"], + edgeId: "middle-right-edge", + connectionName: null, + distToCentermostPortOnZ: 0, + cramped: false, + }, + ], + layerCount: 1, + connectivityMap, + simpleRouteJsonConnections, + }) + const solver = new TinyHypergraphPortPointPathingSolver({ + graph, + connections, + layerCount: 1, + effort: 0.1, + flags: { + FORCE_CENTER_FIRST: true, + RIPPING_ENABLED: true, + USE_SELECTIVE_RERIP_ROUTING: true, + }, + weights: { + SHUFFLE_SEED: 0, + MEMORY_PF_FACTOR: 4, + CENTER_OFFSET_DIST_PENALTY_FACTOR: 0, + CENTER_OFFSET_FOCUS_SHIFT: 0, + NODE_PF_FACTOR: 0, + LAYER_CHANGE_COST: 0, + RIPPING_PF_COST: 0, + NODE_PF_MAX_PENALTY: 100, + BASE_CANDIDATE_COST: 0.6, + MAX_ITERATIONS_PER_PATH: 0, + RANDOM_WALK_DISTANCE: 0, + START_RIPPING_PF_THRESHOLD: 0.3, + END_RIPPING_PF_THRESHOLD: 1, + MAX_RIPS: 1000, + RANDOM_RIP_FRACTION: 0.3, + STRAIGHT_LINE_DEVIATION_PENALTY_FACTOR: 4, + GREEDY_MULTIPLIER: 0.7, + MIN_ALLOWED_BOARD_SCORE: -10000, + }, + }) + const serializedGraph = (solver as any).tinyPipelineSolver.inputProblem + .serializedHyperGraph + const sourcePort = serializedGraph.ports.find( + (port: any) => port.portId === "left-middle::0", + ) + const duplicatePorts = serializedGraph.ports.filter( + (port: any) => port.d?.duplicatedFromPortId === "left-middle::0", + ) + + expect(sourcePort?.d?._preloadedFixedNetIds).toEqual(["fixed-root"]) + expect(duplicatePorts.length).toBeGreaterThan(0) + expect( + duplicatePorts.every( + (port: any) => port.d?._preloadedFixedNetIds === undefined, + ), + ).toBe(true) +}) From a0be70079dccd2fc6450a15e7b07ff5ba6f85c6a Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 26 Jul 2026 20:30:22 -0700 Subject: [PATCH 11/22] fix: preload Pipeline9 fixed graph occupancy --- ...-pipeline-solver9-preloaded-trace-graph.ts | 15 +- .../pipeline9-exact-drc-repair-solver.ts | 758 ++++++++++++++++-- .../preloaded-trace-graph-solver.ts | 90 ++- .../AvailableSegmentPointSolver.ts | 11 + .../buildHyperGraph.ts | 20 +- .../hgportpointpathingsolver/types.ts | 2 + .../TinyHypergraphPortPointPathingSolver.ts | 311 ++++++- lib/testing/evaluate-relaxed-drc.ts | 36 +- ...ne9-srj23-sample32-preloaded-graph.test.ts | 4 +- .../preloaded-fixed-segment-occupancy.test.ts | 151 ++++ .../preloaded-port-duplication.test.ts | 19 +- .../preloaded-trace-graph-solver.test.ts | 10 + 12 files changed, 1320 insertions(+), 107 deletions(-) create mode 100644 tests/features/preloaded-fixed-segment-occupancy.test.ts diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts index 037cf29e4..ed7cb4c86 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts @@ -63,14 +63,13 @@ const createPadCenteredDrcEvaluator = ( const physicalObstacleById = new Map() for (const obstacle of originalObstacles) { - const physicalIds = [ - obstacle.connectedTo.find((id) => id.startsWith("pcb_smtpad_")), - obstacle.connectedTo.find((id) => id.startsWith("pcb_plated_hole_")), - ] - for (const physicalId of physicalIds) { - if (physicalId && !physicalObstacleById.has(physicalId)) { - physicalObstacleById.set(physicalId, obstacle) - } + const physicalId = obstacle.connectedTo[0] + if ( + physicalId && + (physicalId.startsWith("pcb_smtpad_") || + physicalId.startsWith("pcb_plated_hole_")) + ) { + physicalObstacleById.set(physicalId, obstacle) } } const addAccuratePadCenters = ( diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts index b0ca86204..f87c39797 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts @@ -103,9 +103,14 @@ const PRELOADED_TERMINAL_MATCH_TOLERANCE = 1e-3 const ENDPOINT_SLIDE_RADII = [ 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.2, 0.25, 0.3, ] as const -const VIA_MICRO_SHIFT_RADII = ENDPOINT_SLIDE_RADII +const VIA_MICRO_SHIFT_RADII = [ + ...ENDPOINT_SLIDE_RADII, + 0.4, + 0.5, + 0.75, +] as const const MAX_VIA_GROUPS_PER_ROUTE = 3 -const MAX_VIA_MICRO_SHIFT_DRC_EVALUATIONS_PER_SWEEP = 32 +const MAX_VIA_MICRO_SHIFT_DRC_EVALUATIONS_PER_SWEEP = 128 const MAX_VIA_MICRO_SHIFT_SNAPSHOT_ISSUES = 8 const BATCHED_TRACE_FORCE_SCALES = [4, 2, 1, 0.5, 0.25] as const const MAX_BATCHED_TRACE_FORCE_PASSES = 20 @@ -131,7 +136,7 @@ const MAX_ERROR_OWNED_CLUSTER_ROUTE_ITERATIONS = 15_000 const MAX_ERROR_OWNED_CLUSTER_PASSES = 2 const MAX_ERROR_OWNED_CLUSTER_TERMINAL_ESCAPE_CANDIDATES = 4 const MAX_ERROR_OWNED_CLUSTER_TERMINAL_ESCAPE_ITERATIONS = 10_000 -const MAX_POST_CLUSTER_VIA_MICRO_SHIFT_DRC_EVALUATIONS = 32 +const MAX_POST_CLUSTER_VIA_MICRO_SHIFT_DRC_EVALUATIONS = 128 const MAX_FINAL_OWNER_B01_ITERATIONS = 50_000 const MAX_FINAL_OWNER_FULL_ROUTE_ITERATIONS = 25_000 const MAX_FINAL_OWNER_LONG_ROUTE_ITERATIONS = 4_000 @@ -167,6 +172,8 @@ const MAX_FIXED_COPPER_COMPOSITE_FOLLOWUP_OWNERS = 2 const MAX_FINAL_ENDPOINT_SLIDE_DRC_EVALUATIONS = 32 const MAX_FINAL_CONTINUITY_TERMINAL_VIA_ATTEMPTS = 32 const MAX_FINAL_CONTINUITY_TERMINAL_VIA_DRC_EVALUATIONS = 8 +const MAX_EARLY_FIXED_OVERLAP_LAYER_DETOUR_DRC_EVALUATIONS = 128 +const MAX_FINAL_FIXED_OVERLAP_LAYER_DETOUR_DRC_EVALUATIONS = 64 const POST_FINAL_COMPOSITE_INTERIOR_EXPANSIONS = [4, 8] as const const POST_FINAL_COMPOSITE_TERMINAL_PROXIMITY = 4 const FULL_B01_VARIANTS = [ @@ -265,9 +272,7 @@ const obstacleSharesRouteNet = ( const obstacleRepresentsPhysicalPad = ( obstacle: Obstacle, padId: string, -): boolean => - obstacle.connectedTo.find((id) => id.startsWith("pcb_smtpad_")) === padId || - obstacle.connectedTo.find((id) => id.startsWith("pcb_plated_hole_")) === padId +): boolean => obstacle.connectedTo[0] === padId const getOtherTraceId = ( error: DrcError, @@ -353,6 +358,31 @@ const routeHasValidLayerTransitions = (route: HighDensityRoute): boolean => ) }) +const normalizePipeline9ViaMetadataFromLayerTransitions = ( + routes: HighDensityRoute[], +): HighDensityRoute[] => + materializeRoutes(routes).map((route) => { + const seenViaLocations = new Set() + const vias = route.route.flatMap((point, pointIndex) => { + const nextPoint = route.route[pointIndex + 1] + if ( + !nextPoint || + point.z === nextPoint.z || + point.x !== nextPoint.x || + point.y !== nextPoint.y + ) { + return [] + } + + const locationKey = `${point.x}:${point.y}` + if (seenViaLocations.has(locationKey)) return [] + seenViaLocations.add(locationKey) + return [{ x: point.x, y: point.y }] + }) + + return { ...route, vias } + }) + const getPointToSegmentDistance = ( point: { x: number; y: number }, start: { x: number; y: number }, @@ -402,6 +432,7 @@ const getCoincidentTerminalPointIndexes = ( export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolver { private readonly originalObstacles: Obstacle[] + private readonly initialHdRoutes: HighDensityRoute[] private readonly terminalConstraints: TerminalConstraint[] private readonly b01Rerouter: Pipeline9B01Rerouter private cleanupStarted = false @@ -470,16 +501,30 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private finalContinuityTerminalViaAttempts = 0 private finalContinuityTerminalViaDrcEvaluations = 0 private finalContinuityTerminalViaCandidatesAccepted = 0 + private finalFixedOverlapLayerDetourDrcEvaluations = 0 + private finalFixedOverlapLayerDetourCandidatesAccepted = 0 + private finalFixedOverlapBestRemovedTargetIssueCount = + Number.POSITIVE_INFINITY + private finalFixedOverlapBestRemovedTargetFixedScore = + Number.POSITIVE_INFINITY constructor(params: Pipeline9ExactDrcRepairSolverParams) { - super(params) + const normalizedParams = { + ...params, + hdRoutes: normalizePipeline9ViaMetadataFromLayerTransitions( + params.hdRoutes, + ), + } + super(normalizedParams) + this.initialHdRoutes = cloneRoutes(normalizedParams.hdRoutes) this.originalObstacles = params.originalObstacles this.b01Rerouter = new Pipeline9B01Rerouter({ srj: params.srj, baseObstacles: params.b01BaseObstacles, connMap: params.connMap, }) - this.terminalConstraints = params.hdRoutes.flatMap((route, routeIndex) => + this.terminalConstraints = normalizedParams.hdRoutes.flatMap( + (route, routeIndex) => (["start", "end"] as const).flatMap((endpoint) => { const point = endpoint === "start" ? route.route[0] : route.route.at(-1) if (!point) return [] @@ -513,6 +558,71 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve ) } + private isFixedCopperIssue( + error: DrcError, + snapshot: DrcSnapshot, + ): boolean { + const errorType = getErrorType(error) + const primaryTraceId = + typeof error.pcb_trace_id === "string" + ? error.pcb_trace_id + : undefined + const otherTraceId = getRawOtherTraceId(error) + const primaryIsCandidate = Boolean( + primaryTraceId && snapshot.traceRouteIndexById.has(primaryTraceId), + ) + if ( + errorType === "pcb_via_trace_clearance_error" && + primaryTraceId && + !primaryIsCandidate + ) { + return true + } + if ( + errorType !== "pcb_trace_error" || + !primaryTraceId || + !otherTraceId + ) { + return false + } + const otherIsCandidate = snapshot.traceRouteIndexById.has(otherTraceId) + return primaryIsCandidate !== otherIsCandidate + } + + private getFixedCopperIssueCount(snapshot: DrcSnapshot): number { + return snapshot.errors.filter((error) => + this.isFixedCopperIssue(error, snapshot), + ).length + } + + private getFixedCopperIssueScore(snapshot: DrcSnapshot): number { + return snapshot.errors.reduce((score, error) => { + if (!this.isFixedCopperIssue(error, snapshot)) return score + return score + (getErrorType(error) === "pcb_trace_error" ? 2 : 1) + }, 0) + } + + private getFixedCopperTraceOverlapCount(snapshot: DrcSnapshot): number { + return snapshot.errors.filter( + (error) => + getErrorType(error) === "pcb_trace_error" && + this.isFixedCopperIssue(error, snapshot), + ).length + } + + private snapshotImprovesWithoutFixedCopperRegression( + candidate: DrcSnapshot, + baseline: DrcSnapshot, + ): boolean { + return ( + candidate.count < baseline.count && + this.getFixedCopperTraceOverlapCount(candidate) <= + this.getFixedCopperTraceOverlapCount(baseline) && + this.getFixedCopperIssueScore(candidate) <= + this.getFixedCopperIssueScore(baseline) + ) + } + private unlockCleanupTerminals( routes: HighDensityRoute[], ): HighDensityRoute[] { @@ -583,7 +693,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private candidateImprovesSnapshot( candidateRoutes: HighDensityRoute[], - currentIssueCount: number, + currentSnapshot: DrcSnapshot, source: "local" | "b01" = "local", ): boolean { this.cleanupCandidateAttempts += 1 @@ -598,7 +708,12 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve if (source === "local") this.localCleanupDrcEvaluations += 1 const candidateSnapshot = this.getSnapshot(candidateRoutes) - if (candidateSnapshot.count >= currentIssueCount) { + if ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + currentSnapshot, + ) + ) { if (source === "local") { this.consecutiveLocalCleanupDrcMisses += 1 this.maxConsecutiveLocalCleanupDrcMisses = Math.max( @@ -817,7 +932,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve if ( this.candidateImprovesSnapshot( materializedCandidate, - snapshot.count, + snapshot, ) ) { this.viaMicroShiftsAccepted += 1 @@ -967,7 +1082,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve if ( this.candidateImprovesSnapshot( materializedCandidate, - snapshot.count, + snapshot, ) ) { return materializedCandidate @@ -982,6 +1097,10 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private tryLocalTraceLayerDetour( routes: HighDensityRoute[], error: DrcError, + options: { + preferFixedCopperIssueReduction?: boolean + maxIssueCountIncrease?: number + } = {}, ): HighDensityRoute[] | undefined { if (!this.hasLocalCleanupBudget()) return undefined const errorType = getErrorType(error) @@ -1010,6 +1129,15 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve snapshot.traceRouteIndexById.has(traceId), ) const errorCenter = center as { x: number; y: number } + const baselineFixedCopperIssueScore = + this.getFixedCopperIssueScore(snapshot) + const targetErrorIdentity = this.getDrcErrorIdentity(error) + let bestFixedCopperCandidate: + | { + routes: HighDensityRoute[] + snapshot: DrcSnapshot + } + | undefined for (const traceId of traceIds) { const routeIndex = snapshot.traceRouteIndexById.get(traceId)! @@ -1103,10 +1231,72 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve ) const materializedCandidate = materializeRoutes(candidateRoutes) + if (options.preferFixedCopperIssueReduction) { + this.cleanupCandidateAttempts += 1 + if (!this.candidatePreservesTerminals(materializedCandidate)) { + continue + } + if (!this.hasLocalCleanupBudget()) { + return bestFixedCopperCandidate?.routes + } + this.localCleanupDrcEvaluations += 1 + const candidateSnapshot = this.getSnapshot(materializedCandidate) + const candidateFixedCopperIssueScore = + this.getFixedCopperIssueScore(candidateSnapshot) + const targetErrorWasRemoved = !candidateSnapshot.errors.some( + (candidateError) => + this.getDrcErrorIdentity(candidateError) === + targetErrorIdentity, + ) + if (targetErrorWasRemoved) { + this.finalFixedOverlapBestRemovedTargetIssueCount = Math.min( + this.finalFixedOverlapBestRemovedTargetIssueCount, + candidateSnapshot.count, + ) + this.finalFixedOverlapBestRemovedTargetFixedScore = Math.min( + this.finalFixedOverlapBestRemovedTargetFixedScore, + candidateFixedCopperIssueScore, + ) + } + const isEligible = + (candidateFixedCopperIssueScore < + baselineFixedCopperIssueScore || + (candidateFixedCopperIssueScore === + baselineFixedCopperIssueScore && + targetErrorWasRemoved)) && + candidateSnapshot.count <= + snapshot.count + (options.maxIssueCountIncrease ?? 2) + const isBetter = + isEligible && + (!bestFixedCopperCandidate || + candidateFixedCopperIssueScore < + this.getFixedCopperIssueScore( + bestFixedCopperCandidate.snapshot, + ) || + (candidateFixedCopperIssueScore === + this.getFixedCopperIssueScore( + bestFixedCopperCandidate.snapshot, + ) && + candidateSnapshot.count < + bestFixedCopperCandidate.snapshot.count)) + if (isBetter) { + bestFixedCopperCandidate = { + routes: materializedCandidate, + snapshot: candidateSnapshot, + } + } else { + this.consecutiveLocalCleanupDrcMisses += 1 + this.maxConsecutiveLocalCleanupDrcMisses = Math.max( + this.maxConsecutiveLocalCleanupDrcMisses, + this.consecutiveLocalCleanupDrcMisses, + ) + } + continue + } if ( this.candidateImprovesSnapshot( materializedCandidate, - snapshot.count, + snapshot, ) ) { return materializedCandidate @@ -1115,6 +1305,11 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve } } + if (bestFixedCopperCandidate) { + this.cleanupCandidatesAccepted += 1 + this.consecutiveLocalCleanupDrcMisses = 0 + return bestFixedCopperCandidate.routes + } return undefined } @@ -1158,7 +1353,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve if ( this.candidateImprovesSnapshot( materializedCandidate, - snapshot.count, + snapshot, ) ) { return materializedCandidate @@ -1200,7 +1395,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve if ( !this.candidateImprovesSnapshot( materializedCandidate, - snapshot.count, + snapshot, "b01", ) ) { @@ -1348,13 +1543,47 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve nearestSegmentIndex = segmentIndex } } - if (nearestSegmentIndex < 1) return [] + if (nearestSegmentIndex < 0) return [] + const lastRouteIndex = route.route.length - 1 const lastInteriorIndex = route.route.length - 2 - if (nearestSegmentIndex + 1 > lastInteriorIndex) return [] const windows: Array<{ startIndex: number; endIndex: number }> = [] const seenWindows = new Set() + const addRawWindow = (startIndex: number, endIndex: number) => { + if ( + startIndex < 0 || + endIndex > lastRouteIndex || + startIndex >= endIndex + ) { + return + } + const key = `${startIndex}:${endIndex}` + if (seenWindows.has(key)) return + seenWindows.add(key) + windows.push({ startIndex, endIndex }) + } + if (nearestSegmentIndex === 0) { + for (const endIndex of [2, 4, 8]) { + addRawWindow(0, Math.min(lastRouteIndex, endIndex)) + } + } + if (nearestSegmentIndex === lastRouteIndex - 1) { + for (const startIndex of [ + lastRouteIndex - 2, + lastRouteIndex - 4, + lastRouteIndex - 8, + ]) { + addRawWindow(Math.max(0, startIndex), lastRouteIndex) + } + } + if ( + nearestSegmentIndex < 1 || + nearestSegmentIndex + 1 > lastInteriorIndex + ) { + return windows + } + const addWindow = (startIndex: number, endIndex: number) => { const boundedStart = Math.max(1, startIndex) const boundedEnd = Math.min(lastInteriorIndex, endIndex) @@ -1365,10 +1594,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve ) { return } - const key = `${boundedStart}:${boundedEnd}` - if (seenWindows.has(key)) return - seenWindows.add(key) - windows.push({ startIndex: boundedStart, endIndex: boundedEnd }) + addRawWindow(boundedStart, boundedEnd) } addWindow( @@ -1788,7 +2014,14 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.errorOwnedClusterDrcEvaluations += 1 this.cleanupCandidateAttempts += 1 const candidateSnapshot = this.getSnapshot(materializedCandidate) - if (candidateSnapshot.count >= baselineSnapshot.count) continue + if ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + baselineSnapshot, + ) + ) { + continue + } let acceptedRoutes = materializedCandidate let acceptedSnapshot = candidateSnapshot @@ -1838,7 +2071,14 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve const postCandidateSnapshot = this.getSnapshot( materializedPostCandidate, ) - if (postCandidateSnapshot.count >= acceptedSnapshot.count) continue + if ( + !this.snapshotImprovesWithoutFixedCopperRegression( + postCandidateSnapshot, + acceptedSnapshot, + ) + ) { + continue + } acceptedRoutes = materializedPostCandidate acceptedSnapshot = postCandidateSnapshot @@ -2008,7 +2248,14 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.finalOwnerDrcEvaluations += 1 this.cleanupCandidateAttempts += 1 const candidateSnapshot = this.getSnapshot(materializedCandidate) - if (candidateSnapshot.count >= snapshot.count) return undefined + if ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + snapshot, + ) + ) { + return undefined + } this.finalOwnerCandidatesAccepted += 1 this.cleanupCandidatesAccepted += 1 @@ -2177,27 +2424,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private normalizeViaMetadataFromLayerTransitions( routes: HighDensityRoute[], ): HighDensityRoute[] { - return materializeRoutes(routes).map((route) => { - const seenViaLocations = new Set() - const vias = route.route.flatMap((point, pointIndex) => { - const nextPoint = route.route[pointIndex + 1] - if ( - !nextPoint || - point.z === nextPoint.z || - point.x !== nextPoint.x || - point.y !== nextPoint.y - ) { - return [] - } - - const locationKey = `${point.x}:${point.y}` - if (seenViaLocations.has(locationKey)) return [] - seenViaLocations.add(locationKey) - return [{ x: point.x, y: point.y }] - }) - - return { ...route, vias } - }) + return normalizePipeline9ViaMetadataFromLayerTransitions(routes) } private getCanonicalNetForRoute(route: HighDensityRoute): string | undefined { @@ -2336,7 +2563,14 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.postRepairSameNetViaMergeDrcEvaluations += 1 this.cleanupCandidateAttempts += 1 const candidateSnapshot = this.getSnapshot(materializedCandidate) - if (candidateSnapshot.count >= baselineSnapshot.count) continue + if ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + baselineSnapshot, + ) + ) { + continue + } this.postRepairSameNetViaMergeCandidatesAccepted += 1 this.cleanupCandidatesAccepted += 1 @@ -2631,12 +2865,23 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.sharedTerminalCompositeDrcEvaluations += 1 this.cleanupCandidateAttempts += 1 const relocatedSnapshot = this.getSnapshot(atomicCandidate) - if (relocatedSnapshot.count < baselineSnapshot.count) { + if ( + this.snapshotImprovesWithoutFixedCopperRegression( + relocatedSnapshot, + baselineSnapshot, + ) + ) { this.sharedTerminalCompositeCandidatesAccepted += 1 this.cleanupCandidatesAccepted += 1 return atomicCandidate } - if (relocatedSnapshot.count > baselineSnapshot.count) continue + if ( + relocatedSnapshot.count > baselineSnapshot.count || + this.getFixedCopperIssueScore(relocatedSnapshot) > + this.getFixedCopperIssueScore(baselineSnapshot) + ) { + continue + } const relocatedErrorIds = new Set( relocatedSnapshot.errors.map((error, errorIndex) => @@ -2736,7 +2981,14 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.sharedTerminalCompositeDrcEvaluations += 1 this.cleanupCandidateAttempts += 1 const candidateSnapshot = this.getSnapshot(atomicCandidate) - if (candidateSnapshot.count >= baselineSnapshot.count) continue + if ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + baselineSnapshot, + ) + ) { + continue + } this.sharedTerminalCompositeCandidatesAccepted += 1 this.cleanupCandidatesAccepted += 1 @@ -2912,7 +3164,12 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.postFinalCompositeDrcEvaluations += 1 this.cleanupCandidateAttempts += 1 const rawCandidateSnapshot = this.getSnapshot(rawCandidate) - if (rawCandidateSnapshot.count < snapshot.count) { + if ( + this.snapshotImprovesWithoutFixedCopperRegression( + rawCandidateSnapshot, + snapshot, + ) + ) { this.postFinalCompositeCandidatesAccepted += 1 this.cleanupCandidatesAccepted += 1 return { routes: rawCandidate, snapshot: rawCandidateSnapshot } @@ -2950,7 +3207,14 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.postFinalCompositeDrcEvaluations += 1 this.cleanupCandidateAttempts += 1 const atomicCandidateSnapshot = this.getSnapshot(atomicCandidate) - if (atomicCandidateSnapshot.count >= snapshot.count) return undefined + if ( + !this.snapshotImprovesWithoutFixedCopperRegression( + atomicCandidateSnapshot, + snapshot, + ) + ) { + return undefined + } this.postFinalCompositeCandidatesAccepted += 1 this.cleanupCandidatesAccepted += 1 @@ -3280,7 +3544,14 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.anchoredFixedCopperDrcEvaluations += 1 this.cleanupCandidateAttempts += 1 const candidateSnapshot = this.getSnapshot(materializedCandidate) - if (candidateSnapshot.count >= snapshot.count) return undefined + if ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + snapshot, + ) + ) { + return undefined + } this.anchoredFixedCopperCandidatesAccepted += 1 this.cleanupCandidatesAccepted += 1 @@ -3569,7 +3840,12 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve const primarySnapshot = this.evaluateFixedCopperCompositeCandidate(materializedPrimary) if (!primarySnapshot) return undefined - if (primarySnapshot.count < baselineSnapshot.count) { + if ( + this.snapshotImprovesWithoutFixedCopperRegression( + primarySnapshot, + baselineSnapshot, + ) + ) { return this.acceptFixedCopperCompositeCandidate( materializedPrimary, primarySnapshot, @@ -3651,7 +3927,12 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve const followupSnapshot = this.evaluateFixedCopperCompositeCandidate(materializedFollowup) if (!followupSnapshot) break - if (followupSnapshot.count < baselineSnapshot.count) { + if ( + this.snapshotImprovesWithoutFixedCopperRegression( + followupSnapshot, + baselineSnapshot, + ) + ) { return this.acceptFixedCopperCompositeCandidate( materializedFollowup, followupSnapshot, @@ -3674,7 +3955,12 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve workingSnapshot = bestOwnerCandidate.snapshot } - if (workingSnapshot.count < baselineSnapshot.count) { + if ( + this.snapshotImprovesWithoutFixedCopperRegression( + workingSnapshot, + baselineSnapshot, + ) + ) { return this.acceptFixedCopperCompositeCandidate( workingRoutes, workingSnapshot, @@ -3895,7 +4181,14 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.finalEndpointSlideDrcEvaluations += 1 this.cleanupCandidateAttempts += 1 const candidateSnapshot = this.getSnapshot(materializedCandidate) - if (candidateSnapshot.count >= snapshot.count) continue + if ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + snapshot, + ) + ) { + continue + } this.finalEndpointSlideCandidatesAccepted += 1 this.finalEndpointSlideRelocatedBranches += branches.length @@ -3951,6 +4244,292 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve return improvedRoutes } + private tryInterpolatedFixedOverlapLayerBridge( + routes: HighDensityRoute[], + error: DrcError, + maxIssueCountIncrease: number, + ): HighDensityRoute[] | undefined { + if (getErrorType(error) !== "pcb_trace_error") return undefined + const center = this.getErrorCenter(error) + if (!center) return undefined + const snapshot = this.getSnapshot(routes) + const routeIndex = this.getCandidateRouteIndexesForError( + error, + snapshot, + )[0] + const route = routeIndex === undefined ? undefined : routes[routeIndex] + if (!route || route.route.length < 2) return undefined + + const cumulativeDistances = [0] + for (let index = 0; index < route.route.length - 1; index += 1) { + cumulativeDistances.push( + cumulativeDistances[index]! + + getPointDistance(route.route[index]!, route.route[index + 1]!), + ) + } + let nearestSegmentIndex = -1 + let nearestProjection = 0 + let nearestDistance = Number.POSITIVE_INFINITY + for ( + let segmentIndex = 0; + segmentIndex < route.route.length - 1; + segmentIndex += 1 + ) { + const start = route.route[segmentIndex]! + const end = route.route[segmentIndex + 1]! + if (start.z !== end.z) continue + const segmentLength = getPointDistance(start, end) + if (segmentLength <= POSITION_EPSILON) continue + const distance = getPointToSegmentDistance(center, start, end) + if (distance >= nearestDistance) continue + const deltaX = end.x - start.x + const deltaY = end.y - start.y + nearestDistance = distance + nearestSegmentIndex = segmentIndex + nearestProjection = Math.max( + 0, + Math.min( + 1, + ((center.x - start.x) * deltaX + + (center.y - start.y) * deltaY) / + (segmentLength * segmentLength), + ), + ) + } + if (nearestSegmentIndex < 0) return undefined + + const sourceZ = route.route[nearestSegmentIndex]!.z + let sameLayerStart = nearestSegmentIndex + let sameLayerEnd = nearestSegmentIndex + 1 + while ( + sameLayerStart > 0 && + route.route[sameLayerStart - 1]!.z === sourceZ + ) { + sameLayerStart -= 1 + } + while ( + sameLayerEnd < route.route.length - 1 && + route.route[sameLayerEnd + 1]!.z === sourceZ + ) { + sameLayerEnd += 1 + } + const projectedRouteDistance = + cumulativeDistances[nearestSegmentIndex]! + + nearestProjection * + getPointDistance( + route.route[nearestSegmentIndex]!, + route.route[nearestSegmentIndex + 1]!, + ) + + const getAnchor = ( + routeDistance: number, + ): + | { + segmentIndex: number + point: HighDensityRoute["route"][number] + } + | undefined => { + for ( + let segmentIndex = sameLayerStart; + segmentIndex < sameLayerEnd; + segmentIndex += 1 + ) { + const startDistance = cumulativeDistances[segmentIndex]! + const endDistance = cumulativeDistances[segmentIndex + 1]! + if (routeDistance > endDistance + POSITION_EPSILON) continue + const segmentLength = endDistance - startDistance + if (segmentLength <= POSITION_EPSILON) continue + const start = route.route[segmentIndex]! + const end = route.route[segmentIndex + 1]! + const fraction = Math.max( + 0, + Math.min(1, (routeDistance - startDistance) / segmentLength), + ) + return { + segmentIndex, + point: { + x: start.x + (end.x - start.x) * fraction, + y: start.y + (end.y - start.y) * fraction, + z: sourceZ, + traceThickness: route.traceThickness, + }, + } + } + return undefined + } + + const targetErrorIdentity = this.getDrcErrorIdentity(error) + const baselineFixedScore = this.getFixedCopperIssueScore(snapshot) + let bestCandidate: + | { routes: HighDensityRoute[]; snapshot: DrcSnapshot } + | undefined + for (const halfSpan of [0.35, 0.5, 0.75, 1, 1.5, 2, 3]) { + const startDistance = Math.max( + cumulativeDistances[sameLayerStart]!, + projectedRouteDistance - halfSpan, + ) + const endDistance = Math.min( + cumulativeDistances[sameLayerEnd]!, + projectedRouteDistance + halfSpan, + ) + const startAnchor = getAnchor(startDistance) + const endAnchor = getAnchor(endDistance) + if ( + !startAnchor || + !endAnchor || + startAnchor.segmentIndex > endAnchor.segmentIndex + ) { + continue + } + for ( + let targetZ = 0; + targetZ < this.params.srj.layerCount; + targetZ += 1 + ) { + if (targetZ === sourceZ || !this.hasLocalCleanupBudget()) continue + const bridgeRoute = [ + ...route.route.slice(0, startAnchor.segmentIndex + 1), + { ...startAnchor.point }, + { + ...startAnchor.point, + z: targetZ, + pcb_port_id: undefined, + }, + { + ...endAnchor.point, + z: targetZ, + pcb_port_id: undefined, + }, + { ...endAnchor.point }, + ...route.route.slice(endAnchor.segmentIndex + 1), + ] + const candidateRoutes = cloneRoutes(routes) + candidateRoutes[routeIndex] = { ...route, route: bridgeRoute } + const materializedCandidate = materializeRoutes(candidateRoutes) + if ( + !routeHasValidLayerTransitions(materializedCandidate[routeIndex]!) || + !this.candidatePreservesTerminals(materializedCandidate) + ) { + continue + } + + this.cleanupCandidateAttempts += 1 + this.localCleanupDrcEvaluations += 1 + const candidateSnapshot = this.getSnapshot(materializedCandidate) + const candidateFixedScore = + this.getFixedCopperIssueScore(candidateSnapshot) + const targetWasRemoved = !candidateSnapshot.errors.some( + (candidateError) => + this.getDrcErrorIdentity(candidateError) === targetErrorIdentity, + ) + if ( + !targetWasRemoved || + candidateFixedScore > baselineFixedScore || + candidateSnapshot.count > snapshot.count + maxIssueCountIncrease + ) { + continue + } + if ( + !bestCandidate || + candidateFixedScore < + this.getFixedCopperIssueScore(bestCandidate.snapshot) || + (candidateFixedScore === + this.getFixedCopperIssueScore(bestCandidate.snapshot) && + candidateSnapshot.count < bestCandidate.snapshot.count) + ) { + bestCandidate = { + routes: materializedCandidate, + snapshot: candidateSnapshot, + } + } + } + } + + if (!bestCandidate) return undefined + this.cleanupCandidatesAccepted += 1 + this.consecutiveLocalCleanupDrcMisses = 0 + return bestCandidate.routes + } + + private runFinalFixedOverlapLayerDetour( + routes: HighDensityRoute[], + options: { + drcEvaluationLimit?: number + maxIssueCountIncrease?: number + } = {}, + ): HighDensityRoute[] { + let improvedRoutes = routes + const drcEvaluationLimit = + options.drcEvaluationLimit ?? + MAX_FINAL_FIXED_OVERLAP_LAYER_DETOUR_DRC_EVALUATIONS + const baseEvaluationLimit = this.selectedLocalCleanupDrcEvaluationLimit + const baseConsecutiveMissLimit = + this.selectedConsecutiveLocalCleanupDrcMissLimit + const evaluationsBeforeSweep = this.localCleanupDrcEvaluations + this.selectedLocalCleanupDrcEvaluationLimit = + evaluationsBeforeSweep + drcEvaluationLimit + this.selectedConsecutiveLocalCleanupDrcMissLimit = + drcEvaluationLimit + this.consecutiveLocalCleanupDrcMisses = 0 + + try { + while ( + this.localCleanupDrcEvaluations - evaluationsBeforeSweep < + drcEvaluationLimit && + this.hasLocalCleanupBudget() + ) { + const snapshot = this.getSnapshot(improvedRoutes) + if (snapshot.count === 0) break + let nextRoutes: HighDensityRoute[] | undefined + + for (const error of snapshot.errors) { + if (getErrorType(error) !== "pcb_trace_error") continue + const primaryTraceId = + typeof error.pcb_trace_id === "string" + ? error.pcb_trace_id + : undefined + const otherTraceId = getRawOtherTraceId(error) + const primaryIsCandidate = Boolean( + primaryTraceId && + snapshot.traceRouteIndexById.has(primaryTraceId), + ) + const otherIsCandidate = Boolean( + otherTraceId && snapshot.traceRouteIndexById.has(otherTraceId), + ) + if (primaryIsCandidate === otherIsCandidate) continue + + nextRoutes = this.tryInterpolatedFixedOverlapLayerBridge( + improvedRoutes, + error, + options.maxIssueCountIncrease ?? 2, + ) + nextRoutes ??= this.tryLocalTraceLayerDetour( + improvedRoutes, + error, + { + preferFixedCopperIssueReduction: true, + maxIssueCountIncrease: options.maxIssueCountIncrease, + }, + ) + if (nextRoutes) break + if (!this.hasLocalCleanupBudget()) break + } + + if (!nextRoutes) break + this.finalFixedOverlapLayerDetourCandidatesAccepted += 1 + improvedRoutes = nextRoutes + } + } finally { + this.finalFixedOverlapLayerDetourDrcEvaluations += + this.localCleanupDrcEvaluations - evaluationsBeforeSweep + this.selectedLocalCleanupDrcEvaluationLimit = baseEvaluationLimit + this.selectedConsecutiveLocalCleanupDrcMissLimit = + baseConsecutiveMissLimit + } + + return improvedRoutes + } + private getCombinedMissingConnectionCanonicalNet( error: DrcError, ): string | undefined { @@ -4231,7 +4810,14 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.finalContinuityTerminalViaDrcEvaluations += 1 this.cleanupCandidateAttempts += 1 const candidateSnapshot = this.getSnapshot(materializedCandidate) - if (candidateSnapshot.count >= snapshot.count) continue + if ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + snapshot, + ) + ) { + continue + } this.finalContinuityTerminalViaCandidatesAccepted += 1 this.cleanupCandidatesAccepted += 1 @@ -4253,7 +4839,9 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private runPipeline9Cleanup(routes: HighDensityRoute[]): HighDensityRoute[] { this.selectAdaptiveCleanupLimits() - let improvedRoutes = this.unlockCleanupTerminals(routes) + let improvedRoutes = this.normalizeViaMetadataFromLayerTransitions( + this.unlockCleanupTerminals(routes), + ) improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) for (let pass = 0; pass < MAX_CLEANUP_PASSES; pass += 1) { @@ -4284,6 +4872,13 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve improvedRoutes = nextRoutes } improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) + improvedRoutes = + this.runFinalFixedOverlapLayerDetour(improvedRoutes, { + drcEvaluationLimit: + MAX_EARLY_FIXED_OVERLAP_LAYER_DETOUR_DRC_EVALUATIONS, + maxIssueCountIncrease: 2, + }) + improvedRoutes = this.runPostClusterViaMicroShiftCleanup(improvedRoutes) for (let round = 0; round < MAX_B01_PHASE_ROUNDS; round += 1) { const issueCountBeforeRound = this.getSnapshot(improvedRoutes).count @@ -4323,8 +4918,13 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve improvedRoutes = this.runPostFinalCompositeRepair(improvedRoutes) improvedRoutes = this.runAnchoredFixedCopperRepair(improvedRoutes) improvedRoutes = this.runFixedCopperCompositeRepair(improvedRoutes) + improvedRoutes = + this.runFinalFixedOverlapLayerDetour(improvedRoutes) + improvedRoutes = this.runPostClusterViaMicroShiftCleanup(improvedRoutes) improvedRoutes = this.runFinalEndpointSlideCleanup(improvedRoutes) improvedRoutes = this.runFinalContinuityTerminalViaBridge(improvedRoutes) + improvedRoutes = + this.normalizeViaMetadataFromLayerTransitions(improvedRoutes) return this.restoreTerminalIds(improvedRoutes) } @@ -4337,10 +4937,32 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.solved = false } - this.outputHdRoutes = this.runPipeline9Cleanup(this.outputHdRoutes) + const inheritedRepairSnapshot = this.getSnapshot(this.outputHdRoutes) + const initialHypergraphSnapshot = this.getSnapshot(this.initialHdRoutes) + const inheritedFixedCopperIssueCount = this.getFixedCopperIssueCount( + inheritedRepairSnapshot, + ) + const initialFixedCopperIssueCount = this.getFixedCopperIssueCount( + initialHypergraphSnapshot, + ) + const cleanupInputRoutes = + initialFixedCopperIssueCount < inheritedFixedCopperIssueCount || + (initialFixedCopperIssueCount === inheritedFixedCopperIssueCount && + initialHypergraphSnapshot.count < inheritedRepairSnapshot.count) + ? this.initialHdRoutes + : this.outputHdRoutes + this.outputHdRoutes = this.runPipeline9Cleanup(cleanupInputRoutes) const finalSnapshot = this.getSnapshot(this.outputHdRoutes) this.stats = { ...this.stats, + pipeline9InheritedRepairDrcIssueCount: inheritedRepairSnapshot.count, + pipeline9InheritedRepairFixedCopperIssueCount: + inheritedFixedCopperIssueCount, + pipeline9InitialHypergraphDrcIssueCount: initialHypergraphSnapshot.count, + pipeline9InitialHypergraphFixedCopperIssueCount: + initialFixedCopperIssueCount, + pipeline9CleanupUsedInitialHypergraphRoutes: + cleanupInputRoutes === this.initialHdRoutes, finalDrcIssueCount: finalSnapshot.count, pipeline9DrcCleanupCandidateAttempts: this.cleanupCandidateAttempts, pipeline9DrcCleanupCandidatesAccepted: this.cleanupCandidatesAccepted, @@ -4475,6 +5097,24 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve MAX_FINAL_CONTINUITY_TERMINAL_VIA_ATTEMPTS, pipeline9FinalContinuityTerminalViaDrcEvaluationLimit: MAX_FINAL_CONTINUITY_TERMINAL_VIA_DRC_EVALUATIONS, + pipeline9FinalFixedOverlapLayerDetourDrcEvaluations: + this.finalFixedOverlapLayerDetourDrcEvaluations, + pipeline9FinalFixedOverlapLayerDetourCandidatesAccepted: + this.finalFixedOverlapLayerDetourCandidatesAccepted, + pipeline9FinalFixedOverlapLayerDetourDrcEvaluationLimit: + MAX_FINAL_FIXED_OVERLAP_LAYER_DETOUR_DRC_EVALUATIONS, + pipeline9FinalFixedOverlapBestRemovedTargetIssueCount: + Number.isFinite( + this.finalFixedOverlapBestRemovedTargetIssueCount, + ) + ? this.finalFixedOverlapBestRemovedTargetIssueCount + : undefined, + pipeline9FinalFixedOverlapBestRemovedTargetFixedScore: + Number.isFinite( + this.finalFixedOverlapBestRemovedTargetFixedScore, + ) + ? this.finalFixedOverlapBestRemovedTargetFixedScore + : undefined, } this.progress = 1 this.solved = true diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts index 3f15f1ee1..ee201f671 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts @@ -1,12 +1,11 @@ import { pointToSegmentDistance } from "@tscircuit/math-utils" import { BaseSolver } from "lib/solvers/BaseSolver" import type { + PreloadedTracePortAssignment, SharedEdgeSegment, SegmentPortPoint, } from "lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver" import type { SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" -import { getViaDimensions } from "lib/utils/getViaDimensions" -import { JUMPER_DIMENSIONS } from "lib/utils/jumperSizes" import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" import { minimumDistanceBetweenSegments } from "lib/utils/minimumDistanceBetweenSegments" import { resolvePreloadedTraceCanonicalNetIds } from "lib/utils/resolvePreloadedTraceCanonicalNetIds" @@ -14,12 +13,14 @@ import { resolvePreloadedTraceCanonicalNetIds } from "lib/utils/resolvePreloaded type Point = { x: number; y: number } type PreloadedTracePrimitive = { + traceId: string fixedNetId: string connectionName: string + routePositionStart: number + routePositionEnd: number zLayers: number[] start: Point end: Point - radius: number } type RoutePoint = SimplifiedPcbTrace["route"][number] @@ -47,7 +48,6 @@ const getPreloadedTracePrimitives = ( ): PreloadedTracePrimitive[] => { const primitives: PreloadedTracePrimitive[] = [] const canonicalNetIdByTraceId = resolvePreloadedTraceCanonicalNetIds(srj) - const defaultViaDiameter = getViaDimensions(srj).padDiameter for (const trace of srj.traces ?? []) { if (!trace.connection_name) { @@ -58,11 +58,14 @@ const getPreloadedTracePrimitives = ( const fixedNetId = canonicalNetIdByTraceId.get(trace.pcb_trace_id) ?? trace.connection_name - for (const routePoint of trace.route) { + for (const [routePosition, routePoint] of trace.route.entries()) { if (routePoint.route_type === "via") { primitives.push({ + traceId: trace.pcb_trace_id, fixedNetId, connectionName: trace.connection_name, + routePositionStart: routePosition, + routePositionEnd: routePosition, zLayers: getLayersBetween( routePoint.from_layer, routePoint.to_layer, @@ -70,12 +73,14 @@ const getPreloadedTracePrimitives = ( ), start: routePoint, end: routePoint, - radius: (routePoint.via_diameter ?? defaultViaDiameter) / 2, }) } else if (routePoint.route_type === "through_obstacle") { primitives.push({ + traceId: trace.pcb_trace_id, fixedNetId, connectionName: trace.connection_name, + routePositionStart: routePosition, + routePositionEnd: routePosition + 1, zLayers: getLayersBetween( routePoint.from_layer, routePoint.to_layer, @@ -83,23 +88,22 @@ const getPreloadedTracePrimitives = ( ), start: routePoint.start, end: routePoint.end, - radius: routePoint.width / 2, }) } else if (routePoint.route_type === "jumper") { - const dimensions = JUMPER_DIMENSIONS[routePoint.footprint] - const padRadius = Math.hypot( - dimensions.padLength / 2, - dimensions.padWidth / 2, - ) const z = mapLayerNameToZ(routePoint.layer, srj.layerCount) - for (const padCenter of [routePoint.start, routePoint.end]) { + for (const [padIndex, padCenter] of [ + routePoint.start, + routePoint.end, + ].entries()) { primitives.push({ + traceId: trace.pcb_trace_id, fixedNetId, connectionName: trace.connection_name, + routePositionStart: routePosition + padIndex, + routePositionEnd: routePosition + padIndex, zLayers: [z], start: padCenter, end: padCenter, - radius: padRadius, }) } } @@ -116,12 +120,14 @@ const getPreloadedTracePrimitives = ( continue } primitives.push({ + traceId: trace.pcb_trace_id, fixedNetId, connectionName: trace.connection_name, + routePositionStart: pointIndex, + routePositionEnd: pointIndex + 1, zLayers: [mapLayerNameToZ(start.layer, srj.layerCount)], start, end, - radius: Math.max(start.width, end.width) / 2, }) } } @@ -157,6 +163,7 @@ const getClosestPortPoint = ( const preloadPort = ( portPoint: SegmentPortPoint, primitive: PreloadedTracePrimitive, + z: number, ) => { const fixedNetIds = [ ...new Set([ @@ -165,6 +172,54 @@ const preloadPort = ( ]), ].sort() portPoint._preloadedFixedNetIds = fixedNetIds + const dx = primitive.end.x - primitive.start.x + const dy = primitive.end.y - primitive.start.y + const lengthSquared = dx * dx + dy * dy + const projection = + lengthSquared === 0 + ? 0 + : Math.max( + 0, + Math.min( + 1, + ((portPoint.x - primitive.start.x) * dx + + (portPoint.y - primitive.start.y) * dy) / + lengthSquared, + ), + ) + const assignment: PreloadedTracePortAssignment = { + traceId: primitive.traceId, + fixedNetId: primitive.fixedNetId, + routePosition: + primitive.routePositionStart + + projection * + (primitive.routePositionEnd - primitive.routePositionStart), + z, + traceX: primitive.start.x + projection * dx, + traceY: primitive.start.y + projection * dy, + } + const existingAssignments = + portPoint._preloadedTracePortAssignments ?? [] + if ( + !existingAssignments.some( + (existing) => + existing.traceId === assignment.traceId && + existing.fixedNetId === assignment.fixedNetId && + existing.z === assignment.z && + Math.abs(existing.routePosition - assignment.routePosition) <= + GEOMETRIC_TOLERANCE, + ) + ) { + portPoint._preloadedTracePortAssignments = [ + ...existingAssignments, + assignment, + ].sort( + (left, right) => + left.traceId.localeCompare(right.traceId) || + left.routePosition - right.routePosition || + left.z - right.z, + ) + } if (portPoint.connectionName === null) { portPoint.connectionName = primitive.connectionName @@ -204,8 +259,7 @@ export class PreloadedTraceGraphSolver extends BaseSolver { primitive.end, segment.start, segment.end, - ) > - primitive.radius + GEOMETRIC_TOLERANCE + ) > GEOMETRIC_TOLERANCE ) { continue } @@ -213,7 +267,7 @@ export class PreloadedTraceGraphSolver extends BaseSolver { for (const z of primitive.zLayers) { if (!segment.availableZ.includes(z)) continue const portPoint = getClosestPortPoint(segment, primitive, z) - if (portPoint) preloadPort(portPoint, primitive) + if (portPoint) preloadPort(portPoint, primitive, z) } } } diff --git a/lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver.ts b/lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver.ts index bd6e7de66..f7e9412ba 100644 --- a/lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver.ts +++ b/lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver.ts @@ -8,6 +8,15 @@ import type { import type { GraphicsObject } from "graphics-debug" import { getNodeEdgeMap } from "../CapacityMeshSolver/getNodeEdgeMap" +export interface PreloadedTracePortAssignment { + traceId: string + fixedNetId: string + routePosition: number + z: number + traceX?: number + traceY?: number +} + export interface SegmentPortPoint { segmentPortPointId: string x: number @@ -29,6 +38,8 @@ export interface SegmentPortPoint { tinyHypergraphPortPenalty?: number /** Canonical fixed-net ids loaded onto this existing graph port. */ _preloadedFixedNetIds?: string[] + /** Ordered crossings used to load fixed intra-region segments. */ + _preloadedTracePortAssignments?: PreloadedTracePortAssignment[] } export interface SharedEdgeSegment { diff --git a/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/buildHyperGraph.ts b/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/buildHyperGraph.ts index 4ed17e37c..4204d618f 100644 --- a/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/buildHyperGraph.ts +++ b/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/buildHyperGraph.ts @@ -206,6 +206,23 @@ export function buildHyperGraph(params: { ) for (const z of spp.availableZ) { + const preloadedTracePortAssignments = + spp._preloadedTracePortAssignments?.filter( + (assignment) => assignment.z === z, + ) + const preloadedFixedNetIds = + preloadedTracePortAssignments && + preloadedTracePortAssignments.length > 0 + ? [ + ...new Set( + preloadedTracePortAssignments.map( + (assignment) => assignment.fixedNetId, + ), + ), + ].sort() + : spp._preloadedTracePortAssignments + ? undefined + : spp._preloadedFixedNetIds const port: RawPort = { portId: `${spp.segmentPortPointId}::${z}`, x: spp.x, @@ -215,7 +232,8 @@ export function buildHyperGraph(params: { cramped: spp.cramped, regions: [region1, region2], tinyHypergraphPortPenalty: spp.tinyHypergraphPortPenalty, - _preloadedFixedNetIds: spp._preloadedFixedNetIds, + _preloadedFixedNetIds: preloadedFixedNetIds, + _preloadedTracePortAssignments: preloadedTracePortAssignments, } const hgPort: RegionPortHg = { portId: spp.segmentPortPointId, diff --git a/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/types.ts b/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/types.ts index e20e1a60e..fd579c605 100644 --- a/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/types.ts +++ b/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/types.ts @@ -13,6 +13,7 @@ import type { CapacityMeshNodeId, SimpleRouteConnection, } from "lib/types" +import type { PreloadedTracePortAssignment } from "lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver" export type RawPort = { portId: string @@ -24,6 +25,7 @@ export type RawPort = { regions: RegionHg[] tinyHypergraphPortPenalty?: number _preloadedFixedNetIds?: string[] + _preloadedTracePortAssignments?: PreloadedTracePortAssignment[] } export type RegionPortHg = Omit & { diff --git a/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts b/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts index e28c48596..957327328 100644 --- a/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts +++ b/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts @@ -13,6 +13,7 @@ import type { } from "lib/types/high-density-types" import { getIntraNodeCrossingsUsingCircle } from "lib/utils/getIntraNodeCrossingsUsingCircle" import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" +import { minimumDistanceBetweenSegments } from "lib/utils/minimumDistanceBetweenSegments" import { DuplicateCongestedPortSolver, orderConnectionsByNetCardinality, @@ -30,6 +31,7 @@ import type { ConnectionHgWithSimpleRouteConnection, HgPortPointPathingSolverParams, } from "../hgportpointpathingsolver/types" +import type { PreloadedTracePortAssignment } from "lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver" import { createTinyRouteNetIndexer } from "./createTinyRouteNetIndexer" import { getRegionNetIdByRegionId } from "./getRegionNetIdByRegionId" @@ -80,6 +82,7 @@ type TinyPortMetadata = { tinyHypergraphPortPenalty?: number duplicatedFromPortId?: string _preloadedFixedNetIds?: string[] + _preloadedTracePortAssignments?: PreloadedTracePortAssignment[] } type LoadedTinyGraph = { @@ -273,6 +276,8 @@ const toSerializedPortData = ( tinyHypergraphPortPenalty: port.d.tinyHypergraphPortPenalty, cramped: port.d.cramped, _preloadedFixedNetIds: port.d._preloadedFixedNetIds, + _preloadedTracePortAssignments: + port.d._preloadedTracePortAssignments, } } @@ -629,7 +634,7 @@ const applyMetadataPortPenalties = (loaded: LoadedTinyGraph) => { return metadataPortPenaltyCount } -const applyPreloadedPortReservations = ( +const createPreloadedFixedNetIndexer = ( solver: TinyHyperGraphSolver & LoadedTinyGraph, ) => { const netIndexByAlias = new Map() @@ -675,6 +680,13 @@ const applyPreloadedPortReservations = ( return fixedNetIndex } + return getFixedNetIndex +} + +const applyPreloadedPortReservations = ( + solver: TinyHyperGraphSolver & LoadedTinyGraph, + getFixedNetIndex: (fixedNetId: string) => number, +) => { let preloadedPortCount = 0 const problemSetup = solver.problemSetup for (let portId = 0; portId < solver.topology.portCount; portId++) { @@ -708,12 +720,245 @@ const applyPreloadedPortReservations = ( return preloadedPortCount } +type PreloadedFixedSegment = { + traceId: string + fixedNetIndex: number + regionId: number + fromPortId: number + toPortId: number + z: number + traceStart: { x: number; y: number } + traceEnd: { x: number; y: number } +} + +const getPreloadedFixedSegments = ( + solver: TinyHyperGraphSolver & LoadedTinyGraph, + getFixedNetIndex: (fixedNetId: string) => number, +): PreloadedFixedSegment[] => { + const assignmentsByTraceId = new Map< + string, + Array + >() + + for (let portId = 0; portId < solver.topology.portCount; portId++) { + for (const assignment of solver.topology.portMetadata?.[portId] + ?._preloadedTracePortAssignments ?? []) { + const traceAssignments = + assignmentsByTraceId.get(assignment.traceId) ?? [] + traceAssignments.push({ ...assignment, portId }) + assignmentsByTraceId.set(assignment.traceId, traceAssignments) + } + } + + const segments: PreloadedFixedSegment[] = [] + for (const [traceId, assignments] of assignmentsByTraceId) { + assignments.sort( + (left, right) => + left.routePosition - right.routePosition || + left.z - right.z || + left.portId - right.portId, + ) + const orderedAssignments = assignments.filter( + (assignment, index) => + index === 0 || + assignment.portId !== assignments[index - 1]!.portId, + ) + + for (let index = 1; index < orderedAssignments.length; index++) { + const from = orderedAssignments[index - 1]! + const to = orderedAssignments[index]! + if (from.z !== to.z) continue + const sharedRegionIds = ( + solver.topology.incidentPortRegion[from.portId] ?? [] + ).filter((regionId) => + (solver.topology.incidentPortRegion[to.portId] ?? []).includes( + regionId, + ), + ) + if (sharedRegionIds.length === 0) continue + + for (const regionId of sharedRegionIds) { + segments.push({ + traceId, + fixedNetIndex: getFixedNetIndex(from.fixedNetId), + regionId, + fromPortId: from.portId, + toPortId: to.portId, + z: from.z, + traceStart: { + x: + typeof from.traceX === "number" + ? from.traceX + : Number(solver.topology.portMetadata?.[from.portId]?.x ?? 0), + y: + typeof from.traceY === "number" + ? from.traceY + : Number(solver.topology.portMetadata?.[from.portId]?.y ?? 0), + }, + traceEnd: { + x: + typeof to.traceX === "number" + ? to.traceX + : Number(solver.topology.portMetadata?.[to.portId]?.x ?? 0), + y: + typeof to.traceY === "number" + ? to.traceY + : Number(solver.topology.portMetadata?.[to.portId]?.y ?? 0), + }, + }) + } + } + } + + return segments +} + +const hasPreloadedSegmentInCache = ( + solver: TinyHyperGraphSolver, + segment: PreloadedFixedSegment, +) => { + const geometry = { + ...solver.populateSegmentGeometryScratch( + segment.regionId, + segment.fromPortId, + segment.toPortId, + ), + } + const cache = solver.state.regionIntersectionCaches[segment.regionId] + for (let index = 0; index < cache.netIds.length; index++) { + if ( + cache.netIds[index] === segment.fixedNetIndex && + cache.lesserAngles[index] === geometry.lesserAngle && + cache.greaterAngles[index] === geometry.greaterAngle && + cache.layerMasks[index] === geometry.layerMask + ) { + return true + } + } + return false +} + +const ensurePreloadedFixedSegments = ( + solver: TinyHyperGraphSolver, + segments: PreloadedFixedSegment[], +) => { + let preloadedFixedSegmentCount = 0 + const previousRouteNetId = solver.state.currentRouteNetId + try { + for (const segment of segments) { + if (!hasPreloadedSegmentInCache(solver, segment)) { + solver.state.currentRouteNetId = segment.fixedNetIndex + solver.appendSegmentToRegionCache( + segment.regionId, + segment.fromPortId, + segment.toPortId, + ) + } + preloadedFixedSegmentCount++ + } + } finally { + solver.state.currentRouteNetId = previousRouteNetId + } + return preloadedFixedSegmentCount +} + +const segmentsCrossOnSameLayer = ( + solver: TinyHyperGraphSolver, + regionId: number, + firstFromPortId: number, + firstToPortId: number, + secondFromPortId: number, + secondToPortId: number, +) => { + const first = { + ...solver.populateSegmentGeometryScratch( + regionId, + firstFromPortId, + firstToPortId, + ), + } + const second = { + ...solver.populateSegmentGeometryScratch( + regionId, + secondFromPortId, + secondToPortId, + ), + } + if ((first.layerMask & second.layerMask) === 0) return false + if ( + first.lesserAngle === second.lesserAngle || + first.lesserAngle === second.greaterAngle || + first.greaterAngle === second.lesserAngle || + first.greaterAngle === second.greaterAngle + ) { + return false + } + + const secondLesserInsideFirst = + first.lesserAngle < second.lesserAngle && + second.lesserAngle < first.greaterAngle + const secondGreaterInsideFirst = + first.lesserAngle < second.greaterAngle && + second.greaterAngle < first.greaterAngle + return secondLesserInsideFirst !== secondGreaterInsideFirst +} + +const installPreloadedFixedCrossingGuard = ( + solver: TinyHyperGraphSolver, + segments: PreloadedFixedSegment[], +) => { + const originalComputeG = solver.computeG.bind(solver) + solver.computeG = (currentCandidate, neighborPortId) => { + const currentRouteNetId = solver.state.currentRouteNetId + for (const segment of segments) { + const currentPort = + solver.topology.portMetadata?.[currentCandidate.portId] + const neighborPort = solver.topology.portMetadata?.[neighborPortId] + const geometricallyIntersects = + currentPort?.z === segment.z && + neighborPort?.z === segment.z && + typeof currentPort.x === "number" && + typeof currentPort.y === "number" && + typeof neighborPort.x === "number" && + typeof neighborPort.y === "number" && + minimumDistanceBetweenSegments( + { x: currentPort.x, y: currentPort.y }, + { x: neighborPort.x, y: neighborPort.y }, + segment.traceStart, + segment.traceEnd, + ) <= 1e-6 + if ( + segment.regionId === currentCandidate.nextRegionId && + segment.fixedNetIndex !== currentRouteNetId && + (geometricallyIntersects || + segmentsCrossOnSameLayer( + solver, + segment.regionId, + currentCandidate.portId, + neighborPortId, + segment.fromPortId, + segment.toPortId, + )) + ) { + return Number.POSITIVE_INFINITY + } + } + return originalComputeG(currentCandidate, neighborPortId) + } +} + class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSectionPipelineSolver { private configuredSolvers = new WeakSet() + private preloadedFixedSegmentsBySolver = new WeakMap< + TinyHyperGraphSolver, + PreloadedFixedSegment[] + >() + private fixedCrossingGuardedSolvers = new WeakSet() duplicatePortPenaltyCount = 0 metadataPortPenaltyCount = 0 crampedPortPenaltyCount = 0 preloadedPortCount = 0 + preloadedFixedSegmentCount = 0 readonly crampedPortTraversalPenalty: number readonly useSelectiveReripRouting: boolean @@ -736,6 +981,18 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect solveGraphStep.solverClass = SelectiveReripTinyHyperGraphSolver } this.MAX_ITERATIONS = getTinyHyperGraphPipelineMaxIterations(inputProblem) + const hasPreloadedFixedSegments = inputProblem.serializedHyperGraph.ports.some( + (port) => + ( + asTinyPortMetadata(port.d)._preloadedTracePortAssignments?.length ?? + 0 + ) > 0, + ) + if (hasPreloadedFixedSegments) { + this.pipelineDef = this.pipelineDef.filter( + (pipelineStep) => pipelineStep.solverName !== "optimizeSection", + ) + } } override loadHyperGraph(serializedHyperGraph: SerializedHyperGraph) { @@ -772,6 +1029,7 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect throw error } this.configureSolver(this.activeSubSolver) + this.ensurePreloadedFixedSegments(this.activeSubSolver) } override getInitialVisualizationSolver() { @@ -787,6 +1045,7 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect } const solver = super.getInitialVisualizationSolver() this.configureSolver(solver) + this.ensurePreloadedFixedSegments(solver) return solver } @@ -822,17 +1081,48 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect applyTerminalRegionNetIds(loadedSolver) } if (solver instanceof TinyHyperGraphSolver) { + const loadedSolver = solver as TinyHyperGraphSolver & LoadedTinyGraph + const getFixedNetIndex = + createPreloadedFixedNetIndexer(loadedSolver) this.preloadedPortCount = Math.max( this.preloadedPortCount, applyPreloadedPortReservations( - solver as TinyHyperGraphSolver & LoadedTinyGraph, + loadedSolver, + getFixedNetIndex, ), ) + const preloadedFixedSegments = getPreloadedFixedSegments( + loadedSolver, + getFixedNetIndex, + ) + this.preloadedFixedSegmentsBySolver.set(solver, preloadedFixedSegments) + if (!this.fixedCrossingGuardedSolvers.has(solver)) { + installPreloadedFixedCrossingGuard( + solver, + preloadedFixedSegments, + ) + this.fixedCrossingGuardedSolvers.add(solver) + } + this.preloadedFixedSegmentCount = Math.max( + this.preloadedFixedSegmentCount, + ensurePreloadedFixedSegments(solver, preloadedFixedSegments), + ) } this.configuredSolvers.add(solver) } + private ensurePreloadedFixedSegments(solver?: BaseSolver | null) { + if (!(solver instanceof TinyHyperGraphSolver)) return + const preloadedFixedSegments = + this.preloadedFixedSegmentsBySolver.get(solver) + if (!preloadedFixedSegments) return + this.preloadedFixedSegmentCount = Math.max( + this.preloadedFixedSegmentCount, + ensurePreloadedFixedSegments(solver, preloadedFixedSegments), + ) + } + private trySkipOptimizeSection(error: unknown) { if (this.getCurrentStageName() !== "optimizeSection") { return false @@ -917,7 +1207,12 @@ export class TinyHypergraphPortPointPathingSolver extends BaseSolver { ]), ) const serializedGraph = buildSerializedTinyGraph({ ...params, connections }) + const hasPreloadedPortReservations = serializedGraph.ports.some( + (port) => + (asTinyPortMetadata(port.d)._preloadedFixedNetIds?.length ?? 0) > 0, + ) const shouldRunDuplicateCongestedPortPrepass = + !hasPreloadedPortReservations && connections.length <= MAX_CONNECTIONS_FOR_DUPLICATE_CONGESTED_PORT_PREPASS let graphForTiny = serializedGraph if (shouldRunDuplicateCongestedPortPrepass) { @@ -945,15 +1240,11 @@ export class TinyHypergraphPortPointPathingSolver extends BaseSolver { } else { this.duplicateCongestedPortReport = duplicateCongestedPortSolver.report graphForTiny = duplicateCongestedPortSolver.getOutput() - for (const port of graphForTiny.ports) { - const metadata = port.d as TinyPortMetadata - if (typeof metadata.duplicatedFromPortId === "string") { - delete metadata._preloadedFixedNetIds - } - } } } else { - this.duplicateCongestedPortError = `Skipped for ${connections.length} connections` + this.duplicateCongestedPortError = hasPreloadedPortReservations + ? "Skipped to preserve preloaded port topology" + : `Skipped for ${connections.length} connections` } this.duplicatedPortCount = this.duplicateCongestedPortReport?.duplicatedPorts.reduce( @@ -1029,6 +1320,8 @@ export class TinyHypergraphPortPointPathingSolver extends BaseSolver { crampedPortPenalty: this.tinyPipelineSolver.crampedPortTraversalPenalty, crampedPortPenaltyCount: this.tinyPipelineSolver.crampedPortPenaltyCount, preloadedPortCount: this.tinyPipelineSolver.preloadedPortCount, + preloadedFixedSegmentCount: + this.tinyPipelineSolver.preloadedFixedSegmentCount, duplicateCongestedPortError: this.duplicateCongestedPortError, ...(this.tinyPipelineSolver.stats ?? {}), ...(currentTinySolver?.stats ?? {}), diff --git a/lib/testing/evaluate-relaxed-drc.ts b/lib/testing/evaluate-relaxed-drc.ts index 93711f97e..eb54b76d6 100644 --- a/lib/testing/evaluate-relaxed-drc.ts +++ b/lib/testing/evaluate-relaxed-drc.ts @@ -888,6 +888,7 @@ const createPreloadedTraceId = ( export const createRelaxedDrcTraceSet = ( inputSrj: SimpleRouteJson, traces: SimplifiedPcbTrace[], + srjWithPointPairs?: SimpleRouteJson, ): SimplifiedPcbTrace[] => { const usedTraceIds = new Set(traces.map((trace) => trace.pcb_trace_id)) const originalPreloadedTraces = inputSrj.traces ?? [] @@ -903,6 +904,22 @@ export const createRelaxedDrcTraceSet = ( ) const canonicalNetIdByOriginalTraceId = resolvePreloadedTraceCanonicalNetIds(inputSrj) + const pointPairCanonicalNameByAlias = new Map() + for (const connection of srjWithPointPairs?.connections ?? []) { + const canonicalName = + connection.__rootConnectionNames?.[0] ?? + connection.__netConnectionName ?? + connection.name + for (const alias of [ + connection.name, + connection.__netConnectionName, + ...(connection.__rootConnectionNames ?? []), + ]) { + if (typeof alias === "string" && alias.length > 0) { + pointPairCanonicalNameByAlias.set(alias, canonicalName) + } + } + } const preloadedTraceIdByOriginalId = new Map() for (const [index, trace] of originalPreloadedTraces.entries()) { if (originalTraceIdCounts.get(trace.pcb_trace_id) !== 1) continue @@ -911,11 +928,18 @@ export const createRelaxedDrcTraceSet = ( renamedPreloadedTraceIds[index]!, ) } + const getPreloadedCanonicalName = (trace: SimplifiedPcbTrace) => { + const resolvedCanonicalName = + canonicalNetIdByOriginalTraceId.get(trace.pcb_trace_id) ?? + trace.connection_name + return ( + pointPairCanonicalNameByAlias.get(resolvedCanonicalName) ?? + resolvedCanonicalName + ) + } const preloadedTraces = originalPreloadedTraces.map((trace, index) => ({ ...trace, - connection_name: - canonicalNetIdByOriginalTraceId.get(trace.pcb_trace_id) ?? - trace.connection_name, + connection_name: getPreloadedCanonicalName(trace), pcb_trace_id: renamedPreloadedTraceIds[index]!, ...(trace.connectsTo ? { @@ -1005,7 +1029,11 @@ export const evaluateRelaxedDrc = ({ srjWithPointPairs, traces, }: EvaluateRelaxedDrcInput): EvaluateRelaxedDrcResult => { - const combinedTraces = createRelaxedDrcTraceSet(inputSrj, traces) + const combinedTraces = createRelaxedDrcTraceSet( + inputSrj, + traces, + srjWithPointPairs, + ) const candidateTraceIds = new Set(traces.map((trace) => trace.pcb_trace_id)) const preloadedTraceIds = new Set( combinedTraces diff --git a/tests/features/pipeline9-srj23-sample32-preloaded-graph.test.ts b/tests/features/pipeline9-srj23-sample32-preloaded-graph.test.ts index 8dfc3f22a..ee41f6e2a 100644 --- a/tests/features/pipeline9-srj23-sample32-preloaded-graph.test.ts +++ b/tests/features/pipeline9-srj23-sample32-preloaded-graph.test.ts @@ -9,12 +9,12 @@ test("Pipeline9 preserves the srj23 sample 32 capacity topology", async () => { effort: 1, }) - while (!solver.failed && !solver.portPointPathingSolver?.solved) { + while (!solver.failed && !solver.preloadedTraceGraphSolver?.solved) { solver.step() } expect(solver.failed).toBe(false) - expect(solver.portPointPathingSolver?.solved).toBe(true) + expect(solver.preloadedTraceGraphSolver?.solved).toBe(true) expect(solver.preloadedTraceGraphSolver?.stats).toMatchObject({ topologyChanged: false, inputBoundaryCount: diff --git a/tests/features/preloaded-fixed-segment-occupancy.test.ts b/tests/features/preloaded-fixed-segment-occupancy.test.ts new file mode 100644 index 000000000..7bbe35d49 --- /dev/null +++ b/tests/features/preloaded-fixed-segment-occupancy.test.ts @@ -0,0 +1,151 @@ +import { expect, test } from "bun:test" +import { ConnectivityMap } from "circuit-json-to-connectivity-map" +import { buildHyperGraph } from "lib/solvers/PortPointPathingSolver/hgportpointpathingsolver" +import { TinyHypergraphPortPointPathingSolver } from "lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver" +import type { + CapacityMeshNode, + SimpleRouteConnection, +} from "lib/types" +import type { TinyHyperGraphSolver } from "tiny-hypergraph/lib/index" + +test("preloaded trace segments occupy existing hypergraph regions", () => { + const capacityMeshNodes: CapacityMeshNode[] = [ + ["west", -2, 0], + ["center", 0, 0], + ["east", 2, 0], + ["north", 0, 2], + ["south", 0, -2], + ].map(([capacityMeshNodeId, x, y]) => ({ + capacityMeshNodeId: String(capacityMeshNodeId), + center: { x: Number(x), y: Number(y) }, + width: 2, + height: 2, + layer: "top", + availableZ: + capacityMeshNodeId === "center" ? [0, 1] : [0], + })) + const simpleRouteJsonConnections: SimpleRouteConnection[] = [ + { + name: "foreign-route", + __rootConnectionNames: ["foreign-root"], + pointsToConnect: [ + { x: 0, y: 2, layer: "top" }, + { x: 0, y: -2, layer: "top" }, + ], + }, + ] + const connectivityMap = new ConnectivityMap({}) + connectivityMap.addConnections([["foreign-route", "foreign-root"]]) + const createPort = ( + segmentPortPointId: string, + x: number, + y: number, + nodeIds: [string, string], + routePosition?: number, + ) => ({ + segmentPortPointId, + x, + y, + availableZ: [0], + nodeIds, + edgeId: `${segmentPortPointId}-edge`, + connectionName: routePosition === undefined ? null : "fixed-route", + rootConnectionName: + routePosition === undefined ? undefined : "fixed-root", + distToCentermostPortOnZ: 0, + cramped: false, + ...(routePosition === undefined + ? {} + : { + _preloadedFixedNetIds: ["fixed-root"], + _preloadedTracePortAssignments: [ + { + traceId: "fixed-trace", + fixedNetId: "fixed-root", + routePosition, + z: 0, + }, + ], + }), + }) + const { graph, connections } = buildHyperGraph({ + capacityMeshNodes, + segmentPortPoints: [ + createPort("west-center", -1, 0, ["west", "center"], 0), + createPort("center-east", 1, 0, ["center", "east"], 1), + createPort("north-center", 0, 1, ["north", "center"]), + createPort("center-south", 0, -1, ["center", "south"]), + ], + layerCount: 1, + connectivityMap, + simpleRouteJsonConnections, + }) + const solver = new TinyHypergraphPortPointPathingSolver({ + graph, + connections, + layerCount: 1, + effort: 0.1, + flags: { + FORCE_CENTER_FIRST: true, + RIPPING_ENABLED: true, + USE_SELECTIVE_RERIP_ROUTING: true, + }, + weights: { + SHUFFLE_SEED: 0, + MEMORY_PF_FACTOR: 4, + CENTER_OFFSET_DIST_PENALTY_FACTOR: 0, + CENTER_OFFSET_FOCUS_SHIFT: 0, + NODE_PF_FACTOR: 0, + LAYER_CHANGE_COST: 0, + RIPPING_PF_COST: 0, + NODE_PF_MAX_PENALTY: 100, + BASE_CANDIDATE_COST: 0.6, + MAX_ITERATIONS_PER_PATH: 0, + RANDOM_WALK_DISTANCE: 0, + START_RIPPING_PF_THRESHOLD: 0.3, + END_RIPPING_PF_THRESHOLD: 1, + MAX_RIPS: 1000, + RANDOM_RIP_FRACTION: 0.3, + STRAIGHT_LINE_DEVIATION_PENALTY_FACTOR: 4, + GREEDY_MULTIPLIER: 0.7, + MIN_ALLOWED_BOARD_SCORE: -10000, + }, + }) + const tinyPipeline = (solver as any).tinyPipelineSolver + const tinySolver = + tinyPipeline.getInitialVisualizationSolver() as TinyHyperGraphSolver + const centerRegionId = tinySolver.topology.regionMetadata?.findIndex( + (metadata) => metadata.capacityMeshNodeId === "center", + ) + const getPortId = (serializedPortId: string) => + tinySolver.topology.portMetadata?.findIndex( + (metadata) => metadata.serializedPortId === serializedPortId, + ) ?? -1 + const westPortId = getPortId("west-center::0") + const eastPortId = getPortId("center-east::0") + const northPortId = getPortId("north-center::0") + const southPortId = getPortId("center-south::0") + + expect(tinySolver.topology.regionCount).toBe(capacityMeshNodes.length + 2) + expect(centerRegionId).toBeGreaterThanOrEqual(0) + expect(westPortId).toBeGreaterThanOrEqual(0) + expect(eastPortId).toBeGreaterThanOrEqual(0) + expect( + tinySolver.state.regionIntersectionCaches[centerRegionId!] + .existingSegmentCount, + ).toBe(1) + + tinySolver.state.currentRouteNetId = tinySolver.problem.routeNet[0] + expect( + tinySolver.computeG( + { + portId: northPortId, + nextRegionId: centerRegionId!, + f: 0, + g: 0, + h: 0, + }, + southPortId, + ), + ).toBe(Number.POSITIVE_INFINITY) +}) diff --git a/tests/features/preloaded-port-duplication.test.ts b/tests/features/preloaded-port-duplication.test.ts index 2e1293fe3..d9edee856 100644 --- a/tests/features/preloaded-port-duplication.test.ts +++ b/tests/features/preloaded-port-duplication.test.ts @@ -7,7 +7,7 @@ import type { SimpleRouteConnection, } from "lib/types" -test("congestion duplicates do not inherit preloaded port ownership", () => { +test("preloaded port ownership preserves the original graph topology", () => { const capacityMeshNodes: CapacityMeshNode[] = [ { capacityMeshNodeId: "left", @@ -70,6 +70,14 @@ test("congestion duplicates do not inherit preloaded port ownership", () => { distToCentermostPortOnZ: 0, cramped: false, _preloadedFixedNetIds: ["fixed-root"], + _preloadedTracePortAssignments: [ + { + traceId: "fixed-trace", + fixedNetId: "fixed-root", + routePosition: 0, + z: 0, + }, + ], }, { segmentPortPointId: "middle-right", @@ -128,10 +136,9 @@ test("congestion duplicates do not inherit preloaded port ownership", () => { ) expect(sourcePort?.d?._preloadedFixedNetIds).toEqual(["fixed-root"]) - expect(duplicatePorts.length).toBeGreaterThan(0) + expect(sourcePort?.d?._preloadedTracePortAssignments).toHaveLength(1) expect( - duplicatePorts.every( - (port: any) => port.d?._preloadedFixedNetIds === undefined, - ), - ).toBe(true) + serializedGraph.ports.filter((port: any) => !port.d?._tinyTerminal), + ).toHaveLength(graph.ports.length) + expect(duplicatePorts).toHaveLength(0) }) diff --git a/tests/features/preloaded-trace-graph-solver.test.ts b/tests/features/preloaded-trace-graph-solver.test.ts index a1e300a1c..ae3dc93da 100644 --- a/tests/features/preloaded-trace-graph-solver.test.ts +++ b/tests/features/preloaded-trace-graph-solver.test.ts @@ -110,6 +110,16 @@ test("preloaded traces reserve existing graph ports without changing topology", connectionName: "child-net", rootConnectionName: "root-net", _preloadedFixedNetIds: ["root-net"], + _preloadedTracePortAssignments: [ + { + traceId: "fixed-trace", + fixedNetId: "root-net", + routePosition: 0.5, + z: 0, + traceX: 0, + traceY: 0.42, + }, + ], }), expect.objectContaining({ segmentPortPointId: "bottom-high", From d3c313c5a37ff60654219c2f1e866dca60727cd1 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 26 Jul 2026 23:14:55 -0700 Subject: [PATCH 12/22] fix: complete Pipeline9 fixed-copper DRC repair --- .../pipeline9-b01-rerouter.ts | 128 ++++++- .../pipeline9-exact-drc-repair-solver.ts | 362 +++++++++++------- tests/features/pipeline9-b01-rerouter.test.ts | 65 ++++ ...e9-srj23-fixed-overlap-regressions.test.ts | 38 ++ 4 files changed, 457 insertions(+), 136 deletions(-) create mode 100644 tests/features/pipeline9-srj23-fixed-overlap-regressions.test.ts diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts index 4753b57cf..391ab6e22 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts @@ -5,12 +5,14 @@ import { type HighDensityRouteObstacle, type NodeWithPortPoints, } from "@tscircuit/high-density-b01" +import { pointToSegmentDistance } from "@tscircuit/math-utils" import type { ConnectivityMap } from "circuit-json-to-connectivity-map" import { isObstacleConnectedToRoute } from "lib/solvers/TraceWidthSolver/isObstacleConnectedToRoute" import type { Obstacle, SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" import type { HighDensityRoute } from "lib/types/high-density-types" import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" import { mapZToLayerName } from "lib/utils/mapZToLayerName" +import { minimumDistanceBetweenSegments } from "lib/utils/minimumDistanceBetweenSegments" type Pipeline9B01RerouterParams = { srj: SimpleRouteJson @@ -57,6 +59,7 @@ const B01_GRID_STEP = 0.05 const B01_LOW_RESOLUTION_CELL_SIZE = 0.2 const MAX_B01_ROUTING_WINDOW_SIZE = 15 const MAX_TERMINAL_VIA_ESCAPE_CANDIDATES = 64 +const RELAXED_VIA_TRACE_CLEARANCE = 0.1 const pointsAreEqual = ( left: HighDensityRoute["route"][number] | undefined, @@ -443,6 +446,10 @@ export class Pipeline9B01Rerouter { private readonly connMap?: ConnectivityMap private readonly terminalObstacles: Obstacle[] private readonly fixedObstacles: HighDensityObstacle[] + private readonly preloadedObstaclesByTraceId = new Map< + string, + HighDensityRouteObstacle[] + >() private readonly candidateObstacleCache = new WeakMap< HighDensityRoute[], Map @@ -460,14 +467,17 @@ export class Pipeline9B01Rerouter { params.srj.minViaDiameter ?? 0.3 const preloadedTraceObstacles = (params.srj.traces ?? []).flatMap( - (trace, traceIndex) => - convertPreloadedTraceToRouteObstacles( + (trace, traceIndex) => { + const obstacles = convertPreloadedTraceToRouteObstacles( trace, traceIndex, params.srj.layerCount, defaultViaDiameter, params.connMap, - ), + ) + this.preloadedObstaclesByTraceId.set(trace.pcb_trace_id, obstacles) + return obstacles + }, ) const baseRectObstacles = this.terminalObstacles.map( (obstacle, obstacleIndex) => @@ -481,6 +491,118 @@ export class Pipeline9B01Rerouter { this.fixedObstacles = [...baseRectObstacles, ...preloadedTraceObstacles] } + getPreloadedTraceIdForDrcTraceId( + drcTraceId: string | undefined, + ): string | undefined { + if (!drcTraceId) return undefined + for (const traceId of this.preloadedObstaclesByTraceId.keys()) { + if ( + drcTraceId === traceId || + drcTraceId.endsWith(`_${traceId}`) || + drcTraceId.includes(`_${traceId}_`) + ) { + return traceId + } + } + return undefined + } + + countRouteOverlapsWithPreloadedTrace( + route: HighDensityRoute, + preloadedTraceId: string, + ): number { + const fixedObstacles = + this.preloadedObstaclesByTraceId.get(preloadedTraceId) ?? [] + let overlapCount = 0 + + for (const fixedObstacle of fixedObstacles) { + for ( + let fixedIndex = 0; + fixedIndex < fixedObstacle.route.length - 1; + fixedIndex += 1 + ) { + const fixedStart = fixedObstacle.route[fixedIndex]! + const fixedEnd = fixedObstacle.route[fixedIndex + 1]! + if (fixedStart.z !== fixedEnd.z) continue + const requiredDistance = + fixedObstacle.traceThickness / 2 + route.traceThickness / 2 + + for ( + let candidateIndex = 0; + candidateIndex < route.route.length - 1; + candidateIndex += 1 + ) { + const candidateStart = route.route[candidateIndex]! + const candidateEnd = route.route[candidateIndex + 1]! + if ( + candidateStart.z !== candidateEnd.z || + candidateStart.z !== fixedStart.z + ) { + continue + } + if ( + minimumDistanceBetweenSegments( + candidateStart, + candidateEnd, + fixedStart, + fixedEnd, + ) < + requiredDistance - SAME_POINT_EPSILON + ) { + overlapCount += 1 + } + } + } + } + + overlapCount += this.getRouteViaCentersOverlappingPreloadedTrace( + route, + preloadedTraceId, + ).length + + return overlapCount + } + + getRouteViaCentersOverlappingPreloadedTrace( + route: HighDensityRoute, + preloadedTraceId: string, + ): Array<{ x: number; y: number }> { + const fixedObstacles = + this.preloadedObstaclesByTraceId.get(preloadedTraceId) ?? [] + const candidateVias = getRouteVias(route.route) + const overlappingVias: Array<{ x: number; y: number }> = [] + + for (const candidateVia of candidateVias) { + let overlaps = false + for (const fixedObstacle of fixedObstacles) { + const requiredDistance = + fixedObstacle.traceThickness / 2 + + route.viaDiameter / 2 + + RELAXED_VIA_TRACE_CLEARANCE + for ( + let fixedIndex = 0; + fixedIndex < fixedObstacle.route.length - 1; + fixedIndex += 1 + ) { + const fixedStart = fixedObstacle.route[fixedIndex]! + const fixedEnd = fixedObstacle.route[fixedIndex + 1]! + if (fixedStart.z !== fixedEnd.z) continue + if ( + pointToSegmentDistance(candidateVia, fixedStart, fixedEnd) < + requiredDistance - SAME_POINT_EPSILON + ) { + overlaps = true + break + } + } + if (overlaps) break + } + if (overlaps) overlappingVias.push(candidateVia) + } + + return overlappingVias + } + private getOwningTerminalObstacles( route: HighDensityRoute, point: HighDensityRoute["route"][number], diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts index f87c39797..a1279bc61 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts @@ -103,12 +103,7 @@ const PRELOADED_TERMINAL_MATCH_TOLERANCE = 1e-3 const ENDPOINT_SLIDE_RADII = [ 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.2, 0.25, 0.3, ] as const -const VIA_MICRO_SHIFT_RADII = [ - ...ENDPOINT_SLIDE_RADII, - 0.4, - 0.5, - 0.75, -] as const +const VIA_MICRO_SHIFT_RADII = [...ENDPOINT_SLIDE_RADII, 0.4, 0.5, 0.75] as const const MAX_VIA_GROUPS_PER_ROUTE = 3 const MAX_VIA_MICRO_SHIFT_DRC_EVALUATIONS_PER_SWEEP = 128 const MAX_VIA_MICRO_SHIFT_SNAPSHOT_ISSUES = 8 @@ -123,7 +118,7 @@ const DEFAULT_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES = const HIGH_INITIAL_DRC_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES = 64 const MAX_LOCAL_LAYER_DETOUR_EXPANSION = 6 const MAX_B01_FULL_ATTEMPTS_PER_ROUND = 18 -const MAX_B01_INTERIOR_ATTEMPTS_PER_ROUND = 24 +const MAX_B01_INTERIOR_ATTEMPTS_PER_ROUND = 48 const MAX_B01_FIXED_ONLY_ATTEMPTS_PER_ROUND = 8 const DEFAULT_MAX_B01_TOTAL_ITERATIONS = 300_000 const HIGH_INITIAL_DRC_MAX_B01_TOTAL_ITERATIONS = 200_000 @@ -173,7 +168,7 @@ const MAX_FINAL_ENDPOINT_SLIDE_DRC_EVALUATIONS = 32 const MAX_FINAL_CONTINUITY_TERMINAL_VIA_ATTEMPTS = 32 const MAX_FINAL_CONTINUITY_TERMINAL_VIA_DRC_EVALUATIONS = 8 const MAX_EARLY_FIXED_OVERLAP_LAYER_DETOUR_DRC_EVALUATIONS = 128 -const MAX_FINAL_FIXED_OVERLAP_LAYER_DETOUR_DRC_EVALUATIONS = 64 +const MAX_FINAL_FIXED_OVERLAP_LAYER_DETOUR_DRC_EVALUATIONS = 192 const POST_FINAL_COMPOSITE_INTERIOR_EXPANSIONS = [4, 8] as const const POST_FINAL_COMPOSITE_TERMINAL_PROXIMITY = 4 const FULL_B01_VARIANTS = [ @@ -467,6 +462,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private finalOwnerDrcEvaluations = 0 private finalOwnerCandidatesAccepted = 0 private finalOwnerIterations = 0 + private finalOwnerIterationLimit = MAX_FINAL_OWNER_B01_ITERATIONS private postRepairSameNetViaMergeAttempts = 0 private postRepairSameNetViaMergeDrcEvaluations = 0 private postRepairSameNetViaMergeCandidatesAccepted = 0 @@ -525,27 +521,32 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve }) this.terminalConstraints = normalizedParams.hdRoutes.flatMap( (route, routeIndex) => - (["start", "end"] as const).flatMap((endpoint) => { - const point = endpoint === "start" ? route.route[0] : route.route.at(-1) - if (!point) return [] - const traceRadius = - (route.traceThickness ?? params.srj.minTraceWidth) / 2 - const owningObstacles = params.originalObstacles.filter( - (obstacle) => - obstacleSharesRouteNet(obstacle, route) && - obstacleAppliesToLayer(obstacle, point.z, params.srj.layerCount) && - pointFitsInsideObstacle(point, obstacle, 0), - ) - return [ - { - routeIndex, - endpoint, - originalPoint: { ...point }, - traceRadius, - owningObstacles, - }, - ] - }), + (["start", "end"] as const).flatMap((endpoint) => { + const point = + endpoint === "start" ? route.route[0] : route.route.at(-1) + if (!point) return [] + const traceRadius = + (route.traceThickness ?? params.srj.minTraceWidth) / 2 + const owningObstacles = params.originalObstacles.filter( + (obstacle) => + obstacleSharesRouteNet(obstacle, route) && + obstacleAppliesToLayer( + obstacle, + point.z, + params.srj.layerCount, + ) && + pointFitsInsideObstacle(point, obstacle, 0), + ) + return [ + { + routeIndex, + endpoint, + originalPoint: { ...point }, + traceRadius, + owningObstacles, + }, + ] + }), ) } @@ -558,15 +559,10 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve ) } - private isFixedCopperIssue( - error: DrcError, - snapshot: DrcSnapshot, - ): boolean { + private isFixedCopperIssue(error: DrcError, snapshot: DrcSnapshot): boolean { const errorType = getErrorType(error) const primaryTraceId = - typeof error.pcb_trace_id === "string" - ? error.pcb_trace_id - : undefined + typeof error.pcb_trace_id === "string" ? error.pcb_trace_id : undefined const otherTraceId = getRawOtherTraceId(error) const primaryIsCandidate = Boolean( primaryTraceId && snapshot.traceRouteIndexById.has(primaryTraceId), @@ -578,11 +574,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve ) { return true } - if ( - errorType !== "pcb_trace_error" || - !primaryTraceId || - !otherTraceId - ) { + if (errorType !== "pcb_trace_error" || !primaryTraceId || !otherTraceId) { return false } const otherIsCandidate = snapshot.traceRouteIndexById.has(otherTraceId) @@ -861,13 +853,43 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve if (!errorCenter) return undefined const snapshot = this.getSnapshot(routes) - const viaGroups = this.getCandidateRouteIndexesForError(error, snapshot) - .flatMap((routeIndex) => { - const route = routes[routeIndex] - if (!route) return [] - return this.getViaTransitionGroups(route, routeIndex, errorCenter) - }) - .toSorted((left, right) => left.distanceToError - right.distanceToError) + const preloadedTraceId = + errorType === "pcb_via_trace_clearance_error" + ? this.b01Rerouter.getPreloadedTraceIdForDrcTraceId( + typeof error.pcb_trace_id === "string" + ? error.pcb_trace_id + : getRawOtherTraceId(error), + ) + : undefined + const viaGroups: ViaTransitionGroup[] = [] + const seenViaGroups = new Set() + for (const routeIndex of this.getCandidateRouteIndexesForError( + error, + snapshot, + )) { + const route = routes[routeIndex] + if (!route) continue + const overlappingViaCenters = preloadedTraceId + ? this.b01Rerouter.getRouteViaCentersOverlappingPreloadedTrace( + route, + preloadedTraceId, + ) + : [] + const referenceCenters = + overlappingViaCenters.length > 0 ? overlappingViaCenters : [errorCenter] + for (const referenceCenter of referenceCenters) { + for (const viaGroup of this.getViaTransitionGroups( + route, + routeIndex, + referenceCenter, + )) { + const key = `${viaGroup.routeIndex}:${viaGroup.indexes.join(",")}` + if (seenViaGroups.has(key)) continue + seenViaGroups.add(key) + viaGroups.push(viaGroup) + } + } + } if (viaGroups.length === 0) return undefined for (const viaGroup of viaGroups) { @@ -929,12 +951,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.viaMicroShiftAttempts += 1 sweepBudget.remaining -= 1 const materializedCandidate = materializeRoutes(candidateRoutes) - if ( - this.candidateImprovesSnapshot( - materializedCandidate, - snapshot, - ) - ) { + if (this.candidateImprovesSnapshot(materializedCandidate, snapshot)) { this.viaMicroShiftsAccepted += 1 return materializedCandidate } @@ -1079,12 +1096,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve point.y = candidatePoint.y } const materializedCandidate = materializeRoutes(candidateRoutes) - if ( - this.candidateImprovesSnapshot( - materializedCandidate, - snapshot, - ) - ) { + if (this.candidateImprovesSnapshot(materializedCandidate, snapshot)) { return materializedCandidate } } @@ -1259,8 +1271,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve ) } const isEligible = - (candidateFixedCopperIssueScore < - baselineFixedCopperIssueScore || + (candidateFixedCopperIssueScore < baselineFixedCopperIssueScore || (candidateFixedCopperIssueScore === baselineFixedCopperIssueScore && targetErrorWasRemoved)) && @@ -1293,12 +1304,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve } continue } - if ( - this.candidateImprovesSnapshot( - materializedCandidate, - snapshot, - ) - ) { + if (this.candidateImprovesSnapshot(materializedCandidate, snapshot)) { return materializedCandidate } } @@ -1350,12 +1356,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve if (!changed) break const materializedCandidate = materializeRoutes(candidateRoutes) - if ( - this.candidateImprovesSnapshot( - materializedCandidate, - snapshot, - ) - ) { + if (this.candidateImprovesSnapshot(materializedCandidate, snapshot)) { return materializedCandidate } candidateRoutes = cloneRoutes(materializedCandidate) @@ -1393,11 +1394,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve candidateRoutes[options.routeIndex] = result.route const materializedCandidate = materializeRoutes(candidateRoutes) if ( - !this.candidateImprovesSnapshot( - materializedCandidate, - snapshot, - "b01", - ) + !this.candidateImprovesSnapshot(materializedCandidate, snapshot, "b01") ) { return undefined } @@ -1563,6 +1560,29 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve seenWindows.add(key) windows.push({ startIndex, endIndex }) } + const addWindowIfB01Sized = (startIndex: number, endIndex: number) => { + const start = route.route[startIndex] + const end = route.route[endIndex] + if ( + !start || + !end || + Math.abs(start.x - end.x) > 15 + POSITION_EPSILON || + Math.abs(start.y - end.y) > 15 + POSITION_EPSILON + ) { + return + } + addRawWindow(startIndex, endIndex) + } + for (const expansion of [0, 1, 2, 4, 8, 12, 16, 24]) { + addWindowIfB01Sized( + Math.max(0, nearestSegmentIndex - expansion), + lastRouteIndex, + ) + addWindowIfB01Sized( + 0, + Math.min(lastRouteIndex, nearestSegmentIndex + 1 + expansion), + ) + } if (nearestSegmentIndex === 0) { for (const endIndex of [2, 4, 8]) { addRawWindow(0, Math.min(lastRouteIndex, endIndex)) @@ -2141,7 +2161,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve private getRemainingFinalOwnerIterations(): number { return Math.max( 0, - MAX_FINAL_OWNER_B01_ITERATIONS - this.finalOwnerIterations, + this.finalOwnerIterationLimit - this.finalOwnerIterations, ) } @@ -3477,6 +3497,47 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve ) } + private hasFixedTraceGeometryProgress( + baselineRoutes: HighDensityRoute[], + candidateRoutes: HighDensityRoute[], + snapshot: DrcSnapshot, + routeIndex: number, + ): boolean { + const baselineRoute = baselineRoutes[routeIndex] + const candidateRoute = candidateRoutes[routeIndex] + if (!baselineRoute || !candidateRoute) return false + + for (const error of snapshot.errors) { + if ( + !this.getCandidateRouteIndexesForError(error, snapshot).includes( + routeIndex, + ) + ) { + continue + } + const preloadedTraceId = + this.b01Rerouter.getPreloadedTraceIdForDrcTraceId( + typeof error.pcb_trace_id === "string" + ? error.pcb_trace_id + : getRawOtherTraceId(error), + ) + if (!preloadedTraceId) continue + const baselineOverlapCount = + this.b01Rerouter.countRouteOverlapsWithPreloadedTrace( + baselineRoute, + preloadedTraceId, + ) + const candidateOverlapCount = + this.b01Rerouter.countRouteOverlapsWithPreloadedTrace( + candidateRoute, + preloadedTraceId, + ) + if (candidateOverlapCount < baselineOverlapCount) return true + } + + return false + } + private tryAnchoredFixedCopperCandidate( routes: HighDensityRoute[], snapshot: DrcSnapshot, @@ -3544,12 +3605,21 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.anchoredFixedCopperDrcEvaluations += 1 this.cleanupCandidateAttempts += 1 const candidateSnapshot = this.getSnapshot(materializedCandidate) - if ( - !this.snapshotImprovesWithoutFixedCopperRegression( - candidateSnapshot, + const snapshotImproved = this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + snapshot, + ) + const fixedGeometryImproved = + candidateSnapshot.count <= snapshot.count && + this.getFixedCopperIssueScore(candidateSnapshot) <= + this.getFixedCopperIssueScore(snapshot) && + this.hasFixedTraceGeometryProgress( + routes, + materializedCandidate, snapshot, + routeIndex, ) - ) { + if (!snapshotImproved && !fixedGeometryImproved) { return undefined } @@ -3580,19 +3650,38 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve >() for (const error of snapshot.errors) { const center = this.getErrorCenter(error) - if (!center) continue for (const routeIndex of this.getCandidateRouteIndexesForError( error, snapshot, )) { - const centers = centersByRouteIndex.get(routeIndex) ?? [] - if ( - !centers.some( - (existingCenter) => - getPointDistance(existingCenter, center) <= POSITION_EPSILON, + const preloadedTraceId = + this.b01Rerouter.getPreloadedTraceIdForDrcTraceId( + typeof error.pcb_trace_id === "string" + ? error.pcb_trace_id + : getRawOtherTraceId(error), ) - ) { - centers.push(center) + const viaCenters = + getErrorType(error) === "pcb_via_trace_clearance_error" && + preloadedTraceId && + improvedRoutes[routeIndex] + ? this.b01Rerouter.getRouteViaCentersOverlappingPreloadedTrace( + improvedRoutes[routeIndex]!, + preloadedTraceId, + ) + : [] + const repairCenters = [...viaCenters, ...(center ? [center] : [])] + if (repairCenters.length === 0) continue + const centers = centersByRouteIndex.get(routeIndex) ?? [] + for (const repairCenter of repairCenters) { + if ( + !centers.some( + (existingCenter) => + getPointDistance(existingCenter, repairCenter) <= + POSITION_EPSILON, + ) + ) { + centers.push(repairCenter) + } } centersByRouteIndex.set(routeIndex, centers) } @@ -4253,10 +4342,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve const center = this.getErrorCenter(error) if (!center) return undefined const snapshot = this.getSnapshot(routes) - const routeIndex = this.getCandidateRouteIndexesForError( - error, - snapshot, - )[0] + const routeIndex = this.getCandidateRouteIndexesForError(error, snapshot)[0] const route = routeIndex === undefined ? undefined : routes[routeIndex] if (!route || route.route.length < 2) return undefined @@ -4290,8 +4376,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve 0, Math.min( 1, - ((center.x - start.x) * deltaX + - (center.y - start.y) * deltaY) / + ((center.x - start.x) * deltaX + (center.y - start.y) * deltaY) / (segmentLength * segmentLength), ), ) @@ -4360,10 +4445,21 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve const targetErrorIdentity = this.getDrcErrorIdentity(error) const baselineFixedScore = this.getFixedCopperIssueScore(snapshot) + const fixedTraceId = this.b01Rerouter.getPreloadedTraceIdForDrcTraceId( + typeof error.pcb_trace_id === "string" + ? error.pcb_trace_id + : getRawOtherTraceId(error), + ) + const baselineTargetOverlapCount = fixedTraceId + ? this.b01Rerouter.countRouteOverlapsWithPreloadedTrace( + route, + fixedTraceId, + ) + : 0 let bestCandidate: | { routes: HighDensityRoute[]; snapshot: DrcSnapshot } | undefined - for (const halfSpan of [0.35, 0.5, 0.75, 1, 1.5, 2, 3]) { + for (const halfSpan of [0.35, 0.5, 0.75, 1, 1.5, 2, 3, 5, 8, 12, 20, 40]) { const startDistance = Math.max( cumulativeDistances[sameLayerStart]!, projectedRouteDistance - halfSpan, @@ -4422,8 +4518,17 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve (candidateError) => this.getDrcErrorIdentity(candidateError) === targetErrorIdentity, ) + const targetOverlapCount = fixedTraceId + ? this.b01Rerouter.countRouteOverlapsWithPreloadedTrace( + materializedCandidate[routeIndex]!, + fixedTraceId, + ) + : baselineTargetOverlapCount + const targetGeometryImproved = + fixedTraceId !== undefined && + targetOverlapCount < baselineTargetOverlapCount if ( - !targetWasRemoved || + (!targetWasRemoved && !targetGeometryImproved) || candidateFixedScore > baselineFixedScore || candidateSnapshot.count > snapshot.count + maxIssueCountIncrease ) { @@ -4468,8 +4573,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve const evaluationsBeforeSweep = this.localCleanupDrcEvaluations this.selectedLocalCleanupDrcEvaluationLimit = evaluationsBeforeSweep + drcEvaluationLimit - this.selectedConsecutiveLocalCleanupDrcMissLimit = - drcEvaluationLimit + this.selectedConsecutiveLocalCleanupDrcMissLimit = drcEvaluationLimit this.consecutiveLocalCleanupDrcMisses = 0 try { @@ -4490,8 +4594,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve : undefined const otherTraceId = getRawOtherTraceId(error) const primaryIsCandidate = Boolean( - primaryTraceId && - snapshot.traceRouteIndexById.has(primaryTraceId), + primaryTraceId && snapshot.traceRouteIndexById.has(primaryTraceId), ) const otherIsCandidate = Boolean( otherTraceId && snapshot.traceRouteIndexById.has(otherTraceId), @@ -4503,14 +4606,10 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve error, options.maxIssueCountIncrease ?? 2, ) - nextRoutes ??= this.tryLocalTraceLayerDetour( - improvedRoutes, - error, - { - preferFixedCopperIssueReduction: true, - maxIssueCountIncrease: options.maxIssueCountIncrease, - }, - ) + nextRoutes ??= this.tryLocalTraceLayerDetour(improvedRoutes, error, { + preferFixedCopperIssueReduction: true, + maxIssueCountIncrease: options.maxIssueCountIncrease, + }) if (nextRoutes) break if (!this.hasLocalCleanupBudget()) break } @@ -4872,12 +4971,10 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve improvedRoutes = nextRoutes } improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) - improvedRoutes = - this.runFinalFixedOverlapLayerDetour(improvedRoutes, { - drcEvaluationLimit: - MAX_EARLY_FIXED_OVERLAP_LAYER_DETOUR_DRC_EVALUATIONS, - maxIssueCountIncrease: 2, - }) + improvedRoutes = this.runFinalFixedOverlapLayerDetour(improvedRoutes, { + drcEvaluationLimit: MAX_EARLY_FIXED_OVERLAP_LAYER_DETOUR_DRC_EVALUATIONS, + maxIssueCountIncrease: 2, + }) improvedRoutes = this.runPostClusterViaMicroShiftCleanup(improvedRoutes) for (let round = 0; round < MAX_B01_PHASE_ROUNDS; round += 1) { @@ -4918,8 +5015,9 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve improvedRoutes = this.runPostFinalCompositeRepair(improvedRoutes) improvedRoutes = this.runAnchoredFixedCopperRepair(improvedRoutes) improvedRoutes = this.runFixedCopperCompositeRepair(improvedRoutes) - improvedRoutes = - this.runFinalFixedOverlapLayerDetour(improvedRoutes) + improvedRoutes = this.runFinalFixedOverlapLayerDetour(improvedRoutes) + this.finalOwnerIterationLimit += MAX_FINAL_OWNER_B01_ITERATIONS + improvedRoutes = this.runB01FinalErrorOwnerSweep(improvedRoutes) improvedRoutes = this.runPostClusterViaMicroShiftCleanup(improvedRoutes) improvedRoutes = this.runFinalEndpointSlideCleanup(improvedRoutes) improvedRoutes = this.runFinalContinuityTerminalViaBridge(improvedRoutes) @@ -5004,7 +5102,7 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve pipeline9FinalOwnerDrcEvaluations: this.finalOwnerDrcEvaluations, pipeline9FinalOwnerCandidatesAccepted: this.finalOwnerCandidatesAccepted, pipeline9FinalOwnerIterations: this.finalOwnerIterations, - pipeline9FinalOwnerIterationLimit: MAX_FINAL_OWNER_B01_ITERATIONS, + pipeline9FinalOwnerIterationLimit: this.finalOwnerIterationLimit, pipeline9PostRepairSameNetViaMergeAttempts: this.postRepairSameNetViaMergeAttempts, pipeline9PostRepairSameNetViaMergeDrcEvaluations: @@ -5103,18 +5201,16 @@ export class Pipeline9ExactDrcRepairSolver extends GlobalDrcBranchPortfolioSolve this.finalFixedOverlapLayerDetourCandidatesAccepted, pipeline9FinalFixedOverlapLayerDetourDrcEvaluationLimit: MAX_FINAL_FIXED_OVERLAP_LAYER_DETOUR_DRC_EVALUATIONS, - pipeline9FinalFixedOverlapBestRemovedTargetIssueCount: - Number.isFinite( - this.finalFixedOverlapBestRemovedTargetIssueCount, - ) - ? this.finalFixedOverlapBestRemovedTargetIssueCount - : undefined, - pipeline9FinalFixedOverlapBestRemovedTargetFixedScore: - Number.isFinite( - this.finalFixedOverlapBestRemovedTargetFixedScore, - ) - ? this.finalFixedOverlapBestRemovedTargetFixedScore - : undefined, + pipeline9FinalFixedOverlapBestRemovedTargetIssueCount: Number.isFinite( + this.finalFixedOverlapBestRemovedTargetIssueCount, + ) + ? this.finalFixedOverlapBestRemovedTargetIssueCount + : undefined, + pipeline9FinalFixedOverlapBestRemovedTargetFixedScore: Number.isFinite( + this.finalFixedOverlapBestRemovedTargetFixedScore, + ) + ? this.finalFixedOverlapBestRemovedTargetFixedScore + : undefined, } this.progress = 1 this.solved = true diff --git a/tests/features/pipeline9-b01-rerouter.test.ts b/tests/features/pipeline9-b01-rerouter.test.ts index 3d094b6b0..f755c2801 100644 --- a/tests/features/pipeline9-b01-rerouter.test.ts +++ b/tests/features/pipeline9-b01-rerouter.test.ts @@ -102,3 +102,68 @@ test("Pipeline9 B01 keeps off-grid endpoint layer transitions vertical", () => { }), ).toBe(true) }) + +test("Pipeline9 B01 measures hidden fixed-trace crossings and offending vias", () => { + const srj: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + minViaDiameter: 0.3, + bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 }, + obstacles: [], + connections: [], + traces: [ + { + type: "pcb_trace", + pcb_trace_id: "fixed_wall", + connection_name: "fixed_net", + route: [ + { route_type: "wire", x: 0, y: -2, width: 0.1, layer: "top" }, + { route_type: "wire", x: 0, y: 2, width: 0.1, layer: "top" }, + ], + }, + ], + } + const rerouter = new Pipeline9B01Rerouter({ + srj, + baseObstacles: [], + }) + const twiceCrossingRoute: HighDensityRoute = { + connectionName: "candidate", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [], + route: [ + { x: -1, y: -1, z: 0 }, + { x: 1, y: 0, z: 0 }, + { x: -1, y: 1, z: 0 }, + ], + } + const viaRoute: HighDensityRoute = { + connectionName: "candidate_via", + traceThickness: 0.1, + viaDiameter: 0.3, + vias: [{ x: 0.2, y: 0 }], + route: [ + { x: -1, y: 0, z: 0 }, + { x: 0.2, y: 0, z: 0 }, + { x: 0.2, y: 0, z: 1 }, + { x: 1, y: 0, z: 1 }, + ], + } + + expect( + rerouter.countRouteOverlapsWithPreloadedTrace( + twiceCrossingRoute, + "fixed_wall", + ), + ).toBe(2) + expect( + rerouter.getRouteViaCentersOverlappingPreloadedTrace( + viaRoute, + "fixed_wall", + ), + ).toEqual([{ x: 0.2, y: 0 }]) + expect( + rerouter.getPreloadedTraceIdForDrcTraceId("preloaded_0_fixed_wall"), + ).toBe("fixed_wall") +}) diff --git a/tests/features/pipeline9-srj23-fixed-overlap-regressions.test.ts b/tests/features/pipeline9-srj23-fixed-overlap-regressions.test.ts new file mode 100644 index 000000000..49b449764 --- /dev/null +++ b/tests/features/pipeline9-srj23-fixed-overlap-regressions.test.ts @@ -0,0 +1,38 @@ +import { expect, test } from "bun:test" +import { AutoroutingPipelineSolver9_PreloadedTraceGraph } from "lib" +import { evaluateRelaxedDrc } from "lib/testing/evaluate-relaxed-drc" +import { loadScenarioBySampleNumber } from "../../scripts/benchmark/scenarios" + +test( + "Pipeline9 resolves hidden fixed-trace overlaps in srj23", + async () => { + const failures: Array<{ sampleNumber: number; errors: unknown[] }> = [] + + for (const sampleNumber of [38, 91]) { + const { scenario } = await loadScenarioBySampleNumber( + "srj23", + sampleNumber, + ) + const solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph( + scenario, + { + cacheProvider: null, + effort: 1, + }, + ) + solver.solve() + + const { errors } = evaluateRelaxedDrc({ + inputSrj: scenario, + srjWithPointPairs: solver.srjWithPointPairs!, + traces: solver.getOutputSimplifiedPcbTraces(), + }) + if (errors.length > 0) { + failures.push({ sampleNumber, errors }) + } + } + + expect(failures).toEqual([]) + }, + { timeout: 600_000 }, +) From bbfe76e9f7735a311c175fe3ce34990bf3debdb6 Mon Sep 17 00:00:00 2001 From: seveibar Date: Mon, 27 Jul 2026 09:53:08 -0700 Subject: [PATCH 13/22] fix preloaded trace layer visualization --- .../UniformPortDistributionSolver.ts | 2 + .../visualizeUniformPortDistribution.ts | 26 +++++++++- lib/utils/convertSrjToGraphicsObject.ts | 2 +- ...t-distribution-layer-visualization.test.ts | 52 +++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 tests/features/uniform-port-distribution-layer-visualization.test.ts diff --git a/lib/solvers/UniformPortDistributionSolver/UniformPortDistributionSolver.ts b/lib/solvers/UniformPortDistributionSolver/UniformPortDistributionSolver.ts index 8cbd0fa0b..0c5c8b37c 100644 --- a/lib/solvers/UniformPortDistributionSolver/UniformPortDistributionSolver.ts +++ b/lib/solvers/UniformPortDistributionSolver/UniformPortDistributionSolver.ts @@ -23,6 +23,7 @@ export interface UniformPortDistributionSolverInput { nodeWithPortPoints: NodeWithPortPoints[] inputNodesWithPortPoints: InputNodeWithPortPoints[] obstacles: Obstacle[] + layerCount: number } /** @@ -183,6 +184,7 @@ export class UniformPortDistributionSolver extends BaseSolver { ownerPairsToProcess: this.ownerPairsToProcess, currentOwnerPairBeingProcessed: this.currentOwnerPairBeingProcessed, mapOfNodeIdToBounds: this.mapOfNodeIdToBounds, + layerCount: this.input.layerCount, }) } } diff --git a/lib/solvers/UniformPortDistributionSolver/visualizeUniformPortDistribution.ts b/lib/solvers/UniformPortDistributionSolver/visualizeUniformPortDistribution.ts index f371c8097..47e7e8c8b 100644 --- a/lib/solvers/UniformPortDistributionSolver/visualizeUniformPortDistribution.ts +++ b/lib/solvers/UniformPortDistributionSolver/visualizeUniformPortDistribution.ts @@ -1,6 +1,13 @@ import { GraphicsObject, Line, Rect } from "graphics-debug" import { Obstacle } from "lib/types" import { NodeWithPortPoints } from "lib/types/high-density-types" +import { safeTransparentize } from "lib/solvers/colors" +import { getGraphicsLayerColor } from "lib/utils/convertSrjToGraphicsObject" +import { + getGraphicsLayerForObstacle, + getGraphicsZLayersForObstacle, +} from "lib/utils/getGraphicsObjectLayer" +import { mapZToLayerName } from "lib/utils/mapZToLayerName" import { Bounds, OwnerPairKey, @@ -20,6 +27,7 @@ export const visualizeUniformPortDistribution = ({ ownerPairsToProcess, currentOwnerPairBeingProcessed, mapOfNodeIdToBounds, + layerCount, }: { obstacles: Obstacle[] nodeWithPortPoints: NodeWithPortPoints[] @@ -28,10 +36,26 @@ export const visualizeUniformPortDistribution = ({ ownerPairsToProcess: OwnerPairKey[] currentOwnerPairBeingProcessed: OwnerPairKey | null mapOfNodeIdToBounds: Map + layerCount: number }): GraphicsObject => { const rects: Rect[] = obstacles .filter((o) => !o.isCopperPour) - .map((o) => ({ ...o, fill: "#ec000070" })) + .map((o) => { + const zLayers = getGraphicsZLayersForObstacle(o, layerCount) + const fill = + zLayers.length === 1 + ? safeTransparentize( + getGraphicsLayerColor(mapZToLayerName(zLayers[0]!, layerCount)), + 0.56, + ) + : "rgba(128,0,128,0.44)" + + return { + ...o, + fill, + layer: getGraphicsLayerForObstacle(o, layerCount), + } + }) const points: Array<{ x: number; y: number; label?: string }> = [] const lines: Line[] = [] diff --git a/lib/utils/convertSrjToGraphicsObject.ts b/lib/utils/convertSrjToGraphicsObject.ts index 8b5a953e2..8a3deeaa5 100644 --- a/lib/utils/convertSrjToGraphicsObject.ts +++ b/lib/utils/convertSrjToGraphicsObject.ts @@ -35,7 +35,7 @@ export type ConvertSrjToGraphicsObjectOptions = { traceColorMode?: TraceColorMode } -function getGraphicsLayerColor(layerName: string): string { +export function getGraphicsLayerColor(layerName: string): string { if (!Object.hasOwn(GRAPHICS_LAYER_COLORS, layerName)) { throw new Error(`No visualization color for layer "${layerName}"`) } diff --git a/tests/features/uniform-port-distribution-layer-visualization.test.ts b/tests/features/uniform-port-distribution-layer-visualization.test.ts new file mode 100644 index 000000000..f407443ff --- /dev/null +++ b/tests/features/uniform-port-distribution-layer-visualization.test.ts @@ -0,0 +1,52 @@ +import { expect, test } from "bun:test" +import { visualizeUniformPortDistribution } from "lib/solvers/UniformPortDistributionSolver/visualizeUniformPortDistribution" +import type { Obstacle } from "lib/types" + +test("uniform port distribution visualizes obstacles on their copper layers", () => { + const obstacles: Obstacle[] = [ + { + obstacleId: "top-trace-segment", + type: "rect", + layers: ["top"], + __zLayers: [0], + center: { x: 0, y: 0 }, + width: 1, + height: 0.15, + connectedTo: [], + }, + { + obstacleId: "bottom-trace-segment", + type: "rect", + layers: ["bottom"], + __zLayers: [1], + center: { x: 0, y: 1 }, + width: 1, + height: 0.15, + connectedTo: [], + }, + ] + + const graphics = visualizeUniformPortDistribution({ + obstacles, + nodeWithPortPoints: [], + mapOfOwnerPairToPortPoints: new Map(), + mapOfOwnerPairToSharedEdge: new Map(), + ownerPairsToProcess: [], + currentOwnerPairBeingProcessed: null, + mapOfNodeIdToBounds: new Map(), + layerCount: 2, + }) + + expect(graphics.rects).toMatchObject([ + { + obstacleId: "top-trace-segment", + layer: "z0", + fill: "rgba(255,0,0,0.44)", + }, + { + obstacleId: "bottom-trace-segment", + layer: "z1", + fill: "rgba(0,0,255,0.44)", + }, + ]) +}) From 735f27980bb6dc60b865df17efb3b75d4789e923 Mon Sep 17 00:00:00 2001 From: seveibar Date: Mon, 27 Jul 2026 14:38:05 -0700 Subject: [PATCH 14/22] fix: preload Pipeline9 routes as graph assignments --- .../preloaded-trace-graph-solver.ts | 22 +- .../TinyHypergraphPortPointPathingSolver.ts | 499 ++++-------------- .../serializePreloadedTraceAssignments.ts | 297 +++++++++++ .../SameNetViaMergerSolver.ts | 37 +- package.json | 2 +- .../pipeline9-srj23-routing-bounds.test.ts | 2 +- .../preloaded-fixed-segment-occupancy.test.ts | 48 +- ...net-via-merger-direct-overlap-star.test.ts | 45 ++ 8 files changed, 533 insertions(+), 419 deletions(-) create mode 100644 lib/solvers/PortPointPathingSolver/tinyhypergraph/serializePreloadedTraceAssignments.ts diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts index ee201f671..07daca9dc 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts @@ -109,7 +109,11 @@ const getPreloadedTracePrimitives = ( } } - for (let pointIndex = 0; pointIndex < trace.route.length - 1; pointIndex++) { + for ( + let pointIndex = 0; + pointIndex < trace.route.length - 1; + pointIndex++ + ) { const start = trace.route[pointIndex]! const end = trace.route[pointIndex + 1]! if ( @@ -141,7 +145,15 @@ const getClosestPortPoint = ( z: number, ): SegmentPortPoint | undefined => segment.portPoints - .filter((portPoint) => portPoint.availableZ.includes(z)) + .filter( + (portPoint) => + portPoint.availableZ.includes(z) && + !(portPoint._preloadedTracePortAssignments ?? []).some( + (assignment) => + assignment.z === z && + assignment.fixedNetId !== primitive.fixedNetId, + ), + ) .map((portPoint) => ({ portPoint, distance: pointToSegmentDistance( @@ -192,14 +204,12 @@ const preloadPort = ( fixedNetId: primitive.fixedNetId, routePosition: primitive.routePositionStart + - projection * - (primitive.routePositionEnd - primitive.routePositionStart), + projection * (primitive.routePositionEnd - primitive.routePositionStart), z, traceX: primitive.start.x + projection * dx, traceY: primitive.start.y + projection * dy, } - const existingAssignments = - portPoint._preloadedTracePortAssignments ?? [] + const existingAssignments = portPoint._preloadedTracePortAssignments ?? [] if ( !existingAssignments.some( (existing) => diff --git a/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts b/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts index 957327328..3976a862e 100644 --- a/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts +++ b/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts @@ -13,7 +13,6 @@ import type { } from "lib/types/high-density-types" import { getIntraNodeCrossingsUsingCircle } from "lib/utils/getIntraNodeCrossingsUsingCircle" import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" -import { minimumDistanceBetweenSegments } from "lib/utils/minimumDistanceBetweenSegments" import { DuplicateCongestedPortSolver, orderConnectionsByNetCardinality, @@ -26,6 +25,7 @@ import { type TinyHyperGraphSectionSolverOptions, type TinyHyperGraphSolverOptions, } from "tiny-hypergraph/lib/index" +import { applyInitialAssignments } from "tiny-hypergraph/lib/initialAssignments" import type { ConnectionHg, ConnectionHgWithSimpleRouteConnection, @@ -34,10 +34,17 @@ import type { import type { PreloadedTracePortAssignment } from "lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver" import { createTinyRouteNetIndexer } from "./createTinyRouteNetIndexer" import { getRegionNetIdByRegionId } from "./getRegionNetIdByRegionId" +import { + getSerializedPreloadedTraceStats, + isPreloadedTraceConnectionId, + serializePreloadedTraceAssignments, +} from "./serializePreloadedTraceAssignments" type RouteMetadata = { connectionId: string mutuallyConnectedNetworkId: string + startRegionId?: string + endRegionId?: string simpleRouteConnection?: HgPortPointPathingSolverParams["connections"][number]["simpleRouteConnection"] } @@ -64,6 +71,9 @@ type TinyBounds = { type TinyRegionMetadata = { bounds?: TinyBounds + netId?: number + NetId?: number + serializedRegionId?: string _qfpRegionType?: InputNodeWithPortPoints["_qfpRegionType"] _isNarrowQfpPadGap?: boolean _offBoardConnectionId?: string @@ -276,8 +286,7 @@ const toSerializedPortData = ( tinyHypergraphPortPenalty: port.d.tinyHypergraphPortPenalty, cramped: port.d.cramped, _preloadedFixedNetIds: port.d._preloadedFixedNetIds, - _preloadedTracePortAssignments: - port.d._preloadedTracePortAssignments, + _preloadedTracePortAssignments: port.d._preloadedTracePortAssignments, } } @@ -465,12 +474,14 @@ const buildSerializedTinyGraph = ( } as SerializedTinySolvedRoute) } - return { + const serializedHyperGraph = { regions, ports, connections, solvedRoutes, } satisfies SerializedHyperGraph + serializePreloadedTraceAssignments(serializedHyperGraph) + return serializedHyperGraph } const buildInputNodesWithPortPoints = ( @@ -575,6 +586,50 @@ const applyTerminalRegionNetIds = (loaded: LoadedTinyGraph) => { } } +const clearPreloadedEndpointRegionNetIds = (loaded: LoadedTinyGraph) => { + const regionIndexBySerializedId = new Map() + loaded.topology.regionMetadata?.forEach((metadata, regionIndex) => { + if (typeof metadata.serializedRegionId === "string") { + regionIndexBySerializedId.set(metadata.serializedRegionId, regionIndex) + } + }) + + const activeEndpointRegionIds = new Set() + for (const routeMetadata of loaded.problem.routeMetadata ?? []) { + if (isPreloadedTraceConnectionId(routeMetadata.connectionId)) continue + if (typeof routeMetadata.startRegionId === "string") { + activeEndpointRegionIds.add(routeMetadata.startRegionId) + } + if (typeof routeMetadata.endRegionId === "string") { + activeEndpointRegionIds.add(routeMetadata.endRegionId) + } + } + + for (const routeMetadata of loaded.problem.routeMetadata ?? []) { + if (!isPreloadedTraceConnectionId(routeMetadata.connectionId)) continue + for (const serializedRegionId of [ + routeMetadata.startRegionId, + routeMetadata.endRegionId, + ]) { + if ( + typeof serializedRegionId !== "string" || + activeEndpointRegionIds.has(serializedRegionId) + ) { + continue + } + const regionIndex = regionIndexBySerializedId.get(serializedRegionId) + if (regionIndex === undefined) continue + const metadata = loaded.topology.regionMetadata?.[regionIndex] + const hasExplicitNetId = + typeof metadata?.netId === "number" || + typeof metadata?.NetId === "number" + if (!hasExplicitNetId) { + loaded.problem.regionNetId[regionIndex] = -1 + } + } + } +} + const applyPortMetadataPenalties = ( loaded: LoadedTinyGraph, crampedPortTraversalPenalty: number, @@ -634,326 +689,30 @@ const applyMetadataPortPenalties = (loaded: LoadedTinyGraph) => { return metadataPortPenaltyCount } -const createPreloadedFixedNetIndexer = ( - solver: TinyHyperGraphSolver & LoadedTinyGraph, -) => { - const netIndexByAlias = new Map() - let nextFixedNetIndex = 0 - - for ( - let routeIndex = 0; - routeIndex < solver.problem.routeNet.length; - routeIndex++ - ) { - const routeNetIndex = solver.problem.routeNet[routeIndex]! - nextFixedNetIndex = Math.max(nextFixedNetIndex, routeNetIndex + 1) - const routeMetadata = solver.problem.routeMetadata?.[routeIndex] - const simpleRouteConnection = routeMetadata?.simpleRouteConnection - for (const alias of [ - routeMetadata?.connectionId, - routeMetadata?.mutuallyConnectedNetworkId, - simpleRouteConnection?.name, - simpleRouteConnection?.__netConnectionName, - ...(simpleRouteConnection?.__rootConnectionNames ?? []), - ]) { - if (typeof alias === "string" && alias.length > 0) { - netIndexByAlias.set(alias, routeNetIndex) - } - } - } - for (const regionNetIndex of solver.problem.regionNetId) { - if (regionNetIndex >= 0) { - nextFixedNetIndex = Math.max(nextFixedNetIndex, regionNetIndex + 1) - } - } - - const fixedNetIndexById = new Map() - const getFixedNetIndex = (fixedNetId: string) => { - const activeNetIndex = netIndexByAlias.get(fixedNetId) - if (activeNetIndex !== undefined) return activeNetIndex - - let fixedNetIndex = fixedNetIndexById.get(fixedNetId) - if (fixedNetIndex === undefined) { - fixedNetIndex = nextFixedNetIndex++ - fixedNetIndexById.set(fixedNetId, fixedNetIndex) - } - return fixedNetIndex - } - - return getFixedNetIndex -} - -const applyPreloadedPortReservations = ( - solver: TinyHyperGraphSolver & LoadedTinyGraph, - getFixedNetIndex: (fixedNetId: string) => number, -) => { - let preloadedPortCount = 0 - const problemSetup = solver.problemSetup - for (let portId = 0; portId < solver.topology.portCount; portId++) { - const fixedNetIds = - solver.topology.portMetadata?.[portId]?._preloadedFixedNetIds - if (!Array.isArray(fixedNetIds) || fixedNetIds.length === 0) continue - - preloadedPortCount++ - const reservedNetIds = [ - ...new Set(fixedNetIds.map(getFixedNetIndex)), - ].sort((left, right) => left - right) - const endpointNetIds = problemSetup.portEndpointNetIds[portId]! - for (const reservedNetId of reservedNetIds) { - endpointNetIds.add(reservedNetId) - } - - const existingReservation = - problemSetup.portEndpointReservationNetId[portId]! - const combinedReservations = new Set(reservedNetIds) - if (existingReservation >= 0) { - combinedReservations.add(existingReservation) - } else if (existingReservation === -2) { - combinedReservations.add(-2) - } - problemSetup.portEndpointReservationNetId[portId] = - combinedReservations.size === 1 - ? [...combinedReservations][0]! - : -2 - } - - return preloadedPortCount -} - -type PreloadedFixedSegment = { - traceId: string - fixedNetIndex: number - regionId: number - fromPortId: number - toPortId: number - z: number - traceStart: { x: number; y: number } - traceEnd: { x: number; y: number } -} - -const getPreloadedFixedSegments = ( - solver: TinyHyperGraphSolver & LoadedTinyGraph, - getFixedNetIndex: (fixedNetId: string) => number, -): PreloadedFixedSegment[] => { - const assignmentsByTraceId = new Map< - string, - Array - >() - - for (let portId = 0; portId < solver.topology.portCount; portId++) { - for (const assignment of solver.topology.portMetadata?.[portId] - ?._preloadedTracePortAssignments ?? []) { - const traceAssignments = - assignmentsByTraceId.get(assignment.traceId) ?? [] - traceAssignments.push({ ...assignment, portId }) - assignmentsByTraceId.set(assignment.traceId, traceAssignments) - } - } - - const segments: PreloadedFixedSegment[] = [] - for (const [traceId, assignments] of assignmentsByTraceId) { - assignments.sort( - (left, right) => - left.routePosition - right.routePosition || - left.z - right.z || - left.portId - right.portId, - ) - const orderedAssignments = assignments.filter( - (assignment, index) => - index === 0 || - assignment.portId !== assignments[index - 1]!.portId, - ) - - for (let index = 1; index < orderedAssignments.length; index++) { - const from = orderedAssignments[index - 1]! - const to = orderedAssignments[index]! - if (from.z !== to.z) continue - const sharedRegionIds = ( - solver.topology.incidentPortRegion[from.portId] ?? [] - ).filter((regionId) => - (solver.topology.incidentPortRegion[to.portId] ?? []).includes( - regionId, - ), - ) - if (sharedRegionIds.length === 0) continue - - for (const regionId of sharedRegionIds) { - segments.push({ - traceId, - fixedNetIndex: getFixedNetIndex(from.fixedNetId), - regionId, - fromPortId: from.portId, - toPortId: to.portId, - z: from.z, - traceStart: { - x: - typeof from.traceX === "number" - ? from.traceX - : Number(solver.topology.portMetadata?.[from.portId]?.x ?? 0), - y: - typeof from.traceY === "number" - ? from.traceY - : Number(solver.topology.portMetadata?.[from.portId]?.y ?? 0), - }, - traceEnd: { - x: - typeof to.traceX === "number" - ? to.traceX - : Number(solver.topology.portMetadata?.[to.portId]?.x ?? 0), - y: - typeof to.traceY === "number" - ? to.traceY - : Number(solver.topology.portMetadata?.[to.portId]?.y ?? 0), - }, - }) - } - } - } - - return segments -} - -const hasPreloadedSegmentInCache = ( - solver: TinyHyperGraphSolver, - segment: PreloadedFixedSegment, -) => { - const geometry = { - ...solver.populateSegmentGeometryScratch( - segment.regionId, - segment.fromPortId, - segment.toPortId, - ), - } - const cache = solver.state.regionIntersectionCaches[segment.regionId] - for (let index = 0; index < cache.netIds.length; index++) { - if ( - cache.netIds[index] === segment.fixedNetIndex && - cache.lesserAngles[index] === geometry.lesserAngle && - cache.greaterAngles[index] === geometry.greaterAngle && - cache.layerMasks[index] === geometry.layerMask - ) { - return true - } - } - return false -} - -const ensurePreloadedFixedSegments = ( - solver: TinyHyperGraphSolver, - segments: PreloadedFixedSegment[], -) => { - let preloadedFixedSegmentCount = 0 - const previousRouteNetId = solver.state.currentRouteNetId - try { - for (const segment of segments) { - if (!hasPreloadedSegmentInCache(solver, segment)) { - solver.state.currentRouteNetId = segment.fixedNetIndex - solver.appendSegmentToRegionCache( - segment.regionId, - segment.fromPortId, - segment.toPortId, - ) - } - preloadedFixedSegmentCount++ - } - } finally { - solver.state.currentRouteNetId = previousRouteNetId - } - return preloadedFixedSegmentCount -} - -const segmentsCrossOnSameLayer = ( - solver: TinyHyperGraphSolver, - regionId: number, - firstFromPortId: number, - firstToPortId: number, - secondFromPortId: number, - secondToPortId: number, -) => { - const first = { - ...solver.populateSegmentGeometryScratch( - regionId, - firstFromPortId, - firstToPortId, - ), - } - const second = { - ...solver.populateSegmentGeometryScratch( - regionId, - secondFromPortId, - secondToPortId, - ), - } - if ((first.layerMask & second.layerMask) === 0) return false - if ( - first.lesserAngle === second.lesserAngle || - first.lesserAngle === second.greaterAngle || - first.greaterAngle === second.lesserAngle || - first.greaterAngle === second.greaterAngle - ) { - return false - } - - const secondLesserInsideFirst = - first.lesserAngle < second.lesserAngle && - second.lesserAngle < first.greaterAngle - const secondGreaterInsideFirst = - first.lesserAngle < second.greaterAngle && - second.greaterAngle < first.greaterAngle - return secondLesserInsideFirst !== secondGreaterInsideFirst -} - -const installPreloadedFixedCrossingGuard = ( - solver: TinyHyperGraphSolver, - segments: PreloadedFixedSegment[], -) => { - const originalComputeG = solver.computeG.bind(solver) - solver.computeG = (currentCandidate, neighborPortId) => { - const currentRouteNetId = solver.state.currentRouteNetId - for (const segment of segments) { - const currentPort = - solver.topology.portMetadata?.[currentCandidate.portId] - const neighborPort = solver.topology.portMetadata?.[neighborPortId] - const geometricallyIntersects = - currentPort?.z === segment.z && - neighborPort?.z === segment.z && - typeof currentPort.x === "number" && - typeof currentPort.y === "number" && - typeof neighborPort.x === "number" && - typeof neighborPort.y === "number" && - minimumDistanceBetweenSegments( - { x: currentPort.x, y: currentPort.y }, - { x: neighborPort.x, y: neighborPort.y }, - segment.traceStart, - segment.traceEnd, - ) <= 1e-6 - if ( - segment.regionId === currentCandidate.nextRegionId && - segment.fixedNetIndex !== currentRouteNetId && - (geometricallyIntersects || - segmentsCrossOnSameLayer( - solver, - segment.regionId, - currentCandidate.portId, - neighborPortId, - segment.fromPortId, - segment.toPortId, - )) - ) { - return Number.POSITIVE_INFINITY - } - } - return originalComputeG(currentCandidate, neighborPortId) +/** + * Selective rerips may move an initially assigned route when it is the actual + * blocker. A fallback global rerip, however, should not eagerly discard every + * preloaded route: restore the original assignments and reroute only the + * remaining routes. + */ +class SelectiveReripTinyHyperGraphSolverWithStableInitialAssignments extends SelectiveReripTinyHyperGraphSolver { + override resetRoutingStateForRerip() { + super.resetRoutingStateForRerip() + if (!this.problem.initialAssignments?.length) return + + applyInitialAssignments({ + topology: this.topology, + problem: this.problem, + state: this.state, + routeSuccessCountByRouteId: this.routeSuccessCountByRouteId, + appendSegmentToRegionCache: (regionId, fromPortId, toPortId) => + this.appendSegmentToRegionCache(regionId, fromPortId, toPortId), + }) } } class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSectionPipelineSolver { private configuredSolvers = new WeakSet() - private preloadedFixedSegmentsBySolver = new WeakMap< - TinyHyperGraphSolver, - PreloadedFixedSegment[] - >() - private fixedCrossingGuardedSolvers = new WeakSet() duplicatePortPenaltyCount = 0 metadataPortPenaltyCount = 0 crampedPortPenaltyCount = 0 @@ -969,6 +728,11 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect super(inputProblem) this.useSelectiveReripRouting = useSelectiveReripRouting this.crampedPortTraversalPenalty = DEFAULT_CRAMPED_PORT_TRAVERSAL_PENALTY + const preloadedStats = getSerializedPreloadedTraceStats( + inputProblem.serializedHyperGraph, + ) + this.preloadedPortCount = preloadedStats.preloadedPortCount + this.preloadedFixedSegmentCount = preloadedStats.preloadedAssignmentCount if (useSelectiveReripRouting) { const solveGraphStep = this.pipelineDef.find( (pipelineStep) => pipelineStep.solverName === "solveGraph", @@ -978,21 +742,15 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect "Tiny hypergraph pipeline is missing the solveGraph stage", ) } - solveGraphStep.solverClass = SelectiveReripTinyHyperGraphSolver + solveGraphStep.solverClass = + SelectiveReripTinyHyperGraphSolverWithStableInitialAssignments } - this.MAX_ITERATIONS = getTinyHyperGraphPipelineMaxIterations(inputProblem) - const hasPreloadedFixedSegments = inputProblem.serializedHyperGraph.ports.some( - (port) => - ( - asTinyPortMetadata(port.d)._preloadedTracePortAssignments?.length ?? - 0 - ) > 0, - ) - if (hasPreloadedFixedSegments) { + if (preloadedStats.preloadedAssignmentCount > 0) { this.pipelineDef = this.pipelineDef.filter( (pipelineStep) => pipelineStep.solverName !== "optimizeSection", ) } + this.MAX_ITERATIONS = getTinyHyperGraphPipelineMaxIterations(inputProblem) } override loadHyperGraph(serializedHyperGraph: SerializedHyperGraph) { @@ -1001,6 +759,7 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect const { duplicatePortPenaltyCount, crampedPortPenaltyCount } = applyPortMetadataPenalties(loaded, this.crampedPortTraversalPenalty) applyTerminalRegionNetIds(loaded) + clearPreloadedEndpointRegionNetIds(loaded) this.metadataPortPenaltyCount = Math.max( this.metadataPortPenaltyCount, metadataPortPenaltyCount, @@ -1029,7 +788,6 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect throw error } this.configureSolver(this.activeSubSolver) - this.ensurePreloadedFixedSegments(this.activeSubSolver) } override getInitialVisualizationSolver() { @@ -1037,15 +795,15 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect const { topology, problem } = this.loadHyperGraph( this.inputProblem.serializedHyperGraph, ) - this.initialVisualizationSolver = new SelectiveReripTinyHyperGraphSolver( - topology, - problem, - this.getSolveGraphOptions(), - ) + this.initialVisualizationSolver = + new SelectiveReripTinyHyperGraphSolverWithStableInitialAssignments( + topology, + problem, + this.getSolveGraphOptions(), + ) } const solver = super.getInitialVisualizationSolver() this.configureSolver(solver) - this.ensurePreloadedFixedSegments(solver) return solver } @@ -1079,50 +837,12 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect const loadedSolver = solver as typeof solver & LoadedTinyGraph applyMetadataPortPenalties(loadedSolver) applyTerminalRegionNetIds(loadedSolver) - } - if (solver instanceof TinyHyperGraphSolver) { - const loadedSolver = solver as TinyHyperGraphSolver & LoadedTinyGraph - const getFixedNetIndex = - createPreloadedFixedNetIndexer(loadedSolver) - this.preloadedPortCount = Math.max( - this.preloadedPortCount, - applyPreloadedPortReservations( - loadedSolver, - getFixedNetIndex, - ), - ) - const preloadedFixedSegments = getPreloadedFixedSegments( - loadedSolver, - getFixedNetIndex, - ) - this.preloadedFixedSegmentsBySolver.set(solver, preloadedFixedSegments) - if (!this.fixedCrossingGuardedSolvers.has(solver)) { - installPreloadedFixedCrossingGuard( - solver, - preloadedFixedSegments, - ) - this.fixedCrossingGuardedSolvers.add(solver) - } - this.preloadedFixedSegmentCount = Math.max( - this.preloadedFixedSegmentCount, - ensurePreloadedFixedSegments(solver, preloadedFixedSegments), - ) + clearPreloadedEndpointRegionNetIds(loadedSolver) } this.configuredSolvers.add(solver) } - private ensurePreloadedFixedSegments(solver?: BaseSolver | null) { - if (!(solver instanceof TinyHyperGraphSolver)) return - const preloadedFixedSegments = - this.preloadedFixedSegmentsBySolver.get(solver) - if (!preloadedFixedSegments) return - this.preloadedFixedSegmentCount = Math.max( - this.preloadedFixedSegmentCount, - ensurePreloadedFixedSegments(solver, preloadedFixedSegments), - ) - } - private trySkipOptimizeSection(error: unknown) { if (this.getCurrentStageName() !== "optimizeSection") { return false @@ -1207,12 +927,12 @@ export class TinyHypergraphPortPointPathingSolver extends BaseSolver { ]), ) const serializedGraph = buildSerializedTinyGraph({ ...params, connections }) - const hasPreloadedPortReservations = serializedGraph.ports.some( - (port) => - (asTinyPortMetadata(port.d)._preloadedFixedNetIds?.length ?? 0) > 0, - ) + const preloadedTraceStats = + getSerializedPreloadedTraceStats(serializedGraph) + const hasPreloadedTraceOccupancy = + preloadedTraceStats.preloadedPortCount > 0 const shouldRunDuplicateCongestedPortPrepass = - !hasPreloadedPortReservations && + !hasPreloadedTraceOccupancy && connections.length <= MAX_CONNECTIONS_FOR_DUPLICATE_CONGESTED_PORT_PREPASS let graphForTiny = serializedGraph if (shouldRunDuplicateCongestedPortPrepass) { @@ -1242,7 +962,7 @@ export class TinyHypergraphPortPointPathingSolver extends BaseSolver { graphForTiny = duplicateCongestedPortSolver.getOutput() } } else { - this.duplicateCongestedPortError = hasPreloadedPortReservations + this.duplicateCongestedPortError = hasPreloadedTraceOccupancy ? "Skipped to preserve preloaded port topology" : `Skipped for ${connections.length} connections` } @@ -1427,8 +1147,18 @@ export class TinyHypergraphPortPointPathingSolver extends BaseSolver { const originalRegion = this.originalRegionById.get(originalRegionId) if (!originalRegion) continue - const portPointsInPairs = regionSegments[regionId].map( - ([routeId, fromPortId, toPortId]) => { + const portPointsInPairs = regionSegments[regionId] + .filter(([routeId]) => { + const connectionId = this.getRouteMetadata( + solvedTinySolver, + routeId, + )?.connectionId + return ( + typeof connectionId !== "string" || + !isPreloadedTraceConnectionId(connectionId) + ) + }) + .map(([routeId, fromPortId, toPortId]) => { const startPoint = this.createAssignedPortPoint( solvedTinySolver, routeId, @@ -1444,8 +1174,7 @@ export class TinyHypergraphPortPointPathingSolver extends BaseSolver { endPoint.prevPortPointId = startPoint.portPointId } return [startPoint, endPoint] satisfies [PortPoint, PortPoint] - }, - ) + }) const portPoints = portPointsInPairs.flat() if (portPoints.length === 0) { diff --git a/lib/solvers/PortPointPathingSolver/tinyhypergraph/serializePreloadedTraceAssignments.ts b/lib/solvers/PortPointPathingSolver/tinyhypergraph/serializePreloadedTraceAssignments.ts new file mode 100644 index 000000000..c4343f70d --- /dev/null +++ b/lib/solvers/PortPointPathingSolver/tinyhypergraph/serializePreloadedTraceAssignments.ts @@ -0,0 +1,297 @@ +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import type { PreloadedTracePortAssignment } from "lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver" + +type SerializedPort = SerializedHyperGraph["ports"][number] +type SerializedRegion = SerializedHyperGraph["regions"][number] +type SerializedConnection = NonNullable< + SerializedHyperGraph["connections"] +>[number] +type SerializedSolvedRoute = NonNullable< + SerializedHyperGraph["solvedRoutes"] +>[number] + +type PortMetadataWithPreloadedAssignments = { + _preloadedTracePortAssignments?: PreloadedTracePortAssignment[] +} + +type OrderedTracePort = { + port: SerializedPort + assignment: PreloadedTracePortAssignment +} + +type PreloadedAssignmentSegment = { + regionId: string + from: OrderedTracePort + to: OrderedTracePort +} + +export type SerializedPreloadedTraceStats = { + preloadedTraceCount: number + preloadedPortCount: number + preloadedAssignmentCount: number +} + +const PRELOADED_TRACE_CONNECTION_PREFIX = "__tscircuit_preloaded_trace__:" +const ROUTE_POSITION_TOLERANCE = 1e-6 + +export const isPreloadedTraceConnectionId = (connectionId: string) => + connectionId.startsWith(PRELOADED_TRACE_CONNECTION_PREFIX) + +const getPreloadedTraceConnectionId = ( + traceId: string, + contiguousRunIndex: number, +) => + `${PRELOADED_TRACE_CONNECTION_PREFIX}${JSON.stringify([ + traceId, + contiguousRunIndex, + ])}` + +const getIncidentRegionIds = (port: SerializedPort) => [ + port.region1Id, + port.region2Id, +] + +const getSharedRegionIds = ( + firstPort: SerializedPort, + secondPort: SerializedPort, +) => { + const secondRegionIds = new Set(getIncidentRegionIds(secondPort)) + return getIncidentRegionIds(firstPort).filter((regionId) => + secondRegionIds.has(regionId), + ) +} + +const chooseAssignmentRegionId = ( + orderedPorts: OrderedTracePort[], + pairIndex: number, + previousRegionId?: string, +) => { + const from = orderedPorts[pairIndex]! + const to = orderedPorts[pairIndex + 1]! + const sharedRegionIds = getSharedRegionIds(from.port, to.port) + if (sharedRegionIds.length === 0) return undefined + if (sharedRegionIds.length === 1) return sharedRegionIds[0] + + const next = orderedPorts[pairIndex + 2] + if (next) { + const nextSharedRegionIds = new Set(getSharedRegionIds(to.port, next.port)) + const continuingRegionId = sharedRegionIds.find((regionId) => + nextSharedRegionIds.has(regionId), + ) + if (continuingRegionId) return continuingRegionId + } + + if (previousRegionId && sharedRegionIds.includes(previousRegionId)) { + return previousRegionId + } + + return [...sharedRegionIds].sort()[0] +} + +const getEndpointRegionId = ( + endpointPort: SerializedPort, + adjacentAssignmentRegionId: string, +) => + getIncidentRegionIds(endpointPort).find( + (regionId) => regionId !== adjacentAssignmentRegionId, + ) ?? adjacentAssignmentRegionId + +/** + * Converts Pipeline9's ordered boundary crossings into ordinary serialized + * hypergraph assignments. The preloaded connections only provide route + * ownership for those assignments; no regions or ports are added. + */ +export const serializePreloadedTraceAssignments = ( + serializedHyperGraph: SerializedHyperGraph, +): SerializedPreloadedTraceStats => { + const orderedPortsByTraceId = new Map() + const fixedNetIdByTraceId = new Map() + const connectedRegionIds = new Set( + (serializedHyperGraph.connections ?? []).flatMap((connection) => [ + connection.startRegionId, + connection.endRegionId, + ]), + ) + const removedObstacleRegionIds = new Set( + serializedHyperGraph.regions + .filter((region) => { + const netId = + typeof region.d?.netId === "number" + ? region.d.netId + : typeof region.d?.NetId === "number" + ? region.d.NetId + : undefined + return ( + region.d?._containsObstacle === true && + (netId === undefined || netId === -1) && + !connectedRegionIds.has(region.regionId) + ) + }) + .map((region) => region.regionId), + ) + let preloadedPortCount = 0 + + for (const port of serializedHyperGraph.ports) { + const metadata = port.d as PortMetadataWithPreloadedAssignments | undefined + const assignments = metadata?._preloadedTracePortAssignments ?? [] + if (assignments.length > 0) preloadedPortCount++ + if ( + removedObstacleRegionIds.has(port.region1Id) || + removedObstacleRegionIds.has(port.region2Id) + ) { + continue + } + + for (const assignment of assignments) { + const existingFixedNetId = fixedNetIdByTraceId.get(assignment.traceId) + if ( + existingFixedNetId !== undefined && + existingFixedNetId !== assignment.fixedNetId + ) { + throw new Error( + `Preloaded trace "${assignment.traceId}" maps to multiple canonical nets`, + ) + } + fixedNetIdByTraceId.set(assignment.traceId, assignment.fixedNetId) + const orderedPorts = orderedPortsByTraceId.get(assignment.traceId) ?? [] + orderedPorts.push({ port, assignment }) + orderedPortsByTraceId.set(assignment.traceId, orderedPorts) + } + } + + const regionById = new Map( + serializedHyperGraph.regions.map((region) => [region.regionId, region]), + ) + const connections = (serializedHyperGraph.connections ??= []) + const solvedRoutes = (serializedHyperGraph.solvedRoutes ??= []) + let preloadedTraceCount = 0 + let preloadedAssignmentCount = 0 + + for (const [traceId, tracePorts] of orderedPortsByTraceId) { + tracePorts.sort( + (left, right) => + left.assignment.routePosition - right.assignment.routePosition || + left.assignment.z - right.assignment.z || + left.port.portId.localeCompare(right.port.portId), + ) + const orderedPorts = tracePorts.filter( + ({ port }, index) => + index === 0 || port.portId !== tracePorts[index - 1]!.port.portId, + ) + if (orderedPorts.length < 2) continue + + const segments: PreloadedAssignmentSegment[] = [] + let previousRegionId: string | undefined + for (let pairIndex = 0; pairIndex < orderedPorts.length - 1; pairIndex++) { + const from = orderedPorts[pairIndex]! + const to = orderedPorts[pairIndex + 1]! + if ( + from.assignment.z === to.assignment.z && + Math.abs(from.assignment.routePosition - to.assignment.routePosition) <= + ROUTE_POSITION_TOLERANCE + ) { + previousRegionId = undefined + continue + } + const regionId = chooseAssignmentRegionId( + orderedPorts, + pairIndex, + previousRegionId, + ) + if (!regionId) { + previousRegionId = undefined + continue + } + segments.push({ regionId, from, to }) + previousRegionId = regionId + } + + const contiguousRuns: PreloadedAssignmentSegment[][] = [] + for (const segment of segments) { + const currentRun = contiguousRuns.at(-1) + if ( + !currentRun || + currentRun.at(-1)!.to.port.portId !== segment.from.port.portId + ) { + contiguousRuns.push([segment]) + } else { + currentRun.push(segment) + } + } + + for (const [runIndex, run] of contiguousRuns.entries()) { + const connectionId = getPreloadedTraceConnectionId(traceId, runIndex) + for (const segment of run) { + const region = regionById.get(segment.regionId) + if (!region) { + throw new Error( + `Preloaded trace "${traceId}" references missing region "${segment.regionId}"`, + ) + } + region.assignments = [ + ...(region.assignments ?? []), + { + regionPort1Id: segment.from.port.portId, + regionPort2Id: segment.to.port.portId, + connectionId, + }, + ] + preloadedAssignmentCount++ + } + + const firstSegment = run[0]! + const lastSegment = run.at(-1)! + const firstPort = firstSegment.from.port + const lastPort = lastSegment.to.port + const connection: SerializedConnection = { + connectionId, + mutuallyConnectedNetworkId: fixedNetIdByTraceId.get(traceId) ?? traceId, + startRegionId: getEndpointRegionId(firstPort, firstSegment.regionId), + endRegionId: getEndpointRegionId(lastPort, lastSegment.regionId), + } + connections.push(connection) + solvedRoutes.push({ + connection: { connectionId }, + path: [{ portId: firstPort.portId }, { portId: lastPort.portId }], + } as SerializedSolvedRoute) + } + if (contiguousRuns.length > 0) { + preloadedTraceCount++ + } + } + + return { + preloadedTraceCount, + preloadedPortCount, + preloadedAssignmentCount, + } +} + +export const getSerializedPreloadedTraceStats = ( + serializedHyperGraph: SerializedHyperGraph, +): SerializedPreloadedTraceStats => { + const preloadedConnectionIds = new Set( + (serializedHyperGraph.connections ?? []) + .map((connection) => connection.connectionId) + .filter(isPreloadedTraceConnectionId), + ) + const preloadedPortCount = serializedHyperGraph.ports.filter( + (port) => + ((port.d as PortMetadataWithPreloadedAssignments | undefined) + ?._preloadedTracePortAssignments?.length ?? 0) > 0, + ).length + const preloadedAssignmentCount = serializedHyperGraph.regions.reduce( + (count, region: SerializedRegion) => + count + + (region.assignments ?? []).filter((assignment) => + preloadedConnectionIds.has(assignment.connectionId), + ).length, + 0, + ) + + return { + preloadedTraceCount: preloadedConnectionIds.size, + preloadedPortCount, + preloadedAssignmentCount, + } +} diff --git a/lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver.ts b/lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver.ts index a4fb22212..a66962a01 100644 --- a/lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver.ts +++ b/lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver.ts @@ -32,6 +32,28 @@ type Via = { const NEAR_VIA_MERGE_DISTANCE_MULTIPLIER = 2.5 const OBSTACLE_MARGIN = 0.1 +const VIA_TRANSITION_POSITION_TOLERANCE = 1e-5 + +const positionsMatch = ( + first: { x: number; y: number }, + second: { x: number; y: number }, +) => + Math.abs(first.x - second.x) <= VIA_TRANSITION_POSITION_TOLERANCE && + Math.abs(first.y - second.y) <= VIA_TRANSITION_POSITION_TOLERANCE + +const getRouteTransitionNearVia = ( + route: HighDensityRoute, + via: { x: number; y: number }, +) => { + for (let pointIndex = 1; pointIndex < route.route.length; pointIndex++) { + const previousPoint = route.route[pointIndex - 1]! + const currentPoint = route.route[pointIndex]! + if (previousPoint.z === currentPoint.z) continue + if (!positionsMatch(previousPoint, currentPoint)) continue + if (positionsMatch(previousPoint, via)) return previousPoint + } + return undefined +} const getNetForRoute = ( connMap: ConnectivityMap, @@ -210,6 +232,8 @@ export class SameNetViaMergerSolver extends BaseSolver { const route = this.mergedViaHdRoutes[i] for (let j = 0; j < route.vias.length; j++) { const viaPoint = route.vias[j] + const transitionPoint = getRouteTransitionNearVia(route, viaPoint) + if (!transitionPoint) continue const layers = [...new Set(route.route.map((p) => p.z))] if (layers.length === 0) { throw new Error( @@ -218,8 +242,8 @@ export class SameNetViaMergerSolver extends BaseSolver { } const via: Via = { - x: viaPoint.x, - y: viaPoint.y, + x: transitionPoint.x, + y: transitionPoint.y, diameter: route.viaDiameter, net: getNetForRoute(this.connMap, route), layers, @@ -403,9 +427,12 @@ export class SameNetViaMergerSolver extends BaseSolver { } if (routePointIndexesToMove.size === 0) { - throw new Error( - `SameNetViaMergerSolver could not find route transition for via at (${viaToRemove.x}, ${viaToRemove.y}) on route "${routeToUpdate.connectionName}"`, + routeToUpdate.vias = routeToUpdate.vias.filter( + (via) => !positionsMatch(via, viaToRemove), ) + this.dedupeRouteVias(routeToUpdate) + if (rebuildVias) this.rebuildVias() + return } for (const routePointIndex of routePointIndexesToMove) { @@ -414,7 +441,7 @@ export class SameNetViaMergerSolver extends BaseSolver { } routeToUpdate.vias = routeToUpdate.vias.map((vx) => { - if (vx.x !== viaToRemove.x || vx.y !== viaToRemove.y) return vx + if (!positionsMatch(vx, viaToRemove)) return vx replacedVia = true return { x: viaKeep.x, y: viaKeep.y } }) diff --git a/package.json b/package.json index 6b1fbbfdd..c303bfeea 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "recharts": "^2.15.1", "tailwind-merge": "^3.2.0", "terser": "^5.43.1", - "tiny-hypergraph": "git+https://github.com/tscircuit/tiny-hypergraph.git#bdd0f01e79df3f1258b8d74c3902138e2facb13f", + "tiny-hypergraph": "git+https://github.com/tscircuit/tiny-hypergraph.git#1fe25466462dfacf371e15ca494bc8b8f869aa30", "tiny-hypergraph-poly": "git+https://github.com/tscircuit/tiny-hypergraph.git#7b93b4c", "tsup": "^8.3.6", "typescript": "^5.9.3", diff --git a/tests/features/pipeline9-srj23-routing-bounds.test.ts b/tests/features/pipeline9-srj23-routing-bounds.test.ts index 09a83afab..074a53507 100644 --- a/tests/features/pipeline9-srj23-routing-bounds.test.ts +++ b/tests/features/pipeline9-srj23-routing-bounds.test.ts @@ -28,5 +28,5 @@ test( expect(solver.originalSrj.bounds).toEqual(scenario.bounds) } }, - { timeout: 15_000 }, + { timeout: 30_000 }, ) diff --git a/tests/features/preloaded-fixed-segment-occupancy.test.ts b/tests/features/preloaded-fixed-segment-occupancy.test.ts index 7bbe35d49..c8c6cb2f1 100644 --- a/tests/features/preloaded-fixed-segment-occupancy.test.ts +++ b/tests/features/preloaded-fixed-segment-occupancy.test.ts @@ -2,10 +2,7 @@ import { expect, test } from "bun:test" import { ConnectivityMap } from "circuit-json-to-connectivity-map" import { buildHyperGraph } from "lib/solvers/PortPointPathingSolver/hgportpointpathingsolver" import { TinyHypergraphPortPointPathingSolver } from "lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver" -import type { - CapacityMeshNode, - SimpleRouteConnection, -} from "lib/types" +import type { CapacityMeshNode, SimpleRouteConnection } from "lib/types" import type { TinyHyperGraphSolver } from "tiny-hypergraph/lib/index" test("preloaded trace segments occupy existing hypergraph regions", () => { @@ -21,8 +18,7 @@ test("preloaded trace segments occupy existing hypergraph regions", () => { width: 2, height: 2, layer: "top", - availableZ: - capacityMeshNodeId === "center" ? [0, 1] : [0], + availableZ: capacityMeshNodeId === "center" ? [0, 1] : [0], })) const simpleRouteJsonConnections: SimpleRouteConnection[] = [ { @@ -50,8 +46,7 @@ test("preloaded trace segments occupy existing hypergraph regions", () => { nodeIds, edgeId: `${segmentPortPointId}-edge`, connectionName: routePosition === undefined ? null : "fixed-route", - rootConnectionName: - routePosition === undefined ? undefined : "fixed-root", + rootConnectionName: routePosition === undefined ? undefined : "fixed-root", distToCentermostPortOnZ: 0, cramped: false, ...(routePosition === undefined @@ -134,18 +129,29 @@ test("preloaded trace segments occupy existing hypergraph regions", () => { tinySolver.state.regionIntersectionCaches[centerRegionId!] .existingSegmentCount, ).toBe(1) - - tinySolver.state.currentRouteNetId = tinySolver.problem.routeNet[0] + const [[preloadedRouteId, preloadedFromPortId, preloadedToPortId]] = + tinySolver.state.regionSegments[centerRegionId!] + const preloadedConnectionId = + tinySolver.problem.routeMetadata?.[preloadedRouteId]?.connectionId + expect( + preloadedConnectionId?.startsWith("__tscircuit_preloaded_trace__:"), + ).toBe(true) expect( - tinySolver.computeG( - { - portId: northPortId, - nextRegionId: centerRegionId!, - f: 0, - g: 0, - h: 0, - }, - southPortId, - ), - ).toBe(Number.POSITIVE_INFINITY) + [preloadedFromPortId, preloadedToPortId].sort((a, b) => a - b), + ).toEqual([westPortId, eastPortId].sort((a, b) => a - b)) + expect(tinySolver.state.portAssignment[westPortId]).toBe( + tinySolver.problem.routeNet[preloadedRouteId], + ) + expect(tinySolver.state.portAssignment[eastPortId]).toBe( + tinySolver.problem.routeNet[preloadedRouteId], + ) + expect(northPortId).toBeGreaterThanOrEqual(0) + expect(southPortId).toBeGreaterThanOrEqual(0) + + tinySolver.resetRoutingStateForRerip() + + expect(tinySolver.state.regionSegments[centerRegionId!]).toEqual([ + [preloadedRouteId, preloadedFromPortId, preloadedToPortId], + ]) + expect(tinySolver.state.unroutedRoutes).not.toContain(preloadedRouteId) }) diff --git a/tests/solvers/same-net-via-merger-direct-overlap-star.test.ts b/tests/solvers/same-net-via-merger-direct-overlap-star.test.ts index 5550716f8..0c7bdc62a 100644 --- a/tests/solvers/same-net-via-merger-direct-overlap-star.test.ts +++ b/tests/solvers/same-net-via-merger-direct-overlap-star.test.ts @@ -44,3 +44,48 @@ test("SameNetViaMergerSolver consolidates a chain within the near-merge radius", { x: 0, y: 0 }, ]) }) + +test("SameNetViaMergerSolver resolves rounded via metadata to its route transition", () => { + const roundedViaRoute = makeViaRoute("route-b", 0.25) + roundedViaRoute.vias = [{ x: 0.250001, y: -0.000001 }] + const solver = new SameNetViaMergerSolver({ + inputHdRoutes: [makeViaRoute("route-a", 0), roundedViaRoute], + obstacles: [], + colorMap: {}, + layerCount: 2, + connMap: new ConnectivityMap({ + net0: ["route-a", "route-b"], + }), + }) + + solver.solve() + + expect(solver.failed).toBe(false) + expect(solver.getMergedViaHdRoutes()?.flatMap((route) => route.vias)).toEqual( + [ + { x: 0, y: 0 }, + { x: 0, y: 0 }, + ], + ) +}) + +test("SameNetViaMergerSolver ignores a stale candidate after its transition moved", () => { + const solver = new SameNetViaMergerSolver({ + inputHdRoutes: [makeViaRoute("route-a", 0), makeViaRoute("route-b", 0.25)], + obstacles: [], + colorMap: {}, + layerCount: 2, + connMap: new ConnectivityMap({ + net0: ["route-a", "route-b"], + }), + }) + solver.mergedViaHdRoutes[1]!.route = [ + { x: 0.25, y: 0, z: 0 }, + { x: 0.5, y: 0, z: 0 }, + ] + + solver.solve() + + expect(solver.failed).toBe(false) + expect(solver.getMergedViaHdRoutes()?.[1]?.vias).toEqual([]) +}) From 9dedf77debfad248d9c253217cf84b15e8ebad2a Mon Sep 17 00:00:00 2001 From: seveibar Date: Mon, 27 Jul 2026 14:41:25 -0700 Subject: [PATCH 15/22] chore: format Pipeline9 regression tests --- tests/features/preloaded-port-duplication.test.ts | 5 +---- .../preloaded-trace-canonical-net-regressions.test.ts | 11 ++++------- tests/features/preloaded-trace-graph-solver.test.ts | 6 +----- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/tests/features/preloaded-port-duplication.test.ts b/tests/features/preloaded-port-duplication.test.ts index d9edee856..f2cbe610c 100644 --- a/tests/features/preloaded-port-duplication.test.ts +++ b/tests/features/preloaded-port-duplication.test.ts @@ -2,10 +2,7 @@ import { expect, test } from "bun:test" import { ConnectivityMap } from "circuit-json-to-connectivity-map" import { buildHyperGraph } from "lib/solvers/PortPointPathingSolver/hgportpointpathingsolver" import { TinyHypergraphPortPointPathingSolver } from "lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver" -import type { - CapacityMeshNode, - SimpleRouteConnection, -} from "lib/types" +import type { CapacityMeshNode, SimpleRouteConnection } from "lib/types" test("preloaded port ownership preserves the original graph topology", () => { const capacityMeshNodes: CapacityMeshNode[] = [ diff --git a/tests/features/preloaded-trace-canonical-net-regressions.test.ts b/tests/features/preloaded-trace-canonical-net-regressions.test.ts index 0f50971f9..97a15eb80 100644 --- a/tests/features/preloaded-trace-canonical-net-regressions.test.ts +++ b/tests/features/preloaded-trace-canonical-net-regressions.test.ts @@ -5,10 +5,7 @@ import { evaluateRelaxedDrc, } from "lib/testing/evaluate-relaxed-drc" import { convertToCircuitJson } from "lib/testing/utils/convertToCircuitJson" -import type { - SimpleRouteJson, - SimplifiedPcbTrace, -} from "lib/types" +import type { SimpleRouteJson, SimplifiedPcbTrace } from "lib/types" import { resolvePreloadedTraceCanonicalNetIds } from "lib/utils/resolvePreloadedTraceCanonicalNetIds" const makePreloadedTraceChain = (): SimpleRouteJson => ({ @@ -161,9 +158,9 @@ test("preloaded graph assigns the canonical net to a middle trace port", () => { solver.solve() expect(solver.getOutput()).toHaveLength(1) - expect( - solver.getOutput()[0]?.portPoints[0]?._preloadedFixedNetIds, - ).toEqual(["root-net"]) + expect(solver.getOutput()[0]?.portPoints[0]?._preloadedFixedNetIds).toEqual([ + "root-net", + ]) }) test("relaxed DRC namespaces trace links before canonical conversion", () => { diff --git a/tests/features/preloaded-trace-graph-solver.test.ts b/tests/features/preloaded-trace-graph-solver.test.ts index ae3dc93da..d15448d6b 100644 --- a/tests/features/preloaded-trace-graph-solver.test.ts +++ b/tests/features/preloaded-trace-graph-solver.test.ts @@ -6,11 +6,7 @@ import type { } from "lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver" import type { SimpleRouteJson } from "lib/types" -const createPort = ( - id: string, - y: number, - z: number, -): SegmentPortPoint => ({ +const createPort = (id: string, y: number, z: number): SegmentPortPoint => ({ segmentPortPointId: id, x: 0, y, From 8e6c5907ef3d4f4f7fda7b014ca04b592bc06b39 Mon Sep 17 00:00:00 2001 From: seveibar Date: Mon, 27 Jul 2026 15:12:14 -0700 Subject: [PATCH 16/22] fix: stabilize Pipeline9 SRJ23 validation --- .../pipeline9-exact-drc-repair-solver.ts | 2 +- lib/testing/evaluate-relaxed-drc.ts | 30 +- ...on.test.ts-portPointPathingSolver.snap.svg | 369 +++++++- ...ipeline7-full-pipeline-svg-frames.snap.svg | 820 +++++++++++++++--- .../tinyhypergraph-port-bridge-repro.snap.svg | 24 +- ...eline9-error-owned-cluster-rebuild.test.ts | 46 +- tests/pipeline-stage-debug-runner.test.ts | 12 +- 7 files changed, 1134 insertions(+), 169 deletions(-) diff --git a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts index a1279bc61..9e5caaa96 100644 --- a/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts @@ -111,7 +111,7 @@ const BATCHED_TRACE_FORCE_SCALES = [4, 2, 1, 0.5, 0.25] as const const MAX_BATCHED_TRACE_FORCE_PASSES = 20 const MAX_CLEANUP_PASSES = 8 const DEFAULT_MAX_LOCAL_CLEANUP_DRC_EVALUATIONS = 500 -const HIGH_INITIAL_DRC_MAX_LOCAL_CLEANUP_DRC_EVALUATIONS = 300 +const HIGH_INITIAL_DRC_MAX_LOCAL_CLEANUP_DRC_EVALUATIONS = 150 const HIGH_INITIAL_DRC_THRESHOLD = 20 const DEFAULT_MAX_CONSECUTIVE_LOCAL_CLEANUP_DRC_MISSES = DEFAULT_MAX_LOCAL_CLEANUP_DRC_EVALUATIONS diff --git a/lib/testing/evaluate-relaxed-drc.ts b/lib/testing/evaluate-relaxed-drc.ts index eb54b76d6..89f2800a0 100644 --- a/lib/testing/evaluate-relaxed-drc.ts +++ b/lib/testing/evaluate-relaxed-drc.ts @@ -888,7 +888,7 @@ const createPreloadedTraceId = ( export const createRelaxedDrcTraceSet = ( inputSrj: SimpleRouteJson, traces: SimplifiedPcbTrace[], - srjWithPointPairs?: SimpleRouteJson, + _srjWithPointPairs?: SimpleRouteJson, ): SimplifiedPcbTrace[] => { const usedTraceIds = new Set(traces.map((trace) => trace.pcb_trace_id)) const originalPreloadedTraces = inputSrj.traces ?? [] @@ -904,22 +904,6 @@ export const createRelaxedDrcTraceSet = ( ) const canonicalNetIdByOriginalTraceId = resolvePreloadedTraceCanonicalNetIds(inputSrj) - const pointPairCanonicalNameByAlias = new Map() - for (const connection of srjWithPointPairs?.connections ?? []) { - const canonicalName = - connection.__rootConnectionNames?.[0] ?? - connection.__netConnectionName ?? - connection.name - for (const alias of [ - connection.name, - connection.__netConnectionName, - ...(connection.__rootConnectionNames ?? []), - ]) { - if (typeof alias === "string" && alias.length > 0) { - pointPairCanonicalNameByAlias.set(alias, canonicalName) - } - } - } const preloadedTraceIdByOriginalId = new Map() for (const [index, trace] of originalPreloadedTraces.entries()) { if (originalTraceIdCounts.get(trace.pcb_trace_id) !== 1) continue @@ -928,15 +912,9 @@ export const createRelaxedDrcTraceSet = ( renamedPreloadedTraceIds[index]!, ) } - const getPreloadedCanonicalName = (trace: SimplifiedPcbTrace) => { - const resolvedCanonicalName = - canonicalNetIdByOriginalTraceId.get(trace.pcb_trace_id) ?? - trace.connection_name - return ( - pointPairCanonicalNameByAlias.get(resolvedCanonicalName) ?? - resolvedCanonicalName - ) - } + const getPreloadedCanonicalName = (trace: SimplifiedPcbTrace) => + canonicalNetIdByOriginalTraceId.get(trace.pcb_trace_id) ?? + trace.connection_name const preloadedTraces = originalPreloadedTraces.map((trace, index) => ({ ...trace, connection_name: getPreloadedCanonicalName(trace), diff --git a/tests/__snapshots__/e2e3-multisection.test.ts-portPointPathingSolver.snap.svg b/tests/__snapshots__/e2e3-multisection.test.ts-portPointPathingSolver.snap.svg index e36ac1029..369c926cd 100644 --- a/tests/__snapshots__/e2e3-multisection.test.ts-portPointPathingSolver.snap.svg +++ b/tests/__snapshots__/e2e3-multisection.test.ts-portPointPathingSolver.snap.svg @@ -135,16 +135,135 @@ z: 0" points="133.44455150571247,402.12086811352253 119.42117646718452,405.62584 region: region-91 z: 0" points="122.69329666751844,354.2068160310005 119.42117646718452,340.1834404092465" fill="none" stroke="hsla(218, 70%, 50%, 0.8)" stroke-width="1px"/>