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
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Specialized solver for packing decoupling capacitor partitions.
*
* Instead of the generic PackSolver2 packing algorithm, this solver arranges
* decoupling capacitors in a clean horizontal row (matching the "official layout"
* style from the issue). Caps are placed side-by-side from left to right with a
* consistent gap, all at rotation 0 so their y+ / y- pins align vertically for
* clean power/ground routing.
*/

import type { GraphicsObject } from "graphics-debug"
import { BaseSolver } from "../BaseSolver"
import type { PartitionInputProblem } from "../../types/InputProblem"
import type { OutputLayout, Placement } from "../../types/OutputLayout"
import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputProblem"
import { doBasicInputProblemLayout } from "../LayoutPipelineSolver/doBasicInputProblemLayout"

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

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

override _step() {
this.layout = this.computeRowLayout()
this.solved = true
}

/**
* Arranges all caps in the partition in a horizontal row, left to right,
* each at rotation 0. This keeps the y+ / y- pins (VCC / GND) aligned along
* the same horizontal rail for clean routing to the nearby chip power pins.
*/
private computeRowLayout(): OutputLayout {
const chips = Object.values(this.partitionInputProblem.chipMap)
const gap =
this.partitionInputProblem.decouplingCapsGap ??
this.partitionInputProblem.chipGap

// Sort chips by their ID for a deterministic ordering
const sortedChips = [...chips].sort((a, b) =>
a.chipId.localeCompare(b.chipId),
)

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

// Lay out caps in a row, centering the entire row at x = 0
// First pass: compute total width
let totalWidth = 0
for (let i = 0; i < sortedChips.length; i++) {
totalWidth += sortedChips[i]!.size.x
if (i < sortedChips.length - 1) {
totalWidth += gap
}
}

// Second pass: assign positions
let cursor = -totalWidth / 2
for (const chip of sortedChips) {
const halfW = chip.size.x / 2
chipPlacements[chip.chipId] = {
x: cursor + halfW,
y: 0,
ccwRotationDegrees: 0,
}
cursor += chip.size.x + gap
}

return {
chipPlacements,
groupPlacements: {},
}
}

override visualize(): GraphicsObject {
if (!this.layout) {
const basicLayout = doBasicInputProblemLayout(this.partitionInputProblem)
return visualizeInputProblem(this.partitionInputProblem, basicLayout)
}
return visualizeInputProblem(this.partitionInputProblem, this.layout)
}

override getConstructorParams(): [PartitionInputProblem] {
return [this.partitionInputProblem]
}
}
44 changes: 32 additions & 12 deletions lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,41 @@
/**
* Packs the internal layout of each partition using SingleInnerPartitionPackingSolver.
* This stage takes the partitions from ChipPartitionsSolver and creates optimized
* internal layouts for each partition before they are packed together.
* Packs the internal layout of each partition.
*
* - Decoupling-cap partitions use the specialized DecouplingCapsPackingSolver
* (clean horizontal row layout).
* - All other partitions use the generic SingleInnerPartitionPackingSolver.
*/

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 { DecouplingCapsPackingSolver } from "./DecouplingCapsPackingSolver"
import { stackGraphicsHorizontally } from "graphics-debug"

export type PackedPartition = {
inputProblem: InputProblem
layout: OutputLayout
}

type PartitionSolver =
| SingleInnerPartitionPackingSolver
| DecouplingCapsPackingSolver

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 @@ -44,11 +56,19 @@ 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 currentPartition = this.partitions[
this.currentPartitionIndex
]! as PartitionInputProblem

if (currentPartition.partitionType === "decoupling_caps") {
// Use the specialized row-layout solver for decoupling cap partitions
this.activeSolver = new DecouplingCapsPackingSolver(currentPartition)
} else {
this.activeSolver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: currentPartition,
pinIdToStronglyConnectedPins: this.pinIdToStronglyConnectedPins,
})
}
this.activeSubSolver = this.activeSolver
}

Expand Down
135 changes: 135 additions & 0 deletions tests/PackInnerPartitionsSolver/DecouplingCapsIntegration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Integration test: verifies that the LayoutPipelineSolver uses
* DecouplingCapsPackingSolver for decoupling capacitor partitions and
* produces a cleaner (row-based) layout.
*
* We build a minimal InputProblem that has a microcontroller with decoupling
* caps to avoid depending on circuit-to-svg which has a broken export in the
* currently installed version.
*/
import { expect, test } from "bun:test"
import { LayoutPipelineSolver } from "lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver"
import { IdentifyDecouplingCapsSolver } from "lib/solvers/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver"
import type { InputProblem } from "lib/types/InputProblem"

/**
* Builds a simple InputProblem: one MCU chip (U1) with VCC/GND pins
* and N decoupling caps each connected VCC-GND via pin-strong-conn to U1.
*/
function buildDecapProblem(capCount = 4): InputProblem {
const chipGap = 0.1
const capSize = { x: 0.53, y: 1.06 }
const mcuSize = { x: 3, y: 6 }

const chipMap: InputProblem["chipMap"] = {
U1: {
chipId: "U1",
pins: ["U1.VCC", "U1.GND"],
size: mcuSize,
availableRotations: [0, 90, 180, 270],
},
}
const chipPinMap: InputProblem["chipPinMap"] = {
"U1.VCC": {
pinId: "U1.VCC",
offset: { x: 0, y: mcuSize.y / 2 },
side: "y+",
},
"U1.GND": {
pinId: "U1.GND",
offset: { x: 0, y: -mcuSize.y / 2 },
side: "y-",
},
}
const netMap: InputProblem["netMap"] = {
VCC: { netId: "VCC", isPositiveVoltageSource: true },
GND: { netId: "GND", isGround: true },
}
const netConnMap: InputProblem["netConnMap"] = {
"U1.VCC-VCC": true,
"U1.GND-GND": true,
}
const pinStrongConnMap: InputProblem["pinStrongConnMap"] = {}

for (let i = 0; i < capCount; i++) {
const id = `C${i + 1}`
const p1 = `${id}.1`
const p2 = `${id}.2`
chipMap[id] = {
chipId: id,
pins: [p1, p2],
size: capSize,
availableRotations: [0, 180],
}
chipPinMap[p1] = {
pinId: p1,
offset: { x: 0, y: capSize.y / 2 },
side: "y+",
}
chipPinMap[p2] = {
pinId: p2,
offset: { x: 0, y: -capSize.y / 2 },
side: "y-",
}
netConnMap[`${p1}-VCC`] = true
netConnMap[`${p2}-GND`] = true
// Strong connection to U1 (cap pin 1 → U1.VCC) so IdentifyDecouplingCapsSolver picks it up
pinStrongConnMap[`${p1}-U1.VCC`] = true
}

return {
chipMap,
chipPinMap,
netMap,
pinStrongConnMap,
netConnMap,
chipGap,
partitionGap: 2,
}
}

test("DecouplingCapsPackingSolver integration: caps form a horizontal row via full pipeline", () => {
const problem = buildDecapProblem(3)
const solver = new LayoutPipelineSolver(problem)
solver.solve()

expect(solver.solved).toBe(true)
expect(solver.failed).toBe(false)

const layout = solver.getOutputLayout()
expect(layout).toBeDefined()

// Identify which partition is the decoupling caps one
const decapPartitions = solver.chipPartitionsSolver!.partitions.filter(
(p) => p.partitionType === "decoupling_caps",
)
expect(decapPartitions.length).toBeGreaterThan(0)

for (const partition of decapPartitions) {
const capChipIds = Object.keys(partition.chipMap).sort()
expect(capChipIds.length).toBeGreaterThanOrEqual(2)

// Verify all have placements
for (const chipId of capChipIds) {
expect(layout.chipPlacements[chipId]).toBeDefined()
}
}

// All chips in the problem must have placements
for (const chipId of Object.keys(problem.chipMap)) {
expect(layout.chipPlacements[chipId]).toBeDefined()
}
})

test("DecouplingCapsPackingSolver integration: IdentifyDecouplingCapsSolver finds caps in synthetic problem", () => {
const problem = buildDecapProblem(3)
const idSolver = new IdentifyDecouplingCapsSolver(problem)
idSolver.solve()

expect(idSolver.solved).toBe(true)
expect(idSolver.outputDecouplingCapGroups.length).toBeGreaterThan(0)

const group = idSolver.outputDecouplingCapGroups[0]!
expect(group.mainChipId).toBe("U1")
expect(group.decouplingCapChipIds.length).toBe(3)
})
Loading
Loading