From a5661c721999b0ba87b12aa89dce37ef77e5f347 Mon Sep 17 00:00:00 2001 From: serfersac Date: Fri, 8 May 2026 20:03:11 +0000 Subject: [PATCH] feat: implement specialized layout for decoupling capacitors (Issue #15) Adds SingleInnerPartitionPackingSolver methods for grouped capacitor alignment and support for horizontal/vertical layout directions. --- .../SingleInnerPartitionPackingSolver.ts | 406 +++++++++++++++++- lib/types/InputProblem.ts | 2 + tests/decoupling-cap-layout.test.ts | 211 +++++++++ 3 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 tests/decoupling-cap-layout.test.ts diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 88db103..20506be 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -65,7 +65,12 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } private createPackInput(): PackInput { - // Fall back to filtered mapping (weak + strong) + // Special handling for decoupling capacitors + if (this.partitionInputProblem.partitionType === "decoupling_caps") { + return this.createDecouplingCapPackInput() + } + + // Fall back to filtered mapping (weak + strong) for non-decoupling partitions const pinToNetworkMap = createFilteredNetworkMapping({ inputProblem: this.partitionInputProblem, pinIdToStronglyConnectedPins: this.pinIdToStronglyConnectedPins, @@ -141,9 +146,226 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } } + /** + * Specialized layout algorithm for decoupling capacitors + * Creates a clean, standardized layout for decoupling capacitors + */ + private createDecouplingCapPackInput(): PackInput { + // Get all chips in this partition + const chipEntries = Object.entries(this.partitionInputProblem.chipMap) + const chips = chipEntries.map(([_, chip]) => chip) + + // Find the most common net pairs (power and ground) among the capacitors + const netPairs: Record = {} + + for (const chip of chips) { + if (chip.pins.length !== 2) continue + + const pin1 = this.partitionInputProblem.chipPinMap[chip.pins[0]] + const pin2 = this.partitionInputProblem.chipPinMap[chip.pins[1]] + + if (!pin1 || !pin2) continue + + // Get the nets connected to these pins + const net1 = this.getNetForPin(chip.pins[0]) + const net2 = this.getNetForPin(chip.pins[1]) + + if (!net1 || !net2) continue + + // Determine which net is power and which is ground + const net1IsPower = this.partitionInputProblem.netMap[net1]?.isPositiveVoltageSource + const net2IsPower = this.partitionInputProblem.netMap[net2]?.isPositiveVoltageSource + + let powerNet, groundNet + + if (net1IsPower && !net2IsPower) { + powerNet = net1 + groundNet = net2 + } else if (net2IsPower && !net1IsPower) { + powerNet = net2 + groundNet = net1 + } else { + continue // Skip if we can't determine power/ground + } + + const key = `${powerNet}-${groundNet}` + if (!netPairs[key]) { + netPairs[key] = { powerNet, groundNet, count: 0 } + } + netPairs[key].count++ + } + + // Find the most common net pair + let mostCommonPair = null + let maxCount = 0 + + for (const key in netPairs) { + if (netPairs[key].count > maxCount) { + mostCommonPair = netPairs[key] + maxCount = netPairs[key].count + } + } + + if (!mostCommonPair) { + // Fall back to default packing if we can't determine net pairs + return this.createDefaultPackInput() + } + + // Create pack components with specialized layout for decoupling caps + const packComponents = chipEntries.map(([chipId, chip]) => { + // Create pads for all pins of this chip + const pads: Array<{ + padId: string + networkId: string + type: "rect" + offset: { x: number; y: number } + size: { x: number; y: number } + }> = [] + + // Create a pad for each pin on this chip + for (const pinId of chip.pins) { + const pin = this.partitionInputProblem.chipPinMap[pinId] + if (!pin) continue + + // Find network for this pin + const networkId = this.getNetForPin(pinId) || `${pinId}_isolated` + + pads.push({ + padId: pinId, + networkId: networkId, + type: "rect" as const, + offset: { x: pin.offset.x, y: pin.offset.y }, + size: { x: PIN_SIZE, y: PIN_SIZE }, + }) + } + + const padsBoundingBox = getPadsBoundingBox(pads) + const padsBoundingBoxSize = { + x: padsBoundingBox.maxX - padsBoundingBox.minX, + y: padsBoundingBox.maxY - padsBoundingBox.minY, + } + + // Add chip body pad + pads.push({ + padId: `${chipId}_body`, + networkId: `${chipId}_body_disconnected`, + type: "rect" as const, + offset: { x: 0, y: 0 }, + size: { + x: Math.max(padsBoundingBoxSize.x, chip.size.x), + y: Math.max(padsBoundingBoxSize.y, chip.size.y), + }, + }) + + return { + componentId: chipId, + pads, + // For decoupling caps, we typically want to restrict rotation to 0 or 180 degrees + // to maintain consistent orientation + availableRotationDegrees: chip.availableRotations || [0, 180], + } + }) + + // Use a specialized packing strategy for decoupling capacitors + return { + components: packComponents, + minGap: this.partitionInputProblem.decouplingCapsGap || this.partitionInputProblem.chipGap, + packOrderStrategy: "largest_to_smallest", + // Use a specialized placement strategy for decoupling caps + packPlacementStrategy: "decoupling_capacitor_layout", + } + } + + /** + * Helper method to get the net for a pin + */ + private getNetForPin(pinId: PinId): NetId | null { + for (const [connKey, isConnected] of Object.entries( + this.partitionInputProblem.netConnMap, + )) { + if (!isConnected) continue + const [pin, net] = connKey.split("-") as [PinId, NetId] + if (pin === pinId) return net + } + return null + } + + /** + * Fallback to default packing if specialized layout can't be determined + */ + private createDefaultPackInput(): PackInput { + const pinToNetworkMap = createFilteredNetworkMapping({ + inputProblem: this.partitionInputProblem, + pinIdToStronglyConnectedPins: this.pinIdToStronglyConnectedPins, + }).pinToNetworkMap + + const packComponents = Object.entries( + this.partitionInputProblem.chipMap, + ).map(([chipId, chip]) => { + const pads: Array<{ + padId: string + networkId: string + type: "rect" + offset: { x: number; y: number } + size: { x: number; y: number } + }> = [] + + for (const pinId of chip.pins) { + const pin = this.partitionInputProblem.chipPinMap[pinId] + if (!pin) continue + + const networkId = pinToNetworkMap.get(pinId) || `${pinId}_isolated` + + pads.push({ + padId: pinId, + networkId: networkId, + type: "rect" as const, + offset: { x: pin.offset.x, y: pin.offset.y }, + size: { x: PIN_SIZE, y: PIN_SIZE }, + }) + } + + const padsBoundingBox = getPadsBoundingBox(pads) + const padsBoundingBoxSize = { + x: padsBoundingBox.maxX - padsBoundingBox.minX, + y: padsBoundingBox.maxY - padsBoundingBox.minY, + } + + pads.push({ + padId: `${chipId}_body`, + networkId: `${chipId}_body_disconnected`, + type: "rect" as const, + offset: { x: 0, y: 0 }, + size: { + x: Math.max(padsBoundingBoxSize.x, chip.size.x), + y: Math.max(padsBoundingBoxSize.y, chip.size.y), + }, + }) + + return { + componentId: chipId, + pads, + availableRotationDegrees: chip.availableRotations || [0, 90, 180, 270], + } + }) + + return { + components: packComponents, + minGap: this.partitionInputProblem.decouplingCapsGap || this.partitionInputProblem.chipGap, + packOrderStrategy: "largest_to_smallest", + packPlacementStrategy: "minimum_closest_sum_squared_distance", + } + } + private createLayoutFromPackingResult( packedComponents: PackSolver2["packedComponents"], ): OutputLayout { + // Special handling for decoupling capacitors + if (this.partitionInputProblem.partitionType === "decoupling_caps") { + return this.createDecouplingCapLayout(packedComponents) + } + + // Default handling for other components const chipPlacements: Record = {} for (const packedComponent of packedComponents) { @@ -165,6 +387,188 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } } + /** + * Creates a specialized layout for decoupling capacitors + * Aligns capacitors in a clean, standardized format + */ + private createDecouplingCapLayout( + packedComponents: PackSolver2["packedComponents"], + ): OutputLayout { + const chipPlacements: Record = {} + + // Get all chips in this partition + const chipEntries = Object.entries(this.partitionInputProblem.chipMap) + const chips = chipEntries.map(([_, chip]) => chip) + + // Find the most common net pairs (power and ground) among the capacitors + const netPairs: Record = {} + + for (const chip of chips) { + if (chip.pins.length !== 2) continue + + const pin1 = this.partitionInputProblem.chipPinMap[chip.pins[0]] + const pin2 = this.partitionInputProblem.chipPinMap[chip.pins[1]] + + if (!pin1 || !pin2) continue + + // Get the nets connected to these pins + const net1 = this.getNetForPin(chip.pins[0]) + const net2 = this.getNetForPin(chip.pins[1]) + + if (!net1 || !net2) continue + + // Determine which net is power and which is ground + const net1IsPower = this.partitionInputProblem.netMap[net1]?.isPositiveVoltageSource + const net2IsPower = this.partitionInputProblem.netMap[net2]?.isPositiveVoltageSource + + let powerNet, groundNet + + if (net1IsPower && !net2IsPower) { + powerNet = net1 + groundNet = net2 + } else if (net2IsPower && !net1IsPower) { + powerNet = net2 + groundNet = net1 + } else { + continue // Skip if we can't determine power/ground + } + + const key = `${powerNet}-${groundNet}` + if (!netPairs[key]) { + netPairs[key] = { powerNet, groundNet, count: 0 } + } + netPairs[key].count++ + } + + // Find the most common net pair + let mostCommonPair = null + let maxCount = 0 + + for (const key in netPairs) { + if (netPairs[key].count > maxCount) { + mostCommonPair = netPairs[key] + maxCount = netPairs[key].count + } + } + + if (!mostCommonPair) { + // Fall back to default layout if we can't determine net pairs + for (const packedComponent of packedComponents) { + const chipId = packedComponent.componentId + chipPlacements[chipId] = { + x: packedComponent.center.x, + y: packedComponent.center.y, + ccwRotationDegrees: + packedComponent.ccwRotationOffset || + packedComponent.ccwRotationDegrees || + 0, + } + } + return { + chipPlacements, + groupPlacements: {}, + } + } + + // Get all chips that belong to the most common net pair + const relevantChips: Array<{chipId: string, chip: any, powerPin: string, groundPin: string}> = [] + + for (const [chipId, chip] of chipEntries) { + if (chip.pins.length !== 2) continue + + const pin1 = this.partitionInputProblem.chipPinMap[chip.pins[0]] + const pin2 = this.partitionInputProblem.chipPinMap[chip.pins[1]] + + if (!pin1 || !pin2) continue + + const net1 = this.getNetForPin(chip.pins[0]) + const net2 = this.getNetForPin(chip.pins[1]) + + if (!net1 || !net2) continue + + const net1IsPower = this.partitionInputProblem.netMap[net1]?.isPositiveVoltageSource + const net2IsPower = this.partitionInputProblem.netMap[net2]?.isPositiveVoltageSource + + if ((net1 === mostCommonPair.powerNet && net2 === mostCommonPair.groundNet) || + (net2 === mostCommonPair.powerNet && net1 === mostCommonPair.groundNet)) { + relevantChips.push({ + chipId, + chip, + powerPin: net1IsPower ? chip.pins[0] : chip.pins[1], + groundPin: net1IsPower ? chip.pins[1] : chip.pins[0] + }) + } + } + + // Sort chips by size (largest to smallest) + relevantChips.sort((a, b) => { + const sizeA = a.chip.size.x * a.chip.size.y + const sizeB = b.chip.size.x * b.chip.size.y + return sizeB - sizeA // Descending order + }) + + // Calculate layout for decoupling capacitors + // We'll arrange them in a row with consistent spacing + const minGap = this.partitionInputProblem.decouplingCapsGap || this.partitionInputProblem.chipGap + const chipHeight = Math.max(...relevantChips.map(c => c.chip.size.y)) + const chipWidth = Math.max(...relevantChips.map(c => c.chip.size.x)) + + // Determine layout direction (default to horizontal) + const layoutDirection = this.partitionInputProblem.decouplingCapsLayoutDirection || "horizontal" + + // Start position + let x = 0 + let y = 0 + + // Place each capacitor in a row + for (let i = 0; i < relevantChips.length; i++) { + const chip = relevantChips[i] + const chipId = chip.chipId + + if (layoutDirection === "horizontal") { + // Horizontal layout - place chips side by side + chipPlacements[chipId] = { + x: x + chipWidth / 2, + y: y, + ccwRotationDegrees: 0, // Keep consistent orientation + } + + // Update x position for next chip + x += chipWidth + minGap + } else { + // Vertical layout - stack chips vertically + chipPlacements[chipId] = { + x: x, + y: y + chipHeight / 2, + ccwRotationDegrees: 0, // Keep consistent orientation + } + + // Update y position for next chip + y += chipHeight + minGap + } + } + + // For any chips not in the most common net pair, use the default packed positions + for (const packedComponent of packedComponents) { + const chipId = packedComponent.componentId + if (!chipPlacements[chipId]) { + chipPlacements[chipId] = { + x: packedComponent.center.x, + y: packedComponent.center.y, + ccwRotationDegrees: + packedComponent.ccwRotationOffset || + packedComponent.ccwRotationDegrees || + 0, + } + } + } + + return { + chipPlacements, + groupPlacements: {}, + } + } + override visualize(): GraphicsObject { if (this.activeSubSolver && !this.solved) { return this.activeSubSolver.visualize() diff --git a/lib/types/InputProblem.ts b/lib/types/InputProblem.ts index 38e9f75..bd0bcbe 100644 --- a/lib/types/InputProblem.ts +++ b/lib/types/InputProblem.ts @@ -42,6 +42,8 @@ export type InputProblem = { partitionGap: number decouplingCapsGap?: number + /** Layout direction for decoupling capacitors: "horizontal" or "vertical" */ + decouplingCapsLayoutDirection?: "horizontal" | "vertical" inferDecouplingCaps?: boolean } diff --git a/tests/decoupling-cap-layout.test.ts b/tests/decoupling-cap-layout.test.ts new file mode 100644 index 0000000..b5c92d2 --- /dev/null +++ b/tests/decoupling-cap-layout.test.ts @@ -0,0 +1,211 @@ + +import { LayoutPipelineSolver } from "../lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver" +import type { InputProblem } from "../lib/types/InputProblem" + +describe("Decoupling Capacitor Layout", () => { + it("should create a clean layout for decoupling capacitors", () => { + // Create a test input problem with decoupling capacitors + const inputProblem: InputProblem = { + chipMap: { + // Main IC + "ic1": { + chipId: "ic1", + pins: ["ic1.p1", "ic1.p2", "ic1.p3", "ic1.p4"], + size: { x: 10, y: 10 } + }, + // Decoupling capacitors + "cap1": { + chipId: "cap1", + pins: ["cap1.p1", "cap1.p2"], + size: { x: 2, y: 1 }, + availableRotations: [0, 180] + }, + "cap2": { + chipId: "cap2", + pins: ["cap2.p1", "cap2.p2"], + size: { x: 2, y: 1 }, + availableRotations: [0, 180] + }, + "cap3": { + chipId: "cap3", + pins: ["cap3.p1", "cap3.p2"], + size: { x: 2, y: 1 }, + availableRotations: [0, 180] + } + }, + chipPinMap: { + // IC pins + "ic1.p1": { pinId: "ic1.p1", offset: { x: -5, y: 0 }, side: "x-" }, + "ic1.p2": { pinId: "ic1.p2", offset: { x: 5, y: 0 }, side: "x+" }, + "ic1.p3": { pinId: "ic1.p3", offset: { x: 0, y: -5 }, side: "y-" }, + "ic1.p4": { pinId: "ic1.p4", offset: { x: 0, y: 5 }, side: "y+" }, + // Capacitor pins + "cap1.p1": { pinId: "cap1.p1", offset: { x: -1, y: 0 }, side: "x-" }, + "cap1.p2": { pinId: "cap1.p2", offset: { x: 1, y: 0 }, side: "x+" }, + "cap2.p1": { pinId: "cap2.p1", offset: { x: -1, y: 0 }, side: "x-" }, + "cap2.p2": { pinId: "cap2.p2", offset: { x: 1, y: 0 }, side: "x+" }, + "cap3.p1": { pinId: "cap3.p1", offset: { x: -1, y: 0 }, side: "x-" }, + "cap3.p2": { pinId: "cap3.p2", offset: { x: 1, y: 0 }, side: "x+" } + }, + netMap: { + "VCC": { netId: "VCC", isPositiveVoltageSource: true }, + "GND": { netId: "GND", isGround: true } + }, + pinStrongConnMap: { + // Connect IC power pins to capacitors + "ic1.p4-cap1.p1": true, + "ic1.p4-cap2.p1": true, + "ic1.p4-cap3.p1": true, + // Connect IC ground pins to capacitors + "ic1.p3-cap1.p2": true, + "ic1.p3-cap2.p2": true, + "ic1.p3-cap3.p2": true + }, + netConnMap: { + // Connect capacitors to power and ground nets + "cap1.p1-VCC": true, + "cap1.p2-GND": true, + "cap2.p1-VCC": true, + "cap2.p2-GND": true, + "cap3.p1-VCC": true, + "cap3.p2-GND": true + }, + chipGap: 1, + partitionGap: 2, + decouplingCapsGap: 0.5, + decouplingCapsLayoutDirection: "horizontal" + } + + // Create and run the layout solver + const solver = new LayoutPipelineSolver(inputProblem) + solver.solve() + + // Get the final layout + const layout = solver.getOutputLayout() + + // Verify that all capacitors are placed + expect(layout.chipPlacements["cap1"]).toBeDefined() + expect(layout.chipPlacements["cap2"]).toBeDefined() + expect(layout.chipPlacements["cap3"]).toBeDefined() + + // Verify that capacitors are aligned horizontally + const cap1X = layout.chipPlacements["cap1"].x + const cap2X = layout.chipPlacements["cap2"].x + const cap3X = layout.chipPlacements["cap3"].x + + // Capacitors should be placed in a row with consistent spacing + expect(cap2X).toBeGreaterThan(cap1X) + expect(cap3X).toBeGreaterThan(cap2X) + + // Verify that all capacitors have the same y position + const cap1Y = layout.chipPlacements["cap1"].y + const cap2Y = layout.chipPlacements["cap2"].y + const cap3Y = layout.chipPlacements["cap3"].y + + expect(cap1Y).toBe(cap2Y) + expect(cap2Y).toBe(cap3Y) + }) + + it("should support vertical layout for decoupling capacitors", () => { + // Create a test input problem with decoupling capacitors + const inputProblem: InputProblem = { + chipMap: { + // Main IC + "ic1": { + chipId: "ic1", + pins: ["ic1.p1", "ic1.p2", "ic1.p3", "ic1.p4"], + size: { x: 10, y: 10 } + }, + // Decoupling capacitors + "cap1": { + chipId: "cap1", + pins: ["cap1.p1", "cap1.p2"], + size: { x: 2, y: 1 }, + availableRotations: [0, 180] + }, + "cap2": { + chipId: "cap2", + pins: ["cap2.p1", "cap2.p2"], + size: { x: 2, y: 1 }, + availableRotations: [0, 180] + }, + "cap3": { + chipId: "cap3", + pins: ["cap3.p1", "cap3.p2"], + size: { x: 2, y: 1 }, + availableRotations: [0, 180] + } + }, + chipPinMap: { + // IC pins + "ic1.p1": { pinId: "ic1.p1", offset: { x: -5, y: 0 }, side: "x-" }, + "ic1.p2": { pinId: "ic1.p2", offset: { x: 5, y: 0 }, side: "x+" }, + "ic1.p3": { pinId: "ic1.p3", offset: { x: 0, y: -5 }, side: "y-" }, + "ic1.p4": { pinId: "ic1.p4", offset: { x: 0, y: 5 }, side: "y+" }, + // Capacitor pins + "cap1.p1": { pinId: "cap1.p1", offset: { x: -1, y: 0 }, side: "x-" }, + "cap1.p2": { pinId: "cap1.p2", offset: { x: 1, y: 0 }, side: "x+" }, + "cap2.p1": { pinId: "cap2.p1", offset: { x: -1, y: 0 }, side: "x-" }, + "cap2.p2": { pinId: "cap2.p2", offset: { x: 1, y: 0 }, side: "x+" }, + "cap3.p1": { pinId: "cap3.p1", offset: { x: -1, y: 0 }, side: "x-" }, + "cap3.p2": { pinId: "cap3.p2", offset: { x: 1, y: 0 }, side: "x+" } + }, + netMap: { + "VCC": { netId: "VCC", isPositiveVoltageSource: true }, + "GND": { netId: "GND", isGround: true } + }, + pinStrongConnMap: { + // Connect IC power pins to capacitors + "ic1.p4-cap1.p1": true, + "ic1.p4-cap2.p1": true, + "ic1.p4-cap3.p1": true, + // Connect IC ground pins to capacitors + "ic1.p3-cap1.p2": true, + "ic1.p3-cap2.p2": true, + "ic1.p3-cap3.p2": true + }, + netConnMap: { + // Connect capacitors to power and ground nets + "cap1.p1-VCC": true, + "cap1.p2-GND": true, + "cap2.p1-VCC": true, + "cap2.p2-GND": true, + "cap3.p1-VCC": true, + "cap3.p2-GND": true + }, + chipGap: 1, + partitionGap: 2, + decouplingCapsGap: 0.5, + decouplingCapsLayoutDirection: "vertical" + } + + // Create and run the layout solver + const solver = new LayoutPipelineSolver(inputProblem) + solver.solve() + + // Get the final layout + const layout = solver.getOutputLayout() + + // Verify that all capacitors are placed + expect(layout.chipPlacements["cap1"]).toBeDefined() + expect(layout.chipPlacements["cap2"]).toBeDefined() + expect(layout.chipPlacements["cap3"]).toBeDefined() + + // Verify that capacitors are aligned vertically + const cap1Y = layout.chipPlacements["cap1"].y + const cap2Y = layout.chipPlacements["cap2"].y + const cap3Y = layout.chipPlacements["cap3"].y + + // Capacitors should be placed in a column with consistent spacing + expect(cap2Y).toBeGreaterThan(cap1Y) + expect(cap3Y).toBeGreaterThan(cap2Y) + + // Verify that all capacitors have the same x position + const cap1X = layout.chipPlacements["cap1"].x + const cap2X = layout.chipPlacements["cap2"].x + const cap3X = layout.chipPlacements["cap3"].x + + expect(cap1X).toBe(cap2X) + expect(cap2X).toBe(cap3X) + }) +})