From d62709b5b4335e73394f30dbb8ca5399332e6280 Mon Sep 17 00:00:00 2001 From: drmabus Date: Sat, 4 Apr 2026 16:37:16 +0500 Subject: [PATCH 1/2] feat: add SameNetTraceMergeSolver to merge close same-net trace segments --- .../SameNetTraceMergeSolver.ts | 215 ++++++++++++++++++ .../SchematicTracePipelineSolver.ts | 34 ++- 2 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts diff --git a/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts b/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts new file mode 100644 index 00000000..4e109995 --- /dev/null +++ b/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts @@ -0,0 +1,215 @@ +import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver" +import type { InputProblem } from "lib/types/InputProblem" +import type { ConnectivityMap } from "connectivity-map" +import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath" +import type { Point } from "@tscircuit/math-utils" + +const GAP_THRESHOLD = 0.15 +const EPS = 1e-9 + +type ConnNetId = string + +interface Segment { + traceId: MspConnectionPairId + segIndex: number + p1: Point + p2: Point +} + +/** + * Merges same-net trace segments that are parallel and close together. + * + * After routing, the MST may produce separate traces for the same net that run + * nearly parallel with a small gap. This phase detects those near-parallel + * segments and snaps them onto a shared coordinate, then simplifies the result. + */ +export class SameNetTraceMergeSolver extends BaseSolver { + inputProblem: InputProblem + inputTracePaths: SolvedTracePath[] + globalConnMap: ConnectivityMap + + correctedTraceMap: Record = {} + private traceNetIslands: Record = {} + private netIdsToProcess: ConnNetId[] = [] + + constructor(params: { + inputProblem: InputProblem + inputTracePaths: SolvedTracePath[] + globalConnMap: ConnectivityMap + }) { + super() + this.inputProblem = params.inputProblem + this.inputTracePaths = params.inputTracePaths + this.globalConnMap = params.globalConnMap + + for (const trace of this.inputTracePaths) { + this.correctedTraceMap[trace.mspPairId] = { + ...trace, + tracePath: [...trace.tracePath], + } + } + + this.traceNetIslands = this.computeTraceNetIslands() + this.netIdsToProcess = Object.keys(this.traceNetIslands).filter( + (netId) => this.traceNetIslands[netId]!.length > 1, + ) + } + + override getConstructorParams(): ConstructorParameters< + typeof SameNetTraceMergeSolver + >[0] { + return { + inputProblem: this.inputProblem, + inputTracePaths: this.inputTracePaths, + globalConnMap: this.globalConnMap, + } + } + + private computeTraceNetIslands(): Record { + const islands: Record = {} + for (const trace of this.inputTracePaths) { + const corrected = this.correctedTraceMap[trace.mspPairId]! + const key = corrected.globalConnNetId + if (!islands[key]) islands[key] = [] + islands[key].push(corrected) + } + return islands + } + + override _step() { + const netId = this.netIdsToProcess.pop() + if (!netId) { + // Final simplification pass + for (const trace of Object.values(this.correctedTraceMap)) { + trace.tracePath = simplifyPath(trace.tracePath) + } + this.solved = true + return + } + + const traces = this.traceNetIslands[netId]! + this.mergeCloseSegmentsInNet(traces) + } + + private mergeCloseSegmentsInNet(traces: SolvedTracePath[]) { + // Collect all horizontal and vertical segments across traces in this net + const hSegments: Segment[] = [] + const vSegments: Segment[] = [] + + for (const trace of traces) { + const path = trace.tracePath + for (let i = 0; i < path.length - 1; i++) { + const p1 = path[i]! + const p2 = path[i + 1]! + const seg: Segment = { traceId: trace.mspPairId, segIndex: i, p1, p2 } + if (isHorizontal(p1, p2)) { + hSegments.push(seg) + } else if (isVertical(p1, p2)) { + vSegments.push(seg) + } + } + } + + // Merge close horizontal segments (same Y ± threshold, overlapping in X) + this.mergeParallelSegments(hSegments, "horizontal") + // Merge close vertical segments (same X ± threshold, overlapping in Y) + this.mergeParallelSegments(vSegments, "vertical") + } + + private mergeParallelSegments( + segments: Segment[], + direction: "horizontal" | "vertical", + ) { + const merged = new Set() // "traceId:segIndex" keys already merged + + for (let i = 0; i < segments.length; i++) { + const a = segments[i]! + const keyA = `${a.traceId}:${a.segIndex}` + if (merged.has(keyA)) continue + + for (let j = i + 1; j < segments.length; j++) { + const b = segments[j]! + const keyB = `${b.traceId}:${b.segIndex}` + if (merged.has(keyB)) continue + + // Don't merge segments from the same trace + if (a.traceId === b.traceId) continue + + if (this.shouldMerge(a, b, direction)) { + this.applyMerge(a, b, direction) + merged.add(keyB) + } + } + } + } + + private shouldMerge( + a: Segment, + b: Segment, + direction: "horizontal" | "vertical", + ): boolean { + if (direction === "horizontal") { + // Both are horizontal: check Y gap and X overlap + const yGap = Math.abs(a.p1.y - b.p1.y) + if (yGap > GAP_THRESHOLD || yGap < EPS) return false + return overlaps1D(a.p1.x, a.p2.x, b.p1.x, b.p2.x) + } + // Both are vertical: check X gap and Y overlap + const xGap = Math.abs(a.p1.x - b.p1.x) + if (xGap > GAP_THRESHOLD || xGap < EPS) return false + return overlaps1D(a.p1.y, a.p2.y, b.p1.y, b.p2.y) + } + + /** + * Snap segment b's perpendicular coordinate to match segment a's. + * This aligns the two segments onto the same line. + */ + private applyMerge( + a: Segment, + b: Segment, + direction: "horizontal" | "vertical", + ) { + const traceB = this.correctedTraceMap[b.traceId]! + const path = traceB.tracePath + + // Compute target coordinate (median of the two) + if (direction === "horizontal") { + const targetY = (a.p1.y + b.p1.y) / 2 + // Snap both endpoints of segment b to the target Y + path[b.segIndex]!.y = targetY + path[b.segIndex + 1]!.y = targetY + // Also snap segment a + const traceA = this.correctedTraceMap[a.traceId]! + traceA.tracePath[a.segIndex]!.y = targetY + traceA.tracePath[a.segIndex + 1]!.y = targetY + } else { + const targetX = (a.p1.x + b.p1.x) / 2 + path[b.segIndex]!.x = targetX + path[b.segIndex + 1]!.x = targetX + const traceA = this.correctedTraceMap[a.traceId]! + traceA.tracePath[a.segIndex]!.x = targetX + traceA.tracePath[a.segIndex + 1]!.x = targetX + } + } +} + +function isHorizontal(a: Point, b: Point): boolean { + return Math.abs(a.y - b.y) < EPS +} + +function isVertical(a: Point, b: Point): boolean { + return Math.abs(a.x - b.x) < EPS +} + +/** + * Check if two 1D ranges [a1,a2] and [b1,b2] overlap (order-independent). + */ +function overlaps1D(a1: number, a2: number, b1: number, b2: number): boolean { + const aMin = Math.min(a1, a2) + const aMax = Math.max(a1, a2) + const bMin = Math.min(b1, b2) + const bMax = Math.max(b1, b2) + return aMin < bMax - EPS && bMin < aMax - EPS +} diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index c9d5a995..64183ce2 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -20,6 +20,7 @@ import { expandChipsToFitPins } from "./expandChipsToFitPins" import { LongDistancePairSolver } from "../LongDistancePairSolver/LongDistancePairSolver" import { MergedNetLabelObstacleSolver } from "../TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver" import { TraceCleanupSolver } from "../TraceCleanupSolver/TraceCleanupSolver" +import { SameNetTraceMergeSolver } from "../SameNetTraceMergeSolver/SameNetTraceMergeSolver" type PipelineStep BaseSolver> = { solverName: string @@ -68,6 +69,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { netLabelPlacementSolver?: NetLabelPlacementSolver labelMergingSolver?: MergedNetLabelObstacleSolver traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver + sameNetTraceMergeSolver?: SameNetTraceMergeSolver traceCleanupSolver?: TraceCleanupSolver startTimeOfPhase: Record @@ -143,18 +145,37 @@ export class SchematicTracePipelineSolver extends BaseSolver { onSolved: (_solver) => {}, }, ), + definePipelineStep( + "sameNetTraceMergeSolver", + SameNetTraceMergeSolver, + (instance) => [ + { + inputProblem: instance.inputProblem, + inputTracePaths: Object.values( + instance.traceOverlapShiftSolver?.correctedTraceMap ?? + Object.fromEntries( + instance + .longDistancePairSolver!.getOutput() + .allTracesMerged.map((p) => [p.mspPairId, p]), + ), + ), + globalConnMap: instance.mspConnectionPairSolver!.globalConnMap, + }, + ], + ), definePipelineStep( "netLabelPlacementSolver", NetLabelPlacementSolver, - () => [ + (instance) => [ { - inputProblem: this.inputProblem, + inputProblem: instance.inputProblem, inputTraceMap: - this.traceOverlapShiftSolver?.correctedTraceMap ?? + instance.sameNetTraceMergeSolver?.correctedTraceMap ?? + instance.traceOverlapShiftSolver?.correctedTraceMap ?? Object.fromEntries( - this.longDistancePairSolver!.getOutput().allTracesMerged.map( - (p) => [p.mspPairId, p], - ), + instance + .longDistancePairSolver!.getOutput() + .allTracesMerged.map((p) => [p.mspPairId, p]), ), }, ], @@ -169,6 +190,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { TraceLabelOverlapAvoidanceSolver, (instance) => { const traceMap = + instance.sameNetTraceMergeSolver?.correctedTraceMap ?? instance.traceOverlapShiftSolver?.correctedTraceMap ?? Object.fromEntries( instance From edd07b4f9dbb0f2d35b841779049ce9a9e369e53 Mon Sep 17 00:00:00 2001 From: drmabus Date: Sat, 4 Apr 2026 17:30:27 +0500 Subject: [PATCH 2/2] fix: conservative same-net trace merge to prevent over-merging (example29) --- .../SameNetTraceMergeSolver.ts | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts b/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts index 4e109995..255a210e 100644 --- a/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts +++ b/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts @@ -3,10 +3,10 @@ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/Sche import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver" import type { InputProblem } from "lib/types/InputProblem" import type { ConnectivityMap } from "connectivity-map" -import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath" import type { Point } from "@tscircuit/math-utils" -const GAP_THRESHOLD = 0.15 +const GAP_THRESHOLD = 0.05 +const MIN_OVERLAP_RATIO = 0.5 const EPS = 1e-9 type ConnNetId = string @@ -81,10 +81,6 @@ export class SameNetTraceMergeSolver extends BaseSolver { override _step() { const netId = this.netIdsToProcess.pop() if (!netId) { - // Final simplification pass - for (const trace of Object.values(this.correctedTraceMap)) { - trace.tracePath = simplifyPath(trace.tracePath) - } this.solved = true return } @@ -151,15 +147,13 @@ export class SameNetTraceMergeSolver extends BaseSolver { direction: "horizontal" | "vertical", ): boolean { if (direction === "horizontal") { - // Both are horizontal: check Y gap and X overlap const yGap = Math.abs(a.p1.y - b.p1.y) if (yGap > GAP_THRESHOLD || yGap < EPS) return false - return overlaps1D(a.p1.x, a.p2.x, b.p1.x, b.p2.x) + return hasSignificantOverlap(a.p1.x, a.p2.x, b.p1.x, b.p2.x) } - // Both are vertical: check X gap and Y overlap const xGap = Math.abs(a.p1.x - b.p1.x) if (xGap > GAP_THRESHOLD || xGap < EPS) return false - return overlaps1D(a.p1.y, a.p2.y, b.p1.y, b.p2.y) + return hasSignificantOverlap(a.p1.y, a.p2.y, b.p1.y, b.p2.y) } /** @@ -204,12 +198,29 @@ function isVertical(a: Point, b: Point): boolean { } /** - * Check if two 1D ranges [a1,a2] and [b1,b2] overlap (order-independent). + * Check if two 1D ranges overlap AND the overlapping portion is at least + * MIN_OVERLAP_RATIO of the shorter segment. This prevents merging segments + * that barely touch. */ -function overlaps1D(a1: number, a2: number, b1: number, b2: number): boolean { +function hasSignificantOverlap( + a1: number, + a2: number, + b1: number, + b2: number, +): boolean { const aMin = Math.min(a1, a2) const aMax = Math.max(a1, a2) const bMin = Math.min(b1, b2) const bMax = Math.max(b1, b2) - return aMin < bMax - EPS && bMin < aMax - EPS + + const overlapStart = Math.max(aMin, bMin) + const overlapEnd = Math.min(aMax, bMax) + const overlapLen = overlapEnd - overlapStart + + if (overlapLen < EPS) return false + + const shorter = Math.min(aMax - aMin, bMax - bMin) + if (shorter < EPS) return false + + return overlapLen / shorter >= MIN_OVERLAP_RATIO }