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
134 changes: 121 additions & 13 deletions lib/solvers/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,52 @@ export class IdentifyDecouplingCapsSolver extends BaseSolver {
this.queuedChips = Object.values(inputProblem.chipMap)
}

/** Determine if chip is a 2-pin component with restricted rotation */
/** Determine if chip is a 2-pin component with restricted rotation.
*
* A decoupling capacitor is typically a 0402/0603 passive that either:
* - has an explicitly restricted rotation set (e.g. [0, 180] or [0])
* - has all four rotations but its pins are on the y+/y- sides (the
* component is physically a 2-pin passive oriented vertically)
*
* We accept [0], [180], [0, 180] as well as [90, 270] (horizontal) – any
* subset that excludes the "diagonal" orientations. We also accept a chip
* with all 4 rotations when its pins are on opposite Y sides, because the
* converter doesn't always restrict passives to [0,180].
*/
private isTwoPinRestrictedRotation(chip: Chip): boolean {
if (chip.pins.length !== 2) return false

// Must be restricted to 0/180 or a single fixed orientation
// If no rotation info, can't determine – skip
if (!chip.availableRotations) return false
const allowed = new Set<0 | 180>([0, 180])
return (
chip.availableRotations.length > 0 &&
chip.availableRotations.every((r) => allowed.has(r as 0 | 180))
)

const rotSet = new Set(chip.availableRotations)
// Purely horizontal/vertical subsets: subset of {0,180} or {90,270}
const onlyCardinal =
rotSet.size > 0 &&
[...rotSet].every((r) => r === 0 || r === 180 || r === 90 || r === 270)

if (!onlyCardinal) return false

// Prefer components restricted to fewer rotations (indicates passive)
if (rotSet.size <= 2) return true

// All 4 rotations – only accept if pins are on y+/y- or x+/x- sides
if (rotSet.size === 4) {
return this.pinsOnOppositeYSides(chip) || this.pinsOnOppositeXSides(chip)
}

return true
}

/** Check that the two pins are on opposite X sides (x+ and x-) */
private pinsOnOppositeXSides(chip: Chip): boolean {
if (chip.pins.length !== 2) return false
const [p1, p2] = chip.pins
const cp1 = this.inputProblem.chipPinMap[p1!]
const cp2 = this.inputProblem.chipPinMap[p2!]
if (!cp1 || !cp2) return false
const sides = new Set([cp1.side, cp2.side])
return sides.has("x+") && sides.has("x-")
}

/** Check that the two pins are on opposite Y sides (y+ and y-) */
Expand Down Expand Up @@ -91,9 +126,17 @@ export class IdentifyDecouplingCapsSolver extends BaseSolver {
return neighbors
}

/** Find the main chip id for a decoupling capacitor candidate */
/** Find the main chip id for a decoupling capacitor candidate.
*
* Primary strategy: find chips that are directly strong-connected to one of
* the cap's pins.
*
* Fallback: if no direct strong connection exists, find the chip that has
* the most pins on the cap's supply net(s) — this handles topology where
* the cap is only connected via the net, not via a direct wire.
*/
private findMainChipIdForCap(capChip: Chip): ChipId | null {
// Aggregate strong neighbors from both pins
// Primary: strong neighbours
const strongNeighbors = new Map<ChipId, number>()
for (const pinId of capChip.pins) {
const neighbors = this.getStronglyConnectedNeighborChips(pinId)
Expand All @@ -102,18 +145,56 @@ export class IdentifyDecouplingCapsSolver extends BaseSolver {
strongNeighbors.set(n, (strongNeighbors.get(n) || 0) + 1)
}
}
if (strongNeighbors.size === 0) return null

// Choose the neighbor with the most connections (tie-breaker: lexicographic)
if (strongNeighbors.size > 0) {
let best: { id: ChipId; score: number } | null = null
for (const [id, score] of strongNeighbors.entries()) {
if (
!best ||
score > best.score ||
(score === best.score && id < best.id)
)
best = { id, score }
}
return best ? best.id : null
}

// Fallback: find chip with the most pins on the same supply nets
const netPair = this.getNormalizedNetPair(capChip)
if (!netPair) return null

const netPairSet = new Set(netPair)
const chipScores = new Map<ChipId, number>()

for (const [connKey, connected] of Object.entries(
this.inputProblem.netConnMap,
)) {
if (!connected) continue
const [pinId, netId] = connKey.split("-") as [PinId, NetId]
if (!netPairSet.has(netId)) continue
const chipId = pinId.split(".")[0]
if (!chipId || chipId === capChip.chipId) continue
chipScores.set(chipId, (chipScores.get(chipId) || 0) + 1)
}

if (chipScores.size === 0) return null

let best: { id: ChipId; score: number } | null = null
for (const [id, score] of strongNeighbors.entries()) {
for (const [id, score] of chipScores.entries()) {
if (!best || score > best.score || (score === best.score && id < best.id))
best = { id, score }
}
return best ? best.id : null
}

/** Get all net IDs connected to a pin */
/** Get all net IDs connected to a pin.
*
* Also walks one hop via pinStrongConnMap: if this pin is directly wired
* to another chip's pin that has a net assignment, the cap inherits that
* net. This is the common RP2040 topology where a VCC capacitor pin is
* strong-connected to the RP2040 VCC pin but has no direct netConnMap
* entry of its own.
*/
private getNetIdsForPin(pinId: PinId): Set<NetId> {
const nets = new Set<NetId>()
for (const [connKey, connected] of Object.entries(
Expand All @@ -123,9 +204,36 @@ export class IdentifyDecouplingCapsSolver extends BaseSolver {
const [p, n] = connKey.split("-") as [PinId, NetId]
if (p === pinId) nets.add(n)
}

// One-hop via strong connections
const strongNeighbors = this.getStronglyConnectedNeighborPins(pinId)
for (const neighborPin of strongNeighbors) {
for (const [connKey, connected] of Object.entries(
this.inputProblem.netConnMap,
)) {
if (!connected) continue
const [p, n] = connKey.split("-") as [PinId, NetId]
if (p === neighborPin) nets.add(n)
}
}

return nets
}

/** Get all pins strongly connected to the given pin (excluding the pin itself) */
private getStronglyConnectedNeighborPins(pinId: PinId): Set<PinId> {
const neighbors = new Set<PinId>()
for (const [connKey, connected] of Object.entries(
this.inputProblem.pinStrongConnMap,
)) {
if (!connected) continue
const [a, b] = connKey.split("-") as [PinId, PinId]
if (a === pinId) neighbors.add(b)
else if (b === pinId) neighbors.add(a)
}
return neighbors
}

/** Get a normalized, sorted pair of net IDs connected across the two pins of a capacitor chip */
private getNormalizedNetPair(capChip: Chip): [NetId, NetId] | null {
if (capChip.pins.length !== 2) return null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Specialized solver that arranges decoupling capacitors in a clean horizontal
* row, sorted deterministically by chipId.
*
* This bypasses the general PackSolver2 for decoupling_caps partitions,
* producing the tidy "cap row next to IC" layout shown in the issue's
* "acceptable solution" screenshot.
*/

import { BaseSolver } from "../BaseSolver"
import type { GraphicsObject } from "graphics-debug"
import type { OutputLayout, Placement } from "../../types/OutputLayout"
import type { PartitionInputProblem } from "../../types/InputProblem"
import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputProblem"

export class DecouplingCapsPackingSolver extends BaseSolver {
partitionInputProblem: PartitionInputProblem
layout: OutputLayout | null = null

constructor(params: { partitionInputProblem: PartitionInputProblem }) {
super()
this.partitionInputProblem = params.partitionInputProblem
}

override _step() {
const { chipMap, chipGap, decouplingCapsGap } = this.partitionInputProblem
const gap = decouplingCapsGap ?? chipGap

// Sort chips by chipId for a deterministic, reproducible layout
const chips = Object.values(chipMap).sort((a, b) =>
a.chipId.localeCompare(b.chipId, undefined, { numeric: true }),
)

if (chips.length === 0) {
this.layout = { chipPlacements: {}, groupPlacements: {} }
this.solved = true
return
}

// Compute total row width to centre it at x = 0
const totalWidth = chips.reduce(
(sum, chip, idx) =>
sum + chip.size.x + (idx < chips.length - 1 ? gap : 0),
0,
)
let x = -totalWidth / 2

const chipPlacements: Record<string, Placement> = {}
for (const chip of chips) {
x += chip.size.x / 2
chipPlacements[chip.chipId] = {
x,
y: 0,
ccwRotationDegrees: 0,
}
x += chip.size.x / 2 + gap
}

this.layout = { chipPlacements, groupPlacements: {} }
this.solved = true
}

override visualize(): GraphicsObject {
if (!this.layout) {
return super.visualize()
}
return visualizeInputProblem(this.partitionInputProblem, this.layout)
}

override getConstructorParams(): [PartitionInputProblem] {
return [this.partitionInputProblem]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputPro
import { createFilteredNetworkMapping } from "../../utils/networkFiltering"
import { getPadsBoundingBox } from "./getPadsBoundingBox"
import { doBasicInputProblemLayout } from "../LayoutPipelineSolver/doBasicInputProblemLayout"
import { DecouplingCapsPackingSolver } from "./DecouplingCapsPackingSolver"

const PIN_SIZE = 0.1

export class SingleInnerPartitionPackingSolver extends BaseSolver {
partitionInputProblem: PartitionInputProblem
layout: OutputLayout | null = null
declare activeSubSolver: PackSolver2 | null
declare activeSubSolver: PackSolver2 | DecouplingCapsPackingSolver | null
pinIdToStronglyConnectedPins: Record<PinId, ChipPin[]>

constructor(params: {
Expand All @@ -38,26 +39,50 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver {
}

override _step() {
// Decoupling-cap partitions get a specialised linear-row layout instead
// of the general PackSolver2, which produces overlapping results for
// large numbers of small passive components.
if (
this.partitionInputProblem.partitionType === "decoupling_caps" &&
!this.activeSubSolver
) {
const decapSolver = new DecouplingCapsPackingSolver({
partitionInputProblem: this.partitionInputProblem,
})
this.activeSubSolver = decapSolver
decapSolver.solve()

if (decapSolver.solved && decapSolver.layout) {
this.layout = decapSolver.layout
this.solved = true
} else {
this.failed = true
this.error = "DecouplingCapsPackingSolver failed"
}
return
}

// Initialize PackSolver2 if not already created
if (!this.activeSubSolver) {
const packInput = this.createPackInput()
this.activeSubSolver = new PackSolver2(packInput)
this.activeSubSolver = this.activeSubSolver
}

const packSolver = this.activeSubSolver as PackSolver2

// Run one step of the PackSolver2
this.activeSubSolver.step()
packSolver.step()

if (this.activeSubSolver.failed) {
if (packSolver.failed) {
this.failed = true
this.error = `PackSolver2 failed: ${this.activeSubSolver.error}`
this.error = `PackSolver2 failed: ${packSolver.error}`
return
}

if (this.activeSubSolver.solved) {
if (packSolver.solved) {
// Apply the packing result to create the layout
this.layout = this.createLayoutFromPackingResult(
this.activeSubSolver.packedComponents,
packSolver.packedComponents,
)
this.solved = true
this.activeSubSolver = null
Expand Down
46 changes: 46 additions & 0 deletions lib/testing/getInputProblemFromCircuitJsonSchematic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,29 @@ export const getInputProblemFromCircuitJsonSchematic = (
})
.filter(Boolean) as string[]

// Tag 2-pin passives with restricted rotation so the decoupling cap
// identification phase can recognise them.
const ftype = ((source_component as any).ftype || "") as string
const isTwoPinPassive =
pinIds.length === 2 &&
(ftype === "simple_capacitor" ||
ftype === "capacitor" ||
ftype === "simple_resistor" ||
ftype === "resistor" ||
ftype === "simple_inductor" ||
ftype === "inductor" ||
ftype === "simple_diode" ||
ftype === "diode" ||
ftype === "ferrite_bead")

problem.chipMap[chipId] = {
chipId,
pins: pinIds,
size: {
x: schematic_component.size?.width || 10,
y: schematic_component.size?.height || 10,
},
...(isTwoPinPassive ? { availableRotations: [0, 180] } : {}),
}

// Create chipPinMap entries for each pin
Expand Down Expand Up @@ -146,8 +162,38 @@ export const getInputProblemFromCircuitJsonSchematic = (
readableIdToSourceNetId.set(netId, originalNetId)
}

// Detect ground and positive voltage nets by name so that
// IdentifyDecouplingCapsSolver can validate the net pair.
const name = (sourceNet.name || "").toUpperCase()
const isGround =
name === "GND" ||
name === "AGND" ||
name === "DGND" ||
name === "VSS" ||
name === "VEE" ||
name.startsWith("GND")
const isPositiveVoltageSource =
!isGround &&
(name.startsWith("VCC") ||
name.startsWith("VDD") ||
name.startsWith("VIN") ||
name.startsWith("VBUS") ||
name.startsWith("V3") ||
name.startsWith("V5") ||
name.startsWith("V1") ||
name.startsWith("VIO") ||
name.startsWith("VPP") ||
name === "3V3" ||
name === "5V" ||
name === "3.3V" ||
name === "5.0V" ||
name === "POWER" ||
name === "PWR")

problem.netMap[netId] = {
netId: netId,
...(isGround ? { isGround: true } : {}),
...(isPositiveVoltageSource ? { isPositiveVoltageSource: true } : {}),
}
}

Expand Down
Loading
Loading