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
16 changes: 14 additions & 2 deletions lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { InputChip, InputPin, InputProblem } from "lib/types/InputProblem"
import { ConnectivityMap } from "connectivity-map"
import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
import { wouldCrossChip } from "./chipSideCrossing"
import type { GraphicsObject } from "graphics-debug"
import { getColorFromString } from "lib/utils/getColorFromString"
import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
Expand Down Expand Up @@ -98,6 +99,13 @@ export class MspConnectionPairSolver extends BaseSolver {
const [pin1, pin2] = directlyConnectedPins
const p1 = this.pinMap[pin1!]!
const p2 = this.pinMap[pin2!]!

// Check if this would cross a chip body
if (wouldCrossChip(p1, p2, this.chipMap)) {
// Skip creating an MSP pair for pins that would cross a chip
return
}

// Enforce max pair distance (use Manhattan to match orthogonal routing metric)
const manhattanDist = Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)
if (manhattanDist > this.maxMspPairDistance) {
Expand All @@ -120,11 +128,15 @@ export class MspConnectionPairSolver extends BaseSolver {
return
}

// There are more than 3 pins, so we need to run MSP to find the best pairs
// There are more than 2 pins, so we need to run MSP to find the best pairs
// Pass chipMap to prevent connections across chip bodies

const msp = getOrthogonalMinimumSpanningTree(
directlyConnectedPins.map((p) => this.pinMap[p]!).filter(Boolean),
{ maxDistance: this.maxMspPairDistance },
{
maxDistance: this.maxMspPairDistance,
chipMap: this.chipMap,
},
)

for (const [pin1, pin2] of msp) {
Expand Down
103 changes: 103 additions & 0 deletions lib/solvers/MspConnectionPairSolver/chipSideCrossing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { InputPin, InputChip } from "lib/types/InputProblem"
import { getInputChipBounds } from "../GuidelinesSolver/getInputChipBounds"

export type PinSide = "left" | "right" | "top" | "bottom" | "unknown"

/**
* Determines which side of a chip a pin is on based on its position
* relative to the chip's bounds. For pins outside the chip (typical in schematics),
* determines which side they are closest to.
*/
export function getPinSideOnChip(pin: InputPin, chip: InputChip): PinSide {
const bounds = getInputChipBounds(chip)
const tolerance = 1e-6

// Check if pin is on the boundary within tolerance
const onLeft = Math.abs(pin.x - bounds.minX) < tolerance
const onRight = Math.abs(pin.x - bounds.maxX) < tolerance
const onTop = Math.abs(pin.y - bounds.maxY) < tolerance
const onBottom = Math.abs(pin.y - bounds.minY) < tolerance

// Prefer vertical sides over horizontal for corner cases
if (onLeft) return "left"
if (onRight) return "right"
if (onTop) return "top"
if (onBottom) return "bottom"

// For pins outside the chip bounds, determine which side they're closest to
const centerX = chip.center.x
const centerY = chip.center.y

// Calculate distances to each side
const distToLeft = Math.abs(pin.x - bounds.minX)
const distToRight = Math.abs(pin.x - bounds.maxX)
const distToTop = Math.abs(pin.y - bounds.maxY)
const distToBottom = Math.abs(pin.y - bounds.minY)

// Check if pin is primarily to one side of the chip center
const isLeftOfCenter = pin.x < centerX
const isRightOfCenter = pin.x > centerX
const isAboveCenter = pin.y > centerY
const isBelowCenter = pin.y < centerY

// For pins outside chip bounds, prefer the side they're on relative to center
// and closest to in terms of boundary distance
if (isLeftOfCenter && distToLeft <= distToRight) {
return "left"
}
if (isRightOfCenter && distToRight <= distToLeft) {
return "right"
}
if (isAboveCenter && distToTop <= distToBottom) {
return "top"
}
if (isBelowCenter && distToBottom <= distToTop) {
return "bottom"
}

// Fallback: use minimum distance to any boundary
const minDist = Math.min(distToLeft, distToRight, distToTop, distToBottom)
if (minDist === distToLeft) return "left"
if (minDist === distToRight) return "right"
if (minDist === distToTop) return "top"
if (minDist === distToBottom) return "bottom"

return "unknown"
}

/**
* Checks if two pins on the same chip would require a trace
* to cross through the chip body (i.e., they are on opposite sides).
*/
export function wouldCrossChip(
pin1: InputPin & { chipId: string },
pin2: InputPin & { chipId: string },
chipMap: Record<string, InputChip>,
): boolean {
// Only check if both pins are on the same chip
if (pin1.chipId !== pin2.chipId) {
return false
}

const chip = chipMap[pin1.chipId]
if (!chip) {
return false
}

const side1 = getPinSideOnChip(pin1, chip)
const side2 = getPinSideOnChip(pin2, chip)

// If either pin side is unknown, be conservative and allow connection
if (side1 === "unknown" || side2 === "unknown") {
return false
}

// Check for opposite sides
const isOpposite =
(side1 === "left" && side2 === "right") ||
(side1 === "right" && side2 === "left") ||
(side1 === "top" && side2 === "bottom") ||
(side1 === "bottom" && side2 === "top")

return isOpposite
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { InputPin, PinId } from "lib/types/InputProblem"
import type { InputPin, PinId, InputChip } from "lib/types/InputProblem"
import { wouldCrossChip } from "./chipSideCrossing"

/**
* Compute the Orthogonal (Manhattan/L1) Minimum Spanning Tree (MST)
Expand All @@ -7,6 +8,9 @@ import type { InputPin, PinId } from "lib/types/InputProblem"
* The MST minimizes total |Δx| + |Δy| distance and fully connects all points.
* Uses Prim's algorithm with O(n^2) time and O(n) extra space.
*
* Includes logic to prevent connections that would cross through chip bodies
* (i.e., connecting pins on opposite sides of the same chip).
*
* Edge cases:
* - [] -> []
* - [one point] -> []
Expand All @@ -17,11 +21,12 @@ import type { InputPin, PinId } from "lib/types/InputProblem"
* whose pinId is lexicographically smallest (for stable output).
*/
export function getOrthogonalMinimumSpanningTree(
pins: InputPin[],
opts: { maxDistance?: number } = {},
pins: (InputPin & { chipId?: string })[],
opts: { maxDistance?: number; chipMap?: Record<string, InputChip> } = {},
): Array<[PinId, PinId]> {
const n = pins.length
const maxDistance = opts?.maxDistance ?? Number.POSITIVE_INFINITY
const chipMap = opts?.chipMap
if (n <= 1) return []

// Quick validation (optional; remove if hot path)
Expand All @@ -40,6 +45,19 @@ export function getOrthogonalMinimumSpanningTree(
const manhattan = (a: InputPin, b: InputPin) =>
Math.abs(a.x - b.x) + Math.abs(a.y - b.y)

// Helper: Check if connecting two pins would cross a chip
const wouldCrossChipBody = (i: number, j: number): boolean => {
if (!chipMap) return false
const pin1 = pins[i]
const pin2 = pins[j]
if (!pin1.chipId || !pin2.chipId) return false
return wouldCrossChip(
pin1 as InputPin & { chipId: string },
pin2 as InputPin & { chipId: string },
chipMap,
)
}

// Prim's data structures
const inTree = new Array<boolean>(n).fill(false)
const bestDist = new Array<number>(n).fill(Number.POSITIVE_INFINITY)
Expand Down Expand Up @@ -82,7 +100,14 @@ export function getOrthogonalMinimumSpanningTree(
for (let v = 0; v < n; v++) {
if (!inTree[v]) {
const d0 = manhattan(pins[u], pins[v])
const d = d0 > maxDistance ? Number.POSITIVE_INFINITY : d0

// Check if this connection would cross a chip body
const crossesChip = wouldCrossChipBody(u, v)

// If it crosses a chip or exceeds max distance, set distance to infinity
const d =
d0 > maxDistance || crossesChip ? Number.POSITIVE_INFINITY : d0

if (
d < bestDist[v] ||
(d === bestDist[v] && pins[u].pinId < pins[parent[v]]?.pinId)
Expand Down
178 changes: 178 additions & 0 deletions tests/solvers/MspConnectionPairSolver/preventChipSideCrossing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { test, expect } from "bun:test"
import { MspConnectionPairSolver } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"

test("MspConnectionPairSolver should prevent connections across chip sides", () => {
const input = {
inputProblem: {
chips: [
{
chipId: "schematic_component_0",
center: {
x: 0,
y: 0,
},
width: 2,
height: 1.4,
pins: [
{
pinId: "U3.8",
x: -1.4,
y: 0.42500000000000004,
},
{
pinId: "U3.4",
x: -1.4,
y: -0.42500000000000004,
},
{
pinId: "U3.1",
x: 1.4,
y: 0.5,
},
{
pinId: "U3.6",
x: 1.4,
y: 0.30000000000000004,
},
{
pinId: "U3.5",
x: 1.4,
y: 0.10000000000000009,
},
{
pinId: "U3.2",
x: 1.4,
y: -0.09999999999999998,
},
{
pinId: "U3.3",
x: 1.4,
y: -0.3,
},
{
pinId: "U3.7",
x: 1.4,
y: -0.5,
},
],
},
{
chipId: "schematic_component_1",
center: {
x: -2.3145833,
y: 0,
},
width: 0.5291665999999999,
height: 1.0583333000000001,
pins: [
{
pinId: "C20.1",
x: -2.3148566499999994,
y: 0.5512093000000002,
},
{
pinId: "C20.2",
x: -2.31430995,
y: -0.5512093000000002,
},
],
},
{
chipId: "schematic_component_2",
center: {
x: 1.7577928249999983,
y: 1.7512907000000002,
},
width: 0.3155856499999966,
height: 1.0583332999999997,
pins: [
{
pinId: "R11.1",
x: 1.7580660749999977,
y: 2.3025814000000002,
},
{
pinId: "R11.2",
x: 1.757519574999999,
y: 1.2,
},
],
},
],
directConnections: [
{
pinIds: ["C20.1", "U3.8"],
netId: "capacitor.C20 > port.pin1 to .U3 > .VDD",
},
{
pinIds: ["C20.2", "U3.4"],
netId: "capacitor.C20 > port.pin2 to .U3 > .GND",
},
{
pinIds: ["R11.2", "U3.1"],
netId: "resistor.R11 > port.pin2 to .U3 > .N_CS",
},
],
netConnections: [
{
netId: "V3_3",
pinIds: ["U3.8", "U3.3", "U3.7", "C20.1", "R11.1"],
},
{
netId: "GND",
pinIds: ["U3.4", "C20.2"],
},
{
netId: "FLASH_N_CS",
pinIds: ["U3.1", "R11.2"],
},
],
availableNetLabelOrientations: {
V3_3: ["y+"],
GND: ["y-"],
},
maxMspPairDistance: 5,
},
}

const solver = new MspConnectionPairSolver(input as any)
solver.solve()

// Check that no connections cross chip sides
for (const pair of solver.mspConnectionPairs) {
const [pin1, pin2] = pair.pins

// If both pins are on the same chip (schematic_component_0), check they don't cross sides
if (
pin1.chipId === pin2.chipId &&
pin1.chipId === "schematic_component_0"
) {
// U3.8 is on left side (x = -1.4), U3.3 and U3.7 are on right side (x = 1.4)
// These should not be directly connected
const leftSidePins = ["U3.8", "U3.4"]
const rightSidePins = ["U3.1", "U3.6", "U3.5", "U3.2", "U3.3", "U3.7"]

const pin1IsLeft = leftSidePins.includes(pin1.pinId)
const pin2IsLeft = leftSidePins.includes(pin2.pinId)
const pin1IsRight = rightSidePins.includes(pin1.pinId)
const pin2IsRight = rightSidePins.includes(pin2.pinId)

// Should not connect left side to right side
const connectsLeftToRight =
(pin1IsLeft && pin2IsRight) || (pin1IsRight && pin2IsLeft)

expect(connectsLeftToRight).toBe(false)
}
}

// Specifically check that U3.8 is not directly connected to U3.3 or U3.7
const problematicConnections = solver.mspConnectionPairs.filter((pair) => {
const pinIds = [pair.pins[0].pinId, pair.pins[1].pinId]
return (
(pinIds.includes("U3.8") && pinIds.includes("U3.3")) ||
(pinIds.includes("U3.8") && pinIds.includes("U3.7"))
)
})

expect(problematicConnections).toHaveLength(0)
})