diff --git a/lib/solvers/ChipPartitionsSolver/ChipPartitionsSolver.ts b/lib/solvers/ChipPartitionsSolver/ChipPartitionsSolver.ts index 8335b60..588f505 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,16 @@ export class ChipPartitionsSolver extends BaseSolver { } } + if (opts?.partitionType === "decoupling_caps") { + this.addInferredDecouplingCapNetConnections({ + relevantPinIds, + netPair: opts.decouplingCapNetPair ?? null, + originalProblem, + relevantNetIds, + netConnMap, + }) + } + for (const netId of relevantNetIds) { if (originalProblem.netMap[netId]) { netMap[netId] = originalProblem.netMap[netId] @@ -260,6 +278,50 @@ export class ChipPartitionsSolver extends BaseSolver { } } + private addInferredDecouplingCapNetConnections({ + relevantPinIds, + netPair, + originalProblem, + relevantNetIds, + netConnMap, + }: { + relevantPinIds: Set + netPair: [NetId, NetId] | null + originalProblem: InputProblem + relevantNetIds: Set + netConnMap: Record + }) { + if (!netPair) return + const decouplingNetIds = new Set(netPair) + + for (const [connKey, isConnected] of Object.entries( + originalProblem.pinStrongConnMap, + )) { + if (!isConnected) continue + const [pin1Id, pin2Id] = connKey.split("-") as [PinId, PinId] + + const partitionPinId = relevantPinIds.has(pin1Id) + ? pin1Id + : relevantPinIds.has(pin2Id) + ? pin2Id + : null + const externalPinId = partitionPinId === pin1Id ? pin2Id : pin1Id + + if (!partitionPinId || relevantPinIds.has(externalPinId)) continue + + for (const [netConnKey, isNetConnected] of Object.entries( + originalProblem.netConnMap, + )) { + if (!isNetConnected) continue + const [pinId, netId] = netConnKey.split("-") as [PinId, NetId] + if (pinId !== externalPinId || !decouplingNetIds.has(netId)) continue + + relevantNetIds.add(netId) + netConnMap[`${partitionPinId}-${netId}`] = true + } + } + } + override visualize(): GraphicsObject { if (this.partitions.length === 0) { return super.visualize() diff --git a/lib/solvers/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver.ts b/lib/solvers/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver.ts index 5c503e0..135036b 100644 --- a/lib/solvers/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver.ts +++ b/lib/solvers/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver.ts @@ -51,8 +51,11 @@ export class IdentifyDecouplingCapsSolver extends BaseSolver { private isTwoPinRestrictedRotation(chip: Chip): boolean { if (chip.pins.length !== 2) return false - // Must be restricted to 0/180 or a single fixed orientation - if (!chip.availableRotations) return false + // Missing rotation metadata means the imported schematic has a fixed orientation. + if (!chip.availableRotations) return true + + if (chip.isDecouplingCap) return true + const allowed = new Set<0 | 180>([0, 180]) return ( chip.availableRotations.length > 0 && @@ -113,6 +116,19 @@ export class IdentifyDecouplingCapsSolver extends BaseSolver { return best ? best.id : null } + private isGroundNet(netId: NetId): boolean { + const net = this.inputProblem.netMap[netId] + return Boolean(net?.isGround) || /(^|[_-])gnd($|[_-])/i.test(netId) + } + + private isPositiveVoltageNet(netId: NetId): boolean { + const net = this.inputProblem.netMap[netId] + return ( + Boolean(net?.isPositiveVoltageSource) || + /(^v\d|vcc|vdd|vbat|vbus|usb_vdd|[_-]v\d)/i.test(netId) + ) + } + /** Get all net IDs connected to a pin */ private getNetIdsForPin(pinId: PinId): Set { const nets = new Set() @@ -123,6 +139,24 @@ export class IdentifyDecouplingCapsSolver extends BaseSolver { const [p, n] = connKey.split("-") as [PinId, NetId] if (p === pinId) nets.add(n) } + + for (const [connKey, connected] of Object.entries( + this.inputProblem.pinStrongConnMap, + )) { + if (!connected) continue + const [a, b] = connKey.split("-") as [PinId, PinId] + const connectedPinId = a === pinId ? b : b === pinId ? a : null + if (!connectedPinId) continue + + for (const [netConnKey, isNetConnected] of Object.entries( + this.inputProblem.netConnMap, + )) { + if (!isNetConnected) continue + const [p, n] = netConnKey.split("-") as [PinId, NetId] + if (p === connectedPinId) nets.add(n) + } + } + return nets } @@ -190,11 +224,9 @@ export class IdentifyDecouplingCapsSolver extends BaseSolver { // Ensure the net pair corresponds to a true decoupling capacitor: // one net must be ground and the other a positive voltage source const [n1, n2] = netPair - const net1 = this.inputProblem.netMap[n1] - const net2 = this.inputProblem.netMap[n2] const isDecouplingNetPair = - (net1?.isGround && net2?.isPositiveVoltageSource) || - (net2?.isGround && net1?.isPositiveVoltageSource) + (this.isGroundNet(n1) && this.isPositiveVoltageNet(n2)) || + (this.isGroundNet(n2) && this.isPositiveVoltageNet(n1)) if (!isDecouplingNetPair) return this.addToGroup(mainChipId, netPair, currentChip.chipId) diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 88db103..8d6b9cf 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -38,6 +38,12 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } override _step() { + if (this.partitionInputProblem.partitionType === "decoupling_caps") { + this.layout = this.createDecouplingCapsLayout() + this.solved = true + return + } + // Initialize PackSolver2 if not already created if (!this.activeSubSolver) { const packInput = this.createPackInput() @@ -64,6 +70,59 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } } + private createDecouplingCapsLayout(): OutputLayout { + const chipPlacements: Record = {} + const gap = + this.partitionInputProblem.decouplingCapsGap ?? + this.partitionInputProblem.chipGap + + const chips = Object.values(this.partitionInputProblem.chipMap) + .map((chip) => ({ + chip, + rotation: this.getPreferredDecouplingCapRotation( + chip.availableRotations, + ), + })) + .sort((a, b) => + a.chip.chipId.localeCompare(b.chip.chipId, undefined, { + numeric: true, + sensitivity: "base", + }), + ) + + const widths = chips.map(({ chip, rotation }) => + rotation === 90 || rotation === 270 ? chip.size.y : chip.size.x, + ) + const totalWidth = + widths.reduce((sum, width) => sum + width, 0) + + Math.max(0, chips.length - 1) * gap + + let cursorX = -totalWidth / 2 + + for (const [index, { chip, rotation }] of chips.entries()) { + const width = widths[index]! + chipPlacements[chip.chipId] = { + x: cursorX + width / 2, + y: 0, + ccwRotationDegrees: rotation, + } + cursorX += width + gap + } + + return { + chipPlacements, + groupPlacements: {}, + } + } + + private getPreferredDecouplingCapRotation( + availableRotations: Array<0 | 90 | 180 | 270> = [0, 90, 180, 270], + ): 0 | 90 | 180 | 270 { + if (availableRotations.includes(0)) return 0 + if (availableRotations.includes(180)) return 180 + return availableRotations[0] ?? 0 + } + private createPackInput(): PackInput { // Fall back to filtered mapping (weak + strong) const pinToNetworkMap = createFilteredNetworkMapping({ diff --git a/tests/ChipPartitionsSolver.test.ts b/tests/ChipPartitionsSolver.test.ts index d8eb08c..05d8b6c 100644 --- a/tests/ChipPartitionsSolver.test.ts +++ b/tests/ChipPartitionsSolver.test.ts @@ -188,3 +188,76 @@ test("ChipPartitionsSolver visualization contains partition components", () => { expect(visualization.rects?.length).toBeGreaterThan(0) expect(visualization.texts?.length).toBeGreaterThan(0) }) + +test("ChipPartitionsSolver preserves inferred decoupling cap nets in cap partitions", () => { + const inputProblem: InputProblem = { + chipMap: { + U1: { + chipId: "U1", + pins: ["U1.1", "U1.2"], + size: { x: 2, y: 2 }, + }, + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 0.5, y: 1 }, + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 0.5, y: 1 }, + }, + }, + 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, y: 0.5 }, side: "y+" }, + "C1.2": { pinId: "C1.2", offset: { x: 0, y: -0.5 }, side: "y-" }, + "C2.1": { pinId: "C2.1", offset: { x: 0, y: 0.5 }, side: "y+" }, + "C2.2": { pinId: "C2.2", offset: { x: 0, y: -0.5 }, side: "y-" }, + }, + netMap: { + GND: { netId: "GND", isGround: true }, + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + }, + pinStrongConnMap: { + "U1.1-C1.1": true, + "C1.1-U1.1": true, + "U1.2-C2.1": true, + "C2.1-U1.2": true, + }, + netConnMap: { + "U1.1-VCC": true, + "U1.2-VCC": true, + "C1.2-GND": true, + "C2.2-GND": true, + }, + chipGap: 0.2, + partitionGap: 2, + } + + const solver = new ChipPartitionsSolver({ + inputProblem, + decouplingCapGroups: [ + { + decouplingCapGroupId: "decap_group_U1__GND__VCC", + mainChipId: "U1", + netPair: ["GND", "VCC"], + decouplingCapChipIds: ["C1", "C2"], + }, + ], + }) + solver.solve() + + const decapPartition = solver.partitions.find( + (partition) => partition.partitionType === "decoupling_caps", + ) + + expect(decapPartition).toBeDefined() + expect(decapPartition!.netConnMap["C1.1-VCC"]).toBe(true) + expect(decapPartition!.netConnMap["C2.1-VCC"]).toBe(true) + expect(decapPartition!.netConnMap["C1.2-GND"]).toBe(true) + expect(decapPartition!.netConnMap["C2.2-GND"]).toBe(true) + expect(decapPartition!.netMap.VCC).toBeDefined() + expect(decapPartition!.netMap.GND).toBeDefined() +}) diff --git a/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts b/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts index 615957c..6a718e5 100644 --- a/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts +++ b/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts @@ -1,8 +1,57 @@ import { test, expect } from "bun:test" import { IdentifyDecouplingCapsSolver } from "../../lib/solvers/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver" -import { problem } from "../../pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx" +import type { InputProblem } from "../../lib/types/InputProblem" -test("IdentifyDecouplingCapsSolver identifies decoupling capacitor groups from LayoutPipelineSolver06", () => { +const problem: InputProblem = { + chipMap: { + U1: { + chipId: "U1", + pins: ["U1.1", "U1.2"], + size: { x: 2, y: 2 }, + availableRotations: [0, 90, 180, 270], + }, + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 0.5, y: 1 }, + availableRotations: [0], + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 0.5, y: 1 }, + 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, y: 0.5 }, side: "y+" }, + "C1.2": { pinId: "C1.2", offset: { x: 0, y: -0.5 }, side: "y-" }, + "C2.1": { pinId: "C2.1", offset: { x: 0, y: 0.5 }, side: "y+" }, + "C2.2": { pinId: "C2.2", offset: { x: 0, y: -0.5 }, side: "y-" }, + }, + netMap: { + GND: { netId: "GND", isGround: true }, + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + }, + pinStrongConnMap: { + "U1.1-C1.1": true, + "C1.1-U1.1": true, + "U1.2-C2.1": true, + "C2.1-U1.2": true, + }, + netConnMap: { + "U1.1-VCC": true, + "U1.2-VCC": true, + "C1.2-GND": true, + "C2.2-GND": true, + }, + chipGap: 0.2, + partitionGap: 1, +} + +test("IdentifyDecouplingCapsSolver identifies decoupling capacitor groups", () => { const solver = new IdentifyDecouplingCapsSolver(problem) solver.solve() diff --git a/tests/LayoutPipelineSolver/RP2040Circuit.test.ts b/tests/LayoutPipelineSolver/RP2040Circuit.test.ts index fc82ecb..6ad77f3 100644 --- a/tests/LayoutPipelineSolver/RP2040Circuit.test.ts +++ b/tests/LayoutPipelineSolver/RP2040Circuit.test.ts @@ -96,6 +96,15 @@ test("RP2040Circuit complete pipeline execution", () => { expect(solver.packInnerPartitionsSolver?.solved).toBe(true) expect(solver.partitionPackingSolver?.solved).toBe(true) + expect( + solver.identifyDecouplingCapsSolver?.outputDecouplingCapGroups.length, + ).toBeGreaterThan(0) + expect( + solver.chipPartitions?.some( + (partition) => partition.partitionType === "decoupling_caps", + ), + ).toBe(true) + // Test getOutputLayout method const outputLayout = solver.getOutputLayout() expect(outputLayout).toBeDefined() diff --git a/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts b/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts new file mode 100644 index 0000000..3821a6e --- /dev/null +++ b/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts @@ -0,0 +1,93 @@ +import { expect, test } from "bun:test" +import { SingleInnerPartitionPackingSolver } from "../../lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver" +import type { PartitionInputProblem } from "../../lib/types/InputProblem" + +const createDecouplingCapPartition = (): PartitionInputProblem => ({ + chipMap: { + C10: { + chipId: "C10", + pins: ["C10.1", "C10.2"], + size: { x: 0.55, y: 0.32 }, + availableRotations: [0, 180], + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 0.45, y: 0.28 }, + availableRotations: [0, 180], + }, + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 0.5, y: 0.3 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: { + "C10.1": { pinId: "C10.1", offset: { x: 0, y: -0.16 }, side: "y-" }, + "C10.2": { pinId: "C10.2", offset: { x: 0, y: 0.16 }, side: "y+" }, + "C2.1": { pinId: "C2.1", offset: { x: 0, y: -0.14 }, side: "y-" }, + "C2.2": { pinId: "C2.2", offset: { x: 0, y: 0.14 }, side: "y+" }, + "C1.1": { pinId: "C1.1", offset: { x: 0, y: -0.15 }, side: "y-" }, + "C1.2": { pinId: "C1.2", offset: { x: 0, y: 0.15 }, side: "y+" }, + }, + netMap: { + GND: { netId: "GND", isGround: true }, + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + }, + pinStrongConnMap: {}, + netConnMap: { + "C10.1-GND": true, + "C10.2-VCC": true, + "C2.1-GND": true, + "C2.2-VCC": true, + "C1.1-GND": true, + "C1.2-VCC": true, + }, + chipGap: 0.1, + partitionGap: 1, + decouplingCapsGap: 0.25, + isPartition: true, + partitionType: "decoupling_caps", +}) + +test("SingleInnerPartitionPackingSolver lays decoupling caps out in a deterministic non-overlapping row", () => { + const partitionInputProblem = createDecouplingCapPartition() + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem, + pinIdToStronglyConnectedPins: {}, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.layout).toBeDefined() + + const placements = solver.layout!.chipPlacements + const orderedChipIds = Object.entries(placements) + .sort(([, a], [, b]) => a.x - b.x) + .map(([chipId]) => chipId) + + expect(orderedChipIds).toEqual(["C1", "C2", "C10"]) + + for (const placement of Object.values(placements)) { + expect(placement.y).toBe(0) + expect(placement.ccwRotationDegrees).toBe(0) + } + + for (let i = 0; i < orderedChipIds.length - 1; i++) { + const leftChipId = orderedChipIds[i]! + const rightChipId = orderedChipIds[i + 1]! + const leftChip = partitionInputProblem.chipMap[leftChipId]! + const rightChip = partitionInputProblem.chipMap[rightChipId]! + const leftPlacement = placements[leftChipId]! + const rightPlacement = placements[rightChipId]! + const edgeGap = + rightPlacement.x - + rightChip.size.x / 2 - + (leftPlacement.x + leftChip.size.x / 2) + + expect(edgeGap).toBeCloseTo(partitionInputProblem.decouplingCapsGap!, 6) + } +})