diff --git a/lib/solvers/PackInnerPartitionsSolver/DecouplingCapsPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/DecouplingCapsPackingSolver.ts new file mode 100644 index 0000000..3ab046d --- /dev/null +++ b/lib/solvers/PackInnerPartitionsSolver/DecouplingCapsPackingSolver.ts @@ -0,0 +1,216 @@ +/** + * Specialized solver for packing decoupling capacitors in a linear layout. + * Decoupling caps are arranged horizontally, sorted by their connection + * to the main chip's pins for optimal routing proximity. + * + * Layout strategy: + * 1. Identify the main chip (largest non-cap chip) in the partition + * 2. Sort decoupling caps by their connected pin positions on the main chip + * 3. Arrange caps in a horizontal line with consistent spacing + * 4. Align caps so that pins connected to the same nets face the same direction + */ + +import type { GraphicsObject } from "graphics-debug" +import { BaseSolver } from "../BaseSolver" +import type { OutputLayout, Placement } from "../../types/OutputLayout" +import type { + PartitionInputProblem, + ChipId, + PinId, + NetId, + ChipPin, +} from "../../types/InputProblem" +import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputProblem" +import { doBasicInputProblemLayout } from "../LayoutPipelineSolver/doBasicInputProblemLayout" + +export class DecouplingCapsPackingSolver extends BaseSolver { + partitionInputProblem: PartitionInputProblem + layout: OutputLayout | null = null + + constructor(params: { partitionInputProblem: PartitionInputProblem }) { + super() + this.partitionInputProblem = params.partitionInputProblem + } + + override _step() { + this.layout = this.computeDecouplingCapsLayout() + this.solved = true + } + + /** + * Compute a linear layout for decoupling capacitors. + * Caps are sorted by the y-position of their connected pins on the main chip, + * then placed horizontally with consistent gap. + */ + private computeDecouplingCapsLayout(): OutputLayout { + const { chipMap, chipPinMap, netMap } = this.partitionInputProblem + + const chipIds = Object.keys(chipMap) + + // Identify the main chip (the one with most pins, typically the IC) + let mainChipId: ChipId | null = null + let maxPins = 0 + const capChipIds: ChipId[] = [] + + for (const chipId of chipIds) { + const chip = chipMap[chipId] + // Decoupling caps typically have exactly 2 pins + if (chip.pins.length <= 2) { + capChipIds.push(chipId) + } else { + if (chip.pins.length > maxPins) { + maxPins = chip.pins.length + mainChipId = chipId + } + } + } + + // If no main chip found, fall back to first non-cap chip or just pack linearly + if (!mainChipId && chipIds.length > 0) { + mainChipId = chipIds.find((id) => !capChipIds.includes(id)) || chipIds[0] + } + + const gap = this.partitionInputProblem.decouplingCapsGap + ?? this.partitionInputProblem.chipGap + + // Build a map from netId to pin positions on the main chip + const mainChipPinPositions: Record = {} + if (mainChipId) { + const mainChip = chipMap[mainChipId] + for (const pinId of mainChip.pins) { + const pin = chipPinMap[pinId] + if (pin) { + mainChipPinPositions[pinId] = { x: pin.offset.x, y: pin.offset.y } + } + } + } + + // For each cap, find the main chip pin it's connected to via a shared net + const capSortKeys: Array<{ chipId: ChipId; sortKey: number }> = [] + for (const capId of capChipIds) { + const cap = chipMap[capId] + let bestSortKey = Infinity + + for (const pinId of cap.pins) { + const pin = chipPinMap[pinId] + if (!pin) continue + + // Find nets this pin belongs to + for (const [netId, netPins] of Object.entries(netMap)) { + if (!netPins.includes(pinId)) continue + + // Check if main chip has a pin on this net + if (mainChipId) { + const mainChip = chipMap[mainChipId] + for (const mainPinId of mainChip.pins) { + if (netPins.includes(mainPinId) && mainChipPinPositions[mainPinId]) { + const pos = mainChipPinPositions[mainPinId] + // Use y-position of the connected main chip pin as sort key + bestSortKey = Math.min(bestSortKey, pos.y) + } + } + } + } + } + + capSortKeys.push({ chipId: capId, sortKey: bestSortKey }) + } + + // Sort caps by their connection proximity on main chip + capSortKeys.sort((a, b) => a.sortKey - b.sortKey) + const sortedCapIds = capSortKeys.map((entry) => entry.chipId) + + // Place main chip at center, caps in horizontal line below + const chipPlacements: Record = {} + + // Determine the orientation for each cap based on pin sides + // For decoupling caps with 2 pins, try to align them consistently + const getCapRotation = (capId: ChipId): number => { + const cap = chipMap[capId] + if (cap.availableRotations) { + // Prefer 0 rotation, then 180, then others + if (cap.availableRotations.includes(0)) return 0 + if (cap.availableRotations.includes(180)) return 180 + return cap.availableRotations[0] + } + return 0 + } + + if (mainChipId) { + const mainChip = chipMap[mainChipId] + chipPlacements[mainChipId] = { + x: 0, + y: 0, + ccwRotationDegrees: 0, + } + + // Place caps in a horizontal line below the main chip + const mainChipHeight = mainChip.size.y + let currentX = 0 + + for (const capId of sortedCapIds) { + const cap = chipMap[capId] + const rotation = getCapRotation(capId) + const capWidth = rotation % 180 === 0 ? cap.size.x : cap.size.y + const capHeight = rotation % 180 === 0 ? cap.size.y : cap.size.x + + chipPlacements[capId] = { + x: currentX + capWidth / 2, + y: -(mainChipHeight / 2 + gap + capHeight / 2), + ccwRotationDegrees: rotation, + } + + currentX += capWidth + gap + } + + // Center the cap row relative to main chip + if (sortedCapIds.length > 0) { + const lastCapId = sortedCapIds[sortedCapIds.length - 1] + const lastCap = chipMap[lastCapId] + const lastRotation = getCapRotation(lastCapId) + const lastCapWidth = lastRotation % 180 === 0 ? lastCap.size.x : lastCap.size.y + const totalWidth = chipPlacements[lastCapId].x + lastCapWidth / 2 + + // Shift all caps left by half total width to center + const shiftX = -totalWidth / 2 + for (const capId of sortedCapIds) { + chipPlacements[capId].x += shiftX + } + } + } else { + // No main chip, just place all chips in a line + let currentX = 0 + for (const chipId of sortedCapIds.length > 0 ? sortedCapIds : chipIds) { + const chip = chipMap[chipId] + const rotation = getCapRotation(chipId) + const chipWidth = rotation % 180 === 0 ? chip.size.x : chip.size.y + + chipPlacements[chipId] = { + x: currentX + chipWidth / 2, + y: 0, + ccwRotationDegrees: rotation, + } + + currentX += chipWidth + gap + } + } + + return { + chipPlacements, + groupPlacements: {}, + } + } + + override visualize(): GraphicsObject { + if (!this.layout) { + const basicLayout = doBasicInputProblemLayout(this.partitionInputProblem) + return visualizeInputProblem(this.partitionInputProblem, basicLayout) + } + + return visualizeInputProblem(this.partitionInputProblem, this.layout) + } + + override getConstructorParams(): [PartitionInputProblem] { + return [this.partitionInputProblem] + } +} \ No newline at end of file diff --git a/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts b/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts index dd88906..ddec681 100644 --- a/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts @@ -2,6 +2,9 @@ * Packs the internal layout of each partition using SingleInnerPartitionPackingSolver. * This stage takes the partitions from ChipPartitionsSolver and creates optimized * internal layouts for each partition before they are packed together. + * + * For decoupling capacitor partitions, uses DecouplingCapsPackingSolver for + * specialized linear layout optimized for routing proximity. */ import type { GraphicsObject } from "graphics-debug" @@ -9,6 +12,7 @@ import { BaseSolver } from "../BaseSolver" import type { ChipPin, InputProblem, PinId } from "../../types/InputProblem" import type { OutputLayout } from "../../types/OutputLayout" import { SingleInnerPartitionPackingSolver } from "./SingleInnerPartitionPackingSolver" +import { DecouplingCapsPackingSolver } from "./DecouplingCapsPackingSolver" import { stackGraphicsHorizontally } from "graphics-debug" export type PackedPartition = { @@ -19,11 +23,11 @@ export type PackedPartition = { export class PackInnerPartitionsSolver extends BaseSolver { partitions: InputProblem[] packedPartitions: PackedPartition[] = [] - completedSolvers: SingleInnerPartitionPackingSolver[] = [] - activeSolver: SingleInnerPartitionPackingSolver | null = null + completedSolvers: (SingleInnerPartitionPackingSolver | DecouplingCapsPackingSolver)[] = [] + activeSolver: SingleInnerPartitionPackingSolver | DecouplingCapsPackingSolver | null = null currentPartitionIndex = 0 - declare activeSubSolver: SingleInnerPartitionPackingSolver | null + declare activeSubSolver: SingleInnerPartitionPackingSolver | DecouplingCapsPackingSolver | null pinIdToStronglyConnectedPins: Record constructor(params: { @@ -45,10 +49,18 @@ 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, - }) + + // Use DecouplingCapsPackingSolver for decoupling capacitor partitions + if (currentPartition.partitionType === "decoupling_caps") { + this.activeSolver = new DecouplingCapsPackingSolver({ + partitionInputProblem: currentPartition, + }) + } else { + this.activeSolver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: currentPartition, + pinIdToStronglyConnectedPins: this.pinIdToStronglyConnectedPins, + }) + } this.activeSubSolver = this.activeSolver } diff --git a/tests/DecouplingCapsPackingSolver.test.ts b/tests/DecouplingCapsPackingSolver.test.ts new file mode 100644 index 0000000..610e2be --- /dev/null +++ b/tests/DecouplingCapsPackingSolver.test.ts @@ -0,0 +1,168 @@ +import { test, expect } from "bun:test" +import { DecouplingCapsPackingSolver } from "../lib/solvers/PackInnerPartitionsSolver/DecouplingCapsPackingSolver" +import type { PartitionInputProblem } from "../lib/types/InputProblem" + +test("DecouplingCapsPackingSolver creates linear layout for decoupling caps", () => { + const partitionInputProblem: PartitionInputProblem = { + chipMap: { + U1: { + chipId: "U1", + pins: ["U1.1", "U1.2", "U1.3"], + size: { x: 2, y: 2 }, + availableRotations: [0, 180], + }, + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 0.5, y: 0.25 }, + availableRotations: [0, 180], + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 0.5, y: 0.25 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: { + "U1.1": { pinId: "U1.1", offset: { x: -1, y: 0.5 }, side: "x-" }, + "U1.2": { pinId: "U1.2", offset: { x: -1, y: -0.5 }, side: "x-" }, + "U1.3": { pinId: "U1.3", offset: { x: 1, y: 0 }, side: "x+" }, + "C1.1": { pinId: "C1.1", offset: { x: -0.25, y: 0 }, side: "x-" }, + "C1.2": { pinId: "C1.2", offset: { x: 0.25, y: 0 }, side: "x+" }, + "C2.1": { pinId: "C2.1", offset: { x: -0.25, y: 0 }, side: "x-" }, + "C2.2": { pinId: "C2.2", offset: { x: 0.25, y: 0 }, side: "x+" }, + }, + netMap: { + VCC: ["U1.1", "C1.1"], + GND: ["U1.2", "C1.2", "C2.2"], + SIG: ["U1.3", "C2.1"], + }, + pinDirections: {}, + pinStronglyConnectedPins: {}, + pinStronglyConnectedGroups: {}, + chipGap: 0.2, + partitionGap: 0.5, + partitionType: "decoupling_caps", + } + + const solver = new DecouplingCapsPackingSolver({ partitionInputProblem }) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.layout).not.toBeNull() + expect(solver.layout!.chipPlacements).toBeDefined() + + // Main chip should be placed + expect(solver.layout!.chipPlacements["U1"]).toBeDefined() + + // Decoupling caps should be placed in a horizontal line + expect(solver.layout!.chipPlacements["C1"]).toBeDefined() + expect(solver.layout!.chipPlacements["C2"]).toBeDefined() + + // Caps should be below the main chip (negative y) + const c1Y = solver.layout!.chipPlacements["C1"]!.y + const c2Y = solver.layout!.chipPlacements["C2"]!.y + expect(c1Y).toBeLessThan(0) + expect(c2Y).toBeLessThan(0) +}) + +test("DecouplingCapsPackingSolver handles caps without main chip", () => { + const partitionInputProblem: PartitionInputProblem = { + chipMap: { + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 0.5, y: 0.25 }, + availableRotations: [0, 180], + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 0.5, y: 0.25 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: { + "C1.1": { pinId: "C1.1", offset: { x: -0.25, y: 0 }, side: "x-" }, + "C1.2": { pinId: "C1.2", offset: { x: 0.25, y: 0 }, side: "x+" }, + "C2.1": { pinId: "C2.1", offset: { x: -0.25, y: 0 }, side: "x-" }, + "C2.2": { pinId: "C2.2", offset: { x: 0.25, y: 0 }, side: "x+" }, + }, + netMap: { + VCC: ["C1.1", "C2.1"], + GND: ["C1.2", "C2.2"], + }, + pinDirections: {}, + pinStronglyConnectedPins: {}, + pinStronglyConnectedGroups: {}, + chipGap: 0.2, + partitionGap: 0.5, + partitionType: "decoupling_caps", + } + + const solver = new DecouplingCapsPackingSolver({ partitionInputProblem }) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.layout).not.toBeNull() + expect(solver.layout!.chipPlacements["C1"]).toBeDefined() + expect(solver.layout!.chipPlacements["C2"]).toBeDefined() +}) + +test("DecouplingCapsPackingSolver respects decouplingCapsGap", () => { + const partitionInputProblem: PartitionInputProblem = { + chipMap: { + U1: { + chipId: "U1", + pins: ["U1.1", "U1.2"], + size: { x: 2, y: 2 }, + availableRotations: [0], + }, + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 0.5, y: 0.25 }, + availableRotations: [0], + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 0.5, y: 0.25 }, + availableRotations: [0], + }, + }, + chipPinMap: { + "U1.1": { pinId: "U1.1", offset: { x: -1, y: 0.5 }, side: "x-" }, + "U1.2": { pinId: "U1.2", offset: { x: -1, y: -0.5 }, side: "x-" }, + "C1.1": { pinId: "C1.1", offset: { x: -0.25, y: 0 }, side: "x-" }, + "C1.2": { pinId: "C1.2", offset: { x: 0.25, y: 0 }, side: "x+" }, + "C2.1": { pinId: "C2.1", offset: { x: -0.25, y: 0 }, side: "x-" }, + "C2.2": { pinId: "C2.2", offset: { x: 0.25, y: 0 }, side: "x+" }, + }, + netMap: { + VCC: ["U1.1", "C1.1"], + GND: ["U1.2", "C1.2", "C2.2"], + }, + pinDirections: {}, + pinStronglyConnectedPins: {}, + pinStronglyConnectedGroups: {}, + chipGap: 0.2, + partitionGap: 0.5, + decouplingCapsGap: 0.5, // Custom gap for decoupling caps + partitionType: "decoupling_caps", + } + + const solver = new DecouplingCapsPackingSolver({ partitionInputProblem }) + solver.solve() + + expect(solver.solved).toBe(true) + + // The gap between caps should be at least decouplingCapsGap (0.5) + const c1X = solver.layout!.chipPlacements["C1"]!.x + const c2X = solver.layout!.chipPlacements["C2"]!.x + const distance = Math.abs(c2X - c1X) + + // Distance should be at least cap width (0.5) + gap (0.5) = 1.0 + expect(distance).toBeGreaterThanOrEqual(0.5 + 0.5 - 0.01) // Small tolerance +}) \ No newline at end of file