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
Expand Up @@ -144,6 +144,12 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver {
private createLayoutFromPackingResult(
packedComponents: PackSolver2["packedComponents"],
): OutputLayout {
// For decoupling capacitor partitions, arrange in a clean horizontal row
// instead of using the general-purpose packer which produces messy layouts
if (this.partitionInputProblem.partitionType === "decoupling_caps") {
return this.createDecouplingCapsLinearLayout(packedComponents)
}

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

for (const packedComponent of packedComponents) {
Expand All @@ -165,6 +171,58 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver {
}
}

/**
* Creates a clean linear horizontal layout for decoupling capacitor partitions.
* Capacitors are sorted by chip ID for deterministic ordering and placed
* in a single horizontal row with consistent spacing.
*/
private createDecouplingCapsLinearLayout(
packedComponents: PackSolver2["packedComponents"],
): OutputLayout {
const chipPlacements: Record<string, Placement> = {}

// Sort by chip ID for deterministic layout
const sorted = [...packedComponents].sort((a, b) =>
a.componentId.localeCompare(b.componentId),
)

// Calculate total width and gaps for centering
const gap =
this.partitionInputProblem.decouplingCapsGap ??
this.partitionInputProblem.chipGap

let totalWidth = 0
const chipWidths: number[] = []
for (const comp of sorted) {
// Find the body pad to get chip dimensions
const bodyPad = comp.pads.find((p) => p.padId.endsWith("_body"))
const width = bodyPad?.size.x ?? 1
chipWidths.push(width)
totalWidth += width
if (chipWidths.length > 1) totalWidth += gap
}

// Start from the left, centered around x=0
let xOffset = -totalWidth / 2
for (let i = 0; i < sorted.length; i++) {
const comp = sorted[i]
const chipWidth = chipWidths[i]

chipPlacements[comp.componentId] = {
x: xOffset + chipWidth / 2,
y: 0,
ccwRotationDegrees: 0,
}

xOffset += chipWidth + gap
}

return {
chipPlacements,
groupPlacements: {},
}
}

override visualize(): GraphicsObject {
if (this.activeSubSolver && !this.solved) {
return this.activeSubSolver.visualize()
Expand Down
96 changes: 96 additions & 0 deletions tests/DecouplingCapLayout/DecouplingCapLayout01.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { expect, test } from "bun:test"
import { SingleInnerPartitionPackingSolver } from "lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver"
import type { PartitionInputProblem } from "lib/types/InputProblem"

test("SingleInnerPartitionPackingSolver places decoupling caps in horizontal row", () => {
// Create a simple decoupling caps partition with 3 capacitors
const problem: PartitionInputProblem = {
isPartition: true,
partitionType: "decoupling_caps",
chipMap: {
C1: {
chipId: "C1",
pins: ["C1.pin1", "C1.pin2"],
size: { x: 1, y: 0.5 },
availableRotations: [0, 180],
},
C2: {
chipId: "C2",
pins: ["C2.pin1", "C2.pin2"],
size: { x: 1, y: 0.5 },
availableRotations: [0, 180],
},
C3: {
chipId: "C3",
pins: ["C3.pin1", "C3.pin2"],
size: { x: 1, y: 0.5 },
availableRotations: [0, 180],
},
},
chipPinMap: {
"C1.pin1": { pinId: "C1.pin1", offset: { x: 0, y: 0 }, side: "left" },
"C1.pin2": { pinId: "C1.pin2", offset: { x: 1, y: 0 }, side: "right" },
"C2.pin1": { pinId: "C2.pin1", offset: { x: 0, y: 0 }, side: "left" },
"C2.pin2": { pinId: "C2.pin2", offset: { x: 1, y: 0 }, side: "right" },
"C3.pin1": { pinId: "C3.pin1", offset: { x: 0, y: 0 }, side: "left" },
"C3.pin2": { pinId: "C3.pin2", offset: { x: 1, y: 0 }, side: "right" },
},
netMap: {
VCC: { netId: "VCC", isPositiveVoltageSource: true },
GND: { netId: "GND", isGround: true },
},
pinStrongConnMap: {},
netConnMap: {
"C1.pin1-VCC": true,
"C1.pin2-GND": true,
"C2.pin1-VCC": true,
"C2.pin2-GND": true,
"C3.pin1-VCC": true,
"C3.pin2-GND": true,
},
chipGap: 0.2,
partitionGap: 2,
decouplingCapsGap: 0.3,
}

const pinIdToStronglyConnectedPins: Record<string, any> = {}
const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: problem,
pinIdToStronglyConnectedPins,
})

// Solve
solver.solve()

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

const layout = solver.layout
expect(layout).toBeDefined()
expect(layout.chipPlacements["C1"]).toBeDefined()
expect(layout.chipPlacements["C2"]).toBeDefined()
expect(layout.chipPlacements["C3"]).toBeDefined()

// All capacitors should be on the same Y axis (horizontal row)
const y1 = layout.chipPlacements["C1"]!.y
const y2 = layout.chipPlacements["C2"]!.y
const y3 = layout.chipPlacements["C3"]!.y
expect(Math.abs(y1 - y2)).toBeLessThan(0.001)
expect(Math.abs(y2 - y3)).toBeLessThan(0.001)

// C1 should be to the left of C2, C2 to the left of C3 (sorted by ID)
const x1 = layout.chipPlacements["C1"]!.x
const x2 = layout.chipPlacements["C2"]!.x
const x3 = layout.chipPlacements["C3"]!.x
expect(x1).toBeLessThan(x2)
expect(x2).toBeLessThan(x3)

// All capacitors should have 0 rotation (they're symmetric)
expect(layout.chipPlacements["C1"]!.ccwRotationDegrees).toBe(0)
expect(layout.chipPlacements["C2"]!.ccwRotationDegrees).toBe(0)
expect(layout.chipPlacements["C3"]!.ccwRotationDegrees).toBe(0)

// Layout should be centered around x=0
const avgX = (x1 + x2 + x3) / 3
expect(Math.abs(avgX)).toBeLessThan(0.1)
})
Loading