From 42d72d5de1f888e7575f1a42e1e555ecfdd21f04 Mon Sep 17 00:00:00 2001 From: 769066112-ops <769066112-ops@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:13:34 +0800 Subject: [PATCH] feat: integrate overlap resolution solver with voltage-aware biasing (#12) - Integrate OverlapResolutionSolver as a new pipeline phase after PartitionPackingSolver - Add voltage-aware biasing: VCC-connected chips biased upward, GND-connected downward - Add force damping for stable convergence during overlap resolution - Re-center layout after overlap resolution for clean output - Cache connected pairs and chip-net mappings for performance - Add tests for overlap resolution integration and voltage biasing --- .../LayoutPipelineSolver.ts | 37 +- .../OverlapResolutionSolver.ts | 369 ++++++++++++++++++ .../OverlapResolutionSolver.test.ts | 236 +++++++++++ 3 files changed, 640 insertions(+), 2 deletions(-) create mode 100644 lib/solvers/OverlapResolutionSolver/OverlapResolutionSolver.ts create mode 100644 tests/OverlapResolutionSolver/OverlapResolutionSolver.test.ts diff --git a/lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver.ts b/lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver.ts index 33c7dd2..8006a40 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 { OverlapResolutionSolver } from "lib/solvers/OverlapResolutionSolver/OverlapResolutionSolver" 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 + overlapResolutionSolver?: OverlapResolutionSolver startTimeOfPhase: Record endTimeOfPhase: Record @@ -124,6 +126,24 @@ export class LayoutPipelineSolver extends BaseSolver { }, }, ), + definePipelineStep( + "overlapResolutionSolver", + OverlapResolutionSolver, + () => [ + { + inputProblem: this.inputProblem, + layout: this.partitionPackingSolver?.finalLayout ?? { + chipPlacements: {}, + groupPlacements: {}, + }, + }, + ], + { + onSolved: (_solver) => { + // Overlap resolution complete + }, + }, + ), ] constructor(inputProblem: InputProblem) { @@ -188,6 +208,12 @@ export class LayoutPipelineSolver extends BaseSolver { if (!this.solved && this.activeSubSolver) return this.activeSubSolver.visualize() + // If the pipeline is complete and we have an overlap resolution solver, + // show the final resolved layout + if (this.solved && this.overlapResolutionSolver?.solved) { + return this.overlapResolutionSolver.visualize() + } + // If the pipeline is complete and we have a partition packing solver, // show only the final chip placements if (this.solved && this.partitionPackingSolver?.solved) { @@ -199,6 +225,7 @@ export class LayoutPipelineSolver extends BaseSolver { const chipPartitionsViz = this.chipPartitionsSolver?.visualize() const packInnerPartitionsViz = this.packInnerPartitionsSolver?.visualize() const partitionPackingViz = this.partitionPackingSolver?.visualize() + const overlapResolutionViz = this.overlapResolutionSolver?.visualize() // Get basic layout positions to avoid overlapping at (0,0) const basicLayout = doBasicInputProblemLayout(this.inputProblem) @@ -210,6 +237,7 @@ export class LayoutPipelineSolver extends BaseSolver { chipPartitionsViz, packInnerPartitionsViz, partitionPackingViz, + overlapResolutionViz, ] .filter(Boolean) .map((viz, stepIndex) => { @@ -253,6 +281,9 @@ export class LayoutPipelineSolver extends BaseSolver { } // Show the most recent solver's output + if (this.overlapResolutionSolver?.solved) { + return this.overlapResolutionSolver.visualize() + } if (this.partitionPackingSolver?.solved) { return this.partitionPackingSolver.visualize() } @@ -400,8 +431,10 @@ export class LayoutPipelineSolver extends BaseSolver { let finalLayout: OutputLayout - // Get the final layout from the partition packing solver - if ( + // Prefer the overlap-resolved layout if available + if (this.overlapResolutionSolver?.solved) { + finalLayout = this.overlapResolutionSolver.getOutputLayout() + } else if ( this.partitionPackingSolver?.solved && this.partitionPackingSolver.finalLayout ) { diff --git a/lib/solvers/OverlapResolutionSolver/OverlapResolutionSolver.ts b/lib/solvers/OverlapResolutionSolver/OverlapResolutionSolver.ts new file mode 100644 index 0000000..aa2f4bd --- /dev/null +++ b/lib/solvers/OverlapResolutionSolver/OverlapResolutionSolver.ts @@ -0,0 +1,369 @@ +/** + * Resolves chip overlaps in the final layout using a force-directed approach. + * After the partition packing solver produces a layout, this solver: + * 1. Detects overlapping chips (including minimum gap enforcement) + * 2. Applies repulsion forces to separate overlapping chips + * 3. Applies attraction forces to keep connected chips close + * 4. Applies voltage-aware biasing (VCC up, GND down) for conventional schematics + * 5. Re-centers the layout after resolution + * 6. Iterates until no overlaps remain or max iterations reached + */ + +import type { GraphicsObject } from "graphics-debug" +import { BaseSolver } from "../BaseSolver" +import type { OutputLayout, Placement } from "../../types/OutputLayout" +import type { + InputProblem, + ChipId, + PinId, + NetId, +} from "../../types/InputProblem" +import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputProblem" + +interface ChipBounds { + chipId: string + minX: number + maxX: number + minY: number + maxY: number + cx: number + cy: number +} + +export class OverlapResolutionSolver extends BaseSolver { + inputProblem: InputProblem + layout: OutputLayout + resolvedLayout: OutputLayout | null = null + + private readonly REPULSION_STRENGTH = 1.5 + private readonly ATTRACTION_STRENGTH = 0.03 + private readonly VOLTAGE_BIAS_STRENGTH = 0.15 + private readonly MIN_SEPARATION: number + private readonly MAX_RESOLVE_ITERATIONS = 300 + private resolveIterations = 0 + + /** Cache: chip -> set of net IDs it connects to */ + private chipNetCache: Map> | null = null + /** Cache: connected chip pairs via strong connections */ + private connectedPairsCache: Set | null = null + + constructor(params: { inputProblem: InputProblem; layout: OutputLayout }) { + super() + this.inputProblem = params.inputProblem + this.layout = params.layout + this.MAX_ITERATIONS = this.MAX_RESOLVE_ITERATIONS + 10 + this.MIN_SEPARATION = params.inputProblem.chipGap + } + + override _step() { + if (this.resolveIterations === 0) { + // Deep clone the layout to avoid mutating the original + this.resolvedLayout = { + chipPlacements: {}, + groupPlacements: { ...this.layout.groupPlacements }, + } + for (const [id, p] of Object.entries(this.layout.chipPlacements)) { + this.resolvedLayout.chipPlacements[id] = { ...p } + } + } + + const overlaps = this.detectOverlaps() + + if ( + overlaps.length === 0 || + this.resolveIterations >= this.MAX_RESOLVE_ITERATIONS + ) { + // Final pass: re-center the layout around origin + this.recenterLayout() + this.solved = true + return + } + + this.applyForces(overlaps) + this.resolveIterations++ + } + + private getChipBounds(chipId: string): ChipBounds | null { + const placement = this.resolvedLayout!.chipPlacements[chipId] + const chip = this.inputProblem.chipMap[chipId] + if (!placement || !chip) return null + + const rot = placement.ccwRotationDegrees || 0 + let w = chip.size.x + let h = chip.size.y + if (rot === 90 || rot === 270) { + ;[w, h] = [h, w] + } + + const halfW = w / 2 + const halfH = h / 2 + + return { + chipId, + minX: placement.x - halfW, + maxX: placement.x + halfW, + minY: placement.y - halfH, + maxY: placement.y + halfH, + cx: placement.x, + cy: placement.y, + } + } + + private detectOverlaps(): Array<{ + chip1: string + chip2: string + overlapX: number + overlapY: number + }> { + const overlaps: Array<{ + chip1: string + chip2: string + overlapX: number + overlapY: number + }> = [] + + const chipIds = Object.keys(this.resolvedLayout!.chipPlacements) + const sep = this.MIN_SEPARATION + + for (let i = 0; i < chipIds.length; i++) { + for (let j = i + 1; j < chipIds.length; j++) { + const b1 = this.getChipBounds(chipIds[i]!) + const b2 = this.getChipBounds(chipIds[j]!) + if (!b1 || !b2) continue + + const adjustedOverlapX = + Math.min(b1.maxX, b2.maxX) - Math.max(b1.minX, b2.minX) + sep + const adjustedOverlapY = + Math.min(b1.maxY, b2.maxY) - Math.max(b1.minY, b2.minY) + sep + + if (adjustedOverlapX > 0 && adjustedOverlapY > 0) { + overlaps.push({ + chip1: chipIds[i]!, + chip2: chipIds[j]!, + overlapX: adjustedOverlapX, + overlapY: adjustedOverlapY, + }) + } + } + } + + return overlaps + } + + private getConnectedChipPairs(): Set { + if (this.connectedPairsCache) return this.connectedPairsCache + + const pairs = new Set() + + for (const [connKey, connected] of Object.entries( + this.inputProblem.pinStrongConnMap, + )) { + if (!connected) continue + const [pin1, pin2] = connKey.split("-") + if (!pin1 || !pin2) continue + + let chip1: string | null = null + let chip2: string | null = null + + for (const [chipId, chip] of Object.entries(this.inputProblem.chipMap)) { + if (chip.pins.includes(pin1)) chip1 = chipId + if (chip.pins.includes(pin2)) chip2 = chipId + } + + if (chip1 && chip2 && chip1 !== chip2) { + const key = chip1 < chip2 ? `${chip1}-${chip2}` : `${chip2}-${chip1}` + pairs.add(key) + } + } + + this.connectedPairsCache = pairs + return pairs + } + + /** Build a map of chipId -> set of netIds the chip connects to */ + private getChipNets(): Map> { + if (this.chipNetCache) return this.chipNetCache + + const chipNets = new Map>() + + for (const [connKey, connected] of Object.entries( + this.inputProblem.netConnMap, + )) { + if (!connected) continue + const [pinId, netId] = connKey.split("-") + if (!pinId || !netId) continue + + // Find which chip owns this pin + for (const [chipId, chip] of Object.entries(this.inputProblem.chipMap)) { + if (chip.pins.includes(pinId)) { + if (!chipNets.has(chipId)) chipNets.set(chipId, new Set()) + chipNets.get(chipId)!.add(netId) + break + } + } + } + + this.chipNetCache = chipNets + return chipNets + } + + /** Determine voltage bias direction for a chip based on its net connections */ + private getVoltageBias(chipId: string): number { + const chipNets = this.getChipNets() + const nets = chipNets.get(chipId) + if (!nets) return 0 + + let bias = 0 + for (const netId of nets) { + const net = this.inputProblem.netMap[netId] + if (!net) continue + // Positive voltage sources should be biased upward (positive Y) + if (net.isPositiveVoltageSource) bias += 1 + // Ground should be biased downward (negative Y) + if (net.isGround) bias -= 1 + } + + return bias + } + + private applyForces( + overlaps: Array<{ + chip1: string + chip2: string + overlapX: number + overlapY: number + }>, + ) { + const forces: Record = {} + + for (const chipId of Object.keys(this.resolvedLayout!.chipPlacements)) { + forces[chipId] = { fx: 0, fy: 0 } + } + + // 1. Repulsion forces for overlapping chips + for (const overlap of overlaps) { + const p1 = this.resolvedLayout!.chipPlacements[overlap.chip1]! + const p2 = this.resolvedLayout!.chipPlacements[overlap.chip2]! + + let dx = p2.x - p1.x + let dy = p2.y - p1.y + const dist = Math.sqrt(dx * dx + dy * dy) + + if (dist < 0.001) { + dx = 1 + dy = 0 + } else { + dx /= dist + dy /= dist + } + + const pushX = overlap.overlapX * this.REPULSION_STRENGTH + const pushY = overlap.overlapY * this.REPULSION_STRENGTH + + if (overlap.overlapX < overlap.overlapY) { + const sign = dx >= 0 ? 1 : -1 + forces[overlap.chip1]!.fx -= (pushX / 2) * sign + forces[overlap.chip2]!.fx += (pushX / 2) * sign + } else { + const sign = dy >= 0 ? 1 : -1 + forces[overlap.chip1]!.fy -= (pushY / 2) * sign + forces[overlap.chip2]!.fy += (pushY / 2) * sign + } + } + + // 2. Attraction forces for connected chips (keep layout compact) + const connectedPairs = this.getConnectedChipPairs() + for (const pairKey of connectedPairs) { + const [c1, c2] = pairKey.split("-") + const p1 = this.resolvedLayout!.chipPlacements[c1!] + const p2 = this.resolvedLayout!.chipPlacements[c2!] + if (!p1 || !p2) continue + + const dx = p2.x - p1.x + const dy = p2.y - p1.y + const dist = Math.sqrt(dx * dx + dy * dy) + + if (dist > this.MIN_SEPARATION * 3) { + const attractX = dx * this.ATTRACTION_STRENGTH + const attractY = dy * this.ATTRACTION_STRENGTH + if (forces[c1!]) { + forces[c1!].fx += attractX + forces[c1!].fy += attractY + } + if (forces[c2!]) { + forces[c2!].fx -= attractX + forces[c2!].fy -= attractY + } + } + } + + // 3. Voltage-aware biasing: push chips toward conventional positions + // VCC-connected chips biased upward, GND-connected chips biased downward + for (const chipId of Object.keys(this.resolvedLayout!.chipPlacements)) { + const bias = this.getVoltageBias(chipId) + if (bias !== 0 && forces[chipId]) { + forces[chipId].fy += bias * this.VOLTAGE_BIAS_STRENGTH + } + } + + // Apply forces with damping that increases over iterations for convergence + const damping = Math.max( + 0.3, + 1.0 - this.resolveIterations / this.MAX_RESOLVE_ITERATIONS, + ) + for (const [chipId, force] of Object.entries(forces)) { + const placement = this.resolvedLayout!.chipPlacements[chipId] + if (!placement) continue + placement.x += force.fx * damping + placement.y += force.fy * damping + } + } + + /** Re-center the layout so the bounding box center is at the origin */ + private recenterLayout() { + if (!this.resolvedLayout) return + + const chipIds = Object.keys(this.resolvedLayout.chipPlacements) + if (chipIds.length === 0) return + + let minX = Infinity + let maxX = -Infinity + let minY = Infinity + let maxY = -Infinity + + for (const chipId of chipIds) { + const bounds = this.getChipBounds(chipId) + if (!bounds) continue + minX = Math.min(minX, bounds.minX) + maxX = Math.max(maxX, bounds.maxX) + minY = Math.min(minY, bounds.minY) + maxY = Math.max(maxY, bounds.maxY) + } + + const centerX = (minX + maxX) / 2 + const centerY = (minY + maxY) / 2 + + for (const chipId of chipIds) { + const placement = this.resolvedLayout.chipPlacements[chipId] + if (!placement) continue + placement.x -= centerX + placement.y -= centerY + } + } + + getOutputLayout(): OutputLayout { + if (!this.resolvedLayout) { + throw new Error("OverlapResolutionSolver not solved yet") + } + return this.resolvedLayout + } + + override visualize(): GraphicsObject { + const layout = this.resolvedLayout || this.layout + return visualizeInputProblem(this.inputProblem, layout) + } + + override getConstructorParams() { + return [{ inputProblem: this.inputProblem, layout: this.layout }] + } +} diff --git a/tests/OverlapResolutionSolver/OverlapResolutionSolver.test.ts b/tests/OverlapResolutionSolver/OverlapResolutionSolver.test.ts new file mode 100644 index 0000000..4b0a734 --- /dev/null +++ b/tests/OverlapResolutionSolver/OverlapResolutionSolver.test.ts @@ -0,0 +1,236 @@ +import { expect, test } from "bun:test" +import { LayoutPipelineSolver } from "lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver" +import type { InputProblem } from "lib/types/InputProblem" +import { normalizeSide } from "lib/types/Side" + +test("OverlapResolution - pipeline integrates overlap resolution phase", () => { + // Create a problem with multiple chips that are likely to overlap + const problem: InputProblem = { + chipMap: { + U1: { + chipId: "U1", + pins: ["U1.1", "U1.2", "U1.3", "U1.4"], + size: { x: 3.0, y: 2.0 }, + }, + C1: { + chipId: "C1", + pins: ["C1.1", "C1.2"], + size: { x: 1.0, y: 0.6 }, + availableRotations: [0, 180], + }, + C2: { + chipId: "C2", + pins: ["C2.1", "C2.2"], + size: { x: 1.0, y: 0.6 }, + availableRotations: [0, 180], + }, + R1: { + chipId: "R1", + pins: ["R1.1", "R1.2"], + size: { x: 1.0, y: 0.5 }, + availableRotations: [0, 90, 180, 270], + }, + }, + chipPinMap: { + "U1.1": { + pinId: "U1.1", + offset: { x: -1.5, y: -0.5 }, + side: normalizeSide("left"), + }, + "U1.2": { + pinId: "U1.2", + offset: { x: -1.5, y: 0.5 }, + side: normalizeSide("left"), + }, + "U1.3": { + pinId: "U1.3", + offset: { x: 1.5, y: -0.5 }, + side: normalizeSide("right"), + }, + "U1.4": { + pinId: "U1.4", + offset: { x: 1.5, y: 0.5 }, + side: normalizeSide("right"), + }, + "C1.1": { + pinId: "C1.1", + offset: { x: 0, y: -0.3 }, + side: normalizeSide("top"), + }, + "C1.2": { + pinId: "C1.2", + offset: { x: 0, y: 0.3 }, + side: normalizeSide("bottom"), + }, + "C2.1": { + pinId: "C2.1", + offset: { x: 0, y: -0.3 }, + side: normalizeSide("top"), + }, + "C2.2": { + pinId: "C2.2", + offset: { x: 0, y: 0.3 }, + side: normalizeSide("bottom"), + }, + "R1.1": { + pinId: "R1.1", + offset: { x: -0.5, y: 0 }, + side: normalizeSide("left"), + }, + "R1.2": { + pinId: "R1.2", + offset: { x: 0.5, y: 0 }, + side: normalizeSide("right"), + }, + }, + netMap: { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + }, + pinStrongConnMap: { + "U1.3-R1.1": true, + "R1.1-U1.3": true, + "U1.1-C1.1": true, + "C1.1-U1.1": true, + "U1.2-C2.1": true, + "C2.1-U1.2": true, + }, + netConnMap: { + "C1.2-GND": true, + "C2.2-GND": true, + "U1.4-VCC": true, + "R1.2-VCC": true, + }, + chipGap: 0.2, + partitionGap: 2, + } + + const solver = new LayoutPipelineSolver(problem) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + + // Verify overlap resolution phase ran + expect(solver.overlapResolutionSolver).toBeDefined() + expect(solver.overlapResolutionSolver!.solved).toBe(true) + + // Get the final layout + const layout = solver.getOutputLayout() + expect(layout).toBeDefined() + + // All chips should have placements + expect(layout.chipPlacements["U1"]).toBeDefined() + expect(layout.chipPlacements["C1"]).toBeDefined() + expect(layout.chipPlacements["C2"]).toBeDefined() + expect(layout.chipPlacements["R1"]).toBeDefined() + + // No overlaps in final layout + const overlaps = solver.checkForOverlaps(layout) + expect(overlaps.length).toBe(0) + + // Visualization should work + const viz = solver.visualize() + expect(viz).toBeDefined() + expect(viz.rects).toBeDefined() + expect(viz.rects!.length).toBeGreaterThan(0) +}) + +test("OverlapResolution - voltage biasing pushes VCC up and GND down", () => { + // Create a simple problem with clear voltage connections + const problem: InputProblem = { + chipMap: { + U1: { + chipId: "U1", + pins: ["U1.vcc", "U1.gnd", "U1.out"], + size: { x: 2.0, y: 2.0 }, + }, + CVCC: { + chipId: "CVCC", + pins: ["CVCC.1", "CVCC.2"], + size: { x: 1.0, y: 0.6 }, + availableRotations: [0, 180], + }, + CGND: { + chipId: "CGND", + pins: ["CGND.1", "CGND.2"], + size: { x: 1.0, y: 0.6 }, + availableRotations: [0, 180], + }, + }, + chipPinMap: { + "U1.vcc": { + pinId: "U1.vcc", + offset: { x: 0, y: 1.0 }, + side: normalizeSide("top"), + }, + "U1.gnd": { + pinId: "U1.gnd", + offset: { x: 0, y: -1.0 }, + side: normalizeSide("bottom"), + }, + "U1.out": { + pinId: "U1.out", + offset: { x: 1.0, y: 0 }, + side: normalizeSide("right"), + }, + "CVCC.1": { + pinId: "CVCC.1", + offset: { x: 0, y: -0.3 }, + side: normalizeSide("top"), + }, + "CVCC.2": { + pinId: "CVCC.2", + offset: { x: 0, y: 0.3 }, + side: normalizeSide("bottom"), + }, + "CGND.1": { + pinId: "CGND.1", + offset: { x: 0, y: -0.3 }, + side: normalizeSide("top"), + }, + "CGND.2": { + pinId: "CGND.2", + offset: { x: 0, y: 0.3 }, + side: normalizeSide("bottom"), + }, + }, + netMap: { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + }, + pinStrongConnMap: { + "U1.vcc-CVCC.1": true, + "CVCC.1-U1.vcc": true, + "U1.gnd-CGND.1": true, + "CGND.1-U1.gnd": true, + }, + netConnMap: { + "U1.vcc-VCC": true, + "CVCC.1-VCC": true, + "CVCC.2-GND": true, + "U1.gnd-GND": true, + "CGND.1-GND": true, + "CGND.2-GND": true, + }, + chipGap: 0.2, + partitionGap: 2, + } + + const solver = new LayoutPipelineSolver(problem) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + + const layout = solver.getOutputLayout() + + // No overlaps + const overlaps = solver.checkForOverlaps(layout) + expect(overlaps.length).toBe(0) + + // VCC-connected cap should be above GND-connected cap (higher Y) + const cvccY = layout.chipPlacements["CVCC"]!.y + const cgndY = layout.chipPlacements["CGND"]!.y + expect(cvccY).toBeGreaterThan(cgndY) +})