diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 88db103..d44366e 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -38,6 +38,13 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } override _step() { + if (this.partitionInputProblem.partitionType === "decoupling_caps") { + this.layout = this.createDecouplingCapLayout() + this.solved = true + this.activeSubSolver = null + return + } + // Initialize PackSolver2 if not already created if (!this.activeSubSolver) { const packInput = this.createPackInput() @@ -64,6 +71,133 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } } + private createDecouplingCapLayout(): OutputLayout { + const chipIds = Object.keys(this.partitionInputProblem.chipMap) + const direction = this.getDecouplingCapLayoutDirection(chipIds) + const sortedChipIds = this.sortDecouplingCapChipIds(chipIds, direction) + const gap = + this.partitionInputProblem.decouplingCapsGap ?? + this.partitionInputProblem.chipGap + const pitch = this.getDecouplingCapPitch(sortedChipIds, direction, gap) + const midpoint = (sortedChipIds.length - 1) / 2 + const chipPlacements: Record = {} + + for (let i = 0; i < sortedChipIds.length; i++) { + const chipId = sortedChipIds[i]! + const chip = this.partitionInputProblem.chipMap[chipId]! + const rotation = this.chooseDecouplingCapRotation(chip.availableRotations) + const centeredIndex = i - midpoint + + chipPlacements[chipId] = { + x: direction === "horizontal" ? centeredIndex * pitch : 0, + y: direction === "vertical" ? -centeredIndex * pitch : 0, + ccwRotationDegrees: rotation, + } + } + + return { + chipPlacements, + groupPlacements: {}, + } + } + + private getDecouplingCapLayoutDirection( + chipIds: ChipId[], + ): "horizontal" | "vertical" { + let xSideConnections = 0 + let ySideConnections = 0 + + for (const chipId of chipIds) { + for (const pin of this.getExternalStrongPinsForChip(chipId)) { + if (pin.side === "x-" || pin.side === "x+") xSideConnections++ + if (pin.side === "y-" || pin.side === "y+") ySideConnections++ + } + } + + return xSideConnections >= ySideConnections ? "vertical" : "horizontal" + } + + private sortDecouplingCapChipIds( + chipIds: ChipId[], + direction: "horizontal" | "vertical", + ): ChipId[] { + return [...chipIds].sort((chipIdA, chipIdB) => { + const keyA = this.getDecouplingCapSortKey(chipIdA, direction) + const keyB = this.getDecouplingCapSortKey(chipIdB, direction) + + if (keyA !== keyB) return keyA - keyB + return chipIdA.localeCompare(chipIdB, undefined, { numeric: true }) + }) + } + + private getDecouplingCapSortKey( + chipId: ChipId, + direction: "horizontal" | "vertical", + ): number { + const externalPins = this.getExternalStrongPinsForChip(chipId) + if (externalPins.length === 0) return 0 + + const coordinateSum = externalPins.reduce((sum, pin) => { + return sum + (direction === "vertical" ? -pin.offset.y : pin.offset.x) + }, 0) + + return coordinateSum / externalPins.length + } + + private getExternalStrongPinsForChip(chipId: ChipId): ChipPin[] { + const chip = this.partitionInputProblem.chipMap[chipId] + if (!chip) return [] + + const partitionPinIds = new Set( + Object.keys(this.partitionInputProblem.chipPinMap), + ) + const externalPins: ChipPin[] = [] + + for (const pinId of chip.pins) { + const stronglyConnectedPins = + this.pinIdToStronglyConnectedPins[pinId] ?? [] + + for (const connectedPin of stronglyConnectedPins) { + if (!partitionPinIds.has(connectedPin.pinId)) { + externalPins.push(connectedPin) + } + } + } + + return externalPins + } + + private getDecouplingCapPitch( + chipIds: ChipId[], + direction: "horizontal" | "vertical", + gap: number, + ): number { + const maxAxisSize = chipIds.reduce((maxSize, chipId) => { + const chip = this.partitionInputProblem.chipMap[chipId] + if (!chip) return maxSize + const rotation = this.chooseDecouplingCapRotation(chip.availableRotations) + const size = this.getRotatedChipSize(chip.size, rotation) + const axisSize = direction === "horizontal" ? size.x : size.y + return Math.max(maxSize, axisSize) + }, 0) + + return maxAxisSize + gap + } + + private chooseDecouplingCapRotation( + availableRotations?: Array<0 | 90 | 180 | 270>, + ): 0 | 90 | 180 | 270 { + if (!availableRotations || availableRotations.length === 0) return 0 + return availableRotations.includes(0) ? 0 : availableRotations[0]! + } + + private getRotatedChipSize( + size: { x: number; y: number }, + rotation: number, + ): { x: number; y: number } { + return rotation === 90 || rotation === 270 ? { x: size.y, y: size.x } : size + } + private createPackInput(): PackInput { // Fall back to filtered mapping (weak + strong) const pinToNetworkMap = createFilteredNetworkMapping({ diff --git a/pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx b/pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx index c0767bb..b1aafd6 100644 --- a/pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx +++ b/pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx @@ -1,7 +1,13 @@ import type { PackInput } from "calculate-packing" -import { LayoutPipelineDebugger } from "lib/components/LayoutPipelineDebugger" +import { lazy, Suspense } from "react" import type { InputProblem } from "lib/index" +const LayoutPipelineDebugger = lazy(() => + import("lib/components/LayoutPipelineDebugger").then((mod) => ({ + default: mod.LayoutPipelineDebugger, + })), +) + export const problem: InputProblem = { chipMap: { U3: { @@ -877,5 +883,9 @@ export const problem: InputProblem = { } export default function LayoutPipelineSolver06Page() { - return + return ( + + + + ) } diff --git a/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts b/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts new file mode 100644 index 0000000..1411e55 --- /dev/null +++ b/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts @@ -0,0 +1,125 @@ +import { expect, test } from "bun:test" +import { SingleInnerPartitionPackingSolver } from "lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver" +import type { + ChipId, + ChipPin, + NetId, + PartitionInputProblem, + PinId, +} from "lib/types/InputProblem" + +const makeCap = (chipId: ChipId) => ({ + chipId, + pins: [`${chipId}.1`, `${chipId}.2`], + size: { x: 0.53, y: 1.06 }, + availableRotations: [0] as Array<0 | 90 | 180 | 270>, +}) + +const makeCapPinMap = (chipIds: ChipId[]) => { + const chipPinMap: Record = {} + + for (const chipId of chipIds) { + chipPinMap[`${chipId}.1`] = { + pinId: `${chipId}.1`, + offset: { x: 0, y: 0.53 }, + side: "y+", + } + chipPinMap[`${chipId}.2`] = { + pinId: `${chipId}.2`, + offset: { x: 0, y: -0.53 }, + side: "y-", + } + } + + return chipPinMap +} + +const makeDecouplingCapPartition = ( + chipIds: ChipId[], +): PartitionInputProblem => { + const chipMap = Object.fromEntries( + chipIds.map((chipId) => [chipId, makeCap(chipId)]), + ) + const netConnMap: Record<`${PinId}-${NetId}`, boolean> = {} + + for (const chipId of chipIds) { + netConnMap[`${chipId}.1-VCC`] = true + netConnMap[`${chipId}.2-GND`] = true + } + + return { + chipMap, + chipPinMap: makeCapPinMap(chipIds), + netMap: { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + }, + pinStrongConnMap: {}, + netConnMap, + chipGap: 0.6, + decouplingCapsGap: 0.2, + partitionGap: 1.2, + isPartition: true, + partitionType: "decoupling_caps", + } +} + +test("SingleInnerPartitionPackingSolver creates a vertical decoupling capacitor column ordered by main-chip pins", () => { + const partitionInputProblem = makeDecouplingCapPartition([ + "C_LOW", + "C_TOP", + "C_MID", + ]) + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem, + pinIdToStronglyConnectedPins: { + "C_TOP.1": [{ pinId: "U1.1", offset: { x: -2, y: 2 }, side: "x-" }], + "C_MID.1": [{ pinId: "U1.2", offset: { x: -2, y: 0 }, side: "x-" }], + "C_LOW.1": [{ pinId: "U1.3", offset: { x: -2, y: -2 }, side: "x-" }], + }, + }) + + solver.solve() + + expect(solver.failed).toBe(false) + expect(solver.layout).not.toBeNull() + + const placements = solver.layout!.chipPlacements + expect(placements.C_TOP!.y).toBeGreaterThan(placements.C_MID!.y) + expect(placements.C_MID!.y).toBeGreaterThan(placements.C_LOW!.y) + expect(placements.C_TOP!.x).toBeCloseTo(0, 6) + expect(placements.C_MID!.x).toBeCloseTo(0, 6) + expect(placements.C_LOW!.x).toBeCloseTo(0, 6) + expect(placements.C_TOP!.y - placements.C_MID!.y).toBeCloseTo(1.26, 6) + expect(placements.C_MID!.y - placements.C_LOW!.y).toBeCloseTo(1.26, 6) +}) + +test("SingleInnerPartitionPackingSolver creates a horizontal decoupling capacitor row for y-side main-chip pins", () => { + const partitionInputProblem = makeDecouplingCapPartition([ + "C_RIGHT", + "C_LEFT", + "C_CENTER", + ]) + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem, + pinIdToStronglyConnectedPins: { + "C_LEFT.1": [{ pinId: "U1.1", offset: { x: -2, y: 2 }, side: "y+" }], + "C_CENTER.1": [{ pinId: "U1.2", offset: { x: 0, y: 2 }, side: "y+" }], + "C_RIGHT.1": [{ pinId: "U1.3", offset: { x: 2, y: 2 }, side: "y+" }], + }, + }) + + solver.solve() + + expect(solver.failed).toBe(false) + expect(solver.layout).not.toBeNull() + + const placements = solver.layout!.chipPlacements + expect(placements.C_LEFT!.x).toBeLessThan(placements.C_CENTER!.x) + expect(placements.C_CENTER!.x).toBeLessThan(placements.C_RIGHT!.x) + expect(placements.C_LEFT!.y).toBeCloseTo(0, 6) + expect(placements.C_CENTER!.y).toBeCloseTo(0, 6) + expect(placements.C_RIGHT!.y).toBeCloseTo(0, 6) + expect(placements.C_CENTER!.x - placements.C_LEFT!.x).toBeCloseTo(0.73, 6) + expect(placements.C_RIGHT!.x - placements.C_CENTER!.x).toBeCloseTo(0.73, 6) +})