From ed2c8f81aac468427de13ee0a3e821b3837fd17a Mon Sep 17 00:00:00 2001 From: Sagar Maurya Date: Sat, 13 Jun 2026 22:30:31 +0530 Subject: [PATCH] feat: implement vertical schematic layout bias for power and ground nets --- .../SingleInnerPartitionPackingSolver.ts | 50 ++++++++++++- .../PartitionPackingSolver.ts | 51 ++++++++++++- lib/utils/netBiasUtils.ts | 30 ++++++++ lib/utils/networkFiltering.ts | 36 ++++----- .../LayoutPipelineSolverPowerBias.test.ts | 73 +++++++++++++++++++ 5 files changed, 218 insertions(+), 22 deletions(-) create mode 100644 lib/utils/netBiasUtils.ts create mode 100644 tests/LayoutPipelineSolver/LayoutPipelineSolverPowerBias.test.ts diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index d5560860..6e827bd7 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -17,6 +17,7 @@ import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputPro import { createFilteredNetworkMapping } from "../../utils/networkFiltering" import { getPadsBoundingBox } from "./getPadsBoundingBox" import { doBasicInputProblemLayout } from "../LayoutPipelineSolver/doBasicInputProblemLayout" +import { isGroundNet, isPositiveVoltageNet } from "../../utils/netBiasUtils" const PIN_SIZE = 0.1 @@ -129,13 +130,59 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } }) + // Add static attractor components for power/ground nets + const activeNets = new Set() + for (const netId of pinToNetworkMap.values()) { + activeNets.add(netId) + } + + const attractorComponents: any[] = [] + for (const netId of activeNets) { + const net = this.partitionInputProblem.netMap[netId] + if (isPositiveVoltageNet(netId, net)) { + attractorComponents.push({ + componentId: `attractor_vcc_${netId}`, + pads: [ + { + padId: `attractor_vcc_${netId}_pad`, + networkId: netId, + type: "rect" as const, + offset: { x: 0, y: 0 }, + size: { x: PIN_SIZE, y: PIN_SIZE }, + }, + ], + availableRotationDegrees: [0] as Array<0 | 90 | 180 | 270>, + isStatic: true as const, + center: { x: 0, y: -100 }, + ccwRotationOffset: 0, + }) + } else if (isGroundNet(netId, net)) { + attractorComponents.push({ + componentId: `attractor_gnd_${netId}`, + pads: [ + { + padId: `attractor_gnd_${netId}_pad`, + networkId: netId, + type: "rect" as const, + offset: { x: 0, y: 0 }, + size: { x: PIN_SIZE, y: PIN_SIZE }, + }, + ], + availableRotationDegrees: [0] as Array<0 | 90 | 180 | 270>, + isStatic: true as const, + center: { x: 0, y: 100 }, + ccwRotationOffset: 0, + }) + } + } + let minGap = this.partitionInputProblem.chipGap if (this.partitionInputProblem.partitionType === "decoupling_caps") { minGap = this.partitionInputProblem.decouplingCapsGap ?? minGap } return { - components: packComponents, + components: [...packComponents, ...attractorComponents], minGap, packOrderStrategy: "largest_to_smallest", packPlacementStrategy: "minimum_closest_sum_squared_distance", @@ -149,6 +196,7 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { for (const packedComponent of packedComponents) { const chipId = packedComponent.componentId + if (chipId.startsWith("attractor_")) continue chipPlacements[chipId] = { x: packedComponent.center.x, diff --git a/lib/solvers/PartitionPackingSolver/PartitionPackingSolver.ts b/lib/solvers/PartitionPackingSolver/PartitionPackingSolver.ts index e04353be..bd30c2c0 100644 --- a/lib/solvers/PartitionPackingSolver/PartitionPackingSolver.ts +++ b/lib/solvers/PartitionPackingSolver/PartitionPackingSolver.ts @@ -10,6 +10,7 @@ import type { OutputLayout, Placement } from "../../types/OutputLayout" import type { InputProblem, PinId, NetId } from "../../types/InputProblem" import { visualizeInputProblem } from "../LayoutPipelineSolver/visualizeInputProblem" import type { PackedPartition } from "../PackInnerPartitionsSolver/PackInnerPartitionsSolver" +import { isGroundNet, isPositiveVoltageNet } from "../../utils/netBiasUtils" export interface PartitionPackingSolverInput { packedPartitions: PackedPartition[] @@ -261,8 +262,54 @@ export class PartitionPackingSolver extends BaseSolver { } }) + // Add static attractor components for power/ground nets + const activeNets = new Set() + for (const netId of pinToNetworkMap.values()) { + activeNets.add(netId) + } + + const attractorComponents: any[] = [] + for (const netId of activeNets) { + const net = this.inputProblem.netMap[netId] + if (isPositiveVoltageNet(netId, net)) { + attractorComponents.push({ + componentId: `attractor_vcc_${netId}`, + pads: [ + { + padId: `attractor_vcc_${netId}_pad`, + networkId: netId, + type: "rect" as const, + offset: { x: 0, y: 0 }, + size: { x: 0.1, y: 0.1 }, + }, + ], + availableRotationDegrees: [0] as Array<0 | 90 | 180 | 270>, + isStatic: true as const, + center: { x: 0, y: -100 }, + ccwRotationOffset: 0, + }) + } else if (isGroundNet(netId, net)) { + attractorComponents.push({ + componentId: `attractor_gnd_${netId}`, + pads: [ + { + padId: `attractor_gnd_${netId}_pad`, + networkId: netId, + type: "rect" as const, + offset: { x: 0, y: 0 }, + size: { x: 0.1, y: 0.1 }, + }, + ], + availableRotationDegrees: [0] as Array<0 | 90 | 180 | 270>, + isStatic: true as const, + center: { x: 0, y: 100 }, + ccwRotationOffset: 0, + }) + } + } + return { - components: packComponents, + components: [...packComponents, ...attractorComponents], minGap: this.inputProblem.partitionGap, packOrderStrategy: "largest_to_smallest", packPlacementStrategy: "minimum_sum_squared_distance_to_network", @@ -277,6 +324,8 @@ export class PartitionPackingSolver extends BaseSolver { const newChipPlacements: Record = {} for (const packedComponent of packedComponents) { + if (packedComponent.componentId.startsWith("attractor_")) continue + const partitionIndex = parseInt( packedComponent.componentId.replace("partition_", ""), ) diff --git a/lib/utils/netBiasUtils.ts b/lib/utils/netBiasUtils.ts new file mode 100644 index 00000000..aeb918ad --- /dev/null +++ b/lib/utils/netBiasUtils.ts @@ -0,0 +1,30 @@ +import type { Net, NetId } from "../types/InputProblem" + +export function isGroundNet(netId: NetId, net?: Net): boolean { + if (net?.isGround) return true + const lower = netId.toLowerCase() + return lower.includes("gnd") || lower.includes("vss") +} + +export function isPositiveVoltageNet(netId: NetId, net?: Net): boolean { + if (isGroundNet(netId, net)) return false + if (net?.isPositiveVoltageSource) return true + + const lower = netId.toLowerCase() + // Common positive voltage names + if ( + lower.includes("vcc") || + lower.includes("vdd") || + lower.includes("vsys") || + lower === "v+" || + lower.startsWith("+") + ) { + return true + } + // Matches like "v3_3", "v1_1", "v5", "3.3v", "5v", "3v3", "12v" + if (/^v\d/.test(lower)) return true + if (/\d+v/.test(lower)) return true + if (/\d+v\d+/.test(lower)) return true + + return false +} diff --git a/lib/utils/networkFiltering.ts b/lib/utils/networkFiltering.ts index df3c7c64..85b49a91 100644 --- a/lib/utils/networkFiltering.ts +++ b/lib/utils/networkFiltering.ts @@ -6,6 +6,7 @@ */ import type { ChipPin, InputProblem, PinId } from "../types/InputProblem" +import { isGroundNet, isPositiveVoltageNet } from "./netBiasUtils" export interface NetworkFilteringResult { /** Map from pinId to networkId, with filtered networks marked as disconnected */ @@ -68,27 +69,22 @@ export function createFilteredNetworkMapping(params: { } // Process net connections - if (hasStrongConnections) { - // If any strong connections exist anywhere in the problem, filter out all weak (pin-to-net) connections - for (const [connKey, connected] of Object.entries( - inputProblem.netConnMap, - )) { - if (!connected) continue - const [pinId, netId] = connKey.split("-") - if (pinId && netId) { - // Do not assign a network for weak connections when strong connections are present. - // Mark this pin as filtered so callers can inspect what was ignored. + for (const [connKey, connected] of Object.entries(inputProblem.netConnMap)) { + if (!connected) continue + const [pinId, netId] = connKey.split("-") + if (pinId && netId) { + const net = inputProblem.netMap[netId] + const isPowerOrGround = + isGroundNet(netId, net) || isPositiveVoltageNet(netId, net) + + if (isPowerOrGround) { + // Always preserve power and ground nets + pinToNetworkMap.set(pinId, netId) + } else if (hasStrongConnections) { + // Filter out other weak (pin-to-net) connections when strong connections exist filteredPins.add(pinId) - } - } - } else { - // No strong connections exist; include weak connections with basic opposite-side filtering - for (const [connKey, connected] of Object.entries( - inputProblem.netConnMap, - )) { - if (!connected) continue - const [pinId, netId] = connKey.split("-") - if (pinId && netId) { + } else { + // No strong connections exist; include weak connections with basic opposite-side filtering const pin = inputProblem.chipPinMap[pinId] if (!pin) continue diff --git a/tests/LayoutPipelineSolver/LayoutPipelineSolverPowerBias.test.ts b/tests/LayoutPipelineSolver/LayoutPipelineSolverPowerBias.test.ts new file mode 100644 index 00000000..afcc4b38 --- /dev/null +++ b/tests/LayoutPipelineSolver/LayoutPipelineSolverPowerBias.test.ts @@ -0,0 +1,73 @@ +import { expect, test } from "bun:test" +import { LayoutPipelineSolver } from "lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver" +import { getInputProblemFromCircuitJsonSchematic } from "lib/testing/getInputProblemFromCircuitJsonSchematic" +import { getExampleCircuitJson } from "../assets/ExampleCircuit04" + +test("LayoutPipelineSolverPowerBias - verifies VCC and GND sorting", () => { + const circuitJson = getExampleCircuitJson() + const problem = getInputProblemFromCircuitJsonSchematic(circuitJson, { + useReadableIds: true, + }) + + // Create solver and run the pipeline + const solver = new LayoutPipelineSolver(problem) + solver.solve() + expect(solver.solved).toBe(true) + + const finalLayout = solver.getOutputLayout() + expect(finalLayout).toBeDefined() + + // Let's verify that decoupling capacitors C1, C2, C5, C6 + // are oriented such that their positive pins (Pin 1) are above their ground pins (Pin 2). + // In schematic layouts, negative Y is upward. So absolutePin1Y should be less than absolutePin2Y. + for (const capId of ["C1", "C2", "C5", "C6"]) { + const placement = finalLayout.chipPlacements[capId] + expect(placement).toBeDefined() + + const chip = problem.chipMap[capId] + expect(chip).toBeDefined() + + // Get pin 1 and pin 2 + const pin1Id = chip!.pins.find((p) => p.endsWith(".1")) + const pin2Id = chip!.pins.find((p) => p.endsWith(".2")) + + expect(pin1Id).toBeDefined() + expect(pin2Id).toBeDefined() + + const pin1 = problem.chipPinMap[pin1Id!] + const pin2 = problem.chipPinMap[pin2Id!] + + expect(pin1).toBeDefined() + expect(pin2).toBeDefined() + + // Calculate rotated offset for pin 1 + let rotatedPin1Offset = { x: pin1!.offset.x, y: pin1!.offset.y } + const rot = placement!.ccwRotationDegrees ?? 0 + if (rot === 90) { + rotatedPin1Offset = { x: -pin1!.offset.y, y: pin1!.offset.x } + } else if (rot === 180) { + rotatedPin1Offset = { x: -pin1!.offset.x, y: -pin1!.offset.y } + } else if (rot === 270) { + rotatedPin1Offset = { x: pin1!.offset.y, y: -pin1!.offset.x } + } + + // Calculate rotated offset for pin 2 + let rotatedPin2Offset = { x: pin2!.offset.x, y: pin2!.offset.y } + if (rot === 90) { + rotatedPin2Offset = { x: -pin2!.offset.y, y: pin2!.offset.x } + } else if (rot === 180) { + rotatedPin2Offset = { x: -pin2!.offset.x, y: -pin2!.offset.y } + } else if (rot === 270) { + rotatedPin2Offset = { x: pin2!.offset.y, y: -pin2!.offset.x } + } + + const absolutePin1Y = placement!.y + rotatedPin1Offset.y + const absolutePin2Y = placement!.y + rotatedPin2Offset.y + + // Pin 1 (positive net) should be above (more negative Y) than Pin 2 (GND) + console.log( + `${capId} absolute pin Y: Pin 1 (VCC) = ${absolutePin1Y}, Pin 2 (GND) = ${absolutePin2Y}`, + ) + expect(absolutePin1Y).toBeLessThan(absolutePin2Y) + } +})