From eba1271996958dca67bbc7a3972403966d0f6930 Mon Sep 17 00:00:00 2001 From: nexus124324 Date: Wed, 3 Jun 2026 18:21:31 -0700 Subject: [PATCH] Lay out decoupling capacitor partitions as uniform banks --- bunfig.toml | 4 +- .../SingleInnerPartitionPackingSolver.ts | 9 + .../layoutDecouplingCapPartition.ts | 165 ++++++++++++++++++ .../LayoutPipelineSolver06.test.ts | 51 ++++++ .../LayoutPipelineSolver06.snap.svg | 44 +++++ .../decouplingCapBank01.snap.svg | 44 +++++ .../layoutDecouplingCapPartition01.test.ts | 155 ++++++++++++++++ tests/fixtures/preload.ts | 2 + 8 files changed, 472 insertions(+), 2 deletions(-) create mode 100644 lib/solvers/PackInnerPartitionsSolver/layoutDecouplingCapPartition.ts create mode 100644 tests/LayoutPipelineSolver/LayoutPipelineSolver06.test.ts create mode 100644 tests/LayoutPipelineSolver/__snapshots__/LayoutPipelineSolver06.snap.svg create mode 100644 tests/PackInnerPartitionsSolver/__snapshots__/decouplingCapBank01.snap.svg create mode 100644 tests/PackInnerPartitionsSolver/layoutDecouplingCapPartition01.test.ts create mode 100644 tests/fixtures/preload.ts diff --git a/bunfig.toml b/bunfig.toml index 3ce444d..f60ba6a 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,5 +1,5 @@ -# [test] -# preload = ["./tests/fixtures/preload.ts"] +[test] +preload = ["./tests/fixtures/preload.ts"] [install.lockfile] save = false diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 88db103..11b27f0 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -19,6 +19,7 @@ import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputPro import { createFilteredNetworkMapping } from "../../utils/networkFiltering" import { getPadsBoundingBox } from "./getPadsBoundingBox" import { doBasicInputProblemLayout } from "../LayoutPipelineSolver/doBasicInputProblemLayout" +import { layoutDecouplingCapPartition } from "./layoutDecouplingCapPartition" const PIN_SIZE = 0.1 @@ -38,6 +39,14 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } override _step() { + // Decoupling capacitor partitions use a specialized bank layout instead + // of the generic packing algorithm + if (this.partitionInputProblem.partitionType === "decoupling_caps") { + this.layout = layoutDecouplingCapPartition(this.partitionInputProblem) + this.solved = true + return + } + // Initialize PackSolver2 if not already created if (!this.activeSubSolver) { const packInput = this.createPackInput() diff --git a/lib/solvers/PackInnerPartitionsSolver/layoutDecouplingCapPartition.ts b/lib/solvers/PackInnerPartitionsSolver/layoutDecouplingCapPartition.ts new file mode 100644 index 0000000..d65b1e1 --- /dev/null +++ b/lib/solvers/PackInnerPartitionsSolver/layoutDecouplingCapPartition.ts @@ -0,0 +1,165 @@ +/** + * Specialized layout for decoupling capacitor partitions. + * + * Lays the capacitors out as a tight, uniform bank (the way decoupling + * capacitors are drawn in official datasheets): every capacitor is oriented + * the same way (pin connected to the positive voltage source facing y+, pin + * connected to ground facing y-), ordered deterministically by chip id, and + * spaced with a consistent gap so the bank packs flush next to the main chip. + */ + +import type { OutputLayout, Placement } from "../../types/OutputLayout" +import type { + Chip, + ChipId, + NetId, + PartitionInputProblem, + PinId, +} from "../../types/InputProblem" + +/** Maximum number of capacitors per row before the bank wraps into a grid */ +const MAX_CAPS_PER_ROW = 8 + +/** Natural sort so C2 comes before C10 */ +const compareChipIds = (a: ChipId, b: ChipId): number => + a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }) + +/** Get the net ids directly connected to a pin */ +const getNetIdsForPin = ( + pinId: PinId, + partition: PartitionInputProblem, +): Set => { + const nets = new Set() + for (const [connKey, connected] of Object.entries(partition.netConnMap)) { + if (!connected) continue + const [p, n] = connKey.split("-") as [PinId, NetId] + if (p === pinId) nets.add(n) + } + return nets +} + +/** + * Determine the rotation that orients the capacitor with its positive-voltage + * pin facing y+ and its ground pin facing y-. Returns null when the + * orientation cannot be determined (e.g. nets are not marked as + * ground/positive voltage source). + */ +const getUniformCapRotation = ( + chip: Chip, + partition: PartitionInputProblem, +): 0 | 180 | null => { + if (chip.pins.length !== 2) return null + const [pin1Id, pin2Id] = chip.pins as [PinId, PinId] + const pin1 = partition.chipPinMap[pin1Id] + const pin2 = partition.chipPinMap[pin2Id] + if (!pin1 || !pin2) return null + + // Identify which pin is on top (y+) at rotation 0 + const topPinId = pin1.offset.y >= pin2.offset.y ? pin1Id : pin2Id + const bottomPinId = topPinId === pin1Id ? pin2Id : pin1Id + + const isPositive = (pinId: PinId): boolean => { + for (const netId of getNetIdsForPin(pinId, partition)) { + if (partition.netMap[netId]?.isPositiveVoltageSource) return true + } + return false + } + const isGround = (pinId: PinId): boolean => { + for (const netId of getNetIdsForPin(pinId, partition)) { + if (partition.netMap[netId]?.isGround) return true + } + return false + } + + if (isPositive(topPinId) && isGround(bottomPinId)) return 0 + if (isPositive(bottomPinId) && isGround(topPinId)) return 180 + return null +} + +/** Clamp a desired rotation to the chip's available rotations */ +const clampToAvailableRotations = ( + chip: Chip, + desiredRotation: 0 | 180, +): number => { + const available = chip.availableRotations + if (!available || available.length === 0) return desiredRotation + if (available.includes(desiredRotation)) return desiredRotation + return available[0]! +} + +/** Get the y offset of the topmost pin of a chip at the given rotation */ +const getTopPinYOffset = ( + chip: Chip, + partition: PartitionInputProblem, + rotationDegrees: number, +): number => { + let maxY = -Infinity + for (const pinId of chip.pins) { + const pin = partition.chipPinMap[pinId] + if (!pin) continue + const y = rotationDegrees === 180 ? -pin.offset.y : pin.offset.y + if (y > maxY) maxY = y + } + return Number.isFinite(maxY) ? maxY : chip.size.y / 2 +} + +/** + * Lay out a decoupling capacitor partition as a uniform bank. + * + * - Capacitors are ordered deterministically (natural sort by chip id) + * - Every capacitor gets the same orientation: positive voltage pin facing + * y+, ground pin facing y- (when determinable and allowed by the chip's + * availableRotations) + * - Capacitors are spaced with a consistent gap + * (decouplingCapsGap ?? chipGap) and aligned so the positive-voltage pins + * form a straight rail + * - Large groups wrap into balanced rows of at most MAX_CAPS_PER_ROW + */ +export const layoutDecouplingCapPartition = ( + partition: PartitionInputProblem, +): OutputLayout => { + const chipIds = Object.keys(partition.chipMap).sort(compareChipIds) + const gap = partition.decouplingCapsGap ?? partition.chipGap + + // Uniform cell size so the bank forms a regular grid + let cellWidth = 0 + let cellHeight = 0 + for (const chipId of chipIds) { + const chip = partition.chipMap[chipId]! + cellWidth = Math.max(cellWidth, chip.size.x) + cellHeight = Math.max(cellHeight, chip.size.y) + } + + // Balanced grid: at most MAX_CAPS_PER_ROW columns + const rowCount = Math.ceil(chipIds.length / MAX_CAPS_PER_ROW) + const colCount = Math.ceil(chipIds.length / rowCount) + + const chipPlacements: Record = {} + + for (let i = 0; i < chipIds.length; i++) { + const chipId = chipIds[i]! + const chip = partition.chipMap[chipId]! + + const desiredRotation = getUniformCapRotation(chip, partition) ?? 0 + const rotation = clampToAvailableRotations(chip, desiredRotation) + + const row = Math.floor(i / colCount) + const col = i % colCount + + const x = col * (cellWidth + gap) + // Align the top (positive voltage) pins of each row into a straight rail + const rowTopY = -row * (cellHeight + gap) + const y = rowTopY - getTopPinYOffset(chip, partition, rotation) + + chipPlacements[chipId] = { + x, + y, + ccwRotationDegrees: rotation, + } + } + + return { + chipPlacements, + groupPlacements: {}, + } +} diff --git a/tests/LayoutPipelineSolver/LayoutPipelineSolver06.test.ts b/tests/LayoutPipelineSolver/LayoutPipelineSolver06.test.ts new file mode 100644 index 0000000..bf0e7de --- /dev/null +++ b/tests/LayoutPipelineSolver/LayoutPipelineSolver06.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from "bun:test" +import { LayoutPipelineSolver } from "lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver" +import { problem } from "../../pages/LayoutPipelineSolver/LayoutPipelineSolver06.page" + +test("LayoutPipelineSolver06 lays out decoupling capacitors as uniform banks", async () => { + const solver = new LayoutPipelineSolver(problem) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + + const layout = solver.getOutputLayout() + + // Both decoupling cap groups identified on U3 + const decapGroups = + solver.identifyDecouplingCapsSolver!.outputDecouplingCapGroups + expect(decapGroups.length).toBe(2) + + const gap = problem.decouplingCapsGap! + + for (const group of decapGroups) { + const placements = group.decouplingCapChipIds.map( + (chipId) => layout.chipPlacements[chipId]!, + ) + + // Every cap in the group has the same orientation (VCC pin facing y+) + const rotations = new Set(placements.map((p) => p.ccwRotationDegrees)) + expect(rotations.size).toBe(1) + + // All caps in the bank sit on the same row (same y) + const ys = new Set(placements.map((p) => p.y.toFixed(5))) + expect(ys.size).toBe(1) + + // Caps are ordered deterministically by chip id (natural sort) and + // spaced with a uniform pitch of cap width + decouplingCapsGap + const sortedChipIds = [...group.decouplingCapChipIds].sort((a, b) => + a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }), + ) + const xs = sortedChipIds.map((chipId) => layout.chipPlacements[chipId]!.x) + const capWidth = problem.chipMap[sortedChipIds[0]!]!.size.x + for (let i = 1; i < xs.length; i++) { + expect(xs[i]! - xs[i - 1]!).toBeCloseTo(capWidth + gap, 5) + } + } + + // No overlapping components in the final layout + const overlaps = solver.checkForOverlaps(layout) + expect(overlaps.length).toBe(0) + + await expect(solver.visualize()).toMatchGraphicsSvg(import.meta.path) +}) diff --git a/tests/LayoutPipelineSolver/__snapshots__/LayoutPipelineSolver06.snap.svg b/tests/LayoutPipelineSolver/__snapshots__/LayoutPipelineSolver06.snap.svg new file mode 100644 index 0000000..7a27c55 --- /dev/null +++ b/tests/LayoutPipelineSolver/__snapshots__/LayoutPipelineSolver06.snap.svg @@ -0,0 +1,44 @@ +C12C14C8C13C15C19C18C7U3C10C11C9 \ No newline at end of file diff --git a/tests/PackInnerPartitionsSolver/__snapshots__/decouplingCapBank01.snap.svg b/tests/PackInnerPartitionsSolver/__snapshots__/decouplingCapBank01.snap.svg new file mode 100644 index 0000000..8c31903 --- /dev/null +++ b/tests/PackInnerPartitionsSolver/__snapshots__/decouplingCapBank01.snap.svg @@ -0,0 +1,44 @@ +C10C2C3 \ No newline at end of file diff --git a/tests/PackInnerPartitionsSolver/layoutDecouplingCapPartition01.test.ts b/tests/PackInnerPartitionsSolver/layoutDecouplingCapPartition01.test.ts new file mode 100644 index 0000000..faeea6d --- /dev/null +++ b/tests/PackInnerPartitionsSolver/layoutDecouplingCapPartition01.test.ts @@ -0,0 +1,155 @@ +import { expect, test } from "bun:test" +import { layoutDecouplingCapPartition } from "lib/solvers/PackInnerPartitionsSolver/layoutDecouplingCapPartition" +import { SingleInnerPartitionPackingSolver } from "lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver" +import type { PartitionInputProblem } from "lib/types/InputProblem" + +/** + * Decoupling capacitor partition where one capacitor (C3) is wired flipped: + * its pin 1 (top at rotation 0) connects to GND instead of VCC. The layout + * must rotate it 180 degrees so every capacitor in the bank has its + * VCC-connected pin facing y+ and its GND-connected pin facing y-. + */ +const createDecapPartition = (): PartitionInputProblem => ({ + chipMap: { + C10: { + chipId: "C10", + pins: ["C10.1", "C10.2"], + size: { x: 0.53, y: 1.06 }, + availableRotations: [0, 180], + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 0.53, y: 1.06 }, + availableRotations: [0, 180], + }, + C3: { + chipId: "C3", + pins: ["C3.1", "C3.2"], + size: { x: 0.53, y: 1.06 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: { + "C10.1": { pinId: "C10.1", offset: { x: 0, y: 0.55 }, side: "y+" }, + "C10.2": { pinId: "C10.2", offset: { x: 0, y: -0.55 }, side: "y-" }, + "C2.1": { pinId: "C2.1", offset: { x: 0, y: 0.55 }, side: "y+" }, + "C2.2": { pinId: "C2.2", offset: { x: 0, y: -0.55 }, side: "y-" }, + "C3.1": { pinId: "C3.1", offset: { x: 0, y: 0.55 }, side: "y+" }, + "C3.2": { pinId: "C3.2", offset: { x: 0, y: -0.55 }, side: "y-" }, + }, + netMap: { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + }, + pinStrongConnMap: {}, + netConnMap: { + "C10.1-VCC": true, + "C10.2-GND": true, + "C2.1-VCC": true, + "C2.2-GND": true, + // C3 is wired flipped: pin 1 (top) goes to GND + "C3.1-GND": true, + "C3.2-VCC": true, + }, + chipGap: 0.6, + decouplingCapsGap: 0.2, + partitionGap: 1.2, + isPartition: true, + partitionType: "decoupling_caps", +}) + +test("layoutDecouplingCapPartition lays caps out as a uniform bank", () => { + const partition = createDecapPartition() + const layout = layoutDecouplingCapPartition(partition) + + const c2 = layout.chipPlacements["C2"]! + const c3 = layout.chipPlacements["C3"]! + const c10 = layout.chipPlacements["C10"]! + + // Deterministic natural ordering: C2, C3, C10 from left to right + expect(c2.x).toBeLessThan(c3.x) + expect(c3.x).toBeLessThan(c10.x) + + // Uniform pitch: cap width (0.53) + decouplingCapsGap (0.2) + expect(c3.x - c2.x).toBeCloseTo(0.73, 5) + expect(c10.x - c3.x).toBeCloseTo(0.73, 5) + + // All caps aligned on the same row + expect(c2.y).toBeCloseTo(c3.y, 5) + expect(c3.y).toBeCloseTo(c10.y, 5) + + // Uniform orientation: VCC pin faces y+, GND pin faces y-. + // C2 and C10 are wired normally so they stay at 0; C3 is wired flipped + // so it must be rotated 180 + expect(c2.ccwRotationDegrees).toBe(0) + expect(c10.ccwRotationDegrees).toBe(0) + expect(c3.ccwRotationDegrees).toBe(180) + + // The VCC pins form a straight rail at the same y coordinate + const vccPinY = (placement: { y: number }, pinOffsetY: number) => + placement.y + pinOffsetY + const c2VccY = vccPinY(c2, 0.55) // C2.1 at rotation 0 + const c3VccY = vccPinY(c3, 0.55) // C3.2 at rotation 180: -(-0.55) + const c10VccY = vccPinY(c10, 0.55) // C10.1 at rotation 0 + expect(c2VccY).toBeCloseTo(c3VccY, 5) + expect(c3VccY).toBeCloseTo(c10VccY, 5) +}) + +test("layoutDecouplingCapPartition wraps large groups into balanced rows", () => { + const partition = createDecapPartition() + + // Expand to 10 identical caps (C1..C10) to exceed the 8-per-row maximum + for (let i = 1; i <= 10; i++) { + const chipId = `C${i}` + if (partition.chipMap[chipId]) continue + partition.chipMap[chipId] = { + chipId, + pins: [`${chipId}.1`, `${chipId}.2`], + size: { x: 0.53, y: 1.06 }, + availableRotations: [0, 180], + } + partition.chipPinMap[`${chipId}.1`] = { + pinId: `${chipId}.1`, + offset: { x: 0, y: 0.55 }, + side: "y+", + } + partition.chipPinMap[`${chipId}.2`] = { + pinId: `${chipId}.2`, + offset: { x: 0, y: -0.55 }, + side: "y-", + } + partition.netConnMap[`${chipId}.1-VCC`] = true + partition.netConnMap[`${chipId}.2-GND`] = true + } + + const layout = layoutDecouplingCapPartition(partition) + + const ys = new Set( + Object.values(layout.chipPlacements).map((p) => p.y.toFixed(5)), + ) + // 10 caps wrap into 2 balanced rows of 5 + expect(ys.size).toBe(2) + const rowYs = [...ys].map(Number).sort((a, b) => b - a) + const capsInTopRow = Object.values(layout.chipPlacements).filter( + (p) => p.y.toFixed(5) === rowYs[0]!.toFixed(5), + ) + expect(capsInTopRow.length).toBe(5) +}) + +test("SingleInnerPartitionPackingSolver uses the bank layout for decoupling_caps partitions", async () => { + const partition = createDecapPartition() + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: partition, + pinIdToStronglyConnectedPins: {}, + }) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.layout).not.toBeNull() + expect(solver.layout).toEqual(layoutDecouplingCapPartition(partition)) + + await expect(solver.visualize()).toMatchGraphicsSvg(import.meta.path, { + svgName: "decouplingCapBank01", + }) +}) diff --git a/tests/fixtures/preload.ts b/tests/fixtures/preload.ts new file mode 100644 index 0000000..c4d88a0 --- /dev/null +++ b/tests/fixtures/preload.ts @@ -0,0 +1,2 @@ +import "bun-match-svg" +import "graphics-debug/matcher"