diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 88db103..539b518 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -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() @@ -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 = {} + + // 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({ diff --git a/tests/DecouplingCapsLayout/DecouplingCapsLayout.test.ts b/tests/DecouplingCapsLayout/DecouplingCapsLayout.test.ts new file mode 100644 index 0000000..4a63ae3 --- /dev/null +++ b/tests/DecouplingCapsLayout/DecouplingCapsLayout.test.ts @@ -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 = {} + const chipPinMap: Record = {} + const netConnMap: Record = {} + const pinStrongConnMap: Record = {} + + 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) + } +})