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 @@ -22,6 +22,11 @@ import { doBasicInputProblemLayout } from "../LayoutPipelineSolver/doBasicInputP

const PIN_SIZE = 0.1

const naturalSort = new Intl.Collator(undefined, {
numeric: true,
sensitivity: "base",
})

export class SingleInnerPartitionPackingSolver extends BaseSolver {
partitionInputProblem: PartitionInputProblem
layout: OutputLayout | null = null
Expand All @@ -38,6 +43,17 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver {
}

override _step() {
// Decoupling cap partitions get a deterministic row layout instead of PackSolver2
if (
this.partitionInputProblem.partitionType === "decoupling_caps" &&
!this.layout
) {
this.layout = this.createDecouplingCapsRowLayout()
this.activeSubSolver = null
this.solved = true
return
}

// Initialize PackSolver2 if not already created
if (!this.activeSubSolver) {
const packInput = this.createPackInput()
Expand All @@ -64,6 +80,60 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver {
}
}

/**
* Arranges decoupling capacitors in a horizontal row sorted by chip ID
* (natural/numeric order: C1, C2, C10 not C1, C10, C2), centered at the origin.
*/
private createDecouplingCapsRowLayout(): OutputLayout {
const gap =
this.partitionInputProblem.decouplingCapsGap ??
this.partitionInputProblem.chipGap

const chips = Object.values(this.partitionInputProblem.chipMap).sort(
(a, b) => naturalSort.compare(a.chipId, b.chipId),
)

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

// Pick a rotation that keeps the cap in the 0/180 axis (portrait), default 0
const pickRotation = (chip: {
availableRotations?: Array<0 | 90 | 180 | 270>
}): 0 | 90 | 180 | 270 => {
const avail = chip.availableRotations ?? [0, 90, 180, 270]
for (const r of [0, 180, 90, 270] as const) {
if (avail.includes(r)) return r
}
return 0
}

const items = chips.map((chip) => {
const rotation = pickRotation(chip)
const swapped = rotation === 90 || rotation === 270
return {
chipId: chip.chipId,
rotation,
width: swapped ? chip.size.y : chip.size.x,
}
})

const totalWidth =
items.reduce((s, item) => s + item.width, 0) +
Math.max(0, items.length - 1) * gap

let cursor = -totalWidth / 2

for (const item of items) {
chipPlacements[item.chipId] = {
x: cursor + item.width / 2,
y: 0,
ccwRotationDegrees: item.rotation,
}
cursor += item.width + gap
}

return { chipPlacements, groupPlacements: {} }
}

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

function makeDecouplingPartition(
chips: Record<
string,
{ size: { x: number; y: number }; rotations?: Array<0 | 90 | 180 | 270> }
>,
opts: Partial<PartitionInputProblem> = {},
): PartitionInputProblem {
const chipMap: PartitionInputProblem["chipMap"] = {}
for (const [chipId, cfg] of Object.entries(chips)) {
chipMap[chipId] = {
chipId,
pins: [],
size: cfg.size,
availableRotations: cfg.rotations ?? [0, 180],
}
}
return {
chipMap,
chipPinMap: {},
netMap: {},
pinStrongConnMap: {},
netConnMap: {},
chipGap: 0.5,
partitionGap: 2,
decouplingCapsGap: 0.25,
isPartition: true,
partitionType: "decoupling_caps",
...opts,
}
}

function solve(partition: PartitionInputProblem) {
const solver = new SingleInnerPartitionPackingSolver({
partitionInputProblem: partition,
pinIdToStronglyConnectedPins: {},
})
solver.step()
return solver
}

test("decoupling caps: chips are placed in a centered horizontal row", () => {
// Three caps of equal width 1, gap 0.25 → total width = 3 + 2*0.25 = 3.5
// Centers: -1.375, 0 (−1.375+1+0.25+0.5=0), +1.375 — wait, let's compute:
// gap=0.25, widths: C1=1, C2=1, C3=1
// totalWidth = 1 + 1 + 1 + 2*0.25 = 3.5
// start cursor = -1.75
// C1 center = -1.75 + 0.5 = -1.25
// C2 center = -1.25 + 0.5 + 0.25 + 0.5 = -0.0 ... let me just check actual values
const solver = solve(
makeDecouplingPartition({
C1: { size: { x: 1, y: 0.5 } },
C2: { size: { x: 1, y: 0.5 } },
C3: { size: { x: 1, y: 0.5 } },
}),
)

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

const placements = solver.layout!.chipPlacements
// All caps should be on y=0
for (const [, placement] of Object.entries(placements)) {
expect(placement.y).toBe(0)
}

// Should have exactly 3 placements
expect(Object.keys(placements)).toHaveLength(3)

// C1 should be leftmost, C3 rightmost
expect(placements["C1"]!.x).toBeLessThan(placements["C2"]!.x)
expect(placements["C2"]!.x).toBeLessThan(placements["C3"]!.x)
})

test("decoupling caps: natural sort orders C1, C2, C10 not C1, C10, C2", () => {
const solver = solve(
makeDecouplingPartition({
C10: { size: { x: 1, y: 0.5 } },
C2: { size: { x: 2, y: 0.5 } },
C1: { size: { x: 1, y: 0.5 } },
}),
)

expect(solver.solved).toBe(true)
const p = solver.layout!.chipPlacements

// Natural order: C1 < C2 < C10
expect(p["C1"]!.x).toBeLessThan(p["C2"]!.x)
expect(p["C2"]!.x).toBeLessThan(p["C10"]!.x)
})

test("decoupling caps: exact positions with decouplingCapsGap", () => {
// C1: width=1, C2: width=2, C10: width=1; gap=0.25
// natural order: C1, C2, C10
// totalWidth = 1 + 2 + 1 + 2*0.25 = 4.5
// cursor starts at -2.25
// C1: center=-2.25+0.5=-1.75, cursor=-2.25+1+0.25=-1.0
// C2: center=-1.0+1=0, cursor=-1.0+2+0.25=1.25
// C10: center=1.25+0.5=1.75
const solver = solve(
makeDecouplingPartition({
C10: { size: { x: 1, y: 0.5 } },
C2: { size: { x: 2, y: 0.5 } },
C1: { size: { x: 1, y: 0.5 } },
}),
)

const p = solver.layout!.chipPlacements
expect(p["C1"]!).toEqual({ x: -1.75, y: 0, ccwRotationDegrees: 0 })
expect(p["C2"]!).toEqual({ x: 0, y: 0, ccwRotationDegrees: 0 })
expect(p["C10"]!).toEqual({ x: 1.75, y: 0, ccwRotationDegrees: 0 })
})

test("decoupling caps: falls back to chipGap when decouplingCapsGap is absent", () => {
// C1: width=1, C2: width=2, C10: width=1; chipGap=0.5
// naturalOrder: C1, C2, C10
// totalWidth = 1 + 2 + 1 + 2*0.5 = 5
// cursor: -2.5 → C1 at -2.0, C2 at 0.0, C10 at 2.0
const solver = solve(
makeDecouplingPartition(
{
C10: { size: { x: 1, y: 0.5 } },
C2: { size: { x: 2, y: 0.5 } },
C1: { size: { x: 1, y: 0.5 } },
},
{ decouplingCapsGap: undefined },
),
)

const p = solver.layout!.chipPlacements
expect(p["C1"]!.x).toBe(-2)
expect(p["C2"]!.x).toBe(0)
expect(p["C10"]!.x).toBe(2)
})

test("decoupling caps: rotation 90/270 swaps width and height for spacing", () => {
// A cap with size x=0.5, y=1 and rotation=90: effective width = size.y = 1
const solver = solve(
makeDecouplingPartition({
C1: { size: { x: 0.5, y: 1 }, rotations: [90, 270] },
C2: { size: { x: 0.5, y: 1 }, rotations: [90, 270] },
}),
)

expect(solver.solved).toBe(true)
const p = solver.layout!.chipPlacements
// Both should use 90-degree rotation
expect(p["C1"]!.ccwRotationDegrees).toBe(90)
expect(p["C2"]!.ccwRotationDegrees).toBe(90)
// Width used for spacing is size.y=1, gap=0.25
// total = 1+1+0.25 = 2.25, start=-1.125
// C1: -1.125+0.5=-0.625, C2: -0.625+1+0.25+0.5=1.125... wait let me recalc
// Actually C1 center = -1.125 + 1/2 = -0.625
// cursor after C1 = -1.125 + 1 + 0.25 = 0.125
// C2 center = 0.125 + 0.5 = 0.625
expect(p["C1"]!.x).toBe(-0.625)
expect(p["C2"]!.x).toBe(0.625)
})

test("decoupling caps: PackSolver2 not invoked (activeSubSolver is null)", () => {
const solver = solve(
makeDecouplingPartition({
C1: { size: { x: 1, y: 0.5 } },
C2: { size: { x: 1, y: 0.5 } },
}),
)

expect(solver.solved).toBe(true)
// PackSolver2 must not have been created
expect(solver.activeSubSolver).toBeNull()
})