From 203e817b82eface6a13e81ed992b88885827442c Mon Sep 17 00:00:00 2001 From: Gimyoonsoo Date: Tue, 12 May 2026 15:21:34 +0900 Subject: [PATCH 1/3] Improve decoupling cap main-pin layout --- .../SingleInnerPartitionPackingSolver.ts | 145 +++++++++++++++++- .../decoupling-cap-main-pin-layout.test.ts | 110 +++++++++++++ 2 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 tests/pack-inner-partitions-solver/decoupling-cap-main-pin-layout.test.ts diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 88db103..9ead561 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -11,7 +11,7 @@ import type { InputProblem, PinId, ChipId, - NetId, + Chip, ChipPin, PartitionInputProblem, } from "../../types/InputProblem" @@ -22,6 +22,48 @@ import { doBasicInputProblemLayout } from "../LayoutPipelineSolver/doBasicInputP const PIN_SIZE = 0.1 +type LayoutAxis = "x" | "y" + +const compareNaturalChipIds = (a: ChipId, b: ChipId) => { + const aParts = a.match(/\d+|\D+/g) ?? [a] + const bParts = b.match(/\d+|\D+/g) ?? [b] + const partCount = Math.min(aParts.length, bParts.length) + + for (let i = 0; i < partCount; i++) { + const aPart = aParts[i]! + const bPart = bParts[i]! + const aNumber = Number(aPart) + const bNumber = Number(bPart) + const aIsNumber = Number.isInteger(aNumber) + const bIsNumber = Number.isInteger(bNumber) + + if (aIsNumber && bIsNumber && aNumber !== bNumber) { + return aNumber - bNumber + } + + if (aPart !== bPart) { + return aPart.localeCompare(bPart) + } + } + + return aParts.length - bParts.length +} + +const getChipIdFromPinId = (pinId: PinId): ChipId => + pinId.split(".")[0] ?? pinId + +const getPreferredRotation = (chip: Chip): 0 | 90 | 180 | 270 => { + if (!chip.availableRotations?.length) return 0 + return chip.availableRotations.includes(0) ? 0 : chip.availableRotations[0]! +} + +const getRotatedSize = (chip: Chip, rotation: number) => { + if (rotation === 90 || rotation === 270) { + return { x: chip.size.y, y: chip.size.x } + } + return chip.size +} + export class SingleInnerPartitionPackingSolver extends BaseSolver { partitionInputProblem: PartitionInputProblem layout: OutputLayout | null = null @@ -38,6 +80,13 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } override _step() { + if (this.partitionInputProblem.partitionType === "decoupling_caps") { + this.layout = this.createDecouplingCapsLayout() + this.solved = true + this.activeSubSolver = null + return + } + // Initialize PackSolver2 if not already created if (!this.activeSubSolver) { const packInput = this.createPackInput() @@ -64,6 +113,100 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } } + private createDecouplingCapsLayout(): OutputLayout { + const entries = Object.entries(this.partitionInputProblem.chipMap).map( + ([chipId, chip]) => { + const connectedMainPin = this.getStronglyConnectedExternalPin( + chipId, + chip, + ) + const rotation = getPreferredRotation(chip) + const rotatedSize = getRotatedSize(chip, rotation) + + return { + chipId, + connectedMainPin, + rotation, + rotatedSize, + } + }, + ) + + const externalPinSideCounts = entries.reduce( + (counts, entry) => { + if (entry.connectedMainPin?.side.startsWith("x")) counts.x += 1 + if (entry.connectedMainPin?.side.startsWith("y")) counts.y += 1 + return counts + }, + { x: 0, y: 0 }, + ) + const layoutAxis: LayoutAxis = + externalPinSideCounts.x > externalPinSideCounts.y ? "y" : "x" + + entries.sort((a, b) => { + const aHasMainPin = a.connectedMainPin ? 1 : 0 + const bHasMainPin = b.connectedMainPin ? 1 : 0 + if (aHasMainPin !== bHasMainPin) return bHasMainPin - aHasMainPin + + const aCoordinate = + layoutAxis === "x" + ? (a.connectedMainPin?.offset.x ?? 0) + : (a.connectedMainPin?.offset.y ?? 0) + const bCoordinate = + layoutAxis === "x" + ? (b.connectedMainPin?.offset.x ?? 0) + : (b.connectedMainPin?.offset.y ?? 0) + + if (aCoordinate !== bCoordinate) return aCoordinate - bCoordinate + return compareNaturalChipIds(a.chipId, b.chipId) + }) + + const gap = + this.partitionInputProblem.decouplingCapsGap ?? + this.partitionInputProblem.chipGap + const totalSpan = + entries.reduce((sum, entry) => { + return sum + entry.rotatedSize[layoutAxis] + }, 0) + + Math.max(0, entries.length - 1) * gap + const chipPlacements: Record = {} + let cursor = -totalSpan / 2 + + for (const entry of entries) { + const span = entry.rotatedSize[layoutAxis] + const center = cursor + span / 2 + + chipPlacements[entry.chipId] = { + x: layoutAxis === "x" ? center : 0, + y: layoutAxis === "y" ? center : 0, + ccwRotationDegrees: entry.rotation, + } + + cursor += span + gap + } + + return { + chipPlacements, + groupPlacements: {}, + } + } + + private getStronglyConnectedExternalPin( + chipId: ChipId, + chip: Chip, + ): ChipPin | null { + for (const pinId of chip.pins) { + for (const connectedPin of this.pinIdToStronglyConnectedPins[pinId] ?? + []) { + if (getChipIdFromPinId(connectedPin.pinId) !== chipId) { + return connectedPin + } + } + } + + return null + } + private createPackInput(): PackInput { // Fall back to filtered mapping (weak + strong) const pinToNetworkMap = createFilteredNetworkMapping({ diff --git a/tests/pack-inner-partitions-solver/decoupling-cap-main-pin-layout.test.ts b/tests/pack-inner-partitions-solver/decoupling-cap-main-pin-layout.test.ts new file mode 100644 index 0000000..a3457e5 --- /dev/null +++ b/tests/pack-inner-partitions-solver/decoupling-cap-main-pin-layout.test.ts @@ -0,0 +1,110 @@ +import { expect, test } from "bun:test" +import { SingleInnerPartitionPackingSolver } from "../../lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver" +import type { + ChipPin, + PartitionInputProblem, + PinId, +} from "../../lib/types/InputProblem" + +const makeDecouplingCapPartition = (): PartitionInputProblem => ({ + chipMap: { + C10: { + chipId: "C10", + pins: ["C10.1", "C10.2"], + size: { x: 1, y: 0.5 }, + availableRotations: [0], + }, + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 1, y: 0.5 }, + availableRotations: [0], + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 1, y: 0.5 }, + availableRotations: [0], + }, + }, + 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-" }, + "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-" }, + "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-" }, + }, + netMap: { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + }, + pinStrongConnMap: {}, + netConnMap: { + "C10.1-VCC": true, + "C10.2-GND": true, + "C1.1-VCC": true, + "C1.2-GND": true, + "C2.1-VCC": true, + "C2.2-GND": true, + }, + chipGap: 0.6, + decouplingCapsGap: 0.25, + partitionGap: 1.2, + isPartition: true, + partitionType: "decoupling_caps", +}) + +const makeMainPin = ( + pinId: PinId, + offset: { x: number; y: number }, + side: ChipPin["side"], +): ChipPin => ({ + pinId, + offset, + side, +}) + +test("decoupling caps connected to x-side main pins are stacked by main pin y order", () => { + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: makeDecouplingCapPartition(), + pinIdToStronglyConnectedPins: { + "C10.1": [makeMainPin("U1.10", { x: 2, y: 2 }, "x+")], + "C1.1": [makeMainPin("U1.1", { x: 2, y: -2 }, "x+")], + "C2.1": [makeMainPin("U1.2", { x: 2, y: 0 }, "x+")], + }, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + const placements = solver.layout!.chipPlacements + + expect(placements.C1!.x).toBeCloseTo(0) + expect(placements.C2!.x).toBeCloseTo(0) + expect(placements.C10!.x).toBeCloseTo(0) + expect(placements.C1!.y).toBeLessThan(placements.C2!.y) + expect(placements.C2!.y).toBeLessThan(placements.C10!.y) +}) + +test("decoupling caps connected to y-side main pins are laid out by main pin x order", () => { + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: makeDecouplingCapPartition(), + pinIdToStronglyConnectedPins: { + "C10.1": [makeMainPin("U1.10", { x: 2, y: 2 }, "y+")], + "C1.1": [makeMainPin("U1.1", { x: -2, y: 2 }, "y+")], + "C2.1": [makeMainPin("U1.2", { x: 0, y: 2 }, "y+")], + }, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + const placements = solver.layout!.chipPlacements + + expect(placements.C1!.y).toBeCloseTo(0) + expect(placements.C2!.y).toBeCloseTo(0) + expect(placements.C10!.y).toBeCloseTo(0) + expect(placements.C1!.x).toBeLessThan(placements.C2!.x) + expect(placements.C2!.x).toBeLessThan(placements.C10!.x) +}) From 0ff168ac4378a6aaa87174281912d2093f8a679a Mon Sep 17 00:00:00 2001 From: Gimyoonsoo Date: Tue, 12 May 2026 15:53:36 +0900 Subject: [PATCH 2/3] Address decoupling cap layout review --- .../SingleInnerPartitionPackingSolver.ts | 79 ++++++++++++++----- .../decoupling-cap-main-pin-layout.test.ts | 78 ++++++++++++++++++ 2 files changed, 137 insertions(+), 20 deletions(-) diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 9ead561..af8932e 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -3,22 +3,22 @@ * Uses a packing algorithm to arrange chips and their connections within the partition. */ -import type { GraphicsObject } from "graphics-debug" import { type PackInput, PackSolver2 } from "calculate-packing" -import { BaseSolver } from "../BaseSolver" -import type { OutputLayout, Placement } from "../../types/OutputLayout" +import type { GraphicsObject } from "graphics-debug" import type { - InputProblem, - PinId, - ChipId, Chip, + ChipId, ChipPin, + InputProblem, PartitionInputProblem, + PinId, } from "../../types/InputProblem" -import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputProblem" +import type { OutputLayout, Placement } from "../../types/OutputLayout" import { createFilteredNetworkMapping } from "../../utils/networkFiltering" -import { getPadsBoundingBox } from "./getPadsBoundingBox" +import { BaseSolver } from "../BaseSolver" import { doBasicInputProblemLayout } from "../LayoutPipelineSolver/doBasicInputProblemLayout" +import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputProblem" +import { getPadsBoundingBox } from "./getPadsBoundingBox" const PIN_SIZE = 0.1 @@ -57,11 +57,19 @@ const getPreferredRotation = (chip: Chip): 0 | 90 | 180 | 270 => { return chip.availableRotations.includes(0) ? 0 : chip.availableRotations[0]! } -const getRotatedSize = (chip: Chip, rotation: number) => { +const getRotatedSize = (size: { x: number; y: number }, rotation: number) => { if (rotation === 90 || rotation === 270) { - return { x: chip.size.y, y: chip.size.x } + return { x: size.y, y: size.x } } - return chip.size + return size +} + +const getLayoutAxisForExternalSide = ( + side: ChipPin["side"] | undefined, +): LayoutAxis | null => { + if (side?.startsWith("x")) return "y" + if (side?.startsWith("y")) return "x" + return null } export class SingleInnerPartitionPackingSolver extends BaseSolver { @@ -121,7 +129,8 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { chip, ) const rotation = getPreferredRotation(chip) - const rotatedSize = getRotatedSize(chip, rotation) + const layoutSize = this.getChipLayoutSize(chip) + const rotatedSize = getRotatedSize(layoutSize, rotation) return { chipId, @@ -148,14 +157,8 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { const bHasMainPin = b.connectedMainPin ? 1 : 0 if (aHasMainPin !== bHasMainPin) return bHasMainPin - aHasMainPin - const aCoordinate = - layoutAxis === "x" - ? (a.connectedMainPin?.offset.x ?? 0) - : (a.connectedMainPin?.offset.y ?? 0) - const bCoordinate = - layoutAxis === "x" - ? (b.connectedMainPin?.offset.x ?? 0) - : (b.connectedMainPin?.offset.y ?? 0) + const aCoordinate = this.getDecouplingCapSortCoordinate(a, layoutAxis) + const bCoordinate = this.getDecouplingCapSortCoordinate(b, layoutAxis) if (aCoordinate !== bCoordinate) return aCoordinate - bCoordinate return compareNaturalChipIds(a.chipId, b.chipId) @@ -191,6 +194,42 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } } + private getChipLayoutSize(chip: Chip) { + let minX = -chip.size.x / 2 + let maxX = chip.size.x / 2 + let minY = -chip.size.y / 2 + let maxY = chip.size.y / 2 + + for (const pinId of chip.pins) { + const pin = this.partitionInputProblem.chipPinMap[pinId] + if (!pin) continue + + minX = Math.min(minX, pin.offset.x - PIN_SIZE / 2) + maxX = Math.max(maxX, pin.offset.x + PIN_SIZE / 2) + minY = Math.min(minY, pin.offset.y - PIN_SIZE / 2) + maxY = Math.max(maxY, pin.offset.y + PIN_SIZE / 2) + } + + return { + x: maxX - minX, + y: maxY - minY, + } + } + + private getDecouplingCapSortCoordinate( + entry: { + connectedMainPin: ChipPin | null + }, + fallbackLayoutAxis: LayoutAxis, + ) { + const mainPin = entry.connectedMainPin + if (!mainPin) return 0 + + const sideAwareAxis = + getLayoutAxisForExternalSide(mainPin.side) ?? fallbackLayoutAxis + return mainPin.offset[sideAwareAxis] + } + private getStronglyConnectedExternalPin( chipId: ChipId, chip: Chip, diff --git a/tests/pack-inner-partitions-solver/decoupling-cap-main-pin-layout.test.ts b/tests/pack-inner-partitions-solver/decoupling-cap-main-pin-layout.test.ts index a3457e5..1262565 100644 --- a/tests/pack-inner-partitions-solver/decoupling-cap-main-pin-layout.test.ts +++ b/tests/pack-inner-partitions-solver/decoupling-cap-main-pin-layout.test.ts @@ -108,3 +108,81 @@ test("decoupling caps connected to y-side main pins are laid out by main pin x o expect(placements.C1!.x).toBeLessThan(placements.C2!.x) expect(placements.C2!.x).toBeLessThan(placements.C10!.x) }) + +test("decoupling cap spacing includes pin envelope beyond the chip body", () => { + const partition = makeDecouplingCapPartition() + partition.chipMap = { + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 0.2, y: 0.2 }, + availableRotations: [0], + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 0.2, y: 0.2 }, + availableRotations: [0], + }, + } + partition.chipPinMap = { + "C1.1": { pinId: "C1.1", offset: { x: -0.6, y: 0 }, side: "x-" }, + "C1.2": { pinId: "C1.2", offset: { x: 0.6, y: 0 }, side: "x+" }, + "C2.1": { pinId: "C2.1", offset: { x: -0.6, y: 0 }, side: "x-" }, + "C2.2": { pinId: "C2.2", offset: { x: 0.6, y: 0 }, side: "x+" }, + } + + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: partition, + pinIdToStronglyConnectedPins: { + "C1.1": [makeMainPin("U1.1", { x: -2, y: 2 }, "y+")], + "C2.1": [makeMainPin("U1.2", { x: 2, y: 2 }, "y+")], + }, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + const placements = solver.layout!.chipPlacements + const centerDistance = Math.abs(placements.C2!.x - placements.C1!.x) + + expect(centerDistance).toBeCloseTo(1.55) +}) + +test("mixed x-side and y-side main pins keep side-aware order", () => { + const partition = makeDecouplingCapPartition() + partition.chipMap.C3 = { + chipId: "C3", + pins: ["C3.1", "C3.2"], + size: { x: 1, y: 0.5 }, + availableRotations: [0], + } + partition.chipPinMap["C3.1"] = { + pinId: "C3.1", + offset: { x: 0, y: 0.25 }, + side: "y+", + } + partition.chipPinMap["C3.2"] = { + pinId: "C3.2", + offset: { x: 0, y: -0.25 }, + side: "y-", + } + + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: partition, + pinIdToStronglyConnectedPins: { + "C10.1": [makeMainPin("U1.10", { x: 10, y: -2 }, "x+")], + "C2.1": [makeMainPin("U1.2", { x: 10, y: 2 }, "x+")], + "C1.1": [makeMainPin("U1.1", { x: -2, y: 10 }, "y+")], + "C3.1": [makeMainPin("U1.3", { x: 2, y: 10 }, "y+")], + }, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + const placements = solver.layout!.chipPlacements + + expect(placements.C10!.x).toBeLessThan(placements.C2!.x) + expect(placements.C1!.x).toBeLessThan(placements.C3!.x) +}) From f669cef409a5cef674db4aff2fdf108096823644 Mon Sep 17 00:00:00 2001 From: Gimyoonsoo Date: Wed, 13 May 2026 01:57:36 +0900 Subject: [PATCH 3/3] Avoid debugger import in decoupling test --- .../IdentifyDecouplingCapsSolver06.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts b/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts index 615957c..40074f6 100644 --- a/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts +++ b/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts @@ -1,6 +1,13 @@ -import { test, expect } from "bun:test" +import { test, expect, mock } from "bun:test" import { IdentifyDecouplingCapsSolver } from "../../lib/solvers/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver" -import { problem } from "../../pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx" + +mock.module("lib/components/LayoutPipelineDebugger", () => ({ + LayoutPipelineDebugger: () => null, +})) + +const { problem } = await import( + "../../pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx" +) test("IdentifyDecouplingCapsSolver identifies decoupling capacitor groups from LayoutPipelineSolver06", () => { const solver = new IdentifyDecouplingCapsSolver(problem)