diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 88db103..a8e8722 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -38,6 +38,15 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } override _step() { + if ( + !this.layout && + 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 +73,81 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } } + private createDecouplingCapsLayout(): OutputLayout { + const gap = + this.partitionInputProblem.decouplingCapsGap ?? + this.partitionInputProblem.chipGap + const chipIds = Object.keys(this.partitionInputProblem.chipMap).sort( + naturalCompare, + ) + const capItems = chipIds.map((chipId) => { + const chip = this.partitionInputProblem.chipMap[chipId]! + const rotation = this.getPreferredDecouplingCapRotation(chipId) + const footprint = + rotation === 90 || rotation === 270 + ? { x: chip.size.y, y: chip.size.x } + : chip.size + + return { chipId, rotation, footprint } + }) + + const totalWidth = + capItems.reduce((sum, item) => sum + item.footprint.x, 0) + + Math.max(0, capItems.length - 1) * gap + let cursorX = -totalWidth / 2 + const chipPlacements: Record = {} + + for (const item of capItems) { + chipPlacements[item.chipId] = { + x: cursorX + item.footprint.x / 2, + y: 0, + ccwRotationDegrees: item.rotation, + } + cursorX += item.footprint.x + gap + } + + return { + chipPlacements, + groupPlacements: {}, + } + } + + private getPreferredDecouplingCapRotation( + chipId: ChipId, + ): 0 | 90 | 180 | 270 { + const chip = this.partitionInputProblem.chipMap[chipId]! + const allowedRotations = chip.availableRotations ?? [0, 90, 180, 270] + const fallback = allowedRotations[0] ?? 0 + const positiveVoltagePin = chip.pins.find((pinId) => + this.isPinConnectedToPositiveVoltage(pinId), + ) + + if (!positiveVoltagePin) return fallback + + const pin = this.partitionInputProblem.chipPinMap[positiveVoltagePin] + if (!pin) return fallback + + const preferredRotation = pin.side === "y-" ? 180 : 0 + return allowedRotations.includes(preferredRotation) + ? preferredRotation + : fallback + } + + private isPinConnectedToPositiveVoltage(pinId: PinId): boolean { + for (const [connKey, connected] of Object.entries( + this.partitionInputProblem.netConnMap, + )) { + if (!connected) continue + const [connectedPinId, netId] = connKey.split("-") as [PinId, NetId] + if (connectedPinId !== pinId) continue + if (this.partitionInputProblem.netMap[netId]?.isPositiveVoltageSource) { + return true + } + } + + return false + } + private createPackInput(): PackInput { // Fall back to filtered mapping (weak + strong) const pinToNetworkMap = createFilteredNetworkMapping({ @@ -182,3 +266,7 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { return [this.partitionInputProblem] } } + +function naturalCompare(a: string, b: string): number { + return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }) +} diff --git a/package.json b/package.json index 1fd506e..6da5aff 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@types/bun": "latest", "bpc-graph": "^0.0.66", "calculate-packing": "^0.0.31", + "circuit-to-svg": "0.0.345", "circuit-json": "^0.0.226", "graphics-debug": "^0.0.64", "react-cosmos": "^7.0.0", diff --git a/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts b/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts new file mode 100644 index 0000000..db371d1 --- /dev/null +++ b/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts @@ -0,0 +1,91 @@ +import { expect, test } from "bun:test" +import { SingleInnerPartitionPackingSolver } from "../../lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver" +import type { PartitionInputProblem } from "../../lib/types/InputProblem" + +function createDecouplingCapsPartition(): PartitionInputProblem { + return { + chipMap: { + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 0.8, y: 0.4 }, + availableRotations: [0, 180], + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 1.2, y: 0.4 }, + availableRotations: [0, 180], + }, + C10: { + chipId: "C10", + pins: ["C10.1", "C10.2"], + size: { x: 1, y: 0.4 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: { + "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-" }, + "C10.1": { pinId: "C10.1", offset: { x: 0, y: 0.2 }, side: "y+" }, + "C10.2": { pinId: "C10.2", offset: { x: 0, y: -0.2 }, side: "y-" }, + }, + netMap: { + GND: { netId: "GND", isGround: true }, + VDD: { netId: "VDD", isPositiveVoltageSource: true }, + }, + pinStrongConnMap: {}, + netConnMap: { + "C1.1-VDD": true, + "C1.2-GND": true, + "C2.1-VDD": true, + "C2.2-GND": true, + "C10.1-VDD": true, + "C10.2-GND": true, + }, + chipGap: 0.2, + partitionGap: 1, + decouplingCapsGap: 0.3, + isPartition: true, + partitionType: "decoupling_caps", + } +} + +test("SingleInnerPartitionPackingSolver centers decoupling caps in a deterministic row", () => { + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: createDecouplingCapsPartition(), + pinIdToStronglyConnectedPins: {}, + }) + + solver.step() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.activeSubSolver).toBeUndefined() + expect(Object.keys(solver.layout!.chipPlacements)).toEqual([ + "C1", + "C2", + "C10", + ]) + expect(solver.layout!.chipPlacements.C1!.x).toBeCloseTo(-1.4) + expect(solver.layout!.chipPlacements.C2!.x).toBeCloseTo(-0.1) + expect(solver.layout!.chipPlacements.C10!.x).toBeCloseTo(1.3) + expect(solver.layout!.chipPlacements.C1!.y).toBe(0) + expect(solver.layout!.chipPlacements.C2!.y).toBe(0) + expect(solver.layout!.chipPlacements.C10!.y).toBe(0) +}) + +test("SingleInnerPartitionPackingSolver rotates caps so positive voltage pins face y+", () => { + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: createDecouplingCapsPartition(), + pinIdToStronglyConnectedPins: {}, + }) + + solver.step() + + expect(solver.layout!.chipPlacements.C1!.ccwRotationDegrees).toBe(180) + expect(solver.layout!.chipPlacements.C2!.ccwRotationDegrees).toBe(0) + expect(solver.layout!.chipPlacements.C10!.ccwRotationDegrees).toBe(0) +})