Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions lib/solvers/PackInnerPartitionsSolver/DecouplingCapBankLayoutSolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* Lays out a decoupling-capacitor partition as a uniform bank:
*
* - All caps oriented with their VCC/positive pin facing y+ (top) and their
* GND/negative pin facing y- (bottom), matching datasheet convention.
* - Caps are placed side-by-side with uniform pitch (cap width + gap).
* - When there are more than `maxPerRow` caps the bank wraps into multiple
* balanced rows, keeping the VCC rail continuous.
*
* This is a synchronous, single-step solver because the placement is fully
* deterministic — no search needed.
*/

import { BaseSolver } from "../BaseSolver"
import type { OutputLayout, Placement } from "../../types/OutputLayout"
import type { PartitionInputProblem, ChipId } from "../../types/InputProblem"

const MAX_PER_ROW = 8

export class DecouplingCapBankLayoutSolver extends BaseSolver {
partitionInputProblem: PartitionInputProblem
layout: OutputLayout | null = null

constructor(partitionInputProblem: PartitionInputProblem) {
super()
this.partitionInputProblem = partitionInputProblem
}

override _step() {
const { chipMap, chipPinMap, netMap, netConnMap } =
this.partitionInputProblem
const gap = this.partitionInputProblem.decouplingCapsGap ?? 0.1

const chips = Object.values(chipMap)
if (chips.length === 0) {
this.layout = { chipPlacements: {}, groupPlacements: {} }
this.solved = true
return
}

// Sort caps deterministically
const sortedChips = [...chips].sort((a, b) =>
a.chipId.localeCompare(b.chipId, undefined, { numeric: true }),
)

// Figure out which pin of each cap is the VCC pin (isPositiveVoltageSource)
// and whether the cap needs to be flipped (rotated 180°)
const capRotations = new Map<ChipId, 0 | 180>()

for (const chip of sortedChips) {
const [pin1Id, pin2Id] = chip.pins
if (!pin1Id || !pin2Id) {
capRotations.set(chip.chipId, 0)
continue
}

// Find which net each pin connects to
const getNet = (pinId: string) => {
for (const key of Object.keys(netConnMap)) {
const [p, n] = key.split("-") as [string, string]
if (p === pinId) return netMap[n]
}
return undefined
}

const net1 = getNet(pin1Id)
const net2 = getNet(pin2Id)

// pin1 is typically "top" in default (0°) rotation.
// If pin1 is GND and pin2 is VCC, we need to flip (180°).
const pin1IsGnd = net1?.isGround ?? false
const pin2IsGnd = net2?.isGround ?? false

// Flip if pin1 is GND (meaning VCC is at bottom in default orientation)
capRotations.set(chip.chipId, pin1IsGnd && !pin2IsGnd ? 180 : 0)
}

// Use the first chip's dimensions as the uniform cap size
const refChip = sortedChips[0]!
const capW = refChip.size.x
const capH = refChip.size.y

const pitch = capW + gap
const perRow = Math.min(MAX_PER_ROW, sortedChips.length)
const rowCount = Math.ceil(sortedChips.length / perRow)
const rowPitch = capH + gap

const chipPlacements: Record<ChipId, Placement> = {}

for (let i = 0; i < sortedChips.length; i++) {
const chip = sortedChips[i]!
const col = i % perRow
const row = Math.floor(i / perRow)
const x = col * pitch
const y = -row * rowPitch
chipPlacements[chip.chipId] = {
x,
y,
ccwRotationDegrees: capRotations.get(chip.chipId) ?? 0,
}
}

this.layout = { chipPlacements, groupPlacements: {} }
this.solved = true
}

override getConstructorParams(): ConstructorParameters<
typeof DecouplingCapBankLayoutSolver
> {
return [this.partitionInputProblem]
}
}
34 changes: 26 additions & 8 deletions lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,22 @@

import type { GraphicsObject } from "graphics-debug"
import { BaseSolver } from "../BaseSolver"
import type { ChipPin, InputProblem, PinId } from "../../types/InputProblem"
import type {
ChipPin,
InputProblem,
PartitionInputProblem,
PinId,
} from "../../types/InputProblem"
import type { OutputLayout } from "../../types/OutputLayout"
import { SingleInnerPartitionPackingSolver } from "./SingleInnerPartitionPackingSolver"
import { DecouplingCapBankLayoutSolver } from "./DecouplingCapBankLayoutSolver"
import { stackGraphicsHorizontally } from "graphics-debug"

type PartitionSolver = (SingleInnerPartitionPackingSolver | DecouplingCapBankLayoutSolver) & {
layout: OutputLayout | null
visualize(): GraphicsObject
}

export type PackedPartition = {
inputProblem: InputProblem
layout: OutputLayout
Expand All @@ -19,11 +30,11 @@ export type PackedPartition = {
export class PackInnerPartitionsSolver extends BaseSolver {
partitions: InputProblem[]
packedPartitions: PackedPartition[] = []
completedSolvers: SingleInnerPartitionPackingSolver[] = []
activeSolver: SingleInnerPartitionPackingSolver | null = null
completedSolvers: PartitionSolver[] = []
activeSolver: PartitionSolver | null = null
currentPartitionIndex = 0

declare activeSubSolver: SingleInnerPartitionPackingSolver | null
declare activeSubSolver: PartitionSolver | null
pinIdToStronglyConnectedPins: Record<PinId, ChipPin[]>

constructor(params: {
Expand All @@ -45,10 +56,17 @@ export class PackInnerPartitionsSolver extends BaseSolver {
// If no active solver, create one for the current partition
if (!this.activeSolver) {
const currentPartition = this.partitions[this.currentPartitionIndex]!
this.activeSolver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: currentPartition,
pinIdToStronglyConnectedPins: this.pinIdToStronglyConnectedPins,
})
const partitionType = (currentPartition as PartitionInputProblem).partitionType
if (partitionType === "decoupling_caps") {
this.activeSolver = new DecouplingCapBankLayoutSolver(
currentPartition as PartitionInputProblem,
)
} else {
this.activeSolver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: currentPartition,
pinIdToStronglyConnectedPins: this.pinIdToStronglyConnectedPins,
})
}
this.activeSubSolver = this.activeSolver
}

Expand Down
105 changes: 105 additions & 0 deletions tests/PackInnerPartitionsSolver/DecouplingCapBankLayoutSolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { expect, test } from "bun:test"
import { DecouplingCapBankLayoutSolver } from "lib/solvers/PackInnerPartitionsSolver/DecouplingCapBankLayoutSolver"
import type { PartitionInputProblem } from "lib/types/InputProblem"

const makeProblem = (
capCount: number,
pin1IsGnd = false,
): PartitionInputProblem => {
const chipMap: PartitionInputProblem["chipMap"] = {}
const chipPinMap: PartitionInputProblem["chipPinMap"] = {}
const netConnMap: PartitionInputProblem["netConnMap"] = {}

for (let i = 1; i <= capCount; i++) {
const cid = `C${i}`
const p1 = `${cid}_P1`
const p2 = `${cid}_P2`
chipMap[cid] = {
chipId: cid,
pins: [p1, p2],
size: { x: 0.53, y: 1.1 },
isDecouplingCap: true,
}
chipPinMap[p1] = { pinId: p1, offset: { x: 0, y: 0.55 }, side: "top" }
chipPinMap[p2] = { pinId: p2, offset: { x: 0, y: -0.55 }, side: "bottom" }
netConnMap[`${p1}-${pin1IsGnd ? "GND" : "VCC"}`] = true
netConnMap[`${p2}-${pin1IsGnd ? "VCC" : "GND"}`] = true
}

return {
partitionType: "decoupling_caps",
chipMap,
chipPinMap,
netMap: {
VCC: { netId: "VCC", isPositiveVoltageSource: true },
GND: { netId: "GND", isGround: true },
},
pinStrongConnMap: {},
netConnMap,
chipGap: 0.2,
partitionGap: 2,
decouplingCapsGap: 0.1,
}
}

test("places 3 caps in a single row with correct pitch", () => {
const solver = new DecouplingCapBankLayoutSolver(makeProblem(3))
solver.solve()
expect(solver.solved).toBe(true)
const placements = solver.layout!.chipPlacements
const pitch = 0.53 + 0.1
expect(placements["C1"]!.x).toBeCloseTo(0)
expect(placements["C2"]!.x).toBeCloseTo(pitch)
expect(placements["C3"]!.x).toBeCloseTo(2 * pitch)
// all in same row
expect(placements["C1"]!.y).toBeCloseTo(0)
expect(placements["C2"]!.y).toBeCloseTo(0)
expect(placements["C3"]!.y).toBeCloseTo(0)
})

test("caps with VCC on pin1 have 0° rotation", () => {
const solver = new DecouplingCapBankLayoutSolver(makeProblem(2, false))
solver.solve()
const p = solver.layout!.chipPlacements
expect(p["C1"]!.ccwRotationDegrees).toBe(0)
expect(p["C2"]!.ccwRotationDegrees).toBe(0)
})

test("caps with GND on pin1 are rotated 180°", () => {
const solver = new DecouplingCapBankLayoutSolver(makeProblem(2, true))
solver.solve()
const p = solver.layout!.chipPlacements
expect(p["C1"]!.ccwRotationDegrees).toBe(180)
expect(p["C2"]!.ccwRotationDegrees).toBe(180)
})

test("wraps into two rows when more than 8 caps", () => {
const solver = new DecouplingCapBankLayoutSolver(makeProblem(10))
solver.solve()
expect(solver.solved).toBe(true)
const p = solver.layout!.chipPlacements
// First 8 caps in row 0 (y=0), caps 9-10 in row 1 (y negative)
expect(p["C1"]!.y).toBeCloseTo(0)
expect(p["C8"]!.y).toBeCloseTo(0)
const rowPitch = 1.1 + 0.1
expect(p["C9"]!.y).toBeCloseTo(-rowPitch)
expect(p["C10"]!.y).toBeCloseTo(-rowPitch)
// Row 1 starts at col 0
expect(p["C9"]!.x).toBeCloseTo(0)
})

test("empty partition produces empty layout", () => {
const solver = new DecouplingCapBankLayoutSolver({
partitionType: "decoupling_caps",
chipMap: {},
chipPinMap: {},
netMap: {},
pinStrongConnMap: {},
netConnMap: {},
chipGap: 0.2,
partitionGap: 2,
})
solver.solve()
expect(solver.solved).toBe(true)
expect(Object.keys(solver.layout!.chipPlacements).length).toBe(0)
})