Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import type { Point } from "@tscircuit/math-utils"
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"

export interface SameNetTraceMergeSolverParams {
traces: SolvedTracePath[]
gapThreshold?: number
}

type Axis = "x" | "y"

interface StraightTrace {
trace: SolvedTracePath
axis: Axis
fixed: number
min: number
max: number
start: Point
end: Point
}

const DEFAULT_GAP_THRESHOLD = 0.15
const EPSILON = 1e-9

const nearlyEqual = (a: number, b: number) => Math.abs(a - b) <= EPSILON

const idsFor = (trace: SolvedTracePath) =>
trace.mspConnectionPairIds.length > 0
? trace.mspConnectionPairIds
: [trace.mspPairId]

const asStraightTrace = (trace: SolvedTracePath): StraightTrace | null => {
if (trace.tracePath.length < 2) return null

const first = trace.tracePath[0]!
const last = trace.tracePath[trace.tracePath.length - 1]!
const isHorizontal = trace.tracePath.every((p) => nearlyEqual(p.y, first.y))
const isVertical = trace.tracePath.every((p) => nearlyEqual(p.x, first.x))

if (!isHorizontal && !isVertical) return null

if (isHorizontal) {
const xs = trace.tracePath.map((p) => p.x)
return {
trace,
axis: "x",
fixed: first.y,
min: Math.min(...xs),
max: Math.max(...xs),
start: first,
end: last,
}
}

const ys = trace.tracePath.map((p) => p.y)
return {
trace,
axis: "y",
fixed: first.x,
min: Math.min(...ys),
max: Math.max(...ys),
start: first,
end: last,
}
}

const getGap = (a: StraightTrace, b: StraightTrace) => {
if (a.max < b.min) return b.min - a.max
if (b.max < a.min) return a.min - b.max
return 0
}

const canMerge = (a: StraightTrace, b: StraightTrace, gapThreshold: number) => {
if (a.trace.globalConnNetId !== b.trace.globalConnNetId) return false
if (a.axis !== b.axis) return false
if (!nearlyEqual(a.fixed, b.fixed)) return false
return getGap(a, b) <= gapThreshold
}

const mergePair = (a: StraightTrace, b: StraightTrace): SolvedTracePath => {
const min = Math.min(a.min, b.min)
const max = Math.max(a.max, b.max)
const tracePath =
a.axis === "x"
? [
{ x: min, y: a.fixed },
{ x: max, y: a.fixed },
]
: [
{ x: a.fixed, y: min },
{ x: a.fixed, y: max },
]

const mspConnectionPairIds = [...idsFor(a.trace), ...idsFor(b.trace)]
const pinIds = Array.from(new Set([...a.trace.pinIds, ...b.trace.pinIds]))

return {
...a.trace,
mspPairId: mspConnectionPairIds.join("+"),
mspConnectionPairIds,
pinIds,
pins: [a.trace.pins[0], b.trace.pins[1]],
tracePath,
}
}

/**
* Merges straight, collinear trace segments on the same global net when their
* endpoints are already touching or separated by only a tiny gap.
*/
export class SameNetTraceMergeSolver extends BaseSolver {
private outputTraces: SolvedTracePath[]
private gapThreshold: number

constructor(params: SameNetTraceMergeSolverParams) {
super()
this.outputTraces = [...params.traces]
this.gapThreshold = params.gapThreshold ?? DEFAULT_GAP_THRESHOLD
}

override getConstructorParams(): ConstructorParameters<
typeof SameNetTraceMergeSolver
>[0] {
return {
traces: this.outputTraces,
gapThreshold: this.gapThreshold,
}
}

override _step() {
let merged = true

while (merged) {
merged = false

for (let i = 0; i < this.outputTraces.length; i++) {
const a = asStraightTrace(this.outputTraces[i]!)
if (!a) continue

for (let j = i + 1; j < this.outputTraces.length; j++) {
const b = asStraightTrace(this.outputTraces[j]!)
if (!b) continue
if (!canMerge(a, b, this.gapThreshold)) continue

const mergedTrace = mergePair(a, b)
this.outputTraces.splice(j, 1)
this.outputTraces.splice(i, 1, mergedTrace)
merged = true
break
}

if (merged) break
}
}

this.solved = true
}

getOutput() {
return {
traces: this.outputTraces,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/
import { VccNetLabelCornerPlacementSolver } from "../VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver"
import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver"
import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver"
import { SameNetTraceMergeSolver } from "../SameNetTraceMergeSolver/SameNetTraceMergeSolver"

type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
solverName: string
Expand Down Expand Up @@ -75,6 +76,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
labelMergingSolver?: MergedNetLabelObstacleSolver
traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver
traceCleanupSolver?: TraceCleanupSolver
sameNetTraceMergeSolver?: SameNetTraceMergeSolver
example28Solver?: Example28Solver
availableNetOrientationSolver?: AvailableNetOrientationSolver
vccNetLabelCornerPlacementSolver?: VccNetLabelCornerPlacementSolver
Expand Down Expand Up @@ -217,11 +219,21 @@ export class SchematicTracePipelineSolver extends BaseSolver {
},
]
}),
definePipelineStep(
"sameNetTraceMergeSolver",
SameNetTraceMergeSolver,
(instance) => [
{
traces: instance.traceCleanupSolver!.getOutput().traces,
},
],
),
definePipelineStep(
"netLabelPlacementSolver",
NetLabelPlacementSolver,
(instance) => {
const traces =
instance.sameNetTraceMergeSolver?.getOutput().traces ??
instance.traceCleanupSolver?.getOutput().traces ??
instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces

Expand All @@ -237,6 +249,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
),
definePipelineStep("example28Solver", Example28Solver, (instance) => {
const traces =
instance.sameNetTraceMergeSolver?.getOutput().traces ??
instance.traceCleanupSolver?.getOutput().traces ??
instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces

Expand Down
103 changes: 103 additions & 0 deletions tests/same-net-trace-merge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { expect, test } from "bun:test"
import { SameNetTraceMergeSolver } from "lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"

const baseTrace = (overrides: Partial<SolvedTracePath>): SolvedTracePath => ({
mspPairId: "trace",
dcConnNetId: "dc",
globalConnNetId: "net-a",
userNetId: "net-a",
pins: [
{ pinId: "p1", chipId: "u1", x: 0, y: 0 },
{ pinId: "p2", chipId: "u2", x: 1, y: 1 },
],
tracePath: [],
mspConnectionPairIds: [],
pinIds: [],
...overrides,
})

test("merges collinear same-net horizontal segments separated by a small gap", () => {
const solver = new SameNetTraceMergeSolver({
traces: [
baseTrace({
mspPairId: "a",
tracePath: [
{ x: 0, y: 1 },
{ x: 2, y: 1 },
],
}),
baseTrace({
mspPairId: "b",
tracePath: [
{ x: 2.08, y: 1 },
{ x: 4, y: 1 },
],
}),
],
gapThreshold: 0.15,
})

solver.solve()

expect(solver.getOutput().traces).toHaveLength(1)
expect(solver.getOutput().traces[0].tracePath).toEqual([
{ x: 0, y: 1 },
{ x: 4, y: 1 },
])
expect(solver.getOutput().traces[0].mspConnectionPairIds).toEqual(["a", "b"])
})

test("does not merge close collinear segments on different nets", () => {
const solver = new SameNetTraceMergeSolver({
traces: [
baseTrace({
mspPairId: "a",
globalConnNetId: "net-a",
tracePath: [
{ x: 0, y: 1 },
{ x: 2, y: 1 },
],
}),
baseTrace({
mspPairId: "b",
globalConnNetId: "net-b",
tracePath: [
{ x: 2.08, y: 1 },
{ x: 4, y: 1 },
],
}),
],
gapThreshold: 0.15,
})

solver.solve()

expect(solver.getOutput().traces).toHaveLength(2)
})

test("does not merge segments when the gap is larger than the threshold", () => {
const solver = new SameNetTraceMergeSolver({
traces: [
baseTrace({
mspPairId: "a",
tracePath: [
{ x: 0, y: 1 },
{ x: 2, y: 1 },
],
}),
baseTrace({
mspPairId: "b",
tracePath: [
{ x: 2.5, y: 1 },
{ x: 4, y: 1 },
],
}),
],
gapThreshold: 0.15,
})

solver.solve()

expect(solver.getOutput().traces).toHaveLength(2)
})
Loading