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
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver {
}

override _step() {
// For decoupling cap partitions, use a clean linear row layout
// instead of the generic packing algorithm
if (this.partitionInputProblem.partitionType === "decoupling_caps") {
this.layout = this.createLinearDecouplingCapLayout()
this.solved = true
return
}

// Initialize PackSolver2 if not already created
if (!this.activeSubSolver) {
const packInput = this.createPackInput()
Expand All @@ -64,6 +72,51 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver {
}
}

/**
* Creates a clean linear horizontal row layout for decoupling capacitors.
* Caps are sorted by chipId for deterministic ordering, spaced evenly,
* and centered at the origin.
*/
private createLinearDecouplingCapLayout(): OutputLayout {
const chipEntries = Object.entries(this.partitionInputProblem.chipMap)

// Sort by chipId for deterministic, clean ordering
chipEntries.sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }))

const gap =
this.partitionInputProblem.decouplingCapsGap ??
this.partitionInputProblem.chipGap

const chipPlacements: Record<string, Placement> = {}

// Calculate total width needed for the row
let totalWidth = 0
for (let i = 0; i < chipEntries.length; i++) {
const [, chip] = chipEntries[i]!
totalWidth += chip.size.x
if (i < chipEntries.length - 1) {
totalWidth += gap
}
}

// Place each cap in a horizontal row, centered at origin
let currentX = -totalWidth / 2
for (const [chipId, chip] of chipEntries) {
const centerX = currentX + chip.size.x / 2
chipPlacements[chipId] = {
x: centerX,
y: 0,
ccwRotationDegrees: 0,
}
currentX += chip.size.x + gap
}

return {
chipPlacements,
groupPlacements: {},
}
}

private createPackInput(): PackInput {
// Fall back to filtered mapping (weak + strong)
const pinToNetworkMap = createFilteredNetworkMapping({
Expand Down
213 changes: 213 additions & 0 deletions tests/DecouplingCapsLinearLayout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { test, expect } from "bun:test"
import { SingleInnerPartitionPackingSolver } from "../lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver"
import type { PartitionInputProblem } from "../lib/types/InputProblem"

/**
* Test that decoupling capacitor partitions get a clean linear row layout
* instead of the generic packing algorithm.
*/
test("decoupling caps partition uses linear row layout", () => {
const partition: PartitionInputProblem = {
isPartition: true,
partitionType: "decoupling_caps",
chipMap: {
C1: {
chipId: "C1",
pins: ["C1.1", "C1.2"],
size: { x: 0.5, y: 1.0 },
isDecouplingCap: true,
availableRotations: [0, 180],
},
C2: {
chipId: "C2",
pins: ["C2.1", "C2.2"],
size: { x: 0.5, y: 1.0 },
isDecouplingCap: true,
availableRotations: [0, 180],
},
C3: {
chipId: "C3",
pins: ["C3.1", "C3.2"],
size: { x: 0.5, y: 1.0 },
isDecouplingCap: true,
availableRotations: [0, 180],
},
},
chipPinMap: {
"C1.1": { pinId: "C1.1", offset: { x: 0, y: -0.5 }, side: "y-" },
"C1.2": { pinId: "C1.2", offset: { x: 0, y: 0.5 }, side: "y+" },
"C2.1": { pinId: "C2.1", offset: { x: 0, y: -0.5 }, side: "y-" },
"C2.2": { pinId: "C2.2", offset: { x: 0, y: 0.5 }, side: "y+" },
"C3.1": { pinId: "C3.1", offset: { x: 0, y: -0.5 }, side: "y-" },
"C3.2": { pinId: "C3.2", offset: { x: 0, y: 0.5 }, side: "y+" },
},
netMap: {
GND: { netId: "GND", isGround: true },
VCC: { netId: "VCC", isPositiveVoltageSource: true },
},
pinStrongConnMap: {},
netConnMap: {
"C1.1-GND": true,
"C1.2-VCC": true,
"C2.1-GND": true,
"C2.2-VCC": true,
"C3.1-GND": true,
"C3.2-VCC": true,
},
chipGap: 0.2,
partitionGap: 2,
}

const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: partition,
pinIdToStronglyConnectedPins: {},
})

solver.solve()

expect(solver.solved).toBe(true)
expect(solver.failed).toBe(false)
expect(solver.layout).toBeDefined()

const layout = solver.layout!
const placements = layout.chipPlacements

// All 3 caps should have placements
expect(placements["C1"]).toBeDefined()
expect(placements["C2"]).toBeDefined()
expect(placements["C3"]).toBeDefined()

// All caps should be on the same Y line (horizontal row)
expect(placements["C1"]!.y).toBe(0)
expect(placements["C2"]!.y).toBe(0)
expect(placements["C3"]!.y).toBe(0)

// All caps should have 0 rotation
expect(placements["C1"]!.ccwRotationDegrees).toBe(0)
expect(placements["C2"]!.ccwRotationDegrees).toBe(0)
expect(placements["C3"]!.ccwRotationDegrees).toBe(0)

// Caps should be sorted by chipId and spaced correctly
// C1 < C2 < C3, so C1.x < C2.x < C3.x
expect(placements["C1"]!.x).toBeLessThan(placements["C2"]!.x)
expect(placements["C2"]!.x).toBeLessThan(placements["C3"]!.x)

// Layout should be centered at origin
const xs = [placements["C1"]!.x, placements["C2"]!.x, placements["C3"]!.x]
const minX = Math.min(...xs) - 0.25 // half of chip width
const maxX = Math.max(...xs) + 0.25
const center = (minX + maxX) / 2
expect(Math.abs(center)).toBeLessThan(0.01)

// No overlaps: gap between adjacent caps should be >= chipGap
const gap12 =
placements["C2"]!.x -
placements["C1"]!.x -
0.5 // subtract chip widths (0.25 + 0.25)
const gap23 =
placements["C3"]!.x -
placements["C2"]!.x -
0.5
expect(gap12).toBeCloseTo(0.2, 5) // chipGap = 0.2
expect(gap23).toBeCloseTo(0.2, 5)
})

test("decoupling caps layout respects decouplingCapsGap", () => {
const partition: PartitionInputProblem = {
isPartition: true,
partitionType: "decoupling_caps",
chipMap: {
C1: {
chipId: "C1",
pins: ["C1.1", "C1.2"],
size: { x: 0.5, y: 1.0 },
isDecouplingCap: true,
availableRotations: [0, 180],
},
C2: {
chipId: "C2",
pins: ["C2.1", "C2.2"],
size: { x: 0.5, y: 1.0 },
isDecouplingCap: true,
availableRotations: [0, 180],
},
},
chipPinMap: {
"C1.1": { pinId: "C1.1", offset: { x: 0, y: -0.5 }, side: "y-" },
"C1.2": { pinId: "C1.2", offset: { x: 0, y: 0.5 }, side: "y+" },
"C2.1": { pinId: "C2.1", offset: { x: 0, y: -0.5 }, side: "y-" },
"C2.2": { pinId: "C2.2", offset: { x: 0, y: 0.5 }, side: "y+" },
},
netMap: {
GND: { netId: "GND", isGround: true },
VCC: { netId: "VCC", isPositiveVoltageSource: true },
},
pinStrongConnMap: {},
netConnMap: {
"C1.1-GND": true,
"C1.2-VCC": true,
"C2.1-GND": true,
"C2.2-VCC": true,
},
chipGap: 0.2,
partitionGap: 2,
decouplingCapsGap: 0.5,
}

const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: partition,
pinIdToStronglyConnectedPins: {},
})

solver.solve()

expect(solver.solved).toBe(true)
const layout = solver.layout!

// Gap between caps should use decouplingCapsGap (0.5) not chipGap (0.2)
const gap =
layout.chipPlacements["C2"]!.x -
layout.chipPlacements["C1"]!.x -
0.5 // subtract chip widths
expect(gap).toBeCloseTo(0.5, 5)
})

test("non-decoupling partition uses PackSolver2 instead of linear layout", () => {
const partition: PartitionInputProblem = {
isPartition: true,
partitionType: "default",
chipMap: {
R1: {
chipId: "R1",
pins: ["R1.1", "R1.2"],
size: { x: 1, y: 0.5 },
},
},
chipPinMap: {
"R1.1": { pinId: "R1.1", offset: { x: -0.25, y: 0 }, side: "x-" },
"R1.2": { pinId: "R1.2", offset: { x: 0.25, y: 0 }, side: "x+" },
},
netMap: {
N1: { netId: "N1" },
},
pinStrongConnMap: {},
netConnMap: {
"R1.1-N1": true,
},
chipGap: 0.2,
partitionGap: 2,
}

const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: partition,
pinIdToStronglyConnectedPins: {},
})

// Solve fully — it should go through PackSolver2 path
solver.solve()

expect(solver.solved).toBe(true)
expect(solver.failed).toBe(false)
expect(solver.layout).toBeDefined()
expect(solver.layout!.chipPlacements["R1"]).toBeDefined()
})