From a1a92abf03c74240b7c0c235f58ef334500aba96 Mon Sep 17 00:00:00 2001 From: EmergentKnowledgeGroup <120781140+EmergentKnowledgeGroup@users.noreply.github.com> Date: Mon, 11 May 2026 05:57:41 -0500 Subject: [PATCH] Improve decoupling capacitor partition layout --- .../ChipPartitionsSolver.ts | 76 +++++++++++++- .../SingleInnerPartitionPackingSolver.ts | 67 +++++++++++++ .../LayoutPipelineSolver06.page.tsx | 13 ++- tests/ChipPartitionsSolver.test.ts | 99 +++++++++++++++++++ .../SingleInnerPartitionPackingSolver.test.ts | 63 ++++++++++++ 5 files changed, 313 insertions(+), 5 deletions(-) create mode 100644 tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts diff --git a/lib/solvers/ChipPartitionsSolver/ChipPartitionsSolver.ts b/lib/solvers/ChipPartitionsSolver/ChipPartitionsSolver.ts index 8335b60..15fb08e 100644 --- a/lib/solvers/ChipPartitionsSolver/ChipPartitionsSolver.ts +++ b/lib/solvers/ChipPartitionsSolver/ChipPartitionsSolver.ts @@ -49,7 +49,10 @@ export class ChipPartitionsSolver extends BaseSolver { // 1) Build decoupling-cap-only partitions (exclude the main chip for each group) const decapChipIdSet = new Set() - const decapGroupPartitions: ChipId[][] = [] + const decapGroupPartitions: Array<{ + chipIds: ChipId[] + netPair: [NetId, NetId] + }> = [] if (this.decouplingCapGroups && this.decouplingCapGroups.length > 0) { for (const group of this.decouplingCapGroups) { @@ -61,7 +64,10 @@ export class ChipPartitionsSolver extends BaseSolver { } // Only add a partition if there are at least two caps present in the inputProblem if (capsOnly.length >= 2) { - decapGroupPartitions.push(capsOnly) + decapGroupPartitions.push({ + chipIds: capsOnly, + netPair: group.netPair, + }) // Mark these caps as handled by decoupling-cap partitions for (const capId of capsOnly) { decapChipIdSet.add(capId) @@ -119,8 +125,9 @@ export class ChipPartitionsSolver extends BaseSolver { return [ ...decapGroupPartitions.map((partition) => - this.createInputProblemFromPartition(partition, inputProblem, { + this.createInputProblemFromPartition(partition.chipIds, inputProblem, { partitionType: "decoupling_caps", + decouplingCapNetPair: partition.netPair, }), ), ...nonDecapPartitions.map((partition) => @@ -188,6 +195,7 @@ export class ChipPartitionsSolver extends BaseSolver { originalProblem: InputProblem, opts?: { partitionType?: "default" | "decoupling_caps" + decouplingCapNetPair?: [NetId, NetId] }, ): PartitionInputProblem { const chipIds = partition @@ -242,6 +250,19 @@ export class ChipPartitionsSolver extends BaseSolver { } } + if ( + opts?.partitionType === "decoupling_caps" && + opts.decouplingCapNetPair + ) { + this.addExternalDecouplingNetConnections({ + originalProblem, + relevantPinIds, + allowedNetIds: new Set(opts.decouplingCapNetPair), + relevantNetIds, + netConnMap, + }) + } + for (const netId of relevantNetIds) { if (originalProblem.netMap[netId]) { netMap[netId] = originalProblem.netMap[netId] @@ -260,6 +281,55 @@ export class ChipPartitionsSolver extends BaseSolver { } } + private addExternalDecouplingNetConnections({ + originalProblem, + relevantPinIds, + allowedNetIds, + relevantNetIds, + netConnMap, + }: { + originalProblem: InputProblem + relevantPinIds: Set + allowedNetIds: Set + relevantNetIds: Set + netConnMap: Record + }) { + const copyNetsFromExternalPin = ( + innerPinId: PinId, + externalPinId: PinId, + ) => { + for (const [connKey, isConnected] of Object.entries( + originalProblem.netConnMap, + )) { + if (!isConnected) continue + + const [pinId, netId] = connKey.split("-") as [PinId, NetId] + if (pinId !== externalPinId || !allowedNetIds.has(netId)) continue + + relevantNetIds.add(netId) + netConnMap[`${innerPinId}-${netId}`] = true + } + } + + for (const [connKey, isConnected] of Object.entries( + originalProblem.pinStrongConnMap, + )) { + if (!isConnected) continue + + const [pinAId, pinBId] = connKey.split("-") as [PinId, PinId] + const pinAIsRelevant = relevantPinIds.has(pinAId) + const pinBIsRelevant = relevantPinIds.has(pinBId) + + if (pinAIsRelevant === pinBIsRelevant) continue + + if (pinAIsRelevant) { + copyNetsFromExternalPin(pinAId, pinBId) + } else { + copyNetsFromExternalPin(pinBId, pinAId) + } + } + } + override visualize(): GraphicsObject { if (this.partitions.length === 0) { return super.visualize() diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 88db103..30097ab 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -14,6 +14,7 @@ import type { NetId, ChipPin, PartitionInputProblem, + Chip, } from "../../types/InputProblem" import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputProblem" import { createFilteredNetworkMapping } from "../../utils/networkFiltering" @@ -38,6 +39,16 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } override _step() { + if ( + this.partitionInputProblem.partitionType === "decoupling_caps" && + !this.layout + ) { + this.layout = this.createDecouplingCapsLayout() + this.activeSubSolver = null + this.solved = true + return + } + // Initialize PackSolver2 if not already created if (!this.activeSubSolver) { const packInput = this.createPackInput() @@ -141,6 +152,62 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } } + private createDecouplingCapsLayout(): OutputLayout { + const gap = + this.partitionInputProblem.decouplingCapsGap ?? + this.partitionInputProblem.chipGap + const caps = Object.entries(this.partitionInputProblem.chipMap) + .sort(([chipAId], [chipBId]) => + chipAId.localeCompare(chipBId, undefined, { numeric: true }), + ) + .map(([chipId, chip]) => { + const ccwRotationDegrees = this.pickDecouplingCapRotation(chip) + const size = this.getRotatedChipSize(chip, ccwRotationDegrees) + + return { + chipId, + ccwRotationDegrees, + width: size.x, + } + }) + + const totalWidth = + caps.reduce((sum, cap) => sum + cap.width, 0) + + Math.max(0, caps.length - 1) * gap + let nextLeftEdge = -totalWidth / 2 + const chipPlacements: Record = {} + + for (const cap of caps) { + chipPlacements[cap.chipId] = { + x: nextLeftEdge + cap.width / 2, + y: 0, + ccwRotationDegrees: cap.ccwRotationDegrees, + } + nextLeftEdge += cap.width + gap + } + + return { + chipPlacements, + groupPlacements: {}, + } + } + + private pickDecouplingCapRotation(chip: Chip): 0 | 90 | 180 | 270 { + const availableRotations = chip.availableRotations ?? [0, 90, 180, 270] + for (const rotation of [0, 180, 90, 270] as const) { + if (availableRotations.includes(rotation)) return rotation + } + return availableRotations[0] ?? 0 + } + + private getRotatedChipSize(chip: Chip, rotation: 0 | 90 | 180 | 270) { + if (rotation === 90 || rotation === 270) { + return { x: chip.size.y, y: chip.size.x } + } + + return chip.size + } + private createLayoutFromPackingResult( packedComponents: PackSolver2["packedComponents"], ): OutputLayout { diff --git a/pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx b/pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx index c0767bb..703f4de 100644 --- a/pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx +++ b/pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx @@ -1,6 +1,11 @@ import type { PackInput } from "calculate-packing" -import { LayoutPipelineDebugger } from "lib/components/LayoutPipelineDebugger" import type { InputProblem } from "lib/index" +import { lazy, Suspense } from "react" + +const LayoutPipelineDebugger = lazy(async () => { + const mod = await import("lib/components/LayoutPipelineDebugger") + return { default: mod.LayoutPipelineDebugger } +}) export const problem: InputProblem = { chipMap: { @@ -877,5 +882,9 @@ export const problem: InputProblem = { } export default function LayoutPipelineSolver06Page() { - return + return ( + + + + ) } diff --git a/tests/ChipPartitionsSolver.test.ts b/tests/ChipPartitionsSolver.test.ts index d8eb08c..7f62de3 100644 --- a/tests/ChipPartitionsSolver.test.ts +++ b/tests/ChipPartitionsSolver.test.ts @@ -188,3 +188,102 @@ test("ChipPartitionsSolver visualization contains partition components", () => { expect(visualization.rects?.length).toBeGreaterThan(0) expect(visualization.texts?.length).toBeGreaterThan(0) }) + +test("ChipPartitionsSolver keeps decoupling cap nets inherited through main-chip strong connections", () => { + const inputProblem: InputProblem = { + chipMap: { + U1: { + chipId: "U1", + pins: ["U1.VCC", "U1.GND"], + size: { x: 2, y: 2 }, + }, + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 0.8, y: 0.4 }, + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 0.8, y: 0.4 }, + }, + }, + chipPinMap: { + "U1.VCC": { + pinId: "U1.VCC", + offset: { x: 1, y: 0.5 }, + side: "x+", + }, + "U1.GND": { + pinId: "U1.GND", + offset: { x: -1, y: -0.5 }, + side: "x-", + }, + "C1.1": { + pinId: "C1.1", + offset: { x: 0, y: 0.2 }, + side: "y+", + }, + "C1.2": { + pinId: "C1.2", + offset: { x: 0, y: -0.2 }, + side: "y-", + }, + "C2.1": { + pinId: "C2.1", + offset: { x: 0, y: 0.2 }, + side: "y+", + }, + "C2.2": { + pinId: "C2.2", + offset: { x: 0, y: -0.2 }, + side: "y-", + }, + }, + netMap: { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + }, + pinStrongConnMap: { + "C1.1-U1.VCC": true, + "U1.VCC-C1.1": true, + "C1.2-U1.GND": true, + "U1.GND-C1.2": true, + "C2.1-U1.VCC": true, + "U1.VCC-C2.1": true, + "C2.2-U1.GND": true, + "U1.GND-C2.2": true, + }, + netConnMap: { + "U1.VCC-VCC": true, + "U1.GND-GND": true, + }, + chipGap: 0.2, + partitionGap: 2, + } + + const solver = new ChipPartitionsSolver({ + inputProblem, + decouplingCapGroups: [ + { + decouplingCapGroupId: "decap_U1_VCC_GND", + mainChipId: "U1", + netPair: ["GND", "VCC"], + decouplingCapChipIds: ["C1", "C2"], + }, + ], + }) + solver.solve() + + const decapPartition = solver.partitions.find( + (partition) => partition.partitionType === "decoupling_caps", + ) + + expect(decapPartition).toBeDefined() + expect(Object.keys(decapPartition!.chipMap).sort()).toEqual(["C1", "C2"]) + expect(Object.keys(decapPartition!.netMap).sort()).toEqual(["GND", "VCC"]) + expect(decapPartition!.netConnMap["C1.1-VCC"]).toBe(true) + expect(decapPartition!.netConnMap["C1.2-GND"]).toBe(true) + expect(decapPartition!.netConnMap["C2.1-VCC"]).toBe(true) + expect(decapPartition!.netConnMap["C2.2-GND"]).toBe(true) +}) diff --git a/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts b/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts new file mode 100644 index 0000000..af52dc0 --- /dev/null +++ b/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts @@ -0,0 +1,63 @@ +import { expect, test } from "bun:test" +import { SingleInnerPartitionPackingSolver } from "../../lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver" +import type { PartitionInputProblem } from "../../lib/types/InputProblem" + +test("SingleInnerPartitionPackingSolver lays decoupling caps out in a centered deterministic row", () => { + const partitionInputProblem: PartitionInputProblem = { + chipMap: { + C10: { + chipId: "C10", + pins: [], + size: { x: 2, y: 1 }, + availableRotations: [0], + }, + C2: { + chipId: "C2", + pins: [], + size: { x: 1, y: 1 }, + availableRotations: [0], + }, + C1: { + chipId: "C1", + pins: [], + size: { x: 3, y: 1 }, + availableRotations: [0], + }, + }, + chipPinMap: {}, + netMap: {}, + pinStrongConnMap: {}, + netConnMap: {}, + chipGap: 0.2, + partitionGap: 2, + decouplingCapsGap: 0.5, + partitionType: "decoupling_caps", + isPartition: true, + } + + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem, + pinIdToStronglyConnectedPins: {}, + }) + + solver.step() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.activeSubSolver).toBeNull() + expect(solver.layout?.chipPlacements.C1).toEqual({ + x: -2, + y: 0, + ccwRotationDegrees: 0, + }) + expect(solver.layout?.chipPlacements.C2).toEqual({ + x: 0.5, + y: 0, + ccwRotationDegrees: 0, + }) + expect(solver.layout?.chipPlacements.C10).toEqual({ + x: 2.5, + y: 0, + ccwRotationDegrees: 0, + }) +})