diff --git a/lib/solvers/ChipPartitionsSolver/ChipPartitionsSolver.ts b/lib/solvers/ChipPartitionsSolver/ChipPartitionsSolver.ts index 8335b60..16ee11a 100644 --- a/lib/solvers/ChipPartitionsSolver/ChipPartitionsSolver.ts +++ b/lib/solvers/ChipPartitionsSolver/ChipPartitionsSolver.ts @@ -49,7 +49,10 @@ export class ChipPartitionsSolver extends BaseSolver { // 1) Build decoupling-cap-only partitions (exclude the main chip for each group) const decapChipIdSet = new Set() - const decapGroupPartitions: ChipId[][] = [] + const decapGroupPartitions: Array<{ + chipIds: ChipId[] + group: DecouplingCapGroup + }> = [] if (this.decouplingCapGroups && this.decouplingCapGroups.length > 0) { for (const group of this.decouplingCapGroups) { @@ -61,7 +64,7 @@ export class ChipPartitionsSolver extends BaseSolver { } // Only add a partition if there are at least two caps present in the inputProblem if (capsOnly.length >= 2) { - decapGroupPartitions.push(capsOnly) + decapGroupPartitions.push({ chipIds: capsOnly, group }) // Mark these caps as handled by decoupling-cap partitions for (const capId of capsOnly) { decapChipIdSet.add(capId) @@ -119,8 +122,11 @@ export class ChipPartitionsSolver extends BaseSolver { return [ ...decapGroupPartitions.map((partition) => - this.createInputProblemFromPartition(partition, inputProblem, { + this.createInputProblemFromPartition(partition.chipIds, inputProblem, { partitionType: "decoupling_caps", + decouplingCapGroupId: partition.group.decouplingCapGroupId, + decouplingCapMainChipId: partition.group.mainChipId, + decouplingCapNetPair: partition.group.netPair, }), ), ...nonDecapPartitions.map((partition) => @@ -188,6 +194,9 @@ export class ChipPartitionsSolver extends BaseSolver { originalProblem: InputProblem, opts?: { partitionType?: "default" | "decoupling_caps" + decouplingCapGroupId?: string + decouplingCapMainChipId?: ChipId + decouplingCapNetPair?: [NetId, NetId] }, ): PartitionInputProblem { const chipIds = partition @@ -242,6 +251,19 @@ export class ChipPartitionsSolver extends BaseSolver { } } + if ( + opts?.partitionType === "decoupling_caps" && + opts.decouplingCapNetPair + ) { + this.copyInheritedDecouplingNetConnections({ + originalProblem, + relevantPinIds, + allowedNetIds: new Set(opts.decouplingCapNetPair), + relevantNetIds, + netConnMap, + }) + } + for (const netId of relevantNetIds) { if (originalProblem.netMap[netId]) { netMap[netId] = originalProblem.netMap[netId] @@ -257,6 +279,58 @@ export class ChipPartitionsSolver extends BaseSolver { netConnMap, isPartition: true, partitionType: opts?.partitionType, + decouplingCapGroupId: opts?.decouplingCapGroupId, + decouplingCapMainChipId: opts?.decouplingCapMainChipId, + decouplingCapNetPair: opts?.decouplingCapNetPair, + } + } + + private copyInheritedDecouplingNetConnections({ + originalProblem, + relevantPinIds, + allowedNetIds, + relevantNetIds, + netConnMap, + }: { + originalProblem: InputProblem + relevantPinIds: Set + allowedNetIds: Set + relevantNetIds: Set + netConnMap: Record<`${PinId}-${NetId}`, boolean> + }) { + const copyAllowedExternalNets = ( + decapPinId: PinId, + externalPinId: PinId, + ) => { + for (const [connKey, isConnected] of Object.entries( + originalProblem.netConnMap, + )) { + if (!isConnected) continue + + const [pinId, netId] = connKey.split("-") as [PinId, NetId] + if (pinId !== externalPinId || !allowedNetIds.has(netId)) continue + + relevantNetIds.add(netId) + netConnMap[`${decapPinId}-${netId}`] = true + } + } + + for (const [connKey, isConnected] of Object.entries( + originalProblem.pinStrongConnMap, + )) { + if (!isConnected) continue + + const [pinAId, pinBId] = connKey.split("-") as [PinId, PinId] + const pinAInPartition = relevantPinIds.has(pinAId) + const pinBInPartition = relevantPinIds.has(pinBId) + + if (pinAInPartition === pinBInPartition) continue + + if (pinAInPartition) { + copyAllowedExternalNets(pinAId, pinBId) + } else { + copyAllowedExternalNets(pinBId, pinAId) + } } } diff --git a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts index 88db103..fd728d1 100644 --- a/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts +++ b/lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver.ts @@ -12,6 +12,7 @@ import type { PinId, ChipId, NetId, + Chip, ChipPin, PartitionInputProblem, } from "../../types/InputProblem" @@ -21,6 +22,68 @@ import { getPadsBoundingBox } from "./getPadsBoundingBox" import { doBasicInputProblemLayout } from "../LayoutPipelineSolver/doBasicInputProblemLayout" const PIN_SIZE = 0.1 +type LayoutAxis = "x" | "y" + +type DecouplingCapLayoutEntry = { + chipId: ChipId + chip: Chip + rotation: 0 | 90 | 180 | 270 + footprintSize: { x: number; y: number } + preferredMainPin: ChipPin | null +} + +const compareNaturalChipIds = (a: ChipId, b: ChipId) => { + const aParts = a.match(/\d+|\D+/g) ?? [a] + const bParts = b.match(/\d+|\D+/g) ?? [b] + const len = Math.min(aParts.length, bParts.length) + + for (let i = 0; i < len; i++) { + const aPart = aParts[i]! + const bPart = bParts[i]! + const aNum = Number(aPart) + const bNum = Number(bPart) + const aIsNumber = Number.isInteger(aNum) + const bIsNumber = Number.isInteger(bNum) + + if (aIsNumber && bIsNumber && aNum !== bNum) { + return aNum - bNum + } + + if (aPart !== bPart) return aPart.localeCompare(bPart) + } + + return aParts.length - bParts.length +} + +const getChipIdFromPinId = (pinId: PinId): ChipId => + pinId.split(".")[0] ?? pinId + +const getPreferredRotation = (chip: Chip): 0 | 90 | 180 | 270 => { + if (!chip.availableRotations?.length) return 0 + return chip.availableRotations.includes(0) ? 0 : chip.availableRotations[0]! +} + +const getAxisForExternalSide = ( + side: ChipPin["side"] | undefined, +): LayoutAxis | null => { + if (side?.startsWith("x")) return "y" + if (side?.startsWith("y")) return "x" + return null +} + +const rotatePoint = ( + point: { x: number; y: number }, + degrees: number, +): { x: number; y: number } => { + const radians = (degrees * Math.PI) / 180 + const cos = Math.cos(radians) + const sin = Math.sin(radians) + + return { + x: point.x * cos - point.y * sin, + y: point.x * sin + point.y * cos, + } +} export class SingleInnerPartitionPackingSolver extends BaseSolver { partitionInputProblem: PartitionInputProblem @@ -38,6 +101,13 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } override _step() { + if (this.partitionInputProblem.partitionType === "decoupling_caps") { + this.layout = this.createDecouplingCapsLayout() + this.solved = true + this.activeSubSolver = null + return + } + // Initialize PackSolver2 if not already created if (!this.activeSubSolver) { const packInput = this.createPackInput() @@ -64,6 +134,189 @@ export class SingleInnerPartitionPackingSolver extends BaseSolver { } } + private createDecouplingCapsLayout(): OutputLayout { + const entries: DecouplingCapLayoutEntry[] = Object.entries( + this.partitionInputProblem.chipMap, + ).map(([chipId, chip]) => { + const rotation = getPreferredRotation(chip) + return { + chipId, + chip, + rotation, + footprintSize: this.getChipFootprintSize(chip, rotation), + preferredMainPin: this.getPreferredExternalMainPin(chipId, chip), + } + }) + + const layoutAxis = this.getDecouplingCapsLayoutAxis(entries) + entries.sort((a, b) => { + const aHasMainPin = a.preferredMainPin ? 1 : 0 + const bHasMainPin = b.preferredMainPin ? 1 : 0 + if (aHasMainPin !== bHasMainPin) return bHasMainPin - aHasMainPin + + const aCoordinate = this.getSortCoordinate(a, layoutAxis) + const bCoordinate = this.getSortCoordinate(b, layoutAxis) + if (aCoordinate !== bCoordinate) return aCoordinate - bCoordinate + + return compareNaturalChipIds(a.chipId, b.chipId) + }) + + const gap = + this.partitionInputProblem.decouplingCapsGap ?? + this.partitionInputProblem.chipGap + const totalSpan = + entries.reduce((sum, entry) => sum + entry.footprintSize[layoutAxis], 0) + + Math.max(0, entries.length - 1) * gap + + const chipPlacements: Record = {} + let cursor = -totalSpan / 2 + + for (const entry of entries) { + const span = entry.footprintSize[layoutAxis] + const center = cursor + span / 2 + chipPlacements[entry.chipId] = { + x: layoutAxis === "x" ? center : 0, + y: layoutAxis === "y" ? center : 0, + ccwRotationDegrees: entry.rotation, + } + cursor += span + gap + } + + this.stats.decouplingCapsLayout = { + axis: layoutAxis, + gap, + order: entries.map((entry) => entry.chipId), + mainChipId: this.partitionInputProblem.decouplingCapMainChipId ?? null, + } + + return { + chipPlacements, + groupPlacements: {}, + } + } + + private getDecouplingCapsLayoutAxis( + entries: DecouplingCapLayoutEntry[], + ): LayoutAxis { + const axisVotes = entries.reduce( + (votes, entry) => { + const axis = getAxisForExternalSide(entry.preferredMainPin?.side) + if (axis) votes[axis] += 1 + return votes + }, + { x: 0, y: 0 }, + ) + + return axisVotes.y > axisVotes.x ? "y" : "x" + } + + private getSortCoordinate( + entry: DecouplingCapLayoutEntry, + fallbackAxis: LayoutAxis, + ) { + const mainPin = entry.preferredMainPin + if (!mainPin) return 0 + + const sideAwareAxis = getAxisForExternalSide(mainPin.side) ?? fallbackAxis + return mainPin.offset[sideAwareAxis] + } + + private getPreferredExternalMainPin( + chipId: ChipId, + chip: Chip, + ): ChipPin | null { + const mainChipId = this.partitionInputProblem.decouplingCapMainChipId + const candidates: Array<{ + capPinId: PinId + externalPin: ChipPin + isPositiveVoltage: boolean + isGround: boolean + }> = [] + + for (const pinId of chip.pins) { + const externalPins = this.pinIdToStronglyConnectedPins[pinId] ?? [] + for (const externalPin of externalPins) { + const externalChipId = getChipIdFromPinId(externalPin.pinId) + if (externalChipId === chipId) continue + if (mainChipId && externalChipId !== mainChipId) continue + + const netRole = this.getPinNetRole(pinId) + candidates.push({ + capPinId: pinId, + externalPin, + isPositiveVoltage: netRole.isPositiveVoltage, + isGround: netRole.isGround, + }) + } + } + + candidates.sort((a, b) => { + if (a.isPositiveVoltage !== b.isPositiveVoltage) { + return a.isPositiveVoltage ? -1 : 1 + } + if (a.isGround !== b.isGround) return a.isGround ? -1 : 1 + + const capPinCompare = a.capPinId.localeCompare(b.capPinId) + if (capPinCompare !== 0) return capPinCompare + + return a.externalPin.pinId.localeCompare(b.externalPin.pinId) + }) + + return candidates[0]?.externalPin ?? null + } + + private getPinNetRole(pinId: PinId) { + const role = { + isPositiveVoltage: false, + isGround: false, + } + + for (const [connKey, isConnected] of Object.entries( + this.partitionInputProblem.netConnMap, + )) { + if (!isConnected) continue + + const [connectedPinId, netId] = connKey.split("-") as [PinId, NetId] + if (connectedPinId !== pinId) continue + + const net = this.partitionInputProblem.netMap[netId] + role.isPositiveVoltage ||= Boolean(net?.isPositiveVoltageSource) + role.isGround ||= Boolean(net?.isGround) + } + + return role + } + + private getChipFootprintSize(chip: Chip, rotation: number) { + const points: Array<{ x: number; y: number }> = [ + { x: -chip.size.x / 2, y: -chip.size.y / 2 }, + { x: -chip.size.x / 2, y: chip.size.y / 2 }, + { x: chip.size.x / 2, y: -chip.size.y / 2 }, + { x: chip.size.x / 2, y: chip.size.y / 2 }, + ] + + for (const pinId of chip.pins) { + const pin = this.partitionInputProblem.chipPinMap[pinId] + if (!pin) continue + + points.push( + { x: pin.offset.x - PIN_SIZE / 2, y: pin.offset.y - PIN_SIZE / 2 }, + { x: pin.offset.x - PIN_SIZE / 2, y: pin.offset.y + PIN_SIZE / 2 }, + { x: pin.offset.x + PIN_SIZE / 2, y: pin.offset.y - PIN_SIZE / 2 }, + { x: pin.offset.x + PIN_SIZE / 2, y: pin.offset.y + PIN_SIZE / 2 }, + ) + } + + const rotatedPoints = points.map((point) => rotatePoint(point, rotation)) + const xs = rotatedPoints.map((point) => point.x) + const ys = rotatedPoints.map((point) => point.y) + + return { + x: Math.max(...xs) - Math.min(...xs), + y: Math.max(...ys) - Math.min(...ys), + } + } + private createPackInput(): PackInput { // Fall back to filtered mapping (weak + strong) const pinToNetworkMap = createFilteredNetworkMapping({ diff --git a/lib/types/InputProblem.ts b/lib/types/InputProblem.ts index 38e9f75..677d4bc 100644 --- a/lib/types/InputProblem.ts +++ b/lib/types/InputProblem.ts @@ -49,4 +49,7 @@ export type InputProblem = { export interface PartitionInputProblem extends InputProblem { isPartition?: true partitionType?: "default" | "decoupling_caps" + decouplingCapGroupId?: string + decouplingCapMainChipId?: ChipId + decouplingCapNetPair?: [NetId, NetId] } diff --git a/package.json b/package.json index 1fd506e..a0197c5 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "bpc-graph": "^0.0.66", "calculate-packing": "^0.0.31", "circuit-json": "^0.0.226", + "circuit-to-svg": "0.0.345", "graphics-debug": "^0.0.64", "react-cosmos": "^7.0.0", "react-cosmos-plugin-vite": "^7.0.0", diff --git a/tests/ChipPartitionsSolver.test.ts b/tests/ChipPartitionsSolver.test.ts index d8eb08c..308a9a4 100644 --- a/tests/ChipPartitionsSolver.test.ts +++ b/tests/ChipPartitionsSolver.test.ts @@ -188,3 +188,113 @@ test("ChipPartitionsSolver visualization contains partition components", () => { expect(visualization.rects?.length).toBeGreaterThan(0) expect(visualization.texts?.length).toBeGreaterThan(0) }) + +test("ChipPartitionsSolver preserves decoupling group metadata and inherited main-chip nets", () => { + const inputProblem: InputProblem = { + chipMap: { + U1: { + chipId: "U1", + pins: ["U1.VCC", "U1.GND", "U1.SIG"], + size: { x: 3, y: 3 }, + }, + C1: { + chipId: "C1", + pins: ["C1.VCC", "C1.GND"], + size: { x: 0.4, y: 0.8 }, + }, + C2: { + chipId: "C2", + pins: ["C2.VCC", "C2.GND"], + size: { x: 0.4, y: 0.8 }, + }, + }, + chipPinMap: { + "U1.VCC": { + pinId: "U1.VCC", + offset: { x: 1.5, y: 1 }, + side: "x+", + }, + "U1.GND": { + pinId: "U1.GND", + offset: { x: -1.5, y: -1 }, + side: "x-", + }, + "U1.SIG": { + pinId: "U1.SIG", + offset: { x: 0, y: 1.5 }, + side: "y+", + }, + "C1.VCC": { + pinId: "C1.VCC", + offset: { x: 0, y: 0.4 }, + side: "y+", + }, + "C1.GND": { + pinId: "C1.GND", + offset: { x: 0, y: -0.4 }, + side: "y-", + }, + "C2.VCC": { + pinId: "C2.VCC", + offset: { x: 0, y: 0.4 }, + side: "y+", + }, + "C2.GND": { + pinId: "C2.GND", + offset: { x: 0, y: -0.4 }, + side: "y-", + }, + }, + netMap: { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + SIG: { netId: "SIG" }, + }, + pinStrongConnMap: { + "C1.VCC-U1.VCC": true, + "U1.VCC-C1.VCC": true, + "C1.GND-U1.GND": true, + "U1.GND-C1.GND": true, + "C2.VCC-U1.VCC": true, + "U1.VCC-C2.VCC": true, + "C2.GND-U1.GND": true, + "U1.GND-C2.GND": true, + }, + netConnMap: { + "U1.VCC-VCC": true, + "U1.GND-GND": true, + "U1.SIG-SIG": true, + }, + chipGap: 0.2, + partitionGap: 1, + } + + const solver = new ChipPartitionsSolver({ + inputProblem, + decouplingCapGroups: [ + { + decouplingCapGroupId: "decap_group_U1__GND__VCC", + mainChipId: "U1", + netPair: ["GND", "VCC"], + decouplingCapChipIds: ["C1", "C2"], + }, + ], + }) + + solver.solve() + + const decapPartition = solver.partitions.find( + (partition) => partition.partitionType === "decoupling_caps", + ) + + expect(decapPartition).toBeDefined() + expect(decapPartition!.decouplingCapMainChipId).toBe("U1") + expect(decapPartition!.decouplingCapNetPair).toEqual(["GND", "VCC"]) + expect(Object.keys(decapPartition!.chipMap).sort()).toEqual(["C1", "C2"]) + expect(Object.keys(decapPartition!.netMap).sort()).toEqual(["GND", "VCC"]) + expect(decapPartition!.netConnMap["C1.VCC-VCC"]).toBe(true) + expect(decapPartition!.netConnMap["C1.GND-GND"]).toBe(true) + expect(decapPartition!.netConnMap["C2.VCC-VCC"]).toBe(true) + expect(decapPartition!.netConnMap["C2.GND-GND"]).toBe(true) + expect(decapPartition!.netConnMap["C1.VCC-SIG"]).toBeUndefined() +}) diff --git a/tests/PackInnerPartitionsSolver/decoupling-cap-layout.test.ts b/tests/PackInnerPartitionsSolver/decoupling-cap-layout.test.ts new file mode 100644 index 0000000..57036d7 --- /dev/null +++ b/tests/PackInnerPartitionsSolver/decoupling-cap-layout.test.ts @@ -0,0 +1,180 @@ +import { expect, test } from "bun:test" +import { SingleInnerPartitionPackingSolver } from "../../lib/solvers/PackInnerPartitionsSolver/SingleInnerPartitionPackingSolver" +import type { + ChipPin, + PartitionInputProblem, + PinId, +} from "../../lib/types/InputProblem" + +const makeMainPin = ( + pinId: PinId, + offset: { x: number; y: number }, + side: ChipPin["side"], +): ChipPin => ({ + pinId, + offset, + side, +}) + +const summarizeVisual = (solver: SingleInnerPartitionPackingSolver) => { + const graphics = solver.visualize() + const format = (value: number) => + Math.abs(value) < 0.005 ? "0.00" : value.toFixed(2) + const compareLabels = (a: unknown, b: unknown) => + String(a).localeCompare(String(b), undefined, { numeric: true }) + + return { + rects: (graphics.rects ?? []) + .map((rect) => ({ + label: rect.label, + center: `${format(rect.center.x)},${format(rect.center.y)}`, + size: `${format(rect.width)}x${format(rect.height)}`, + })) + .sort((a, b) => compareLabels(a.label, b.label)), + points: (graphics.points ?? []) + .map((point) => ({ + label: point.label, + at: `${format(point.x)},${format(point.y)}`, + })) + .sort((a, b) => compareLabels(a.label, b.label)), + } +} + +const makeCapOnlyPartition = (): PartitionInputProblem => ({ + chipMap: { + C10: { + chipId: "C10", + pins: ["C10.VCC", "C10.GND"], + size: { x: 0.2, y: 0.2 }, + availableRotations: [0], + }, + C2: { + chipId: "C2", + pins: ["C2.VCC", "C2.GND"], + size: { x: 0.2, y: 0.2 }, + availableRotations: [0], + }, + C1: { + chipId: "C1", + pins: ["C1.VCC", "C1.GND"], + size: { x: 0.2, y: 0.2 }, + availableRotations: [0], + }, + }, + chipPinMap: { + "C10.VCC": { pinId: "C10.VCC", offset: { x: -0.6, y: 0 }, side: "x-" }, + "C10.GND": { pinId: "C10.GND", offset: { x: 0.6, y: 0 }, side: "x+" }, + "C2.VCC": { pinId: "C2.VCC", offset: { x: -0.6, y: 0 }, side: "x-" }, + "C2.GND": { pinId: "C2.GND", offset: { x: 0.6, y: 0 }, side: "x+" }, + "C1.VCC": { pinId: "C1.VCC", offset: { x: -0.6, y: 0 }, side: "x-" }, + "C1.GND": { pinId: "C1.GND", offset: { x: 0.6, y: 0 }, side: "x+" }, + }, + netMap: { + VCC: { netId: "VCC", isPositiveVoltageSource: true }, + GND: { netId: "GND", isGround: true }, + }, + pinStrongConnMap: {}, + netConnMap: { + "C10.VCC-VCC": true, + "C10.GND-GND": true, + "C2.VCC-VCC": true, + "C2.GND-GND": true, + "C1.VCC-VCC": true, + "C1.GND-GND": true, + }, + chipGap: 0.5, + decouplingCapsGap: 0.25, + partitionGap: 1, + isPartition: true, + partitionType: "decoupling_caps", +}) + +test("decoupling cap partitions use a one-step centered visual row from the pin envelope", () => { + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: makeCapOnlyPartition(), + pinIdToStronglyConnectedPins: {}, + }) + + solver.step() + + expect(solver.solved).toBe(true) + expect(solver.activeSubSolver).toBeNull() + + const placements = solver.layout!.chipPlacements + expect(Object.keys(placements)).toEqual(["C1", "C2", "C10"]) + expect(placements.C1!.x).toBeCloseTo(-1.55) + expect(placements.C2!.x).toBeCloseTo(0) + expect(placements.C10!.x).toBeCloseTo(1.55) + expect(placements.C1!.y).toBeCloseTo(0) + expect(placements.C2!.y).toBeCloseTo(0) + expect(placements.C10!.y).toBeCloseTo(0) + + expect(summarizeVisual(solver)).toMatchInlineSnapshot(` + { + "points": [ + { + "at": "-0.95,0.00", + "label": "C1.GND (GND)", + }, + { + "at": "-2.15,0.00", + "label": "C1.VCC (VCC)", + }, + { + "at": "0.60,0.00", + "label": "C2.GND (GND)", + }, + { + "at": "-0.60,0.00", + "label": "C2.VCC (VCC)", + }, + { + "at": "2.15,0.00", + "label": "C10.GND (GND)", + }, + { + "at": "0.95,0.00", + "label": "C10.VCC (VCC)", + }, + ], + "rects": [ + { + "center": "-1.55,0.00", + "label": "C1", + "size": "0.20x0.20", + }, + { + "center": "0.00,0.00", + "label": "C2", + "size": "0.20x0.20", + }, + { + "center": "1.55,0.00", + "label": "C10", + "size": "0.20x0.20", + }, + ], + } + `) +}) + +test("decoupling cap layout follows positive-voltage main-pin order on x-side pins", () => { + const solver = new SingleInnerPartitionPackingSolver({ + partitionInputProblem: makeCapOnlyPartition(), + pinIdToStronglyConnectedPins: { + "C10.VCC": [makeMainPin("U1.VCC10", { x: 2, y: 2 }, "x+")], + "C2.VCC": [makeMainPin("U1.VCC2", { x: 2, y: 0 }, "x+")], + "C1.VCC": [makeMainPin("U1.VCC1", { x: 2, y: -2 }, "x+")], + "C1.GND": [makeMainPin("U1.GND1", { x: 2, y: 3 }, "x+")], + }, + }) + + solver.solve() + + const placements = solver.layout!.chipPlacements + expect(placements.C1!.x).toBeCloseTo(0) + expect(placements.C2!.x).toBeCloseTo(0) + expect(placements.C10!.x).toBeCloseTo(0) + expect(placements.C1!.y).toBeLessThan(placements.C2!.y) + expect(placements.C2!.y).toBeLessThan(placements.C10!.y) +})