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/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 new file mode 100644 index 000000000..a21c9384c --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts @@ -0,0 +1,233 @@ +import type { DrcEvaluator } from "high-density-repair03/lib" +import { BaseSolver } from "lib/solvers/BaseSolver" +import type { Obstacle, SimpleRouteJson } from "lib/types" +import { + AutoroutingPipelineSolver7_MultiGraph, + type AutoroutingPipelineSolverOptions, +} from "../AutoroutingPipeline7_MultiGraph/AutoroutingPipelineSolver7_MultiGraph" +import { convertPreloadedTraceToRouteObstacles } from "./pipeline9-b01-rerouter" +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 = { + solverName: string + solverClass: new (...args: any[]) => BaseSolver + getConstructorParams: ( + instance: AutoroutingPipelineSolver7_MultiGraph, + ) => any[] + 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 physicalId = obstacle.connectedTo[0] + if ( + physicalId && + (physicalId.startsWith("pcb_smtpad_") || + physicalId.startsWith("pcb_plated_hole_")) + ) { + 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 + + 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 crampedPortStageIndex = pipelineDef.findIndex( + (step) => step.solverName === "necessaryCrampedPortPointSolver", + ) + if (crampedPortStageIndex === -1) { + throw new Error("Pipeline9 could not find the cramped-port stage") + } + pipelineDef.splice(crampedPortStageIndex + 1, 0, { + solverName: "preloadedTraceGraphSolver", + solverClass: PreloadedTraceGraphSolver, + getConstructorParams: (pipeline) => [ + pipeline.sharedEdgeSegmentsWithNecessaryCrampedPortPoints ?? + pipeline.necessaryCrampedPortPointSolver!.getOutput(), + pipeline.originalSrj, + ], + }) + + 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 traceSimplificationStep = pipelineDef.find( + (step) => step.solverName === "traceSimplificationSolver", + ) + if (!traceSimplificationStep) { + throw new Error("Pipeline9 could not find the trace simplification stage") + } + const getBaseTraceSimplificationParams = + traceSimplificationStep.getConstructorParams + traceSimplificationStep.getConstructorParams = (pipeline) => { + const [params, ...remainingParams] = + getBaseTraceSimplificationParams(pipeline) + const preloadedHdRoutes = (pipeline.srj.traces ?? []).flatMap( + (trace, traceIndex) => + convertPreloadedTraceToRouteObstacles( + trace, + traceIndex, + pipeline.srj.layerCount, + pipeline.viaDiameter, + pipeline.connMap, + ), + ) + return [ + { + ...params, + otherHdRoutes: preloadedHdRoutes, + }, + ...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, + ), + }, + ] + } + + 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, + b01BaseObstacles: originalObstacles, + srj: { + ...params.srj, + obstacles: originalObstacles.map(getAxisAlignedRepairObstacle), + }, + }, + ] + } + } + + override getSolverName(): string { + return "AutoroutingPipelineSolver9_PreloadedTraceGraph" + } +} 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..1de214ca2 --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/infer-preloaded-trace-connectivity.ts @@ -0,0 +1,125 @@ +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 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) => + endpoints.some((endpoint) => + doesConnectionPointMatchEndpoint( + point, + endpoint, + trace.connection_name, + srj, + ), + ), + ) + .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([...preservedNonPointIds, ...inferredPointIds])], + } + }) + + return { ...srj, traces } +} 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-b01-rerouter.ts b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts new file mode 100644 index 000000000..8c8996792 --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-b01-rerouter.ts @@ -0,0 +1,1052 @@ +import { + HighDensitySolverB01, + type HighDensityObstacle, + type HighDensityRectObstacle, + 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 + baseObstacles: Obstacle[] + connMap?: ConnectivityMap +} + +export type Pipeline9B01RerouteOptions = { + 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 Pipeline9B01RerouteResult = { + route?: HighDensityRoute + iterations: number +} + +export type Pipeline9TerminalViaEscapeCandidate = { + alternateZ: number + startVia: { x: number; y: number } + endVia: { x: number; y: number } +} + +export type Pipeline9TerminalViaEscapeOptions = Omit< + Pipeline9B01RerouteOptions, + "startIndex" | "endIndex" +> & { + candidate: Pipeline9TerminalViaEscapeCandidate +} + +const SAME_POINT_EPSILON = 1e-9 +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 MAX_TERMINAL_VIA_ESCAPE_CANDIDATES = 64 +const RELAXED_VIA_TRACE_CLEARANCE = 0.1 + +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 && + 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 getCanonicalRootConnectionName = ( + connectionName: string, + connMap?: ConnectivityMap, +): string => + connMap?.getNetConnectedToId(connectionName) ?? + connectionName.replace(/_mst\d+$/, "") + +const convertRectObstacle = ( + obstacle: Obstacle, + obstacleIndex: number, + layerCount: number, + 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 +} + +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), +}) + +export 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) + 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, + ) + } + continue + } + + 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 +} + +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], + 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) + 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 ( + 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, + } +} + +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 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 Pipeline9B01Rerouter { + private readonly srj: SimpleRouteJson + 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 + >() + + constructor(params: Pipeline9B01RerouterParams) { + this.srj = params.srj + this.connMap = params.connMap + this.terminalObstacles = params.baseObstacles.map((obstacle) => + addCanonicalConnectivity(obstacle, params.connMap), + ) + const defaultViaDiameter = + params.srj.minViaPadDiameter ?? + params.srj.min_via_pad_diameter ?? + params.srj.minViaDiameter ?? + 0.3 + const preloadedTraceObstacles = (params.srj.traces ?? []).flatMap( + (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) => + convertRectObstacle( + obstacle, + obstacleIndex, + params.srj.layerCount, + params.connMap, + ), + ) + 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], + ): 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: snapToB01Grid( + obstacle.center.x + + localX * Math.cos(rotationRadians) - + localY * Math.sin(rotationRadians), + ), + y: snapToB01Grid( + 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, + ): Pipeline9B01RerouteResult | 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, + ): HighDensityRouteObstacle[] { + 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 candidateObstacles = routes.flatMap((route, routeIndex) => { + if ( + routeIndex === targetRouteIndex || + omittedRouteIndexSet.has(routeIndex) || + route.route.length < 2 + ) { + return [] + } + return [convertCandidateRouteToObstacle(route, this.connMap)] + }) + const obstaclesByTarget = + cachedForRoutes ?? new Map() + obstaclesByTarget.set(cacheKey, candidateObstacles) + if (!cachedForRoutes) { + this.candidateObstacleCache.set(routes, obstaclesByTarget) + } + + return candidateObstacles + } + + tryReroute( + routes: HighDensityRoute[], + options: Pipeline9B01RerouteOptions, + ): Pipeline9B01RerouteResult | 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 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, + z: startPoint.z, + } + const end = { + connectionName: targetRoute.connectionName, + rootConnectionName, + x: endPoint.x, + y: endPoint.y, + 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 candidateObstacles = options.includeCandidateCopper + ? this.getCandidateObstacles( + routes, + options.routeIndex, + options.omitCandidateRouteIndexes, + ) + : [] + 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() + 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 }) => ({ + 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 } + reroutedPoints = normalizeEndpointLayerTransitions(reroutedPoints) + 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: 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 new file mode 100644 index 000000000..3bc5d0ac6 --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-exact-drc-repair-solver.ts @@ -0,0 +1,5319 @@ +import { + GlobalDrcBranchPortfolioSolver, + type GlobalDrcBranchPortfolioSolverParams, +} from "high-density-repair03/lib" +import { + applyDrcErrorForces, + cloneRoutes, + 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 Pipeline9B01RerouteOptions, + Pipeline9B01Rerouter, +} from "./pipeline9-b01-rerouter" + +type Pipeline9ExactDrcRepairSolverParams = + GlobalDrcBranchPortfolioSolverParams & { + originalObstacles: Obstacle[] + b01BaseObstacles: Obstacle[] + } + +type DrcError = Record +type DrcSnapshot = ReturnType + +type TerminalConstraint = { + routeIndex: number + endpoint: "start" | "end" + originalPoint: HighDensityRoute["route"][number] + traceRadius: number + 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 AnchoredFixedCopperWindow = { + routes: HighDensityRoute[] + startIndex: number + endIndex: number +} + +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, 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 +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 = 150 +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_B01_FULL_ATTEMPTS_PER_ROUND = 18 +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 +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 = 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 +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_B01_ITERATIONS = 12_500 +const MAX_SHARED_TERMINAL_COMPOSITE_DRC_EVALUATIONS = 2 +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 +const MAX_POST_FINAL_COMPOSITE_SAME_NET_VIA_MERGER_ITERATIONS_PER_ATTEMPT = 8 +const ANCHORED_FIXED_COPPER_HALF_SPAN = 7 +const RESIDUAL_ANCHORED_FIXED_COPPER_HALF_SPANS = [2, 4, 7] as const +const MAX_EARLY_SHORT_ANCHORED_INITIAL_DRC_ISSUES = 50 +const MAX_EARLY_SHORT_ANCHORED_FIXED_COPPER_ATTEMPTS = 16 +const MAX_EARLY_SHORT_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS = 16 +const MAX_EARLY_SHORT_ANCHORED_FIXED_COPPER_ITERATIONS = 80_000 +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_FINAL_ANCHORED_FIXED_COPPER_ATTEMPTS = 48 +const MAX_FINAL_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS = 24 +const MAX_FINAL_ANCHORED_FIXED_COPPER_ITERATIONS = 240_000 +const MAX_TOTAL_ANCHORED_FIXED_COPPER_ATTEMPTS = + MAX_EARLY_SHORT_ANCHORED_FIXED_COPPER_ATTEMPTS + + MAX_ANCHORED_FIXED_COPPER_ATTEMPTS + + MAX_FINAL_ANCHORED_FIXED_COPPER_ATTEMPTS +const MAX_TOTAL_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS = + MAX_EARLY_SHORT_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS + + MAX_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS + + MAX_FINAL_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS +const MAX_TOTAL_ANCHORED_FIXED_COPPER_ITERATIONS = + MAX_EARLY_SHORT_ANCHORED_FIXED_COPPER_ITERATIONS + + MAX_ANCHORED_FIXED_COPPER_ITERATIONS + + MAX_FINAL_ANCHORED_FIXED_COPPER_ITERATIONS +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 MAX_EARLY_FIXED_OVERLAP_LAYER_DETOUR_DRC_EVALUATIONS = 128 +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 = [ + { reverse: false, shortenPath: true }, + { reverse: false, shortenPath: false }, + { reverse: true, shortenPath: false }, +] as const + +const INTERIOR_B01_VARIANTS = [ + { reverse: false, shortenPath: false }, + { reverse: true, shortenPath: false }, +] as const + +const FIXED_ONLY_B01_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" + ? 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[0] === 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 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 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 }, + 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 initialHdRoutes: HighDensityRoute[] + private readonly terminalConstraints: TerminalConstraint[] + private readonly b01Rerouter: Pipeline9B01Rerouter + 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 selectedB01IterationLimit = DEFAULT_MAX_B01_TOTAL_ITERATIONS + private viaMicroShiftAttempts = 0 + private viaMicroShiftsAccepted = 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 + 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 finalOwnerIterationLimit = MAX_FINAL_OWNER_B01_ITERATIONS + private postRepairSameNetViaMergeAttempts = 0 + private postRepairSameNetViaMergeDrcEvaluations = 0 + private postRepairSameNetViaMergeCandidatesAccepted = 0 + private postRepairSameNetViaMergeIterations = 0 + private sharedTerminalCompositeAttempts = 0 + private sharedTerminalCompositeRelocatedBranches = 0 + private sharedTerminalCompositeB01Attempts = 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 anchoredFixedCopperAttempts = 0 + private anchoredFixedCopperDrcEvaluations = 0 + private anchoredFixedCopperCandidatesAccepted = 0 + private anchoredFixedCopperIterations = 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 + private finalFixedOverlapLayerDetourDrcEvaluations = 0 + private finalFixedOverlapLayerDetourCandidatesAccepted = 0 + private finalFixedOverlapBestRemovedTargetIssueCount = + Number.POSITIVE_INFINITY + private finalFixedOverlapBestRemovedTargetFixedScore = + Number.POSITIVE_INFINITY + + constructor(params: Pipeline9ExactDrcRepairSolverParams) { + 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 = 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, + }, + ] + }), + ) + } + + private getSnapshot(routes: HighDensityRoute[]) { + return getDrcSnapshot( + this.params.srj, + routes, + this.params.drcEvaluator, + this.params.connMap, + ) + } + + 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[] { + 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[], + currentSnapshot: DrcSnapshot, + source: "local" | "b01" = "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 ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + currentSnapshot, + ) + ) { + 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.selectedB01IterationLimit = useHighInitialDrcLimits + ? HIGH_INITIAL_DRC_MAX_B01_TOTAL_ITERATIONS + : DEFAULT_MAX_B01_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" && + errorType !== "pcb_trace_error" + ) { + return undefined + } + const errorCenter = this.getErrorCenter(error) + if (!errorCenter) return undefined + + const snapshot = this.getSnapshot(routes) + if (errorType === "pcb_trace_error" && snapshot.count > 2) { + return undefined + } + const preloadedTraceId = + errorType === "pcb_via_trace_clearance_error" || + errorType === "pcb_trace_error" + ? (this.b01Rerouter.getPreloadedTraceIdForDrcTraceId( + typeof error.pcb_trace_id === "string" + ? error.pcb_trace_id + : undefined, + ) ?? + this.b01Rerouter.getPreloadedTraceIdForDrcTraceId( + getRawOtherTraceId(error), + )) + : undefined + if (errorType === "pcb_trace_error" && !preloadedTraceId) { + return 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, + ) + : [] + if ( + errorType === "pcb_trace_error" && + overlappingViaCenters.length === 0 + ) { + continue + } + 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) { + 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)) { + 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 ( + !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) { + if (!this.hasLocalCleanupBudget()) return undefined + 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)) { + return materializedCandidate + } + } + } + } + + return undefined + } + + private tryLocalTraceLayerDetour( + routes: HighDensityRoute[], + error: DrcError, + options: { + preferFixedCopperIssueReduction?: boolean + maxIssueCountIncrease?: number + } = {}, + ): HighDensityRoute[] | undefined { + if (!this.hasLocalCleanupBudget()) return 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 } + 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)! + 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 + } + + 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, + ) + const detourEnd = Math.min( + sameLayerEnd, + nearestSegmentIndex + 1 + expansion, + ) + if (detourEnd <= detourStart) continue + + for ( + let targetZ = 0; + targetZ < this.params.srj.layerCount; + targetZ += 1 + ) { + if (!this.hasLocalCleanupBudget()) return undefined + 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 (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)) { + return materializedCandidate + } + } + } + } + + if (bestFixedCopperCandidate) { + this.cleanupCandidatesAccepted += 1 + this.consecutiveLocalCleanupDrcMisses = 0 + return bestFixedCopperCandidate.routes + } + return undefined + } + + private tryBatchedTraceForce( + routes: HighDensityRoute[], + error: DrcError, + ): HighDensityRoute[] | undefined { + if (!this.hasLocalCleanupBudget()) return 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) { + if (!this.hasLocalCleanupBudget()) return undefined + 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)) { + return materializedCandidate + } + candidateRoutes = cloneRoutes(materializedCandidate) + } + } + } + + return undefined + } + + private getRemainingB01Iterations(): number { + return Math.max(0, this.selectedB01IterationLimit - this.b01Iterations) + } + + private tryB01Candidate( + routes: HighDensityRoute[], + snapshot: DrcSnapshot, + options: Omit, + maxIterations: number, + ): HighDensityRoute[] | undefined { + const iterationLimit = Math.min( + maxIterations, + this.getRemainingB01Iterations(), + ) + if (iterationLimit <= 0) return undefined + + const result = this.b01Rerouter.tryReroute(routes, { + ...options, + maxIterations: iterationLimit, + }) + this.b01Iterations += 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, "b01") + ) { + return undefined + } + + this.b01CandidatesAccepted += 1 + return materializedCandidate + } + + private getOrderedB01RouteIndexes(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) + } + } + } + + 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 runB01FullRouteCleanup( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let roundAttempts = 0 + + while ( + 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.getOrderedB01RouteIndexes(snapshot) + + attemptLoop: for (const variant of FULL_B01_VARIANTS) { + for (const routeIndex of routeIndexes) { + if ( + roundAttempts >= MAX_B01_FULL_ATTEMPTS_PER_ROUND || + this.getRemainingB01Iterations() <= 0 + ) { + break attemptLoop + } + roundAttempts += 1 + this.b01FullAttempts += 1 + nextRoutes = this.tryB01Candidate( + improvedRoutes, + snapshot, + { + routeIndex, + includeCandidateCopper: true, + ...variant, + }, + MAX_B01_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 < 0) return [] + + const lastRouteIndex = route.route.length - 1 + const lastInteriorIndex = route.route.length - 2 + + 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 }) + } + 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)) + } + } + 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) + if ( + boundedStart > nearestSegmentIndex || + boundedEnd < nearestSegmentIndex + 1 || + boundedStart >= boundedEnd + ) { + return + } + addRawWindow(boundedStart, boundedEnd) + } + + addWindow( + nearestSegmentIndex - MAX_B01_INTERIOR_EXPANSION, + nearestSegmentIndex + 1, + ) + addWindow( + nearestSegmentIndex, + nearestSegmentIndex + 1 + MAX_B01_INTERIOR_EXPANSION, + ) + addWindow( + nearestSegmentIndex - MAX_B01_INTERIOR_EXPANSION, + nearestSegmentIndex + 1 + MAX_B01_INTERIOR_EXPANSION, + ) + + for ( + let totalExpansion = 0; + totalExpansion <= MAX_B01_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 runB01InteriorCleanup( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let roundAttempts = 0 + + while ( + roundAttempts < MAX_B01_INTERIOR_ATTEMPTS_PER_ROUND && + this.getRemainingB01Iterations() > 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_B01_VARIANTS) { + for (const target of targets) { + const window = target.windows[windowIndex] + if (!window) continue + if ( + roundAttempts >= MAX_B01_INTERIOR_ATTEMPTS_PER_ROUND || + this.getRemainingB01Iterations() <= 0 + ) { + break attemptLoop + } + roundAttempts += 1 + this.b01InteriorAttempts += 1 + nextRoutes = this.tryB01Candidate( + improvedRoutes, + snapshot, + { + routeIndex: target.routeIndex, + ...window, + includeCandidateCopper: true, + ...variant, + }, + MAX_B01_INTERIOR_ITERATIONS, + ) + if (nextRoutes) break attemptLoop + } + } + } + + if (!nextRoutes) break + improvedRoutes = nextRoutes + } + + return improvedRoutes + } + + private runB01FixedOnlyCleanup( + routes: HighDensityRoute[], + ): HighDensityRoute[] { + let improvedRoutes = routes + let roundAttempts = 0 + + while ( + 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.getOrderedB01RouteIndexes(snapshot) + + attemptLoop: for (const variant of FIXED_ONLY_B01_VARIANTS) { + for (const routeIndex of routeIndexes) { + if ( + roundAttempts >= MAX_B01_FIXED_ONLY_ATTEMPTS_PER_ROUND || + this.getRemainingB01Iterations() <= 0 + ) { + break attemptLoop + } + roundAttempts += 1 + this.b01FixedOnlyAttempts += 1 + nextRoutes = this.tryB01Candidate( + improvedRoutes, + snapshot, + { + routeIndex, + includeCandidateCopper: false, + ...variant, + }, + MAX_B01_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 runB01ErrorOwnedClusterRebuild( + 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.b01Rerouter.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.b01Rerouter + .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.b01Rerouter.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 ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + baselineSnapshot, + ) + ) { + 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.b01Rerouter.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 ( + !this.snapshotImprovesWithoutFixedCopperRegression( + postCandidateSnapshot, + acceptedSnapshot, + ) + ) { + continue + } + + acceptedRoutes = materializedPostCandidate + acceptedSnapshot = postCandidateSnapshot + this.errorOwnedClusterPostCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + } + + this.errorOwnedClusterAccepted += 1 + this.cleanupCandidatesAccepted += 1 + return acceptedRoutes + } + + return routes + } + + private runB01ErrorOwnedClusterRebuildPasses( + 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.runB01ErrorOwnedClusterRebuild(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, + this.finalOwnerIterationLimit - 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 tryFinalOwnerB01Candidate( + 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.b01Rerouter.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 ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + snapshot, + ) + ) { + return undefined + } + + this.finalOwnerCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + return { + routes: materializedCandidate, + snapshot: candidateSnapshot, + } + } + + private runB01FinalErrorOwnerSweep( + 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.getOrderedB01RouteIndexes(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.tryFinalOwnerB01Candidate( + 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.tryFinalOwnerB01Candidate( + 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.getOrderedB01RouteIndexes(snapshot)) { + const remainingIterations = this.getRemainingFinalOwnerIterations() + if (remainingIterations <= 0) break fallbackLoop + acceptedCandidate = this.tryFinalOwnerB01Candidate( + 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 normalizePipeline9ViaMetadataFromLayerTransitions(routes) + } + + 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 ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + baselineSnapshot, + ) + ) { + 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 ( + this.snapshotImprovesWithoutFixedCopperRegression( + relocatedSnapshot, + baselineSnapshot, + ) + ) { + this.sharedTerminalCompositeCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + return atomicCandidate + } + if ( + relocatedSnapshot.count > baselineSnapshot.count || + this.getFixedCopperIssueScore(relocatedSnapshot) > + this.getFixedCopperIssueScore(baselineSnapshot) + ) { + 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_B01_ITERATIONS - + this.sharedTerminalCompositeIterations + if (remainingIterations <= 0) break + this.sharedTerminalCompositeB01Attempts += 1 + const rerouteResult = this.b01Rerouter.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 ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + baselineSnapshot, + ) + ) { + 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_B01_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_B01_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.b01Rerouter.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 ( + this.snapshotImprovesWithoutFixedCopperRegression( + rawCandidateSnapshot, + snapshot, + ) + ) { + 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 ( + !this.snapshotImprovesWithoutFixedCopperRegression( + atomicCandidateSnapshot, + snapshot, + ) + ) { + 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 getAnchoredFixedCopperWindow( + routes: HighDensityRoute[], + routeIndex: number, + centers: ReadonlyArray<{ x: number; y: number }>, + halfSpan: 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) - halfSpan, + ) + const endRouteDistance = Math.min( + totalRouteDistance, + Math.max(...projectedRouteDistances) + halfSpan, + ) + 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_TOTAL_ANCHORED_FIXED_COPPER_ITERATIONS - + this.anchoredFixedCopperIterations, + ) + } + + 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, + routeIndex: number, + centers: ReadonlyArray<{ x: number; y: number }>, + options: { + includeCandidateCopper: boolean + reverse: boolean + halfSpan: number + requireSnapshotImprovement?: boolean + }, + ): + | { + routes: HighDensityRoute[] + snapshot: DrcSnapshot + } + | undefined { + const anchoredWindow = this.getAnchoredFixedCopperWindow( + routes, + routeIndex, + centers, + options.halfSpan, + ) + if (!anchoredWindow) return undefined + const iterationLimit = Math.min( + MAX_ANCHORED_FIXED_COPPER_ITERATIONS_PER_ATTEMPT, + this.getRemainingAnchoredFixedCopperIterations(), + ) + if ( + iterationLimit <= 0 || + this.anchoredFixedCopperAttempts >= + MAX_TOTAL_ANCHORED_FIXED_COPPER_ATTEMPTS || + this.anchoredFixedCopperDrcEvaluations >= + MAX_TOTAL_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) + 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 && + (options.requireSnapshotImprovement || !fixedGeometryImproved) + ) { + return undefined + } + + this.anchoredFixedCopperCandidatesAccepted += 1 + this.cleanupCandidatesAccepted += 1 + return { + routes: materializedCandidate, + snapshot: candidateSnapshot, + } + } + + private runAnchoredFixedCopperRepair( + routes: HighDensityRoute[], + options: { + halfSpans?: ReadonlyArray + requireSnapshotImprovement?: boolean + maxAdditionalAttempts?: number + maxAdditionalDrcEvaluations?: number + maxAdditionalIterations?: number + maximumResidualIssueCount?: number + } = {}, + ): HighDensityRoute[] { + let improvedRoutes = routes + let snapshot = this.getSnapshot(improvedRoutes) + if ( + options.maximumResidualIssueCount !== undefined && + snapshot.count > options.maximumResidualIssueCount + ) { + return routes + } + const attemptLimit = + this.anchoredFixedCopperAttempts + + (options.maxAdditionalAttempts ?? MAX_ANCHORED_FIXED_COPPER_ATTEMPTS) + const drcEvaluationLimit = + this.anchoredFixedCopperDrcEvaluations + + (options.maxAdditionalDrcEvaluations ?? + MAX_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS) + const iterationLimit = + this.anchoredFixedCopperIterations + + (options.maxAdditionalIterations ?? MAX_ANCHORED_FIXED_COPPER_ITERATIONS) + + repairLoop: while ( + snapshot.count > 0 && + this.anchoredFixedCopperIterations < iterationLimit && + this.anchoredFixedCopperAttempts < attemptLimit && + this.anchoredFixedCopperDrcEvaluations < drcEvaluationLimit + ) { + const centersByRouteIndex = new Map< + number, + Array<{ x: number; y: number }> + >() + for (const error of snapshot.errors) { + const center = this.getErrorCenter(error) + for (const routeIndex of this.getCandidateRouteIndexesForError( + error, + snapshot, + )) { + const preloadedTraceId = + this.b01Rerouter.getPreloadedTraceIdForDrcTraceId( + typeof error.pcb_trace_id === "string" + ? error.pcb_trace_id + : getRawOtherTraceId(error), + ) + 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) + } + } + + 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) { + const halfSpans = options.halfSpans ?? [ + ANCHORED_FIXED_COPPER_HALF_SPAN, + ] + for (const halfSpan of halfSpans) { + for (const includeCandidateCopper of [true, false]) { + for (const reverse of [false, true]) { + const candidate = this.tryAnchoredFixedCopperCandidate( + improvedRoutes, + snapshot, + routeIndex, + centerGroup, + { + includeCandidateCopper, + reverse, + halfSpan, + requireSnapshotImprovement: + options.requireSnapshotImprovement, + }, + ) + if (!candidate) { + if ( + this.anchoredFixedCopperIterations >= iterationLimit || + this.anchoredFixedCopperAttempts >= attemptLimit || + this.anchoredFixedCopperDrcEvaluations >= drcEvaluationLimit + ) { + 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.b01Rerouter.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 ( + this.snapshotImprovesWithoutFixedCopperRegression( + primarySnapshot, + baselineSnapshot, + ) + ) { + 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.b01Rerouter.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 ( + this.snapshotImprovesWithoutFixedCopperRegression( + followupSnapshot, + baselineSnapshot, + ) + ) { + 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 ( + this.snapshotImprovesWithoutFixedCopperRegression( + workingSnapshot, + baselineSnapshot, + ) + ) { + 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 ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + snapshot, + ) + ) { + 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 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) + 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, 5, 8, 12, 20, 40]) { + 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, + ) + const targetOverlapCount = fixedTraceId + ? this.b01Rerouter.countRouteOverlapsWithPreloadedTrace( + materializedCandidate[routeIndex]!, + fixedTraceId, + ) + : baselineTargetOverlapCount + const targetGeometryImproved = + fixedTraceId !== undefined && + targetOverlapCount < baselineTargetOverlapCount + if ( + (!targetWasRemoved && !targetGeometryImproved) || + 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 { + 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 ( + !this.snapshotImprovesWithoutFixedCopperRegression( + candidateSnapshot, + snapshot, + ) + ) { + 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.normalizeViaMetadataFromLayerTransitions( + 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) + 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 + if (issueCountBeforeRound === 0) break + + let issueCountBeforePhase = issueCountBeforeRound + improvedRoutes = this.runB01FullRouteCleanup(improvedRoutes) + let issueCountAfterPhase = this.getSnapshot(improvedRoutes).count + if (issueCountAfterPhase < issueCountBeforePhase) { + improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) + } + + issueCountBeforePhase = this.getSnapshot(improvedRoutes).count + improvedRoutes = this.runB01InteriorCleanup(improvedRoutes) + issueCountAfterPhase = this.getSnapshot(improvedRoutes).count + if (issueCountAfterPhase < issueCountBeforePhase) { + improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) + } + + issueCountBeforePhase = this.getSnapshot(improvedRoutes).count + improvedRoutes = this.runB01FixedOnlyCleanup(improvedRoutes) + issueCountAfterPhase = this.getSnapshot(improvedRoutes).count + if (issueCountAfterPhase < issueCountBeforePhase) { + improvedRoutes = this.runViaMicroShiftCleanup(improvedRoutes) + } + + if (this.getSnapshot(improvedRoutes).count >= issueCountBeforeRound) { + break + } + } + + improvedRoutes = this.runB01ErrorOwnedClusterRebuildPasses(improvedRoutes) + improvedRoutes = this.runPostClusterViaMicroShiftCleanup(improvedRoutes) + improvedRoutes = this.runB01FinalErrorOwnerSweep(improvedRoutes) + improvedRoutes = this.runPostRepairSameNetViaMerge(improvedRoutes) + improvedRoutes = this.runSharedTerminalCompositeRepair(improvedRoutes) + improvedRoutes = this.runPostFinalCompositeRepair(improvedRoutes) + if ( + (this.stats.initialDrcIssueCount ?? Number.POSITIVE_INFINITY) <= + MAX_EARLY_SHORT_ANCHORED_INITIAL_DRC_ISSUES + ) { + improvedRoutes = this.runAnchoredFixedCopperRepair(improvedRoutes, { + halfSpans: RESIDUAL_ANCHORED_FIXED_COPPER_HALF_SPANS, + requireSnapshotImprovement: true, + maxAdditionalAttempts: MAX_EARLY_SHORT_ANCHORED_FIXED_COPPER_ATTEMPTS, + maxAdditionalDrcEvaluations: + MAX_EARLY_SHORT_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS, + maxAdditionalIterations: + MAX_EARLY_SHORT_ANCHORED_FIXED_COPPER_ITERATIONS, + }) + } + improvedRoutes = this.runAnchoredFixedCopperRepair(improvedRoutes) + improvedRoutes = this.runFixedCopperCompositeRepair(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) + improvedRoutes = this.runAnchoredFixedCopperRepair(improvedRoutes, { + halfSpans: RESIDUAL_ANCHORED_FIXED_COPPER_HALF_SPANS, + requireSnapshotImprovement: true, + maxAdditionalAttempts: MAX_FINAL_ANCHORED_FIXED_COPPER_ATTEMPTS, + maxAdditionalDrcEvaluations: + MAX_FINAL_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS, + maxAdditionalIterations: MAX_FINAL_ANCHORED_FIXED_COPPER_ITERATIONS, + maximumResidualIssueCount: 2, + }) + improvedRoutes = + this.normalizeViaMetadataFromLayerTransitions(improvedRoutes) + + return this.restoreTerminalIds(improvedRoutes) + } + + override _step(): void { + if (!this.cleanupStarted) { + super._step() + if (!this.solved) return + this.cleanupStarted = true + this.solved = false + } + + 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, + pipeline9LocalCleanupDrcEvaluations: this.localCleanupDrcEvaluations, + pipeline9SelectedLocalCleanupDrcEvaluationLimit: + this.selectedLocalCleanupDrcEvaluationLimit, + pipeline9ConsecutiveLocalCleanupDrcMisses: + this.consecutiveLocalCleanupDrcMisses, + pipeline9MaxConsecutiveLocalCleanupDrcMisses: + this.maxConsecutiveLocalCleanupDrcMisses, + pipeline9SelectedConsecutiveLocalCleanupDrcMissLimit: + this.selectedConsecutiveLocalCleanupDrcMissLimit, + pipeline9ViaMicroShiftAttempts: this.viaMicroShiftAttempts, + pipeline9ViaMicroShiftsAccepted: this.viaMicroShiftsAccepted, + pipeline9B01FullAttempts: this.b01FullAttempts, + pipeline9B01InteriorAttempts: this.b01InteriorAttempts, + pipeline9B01FixedOnlyAttempts: this.b01FixedOnlyAttempts, + pipeline9B01CandidatesAccepted: this.b01CandidatesAccepted, + pipeline9B01Iterations: this.b01Iterations, + pipeline9SelectedB01IterationLimit: this.selectedB01IterationLimit, + 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: this.finalOwnerIterationLimit, + 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, + pipeline9SharedTerminalCompositeB01Attempts: + this.sharedTerminalCompositeB01Attempts, + pipeline9SharedTerminalCompositeDrcEvaluations: + this.sharedTerminalCompositeDrcEvaluations, + pipeline9SharedTerminalCompositeCandidatesAccepted: + this.sharedTerminalCompositeCandidatesAccepted, + pipeline9SharedTerminalCompositeIterations: + this.sharedTerminalCompositeIterations, + pipeline9SharedTerminalCompositeIterationLimit: + MAX_SHARED_TERMINAL_COMPOSITE_B01_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_B01_ITERATIONS, + pipeline9PostFinalCompositeSameNetViaMergeIterations: + 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_TOTAL_ANCHORED_FIXED_COPPER_ATTEMPTS, + pipeline9AnchoredFixedCopperDrcEvaluationLimit: + MAX_TOTAL_ANCHORED_FIXED_COPPER_DRC_EVALUATIONS, + pipeline9AnchoredFixedCopperIterationLimit: + MAX_TOTAL_ANCHORED_FIXED_COPPER_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, + 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 new file mode 100644 index 000000000..07daca9dc --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preloaded-trace-graph-solver.ts @@ -0,0 +1,315 @@ +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 { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ" +import { minimumDistanceBetweenSegments } from "lib/utils/minimumDistanceBetweenSegments" +import { resolvePreloadedTraceCanonicalNetIds } from "lib/utils/resolvePreloadedTraceCanonicalNetIds" + +type Point = { x: number; y: number } + +type PreloadedTracePrimitive = { + traceId: string + fixedNetId: string + connectionName: string + routePositionStart: number + routePositionEnd: number + zLayers: number[] + start: Point + end: Point +} + +type RoutePoint = SimplifiedPcbTrace["route"][number] +type WireRoutePoint = Extract + +const GEOMETRIC_TOLERANCE = 1e-6 + +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 isWireRoutePoint = (point: RoutePoint): point is WireRoutePoint => + point.route_type === "wire" + +const getPreloadedTracePrimitives = ( + srj: SimpleRouteJson, +): PreloadedTracePrimitive[] => { + const primitives: PreloadedTracePrimitive[] = [] + const canonicalNetIdByTraceId = resolvePreloadedTraceCanonicalNetIds(srj) + + 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 fixedNetId = + canonicalNetIdByTraceId.get(trace.pcb_trace_id) ?? trace.connection_name + + 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, + srj.layerCount, + ), + start: routePoint, + end: routePoint, + }) + } 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, + srj.layerCount, + ), + start: routePoint.start, + end: routePoint.end, + }) + } else if (routePoint.route_type === "jumper") { + const z = mapLayerNameToZ(routePoint.layer, srj.layerCount) + 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, + }) + } + } + } + + 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({ + traceId: trace.pcb_trace_id, + fixedNetId, + connectionName: trace.connection_name, + routePositionStart: pointIndex, + routePositionEnd: pointIndex + 1, + zLayers: [mapLayerNameToZ(start.layer, srj.layerCount)], + start, + end, + }) + } + } + + return primitives +} + +const getClosestPortPoint = ( + segment: SharedEdgeSegment, + primitive: PreloadedTracePrimitive, + z: number, +): SegmentPortPoint | undefined => + segment.portPoints + .filter( + (portPoint) => + portPoint.availableZ.includes(z) && + !(portPoint._preloadedTracePortAssignments ?? []).some( + (assignment) => + assignment.z === z && + assignment.fixedNetId !== primitive.fixedNetId, + ), + ) + .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, + z: number, +) => { + const fixedNetIds = [ + ...new Set([ + ...(portPoint._preloadedFixedNetIds ?? []), + primitive.fixedNetId, + ]), + ].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 + portPoint.rootConnectionName = primitive.fixedNetId + } +} + +/** + * 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 primitives: PreloadedTracePrimitive[] + + constructor( + private readonly sharedEdgeSegments: SharedEdgeSegment[], + private readonly srj: SimpleRouteJson, + ) { + super() + this.MAX_ITERATIONS = 1 + this.primitives = getPreloadedTracePrimitives(srj) + } + + override getSolverName(): string { + return "PreloadedTraceGraphSolver" + } + + 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, + ) > GEOMETRIC_TOLERANCE + ) { + continue + } + + for (const z of primitive.zLayers) { + if (!segment.availableZ.includes(z)) continue + const portPoint = getClosestPortPoint(segment, primitive, z) + if (portPoint) preloadPort(portPoint, primitive, z) + } + } + } + + const portPoints = this.sharedEdgeSegments.flatMap( + (segment) => segment.portPoints, + ) + const preloadedPortPoints = portPoints.filter( + (portPoint) => (portPoint._preloadedFixedNetIds?.length ?? 0) > 0, + ) + this.stats = { + preloadedTraceCount: this.srj.traces?.length ?? 0, + 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(): SharedEdgeSegment[] { + if (!this.solved) { + throw new Error("PreloadedTraceGraphSolver has not solved yet") + } + return this.sharedEdgeSegments + } +} 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..cdf383c57 --- /dev/null +++ b/lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/preprocess-simple-route-json-without-trace-obstacles-solver.ts @@ -0,0 +1,31 @@ +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" +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 inputSrjWithTraceConnectivity = inferPreloadedTraceConnectivity( + this.inputSrj, + ) + const { traces, ...inputSrjWithoutTraces } = inputSrjWithTraceConnectivity + const srjWithBoardValidObstacleLayers = + createSrjWithBoardValidObstacleLayers(inputSrjWithoutTraces) + const srjWithExpandedImplicitBounds = + expandImplicitBoundsToConnectedObstacles( + filterObstaclesOutsideBoard(srjWithBoardValidObstacleLayers), + ) + const srjWithApproximatingRects = addApproximatingRectsToSrj( + srjWithExpandedImplicitBounds, + ) + 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/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver.ts b/lib/solvers/AvailableSegmentPointSolver/AvailableSegmentPointSolver.ts index 88ef7a7a1..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 @@ -27,6 +36,10 @@ 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[] + /** Ordered crossings used to load fixed intra-region segments. */ + _preloadedTracePortAssignments?: PreloadedTracePortAssignment[] } export interface SharedEdgeSegment { 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/hgportpointpathingsolver/buildHyperGraph.ts b/lib/solvers/PortPointPathingSolver/hgportpointpathingsolver/buildHyperGraph.ts index 616e5b0e8..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,6 +232,8 @@ export function buildHyperGraph(params: { cramped: spp.cramped, regions: [region1, region2], tinyHypergraphPortPenalty: spp.tinyHypergraphPortPenalty, + _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 597c13955..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 @@ -23,6 +24,8 @@ export type RawPort = { cramped?: boolean 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 4bec9097e..3976a862e 100644 --- a/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts +++ b/lib/solvers/PortPointPathingSolver/tinyhypergraph/TinyHypergraphPortPointPathingSolver.ts @@ -25,17 +25,26 @@ import { type TinyHyperGraphSectionSolverOptions, type TinyHyperGraphSolverOptions, } from "tiny-hypergraph/lib/index" +import { applyInitialAssignments } from "tiny-hypergraph/lib/initialAssignments" import type { ConnectionHg, ConnectionHgWithSimpleRouteConnection, HgPortPointPathingSolverParams, } from "../hgportpointpathingsolver/types" +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"] } @@ -62,6 +71,9 @@ type TinyBounds = { type TinyRegionMetadata = { bounds?: TinyBounds + netId?: number + NetId?: number + serializedRegionId?: string _qfpRegionType?: InputNodeWithPortPoints["_qfpRegionType"] _isNarrowQfpPadGap?: boolean _offBoardConnectionId?: string @@ -79,6 +91,8 @@ type TinyPortMetadata = { _tinyTerminal?: boolean tinyHypergraphPortPenalty?: number duplicatedFromPortId?: string + _preloadedFixedNetIds?: string[] + _preloadedTracePortAssignments?: PreloadedTracePortAssignment[] } type LoadedTinyGraph = { @@ -271,6 +285,8 @@ const toSerializedPortData = ( distToCentermostPortOnZ: port.d.distToCentermostPortOnZ, tinyHypergraphPortPenalty: port.d.tinyHypergraphPortPenalty, cramped: port.d.cramped, + _preloadedFixedNetIds: port.d._preloadedFixedNetIds, + _preloadedTracePortAssignments: port.d._preloadedTracePortAssignments, } } @@ -458,12 +474,14 @@ const buildSerializedTinyGraph = ( } as SerializedTinySolvedRoute) } - return { + const serializedHyperGraph = { regions, ports, connections, solvedRoutes, } satisfies SerializedHyperGraph + serializePreloadedTraceAssignments(serializedHyperGraph) + return serializedHyperGraph } const buildInputNodesWithPortPoints = ( @@ -568,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, @@ -627,11 +689,35 @@ const applyMetadataPortPenalties = (loaded: LoadedTinyGraph) => { return metadataPortPenaltyCount } +/** + * 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() duplicatePortPenaltyCount = 0 metadataPortPenaltyCount = 0 crampedPortPenaltyCount = 0 + preloadedPortCount = 0 + preloadedFixedSegmentCount = 0 readonly crampedPortTraversalPenalty: number readonly useSelectiveReripRouting: boolean @@ -642,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", @@ -651,7 +742,13 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect "Tiny hypergraph pipeline is missing the solveGraph stage", ) } - solveGraphStep.solverClass = SelectiveReripTinyHyperGraphSolver + solveGraphStep.solverClass = + SelectiveReripTinyHyperGraphSolverWithStableInitialAssignments + } + if (preloadedStats.preloadedAssignmentCount > 0) { + this.pipelineDef = this.pipelineDef.filter( + (pipelineStep) => pipelineStep.solverName !== "optimizeSection", + ) } this.MAX_ITERATIONS = getTinyHyperGraphPipelineMaxIterations(inputProblem) } @@ -662,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, @@ -697,11 +795,12 @@ 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) @@ -738,6 +837,7 @@ class TinyHyperGraphSectionPipelineWithTerminalNetIds extends TinyHyperGraphSect const loadedSolver = solver as typeof solver & LoadedTinyGraph applyMetadataPortPenalties(loadedSolver) applyTerminalRegionNetIds(loadedSolver) + clearPreloadedEndpointRegionNetIds(loadedSolver) } this.configuredSolvers.add(solver) @@ -827,7 +927,12 @@ export class TinyHypergraphPortPointPathingSolver extends BaseSolver { ]), ) const serializedGraph = buildSerializedTinyGraph({ ...params, connections }) + const preloadedTraceStats = + getSerializedPreloadedTraceStats(serializedGraph) + const hasPreloadedTraceOccupancy = + preloadedTraceStats.preloadedPortCount > 0 const shouldRunDuplicateCongestedPortPrepass = + !hasPreloadedTraceOccupancy && connections.length <= MAX_CONNECTIONS_FOR_DUPLICATE_CONGESTED_PORT_PREPASS let graphForTiny = serializedGraph if (shouldRunDuplicateCongestedPortPrepass) { @@ -857,7 +962,9 @@ export class TinyHypergraphPortPointPathingSolver extends BaseSolver { graphForTiny = duplicateCongestedPortSolver.getOutput() } } else { - this.duplicateCongestedPortError = `Skipped for ${connections.length} connections` + this.duplicateCongestedPortError = hasPreloadedTraceOccupancy + ? "Skipped to preserve preloaded port topology" + : `Skipped for ${connections.length} connections` } this.duplicatedPortCount = this.duplicateCongestedPortReport?.duplicatedPorts.reduce( @@ -932,6 +1039,9 @@ export class TinyHypergraphPortPointPathingSolver extends BaseSolver { this.tinyPipelineSolver.metadataPortPenaltyCount, crampedPortPenalty: this.tinyPipelineSolver.crampedPortTraversalPenalty, crampedPortPenaltyCount: this.tinyPipelineSolver.crampedPortPenaltyCount, + preloadedPortCount: this.tinyPipelineSolver.preloadedPortCount, + preloadedFixedSegmentCount: + this.tinyPipelineSolver.preloadedFixedSegmentCount, duplicateCongestedPortError: this.duplicateCongestedPortError, ...(this.tinyPipelineSolver.stats ?? {}), ...(currentTinySolver?.stats ?? {}), @@ -1037,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, @@ -1054,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/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/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/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/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver.ts b/lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver.ts index a4fb22212..b2cb58bfb 100644 --- a/lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver.ts +++ b/lib/solvers/SameNetViaMergerSolver/SameNetViaMergerSolver.ts @@ -14,6 +14,8 @@ import { segmentToBoxMinDistance } from "@tscircuit/math-utils" export interface SameNetViaMergerSolverInput { inputHdRoutes: HighDensityRoute[] + /** Routed copper that participates in collision checks but is never changed. */ + otherHdRoutes?: HighDensityRoute[] obstacles: Obstacle[] colorMap: Record layerCount: number @@ -32,12 +34,38 @@ 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, route: HighDensityRoute, ): string => { - const net = connMap.idToNetMap[route.connectionName] + const net = + connMap.idToNetMap[route.connectionName] ?? + (route.rootConnectionName + ? connMap.idToNetMap[route.rootConnectionName] + : undefined) if (!net) { throw new Error( `SameNetViaMergerSolver could not find net for route "${route.connectionName}"`, @@ -192,7 +220,10 @@ export class SameNetViaMergerSolver extends BaseSolver { "flatbush", this.input.obstacles, ) - this.hdRouteSHI = new HighDensityRouteSpatialIndex(this.inputHdRoutes) + this.hdRouteSHI = new HighDensityRouteSpatialIndex([ + ...this.inputHdRoutes, + ...(this.input.otherHdRoutes ?? []), + ]) this.vias = [] this.offendingVias = [] this.connMap = input.connMap @@ -210,6 +241,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 +251,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 +436,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 +450,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/lib/solvers/SimplifiedPathSolver/MultiSimplifiedPathSolver.ts b/lib/solvers/SimplifiedPathSolver/MultiSimplifiedPathSolver.ts index dbe2189e6..b1127453f 100644 --- a/lib/solvers/SimplifiedPathSolver/MultiSimplifiedPathSolver.ts +++ b/lib/solvers/SimplifiedPathSolver/MultiSimplifiedPathSolver.ts @@ -20,6 +20,7 @@ export class MultiSimplifiedPathSolver extends BaseSolver { activeSubSolver: SingleSimplifiedPathSolver | null = null unsimplifiedHdRoutes: HighDensityIntraNodeRoute[] + otherHdRoutes: HighDensityIntraNodeRoute[] obstacles: Obstacle[] connMap: ConnectivityMap colorMap: Record @@ -28,6 +29,8 @@ export class MultiSimplifiedPathSolver extends BaseSolver { constructor(params: { unsimplifiedHdRoutes: HighDensityIntraNodeRoute[] + /** Routed copper that participates in collision checks but is never changed. */ + otherHdRoutes?: HighDensityIntraNodeRoute[] obstacles: Obstacle[] connMap?: ConnectivityMap colorMap?: Record @@ -38,11 +41,12 @@ export class MultiSimplifiedPathSolver extends BaseSolver { this.MAX_ITERATIONS = 100e6 this.unsimplifiedHdRoutes = params.unsimplifiedHdRoutes + this.otherHdRoutes = params.otherHdRoutes ?? [] const inferredLayerCount = Math.max( 2, - ...params.unsimplifiedHdRoutes.flatMap((r) => - r.route.map((p) => p.z + 1), + ...[...params.unsimplifiedHdRoutes, ...this.otherHdRoutes].flatMap( + (r) => r.route.map((p) => p.z + 1), ), ) || 2 this.obstacles = createObjectsWithZLayers( @@ -68,9 +72,11 @@ export class MultiSimplifiedPathSolver extends BaseSolver { this.activeSubSolver = new SingleSimplifiedPathSolver5({ inputRoute: hdRoute, - otherHdRoutes: this.unsimplifiedHdRoutes - .slice(this.currentUnsimplifiedHdRouteIndex + 1) - .concat(this.simplifiedHdRoutes), + otherHdRoutes: this.otherHdRoutes.concat( + this.unsimplifiedHdRoutes + .slice(this.currentUnsimplifiedHdRouteIndex + 1) + .concat(this.simplifiedHdRoutes), + ), obstacles: this.obstacles, connMap: this.connMap, colorMap: this.colorMap, diff --git a/lib/solvers/SimplifiedPathSolver/SingleSimplifiedPathSolver5_Deg45.ts b/lib/solvers/SimplifiedPathSolver/SingleSimplifiedPathSolver5_Deg45.ts index 854f5bf32..56811cc1e 100644 --- a/lib/solvers/SimplifiedPathSolver/SingleSimplifiedPathSolver5_Deg45.ts +++ b/lib/solvers/SimplifiedPathSolver/SingleSimplifiedPathSolver5_Deg45.ts @@ -69,6 +69,25 @@ export class SingleSimplifiedPathSolver5 extends SingleSimplifiedPathSolver { TAIL_JUMP_RATIO: number = 0.8 + private isSameNetRoute(otherRoute: HighDensityIntraNodeRoute): boolean { + const inputRouteIds = [ + this.inputRoute.connectionName, + this.inputRoute.rootConnectionName, + ].filter((id): id is string => id !== undefined) + const otherRouteIds = [ + otherRoute.connectionName, + otherRoute.rootConnectionName, + ].filter((id): id is string => id !== undefined) + + return inputRouteIds.some((inputRouteId) => + otherRouteIds.some( + (otherRouteId) => + inputRouteId === otherRouteId || + this.connMap.areIdsConnected(inputRouteId, otherRouteId), + ), + ) + } + constructor( params: ConstructorParameters[0], ) { @@ -130,12 +149,7 @@ export class SingleSimplifiedPathSolver5 extends SingleSimplifiedPathSolver { this.filteredObstaclePathSegments = this.otherHdRoutes.flatMap( (hdRoute) => { - if ( - this.connMap.areIdsConnected( - this.inputRoute.connectionName, - hdRoute.connectionName, - ) - ) { + if (this.isSameNetRoute(hdRoute)) { return [] } @@ -158,12 +172,7 @@ export class SingleSimplifiedPathSolver5 extends SingleSimplifiedPathSolver { this.segmentTree = new SegmentTree(this.filteredObstaclePathSegments) this.filteredVias = this.otherHdRoutes.flatMap((hdRoute) => { - if ( - this.connMap.areIdsConnected( - this.inputRoute.connectionName, - hdRoute.connectionName, - ) - ) { + if (this.isSameNetRoute(hdRoute)) { return [] } @@ -256,12 +265,7 @@ export class SingleSimplifiedPathSolver5 extends SingleSimplifiedPathSolver { // Collect jumper pads from other routes as obstacles this.filteredJumperPads = this.otherHdRoutes.flatMap((hdRoute) => { - if ( - this.connMap.areIdsConnected( - this.inputRoute.connectionName, - hdRoute.connectionName, - ) - ) { + if (this.isSameNetRoute(hdRoute)) { return [] } diff --git a/lib/solvers/TraceSimplificationSolver/TraceSimplificationSolver.ts b/lib/solvers/TraceSimplificationSolver/TraceSimplificationSolver.ts index 80bede456..8c5d45f88 100644 --- a/lib/solvers/TraceSimplificationSolver/TraceSimplificationSolver.ts +++ b/lib/solvers/TraceSimplificationSolver/TraceSimplificationSolver.ts @@ -71,6 +71,7 @@ export class TraceSimplificationSolver extends BaseSolver { * - defaultViaDiameter: Default diameter for vias * - layerCount: Number of routing layers * - minTraceToPadEdgeClearance: Minimum trace-edge clearance to pads/vias + * - otherHdRoutes: Immutable routed traces to avoid while simplifying * - iterations: Number of complete simplification iterations (default: 2) */ constructor( @@ -83,6 +84,7 @@ export class TraceSimplificationSolver extends BaseSolver { readonly defaultViaDiameter: number readonly layerCount: number readonly minTraceToPadEdgeClearance?: number + readonly otherHdRoutes?: ReadonlyArray }, ) { super() @@ -229,6 +231,7 @@ export class TraceSimplificationSolver extends BaseSolver { case "via_removal": this.activeSubSolver = new UselessViaRemovalSolver({ unsimplifiedHdRoutes: this.hdRoutes, + otherHdRoutes: [...(this.simplificationConfig.otherHdRoutes ?? [])], obstacles: [...this.simplificationConfig.obstacles], colorMap: { ...this.simplificationConfig.colorMap }, layerCount: this.simplificationConfig.layerCount, @@ -250,6 +253,7 @@ export class TraceSimplificationSolver extends BaseSolver { case "via_merging": this.activeSubSolver = new SameNetViaMergerSolver({ inputHdRoutes: this.hdRoutes, + otherHdRoutes: [...(this.simplificationConfig.otherHdRoutes ?? [])], obstacles: [...this.simplificationConfig.obstacles], colorMap: { ...this.simplificationConfig.colorMap }, layerCount: this.simplificationConfig.layerCount, @@ -265,6 +269,7 @@ export class TraceSimplificationSolver extends BaseSolver { case "path_simplification": this.activeSubSolver = new MultiSimplifiedPathSolver({ unsimplifiedHdRoutes: this.hdRoutes, + otherHdRoutes: [...(this.simplificationConfig.otherHdRoutes ?? [])], obstacles: [...this.simplificationConfig.obstacles], connMap: this.simplificationConfig.connMap, colorMap: { ...this.simplificationConfig.colorMap }, 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/solvers/UselessViaRemovalSolver/UselessViaRemovalSolver.ts b/lib/solvers/UselessViaRemovalSolver/UselessViaRemovalSolver.ts index 70c0a7ae0..732a3d07c 100644 --- a/lib/solvers/UselessViaRemovalSolver/UselessViaRemovalSolver.ts +++ b/lib/solvers/UselessViaRemovalSolver/UselessViaRemovalSolver.ts @@ -13,6 +13,8 @@ import type { ConnectivityMap } from "circuit-json-to-connectivity-map" export interface UselessViaRemovalSolverInput { unsimplifiedHdRoutes: HighDensityRoute[] + /** Routed copper that participates in collision checks but is never changed. */ + otherHdRoutes?: HighDensityRoute[] obstacles: Obstacle[] colorMap: Record layerCount: number @@ -52,9 +54,10 @@ export class UselessViaRemovalSolver extends BaseSolver { "flatbush", this.input.obstacles, ) - this.hdRouteSHI = new HighDensityRouteSpatialIndex( - this.unsimplifiedHdRoutes, - ) + this.hdRouteSHI = new HighDensityRouteSpatialIndex([ + ...this.unsimplifiedHdRoutes, + ...(input.otherHdRoutes ?? []), + ]) } _step() { 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/lib/testing/evaluate-relaxed-drc.ts b/lib/testing/evaluate-relaxed-drc.ts index 120c4a7e0..99e6080fb 100644 --- a/lib/testing/evaluate-relaxed-drc.ts +++ b/lib/testing/evaluate-relaxed-drc.ts @@ -1,8 +1,32 @@ +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 { + getCanonicalConnectionName, + getCanonicalConnectionNameMap, +} from "lib/utils/getCanonicalConnectionNameMap" +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,19 +40,1126 @@ 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 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 canonicalNameByInputAlias = getCanonicalConnectionNameMap(inputSrj) + const candidateTraceIdsByCanonicalNet = new Map() + for (const traceId of candidateTraceIds) { + const rawCanonicalNet = canonicalNetByTraceId.get(traceId) + const canonicalNet = rawCanonicalNet + ? (canonicalNameByInputAlias.get(rawCanonicalNet) ?? rawCanonicalNet) + : undefined + 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, + canonicalNameByInputAlias, + ) + 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, + canonicalNameByInputAlias, + ) + 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) => { + const physicalCanonicalNet = + canonicalNameByInputAlias.get(trace.canonicalNet) ?? trace.canonicalNet + return physicalCanonicalNet === 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[], + _srjWithPointPairs?: SimpleRouteJson, +): 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 getPreloadedCanonicalName = (trace: SimplifiedPcbTrace) => + canonicalNetIdByOriginalTraceId.get(trace.pcb_trace_id) ?? + trace.connection_name + const preloadedTraces = originalPreloadedTraces.map((trace, index) => ({ + ...trace, + connection_name: getPreloadedCanonicalName(trace), + 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, + srjWithPointPairs, + ) + 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 4e45ecf96..d488aad0a 100644 --- a/lib/testing/utils/convertToCircuitJson.ts +++ b/lib/testing/utils/convertToCircuitJson.ts @@ -1,11 +1,28 @@ -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" 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 @@ -55,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 } }) @@ -210,9 +242,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,11 +249,116 @@ const getObstacleConnectivityIds = (obstacles: Obstacle[]) => function createSourceTraces( srj: SimpleRouteJson, hdRoutes: SimplifiedPcbTrace[] | HighDensityRoute[], + obstacleSrj: SimpleRouteJson = srj, + originalSrj?: SimpleRouteJson, + resolveMappedConnectionName: ResolveMappedConnectionName = () => undefined, ): 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 [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(canonicalRouteName) ?? + 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( + 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) @@ -236,44 +370,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 canonicalConnectionNames = new Set([ + netConnectionName, + 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 canonicalConnectionNames) { + 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 +407,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) } }) @@ -384,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 @@ -543,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") { @@ -562,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, + ), + }) } }) }) @@ -597,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), + }) } } }) @@ -631,6 +862,7 @@ export type ConvertToCircuitJsonOptions = { minViaDiameter?: number minViaHoleDiameter?: number originalSrj?: SimpleRouteJson + preloadedTraceIds?: ReadonlySet } export function convertToCircuitJson( @@ -643,6 +875,7 @@ export function convertToCircuitJson( minViaDiameter, minViaHoleDiameter, originalSrj, + preloadedTraceIds = new Set(), } = options const viaDimensions = getViaDimensions(srjWithPointPairs) const resolvedMinViaDiameter = minViaDiameter ?? viaDimensions.padDiameter @@ -658,45 +891,82 @@ 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)) - - // Add PCB ports for connection points - circuitJson.push(...createPcbPorts(srjWithPointPairs)) - - // Add PCB pads / plated holes represented by SRJ obstacles - circuitJson.push(...createPcbPadElements(originalSrj ?? srjWithPointPairs)) - - // Extract and add vias as independent pcb_via elements circuitJson.push( - ...extractViasFromRoutes( + ...createSourceTraces( + srjWithPointPairs, routes, - srjWithPointPairs.layerCount, - resolvedMinViaDiameter, - resolvedMinViaHoleDiameter, + circuitSrj, + originalSrj, + resolveMappedConnectionName, ), ) - // 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, - ) - }) + // Add PCB ports for connection points + circuitJson.push(...createPcbPorts(circuitSrj)) + + // Add PCB pads / plated holes represented by SRJ obstacles + circuitJson.push(...createPcbPadElements(circuitSrj)) - // 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, ) }) @@ -707,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/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/lib/utils/convertSrjTracesToObstacles.ts b/lib/utils/convertSrjTracesToObstacles.ts index 80e8751a3..61dacfa6f 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 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), 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/getCanonicalConnectionNameMap.ts b/lib/utils/getCanonicalConnectionNameMap.ts new file mode 100644 index 000000000..8bd2fbd02 --- /dev/null +++ b/lib/utils/getCanonicalConnectionNameMap.ts @@ -0,0 +1,124 @@ +import type { SimpleRouteConnection, SimpleRouteJson } from "lib/types" + +const getConnectionAliases = (connection: SimpleRouteConnection): string[] => + [ + connection.name, + connection.__netConnectionName, + ...(connection.__rootConnectionNames ?? []), + ].filter((alias): alias is string => Boolean(alias)) + +const getDefaultCanonicalConnectionName = ( + connection: SimpleRouteConnection, +): string => + connection.__netConnectionName ?? + connection.__rootConnectionNames?.[0] ?? + connection.name + +const chooseCanonicalConnectionName = ( + members: SimpleRouteConnection[], +): string => { + const explicitNetNames = [ + ...new Set( + members.flatMap((connection) => + connection.__netConnectionName ? [connection.__netConnectionName] : [], + ), + ), + ] + if (explicitNetNames.length === 1) return explicitNetNames[0]! + + // A full-net connection normally contains every terminal while individual + // source-trace aliases contain only two. Prefer that physical net + // definition, retaining input order as the deterministic tie-breaker. + return members.reduce((best, connection) => + connection.pointsToConnect.length > best.pointsToConnect.length + ? connection + : best, + ).name +} + +/** + * Maps every explicitly connected input alias to one canonical physical net. + * + * Only the original SRJ's shared-terminal connectivity is authoritative. + * Point-pair names are intentionally excluded so an unrelated fixed trace + * named like `route_mst0` cannot be claimed by a generated point pair. + */ +export const getCanonicalConnectionNameMap = ( + srj: Pick, +): Map => { + const connections = srj.connections + const parentByConnectionIndex = connections.map((_, index) => index) + const findRoot = (connectionIndex: number): number => { + const parent = parentByConnectionIndex[connectionIndex] ?? connectionIndex + if (parent === connectionIndex) return connectionIndex + const root = findRoot(parent) + parentByConnectionIndex[connectionIndex] = root + return root + } + const union = (leftIndex: number, rightIndex: number) => { + const leftRoot = findRoot(leftIndex) + const rightRoot = findRoot(rightIndex) + if (leftRoot !== rightRoot) { + parentByConnectionIndex[rightRoot] = leftRoot + } + } + const connectionIndexesByExplicitIdentity = new Map() + for (const [connectionIndex, connection] of connections.entries()) { + const explicitIdentities = new Set([ + connection.__netConnectionName, + ...(connection.__rootConnectionNames ?? []), + ...connection.pointsToConnect.flatMap((point) => [ + point.pointId, + point.pcb_port_id, + ]), + ]) + for (const identity of explicitIdentities) { + if (!identity) continue + const indexes = connectionIndexesByExplicitIdentity.get(identity) ?? [] + indexes.push(connectionIndex) + connectionIndexesByExplicitIdentity.set(identity, indexes) + } + } + for (const indexes of connectionIndexesByExplicitIdentity.values()) { + const firstIndex = indexes[0] + if (firstIndex === undefined) continue + for (const connectionIndex of indexes.slice(1)) { + union(firstIndex, connectionIndex) + } + } + + const membersByRoot = new Map() + for (const [connectionIndex, connection] of connections.entries()) { + const root = findRoot(connectionIndex) + const members = membersByRoot.get(root) ?? [] + members.push(connection) + membersByRoot.set(root, members) + } + + const canonicalNameByAlias = new Map() + for (const members of membersByRoot.values()) { + const canonicalName = chooseCanonicalConnectionName(members) + for (const connection of members) { + for (const alias of getConnectionAliases(connection)) { + canonicalNameByAlias.set(alias, canonicalName) + } + } + } + return canonicalNameByAlias +} + +export const getCanonicalConnectionName = ( + connection: SimpleRouteConnection, + canonicalNameByAlias: ReadonlyMap, +): string => { + for (const alias of [ + connection.__netConnectionName, + ...(connection.__rootConnectionNames ?? []), + connection.name, + ]) { + if (!alias) continue + const canonicalName = canonicalNameByAlias.get(alias) + if (canonicalName) return canonicalName + } + return getDefaultCanonicalConnectionName(connection) +} 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..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", @@ -89,7 +89,8 @@ "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", diff --git a/scripts/run-sample.ts b/scripts/run-sample.ts index a0a9844d2..2a7b8ce2c 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,10 @@ 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/__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"/>