diff --git a/lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver.ts b/lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver.ts index 33c7dd2..51344d4 100644 --- a/lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver.ts +++ b/lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver.ts @@ -12,6 +12,7 @@ import { type PackedPartition, } from "lib/solvers/PackInnerPartitionsSolver/PackInnerPartitionsSolver" import { PartitionPackingSolver } from "lib/solvers/PartitionPackingSolver/PartitionPackingSolver" +import { PowerNetBiasSolver } from "lib/solvers/PowerNetBiasSolver/PowerNetBiasSolver" import type { ChipPin, InputProblem, PinId } from "lib/types/InputProblem" import type { OutputLayout } from "lib/types/OutputLayout" import { doBasicInputProblemLayout } from "./doBasicInputProblemLayout" @@ -53,6 +54,7 @@ export class LayoutPipelineSolver extends BaseSolver { chipPartitionsSolver?: ChipPartitionsSolver packInnerPartitionsSolver?: PackInnerPartitionsSolver partitionPackingSolver?: PartitionPackingSolver + powerNetBiasSolver?: PowerNetBiasSolver startTimeOfPhase: Record endTimeOfPhase: Record @@ -124,6 +126,12 @@ export class LayoutPipelineSolver extends BaseSolver { }, }, ), + definePipelineStep("powerNetBiasSolver", PowerNetBiasSolver, () => [ + { + inputProblem: this.inputProblem, + inputLayout: this.partitionPackingSolver!.finalLayout!, + }, + ]), ] constructor(inputProblem: InputProblem) { @@ -188,8 +196,10 @@ export class LayoutPipelineSolver extends BaseSolver { if (!this.solved && this.activeSubSolver) return this.activeSubSolver.visualize() - // If the pipeline is complete and we have a partition packing solver, - // show only the final chip placements + // If the pipeline is complete, show only the final chip placements + if (this.solved && this.powerNetBiasSolver?.solved) { + return this.powerNetBiasSolver.visualize() + } if (this.solved && this.partitionPackingSolver?.solved) { return this.partitionPackingSolver.visualize() } @@ -253,6 +263,9 @@ export class LayoutPipelineSolver extends BaseSolver { } // Show the most recent solver's output + if (this.powerNetBiasSolver?.solved) { + return this.powerNetBiasSolver.visualize() + } if (this.partitionPackingSolver?.solved) { return this.partitionPackingSolver.visualize() } @@ -400,8 +413,13 @@ export class LayoutPipelineSolver extends BaseSolver { let finalLayout: OutputLayout - // Get the final layout from the partition packing solver + // Prefer the bias-corrected layout if available if ( + this.powerNetBiasSolver?.solved && + this.powerNetBiasSolver.outputLayout + ) { + finalLayout = this.powerNetBiasSolver.outputLayout + } else if ( this.partitionPackingSolver?.solved && this.partitionPackingSolver.finalLayout ) { diff --git a/lib/solvers/PowerNetBiasSolver/PowerNetBiasSolver.ts b/lib/solvers/PowerNetBiasSolver/PowerNetBiasSolver.ts new file mode 100644 index 0000000..17cd9da --- /dev/null +++ b/lib/solvers/PowerNetBiasSolver/PowerNetBiasSolver.ts @@ -0,0 +1,207 @@ +/** + * Post-pack phase that applies vertical bias to chips based on their power/ground + * net connectivity, then resolves any resulting overlaps. + * + * Chips predominantly connected to positive voltage nets (VCC, VDD, V+) are + * displaced upward (negative Y). Chips predominantly connected to ground nets + * (GND, VSS) are displaced downward (positive Y). This produces schematics that + * match the conventional "power at top, ground at bottom" readability standard. + */ + +import type { GraphicsObject } from "graphics-debug" +import { BaseSolver } from "../BaseSolver" +import type { InputProblem } from "../../types/InputProblem" +import type { OutputLayout, Placement } from "../../types/OutputLayout" +import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputProblem" + +/** Bias displacement = chipGap × BIAS_SCALE × netRatio */ +const BIAS_SCALE = 5 + +/** Minimum net ratio (0–1) required before any bias is applied */ +const MIN_BIAS_RATIO = 0.2 + +/** Max iterations for the overlap-resolution loop */ +const MAX_OVERLAP_ITERS = 100 + +export class PowerNetBiasSolver extends BaseSolver { + inputProblem: InputProblem + inputLayout: OutputLayout + outputLayout: OutputLayout | null = null + + constructor(params: { + inputProblem: InputProblem + inputLayout: OutputLayout + }) { + super() + this.inputProblem = params.inputProblem + this.inputLayout = params.inputLayout + } + + override _step() { + this.outputLayout = this.applyVerticalBias() + this.solved = true + } + + /** + * Compute each chip's power/ground net ratio, apply vertical bias, then + * run an overlap-resolution pass so no chips end up on top of each other. + */ + private applyVerticalBias(): OutputLayout { + // Deep-copy placements so we don't mutate the input layout + const chipPlacements: Record = {} + for (const [chipId, p] of Object.entries(this.inputLayout.chipPlacements)) { + chipPlacements[chipId] = { ...p } + } + + // Build a fast pin→nets lookup: pinId → netId[] + const pinToNets = new Map() + for (const [connKey, connected] of Object.entries( + this.inputProblem.netConnMap, + )) { + if (!connected) continue + const sep = connKey.indexOf("-") + if (sep === -1) continue + const pinId = connKey.slice(0, sep) + const netId = connKey.slice(sep + 1) + if (!pinToNets.has(pinId)) pinToNets.set(pinId, []) + pinToNets.get(pinId)!.push(netId) + } + + const biasAmount = this.inputProblem.chipGap * BIAS_SCALE + + for (const [chipId, chip] of Object.entries(this.inputProblem.chipMap)) { + if (!chipPlacements[chipId]) continue + const total = chip.pins.length + if (total === 0) continue + + let powerPins = 0 + let groundPins = 0 + + for (const pinId of chip.pins) { + const nets = pinToNets.get(pinId) + if (!nets) continue + for (const netId of nets) { + const net = this.inputProblem.netMap[netId] + if (!net) continue + if (net.isPositiveVoltageSource) powerPins++ + if (net.isGround) groundPins++ + } + } + + const powerRatio = powerPins / total + const groundRatio = groundPins / total + const placement = chipPlacements[chipId]! + + if (powerRatio >= MIN_BIAS_RATIO && powerRatio > groundRatio) { + // Bias upward — positive Y in schematic/math coordinates (power at top) + placement.y += biasAmount * powerRatio + } else if (groundRatio >= MIN_BIAS_RATIO && groundRatio > powerRatio) { + // Bias downward — negative Y (ground at bottom) + placement.y -= biasAmount * groundRatio + } + } + + const biased: OutputLayout = { + chipPlacements, + groupPlacements: { ...this.inputLayout.groupPlacements }, + } + + return this.resolveOverlaps(biased) + } + + /** + * Iteratively push overlapping chip pairs apart until no overlaps remain + * or the iteration limit is reached. + */ + private resolveOverlaps(layout: OutputLayout): OutputLayout { + const placements = layout.chipPlacements + const chipIds = Object.keys(placements) + const minGap = this.inputProblem.chipGap + + for (let iter = 0; iter < MAX_OVERLAP_ITERS; iter++) { + let anyOverlap = false + + for (let i = 0; i < chipIds.length; i++) { + for (let j = i + 1; j < chipIds.length; j++) { + const id1 = chipIds[i]! + const id2 = chipIds[j]! + const p1 = placements[id1]! + const p2 = placements[id2]! + const chip1 = this.inputProblem.chipMap[id1] + const chip2 = this.inputProblem.chipMap[id2] + if (!chip1 || !chip2) continue + + const { x: hw1, y: hh1 } = this.rotatedHalfDims( + chip1.size, + p1.ccwRotationDegrees, + ) + const { x: hw2, y: hh2 } = this.rotatedHalfDims( + chip2.size, + p2.ccwRotationDegrees, + ) + + const requiredX = hw1 + hw2 + minGap + const requiredY = hh1 + hh2 + minGap + const dX = Math.abs(p1.x - p2.x) + const dY = Math.abs(p1.y - p2.y) + + const gapX = requiredX - dX + const gapY = requiredY - dY + + if (gapX > 0 && gapY > 0) { + anyOverlap = true + // Resolve along the axis with the smaller penetration depth + if (gapX <= gapY) { + const push = gapX / 2 + 1e-6 + if (p1.x <= p2.x) { + p1.x -= push + p2.x += push + } else { + p1.x += push + p2.x -= push + } + } else { + const push = gapY / 2 + 1e-6 + if (p1.y <= p2.y) { + p1.y -= push + p2.y += push + } else { + p1.y += push + p2.y -= push + } + } + } + } + } + + if (!anyOverlap) break + } + + return layout + } + + /** Returns axis-aligned half-dimensions of a chip accounting for rotation. */ + private rotatedHalfDims( + size: { x: number; y: number }, + rotation: number, + ): { x: number; y: number } { + const swap = rotation === 90 || rotation === 270 + return { + x: (swap ? size.y : size.x) / 2, + y: (swap ? size.x : size.y) / 2, + } + } + + override visualize(): GraphicsObject { + return visualizeInputProblem( + this.inputProblem, + this.outputLayout ?? this.inputLayout, + ) + } + + override getConstructorParams(): [ + { inputProblem: InputProblem; inputLayout: OutputLayout }, + ] { + return [{ inputProblem: this.inputProblem, inputLayout: this.inputLayout }] + } +} diff --git a/pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx b/pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx index c0767bb..6994074 100644 --- a/pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx +++ b/pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx @@ -1,880 +1,6 @@ -import type { PackInput } from "calculate-packing" import { LayoutPipelineDebugger } from "lib/components/LayoutPipelineDebugger" -import type { InputProblem } from "lib/index" - -export const problem: InputProblem = { - chipMap: { - U3: { - chipId: "U3", - pins: [ - "U3.1", - "U3.2", - "U3.3", - "U3.4", - "U3.5", - "U3.6", - "U3.7", - "U3.8", - "U3.9", - "U3.10", - "U3.11", - "U3.12", - "U3.13", - "U3.14", - "U3.15", - "U3.16", - "U3.17", - "U3.18", - "U3.19", - "U3.20", - "U3.21", - "U3.22", - "U3.23", - "U3.24", - "U3.25", - "U3.26", - "U3.27", - "U3.28", - "U3.29", - "U3.30", - "U3.31", - "U3.32", - "U3.33", - "U3.34", - "U3.35", - "U3.36", - "U3.37", - "U3.38", - "U3.39", - "U3.40", - "U3.41", - "U3.42", - "U3.43", - "U3.44", - "U3.45", - "U3.46", - "U3.47", - "U3.48", - "U3.49", - "U3.50", - "U3.51", - "U3.52", - "U3.53", - "U3.54", - "U3.55", - "U3.56", - "U3.57", - ], - size: { - x: 3, - y: 8.400000000000004, - }, - availableRotations: [0, 90, 180, 270], - }, - C12: { - chipId: "C12", - pins: ["C12.1", "C12.2"], - size: { - x: 0.53, - y: 1.06, - }, - availableRotations: [0], - }, - C14: { - chipId: "C14", - pins: ["C14.1", "C14.2"], - size: { - x: 0.53, - y: 1.06, - }, - availableRotations: [0], - }, - C8: { - chipId: "C8", - pins: ["C8.1", "C8.2"], - size: { - x: 0.53, - y: 1.06, - }, - availableRotations: [0], - }, - C13: { - chipId: "C13", - pins: ["C13.1", "C13.2"], - size: { - x: 0.53, - y: 1.06, - }, - availableRotations: [0], - }, - C15: { - chipId: "C15", - pins: ["C15.1", "C15.2"], - size: { - x: 0.53, - y: 1.06, - }, - availableRotations: [0], - }, - C19: { - chipId: "C19", - pins: ["C19.1", "C19.2"], - size: { - x: 0.53, - y: 1.06, - }, - availableRotations: [0], - }, - C18: { - chipId: "C18", - pins: ["C18.1", "C18.2"], - size: { - x: 0.53, - y: 1.06, - }, - availableRotations: [0], - }, - C7: { - chipId: "C7", - pins: ["C7.1", "C7.2"], - size: { - x: 0.53, - y: 1.06, - }, - availableRotations: [0], - }, - C9: { - chipId: "C9", - pins: ["C9.1", "C9.2"], - size: { - x: 0.53, - y: 1.06, - }, - availableRotations: [0], - }, - C10: { - chipId: "C10", - pins: ["C10.1", "C10.2"], - size: { - x: 0.53, - y: 1.06, - }, - availableRotations: [0], - }, - C11: { - chipId: "C11", - pins: ["C11.1", "C11.2"], - size: { - x: 0.53, - y: 1.06, - }, - availableRotations: [0], - }, - }, - chipPinMap: { - "U3.1": { - pinId: "U3.1", - offset: { - x: -1.9, - y: 1.2000000000000015, - }, - side: "x-", - }, - "U3.2": { - pinId: "U3.2", - offset: { - x: -1.9, - y: -1.8000000000000007, - }, - side: "x-", - }, - "U3.3": { - pinId: "U3.3", - offset: { - x: -1.9, - y: -2.000000000000001, - }, - side: "x-", - }, - "U3.4": { - pinId: "U3.4", - offset: { - x: -1.9, - y: -2.200000000000001, - }, - side: "x-", - }, - "U3.5": { - pinId: "U3.5", - offset: { - x: -1.9, - y: -2.4000000000000012, - }, - side: "x-", - }, - "U3.6": { - pinId: "U3.6", - offset: { - x: -1.9, - y: -2.6000000000000014, - }, - side: "x-", - }, - "U3.7": { - pinId: "U3.7", - offset: { - x: -1.9, - y: -2.8000000000000016, - }, - side: "x-", - }, - "U3.8": { - pinId: "U3.8", - offset: { - x: -1.9, - y: -3.0000000000000018, - }, - side: "x-", - }, - "U3.9": { - pinId: "U3.9", - offset: { - x: -1.9, - y: -3.200000000000002, - }, - side: "x-", - }, - "U3.10": { - pinId: "U3.10", - offset: { - x: -1.9, - y: 1.0000000000000013, - }, - side: "x-", - }, - "U3.11": { - pinId: "U3.11", - offset: { - x: -1.9, - y: -3.400000000000002, - }, - side: "x-", - }, - "U3.12": { - pinId: "U3.12", - offset: { - x: -1.9, - y: -3.6000000000000023, - }, - side: "x-", - }, - "U3.13": { - pinId: "U3.13", - offset: { - x: -1.9, - y: -3.8000000000000025, - }, - side: "x-", - }, - "U3.14": { - pinId: "U3.14", - offset: { - x: -1.9, - y: -4.000000000000002, - }, - side: "x-", - }, - "U3.15": { - pinId: "U3.15", - offset: { - x: 1.9, - y: -0.5000000000000009, - }, - side: "x+", - }, - "U3.16": { - pinId: "U3.16", - offset: { - x: 1.9, - y: -0.7000000000000011, - }, - side: "x+", - }, - "U3.17": { - pinId: "U3.17", - offset: { - x: 1.9, - y: -0.9000000000000012, - }, - side: "x+", - }, - "U3.18": { - pinId: "U3.18", - offset: { - x: 1.9, - y: -1.1000000000000014, - }, - side: "x+", - }, - "U3.19": { - pinId: "U3.19", - offset: { - x: 1.9, - y: -1.3000000000000014, - }, - side: "x+", - }, - "U3.20": { - pinId: "U3.20", - offset: { - x: 1.9, - y: -1.9000000000000015, - }, - side: "x+", - }, - "U3.21": { - pinId: "U3.21", - offset: { - x: 1.9, - y: -2.1000000000000014, - }, - side: "x+", - }, - "U3.22": { - pinId: "U3.22", - offset: { - x: -1.9, - y: 0.8000000000000012, - }, - side: "x-", - }, - "U3.23": { - pinId: "U3.23", - offset: { - x: -1.9, - y: 2.8000000000000016, - }, - side: "x-", - }, - "U3.24": { - pinId: "U3.24", - offset: { - x: 1.9, - y: -2.7000000000000015, - }, - side: "x+", - }, - "U3.25": { - pinId: "U3.25", - offset: { - x: 1.9, - y: -2.9000000000000012, - }, - side: "x+", - }, - "U3.26": { - pinId: "U3.26", - offset: { - x: 1.9, - y: -3.1000000000000014, - }, - side: "x+", - }, - "U3.27": { - pinId: "U3.27", - offset: { - x: 1.9, - y: 0.0999999999999992, - }, - side: "x+", - }, - "U3.28": { - pinId: "U3.28", - offset: { - x: 1.9, - y: 0.2999999999999994, - }, - side: "x+", - }, - "U3.29": { - pinId: "U3.29", - offset: { - x: 1.9, - y: 0.49999999999999956, - }, - side: "x+", - }, - "U3.30": { - pinId: "U3.30", - offset: { - x: 1.9, - y: 0.6999999999999997, - }, - side: "x+", - }, - "U3.31": { - pinId: "U3.31", - offset: { - x: 1.9, - y: 0.8999999999999995, - }, - side: "x+", - }, - "U3.32": { - pinId: "U3.32", - offset: { - x: 1.9, - y: 1.0999999999999996, - }, - side: "x+", - }, - "U3.33": { - pinId: "U3.33", - offset: { - x: -1.9, - y: 0.600000000000001, - }, - side: "x-", - }, - "U3.34": { - pinId: "U3.34", - offset: { - x: 1.9, - y: 1.2999999999999998, - }, - side: "x+", - }, - "U3.35": { - pinId: "U3.35", - offset: { - x: 1.9, - y: 1.5, - }, - side: "x+", - }, - "U3.36": { - pinId: "U3.36", - offset: { - x: 1.9, - y: 1.7000000000000002, - }, - side: "x+", - }, - "U3.37": { - pinId: "U3.37", - offset: { - x: 1.9, - y: 1.9000000000000004, - }, - side: "x+", - }, - "U3.38": { - pinId: "U3.38", - offset: { - x: 1.9, - y: 2.500000000000001, - }, - side: "x+", - }, - "U3.39": { - pinId: "U3.39", - offset: { - x: 1.9, - y: 2.700000000000001, - }, - side: "x+", - }, - "U3.40": { - pinId: "U3.40", - offset: { - x: 1.9, - y: 2.9000000000000012, - }, - side: "x+", - }, - "U3.41": { - pinId: "U3.41", - offset: { - x: 1.9, - y: 3.1000000000000014, - }, - side: "x+", - }, - "U3.42": { - pinId: "U3.42", - offset: { - x: -1.9, - y: 0.4000000000000008, - }, - side: "x-", - }, - "U3.43": { - pinId: "U3.43", - offset: { - x: -1.9, - y: -1.6000000000000005, - }, - side: "x-", - }, - "U3.44": { - pinId: "U3.44", - offset: { - x: -1.9, - y: 2.0000000000000018, - }, - side: "x-", - }, - "U3.45": { - pinId: "U3.45", - offset: { - x: -1.9, - y: 1.8000000000000016, - }, - side: "x-", - }, - "U3.46": { - pinId: "U3.46", - offset: { - x: -1.9, - y: -1.4000000000000004, - }, - side: "x-", - }, - "U3.47": { - pinId: "U3.47", - offset: { - x: -1.9, - y: -1.2000000000000002, - }, - side: "x-", - }, - "U3.48": { - pinId: "U3.48", - offset: { - x: -1.9, - y: -1, - }, - side: "x-", - }, - "U3.49": { - pinId: "U3.49", - offset: { - x: -1.9, - y: 0.20000000000000062, - }, - side: "x-", - }, - "U3.50": { - pinId: "U3.50", - offset: { - x: -1.9, - y: 2.600000000000002, - }, - side: "x-", - }, - "U3.51": { - pinId: "U3.51", - offset: { - x: -1.9, - y: 3.0000000000000018, - }, - side: "x-", - }, - "U3.52": { - pinId: "U3.52", - offset: { - x: -1.9, - y: 3.200000000000002, - }, - side: "x-", - }, - "U3.53": { - pinId: "U3.53", - offset: { - x: -1.9, - y: 3.4000000000000017, - }, - side: "x-", - }, - "U3.54": { - pinId: "U3.54", - offset: { - x: -1.9, - y: 3.600000000000002, - }, - side: "x-", - }, - "U3.55": { - pinId: "U3.55", - offset: { - x: -1.9, - y: 3.8000000000000016, - }, - side: "x-", - }, - "U3.56": { - pinId: "U3.56", - offset: { - x: -1.9, - y: 4.000000000000002, - }, - side: "x-", - }, - "U3.57": { - pinId: "U3.57", - offset: { - x: -1.9, - y: -0.39999999999999947, - }, - side: "x-", - }, - "C12.1": { - pinId: "C12.1", - offset: { - x: -3.469446951953614e-17, - y: 0.55, - }, - side: "y+", - }, - "C12.2": { - pinId: "C12.2", - offset: { - x: 3.469446951953614e-17, - y: -0.55, - }, - side: "y-", - }, - "C14.1": { - pinId: "C14.1", - offset: { - x: -3.469446951953614e-17, - y: 0.55, - }, - side: "y+", - }, - "C14.2": { - pinId: "C14.2", - offset: { - x: 3.469446951953614e-17, - y: -0.55, - }, - side: "y-", - }, - "C8.1": { - pinId: "C8.1", - offset: { - x: -3.469446951953614e-17, - y: 0.55, - }, - side: "y+", - }, - "C8.2": { - pinId: "C8.2", - offset: { - x: 3.469446951953614e-17, - y: -0.55, - }, - side: "y-", - }, - "C13.1": { - pinId: "C13.1", - offset: { - x: -3.469446951953614e-17, - y: 0.55, - }, - side: "y+", - }, - "C13.2": { - pinId: "C13.2", - offset: { - x: 3.469446951953614e-17, - y: -0.55, - }, - side: "y-", - }, - "C15.1": { - pinId: "C15.1", - offset: { - x: -3.469446951953614e-17, - y: 0.55, - }, - side: "y+", - }, - "C15.2": { - pinId: "C15.2", - offset: { - x: 3.469446951953614e-17, - y: -0.55, - }, - side: "y-", - }, - "C19.1": { - pinId: "C19.1", - offset: { - x: -3.469446951953614e-17, - y: 0.55, - }, - side: "y+", - }, - "C19.2": { - pinId: "C19.2", - offset: { - x: 3.469446951953614e-17, - y: -0.55, - }, - side: "y-", - }, - "C18.1": { - pinId: "C18.1", - offset: { - x: -3.469446951953614e-17, - y: 0.55, - }, - side: "y+", - }, - "C18.2": { - pinId: "C18.2", - offset: { - x: 3.469446951953614e-17, - y: -0.55, - }, - side: "y-", - }, - "C7.1": { - pinId: "C7.1", - offset: { - x: -3.469446951953614e-17, - y: 0.55, - }, - side: "y+", - }, - "C7.2": { - pinId: "C7.2", - offset: { - x: 3.469446951953614e-17, - y: -0.55, - }, - side: "y-", - }, - "C9.1": { - pinId: "C9.1", - offset: { - x: -3.469446951953614e-17, - y: 0.55, - }, - side: "y+", - }, - "C9.2": { - pinId: "C9.2", - offset: { - x: 3.469446951953614e-17, - y: -0.55, - }, - side: "y-", - }, - "C10.1": { - pinId: "C10.1", - offset: { - x: -3.469446951953614e-17, - y: 0.55, - }, - side: "y+", - }, - "C10.2": { - pinId: "C10.2", - offset: { - x: 3.469446951953614e-17, - y: -0.55, - }, - side: "y-", - }, - "C11.1": { - pinId: "C11.1", - offset: { - x: -3.469446951953614e-17, - y: 0.55, - }, - side: "y+", - }, - "C11.2": { - pinId: "C11.2", - offset: { - x: 3.469446951953614e-17, - y: -0.55, - }, - side: "y-", - }, - }, - netMap: { - V3_3: { - netId: "V3_3", - isPositiveVoltageSource: true, - }, - V1_1: { - netId: "V1_1", - isPositiveVoltageSource: true, - }, - GND: { - netId: "GND", - isGround: true, - }, - }, - pinStrongConnMap: { - "U3.1-C12.1": true, - "C12.1-U3.1": true, - "U3.10-C14.1": true, - "C14.1-U3.10": true, - "U3.22-C8.1": true, - "C8.1-U3.22": true, - "U3.33-C13.1": true, - "C13.1-U3.33": true, - "U3.42-C15.1": true, - "C15.1-U3.42": true, - "U3.49-C19.1": true, - "C19.1-U3.49": true, - "U3.23-C18.1": true, - "C18.1-U3.23": true, - "U3.50-C7.1": true, - "C7.1-U3.50": true, - "C11.1-U3.43": true, - "U3.43-C11.1": true, - "C10.1-U3.44": true, - "U3.44-C10.1": true, - }, - netConnMap: { - "U3.1-V3_3": true, - "U3.10-V3_3": true, - "U3.22-V3_3": true, - "U3.33-V3_3": true, - "U3.42-V3_3": true, - "U3.49-V3_3": true, - "C12.1-V3_3": true, - "C14.1-V3_3": true, - "C8.1-V3_3": true, - "C13.1-V3_3": true, - "C15.1-V3_3": true, - "C19.1-V3_3": true, - "U3.23-V1_1": true, - "U3.50-V1_1": true, - "C18.1-V1_1": true, - "C7.1-V1_1": true, - "C9.1-V1_1": true, - "C12.2-GND": true, - "C14.2-GND": true, - "C8.2-GND": true, - "C13.2-GND": true, - "C15.2-GND": true, - "C19.2-GND": true, - "C18.2-GND": true, - "C7.2-GND": true, - "C9.2-GND": true, - "C10.2-GND": true, - "C11.2-GND": true, - }, - chipGap: 0.6, - decouplingCapsGap: 0.2, - partitionGap: 1.2, -} +import { problem } from "./LayoutPipelineSolver06.problem" +export { problem } from "./LayoutPipelineSolver06.problem" export default function LayoutPipelineSolver06Page() { return diff --git a/pages/LayoutPipelineSolver/LayoutPipelineSolver06.problem.ts b/pages/LayoutPipelineSolver/LayoutPipelineSolver06.problem.ts new file mode 100644 index 0000000..2edd21a --- /dev/null +++ b/pages/LayoutPipelineSolver/LayoutPipelineSolver06.problem.ts @@ -0,0 +1,875 @@ +import type { InputProblem } from "lib/index" + +export const problem: InputProblem = { + chipMap: { + U3: { + chipId: "U3", + pins: [ + "U3.1", + "U3.2", + "U3.3", + "U3.4", + "U3.5", + "U3.6", + "U3.7", + "U3.8", + "U3.9", + "U3.10", + "U3.11", + "U3.12", + "U3.13", + "U3.14", + "U3.15", + "U3.16", + "U3.17", + "U3.18", + "U3.19", + "U3.20", + "U3.21", + "U3.22", + "U3.23", + "U3.24", + "U3.25", + "U3.26", + "U3.27", + "U3.28", + "U3.29", + "U3.30", + "U3.31", + "U3.32", + "U3.33", + "U3.34", + "U3.35", + "U3.36", + "U3.37", + "U3.38", + "U3.39", + "U3.40", + "U3.41", + "U3.42", + "U3.43", + "U3.44", + "U3.45", + "U3.46", + "U3.47", + "U3.48", + "U3.49", + "U3.50", + "U3.51", + "U3.52", + "U3.53", + "U3.54", + "U3.55", + "U3.56", + "U3.57", + ], + size: { + x: 3, + y: 8.400000000000004, + }, + availableRotations: [0, 90, 180, 270], + }, + C12: { + chipId: "C12", + pins: ["C12.1", "C12.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C14: { + chipId: "C14", + pins: ["C14.1", "C14.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C8: { + chipId: "C8", + pins: ["C8.1", "C8.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C13: { + chipId: "C13", + pins: ["C13.1", "C13.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C15: { + chipId: "C15", + pins: ["C15.1", "C15.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C19: { + chipId: "C19", + pins: ["C19.1", "C19.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C18: { + chipId: "C18", + pins: ["C18.1", "C18.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C7: { + chipId: "C7", + pins: ["C7.1", "C7.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C9: { + chipId: "C9", + pins: ["C9.1", "C9.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C10: { + chipId: "C10", + pins: ["C10.1", "C10.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + C11: { + chipId: "C11", + pins: ["C11.1", "C11.2"], + size: { + x: 0.53, + y: 1.06, + }, + availableRotations: [0], + }, + }, + chipPinMap: { + "U3.1": { + pinId: "U3.1", + offset: { + x: -1.9, + y: 1.2000000000000015, + }, + side: "x-", + }, + "U3.2": { + pinId: "U3.2", + offset: { + x: -1.9, + y: -1.8000000000000007, + }, + side: "x-", + }, + "U3.3": { + pinId: "U3.3", + offset: { + x: -1.9, + y: -2.000000000000001, + }, + side: "x-", + }, + "U3.4": { + pinId: "U3.4", + offset: { + x: -1.9, + y: -2.200000000000001, + }, + side: "x-", + }, + "U3.5": { + pinId: "U3.5", + offset: { + x: -1.9, + y: -2.4000000000000012, + }, + side: "x-", + }, + "U3.6": { + pinId: "U3.6", + offset: { + x: -1.9, + y: -2.6000000000000014, + }, + side: "x-", + }, + "U3.7": { + pinId: "U3.7", + offset: { + x: -1.9, + y: -2.8000000000000016, + }, + side: "x-", + }, + "U3.8": { + pinId: "U3.8", + offset: { + x: -1.9, + y: -3.0000000000000018, + }, + side: "x-", + }, + "U3.9": { + pinId: "U3.9", + offset: { + x: -1.9, + y: -3.200000000000002, + }, + side: "x-", + }, + "U3.10": { + pinId: "U3.10", + offset: { + x: -1.9, + y: 1.0000000000000013, + }, + side: "x-", + }, + "U3.11": { + pinId: "U3.11", + offset: { + x: -1.9, + y: -3.400000000000002, + }, + side: "x-", + }, + "U3.12": { + pinId: "U3.12", + offset: { + x: -1.9, + y: -3.6000000000000023, + }, + side: "x-", + }, + "U3.13": { + pinId: "U3.13", + offset: { + x: -1.9, + y: -3.8000000000000025, + }, + side: "x-", + }, + "U3.14": { + pinId: "U3.14", + offset: { + x: -1.9, + y: -4.000000000000002, + }, + side: "x-", + }, + "U3.15": { + pinId: "U3.15", + offset: { + x: 1.9, + y: -0.5000000000000009, + }, + side: "x+", + }, + "U3.16": { + pinId: "U3.16", + offset: { + x: 1.9, + y: -0.7000000000000011, + }, + side: "x+", + }, + "U3.17": { + pinId: "U3.17", + offset: { + x: 1.9, + y: -0.9000000000000012, + }, + side: "x+", + }, + "U3.18": { + pinId: "U3.18", + offset: { + x: 1.9, + y: -1.1000000000000014, + }, + side: "x+", + }, + "U3.19": { + pinId: "U3.19", + offset: { + x: 1.9, + y: -1.3000000000000014, + }, + side: "x+", + }, + "U3.20": { + pinId: "U3.20", + offset: { + x: 1.9, + y: -1.9000000000000015, + }, + side: "x+", + }, + "U3.21": { + pinId: "U3.21", + offset: { + x: 1.9, + y: -2.1000000000000014, + }, + side: "x+", + }, + "U3.22": { + pinId: "U3.22", + offset: { + x: -1.9, + y: 0.8000000000000012, + }, + side: "x-", + }, + "U3.23": { + pinId: "U3.23", + offset: { + x: -1.9, + y: 2.8000000000000016, + }, + side: "x-", + }, + "U3.24": { + pinId: "U3.24", + offset: { + x: 1.9, + y: -2.7000000000000015, + }, + side: "x+", + }, + "U3.25": { + pinId: "U3.25", + offset: { + x: 1.9, + y: -2.9000000000000012, + }, + side: "x+", + }, + "U3.26": { + pinId: "U3.26", + offset: { + x: 1.9, + y: -3.1000000000000014, + }, + side: "x+", + }, + "U3.27": { + pinId: "U3.27", + offset: { + x: 1.9, + y: 0.0999999999999992, + }, + side: "x+", + }, + "U3.28": { + pinId: "U3.28", + offset: { + x: 1.9, + y: 0.2999999999999994, + }, + side: "x+", + }, + "U3.29": { + pinId: "U3.29", + offset: { + x: 1.9, + y: 0.49999999999999956, + }, + side: "x+", + }, + "U3.30": { + pinId: "U3.30", + offset: { + x: 1.9, + y: 0.6999999999999997, + }, + side: "x+", + }, + "U3.31": { + pinId: "U3.31", + offset: { + x: 1.9, + y: 0.8999999999999995, + }, + side: "x+", + }, + "U3.32": { + pinId: "U3.32", + offset: { + x: 1.9, + y: 1.0999999999999996, + }, + side: "x+", + }, + "U3.33": { + pinId: "U3.33", + offset: { + x: -1.9, + y: 0.600000000000001, + }, + side: "x-", + }, + "U3.34": { + pinId: "U3.34", + offset: { + x: 1.9, + y: 1.2999999999999998, + }, + side: "x+", + }, + "U3.35": { + pinId: "U3.35", + offset: { + x: 1.9, + y: 1.5, + }, + side: "x+", + }, + "U3.36": { + pinId: "U3.36", + offset: { + x: 1.9, + y: 1.7000000000000002, + }, + side: "x+", + }, + "U3.37": { + pinId: "U3.37", + offset: { + x: 1.9, + y: 1.9000000000000004, + }, + side: "x+", + }, + "U3.38": { + pinId: "U3.38", + offset: { + x: 1.9, + y: 2.500000000000001, + }, + side: "x+", + }, + "U3.39": { + pinId: "U3.39", + offset: { + x: 1.9, + y: 2.700000000000001, + }, + side: "x+", + }, + "U3.40": { + pinId: "U3.40", + offset: { + x: 1.9, + y: 2.9000000000000012, + }, + side: "x+", + }, + "U3.41": { + pinId: "U3.41", + offset: { + x: 1.9, + y: 3.1000000000000014, + }, + side: "x+", + }, + "U3.42": { + pinId: "U3.42", + offset: { + x: -1.9, + y: 0.4000000000000008, + }, + side: "x-", + }, + "U3.43": { + pinId: "U3.43", + offset: { + x: -1.9, + y: -1.6000000000000005, + }, + side: "x-", + }, + "U3.44": { + pinId: "U3.44", + offset: { + x: -1.9, + y: 2.0000000000000018, + }, + side: "x-", + }, + "U3.45": { + pinId: "U3.45", + offset: { + x: -1.9, + y: 1.8000000000000016, + }, + side: "x-", + }, + "U3.46": { + pinId: "U3.46", + offset: { + x: -1.9, + y: -1.4000000000000004, + }, + side: "x-", + }, + "U3.47": { + pinId: "U3.47", + offset: { + x: -1.9, + y: -1.2000000000000002, + }, + side: "x-", + }, + "U3.48": { + pinId: "U3.48", + offset: { + x: -1.9, + y: -1, + }, + side: "x-", + }, + "U3.49": { + pinId: "U3.49", + offset: { + x: -1.9, + y: 0.20000000000000062, + }, + side: "x-", + }, + "U3.50": { + pinId: "U3.50", + offset: { + x: -1.9, + y: 2.600000000000002, + }, + side: "x-", + }, + "U3.51": { + pinId: "U3.51", + offset: { + x: -1.9, + y: 3.0000000000000018, + }, + side: "x-", + }, + "U3.52": { + pinId: "U3.52", + offset: { + x: -1.9, + y: 3.200000000000002, + }, + side: "x-", + }, + "U3.53": { + pinId: "U3.53", + offset: { + x: -1.9, + y: 3.4000000000000017, + }, + side: "x-", + }, + "U3.54": { + pinId: "U3.54", + offset: { + x: -1.9, + y: 3.600000000000002, + }, + side: "x-", + }, + "U3.55": { + pinId: "U3.55", + offset: { + x: -1.9, + y: 3.8000000000000016, + }, + side: "x-", + }, + "U3.56": { + pinId: "U3.56", + offset: { + x: -1.9, + y: 4.000000000000002, + }, + side: "x-", + }, + "U3.57": { + pinId: "U3.57", + offset: { + x: -1.9, + y: -0.39999999999999947, + }, + side: "x-", + }, + "C12.1": { + pinId: "C12.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C12.2": { + pinId: "C12.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C14.1": { + pinId: "C14.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C14.2": { + pinId: "C14.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C8.1": { + pinId: "C8.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C8.2": { + pinId: "C8.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C13.1": { + pinId: "C13.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C13.2": { + pinId: "C13.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C15.1": { + pinId: "C15.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C15.2": { + pinId: "C15.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C19.1": { + pinId: "C19.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C19.2": { + pinId: "C19.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C18.1": { + pinId: "C18.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C18.2": { + pinId: "C18.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C7.1": { + pinId: "C7.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C7.2": { + pinId: "C7.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C9.1": { + pinId: "C9.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C9.2": { + pinId: "C9.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C10.1": { + pinId: "C10.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C10.2": { + pinId: "C10.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + "C11.1": { + pinId: "C11.1", + offset: { + x: -3.469446951953614e-17, + y: 0.55, + }, + side: "y+", + }, + "C11.2": { + pinId: "C11.2", + offset: { + x: 3.469446951953614e-17, + y: -0.55, + }, + side: "y-", + }, + }, + netMap: { + V3_3: { + netId: "V3_3", + isPositiveVoltageSource: true, + }, + V1_1: { + netId: "V1_1", + isPositiveVoltageSource: true, + }, + GND: { + netId: "GND", + isGround: true, + }, + }, + pinStrongConnMap: { + "U3.1-C12.1": true, + "C12.1-U3.1": true, + "U3.10-C14.1": true, + "C14.1-U3.10": true, + "U3.22-C8.1": true, + "C8.1-U3.22": true, + "U3.33-C13.1": true, + "C13.1-U3.33": true, + "U3.42-C15.1": true, + "C15.1-U3.42": true, + "U3.49-C19.1": true, + "C19.1-U3.49": true, + "U3.23-C18.1": true, + "C18.1-U3.23": true, + "U3.50-C7.1": true, + "C7.1-U3.50": true, + "C11.1-U3.43": true, + "U3.43-C11.1": true, + "C10.1-U3.44": true, + "U3.44-C10.1": true, + }, + netConnMap: { + "U3.1-V3_3": true, + "U3.10-V3_3": true, + "U3.22-V3_3": true, + "U3.33-V3_3": true, + "U3.42-V3_3": true, + "U3.49-V3_3": true, + "C12.1-V3_3": true, + "C14.1-V3_3": true, + "C8.1-V3_3": true, + "C13.1-V3_3": true, + "C15.1-V3_3": true, + "C19.1-V3_3": true, + "U3.23-V1_1": true, + "U3.50-V1_1": true, + "C18.1-V1_1": true, + "C7.1-V1_1": true, + "C9.1-V1_1": true, + "C12.2-GND": true, + "C14.2-GND": true, + "C8.2-GND": true, + "C13.2-GND": true, + "C15.2-GND": true, + "C19.2-GND": true, + "C18.2-GND": true, + "C7.2-GND": true, + "C9.2-GND": true, + "C10.2-GND": true, + "C11.2-GND": true, + }, + chipGap: 0.6, + decouplingCapsGap: 0.2, + partitionGap: 1.2, +} diff --git a/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts b/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts index 615957c..6502a50 100644 --- a/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts +++ b/tests/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver06.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "bun:test" import { IdentifyDecouplingCapsSolver } from "../../lib/solvers/IdentifyDecouplingCapsSolver/IdentifyDecouplingCapsSolver" -import { problem } from "../../pages/LayoutPipelineSolver/LayoutPipelineSolver06.page.tsx" +import { problem } from "../../pages/LayoutPipelineSolver/LayoutPipelineSolver06.problem" test("IdentifyDecouplingCapsSolver identifies decoupling capacitor groups from LayoutPipelineSolver06", () => { const solver = new IdentifyDecouplingCapsSolver(problem) diff --git a/tests/PowerNetBiasSolver/PowerNetBiasSolver01.test.ts b/tests/PowerNetBiasSolver/PowerNetBiasSolver01.test.ts new file mode 100644 index 0000000..e094d5a --- /dev/null +++ b/tests/PowerNetBiasSolver/PowerNetBiasSolver01.test.ts @@ -0,0 +1,254 @@ +import { test, expect } from "bun:test" +import { PowerNetBiasSolver } from "../../lib/solvers/PowerNetBiasSolver/PowerNetBiasSolver" +import { LayoutPipelineSolver } from "../../lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver" +import { normalizeSide } from "lib/types/Side" +import type { InputProblem } from "lib/types/InputProblem" +import { getInputProblemFromCircuitJsonSchematic } from "lib/testing/getInputProblemFromCircuitJsonSchematic" +import { getExampleCircuitJson } from "../assets/ExampleCircuit02" + +/** + * Build a synthetic problem with two chips: one connected purely to VCC, + * one connected purely to GND. A third chip in the middle has equal connections + * to both. + */ +function buildPowerGroundProblem(): InputProblem { + return { + chipMap: { + VccChip: { + chipId: "VccChip", + pins: ["VccChip.1", "VccChip.2"], + size: { x: 1.0, y: 0.5 }, + }, + GndChip: { + chipId: "GndChip", + pins: ["GndChip.1", "GndChip.2"], + size: { x: 1.0, y: 0.5 }, + }, + MainChip: { + chipId: "MainChip", + pins: ["MainChip.1", "MainChip.2", "MainChip.3", "MainChip.4"], + size: { x: 2.0, y: 2.0 }, + }, + }, + chipPinMap: { + "VccChip.1": { + pinId: "VccChip.1", + offset: { x: -0.25, y: 0 }, + side: normalizeSide("left"), + }, + "VccChip.2": { + pinId: "VccChip.2", + offset: { x: 0.25, y: 0 }, + side: normalizeSide("right"), + }, + "GndChip.1": { + pinId: "GndChip.1", + offset: { x: -0.25, y: 0 }, + side: normalizeSide("left"), + }, + "GndChip.2": { + pinId: "GndChip.2", + offset: { x: 0.25, y: 0 }, + side: normalizeSide("right"), + }, + "MainChip.1": { + pinId: "MainChip.1", + offset: { x: -1.0, y: 0.5 }, + side: normalizeSide("left"), + }, + "MainChip.2": { + pinId: "MainChip.2", + offset: { x: -1.0, y: -0.5 }, + side: normalizeSide("left"), + }, + "MainChip.3": { + pinId: "MainChip.3", + offset: { x: 1.0, y: 0.5 }, + side: normalizeSide("right"), + }, + "MainChip.4": { + pinId: "MainChip.4", + offset: { x: 1.0, y: -0.5 }, + side: normalizeSide("right"), + }, + }, + netMap: { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + SIG: { netId: "SIG" }, + }, + pinStrongConnMap: {}, + netConnMap: { + // VccChip: both pins on VCC + "VccChip.1-VCC": true, + "VccChip.2-VCC": true, + // GndChip: both pins on GND + "GndChip.1-GND": true, + "GndChip.2-GND": true, + // MainChip: one VCC, one GND, two signal pins + "MainChip.1-VCC": true, + "MainChip.2-GND": true, + "MainChip.3-SIG": true, + "MainChip.4-SIG": true, + }, + chipGap: 0.5, + partitionGap: 2, + } +} + +test("PowerNetBiasSolver moves power chip above ground chip", () => { + const problem = buildPowerGroundProblem() + + // Start with a flat layout where all chips are at y=0 + const inputLayout = { + chipPlacements: { + VccChip: { x: -3, y: 0, ccwRotationDegrees: 0 }, + GndChip: { x: 3, y: 0, ccwRotationDegrees: 0 }, + MainChip: { x: 0, y: 0, ccwRotationDegrees: 0 }, + }, + groupPlacements: {}, + } + + const solver = new PowerNetBiasSolver({ inputProblem: problem, inputLayout }) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.outputLayout).not.toBeNull() + + const out = solver.outputLayout! + + // VccChip (100% VCC) must be above (higher Y in positive-Y-up coords) GndChip (100% GND) + expect(out.chipPlacements["VccChip"]!.y).toBeGreaterThan( + out.chipPlacements["GndChip"]!.y, + ) + + // VccChip should have moved upward from y=0 (positive Y = up) + expect(out.chipPlacements["VccChip"]!.y).toBeGreaterThan(0) + + // GndChip should have moved downward from y=0 (negative Y = down) + expect(out.chipPlacements["GndChip"]!.y).toBeLessThan(0) +}) + +test("PowerNetBiasSolver resolves overlaps after bias", () => { + const problem = buildPowerGroundProblem() + + // Start with chips stacked at the same position + const inputLayout = { + chipPlacements: { + VccChip: { x: 0, y: 0, ccwRotationDegrees: 0 }, + GndChip: { x: 0, y: 0, ccwRotationDegrees: 0 }, + MainChip: { x: 0, y: 0, ccwRotationDegrees: 0 }, + }, + groupPlacements: {}, + } + + const solver = new PowerNetBiasSolver({ inputProblem: problem, inputLayout }) + solver.solve() + + expect(solver.solved).toBe(true) + const out = solver.outputLayout! + + // Check no chips overlap each other + const chipIds = Object.keys(out.chipPlacements) + for (let i = 0; i < chipIds.length; i++) { + for (let j = i + 1; j < chipIds.length; j++) { + const id1 = chipIds[i]! + const id2 = chipIds[j]! + const p1 = out.chipPlacements[id1]! + const p2 = out.chipPlacements[id2]! + const chip1 = problem.chipMap[id1]! + const chip2 = problem.chipMap[id2]! + + const dx = Math.abs(p1.x - p2.x) + const dy = Math.abs(p1.y - p2.y) + const minDx = (chip1.size.x + chip2.size.x) / 2 + const minDy = (chip1.size.y + chip2.size.y) / 2 + + // Chips must be separated in at least one axis + const separated = dx >= minDx - 1e-6 || dy >= minDy - 1e-6 + expect(separated).toBe(true) + } + } +}) + +test("PowerNetBiasSolver: power chip ends up above signal chip above ground chip in full pipeline", () => { + const problem = buildPowerGroundProblem() + const solver = new LayoutPipelineSolver(problem) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.powerNetBiasSolver?.solved).toBe(true) + + const layout = solver.getOutputLayout() + + const vccY = layout.chipPlacements["VccChip"]!.y + const gndY = layout.chipPlacements["GndChip"]!.y + + // After the full pipeline + bias, VccChip must be strictly above GndChip + // (positive Y = up in schematic coordinates) + expect(vccY).toBeGreaterThan(gndY) +}) + +test("PowerNetBiasSolver does not break ExampleCircuit02 pipeline", () => { + const circuitJson = getExampleCircuitJson() + const problem = getInputProblemFromCircuitJsonSchematic(circuitJson, { + useReadableIds: true, + }) + + const solver = new LayoutPipelineSolver(problem) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.powerNetBiasSolver?.solved).toBe(true) + + const layout = solver.getOutputLayout() + + // All chips should have placements + for (const chipId of Object.keys(problem.chipMap)) { + expect(layout.chipPlacements[chipId]).toBeDefined() + const p = layout.chipPlacements[chipId]! + expect(typeof p.x).toBe("number") + expect(typeof p.y).toBe("number") + expect(isNaN(p.x)).toBe(false) + expect(isNaN(p.y)).toBe(false) + } + + // GND net exists — any chip connected purely to GND should be below any chip + // connected purely to a positive voltage net + const gndChips: string[] = [] + const vccChips: string[] = [] + + for (const [chipId, chip] of Object.entries(problem.chipMap)) { + let powerPins = 0 + let groundPins = 0 + const total = chip.pins.length + for (const pinId of chip.pins) { + for (const [connKey, connected] of Object.entries(problem.netConnMap)) { + if (!connected) continue + const sep = connKey.indexOf("-") + const cPinId = connKey.slice(0, sep) + const netId = connKey.slice(sep + 1) + if (cPinId !== pinId) continue + const net = problem.netMap[netId] + if (!net) continue + if (net.isPositiveVoltageSource) powerPins++ + if (net.isGround) groundPins++ + } + } + if (total > 0) { + if (groundPins / total >= 0.9) gndChips.push(chipId) + if (powerPins / total >= 0.9) vccChips.push(chipId) + } + } + + // If we found clear power/ground chips, verify ordering + // (positive Y = up: power chips must have higher Y than ground chips) + for (const vChip of vccChips) { + for (const gChip of gndChips) { + expect(layout.chipPlacements[vChip]!.y).toBeGreaterThan( + layout.chipPlacements[gChip]!.y, + ) + } + } +})