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,16 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver {
}

override _step() {
// Use a specialized linear layout for decoupling capacitor partitions
if (
this.partitionInputProblem.partitionType === "decoupling_caps" &&
Object.keys(this.partitionInputProblem.chipMap).length > 0
) {
this.layout = this.layoutDecouplingCapsLinear()
this.solved = true
return
}

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

/**
* Arranges decoupling capacitors in a horizontal row, sorted by chipId
* for deterministic output. The row is centered at the origin.
*/
private layoutDecouplingCapsLinear(): OutputLayout {
const chipEntries = Object.entries(this.partitionInputProblem.chipMap).sort(
([a], [b]) => a.localeCompare(b),
)

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

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

// Compute total row width to center it
let totalWidth = 0
for (const [, chip] of chipEntries) {
totalWidth += chip.size.x
}
totalWidth += gap * Math.max(0, chipEntries.length - 1)

let currentX = -totalWidth / 2

for (const [chipId, chip] of chipEntries) {
chipPlacements[chipId] = {
x: currentX + chip.size.x / 2,
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
156 changes: 156 additions & 0 deletions tests/DecouplingCapsLayout/DecouplingCapsLayout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { test, expect } from "bun:test"
import { SingleInnerPartitionPackingSolver } from "../../lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver"
import type { PartitionInputProblem } from "../../lib/types/InputProblem"

function makeDecouplingCapsPartition(
chipCount: number,
opts?: { decouplingCapsGap?: number; chipGap?: number },
): PartitionInputProblem {
const chipMap: Record<string, any> = {}
const chipPinMap: Record<string, any> = {}
const netConnMap: Record<string, boolean> = {}
const pinStrongConnMap: Record<string, boolean> = {}

for (let i = 1; i <= chipCount; i++) {
const chipId = `C${i}`
const pinTop = `${chipId}.1`
const pinBot = `${chipId}.2`

chipMap[chipId] = {
chipId,
pins: [pinTop, pinBot],
size: { x: 0.5, y: 1.0 },
isDecouplingCap: true,
availableRotations: [0, 180],
}

chipPinMap[pinTop] = { pinId: pinTop, offset: { x: 0, y: 0.4 }, side: "y+" }
chipPinMap[pinBot] = {
pinId: pinBot,
offset: { x: 0, y: -0.4 },
side: "y-",
}

netConnMap[`${pinTop}-VCC`] = true
netConnMap[`${pinBot}-GND`] = true
}

return {
chipMap,
chipPinMap,
netMap: {
VCC: { netId: "VCC", isPositiveVoltageSource: true },
GND: { netId: "GND", isGround: true },
},
pinStrongConnMap,
netConnMap,
chipGap: opts?.chipGap ?? 0.2,
partitionGap: 1.0,
decouplingCapsGap: opts?.decouplingCapsGap,
isPartition: true,
partitionType: "decoupling_caps",
}
}

test("decoupling caps are placed in a horizontal row at y=0", () => {
const partition = makeDecouplingCapsPartition(5)
const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: partition,
pinIdToStronglyConnectedPins: {},
})
solver.solve()

expect(solver.solved).toBe(true)
expect(solver.layout).not.toBeNull()

const placements = solver.layout!.chipPlacements
expect(Object.keys(placements)).toHaveLength(5)

for (const [, placement] of Object.entries(placements)) {
expect(placement.y).toBe(0)
}
})

test("decoupling caps are sorted by chipId for deterministic output", () => {
const partition = makeDecouplingCapsPartition(4)
const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: partition,
pinIdToStronglyConnectedPins: {},
})
solver.solve()

const placements = solver.layout!.chipPlacements
const sortedIds = Object.keys(placements).sort((a, b) => a.localeCompare(b))
const xPositions = sortedIds.map((id) => placements[id]!.x)

for (let i = 1; i < xPositions.length; i++) {
expect(xPositions[i]!).toBeGreaterThan(xPositions[i - 1]!)
}
})

test("decoupling cap row is centered at x=0", () => {
const partition = makeDecouplingCapsPartition(3)
const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: partition,
pinIdToStronglyConnectedPins: {},
})
solver.solve()

const placements = solver.layout!.chipPlacements
const xPositions = Object.values(placements).map((p) => p.x)
const minX = Math.min(...xPositions)
const maxX = Math.max(...xPositions)
const centerX = (minX + maxX) / 2

expect(Math.abs(centerX)).toBeLessThan(0.01)
})

test("gap between adjacent caps matches decouplingCapsGap", () => {
const gap = 0.3
const partition = makeDecouplingCapsPartition(3, { decouplingCapsGap: gap })
const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: partition,
pinIdToStronglyConnectedPins: {},
})
solver.solve()

const placements = solver.layout!.chipPlacements
const sortedIds = Object.keys(placements).sort((a, b) => a.localeCompare(b))

for (let i = 1; i < sortedIds.length; i++) {
const prev = placements[sortedIds[i - 1]!]!
const curr = placements[sortedIds[i]!]!
const chipWidth = 0.5
const actualGap = curr.x - prev.x - chipWidth
expect(Math.abs(actualGap - gap)).toBeLessThan(0.01)
}
})

test("single decoupling cap is placed at origin", () => {
const partition = makeDecouplingCapsPartition(1)
const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: partition,
pinIdToStronglyConnectedPins: {},
})
solver.solve()

const placements = solver.layout!.chipPlacements
expect(Object.keys(placements)).toHaveLength(1)
const placement = Object.values(placements)[0]!
expect(placement.x).toBe(0)
expect(placement.y).toBe(0)
expect(placement.ccwRotationDegrees).toBe(0)
})

test("all decoupling caps have rotation 0", () => {
const partition = makeDecouplingCapsPartition(6)
const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: partition,
pinIdToStronglyConnectedPins: {},
})
solver.solve()

for (const placement of Object.values(solver.layout!.chipPlacements)) {
expect(placement.ccwRotationDegrees).toBe(0)
}
})
Loading