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 @@ -13,6 +13,7 @@ import type {
ChipId,
NetId,
ChipPin,
Chip,
PartitionInputProblem,
} from "../../types/InputProblem"
import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputProblem"
Expand All @@ -38,6 +39,15 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver {
}

override _step() {
// Decoupling-cap partitions use a dedicated linear-row layout instead of
// the generic packer. Caps line up horizontally with each cap rotated so
// its positive-voltage pin faces the same direction across the row.
if (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()
Expand All @@ -64,6 +74,126 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver {
}
}

/**
* Lay out the decoupling capacitors in a horizontal row, centered around the
* partition origin. Each cap is rotated (0° or 180°) so that the pin
* connected to a positive-voltage net consistently faces the same direction
* (y+ when possible) — that's the convention seen in hand-drawn schematics
* and is what makes a row of decoupling caps look uniform.
*/
private createDecouplingCapsLayout(): OutputLayout {
const { chipMap } = this.partitionInputProblem
const chips = Object.values(chipMap)
const gap =
this.partitionInputProblem.decouplingCapsGap ??
this.partitionInputProblem.chipGap ??
0.1

// Pick a target side for the positive-voltage pin once for the partition,
// by majority vote of the caps' natural (rotation=0) layout. This keeps
// already-correctly-oriented caps unchanged when possible.
const naturalSides = chips
.map((c) => this.getPositivePinNaturalSide(c))
.filter((s): s is "y+" | "y-" | "x+" | "x-" => s != null)
const targetSide = this.pickTargetSide(naturalSides)

const chipPlacements: Record<ChipId, Placement> = {}
let currentX = 0
for (const chip of chips) {
const rotation = this.pickRotationForPositivePin(chip, targetSide)
chipPlacements[chip.chipId] = {
x: currentX + chip.size.x / 2,
y: 0,
ccwRotationDegrees: rotation,
}
currentX += chip.size.x + gap
}

// Center the row around x=0.
const totalWidth = Math.max(currentX - gap, 0)
const offsetX = -totalWidth / 2
for (const id in chipPlacements) {
chipPlacements[id]!.x += offsetX
}

return { chipPlacements, groupPlacements: {} }
}

/**
* Side (y+/y-/x+/x-) of the chip where the positive-voltage pin sits at
* rotation 0. Returns null if no pin is connected to a positive-voltage net.
*/
private getPositivePinNaturalSide(
chip: Chip,
): "y+" | "y-" | "x+" | "x-" | null {
const { chipPinMap, netMap, netConnMap } = this.partitionInputProblem
for (const pinId of chip.pins) {
const pin = chipPinMap[pinId]
if (!pin) continue
for (const netId in netMap) {
if (!netMap[netId]?.isPositiveVoltageSource) continue
if (netConnMap[`${pinId}-${netId}` as `${PinId}-${NetId}`]) {
return pin.side
}
}
}
return null
}

/**
* Pick the consistent target side for positive-voltage pins. Prefers y+ when
* supported, otherwise picks the most common natural side so we minimize the
* number of caps that need to be flipped from their input orientation.
*/
private pickTargetSide(
naturalSides: Array<"y+" | "y-" | "x+" | "x-">,
): "y+" | "y-" | "x+" | "x-" {
if (naturalSides.length === 0) return "y+"
const counts: Record<string, number> = {}
for (const s of naturalSides) counts[s] = (counts[s] ?? 0) + 1
// Default to y+ unless another side has strictly more votes.
let best: "y+" | "y-" | "x+" | "x-" = "y+"
let bestCount = counts["y+"] ?? 0
for (const s of ["y-", "x+", "x-"] as const) {
if ((counts[s] ?? 0) > bestCount) {
best = s
bestCount = counts[s]!
}
}
return best
}

/**
* Pick a rotation (in degrees, ccw) such that the cap's positive-voltage pin
* ends up on the target side after rotation. Falls back to the first
* availableRotation when there's no positive-voltage pin to align.
*/
private pickRotationForPositivePin(
chip: Chip,
targetSide: "y+" | "y-" | "x+" | "x-",
): number {
const naturalSide = this.getPositivePinNaturalSide(chip)
const allowed = chip.availableRotations ?? [0, 90, 180, 270]
if (naturalSide == null) return allowed[0] ?? 0

const rotateSide = (
side: "y+" | "y-" | "x+" | "x-",
deg: 0 | 90 | 180 | 270,
): "y+" | "y-" | "x+" | "x-" => {
// CCW rotation of a side label.
const order = ["x+", "y+", "x-", "y-"] as const
const idx = order.indexOf(side)
const steps = (deg / 90) | 0
return order[(idx + steps) % 4]!
}

for (const rot of allowed) {
if (rotateSide(naturalSide, rot) === targetSide) return rot
}
// Target unreachable with this cap's rotation set; keep its first allowed.
return allowed[0] ?? 0
}

private createPackInput(): PackInput {
// Fall back to filtered mapping (weak + strong)
const pinToNetworkMap = createFilteredNetworkMapping({
Expand Down
146 changes: 146 additions & 0 deletions tests/PackInnerPartitionsSolver/DecouplingCapsLayout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { test, expect } from "bun:test"
import { SingleInnerPartitionPackingSolver } from "lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver"
import type { PartitionInputProblem } from "lib/types/InputProblem"

/**
* Regression test for tscircuit/matchpack#15 - Specialized Layout for
* Decoupling Capacitors.
*
* For partitions tagged `decoupling_caps`, the solver should:
*
* 1. Lay caps out in a horizontal row, centered around x=0
* 2. Pick each cap's rotation so the pin connected to the positive-voltage
* net consistently faces the same direction across the row (i.e. all
* VCC pins up, or all VCC pins down — never mixed within one row)
* 3. Use the configured `decouplingCapsGap` for spacing
*/

const baseProblem = (): PartitionInputProblem => ({
chipMap: {},
chipPinMap: {},
netMap: {
VCC: { netId: "VCC", isPositiveVoltageSource: true },
GND: { netId: "GND", isGround: true },
},
pinStrongConnMap: {},
netConnMap: {},
chipGap: 0.1,
partitionGap: 0.2,
decouplingCapsGap: 0.3,
isPartition: true,
partitionType: "decoupling_caps",
})

test("decoupling-caps partition lays caps in a centered horizontal row", () => {
const problem = baseProblem()
problem.chipMap = {
C1: {
chipId: "C1",
pins: ["C1.1", "C1.2"],
size: { x: 0.5, y: 1.0 },
isDecouplingCap: true,
availableRotations: [0, 180],
},
C2: {
chipId: "C2",
pins: ["C2.1", "C2.2"],
size: { x: 0.5, y: 1.0 },
isDecouplingCap: true,
availableRotations: [0, 180],
},
C3: {
chipId: "C3",
pins: ["C3.1", "C3.2"],
size: { x: 0.5, y: 1.0 },
isDecouplingCap: true,
availableRotations: [0, 180],
},
}
// C1 and C3 have VCC on y+ (top). C2 has VCC on y- (bottom).
// The solver should rotate C2 by 180° so all VCC pins face y+.
problem.chipPinMap = {
"C1.1": { pinId: "C1.1", offset: { x: 0, y: 0.5 }, side: "y+" },
"C1.2": { pinId: "C1.1", offset: { x: 0, y: -0.5 }, side: "y-" },
"C2.1": { pinId: "C2.1", offset: { x: 0, y: 0.5 }, side: "y+" },
"C2.2": { pinId: "C2.2", offset: { x: 0, y: -0.5 }, side: "y-" },
"C3.1": { pinId: "C3.1", offset: { x: 0, y: 0.5 }, side: "y+" },
"C3.2": { pinId: "C3.2", offset: { x: 0, y: -0.5 }, side: "y-" },
}
problem.netConnMap = {
"C1.1-VCC": true,
"C1.2-GND": true,
"C2.2-VCC": true, // C2 wired backwards — VCC on bottom pin
"C2.1-GND": true,
"C3.1-VCC": true,
"C3.2-GND": true,
}

const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: problem,
pinIdToStronglyConnectedPins: {},
})
solver.solve()

expect(solver.solved).toBe(true)
expect(solver.layout).not.toBeNull()
const placements = solver.layout!.chipPlacements

// All three caps placed
expect(Object.keys(placements).sort()).toEqual(["C1", "C2", "C3"])

// All at y=0 (linear row)
expect(placements.C1!.y).toBe(0)
expect(placements.C2!.y).toBe(0)
expect(placements.C3!.y).toBe(0)

// Row is centered: leftmost x and rightmost x are symmetric around 0
const xs = [placements.C1!.x, placements.C2!.x, placements.C3!.x].sort(
(a, b) => a - b,
)
expect(xs[0]! + xs[2]!).toBeCloseTo(0, 5)

// Consistent gap (size 0.5 + gap 0.3 = 0.8 between centers)
expect(xs[1]! - xs[0]!).toBeCloseTo(0.8, 5)
expect(xs[2]! - xs[1]!).toBeCloseTo(0.8, 5)

// C1 and C3 keep their natural rotation (VCC already on y+).
expect(placements.C1!.ccwRotationDegrees).toBe(0)
expect(placements.C3!.ccwRotationDegrees).toBe(0)

// C2 is flipped 180° to align its VCC pin to y+.
expect(placements.C2!.ccwRotationDegrees).toBe(180)
})

test("decoupling-caps with no positive-voltage pin info falls back to first available rotation", () => {
const problem = baseProblem()
// Strip the voltage-source flag so we have no orientation signal.
problem.netMap = {
NET_A: { netId: "NET_A" },
NET_B: { netId: "NET_B" },
}
problem.chipMap = {
C1: {
chipId: "C1",
pins: ["C1.1", "C1.2"],
size: { x: 0.5, y: 1.0 },
availableRotations: [0, 180],
},
}
problem.chipPinMap = {
"C1.1": { pinId: "C1.1", offset: { x: 0, y: 0.5 }, side: "y+" },
"C1.2": { pinId: "C1.2", offset: { x: 0, y: -0.5 }, side: "y-" },
}
problem.netConnMap = {
"C1.1-NET_A": true,
"C1.2-NET_B": true,
}

const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: problem,
pinIdToStronglyConnectedPins: {},
})
solver.solve()

expect(solver.solved).toBe(true)
expect(solver.layout!.chipPlacements.C1!.ccwRotationDegrees).toBe(0)
})
Loading