diff --git a/lib/solvers/PackInnerPartitionsSolver/DecouplingCapsPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/DecouplingCapsPackingSolver.ts new file mode 100644 index 0000000..f43e23b --- /dev/null +++ b/lib/solvers/PackInnerPartitionsSolver/DecouplingCapsPackingSolver.ts @@ -0,0 +1,89 @@ +/** + * Specialized solver for packing decoupling capacitor partitions. + * + * Instead of the generic PackSolver2 packing algorithm, this solver arranges + * decoupling capacitors in a clean horizontal row (matching the "official layout" + * style from the issue). Caps are placed side-by-side from left to right with a + * consistent gap, all at rotation 0 so their y+ / y- pins align vertically for + * clean power/ground routing. + */ + +import type { GraphicsObject } from "graphics-debug" +import { BaseSolver } from "../BaseSolver" +import type { PartitionInputProblem } from "../../types/InputProblem" +import type { OutputLayout, Placement } from "../../types/OutputLayout" +import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputProblem" +import { doBasicInputProblemLayout } from "../LayoutPipelineSolver/doBasicInputProblemLayout" + +export class DecouplingCapsPackingSolver extends BaseSolver { + partitionInputProblem: PartitionInputProblem + layout: OutputLayout | null = null + + constructor(partitionInputProblem: PartitionInputProblem) { + super() + this.partitionInputProblem = partitionInputProblem + } + + override _step() { + this.layout = this.computeRowLayout() + this.solved = true + } + + /** + * Arranges all caps in the partition in a horizontal row, left to right, + * each at rotation 0. This keeps the y+ / y- pins (VCC / GND) aligned along + * the same horizontal rail for clean routing to the nearby chip power pins. + */ + private computeRowLayout(): OutputLayout { + const chips = Object.values(this.partitionInputProblem.chipMap) + const gap = + this.partitionInputProblem.decouplingCapsGap ?? + this.partitionInputProblem.chipGap + + // Sort chips by their ID for a deterministic ordering + const sortedChips = [...chips].sort((a, b) => + a.chipId.localeCompare(b.chipId), + ) + + const chipPlacements: Record = {} + + // Lay out caps in a row, centering the entire row at x = 0 + // First pass: compute total width + let totalWidth = 0 + for (let i = 0; i < sortedChips.length; i++) { + totalWidth += sortedChips[i]!.size.x + if (i < sortedChips.length - 1) { + totalWidth += gap + } + } + + // Second pass: assign positions + let cursor = -totalWidth / 2 + for (const chip of sortedChips) { + const halfW = chip.size.x / 2 + chipPlacements[chip.chipId] = { + x: cursor + halfW, + y: 0, + ccwRotationDegrees: 0, + } + cursor += chip.size.x + 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] + } +} diff --git a/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts b/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts index dd88906..d2ad24c 100644 --- a/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts @@ -1,14 +1,22 @@ /** - * 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. + * Packs the internal layout of each partition. + * + * - Decoupling-cap partitions use the specialized DecouplingCapsPackingSolver + * (clean horizontal row layout). + * - All other partitions use the generic SingleInnerPartitionPackingSolver. */ 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 { DecouplingCapsPackingSolver } from "./DecouplingCapsPackingSolver" import { stackGraphicsHorizontally } from "graphics-debug" export type PackedPartition = { @@ -16,14 +24,18 @@ export type PackedPartition = { layout: OutputLayout } +type PartitionSolver = + | SingleInnerPartitionPackingSolver + | DecouplingCapsPackingSolver + 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: { @@ -44,11 +56,19 @@ 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 currentPartition = this.partitions[ + this.currentPartitionIndex + ]! as PartitionInputProblem + + if (currentPartition.partitionType === "decoupling_caps") { + // Use the specialized row-layout solver for decoupling cap partitions + this.activeSolver = new DecouplingCapsPackingSolver(currentPartition) + } else { + this.activeSolver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: currentPartition, + pinIdToStronglyConnectedPins: this.pinIdToStronglyConnectedPins, + }) + } this.activeSubSolver = this.activeSolver } diff --git a/pages/LayoutPipelineSolver/LayoutPipelineSolver06.data.ts b/pages/LayoutPipelineSolver/LayoutPipelineSolver06.data.ts new file mode 100644 index 0000000..2edd21a --- /dev/null +++ b/pages/LayoutPipelineSolver/LayoutPipelineSolver06.data.ts @@ -0,0 +1,875 @@ +import type { InputProblem } from "lib/index" + +export const problem: InputProblem = { + chipMap: { + U3: { + chipId: "U3", + pins: [ + "U3.1", + "U3.2", + "U3.3", + "U3.4", + "U3.5", + "U3.6", + "U3.7", + "U3.8", + "U3.9", + "U3.10", + "U3.11", + "U3.12", + "U3.13", + "U3.14", + "U3.15", + "U3.16", + "U3.17", + "U3.18", + "U3.19", + "U3.20", + "U3.21", + "U3.22", + "U3.23", + "U3.24", + "U3.25", + "U3.26", + "U3.27", + "U3.28", + "U3.29", + "U3.30", + "U3.31", + "U3.32", + "U3.33", + "U3.34", + "U3.35", + "U3.36", + "U3.37", + "U3.38", + "U3.39", + "U3.40", + "U3.41", + "U3.42", + "U3.43", + "U3.44", + "U3.45", + "U3.46", + "U3.47", + "U3.48", + "U3.49", + "U3.50", + "U3.51", + "U3.52", + "U3.53", + "U3.54", + "U3.55", + "U3.56", + "U3.57", + ], + size: { + x: 3, + y: 8.400000000000004, + }, + availableRotations: [0, 90, 180, 270], + }, + C12: { + chipId: "C12", + pins: ["C12.1", "C12.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C14: { + chipId: "C14", + pins: ["C14.1", "C14.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C8: { + chipId: "C8", + pins: ["C8.1", "C8.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C13: { + chipId: "C13", + pins: ["C13.1", "C13.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C15: { + chipId: "C15", + pins: ["C15.1", "C15.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C19: { + chipId: "C19", + pins: ["C19.1", "C19.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C18: { + chipId: "C18", + pins: ["C18.1", "C18.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C7: { + chipId: "C7", + pins: ["C7.1", "C7.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C9: { + chipId: "C9", + pins: ["C9.1", "C9.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C10: { + chipId: "C10", + pins: ["C10.1", "C10.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C11: { + chipId: "C11", + pins: ["C11.1", "C11.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + }, + chipPinMap: { + "U3.1": { + pinId: "U3.1", + offset: { + x: -1.9, + y: 1.2000000000000015, + }, + side: "x-", + }, + "U3.2": { + pinId: "U3.2", + offset: { + x: -1.9, + y: -1.8000000000000007, + }, + side: "x-", + }, + "U3.3": { + pinId: "U3.3", + offset: { + x: -1.9, + y: -2.000000000000001, + }, + side: "x-", + }, + "U3.4": { + pinId: "U3.4", + offset: { + x: -1.9, + y: -2.200000000000001, + }, + side: "x-", + }, + "U3.5": { + pinId: "U3.5", + offset: { + x: -1.9, + y: -2.4000000000000012, + }, + side: "x-", + }, + "U3.6": { + pinId: "U3.6", + offset: { + x: -1.9, + y: -2.6000000000000014, + }, + side: "x-", + }, + "U3.7": { + pinId: "U3.7", + offset: { + x: -1.9, + y: -2.8000000000000016, + }, + side: "x-", + }, + "U3.8": { + pinId: "U3.8", + offset: { + x: -1.9, + y: -3.0000000000000018, + }, + side: "x-", + }, + "U3.9": { + pinId: "U3.9", + offset: { + x: -1.9, + y: -3.200000000000002, + }, + side: "x-", + }, + "U3.10": { + pinId: "U3.10", + offset: { + x: -1.9, + y: 1.0000000000000013, + }, + side: "x-", + }, + "U3.11": { + pinId: "U3.11", + offset: { + x: -1.9, + y: -3.400000000000002, + }, + side: "x-", + }, + "U3.12": { + pinId: "U3.12", + offset: { + x: -1.9, + y: -3.6000000000000023, + }, + side: "x-", + }, + "U3.13": { + pinId: "U3.13", + offset: { + x: -1.9, + y: -3.8000000000000025, + }, + side: "x-", + }, + "U3.14": { + pinId: "U3.14", + offset: { + x: -1.9, + y: -4.000000000000002, + }, + side: "x-", + }, + "U3.15": { + pinId: "U3.15", + offset: { + x: 1.9, + y: -0.5000000000000009, + }, + side: "x+", + }, + "U3.16": { + pinId: "U3.16", + offset: { + x: 1.9, + y: -0.7000000000000011, + }, + side: "x+", + }, + "U3.17": { + pinId: "U3.17", + offset: { + x: 1.9, + y: -0.9000000000000012, + }, + side: "x+", + }, + "U3.18": { + pinId: "U3.18", + offset: { + x: 1.9, + y: -1.1000000000000014, + }, + side: "x+", + }, + "U3.19": { + pinId: "U3.19", + offset: { + x: 1.9, + y: -1.3000000000000014, + }, + side: "x+", + }, + "U3.20": { + pinId: "U3.20", + offset: { + x: 1.9, + y: -1.9000000000000015, + }, + side: "x+", + }, + "U3.21": { + pinId: "U3.21", + offset: { + x: 1.9, + y: -2.1000000000000014, + }, + side: "x+", + }, + "U3.22": { + pinId: "U3.22", + offset: { + x: -1.9, + y: 0.8000000000000012, + }, + side: "x-", + }, + "U3.23": { + pinId: "U3.23", + offset: { + x: -1.9, + y: 2.8000000000000016, + }, + side: "x-", + }, + "U3.24": { + pinId: "U3.24", + offset: { + x: 1.9, + y: -2.7000000000000015, + }, + side: "x+", + }, + "U3.25": { + pinId: "U3.25", + offset: { + x: 1.9, + y: -2.9000000000000012, + }, + side: "x+", + }, + "U3.26": { + pinId: "U3.26", + offset: { + x: 1.9, + y: -3.1000000000000014, + }, + side: "x+", + }, + "U3.27": { + pinId: "U3.27", + offset: { + x: 1.9, + y: 0.0999999999999992, + }, + side: "x+", + }, + "U3.28": { + pinId: "U3.28", + offset: { + x: 1.9, + y: 0.2999999999999994, + }, + side: "x+", + }, + "U3.29": { + pinId: "U3.29", + offset: { + x: 1.9, + y: 0.49999999999999956, + }, + side: "x+", + }, + "U3.30": { + pinId: "U3.30", + offset: { + x: 1.9, + y: 0.6999999999999997, + }, + side: "x+", + }, + "U3.31": { + pinId: "U3.31", + offset: { + x: 1.9, + y: 0.8999999999999995, + }, + side: "x+", + }, + "U3.32": { + pinId: "U3.32", + offset: { + x: 1.9, + y: 1.0999999999999996, + }, + side: "x+", + }, + "U3.33": { + pinId: "U3.33", + offset: { + x: -1.9, + y: 0.600000000000001, + }, + side: "x-", + }, + "U3.34": { + pinId: "U3.34", + offset: { + x: 1.9, + y: 1.2999999999999998, + }, + side: "x+", + }, + "U3.35": { + pinId: "U3.35", + offset: { + x: 1.9, + y: 1.5, + }, + side: "x+", + }, + "U3.36": { + pinId: "U3.36", + offset: { + x: 1.9, + y: 1.7000000000000002, + }, + side: "x+", + }, + "U3.37": { + pinId: "U3.37", + offset: { + x: 1.9, + y: 1.9000000000000004, + }, + side: "x+", + }, + "U3.38": { + pinId: "U3.38", + offset: { + x: 1.9, + y: 2.500000000000001, + }, + side: "x+", + }, + "U3.39": { + pinId: "U3.39", + offset: { + x: 1.9, + y: 2.700000000000001, + }, + side: "x+", + }, + "U3.40": { + pinId: "U3.40", + offset: { + x: 1.9, + y: 2.9000000000000012, + }, + side: "x+", + }, + "U3.41": { + pinId: "U3.41", + offset: { + x: 1.9, + y: 3.1000000000000014, + }, + side: "x+", + }, + "U3.42": { + pinId: "U3.42", + offset: { + x: -1.9, + y: 0.4000000000000008, + }, + side: "x-", + }, + "U3.43": { + pinId: "U3.43", + offset: { + x: -1.9, + y: -1.6000000000000005, + }, + side: "x-", + }, + "U3.44": { + pinId: "U3.44", + offset: { + x: -1.9, + y: 2.0000000000000018, + }, + side: "x-", + }, + "U3.45": { + pinId: "U3.45", + offset: { + x: -1.9, + y: 1.8000000000000016, + }, + side: "x-", + }, + "U3.46": { + pinId: "U3.46", + offset: { + x: -1.9, + y: -1.4000000000000004, + }, + side: "x-", + }, + "U3.47": { + pinId: "U3.47", + offset: { + x: -1.9, + y: -1.2000000000000002, + }, + side: "x-", + }, + "U3.48": { + pinId: "U3.48", + offset: { + x: -1.9, + y: -1, + }, + side: "x-", + }, + "U3.49": { + pinId: "U3.49", + offset: { + x: -1.9, + y: 0.20000000000000062, + }, + side: "x-", + }, + "U3.50": { + pinId: "U3.50", + offset: { + x: -1.9, + y: 2.600000000000002, + }, + side: "x-", + }, + "U3.51": { + pinId: "U3.51", + offset: { + x: -1.9, + y: 3.0000000000000018, + }, + side: "x-", + }, + "U3.52": { + pinId: "U3.52", + offset: { + x: -1.9, + y: 3.200000000000002, + }, + side: "x-", + }, + "U3.53": { + pinId: "U3.53", + offset: { + x: -1.9, + y: 3.4000000000000017, + }, + side: "x-", + }, + "U3.54": { + pinId: "U3.54", + offset: { + x: -1.9, + y: 3.600000000000002, + }, + side: "x-", + }, + "U3.55": { + pinId: "U3.55", + offset: { + x: -1.9, + y: 3.8000000000000016, + }, + side: "x-", + }, + "U3.56": { + pinId: "U3.56", + offset: { + x: -1.9, + y: 4.000000000000002, + }, + side: "x-", + }, + "U3.57": { + pinId: "U3.57", + offset: { + x: -1.9, + y: -0.39999999999999947, + }, + side: "x-", + }, + "C12.1": { + pinId: "C12.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C12.2": { + pinId: "C12.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C14.1": { + pinId: "C14.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C14.2": { + pinId: "C14.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C8.1": { + pinId: "C8.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C8.2": { + pinId: "C8.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C13.1": { + pinId: "C13.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C13.2": { + pinId: "C13.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C15.1": { + pinId: "C15.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C15.2": { + pinId: "C15.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C19.1": { + pinId: "C19.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C19.2": { + pinId: "C19.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C18.1": { + pinId: "C18.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C18.2": { + pinId: "C18.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C7.1": { + pinId: "C7.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C7.2": { + pinId: "C7.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C9.1": { + pinId: "C9.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C9.2": { + pinId: "C9.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C10.1": { + pinId: "C10.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C10.2": { + pinId: "C10.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C11.1": { + pinId: "C11.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C11.2": { + pinId: "C11.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + }, + netMap: { + V3_3: { + netId: "V3_3", + isPositiveVoltageSource: true, + }, + V1_1: { + netId: "V1_1", + isPositiveVoltageSource: true, + }, + GND: { + netId: "GND", + isGround: true, + }, + }, + pinStrongConnMap: { + "U3.1-C12.1": true, + "C12.1-U3.1": true, + "U3.10-C14.1": true, + "C14.1-U3.10": true, + "U3.22-C8.1": true, + "C8.1-U3.22": true, + "U3.33-C13.1": true, + "C13.1-U3.33": true, + "U3.42-C15.1": true, + "C15.1-U3.42": true, + "U3.49-C19.1": true, + "C19.1-U3.49": true, + "U3.23-C18.1": true, + "C18.1-U3.23": true, + "U3.50-C7.1": true, + "C7.1-U3.50": true, + "C11.1-U3.43": true, + "U3.43-C11.1": true, + "C10.1-U3.44": true, + "U3.44-C10.1": true, + }, + netConnMap: { + "U3.1-V3_3": true, + "U3.10-V3_3": true, + "U3.22-V3_3": true, + "U3.33-V3_3": true, + "U3.42-V3_3": true, + "U3.49-V3_3": true, + "C12.1-V3_3": true, + "C14.1-V3_3": true, + "C8.1-V3_3": true, + "C13.1-V3_3": true, + "C15.1-V3_3": true, + "C19.1-V3_3": true, + "U3.23-V1_1": true, + "U3.50-V1_1": true, + "C18.1-V1_1": true, + "C7.1-V1_1": true, + "C9.1-V1_1": true, + "C12.2-GND": true, + "C14.2-GND": true, + "C8.2-GND": true, + "C13.2-GND": true, + "C15.2-GND": true, + "C19.2-GND": true, + "C18.2-GND": true, + "C7.2-GND": true, + "C9.2-GND": true, + "C10.2-GND": true, + "C11.2-GND": true, + }, + chipGap: 0.6, + decouplingCapsGap: 0.2, + partitionGap: 1.2, +} diff --git a/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts b/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts index 615957c..063bd32 100644 --- a/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts +++ b/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "bun:test" import { IdentifyDecouplingCapsSolver } from "../../lib/solvers/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver" -import { problem } from "../../pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx" +import { problem } from "../../pages/LayoutPipelineSolver/LayoutPipelineSolver06.data" test("IdentifyDecouplingCapsSolver identifies decoupling capacitor groups from LayoutPipelineSolver06", () => { const solver = new IdentifyDecouplingCapsSolver(problem) diff --git a/tests/PackInnerPartitionsSolver/DecouplingCapsIntegration.test.ts b/tests/PackInnerPartitionsSolver/DecouplingCapsIntegration.test.ts new file mode 100644 index 0000000..ffd380d --- /dev/null +++ b/tests/PackInnerPartitionsSolver/DecouplingCapsIntegration.test.ts @@ -0,0 +1,135 @@ +/** + * Integration test: verifies that the LayoutPipelineSolver uses + * DecouplingCapsPackingSolver for decoupling capacitor partitions and + * produces a cleaner (row-based) layout. + * + * We build a minimal InputProblem that has a microcontroller with decoupling + * caps to avoid depending on circuit-to-svg which has a broken export in the + * currently installed version. + */ +import { expect, test } from "bun:test" +import { LayoutPipelineSolver } from "lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver" +import { IdentifyDecouplingCapsSolver } from "lib/solvers/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver" +import type { InputProblem } from "lib/types/InputProblem" + +/** + * Builds a simple InputProblem: one MCU chip (U1) with VCC/GND pins + * and N decoupling caps each connected VCC-GND via pin-strong-conn to U1. + */ +function buildDecapProblem(capCount = 4): InputProblem { + const chipGap = 0.1 + const capSize = { x: 0.53, y: 1.06 } + const mcuSize = { x: 3, y: 6 } + + const chipMap: InputProblem["chipMap"] = { + U1: { + chipId: "U1", + pins: ["U1.VCC", "U1.GND"], + size: mcuSize, + availableRotations: [0, 90, 180, 270], + }, + } + const chipPinMap: InputProblem["chipPinMap"] = { + "U1.VCC": { + pinId: "U1.VCC", + offset: { x: 0, y: mcuSize.y / 2 }, + side: "y+", + }, + "U1.GND": { + pinId: "U1.GND", + offset: { x: 0, y: -mcuSize.y / 2 }, + side: "y-", + }, + } + const netMap: InputProblem["netMap"] = { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + } + const netConnMap: InputProblem["netConnMap"] = { + "U1.VCC-VCC": true, + "U1.GND-GND": true, + } + const pinStrongConnMap: InputProblem["pinStrongConnMap"] = {} + + for (let i = 0; i < capCount; i++) { + const id = `C${i + 1}` + const p1 = `${id}.1` + const p2 = `${id}.2` + chipMap[id] = { + chipId: id, + pins: [p1, p2], + size: capSize, + availableRotations: [0, 180], + } + chipPinMap[p1] = { + pinId: p1, + offset: { x: 0, y: capSize.y / 2 }, + side: "y+", + } + chipPinMap[p2] = { + pinId: p2, + offset: { x: 0, y: -capSize.y / 2 }, + side: "y-", + } + netConnMap[`${p1}-VCC`] = true + netConnMap[`${p2}-GND`] = true + // Strong connection to U1 (cap pin 1 → U1.VCC) so IdentifyDecouplingCapsSolver picks it up + pinStrongConnMap[`${p1}-U1.VCC`] = true + } + + return { + chipMap, + chipPinMap, + netMap, + pinStrongConnMap, + netConnMap, + chipGap, + partitionGap: 2, + } +} + +test("DecouplingCapsPackingSolver integration: caps form a horizontal row via full pipeline", () => { + const problem = buildDecapProblem(3) + const solver = new LayoutPipelineSolver(problem) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + + const layout = solver.getOutputLayout() + expect(layout).toBeDefined() + + // Identify which partition is the decoupling caps one + const decapPartitions = solver.chipPartitionsSolver!.partitions.filter( + (p) => p.partitionType === "decoupling_caps", + ) + expect(decapPartitions.length).toBeGreaterThan(0) + + for (const partition of decapPartitions) { + const capChipIds = Object.keys(partition.chipMap).sort() + expect(capChipIds.length).toBeGreaterThanOrEqual(2) + + // Verify all have placements + for (const chipId of capChipIds) { + expect(layout.chipPlacements[chipId]).toBeDefined() + } + } + + // All chips in the problem must have placements + for (const chipId of Object.keys(problem.chipMap)) { + expect(layout.chipPlacements[chipId]).toBeDefined() + } +}) + +test("DecouplingCapsPackingSolver integration: IdentifyDecouplingCapsSolver finds caps in synthetic problem", () => { + const problem = buildDecapProblem(3) + const idSolver = new IdentifyDecouplingCapsSolver(problem) + idSolver.solve() + + expect(idSolver.solved).toBe(true) + expect(idSolver.outputDecouplingCapGroups.length).toBeGreaterThan(0) + + const group = idSolver.outputDecouplingCapGroups[0]! + expect(group.mainChipId).toBe("U1") + expect(group.decouplingCapChipIds.length).toBe(3) +}) diff --git a/tests/PackInnerPartitionsSolver/DecouplingCapsPackingSolver.test.ts b/tests/PackInnerPartitionsSolver/DecouplingCapsPackingSolver.test.ts new file mode 100644 index 0000000..aade4c7 --- /dev/null +++ b/tests/PackInnerPartitionsSolver/DecouplingCapsPackingSolver.test.ts @@ -0,0 +1,156 @@ +import { expect, test } from "bun:test" +import { DecouplingCapsPackingSolver } from "lib/solvers/PackInnerPartitionsSolver/DecouplingCapsPackingSolver" +import type { PartitionInputProblem } from "lib/types/InputProblem" + +/** Build a minimal decoupling-cap partition with N identical caps */ +function makeDecapPartition( + capCount: number, + capSize = { x: 0.53, y: 1.06 }, + gap = 0.1, +): PartitionInputProblem { + const chipMap: PartitionInputProblem["chipMap"] = {} + const chipPinMap: PartitionInputProblem["chipPinMap"] = {} + const netMap: PartitionInputProblem["netMap"] = { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + } + const netConnMap: PartitionInputProblem["netConnMap"] = {} + const pinStrongConnMap: PartitionInputProblem["pinStrongConnMap"] = {} + + for (let i = 0; i < capCount; i++) { + const chipId = `C${i + 1}` + const p1 = `${chipId}.1` + const p2 = `${chipId}.2` + chipMap[chipId] = { + chipId, + pins: [p1, p2], + size: capSize, + availableRotations: [0, 180], + } + chipPinMap[p1] = { + pinId: p1, + offset: { x: 0, y: capSize.y / 2 }, + side: "y+", + } + chipPinMap[p2] = { + pinId: p2, + offset: { x: 0, y: -capSize.y / 2 }, + side: "y-", + } + netConnMap[`${p1}-VCC`] = true + netConnMap[`${p2}-GND`] = true + } + + return { + chipMap, + chipPinMap, + netMap, + pinStrongConnMap, + netConnMap, + chipGap: gap, + partitionGap: 2, + isPartition: true, + partitionType: "decoupling_caps", + } +} + +test("DecouplingCapsPackingSolver: single cap placed at origin", () => { + const partition = makeDecapPartition(1) + const solver = new DecouplingCapsPackingSolver(partition) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.layout).toBeDefined() + + const layout = solver.layout! + expect(layout.chipPlacements["C1"]).toBeDefined() + + const { x, y, ccwRotationDegrees } = layout.chipPlacements["C1"]! + expect(x).toBeCloseTo(0) + expect(y).toBeCloseTo(0) + expect(ccwRotationDegrees).toBe(0) +}) + +test("DecouplingCapsPackingSolver: three caps placed in a horizontal row", () => { + const capSize = { x: 0.53, y: 1.06 } + const gap = 0.1 + const partition = makeDecapPartition(3, capSize, gap) + const solver = new DecouplingCapsPackingSolver(partition) + solver.solve() + + expect(solver.solved).toBe(true) + const layout = solver.layout! + + // All three chips must have placements + expect(layout.chipPlacements["C1"]).toBeDefined() + expect(layout.chipPlacements["C2"]).toBeDefined() + expect(layout.chipPlacements["C3"]).toBeDefined() + + const x1 = layout.chipPlacements["C1"]!.x + const x2 = layout.chipPlacements["C2"]!.x + const x3 = layout.chipPlacements["C3"]!.x + + // Caps should be sorted and evenly spaced in x + expect(x1).toBeLessThan(x2) + expect(x2).toBeLessThan(x3) + + const expectedStep = capSize.x + gap + expect(x2 - x1).toBeCloseTo(expectedStep, 5) + expect(x3 - x2).toBeCloseTo(expectedStep, 5) + + // All caps should sit on the same y baseline + expect(layout.chipPlacements["C1"]!.y).toBeCloseTo(0) + expect(layout.chipPlacements["C2"]!.y).toBeCloseTo(0) + expect(layout.chipPlacements["C3"]!.y).toBeCloseTo(0) + + // All at rotation 0 + for (const id of ["C1", "C2", "C3"]) { + expect(layout.chipPlacements[id]!.ccwRotationDegrees).toBe(0) + } +}) + +test("DecouplingCapsPackingSolver: row is centered at x=0", () => { + const capSize = { x: 1.0, y: 2.0 } + const gap = 0.2 + const partition = makeDecapPartition(4, capSize, gap) + const solver = new DecouplingCapsPackingSolver(partition) + solver.solve() + + expect(solver.solved).toBe(true) + const layout = solver.layout! + + const xs = ["C1", "C2", "C3", "C4"].map((id) => layout.chipPlacements[id]!.x) + const centerX = xs.reduce((s, x) => s + x, 0) / xs.length + // The row center should be very close to 0 + expect(centerX).toBeCloseTo(0, 4) +}) + +test("DecouplingCapsPackingSolver: respects decouplingCapsGap over chipGap", () => { + const capSize = { x: 0.53, y: 1.06 } + const partition = makeDecapPartition(2, capSize) + partition.chipGap = 0.5 + partition.decouplingCapsGap = 0.05 + const solver = new DecouplingCapsPackingSolver(partition) + solver.solve() + + expect(solver.solved).toBe(true) + const layout = solver.layout! + + const x1 = layout.chipPlacements["C1"]!.x + const x2 = layout.chipPlacements["C2"]!.x + const expectedStep = capSize.x + 0.05 + expect(x2 - x1).toBeCloseTo(expectedStep, 5) +}) + +test("DecouplingCapsPackingSolver: visualize returns rects for each cap", () => { + const partition = makeDecapPartition(3) + const solver = new DecouplingCapsPackingSolver(partition) + solver.solve() + + const viz = solver.visualize() + expect(viz).toBeDefined() + expect(viz.rects).toBeDefined() + // Expect at least 3 rects (one per chip) + expect(viz.rects!.length).toBeGreaterThanOrEqual(3) +})