From 535402c138c2f4cb3d19122ad94c64d782879802 Mon Sep 17 00:00:00 2001 From: Chris <86233785+chriszlr@users.noreply.github.com> Date: Wed, 20 May 2026 12:00:19 +0200 Subject: [PATCH] fix: add decoupling cap fast path --- .../SingleInnerPartitionPackingSolver.ts | 62 +++++++ package.json | 1 + .../SingleInnerPartitionPackingSolver.test.ts | 164 ++++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 88db103..8877bda 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -38,6 +38,12 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } override _step() { + if (this.partitionInputProblem.partitionType === "decoupling_caps") { + this.layout = this.createDecouplingCapLayout() + this.solved = true + return + } + // Initialize PackSolver2 if not already created if (!this.activeSubSolver) { const packInput = this.createPackInput() @@ -64,6 +70,62 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } } + private createDecouplingCapLayout(): OutputLayout { + const chipEntries = Object.entries(this.partitionInputProblem.chipMap).sort( + ([a], [b]) => a.localeCompare(b, undefined, { numeric: true }), + ) + const gap = + this.partitionInputProblem.decouplingCapsGap ?? + this.partitionInputProblem.chipGap + const chipWidths = chipEntries.map(([, chip]) => + this.getChipWidthForRotation( + chip.size, + this.getDecouplingCapRotation(chip), + ), + ) + const totalWidth = + chipWidths.reduce((sum, width) => sum + width, 0) + + Math.max(0, chipWidths.length - 1) * gap + const chipPlacements: Record = {} + let nextLeftEdge = -totalWidth / 2 + + for (let i = 0; i < chipEntries.length; i++) { + const [chipId, chip] = chipEntries[i]! + const rotation = this.getDecouplingCapRotation(chip) + const width = chipWidths[i]! + + chipPlacements[chipId] = { + x: nextLeftEdge + width / 2, + y: 0, + ccwRotationDegrees: rotation, + } + nextLeftEdge += width + gap + } + + return { + chipPlacements, + groupPlacements: {}, + } + } + + private getDecouplingCapRotation( + chip: PartitionInputProblem["chipMap"][string], + ): 0 | 90 | 180 | 270 { + const availableRotations = chip.availableRotations ?? [0] + + if (availableRotations.includes(0)) return 0 + if (availableRotations.includes(180)) return 180 + + return availableRotations[0] ?? 0 + } + + private getChipWidthForRotation( + size: { x: number; y: number }, + rotation: 0 | 90 | 180 | 270, + ): number { + return rotation === 90 || rotation === 270 ? size.y : size.x + } + private createPackInput(): PackInput { // Fall back to filtered mapping (weak + strong) const pinToNetworkMap = createFilteredNetworkMapping({ diff --git a/package.json b/package.json index 1fd506e..6da5aff 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@types/bun": "latest", "bpc-graph": "^0.0.66", "calculate-packing": "^0.0.31", + "circuit-to-svg": "0.0.345", "circuit-json": "^0.0.226", "graphics-debug": "^0.0.64", "react-cosmos": "^7.0.0", diff --git a/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts b/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts new file mode 100644 index 0000000..2da8057 --- /dev/null +++ b/tests/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.test.ts @@ -0,0 +1,164 @@ +import { expect, test } from "bun:test" +import { SingleInnerPartitionPackingSolver } from "../../lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver" +import type { PartitionInputProblem } from "../../lib/types/InputProblem" + +const createDecouplingCapProblem = ( + overrides: Partial = {}, +): PartitionInputProblem => ({ + chipMap: { + C10: { + chipId: "C10", + pins: ["C10.1", "C10.2"], + size: { x: 1, y: 0.5 }, + availableRotations: [0, 180], + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 2, y: 0.5 }, + availableRotations: [0, 180], + }, + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 1, y: 0.5 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: { + "C10.1": { pinId: "C10.1", offset: { x: 0, y: 0.25 }, side: "y+" }, + "C10.2": { pinId: "C10.2", offset: { x: 0, y: -0.25 }, side: "y-" }, + "C2.1": { pinId: "C2.1", offset: { x: 0, y: 0.25 }, side: "y+" }, + "C2.2": { pinId: "C2.2", offset: { x: 0, y: -0.25 }, side: "y-" }, + "C1.1": { pinId: "C1.1", offset: { x: 0, y: 0.25 }, side: "y+" }, + "C1.2": { pinId: "C1.2", offset: { x: 0, y: -0.25 }, side: "y-" }, + }, + netMap: {}, + pinStrongConnMap: {}, + netConnMap: {}, + chipGap: 0.2, + partitionGap: 2, + decouplingCapsGap: 0.5, + isPartition: true, + partitionType: "decoupling_caps", + ...overrides, +}) + +const solveSinglePartition = (inputProblem: PartitionInputProblem) => { + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: inputProblem, + pinIdToStronglyConnectedPins: {}, + }) + solver.solve() + expect(solver.failed).toBe(false) + expect(solver.solved).toBe(true) + expect(solver.layout).toBeDefined() + + return solver.layout! +} + +test("decoupling cap partitions are placed in natural-id order", () => { + const layout = solveSinglePartition(createDecouplingCapProblem()) + + expect(layout.chipPlacements.C1!.x).toBeLessThan(layout.chipPlacements.C2!.x) + expect(layout.chipPlacements.C2!.x).toBeLessThan(layout.chipPlacements.C10!.x) +}) + +test("decoupling cap row uses decouplingCapsGap between chip edges", () => { + const layout = solveSinglePartition(createDecouplingCapProblem()) + const c1 = layout.chipPlacements.C1! + const c2 = layout.chipPlacements.C2! + const c10 = layout.chipPlacements.C10! + + expect(c2.x - c1.x).toBeCloseTo(1 / 2 + 0.5 + 2 / 2) + expect(c10.x - c2.x).toBeCloseTo(2 / 2 + 0.5 + 1 / 2) +}) + +test("decoupling cap row falls back to chipGap when no decouplingCapsGap is set", () => { + const layout = solveSinglePartition( + createDecouplingCapProblem({ + decouplingCapsGap: undefined, + chipGap: 0.3, + }), + ) + const c1 = layout.chipPlacements.C1! + const c2 = layout.chipPlacements.C2! + const c10 = layout.chipPlacements.C10! + + expect(c2.x - c1.x).toBeCloseTo(1 / 2 + 0.3 + 2 / 2) + expect(c10.x - c2.x).toBeCloseTo(2 / 2 + 0.3 + 1 / 2) +}) + +test("decoupling cap row is centered around the partition origin", () => { + const layout = solveSinglePartition(createDecouplingCapProblem()) + const leftEdge = layout.chipPlacements.C1!.x - 1 / 2 + const rightEdge = layout.chipPlacements.C10!.x + 1 / 2 + + expect(leftEdge).toBeCloseTo(-rightEdge) +}) + +test("single decoupling cap is centered at the origin", () => { + const layout = solveSinglePartition( + createDecouplingCapProblem({ + chipMap: { + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 1, y: 0.5 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: { + "C1.1": { pinId: "C1.1", offset: { x: 0, y: 0.25 }, side: "y+" }, + "C1.2": { pinId: "C1.2", offset: { x: 0, y: -0.25 }, side: "y-" }, + }, + }), + ) + + expect(layout.chipPlacements.C1).toEqual({ + x: 0, + y: 0, + ccwRotationDegrees: 0, + }) +}) + +test("decoupling cap row uses rotation-aware width", () => { + const layout = solveSinglePartition( + createDecouplingCapProblem({ + chipMap: { + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 2, y: 0.5 }, + availableRotations: [90], + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 2, y: 0.5 }, + availableRotations: [90], + }, + }, + }), + ) + + expect(layout.chipPlacements.C1!.ccwRotationDegrees).toBe(90) + expect(layout.chipPlacements.C2!.ccwRotationDegrees).toBe(90) + expect(layout.chipPlacements.C2!.x - layout.chipPlacements.C1!.x).toBeCloseTo( + 0.5 / 2 + 0.5 + 0.5 / 2, + ) +}) + +test("non-decoupling partitions still use the generic pack solver", () => { + const inputProblem = createDecouplingCapProblem({ + partitionType: "default", + }) + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: inputProblem, + pinIdToStronglyConnectedPins: {}, + }) + + solver.step() + + expect(solver.activeSubSolver).toBeDefined() +})