From 52b60fd803b51d973b003ae77cfdd44d45a0c7f8 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Mon, 11 May 2026 20:55:32 -0700 Subject: [PATCH] feat: add deterministic row layout for decoupling cap partitions When partitionType === "decoupling_caps", bypass PackSolver2 and place caps in a centered horizontal row sorted by chipId. Uses decouplingCapsGap (falling back to chipGap) for spacing between components. Adds focused tests for row centering, natural ordering, gap parameter priority, single-cap case, and regression guard for default partitions. Co-Authored-By: Claude Sonnet 4.6 --- .../SingleInnerPartitionPackingSolver.ts | 40 ++++ .../DecouplingCapRow.test.ts | 183 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 tests/SingleInnerPartitionPackingSolver/DecouplingCapRow.test.ts diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 88db103..bff4bf1 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -38,6 +38,15 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } override _step() { + if ( + this.partitionInputProblem.partitionType === "decoupling_caps" && + !this.activeSubSolver + ) { + this.layout = this.createDecouplingCapRowLayout() + this.solved = true + return + } + // Initialize PackSolver2 if not already created if (!this.activeSubSolver) { const packInput = this.createPackInput() @@ -64,6 +73,37 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } } + private createDecouplingCapRowLayout(): OutputLayout { + const gap = + this.partitionInputProblem.decouplingCapsGap ?? + this.partitionInputProblem.chipGap + + // Sort by chipId for deterministic ordering + const chips = Object.values(this.partitionInputProblem.chipMap).sort( + (a, b) => a.chipId.localeCompare(b.chipId), + ) + + // Compute total row width + const totalWidth = chips.reduce((sum, chip, i) => { + return sum + chip.size.x + (i < chips.length - 1 ? gap : 0) + }, 0) + + const chipPlacements: Record = {} + let x = -totalWidth / 2 + + for (const chip of chips) { + x += chip.size.x / 2 + chipPlacements[chip.chipId] = { + x, + y: 0, + ccwRotationDegrees: 0, + } + x += chip.size.x / 2 + gap + } + + return { chipPlacements, groupPlacements: {} } + } + private createPackInput(): PackInput { // Fall back to filtered mapping (weak + strong) const pinToNetworkMap = createFilteredNetworkMapping({ diff --git a/tests/SingleInnerPartitionPackingSolver/DecouplingCapRow.test.ts b/tests/SingleInnerPartitionPackingSolver/DecouplingCapRow.test.ts new file mode 100644 index 0000000..9ce9d14 --- /dev/null +++ b/tests/SingleInnerPartitionPackingSolver/DecouplingCapRow.test.ts @@ -0,0 +1,183 @@ +import { test, expect } from "bun:test" +import { SingleInnerPartitionPackingSolver } from "../../lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver" +import type { PartitionInputProblem } from "../../lib/types/InputProblem" + +function makeDecouplingCapPartition( + chipIds: string[], + capSize = { x: 1.0, y: 0.5 }, + gap = 0.2, + decouplingCapsGap?: number, +): PartitionInputProblem { + const chipMap: PartitionInputProblem["chipMap"] = {} + const chipPinMap: PartitionInputProblem["chipPinMap"] = {} + + for (const id of chipIds) { + chipMap[id] = { + chipId: id, + pins: [`${id}.1`, `${id}.2`], + size: capSize, + isDecouplingCap: true, + availableRotations: [0, 90, 180, 270], + } + chipPinMap[`${id}.1`] = { + pinId: `${id}.1`, + offset: { x: -capSize.x / 2, y: 0 }, + side: "x-", + } + chipPinMap[`${id}.2`] = { + pinId: `${id}.2`, + offset: { x: capSize.x / 2, y: 0 }, + side: "x+", + } + } + + return { + chipMap, + chipPinMap, + netMap: { GND: { netId: "GND", isGround: true } }, + pinStrongConnMap: {}, + netConnMap: Object.fromEntries( + chipIds.flatMap((id) => [[`${id}.2-GND`, true as const]]), + ), + chipGap: gap, + partitionGap: 2, + decouplingCapsGap, + isPartition: true, + partitionType: "decoupling_caps", + } +} + +test("decoupling caps arranged in centered horizontal row", () => { + const problem = makeDecouplingCapPartition(["C1", "C2", "C3"]) + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: problem, + pinIdToStronglyConnectedPins: {}, + }) + + solver.step() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.layout).toBeDefined() + + const placements = solver.layout!.chipPlacements + expect(placements["C1"]).toBeDefined() + expect(placements["C2"]).toBeDefined() + expect(placements["C3"]).toBeDefined() + + // All caps at y=0 (horizontal row) + expect(placements["C1"]!.y).toBeCloseTo(0) + expect(placements["C2"]!.y).toBeCloseTo(0) + expect(placements["C3"]!.y).toBeCloseTo(0) + + // Sorted by chipId: C1 leftmost, C3 rightmost + expect(placements["C1"]!.x).toBeLessThan(placements["C2"]!.x) + expect(placements["C2"]!.x).toBeLessThan(placements["C3"]!.x) + + // Centered: average x should be ~0 + const avgX = + (placements["C1"]!.x + placements["C2"]!.x + placements["C3"]!.x) / 3 + expect(avgX).toBeCloseTo(0, 5) + + // All at rotation 0 + expect(placements["C1"]!.ccwRotationDegrees).toBe(0) + expect(placements["C2"]!.ccwRotationDegrees).toBe(0) + expect(placements["C3"]!.ccwRotationDegrees).toBe(0) +}) + +test("decoupling cap spacing uses decouplingCapsGap when provided", () => { + const chipGap = 0.2 + const decouplingCapsGap = 0.05 + const capSize = { x: 1.0, y: 0.5 } + + const problem = makeDecouplingCapPartition( + ["C1", "C2"], + capSize, + chipGap, + decouplingCapsGap, + ) + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: problem, + pinIdToStronglyConnectedPins: {}, + }) + + solver.step() + + expect(solver.solved).toBe(true) + const placements = solver.layout!.chipPlacements + + // Expected: C1 at x = -(1.0/2 + decouplingCapsGap/2) = -(0.5 + 0.025) = -0.525 + // C2 at x = +(0.525) + const expectedSpacing = capSize.x + decouplingCapsGap + const actualSpacing = placements["C2"]!.x - placements["C1"]!.x + expect(actualSpacing).toBeCloseTo(expectedSpacing, 5) +}) + +test("decoupling cap spacing falls back to chipGap when decouplingCapsGap is absent", () => { + const chipGap = 0.3 + const capSize = { x: 1.0, y: 0.5 } + + const problem = makeDecouplingCapPartition(["C1", "C2"], capSize, chipGap) + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: problem, + pinIdToStronglyConnectedPins: {}, + }) + + solver.step() + + expect(solver.solved).toBe(true) + const placements = solver.layout!.chipPlacements + + const expectedSpacing = capSize.x + chipGap + const actualSpacing = placements["C2"]!.x - placements["C1"]!.x + expect(actualSpacing).toBeCloseTo(expectedSpacing, 5) +}) + +test("non-decoupling partition still uses generic PackSolver2", () => { + const problem: PartitionInputProblem = { + chipMap: { + U1: { + chipId: "U1", + pins: ["U1.1", "U1.2"], + size: { x: 2, y: 2 }, + }, + }, + chipPinMap: { + "U1.1": { pinId: "U1.1", offset: { x: -0.5, y: 0 }, side: "x-" }, + "U1.2": { pinId: "U1.2", offset: { x: 0.5, y: 0 }, side: "x+" }, + }, + netMap: {}, + pinStrongConnMap: {}, + netConnMap: {}, + chipGap: 0.2, + partitionGap: 2, + isPartition: true, + partitionType: "default", + } + + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: problem, + pinIdToStronglyConnectedPins: {}, + }) + + // Should not be immediately solved (goes through PackSolver2) + expect(solver.solved).toBe(false) + solver.solve() + expect(solver.solved).toBe(true) + expect(solver.layout).toBeDefined() +}) + +test("single decoupling cap centered at origin", () => { + const problem = makeDecouplingCapPartition(["C5"]) + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: problem, + pinIdToStronglyConnectedPins: {}, + }) + + solver.step() + + expect(solver.solved).toBe(true) + const placements = solver.layout!.chipPlacements + expect(placements["C5"]!.x).toBeCloseTo(0, 5) + expect(placements["C5"]!.y).toBeCloseTo(0, 5) +})