diff --git a/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts b/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts new file mode 100644 index 000000000..035ccfd30 --- /dev/null +++ b/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts @@ -0,0 +1,242 @@ +import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" +import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem" +import type { InputProblem } from "lib/types/InputProblem" +import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { getObstacleRects } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect" +import type { Point } from "@tscircuit/math-utils" +import type { GraphicsObject } from "graphics-debug" + +export class SameNetTraceMergeSolver extends BaseSolver { + inputProblem: InputProblem + inputTracePaths: Array + maxMergeDistance: number + correctedTraceMap: Record = {} + + constructor(params: { + inputProblem: InputProblem + traces: Array + maxMergeDistance?: number + }) { + super() + this.inputProblem = params.inputProblem + this.inputTracePaths = params.traces + this.maxMergeDistance = params.maxMergeDistance ?? 0.5 + for (const tracePath of this.inputTracePaths) { + this.correctedTraceMap[tracePath.mspPairId] = { ...tracePath, tracePath: [...tracePath.tracePath] } + } + } + + override getConstructorParams(): ConstructorParameters[0] { + return { + inputProblem: this.inputProblem, + traces: this.inputTracePaths, + maxMergeDistance: this.maxMergeDistance + } + } + + private _doesSegmentIntersectObstacles( + p1: Point, + p2: Point, + staticObstacles: Array<{ minX: number; minY: number; maxX: number; maxY: number }>, + otherTraces: Array, + pad: number = 0.05 + ): boolean { + const isHorizontal = Math.abs(p1.y - p2.y) < 1e-6 + const minX = Math.min(p1.x, p2.x) + const maxX = Math.max(p1.x, p2.x) + const minY = Math.min(p1.y, p2.y) + const maxY = Math.max(p1.y, p2.y) + + // Check static obstacles + for (const obs of staticObstacles) { + if (isHorizontal) { + if (p1.y > obs.minY - pad && p1.y < obs.maxY + pad && maxX > obs.minX - pad && minX < obs.maxX + pad) { + return true + } + } else { + if (p1.x > obs.minX - pad && p1.x < obs.maxX + pad && maxY > obs.minY - pad && minY < obs.maxY + pad) { + return true + } + } + } + + // Check other traces (from different nets) + // We treat other traces as thin lines, so we add padding. + for (const trace of otherTraces) { + for (let i = 0; i < trace.length - 1; i++) { + const t1 = trace[i]! + const t2 = trace[i + 1]! + const tMinX = Math.min(t1.x, t2.x) + const tMaxX = Math.max(t1.x, t2.x) + const tMinY = Math.min(t1.y, t2.y) + const tMaxY = Math.max(t1.y, t2.y) + + // Rect-Rect intersection using thin line rects + if ( + minX - pad < tMaxX + pad && + maxX + pad > tMinX - pad && + minY - pad < tMaxY + pad && + maxY + pad > tMinY - pad + ) { + return true + } + } + } + return false + } + + private _getOtherNetsTraces(currentNetId: string): Array { + const traces: Array = [] + for (const path of Object.values(this.correctedTraceMap)) { + if (path.globalConnNetId !== currentNetId) { + traces.push(path.tracePath) + } + } + return traces + } + + override _step() { + let madeChange = false + const staticObstacles = getObstacleRects(this.inputProblem) + + const netGroups: Record = {} + for (const path of Object.values(this.correctedTraceMap)) { + const netId = path.globalConnNetId + if (!netGroups[netId]) netGroups[netId] = [] + netGroups[netId].push(path) + } + + for (const [netId, paths] of Object.entries(netGroups)) { + if (paths.length < 2) continue + + const otherNetsTraces = this._getOtherNetsTraces(netId) + + for (let i = 0; i < paths.length; i++) { + for (let j = i + 1; j < paths.length; j++) { + const pathA = paths[i]! + const pathB = paths[j]! + + for (let sa = 0; sa < pathA.tracePath.length - 1; sa++) { + for (let sb = 0; sb < pathB.tracePath.length - 1; sb++) { + const a1 = pathA.tracePath[sa]! + const a2 = pathA.tracePath[sa + 1]! + const b1 = pathB.tracePath[sb]! + const b2 = pathB.tracePath[sb + 1]! + + const aVert = Math.abs(a1.x - a2.x) < 1e-6 + const aHorz = Math.abs(a1.y - a2.y) < 1e-6 + const bVert = Math.abs(b1.x - b2.x) < 1e-6 + const bHorz = Math.abs(b1.y - b2.y) < 1e-6 + + if (aVert && bVert) { + // Check if they are close in X and overlap in Y + const distX = Math.abs(a1.x - b1.x) + if (distX > 1e-6 && distX <= this.maxMergeDistance) { + const minYA = Math.min(a1.y, a2.y) + const maxYA = Math.max(a1.y, a2.y) + const minYB = Math.min(b1.y, b2.y) + const maxYB = Math.max(b1.y, b2.y) + + if (Math.max(minYA, minYB) < Math.min(maxYA, maxYB)) { + // Overlap in Y. Try shifting A to B's X. + const newX = b1.x + let safe = true + const newA1 = { x: newX, y: a1.y } + const newA2 = { x: newX, y: a2.y } + + if (this._doesSegmentIntersectObstacles(newA1, newA2, staticObstacles, otherNetsTraces)) safe = false + + if (sa > 0) { + const a0 = pathA.tracePath[sa - 1]! + if (this._doesSegmentIntersectObstacles(a0, newA1, staticObstacles, otherNetsTraces)) safe = false + } + if (sa < pathA.tracePath.length - 2) { + const a3 = pathA.tracePath[sa + 2]! + if (this._doesSegmentIntersectObstacles(newA2, a3, staticObstacles, otherNetsTraces)) safe = false + } + + if (safe) { + // Apply shift + pathA.tracePath[sa] = newA1 + pathA.tracePath[sa + 1] = newA2 + madeChange = true + break + } + } + } + } else if (aHorz && bHorz) { + // Check if they are close in Y and overlap in X + const distY = Math.abs(a1.y - b1.y) + if (distY > 1e-6 && distY <= this.maxMergeDistance) { + const minXA = Math.min(a1.x, a2.x) + const maxXA = Math.max(a1.x, a2.x) + const minXB = Math.min(b1.x, b2.x) + const maxXB = Math.max(b1.x, b2.x) + + if (Math.max(minXA, minXB) < Math.min(maxXA, maxXB)) { + // Overlap in X. Try shifting A to B's Y. + const newY = b1.y + let safe = true + const newA1 = { x: a1.x, y: newY } + const newA2 = { x: a2.x, y: newY } + + if (this._doesSegmentIntersectObstacles(newA1, newA2, staticObstacles, otherNetsTraces)) safe = false + + if (sa > 0) { + const a0 = pathA.tracePath[sa - 1]! + if (this._doesSegmentIntersectObstacles(a0, newA1, staticObstacles, otherNetsTraces)) safe = false + } + if (sa < pathA.tracePath.length - 2) { + const a3 = pathA.tracePath[sa + 2]! + if (this._doesSegmentIntersectObstacles(newA2, a3, staticObstacles, otherNetsTraces)) safe = false + } + + if (safe) { + // Apply shift + pathA.tracePath[sa] = newA1 + pathA.tracePath[sa + 1] = newA2 + madeChange = true + break + } + } + } + } + } + if (madeChange) break + } + if (madeChange) break + } + if (madeChange) break + } + if (madeChange) break + } + + if (!madeChange) { + this.solved = true + } + } + + override visualize(): GraphicsObject { + const graphics = visualizeInputProblem(this.inputProblem, { + chipAlpha: 0.1, + connectionAlpha: 0.1, + }) + + graphics.lines = graphics.lines || [] + for (const trace of Object.values(this.correctedTraceMap)) { + graphics.lines.push({ + points: trace.tracePath, + strokeColor: "magenta", + strokeWidth: 0.05 + }) + } + + return graphics + } + + getOutput() { + return { + traces: Object.values(this.correctedTraceMap) + } + } +} diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index a56b50b7b..339db00ed 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -12,6 +12,7 @@ import { type SolvedTracePath, } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" import { TraceOverlapShiftSolver } from "../TraceOverlapShiftSolver/TraceOverlapShiftSolver" +import { SameNetTraceMergeSolver } from "../SameNetTraceMergeSolver/SameNetTraceMergeSolver" import { NetLabelPlacementSolver } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" import { colorAvailableNetOrientationLabels } from "./colorAvailableNetOrientationLabels" import { visualizeInputProblem } from "./visualizeInputProblem" @@ -72,6 +73,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { schematicTraceLinesSolver?: SchematicTraceLinesSolver longDistancePairSolver?: LongDistancePairSolver traceOverlapShiftSolver?: TraceOverlapShiftSolver + sameNetTraceMergeSolver?: SameNetTraceMergeSolver netLabelPlacementSolver?: NetLabelPlacementSolver labelMergingSolver?: MergedNetLabelObstacleSolver traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver @@ -156,6 +158,27 @@ export class SchematicTracePipelineSolver extends BaseSolver { onSolved: (_solver) => {}, }, ), + definePipelineStep( + "sameNetTraceMergeSolver", + SameNetTraceMergeSolver, + () => [ + { + inputProblem: this.inputProblem, + traces: Object.values( + this.traceOverlapShiftSolver?.correctedTraceMap ?? + Object.fromEntries( + this.longDistancePairSolver!.getOutput().allTracesMerged.map( + (p) => [p.mspPairId, p], + ), + ), + ), + maxMergeDistance: 0.5, + }, + ], + { + onSolved: (_solver) => {}, + }, + ), definePipelineStep( "netLabelPlacementSolver", NetLabelPlacementSolver, @@ -163,6 +186,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { { inputProblem: this.inputProblem, inputTraceMap: + this.sameNetTraceMergeSolver?.correctedTraceMap ?? this.traceOverlapShiftSolver?.correctedTraceMap ?? Object.fromEntries( this.longDistancePairSolver!.getOutput().allTracesMerged.map( @@ -182,6 +206,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { TraceLabelOverlapAvoidanceSolver, (instance) => { const traceMap = + instance.sameNetTraceMergeSolver?.correctedTraceMap ?? instance.traceOverlapShiftSolver?.correctedTraceMap ?? Object.fromEntries( instance @@ -325,6 +350,14 @@ export class SchematicTracePipelineSolver extends BaseSolver { this.endTimeOfPhase = {} this.timeSpentOnPhase = {} this.firstIterationOfPhase = {} + + // Early exit: if there are no connections to route, mark as solved immediately + if ( + this.inputProblem.directConnections.length === 0 && + this.inputProblem.netConnections.length === 0 + ) { + this.solved = true + } } override getConstructorParams(): ConstructorParameters< diff --git a/tests/assets/empty-connections.json b/tests/assets/empty-connections.json new file mode 100644 index 000000000..8a4097696 --- /dev/null +++ b/tests/assets/empty-connections.json @@ -0,0 +1,18 @@ +{ + "chips": [ + { + "chipId": "U1", + "center": { "x": 0, "y": 0 }, + "width": 1.6, + "height": 0.6, + "pins": [ + { "pinId": "U1.1", "x": -0.8, "y": 0.2 }, + { "pinId": "U1.2", "x": -0.8, "y": 0 }, + { "pinId": "U1.3", "x": 0.8, "y": 0 } + ] + } + ], + "directConnections": [], + "netConnections": [], + "availableNetLabelOrientations": {} +} diff --git a/tests/examples/__snapshots__/example02.snap.svg b/tests/examples/__snapshots__/example02.snap.svg index 3815fdc0b..3359e83e3 100644 --- a/tests/examples/__snapshots__/example02.snap.svg +++ b/tests/examples/__snapshots__/example02.snap.svg @@ -159,7 +159,7 @@ orientation: y+" data-x="1.4571549750000001" data-y="0.29999999999999966" cx="53 - + @@ -196,23 +196,19 @@ orientation: y+" data-x="1.4571549750000001" data-y="0.29999999999999966" cx="53 +globalConnNetId: connectivity_net0" data-x="-1.4574283249999997" data-y="1.5274186000000005" x="283.75416992460123" y="184.33736239143013" width="16.926952823252577" height="38.08564385231816" fill="#ef444466" stroke="#ef4444" stroke-width="0.011815475714285715" /> +globalConnNetId: connectivity_net1" data-x="-3.0434765500000003" data-y="-0.4250000000000004" x="149.51935252470935" y="349.5798500586337" width="16.926952823252492" height="38.085643852318185" fill="#00000066" stroke="#000000" stroke-width="0.011815475714285715" /> +globalConnNetId: connectivity_net1" data-x="1.9148566499999995" data-y="-1.2284186000000008" x="569.1667133165424" y="417.5769937562517" width="16.926952823252577" height="38.08564385231813" fill="#00000066" stroke="#000000" stroke-width="0.011815475714285715" /> +globalConnNetId: connectivity_net2" data-x="1.4571549750000001" data-y="0.5249999999999997" x="530.4292400172993" y="269.1768241481843" width="16.926952823252464" height="38.08564385231813" fill="#ef444466" stroke="#ef4444" stroke-width="0.011815475714285715" />