diff --git a/lib/solvers/PackInnerPartitionsSolver/DecouplingCapsLayoutSolver.ts b/lib/solvers/PackInnerPartitionsSolver/DecouplingCapsLayoutSolver.ts new file mode 100644 index 0000000..76fd5f8 --- /dev/null +++ b/lib/solvers/PackInnerPartitionsSolver/DecouplingCapsLayoutSolver.ts @@ -0,0 +1,262 @@ +/** + * Specialized layout solver for decoupling capacitor partitions. + * Arranges decoupling capacitors in a clean, organized pattern around the main chip. + * + * This solver addresses issue #15 by: + * 1. Arranging capacitors in a compact grid pattern for better organization + * 2. Aligning capacitors consistently (all facing the same direction) + * 3. Minimizing trace crossings and messy layouts + * 4. Placing capacitors close to the main chip they're decoupling + * 5. Supporting multiple layout strategies (grid, circular, linear) + */ + +import type { GraphicsObject } from "graphics-debug" +import { BaseSolver } from "../BaseSolver" +import type { OutputLayout, Placement } from "../../types/OutputLayout" +import type { + InputProblem, + PinId, + ChipId, + PartitionInputProblem, + Chip, +} from "../../types/InputProblem" +import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputProblem" +import { doBasicInputProblemLayout } from "../LayoutPipelineSolver/doBasicInputProblemLayout" + +type LayoutStrategy = "grid" | "linear" | "circular" + +export class DecouplingCapsLayoutSolver extends BaseSolver { + partitionInputProblem: PartitionInputProblem + layout: OutputLayout | null = null + layoutStrategy: LayoutStrategy = "grid" + + constructor(params: { + partitionInputProblem: PartitionInputProblem + layoutStrategy?: LayoutStrategy + }) { + super() + this.partitionInputProblem = params.partitionInputProblem + this.layoutStrategy = params.layoutStrategy || "grid" + } + + override _step() { + // Create specialized layout for decoupling capacitors + this.layout = this.createDecouplingCapsLayout() + this.solved = true + } + + private createDecouplingCapsLayout(): OutputLayout { + const chipPlacements: Record = {} + const chips = Object.values(this.partitionInputProblem.chipMap) + + // Separate main chip from decoupling capacitors + const mainChip = chips.find((chip) => chip.pins.length > 2) + const decouplingCaps = chips.filter((chip) => chip.pins.length === 2) + + // Place main chip at center if it exists + if (mainChip) { + chipPlacements[mainChip.chipId] = { + x: 0, + y: 0, + ccwRotationDegrees: 0, + } + } + + // Arrange decoupling capacitors based on strategy + if (decouplingCaps.length > 0) { + switch (this.layoutStrategy) { + case "grid": + this.arrangeDecouplingCapsInGrid( + decouplingCaps, + chipPlacements, + mainChip, + ) + break + case "linear": + this.arrangeDecouplingCapsLinear( + decouplingCaps, + chipPlacements, + mainChip, + ) + break + case "circular": + this.arrangeDecouplingCapsCircular( + decouplingCaps, + chipPlacements, + mainChip, + ) + break + } + } + + return { + chipPlacements, + groupPlacements: {}, + } + } + + /** + * Arranges capacitors in a compact grid pattern + * Best for: 4+ capacitors + */ + private arrangeDecouplingCapsInGrid( + decouplingCaps: Chip[], + chipPlacements: Record, + mainChip: Chip | undefined, + ) { + const gap = this.partitionInputProblem.decouplingCapsGap ?? 0.3 + + // Calculate optimal grid dimensions (prefer square-ish layouts) + const numCaps = decouplingCaps.length + const cols = Math.ceil(Math.sqrt(numCaps)) + const rows = Math.ceil(numCaps / cols) + + // Calculate starting position based on main chip size + const mainChipWidth = mainChip?.size.x ?? 2 + const mainChipHeight = mainChip?.size.y ?? 2 + + // Get average capacitor size + const avgCapWidth = decouplingCaps.reduce((sum, cap) => sum + cap.size.x, 0) / numCaps + const avgCapHeight = decouplingCaps.reduce((sum, cap) => sum + cap.size.y, 0) / numCaps + + // Calculate grid dimensions + const gridWidth = cols * avgCapWidth + (cols - 1) * gap + const gridHeight = rows * avgCapHeight + (rows - 1) * gap + + // Position grid to the right of main chip + const startX = mainChipWidth / 2 + gap * 2 + const startY = -gridHeight / 2 + + // Arrange capacitors in grid + for (let i = 0; i < decouplingCaps.length; i++) { + const cap = decouplingCaps[i]! + const row = Math.floor(i / cols) + const col = i % cols + + const x = startX + col * (avgCapWidth + gap) + avgCapWidth / 2 + const y = startY + row * (avgCapHeight + gap) + avgCapHeight / 2 + + const rotation = this.getOptimalCapacitorRotation(cap) + + chipPlacements[cap.chipId] = { + x, + y, + ccwRotationDegrees: rotation, + } + } + } + + /** + * Arranges capacitors in a single line + * Best for: 2-3 capacitors + */ + private arrangeDecouplingCapsLinear( + decouplingCaps: Chip[], + chipPlacements: Record, + mainChip: Chip | undefined, + ) { + const gap = this.partitionInputProblem.decouplingCapsGap ?? 0.3 + const mainChipWidth = mainChip?.size.x ?? 2 + + const avgCapHeight = decouplingCaps.reduce((sum, cap) => sum + cap.size.y, 0) / decouplingCaps.length + const totalHeight = decouplingCaps.length * avgCapHeight + (decouplingCaps.length - 1) * gap + + const startX = mainChipWidth / 2 + gap * 2 + const startY = -totalHeight / 2 + + for (let i = 0; i < decouplingCaps.length; i++) { + const cap = decouplingCaps[i]! + const y = startY + i * (avgCapHeight + gap) + avgCapHeight / 2 + + const rotation = this.getOptimalCapacitorRotation(cap) + + chipPlacements[cap.chipId] = { + x: startX, + y, + ccwRotationDegrees: rotation, + } + } + } + + /** + * Arranges capacitors in a circular pattern around the main chip + * Best for: 4-8 capacitors, provides symmetrical layout + */ + private arrangeDecouplingCapsCircular( + decouplingCaps: Chip[], + chipPlacements: Record, + mainChip: Chip | undefined, + ) { + const mainChipWidth = mainChip?.size.x ?? 2 + const mainChipHeight = mainChip?.size.y ?? 2 + const gap = this.partitionInputProblem.decouplingCapsGap ?? 0.5 + + // Calculate radius based on main chip size + const radius = Math.max(mainChipWidth, mainChipHeight) / 2 + gap * 2 + + const angleStep = (2 * Math.PI) / decouplingCaps.length + + for (let i = 0; i < decouplingCaps.length; i++) { + const cap = decouplingCaps[i]! + const angle = i * angleStep + + const x = radius * Math.cos(angle) + const y = radius * Math.sin(angle) + + // Rotate capacitor to face the center + const rotationToCenter = (angle * 180 / Math.PI + 90) % 360 + const rotation = this.getClosestAvailableRotation(cap, rotationToCenter) + + chipPlacements[cap.chipId] = { + x, + y, + ccwRotationDegrees: rotation, + } + } + } + + private getOptimalCapacitorRotation(cap: Chip): number { + const availableRotations = cap.availableRotations || [0, 180] + + // For grid/linear layouts, prefer horizontal orientation (0 or 180) + if (availableRotations.includes(0)) { + return 0 + } + if (availableRotations.includes(180)) { + return 180 + } + + return availableRotations[0] || 0 + } + + private getClosestAvailableRotation(cap: Chip, targetRotation: number): number { + const availableRotations = cap.availableRotations || [0, 180] + + // Find the closest available rotation to the target + let closest = availableRotations[0] || 0 + let minDiff = Math.abs(targetRotation - closest) + + for (const rotation of availableRotations) { + const diff = Math.abs(targetRotation - rotation) + if (diff < minDiff) { + minDiff = diff + closest = rotation + } + } + + return closest + } + + override visualize(): GraphicsObject { + if (!this.layout) { + const basicLayout = doBasicInputProblemLayout(this.partitionInputProblem) + return visualizeInputProblem(this.partitionInputProblem, basicLayout) + } + + return visualizeInputProblem(this.partitionInputProblem, this.layout) + } + + override getConstructorParams(): [InputProblem] { + return [this.partitionInputProblem] + } +} diff --git a/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts b/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts index dd88906..5df2706 100644 --- a/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts @@ -2,6 +2,8 @@ * 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 DecouplingCapsLayoutSolver for cleaner layouts. */ import type { GraphicsObject } from "graphics-debug" @@ -9,6 +11,7 @@ import { BaseSolver } from "../BaseSolver" import type { ChipPin, InputProblem, PinId } from "../../types/InputProblem" import type { OutputLayout } from "../../types/OutputLayout" import { SingleInnerPartitionPackingSolver } from "./SingleInnerPartitionPackingSolver" +import { DecouplingCapsLayoutSolver } from "./DecouplingCapsLayoutSolver" import { stackGraphicsHorizontally } from "graphics-debug" export type PackedPartition = { @@ -19,11 +22,11 @@ export type PackedPartition = { export class PackInnerPartitionsSolver extends BaseSolver { partitions: InputProblem[] packedPartitions: PackedPartition[] = [] - completedSolvers: SingleInnerPartitionPackingSolver[] = [] - activeSolver: SingleInnerPartitionPackingSolver | null = null + completedSolvers: (SingleInnerPartitionPackingSolver | DecouplingCapsLayoutSolver)[] = [] + activeSolver: SingleInnerPartitionPackingSolver | DecouplingCapsLayoutSolver | null = null currentPartitionIndex = 0 - declare activeSubSolver: SingleInnerPartitionPackingSolver | null + declare activeSubSolver: SingleInnerPartitionPackingSolver | DecouplingCapsLayoutSolver | null pinIdToStronglyConnectedPins: Record constructor(params: { @@ -45,10 +48,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 specialized solver for decoupling capacitor partitions + if (currentPartition.partitionType === "decoupling_caps") { + this.activeSolver = new DecouplingCapsLayoutSolver({ + partitionInputProblem: currentPartition, + }) + } else { + this.activeSolver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: currentPartition, + pinIdToStronglyConnectedPins: this.pinIdToStronglyConnectedPins, + }) + } this.activeSubSolver = this.activeSolver } diff --git a/tests/DecouplingCapsLayoutSolver.test.ts b/tests/DecouplingCapsLayoutSolver.test.ts new file mode 100644 index 0000000..22474bd --- /dev/null +++ b/tests/DecouplingCapsLayoutSolver.test.ts @@ -0,0 +1,242 @@ +/** + * Tests for DecouplingCapsLayoutSolver + * Verifies that decoupling capacitors are arranged in clean, organized patterns + */ + +import { test, expect } from "bun:test" +import { DecouplingCapsLayoutSolver } from "../lib/solvers/PackInnerPartitionsSolver/DecouplingCapsLayoutSolver" +import type { PartitionInputProblem } from "../lib/types/InputProblem" + +test("DecouplingCapsLayoutSolver - Grid layout with 4 capacitors", () => { + const inputProblem: PartitionInputProblem = { + chipMap: { + main_chip: { + chipId: "main_chip", + pins: ["main_chip.1", "main_chip.2", "main_chip.3", "main_chip.4"], + size: { x: 2, y: 2 }, + availableRotations: [0], + }, + cap1: { + chipId: "cap1", + pins: ["cap1.1", "cap1.2"], + size: { x: 0.5, y: 0.3 }, + availableRotations: [0, 180], + }, + cap2: { + chipId: "cap2", + pins: ["cap2.1", "cap2.2"], + size: { x: 0.5, y: 0.3 }, + availableRotations: [0, 180], + }, + cap3: { + chipId: "cap3", + pins: ["cap3.1", "cap3.2"], + size: { x: 0.5, y: 0.3 }, + availableRotations: [0, 180], + }, + cap4: { + chipId: "cap4", + pins: ["cap4.1", "cap4.2"], + size: { x: 0.5, y: 0.3 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: {}, + netMap: {}, + netConnMap: {}, + pinStrongConnMap: {}, + chipGap: 0.3, + decouplingCapsGap: 0.3, + partitionType: "decoupling_caps", + } + + const solver = new DecouplingCapsLayoutSolver({ + partitionInputProblem: inputProblem, + layoutStrategy: "grid", + }) + + // Run solver to completion + while (!solver.solved && !solver.failed) { + solver.step() + } + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.layout).toBeDefined() + + // Verify all chips are placed + const placements = solver.layout!.chipPlacements + expect(Object.keys(placements).length).toBe(5) // 1 main chip + 4 caps + + // Verify main chip is at origin + expect(placements.main_chip?.x).toBe(0) + expect(placements.main_chip?.y).toBe(0) + + // Verify capacitors are placed to the right of main chip + for (const capId of ["cap1", "cap2", "cap3", "cap4"]) { + const placement = placements[capId] + expect(placement).toBeDefined() + expect(placement!.x).toBeGreaterThan(0) // Should be to the right + } +}) + +test("DecouplingCapsLayoutSolver - Linear layout with 3 capacitors", () => { + const inputProblem: PartitionInputProblem = { + chipMap: { + main_chip: { + chipId: "main_chip", + pins: ["main_chip.1", "main_chip.2", "main_chip.3"], + size: { x: 2, y: 2 }, + availableRotations: [0], + }, + cap1: { + chipId: "cap1", + pins: ["cap1.1", "cap1.2"], + size: { x: 0.5, y: 0.3 }, + availableRotations: [0, 180], + }, + cap2: { + chipId: "cap2", + pins: ["cap2.1", "cap2.2"], + size: { x: 0.5, y: 0.3 }, + availableRotations: [0, 180], + }, + cap3: { + chipId: "cap3", + pins: ["cap3.1", "cap3.2"], + size: { x: 0.5, y: 0.3 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: {}, + netMap: {}, + netConnMap: {}, + pinStrongConnMap: {}, + chipGap: 0.3, + decouplingCapsGap: 0.3, + partitionType: "decoupling_caps", + } + + const solver = new DecouplingCapsLayoutSolver({ + partitionInputProblem: inputProblem, + layoutStrategy: "linear", + }) + + while (!solver.solved && !solver.failed) { + solver.step() + } + + expect(solver.solved).toBe(true) + expect(solver.layout).toBeDefined() + + const placements = solver.layout!.chipPlacements + + // Verify capacitors are in a vertical line (same x coordinate) + const cap1X = placements.cap1!.x + expect(placements.cap2!.x).toBeCloseTo(cap1X, 2) + expect(placements.cap3!.x).toBeCloseTo(cap1X, 2) + + // Verify they have different y coordinates (vertical spacing) + const yCoords = [ + placements.cap1!.y, + placements.cap2!.y, + placements.cap3!.y, + ].sort((a, b) => a - b) + + expect(yCoords[1]! - yCoords[0]!).toBeGreaterThan(0.3) // Gap between caps + expect(yCoords[2]! - yCoords[1]!).toBeGreaterThan(0.3) +}) + +test("DecouplingCapsLayoutSolver - Circular layout with 6 capacitors", () => { + const inputProblem: PartitionInputProblem = { + chipMap: { + main_chip: { + chipId: "main_chip", + pins: ["main_chip.1", "main_chip.2", "main_chip.3"], + size: { x: 2, y: 2 }, + availableRotations: [0], + }, + ...Object.fromEntries( + Array.from({ length: 6 }, (_, i) => [ + `cap${i + 1}`, + { + chipId: `cap${i + 1}`, + pins: [`cap${i + 1}.1`, `cap${i + 1}.2`], + size: { x: 0.5, y: 0.3 }, + availableRotations: [0, 90, 180, 270], + }, + ]), + ), + }, + chipPinMap: {}, + netMap: {}, + netConnMap: {}, + pinStrongConnMap: {}, + chipGap: 0.3, + decouplingCapsGap: 0.5, + partitionType: "decoupling_caps", + } + + const solver = new DecouplingCapsLayoutSolver({ + partitionInputProblem: inputProblem, + layoutStrategy: "circular", + }) + + while (!solver.solved && !solver.failed) { + solver.step() + } + + expect(solver.solved).toBe(true) + expect(solver.layout).toBeDefined() + + const placements = solver.layout!.chipPlacements + + // Verify all capacitors are roughly equidistant from center + const distances = Array.from({ length: 6 }, (_, i) => { + const placement = placements[`cap${i + 1}`]! + return Math.sqrt(placement.x ** 2 + placement.y ** 2) + }) + + const avgDistance = distances.reduce((a, b) => a + b, 0) / distances.length + for (const distance of distances) { + expect(Math.abs(distance - avgDistance)).toBeLessThan(0.1) // All roughly same distance + } +}) + +test("DecouplingCapsLayoutSolver - Handles partition without main chip", () => { + const inputProblem: PartitionInputProblem = { + chipMap: { + cap1: { + chipId: "cap1", + pins: ["cap1.1", "cap1.2"], + size: { x: 0.5, y: 0.3 }, + availableRotations: [0, 180], + }, + cap2: { + chipId: "cap2", + pins: ["cap2.1", "cap2.2"], + size: { x: 0.5, y: 0.3 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: {}, + netMap: {}, + netConnMap: {}, + pinStrongConnMap: {}, + chipGap: 0.3, + decouplingCapsGap: 0.3, + partitionType: "decoupling_caps", + } + + const solver = new DecouplingCapsLayoutSolver({ + partitionInputProblem: inputProblem, + }) + + while (!solver.solved && !solver.failed) { + solver.step() + } + + expect(solver.solved).toBe(true) + expect(solver.layout).toBeDefined() + expect(Object.keys(solver.layout!.chipPlacements).length).toBe(2) +})