diff --git a/lib/solvers/PackInnerPartitionsSolver/DecouplingCapBankLayoutSolver.ts b/lib/solvers/PackInnerPartitionsSolver/DecouplingCapBankLayoutSolver.ts new file mode 100644 index 0000000..d802ca0 --- /dev/null +++ b/lib/solvers/PackInnerPartitionsSolver/DecouplingCapBankLayoutSolver.ts @@ -0,0 +1,112 @@ +/** + * Lays out a decoupling-capacitor partition as a uniform bank: + * + * - All caps oriented with their VCC/positive pin facing y+ (top) and their + * GND/negative pin facing y- (bottom), matching datasheet convention. + * - Caps are placed side-by-side with uniform pitch (cap width + gap). + * - When there are more than `maxPerRow` caps the bank wraps into multiple + * balanced rows, keeping the VCC rail continuous. + * + * This is a synchronous, single-step solver because the placement is fully + * deterministic — no search needed. + */ + +import { BaseSolver } from "../BaseSolver" +import type { OutputLayout, Placement } from "../../types/OutputLayout" +import type { PartitionInputProblem, ChipId } from "../../types/InputProblem" + +const MAX_PER_ROW = 8 + +export class DecouplingCapBankLayoutSolver extends BaseSolver { + partitionInputProblem: PartitionInputProblem + layout: OutputLayout | null = null + + constructor(partitionInputProblem: PartitionInputProblem) { + super() + this.partitionInputProblem = partitionInputProblem + } + + override _step() { + const { chipMap, chipPinMap, netMap, netConnMap } = + this.partitionInputProblem + const gap = this.partitionInputProblem.decouplingCapsGap ?? 0.1 + + const chips = Object.values(chipMap) + if (chips.length === 0) { + this.layout = { chipPlacements: {}, groupPlacements: {} } + this.solved = true + return + } + + // Sort caps deterministically + const sortedChips = [...chips].sort((a, b) => + a.chipId.localeCompare(b.chipId, undefined, { numeric: true }), + ) + + // Figure out which pin of each cap is the VCC pin (isPositiveVoltageSource) + // and whether the cap needs to be flipped (rotated 180°) + const capRotations = new Map() + + for (const chip of sortedChips) { + const [pin1Id, pin2Id] = chip.pins + if (!pin1Id || !pin2Id) { + capRotations.set(chip.chipId, 0) + continue + } + + // Find which net each pin connects to + const getNet = (pinId: string) => { + for (const key of Object.keys(netConnMap)) { + const [p, n] = key.split("-") as [string, string] + if (p === pinId) return netMap[n] + } + return undefined + } + + const net1 = getNet(pin1Id) + const net2 = getNet(pin2Id) + + // pin1 is typically "top" in default (0°) rotation. + // If pin1 is GND and pin2 is VCC, we need to flip (180°). + const pin1IsGnd = net1?.isGround ?? false + const pin2IsGnd = net2?.isGround ?? false + + // Flip if pin1 is GND (meaning VCC is at bottom in default orientation) + capRotations.set(chip.chipId, pin1IsGnd && !pin2IsGnd ? 180 : 0) + } + + // Use the first chip's dimensions as the uniform cap size + const refChip = sortedChips[0]! + const capW = refChip.size.x + const capH = refChip.size.y + + const pitch = capW + gap + const perRow = Math.min(MAX_PER_ROW, sortedChips.length) + const rowCount = Math.ceil(sortedChips.length / perRow) + const rowPitch = capH + gap + + const chipPlacements: Record = {} + + for (let i = 0; i < sortedChips.length; i++) { + const chip = sortedChips[i]! + const col = i % perRow + const row = Math.floor(i / perRow) + const x = col * pitch + const y = -row * rowPitch + chipPlacements[chip.chipId] = { + x, + y, + ccwRotationDegrees: capRotations.get(chip.chipId) ?? 0, + } + } + + this.layout = { chipPlacements, groupPlacements: {} } + this.solved = true + } + + override getConstructorParams(): ConstructorParameters< + typeof DecouplingCapBankLayoutSolver + > { + return [this.partitionInputProblem] + } +} diff --git a/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts b/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts index dd88906..6f08492 100644 --- a/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts @@ -6,11 +6,22 @@ import type { GraphicsObject } from "graphics-debug" import { BaseSolver } from "../BaseSolver" -import type { ChipPin, InputProblem, PinId } from "../../types/InputProblem" +import type { + ChipPin, + InputProblem, + PartitionInputProblem, + PinId, +} from "../../types/InputProblem" import type { OutputLayout } from "../../types/OutputLayout" import { SingleInnerPartitionPackingSolver } from "./SingleInnerPartitionPackingSolver" +import { DecouplingCapBankLayoutSolver } from "./DecouplingCapBankLayoutSolver" import { stackGraphicsHorizontally } from "graphics-debug" +type PartitionSolver = (SingleInnerPartitionPackingSolver | DecouplingCapBankLayoutSolver) & { + layout: OutputLayout | null + visualize(): GraphicsObject +} + export type PackedPartition = { inputProblem: InputProblem layout: OutputLayout @@ -19,11 +30,11 @@ export type PackedPartition = { export class PackInnerPartitionsSolver extends BaseSolver { partitions: InputProblem[] packedPartitions: PackedPartition[] = [] - completedSolvers: SingleInnerPartitionPackingSolver[] = [] - activeSolver: SingleInnerPartitionPackingSolver | null = null + completedSolvers: PartitionSolver[] = [] + activeSolver: PartitionSolver | null = null currentPartitionIndex = 0 - declare activeSubSolver: SingleInnerPartitionPackingSolver | null + declare activeSubSolver: PartitionSolver | null pinIdToStronglyConnectedPins: Record constructor(params: { @@ -45,10 +56,17 @@ export class PackInnerPartitionsSolver extends BaseSolver { // If no active solver, create one for the current partition if (!this.activeSolver) { const currentPartition = this.partitions[this.currentPartitionIndex]! - this.activeSolver = new SingleInnerPartitionPackingSolver({ - partitionInputProblem: currentPartition, - pinIdToStronglyConnectedPins: this.pinIdToStronglyConnectedPins, - }) + const partitionType = (currentPartition as PartitionInputProblem).partitionType + if (partitionType === "decoupling_caps") { + this.activeSolver = new DecouplingCapBankLayoutSolver( + currentPartition as PartitionInputProblem, + ) + } else { + this.activeSolver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: currentPartition, + pinIdToStronglyConnectedPins: this.pinIdToStronglyConnectedPins, + }) + } this.activeSubSolver = this.activeSolver } diff --git a/tests/PackInnerPartitionsSolver/DecouplingCapBankLayoutSolver.test.ts b/tests/PackInnerPartitionsSolver/DecouplingCapBankLayoutSolver.test.ts new file mode 100644 index 0000000..eec3eea --- /dev/null +++ b/tests/PackInnerPartitionsSolver/DecouplingCapBankLayoutSolver.test.ts @@ -0,0 +1,105 @@ +import { expect, test } from "bun:test" +import { DecouplingCapBankLayoutSolver } from "lib/solvers/PackInnerPartitionsSolver/DecouplingCapBankLayoutSolver" +import type { PartitionInputProblem } from "lib/types/InputProblem" + +const makeProblem = ( + capCount: number, + pin1IsGnd = false, +): PartitionInputProblem => { + const chipMap: PartitionInputProblem["chipMap"] = {} + const chipPinMap: PartitionInputProblem["chipPinMap"] = {} + const netConnMap: PartitionInputProblem["netConnMap"] = {} + + for (let i = 1; i <= capCount; i++) { + const cid = `C${i}` + const p1 = `${cid}_P1` + const p2 = `${cid}_P2` + chipMap[cid] = { + chipId: cid, + pins: [p1, p2], + size: { x: 0.53, y: 1.1 }, + isDecouplingCap: true, + } + chipPinMap[p1] = { pinId: p1, offset: { x: 0, y: 0.55 }, side: "top" } + chipPinMap[p2] = { pinId: p2, offset: { x: 0, y: -0.55 }, side: "bottom" } + netConnMap[`${p1}-${pin1IsGnd ? "GND" : "VCC"}`] = true + netConnMap[`${p2}-${pin1IsGnd ? "VCC" : "GND"}`] = true + } + + return { + partitionType: "decoupling_caps", + chipMap, + chipPinMap, + netMap: { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + }, + pinStrongConnMap: {}, + netConnMap, + chipGap: 0.2, + partitionGap: 2, + decouplingCapsGap: 0.1, + } +} + +test("places 3 caps in a single row with correct pitch", () => { + const solver = new DecouplingCapBankLayoutSolver(makeProblem(3)) + solver.solve() + expect(solver.solved).toBe(true) + const placements = solver.layout!.chipPlacements + const pitch = 0.53 + 0.1 + expect(placements["C1"]!.x).toBeCloseTo(0) + expect(placements["C2"]!.x).toBeCloseTo(pitch) + expect(placements["C3"]!.x).toBeCloseTo(2 * pitch) + // all in same row + expect(placements["C1"]!.y).toBeCloseTo(0) + expect(placements["C2"]!.y).toBeCloseTo(0) + expect(placements["C3"]!.y).toBeCloseTo(0) +}) + +test("caps with VCC on pin1 have 0° rotation", () => { + const solver = new DecouplingCapBankLayoutSolver(makeProblem(2, false)) + solver.solve() + const p = solver.layout!.chipPlacements + expect(p["C1"]!.ccwRotationDegrees).toBe(0) + expect(p["C2"]!.ccwRotationDegrees).toBe(0) +}) + +test("caps with GND on pin1 are rotated 180°", () => { + const solver = new DecouplingCapBankLayoutSolver(makeProblem(2, true)) + solver.solve() + const p = solver.layout!.chipPlacements + expect(p["C1"]!.ccwRotationDegrees).toBe(180) + expect(p["C2"]!.ccwRotationDegrees).toBe(180) +}) + +test("wraps into two rows when more than 8 caps", () => { + const solver = new DecouplingCapBankLayoutSolver(makeProblem(10)) + solver.solve() + expect(solver.solved).toBe(true) + const p = solver.layout!.chipPlacements + // First 8 caps in row 0 (y=0), caps 9-10 in row 1 (y negative) + expect(p["C1"]!.y).toBeCloseTo(0) + expect(p["C8"]!.y).toBeCloseTo(0) + const rowPitch = 1.1 + 0.1 + expect(p["C9"]!.y).toBeCloseTo(-rowPitch) + expect(p["C10"]!.y).toBeCloseTo(-rowPitch) + // Row 1 starts at col 0 + expect(p["C9"]!.x).toBeCloseTo(0) +}) + +test("empty partition produces empty layout", () => { + const solver = new DecouplingCapBankLayoutSolver({ + partitionType: "decoupling_caps", + chipMap: {}, + chipPinMap: {}, + netMap: {}, + pinStrongConnMap: {}, + netConnMap: {}, + chipGap: 0.2, + partitionGap: 2, + }) + solver.solve() + expect(solver.solved).toBe(true) + expect(Object.keys(solver.layout!.chipPlacements).length).toBe(0) +})