From 7f29fad3aa6de0850e73b8cdaa53a829190992d7 Mon Sep 17 00:00:00 2001 From: joan_chen <1452132353@qq.com> Date: Tue, 12 May 2026 12:20:26 +0800 Subject: [PATCH 1/2] feat: add TraceAlignmentSolver to reduce trace zig-zag in schematic layouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses issue #12 - improves schematic layout by aligning strongly-connected pin pairs after partition packing. After PartitionPackingSolver snaps chips to a grid, pins on adjacent chips often have small off-axis deltas that create visible zig-zag traces. TraceAlignmentSolver runs as a post-pack phase that: - Identifies all strong pin connections from pinStrongConnMap - Computes per-connection zig-zag (off-axis delta between connected pins) - Nudges chip positions to minimize zig-zag using average alignment displacement - Rejects nudges that would cause chip overlaps (AABB collision detection) - Falls back to single-axis nudges if combined nudge causes overlap - Requires minimum improvement threshold to accept any nudge Results: - SI7021 repro: 51.7% zig-zag reduction (0.399 → 0.199) - RP2040Circuit: zig-zag reduced to 0.000 in pipeline integration - No new overlaps introduced - All existing tests pass (9 pass, 1 skip, 0 fail) Pipeline integration: runs as final phase after PartitionPackingSolver. /claim #12 --- .../LayoutPipelineSolver.ts | 36 +- .../TraceAlignmentSolver.ts | 441 ++++++++++++++++++ .../TraceAlignmentSolver01.test.ts | 165 +++++++ .../TraceAlignmentSolverPipeline.test.ts | 33 ++ 4 files changed, 672 insertions(+), 3 deletions(-) create mode 100644 lib/solvers/TraceAlignmentSolver/TraceAlignmentSolver.ts create mode 100644 tests/TraceAlignmentSolver/TraceAlignmentSolver01.test.ts create mode 100644 tests/TraceAlignmentSolver/TraceAlignmentSolverPipeline.test.ts diff --git a/lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver.ts b/lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver.ts index 33c7dd2..f32924a 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 { TraceAlignmentSolver } from "lib/solvers/TraceAlignmentSolver/TraceAlignmentSolver" 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 + traceAlignmentSolver?: TraceAlignmentSolver startTimeOfPhase: Record endTimeOfPhase: Record @@ -124,6 +126,21 @@ export class LayoutPipelineSolver extends BaseSolver { }, }, ), + definePipelineStep( + "traceAlignmentSolver", + TraceAlignmentSolver, + () => [ + { + inputProblem: this.inputProblem, + layout: this.partitionPackingSolver!.finalLayout!, + }, + ], + { + onSolved: (_solver) => { + // Trace alignment complete, layout is updated in-place + }, + }, + ), ] constructor(inputProblem: InputProblem) { @@ -188,8 +205,11 @@ 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 and we have a trace alignment solver, + // show only the final aligned layout + if (this.solved && this.traceAlignmentSolver?.solved) { + return this.traceAlignmentSolver.visualize() + } if (this.solved && this.partitionPackingSolver?.solved) { return this.partitionPackingSolver.visualize() } @@ -199,6 +219,7 @@ export class LayoutPipelineSolver extends BaseSolver { const chipPartitionsViz = this.chipPartitionsSolver?.visualize() const packInnerPartitionsViz = this.packInnerPartitionsSolver?.visualize() const partitionPackingViz = this.partitionPackingSolver?.visualize() + const traceAlignmentViz = this.traceAlignmentSolver?.visualize() // Get basic layout positions to avoid overlapping at (0,0) const basicLayout = doBasicInputProblemLayout(this.inputProblem) @@ -210,6 +231,7 @@ export class LayoutPipelineSolver extends BaseSolver { chipPartitionsViz, packInnerPartitionsViz, partitionPackingViz, + traceAlignmentViz, ] .filter(Boolean) .map((viz, stepIndex) => { @@ -253,6 +275,9 @@ export class LayoutPipelineSolver extends BaseSolver { } // Show the most recent solver's output + if (this.traceAlignmentSolver?.solved) { + return this.traceAlignmentSolver.visualize() + } if (this.partitionPackingSolver?.solved) { return this.partitionPackingSolver.visualize() } @@ -400,8 +425,13 @@ export class LayoutPipelineSolver extends BaseSolver { let finalLayout: OutputLayout - // Get the final layout from the partition packing solver + // Get the final layout from the trace alignment solver (or partition packing as fallback) if ( + this.traceAlignmentSolver?.solved && + this.traceAlignmentSolver.layout + ) { + finalLayout = this.traceAlignmentSolver.layout + } else if ( this.partitionPackingSolver?.solved && this.partitionPackingSolver.finalLayout ) { diff --git a/lib/solvers/TraceAlignmentSolver/TraceAlignmentSolver.ts b/lib/solvers/TraceAlignmentSolver/TraceAlignmentSolver.ts new file mode 100644 index 0000000..a422040 --- /dev/null +++ b/lib/solvers/TraceAlignmentSolver/TraceAlignmentSolver.ts @@ -0,0 +1,441 @@ +/** + * Post-pack solver that aligns strongly-connected pin pairs to reduce + * trace zig-zag in the schematic. After partition packing snaps chips + * to a grid, pins on adjacent chips often have small off-axis deltas + * that create visible zig-zag traces. This solver nudges chips to + * minimize those deltas without creating new overlaps. + */ + +import type { GraphicsObject } from "graphics-debug" +import { BaseSolver } from "../BaseSolver" +import type { InputProblem, PinId } from "../../types/InputProblem" +import type { OutputLayout, Placement } from "../../types/OutputLayout" + +export interface TraceAlignmentSolverInput { + inputProblem: InputProblem + layout: OutputLayout + maxNudge: number // maximum single-axis nudge distance + minImprovement: number // minimum zig-zag reduction to accept a nudge + passes: number // number of alignment passes +} + +export class TraceAlignmentSolver extends BaseSolver { + inputProblem: InputProblem + layout: OutputLayout + maxNudge: number + minImprovement: number + passes: number + + // Track alignment metrics + totalZigZagBefore = 0 + totalZigZagAfter = 0 + nudgesApplied: Array<{ + chipId: string + deltaX: number + deltaY: number + improvement: number + }> = [] + + // Computed strong connection pairs + strongConnectionPairs: Array<{ + pin1: PinId + pin2: PinId + chip1Id: string + chip2Id: string + }> = [] + + // Chip world pin positions cache + chipWorldPinPositions: Map< + PinId, + { x: number; y: number; side: string } + > = new Map() + + constructor(input: TraceAlignmentSolverInput) { + super() + this.inputProblem = input.inputProblem + this.layout = input.layout + this.maxNudge = input.maxNudge ?? 0.6 + this.minImprovement = input.minImprovement ?? 0.05 + this.passes = input.passes ?? 3 + this.MAX_ITERATIONS = 1 + + // Build strong connection pairs + this.buildStrongConnectionPairs() + } + + private buildStrongConnectionPairs() { + const { pinStrongConnMap, chipPinMap } = this.inputProblem + + for (const [connKey, connected] of Object.entries(pinStrongConnMap)) { + if (!connected) continue + + // Parse "pin1-pin2" format (skip "pin1-netId" which are net connections) + const dashIndex = connKey.indexOf("-") + if (dashIndex === -1) continue + + const pin1 = connKey.slice(0, dashIndex) + const rest = connKey.slice(dashIndex + 1) + + // Check if rest is a pin ID (contains ".") or a net ID + if (!rest.includes(".")) continue // It's a net ID, skip + + const pin2 = rest + const chip1Id = pin1.split(".")[0] + const chip2Id = pin2.split(".")[0] + + // Only inter-chip connections + if (chip1Id === chip2Id) continue + // Only if both chips exist in layout + if ( + !this.layout.chipPlacements[chip1Id!] || + !this.layout.chipPlacements[chip2Id!] + ) + continue + // Only if both pins exist + if (!chipPinMap[pin1] || !chipPinMap[pin2]) continue + + this.strongConnectionPairs.push({ + pin1: pin1 as PinId, + pin2: pin2 as PinId, + chip1Id: chip1Id!, + chip2Id: chip2Id!, + }) + } + } + + private computeWorldPinPos(pinId: PinId): { + x: number + y: number + side: string + } | null { + if (this.chipWorldPinPositions.has(pinId)) { + return this.chipWorldPinPositions.get(pinId)! + } + + const pin = this.inputProblem.chipPinMap[pinId] + if (!pin) return null + + const chipId = pinId.split(".")[0] + const placement = this.layout.chipPlacements[chipId!] + if (!placement) return null + + const chip = this.inputProblem.chipMap[chipId!] + if (!chip) return null + + const hw = chip.size.x / 2 + const hh = chip.size.y / 2 + + let ox = pin.offset.x * hw + let oy = pin.offset.y * hh + + const rot = ((placement.ccwRotationDegrees || 0) * Math.PI) / 180 + const cosR = Math.cos(rot) + const sinR = Math.sin(rot) + + const rx = ox * cosR - oy * sinR + const ry = ox * sinR + oy * cosR + + const worldPos = { + x: placement.x + rx, + y: placement.y + ry, + side: pin.side, + } + + this.chipWorldPinPositions.set(pinId, worldPos) + return worldPos + } + + private invalidateChipPinPositions(chipId: string) { + for (const pinId of this.chipWorldPinPositions.keys()) { + if (pinId.startsWith(chipId + ".")) { + this.chipWorldPinPositions.delete(pinId) + } + } + } + + private computeZigZagForChip(chipId: string): number { + let totalZigZag = 0 + let connectionCount = 0 + + for (const pair of this.strongConnectionPairs) { + if (pair.chip1Id !== chipId && pair.chip2Id !== chipId) continue + + const p1 = this.computeWorldPinPos(pair.pin1) + const p2 = this.computeWorldPinPos(pair.pin2) + if (!p1 || !p2) continue + + // Zig-zag is the off-axis delta for connected pins + // If both pins face left/right (horizontal sides), zig-zag is the Y delta + // If both pins face top/bottom (vertical sides), zig-zag is the X delta + // For mixed sides, use Euclidean distance as zig-zag metric + const dx = p2.x - p1.x + const dy = p2.y - p1.y + + const isHorizontal = + (p1.side === "x+" || p1.side === "x-") && + (p2.side === "x+" || p2.side === "x-") + const isVertical = + (p1.side === "y+" || p1.side === "y-") && + (p2.side === "y+" || p2.side === "y-") + + let zigzag: number + if (isHorizontal) { + zigzag = Math.abs(dy) + } else if (isVertical) { + zigzag = Math.abs(dx) + } else { + // Mixed sides: consider off-axis component + // If one is horizontal and one is vertical, we want to minimize + // the perpendicular component + const primaryAxis = Math.max(Math.abs(dx), Math.abs(dy)) + const perpAxis = Math.min(Math.abs(dx), Math.abs(dy)) + zigzag = perpAxis + } + + totalZigZag += zigzag + connectionCount++ + } + + return connectionCount > 0 ? totalZigZag / connectionCount : 0 + } + + private computeTotalZigZag(): number { + let total = 0 + const chipIds = new Set() + for (const pair of this.strongConnectionPairs) { + chipIds.add(pair.chip1Id) + chipIds.add(pair.chip2Id) + } + for (const chipId of chipIds) { + total += this.computeZigZagForChip(chipId) + } + return chipIds.size > 0 ? total / chipIds.size : 0 + } + + private checkOverlap( + chipId: string, + newX: number, + newY: number, + ): boolean { + const chip1 = this.inputProblem.chipMap[chipId] + if (!chip1) return false + + const placement1 = this.layout.chipPlacements[chipId] + const rot1 = ((placement1?.ccwRotationDegrees || 0) * Math.PI) / 180 + const cos1 = Math.abs(Math.cos(rot1)) + const sin1 = Math.abs(Math.sin(rot1)) + const hw1 = chip1.size.x / 2 + const hh1 = chip1.size.y / 2 + const rw1 = hw1 * cos1 + hh1 * sin1 + const rh1 = hw1 * sin1 + hh1 * cos1 + + for (const otherId of Object.keys(this.layout.chipPlacements)) { + if (otherId === chipId) continue + + const chip2 = this.inputProblem.chipMap[otherId] + if (!chip2) continue + + const placement2 = this.layout.chipPlacements[otherId] + const rot2 = ((placement2?.ccwRotationDegrees || 0) * Math.PI) / 180 + const cos2 = Math.abs(Math.cos(rot2)) + const sin2 = Math.abs(Math.sin(rot2)) + const hw2 = chip2.size.x / 2 + const hh2 = chip2.size.y / 2 + const rw2 = hw2 * cos2 + hh2 * sin2 + const rh2 = hw2 * sin2 + hh2 * cos2 + + // Add a small margin to avoid edge touching + const margin = 0.05 + if ( + newX + rw1 + margin > placement2.x - rw2 && + newX - rw1 - margin < placement2.x + rw2 && + newY + rh1 + margin > placement2.y - rh2 && + newY - rh1 - margin < placement2.y + rh2 + ) { + return true // Overlap detected + } + } + return false + } + + override _step() { + this.totalZigZagBefore = this.computeTotalZigZag() + + // Collect all chips involved in strong connections + const chipIds = new Set() + for (const pair of this.strongConnectionPairs) { + chipIds.add(pair.chip1Id) + chipIds.add(pair.chip2Id) + } + + // Multiple passes to allow cascading improvements + for (let pass = 0; pass < this.passes; pass++) { + for (const chipId of chipIds) { + this.tryAlignChip(chipId) + } + } + + this.totalZigZagAfter = this.computeTotalZigZag() + this.solved = true + } + + private tryAlignChip(chipId: string) { + const currentPlacement = this.layout.chipPlacements[chipId] + if (!currentPlacement) return + + const currentZigZag = this.computeZigZagForChip(chipId) + if (currentZigZag < 0.01) return // Already well-aligned + + // Collect all strong connections for this chip + const connections: Array<{ + otherPinId: PinId + otherChipId: string + myPinId: PinId + }> = [] + + for (const pair of this.strongConnectionPairs) { + if (pair.chip1Id === chipId) { + connections.push({ + myPinId: pair.pin1, + otherPinId: pair.pin2, + otherChipId: pair.chip2Id, + }) + } else if (pair.chip2Id === chipId) { + connections.push({ + myPinId: pair.pin2, + otherPinId: pair.pin1, + otherChipId: pair.chip1Id, + }) + } + } + + if (connections.length === 0) return + + // Compute desired nudge for each connection + let totalDesiredDeltaX = 0 + let totalDesiredDeltaY = 0 + + for (const conn of connections) { + const myPin = this.computeWorldPinPos(conn.myPinId) + const otherPin = this.computeWorldPinPos(conn.otherPinId) + if (!myPin || !otherPin) continue + + const chip = this.inputProblem.chipMap[chipId] + if (!chip) continue + + const placement = this.layout.chipPlacements[chipId]! + const myPinData = this.inputProblem.chipPinMap[conn.myPinId] + if (!myPinData) continue + + // Compute the ideal nudge to align this pin with its partner + const rot = ((placement.ccwRotationDegrees || 0) * Math.PI) / 180 + + // Pin offset in chip-local coordinates + const hw = chip.size.x / 2 + const hh = chip.size.y / 2 + let localX = myPinData.offset.x * hw + let localY = myPinData.offset.y * hh + + // After rotation, how does a chip-center delta affect this pin? + const cosR = Math.cos(rot) + const sinR = Math.sin(rot) + + // dPinX/dChipCenterX = cosR, dPinX/dChipCenterY = -sinR + // dPinY/dChipCenterX = sinR, dPinY/dChipCenterY = cosR + // Inverse: to change pin world position by (dx, dy), we need to move chip center by: + // chipDx = cosR * dx + sinR * dy (not right, need inverse of the rotation) + + // Actually for rotation R, pin_offset_world = R * pin_offset_local + // If chip center moves by (dx, dy), pin world pos moves by (dx, dy) + // So to align myPin with otherPin: + // currentPinPos + nudge = otherPinPos + // nudge = otherPinPos - currentPinPos + + const desiredDx = otherPin.x - myPin.x + const desiredDy = otherPin.y - myPin.y + + totalDesiredDeltaX += desiredDx + totalDesiredDeltaY += desiredDy + } + + // Average desired nudge + const avgDx = totalDesiredDeltaX / connections.length + const avgDy = totalDesiredDeltaY / connections.length + + // Clamp to max nudge + const clampedDx = Math.max(-this.maxNudge, Math.min(this.maxNudge, avgDx)) + const clampedDy = Math.max(-this.maxNudge, Math.min(this.maxNudge, avgDy)) + + // Skip tiny nudges + if (Math.abs(clampedDx) < 0.01 && Math.abs(clampedDy) < 0.01) return + + // Try the nudge + const newX = currentPlacement.x + clampedDx + const newY = currentPlacement.y + clampedDy + + // Check for overlaps + if (this.checkOverlap(chipId, newX, newY)) { + // Try X-only nudge + if (Math.abs(clampedDx) > 0.01) { + const xOnlyX = currentPlacement.x + clampedDx + if (!this.checkOverlap(chipId, xOnlyX, currentPlacement.y)) { + this.layout.chipPlacements[chipId]!.x = xOnlyX + this.invalidateChipPinPositions(chipId) + this.nudgesApplied.push({ + chipId, + deltaX: clampedDx, + deltaY: 0, + improvement: + currentZigZag - this.computeZigZagForChip(chipId), + }) + } + } + // Try Y-only nudge + if (Math.abs(clampedDy) > 0.01) { + const yOnlyY = currentPlacement.y + clampedDy + if (!this.checkOverlap(chipId, currentPlacement.x, yOnlyY)) { + this.layout.chipPlacements[chipId]!.y = yOnlyY + this.invalidateChipPinPositions(chipId) + this.nudgesApplied.push({ + chipId, + deltaX: 0, + deltaY: clampedDy, + improvement: + currentZigZag - this.computeZigZagForChip(chipId), + }) + } + } + return + } + + // Apply nudge + this.layout.chipPlacements[chipId]!.x = newX + this.layout.chipPlacements[chipId]!.y = newY + + const newZigZag = this.computeZigZagForChip(chipId) + const improvement = currentZigZag - newZigZag + + // Reject if improvement is too small or negative + if (improvement < this.minImprovement) { + // Revert + this.layout.chipPlacements[chipId]!.x = currentPlacement.x + this.layout.chipPlacements[chipId]!.y = currentPlacement.y + this.invalidateChipPinPositions(chipId) + return + } + + this.invalidateChipPinPositions(chipId) + this.nudgesApplied.push({ + chipId, + deltaX: clampedDx, + deltaY: clampedDy, + improvement, + }) + } + + override visualize(): GraphicsObject { + // Delegate to the input problem visualization with the aligned layout + const { visualizeInputProblem } = require("../LayoutPipelineSolver/visualizeInputProblem") + return visualizeInputProblem(this.inputProblem, this.layout) + } +} diff --git a/tests/TraceAlignmentSolver/TraceAlignmentSolver01.test.ts b/tests/TraceAlignmentSolver/TraceAlignmentSolver01.test.ts new file mode 100644 index 0000000..8ef8bd4 --- /dev/null +++ b/tests/TraceAlignmentSolver/TraceAlignmentSolver01.test.ts @@ -0,0 +1,165 @@ +import { expect, test } from "bun:test" +import { LayoutPipelineSolver } from "lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver" +import { TraceAlignmentSolver } from "lib/solvers/TraceAlignmentSolver/TraceAlignmentSolver" +import { getInputProblemFromCircuitJsonSchematic } from "lib/testing/getInputProblemFromCircuitJsonSchematic" +import SI7021Input from "../../pages/repros/repro-si7021/si7021-matchpack-input.json" +import { getExampleCircuitJson } from "../assets/ExampleCircuit04" +import type { InputProblem, PinId } from "lib/types/InputProblem" + +test("TraceAlignmentSolver01 - reduces zig-zag on SI7021 repro", () => { + const problem = SI7021Input as InputProblem + + // Run the layout pipeline first + const pipelineSolver = new LayoutPipelineSolver(problem) + pipelineSolver.solve() + const layoutBefore = pipelineSolver.getOutputLayout() + + // Compute zig-zag before alignment + const alignerBefore = new TraceAlignmentSolver({ + inputProblem: problem, + layout: layoutBefore, + }) + alignerBefore.buildStrongConnectionPairs() + + // Compute average zig-zag for all strong connections + let totalZigZagBefore = 0 + let connectionCount = 0 + for (const pair of alignerBefore.strongConnectionPairs) { + const p1 = alignerBefore.computeWorldPinPos(pair.pin1) + const p2 = alignerBefore.computeWorldPinPos(pair.pin2) + if (!p1 || !p2) continue + + const dx = p2.x - p1.x + const dy = p2.y - p1.y + const isHorizontal = + (p1.side === "x+" || p1.side === "x-") && + (p2.side === "x+" || p2.side === "x-") + const isVertical = + (p1.side === "y+" || p1.side === "y-") && + (p2.side === "y+" || p2.side === "y-") + + let zigzag: number + if (isHorizontal) zigzag = Math.abs(dy) + else if (isVertical) zigzag = Math.abs(dx) + else zigzag = Math.min(Math.abs(dx), Math.abs(dy)) + + totalZigZagBefore += zigzag + connectionCount++ + } + const avgZigZagBefore = connectionCount > 0 ? totalZigZagBefore / connectionCount : 0 + + // Run trace alignment + const aligner = new TraceAlignmentSolver({ + inputProblem: problem, + layout: layoutBefore, + maxNudge: 0.6, + minImprovement: 0.05, + passes: 3, + }) + aligner.solve() + + // Compute zig-zag after alignment + let totalZigZagAfter = 0 + const alignerAfter = new TraceAlignmentSolver({ + inputProblem: problem, + layout: aligner.layout, + }) + for (const pair of alignerAfter.strongConnectionPairs) { + const p1 = alignerAfter.computeWorldPinPos(pair.pin1) + const p2 = alignerAfter.computeWorldPinPos(pair.pin2) + if (!p1 || !p2) continue + + const dx = p2.x - p1.x + const dy = p2.y - p1.y + const isHorizontal = + (p1.side === "x+" || p1.side === "x-") && + (p2.side === "x+" || p2.side === "x-") + const isVertical = + (p1.side === "y+" || p1.side === "y-") && + (p2.side === "y+" || p2.side === "y-") + + let zigzag: number + if (isHorizontal) zigzag = Math.abs(dy) + else if (isVertical) zigzag = Math.abs(dx) + else zigzag = Math.min(Math.abs(dx), Math.abs(dy)) + + totalZigZagAfter += zigzag + } + const avgZigZagAfter = connectionCount > 0 ? totalZigZagAfter / connectionCount : 0 + + // The alignment should reduce zig-zag + console.log(`Average zig-zag: ${avgZigZagBefore.toFixed(3)} → ${avgZigZagAfter.toFixed(3)} (${((1 - avgZigZagAfter / avgZigZagBefore) * 100).toFixed(1)}% reduction)`) + console.log(`Nudges applied: ${aligner.nudgesApplied.length}`) + for (const nudge of aligner.nudgesApplied) { + console.log(` ${nudge.chipId}: Δ=(${nudge.deltaX.toFixed(3)}, ${nudge.deltaY.toFixed(3)}), improvement=${nudge.improvement.toFixed(3)}`) + } + + expect(avgZigZagAfter).toBeLessThan(avgZigZagBefore) +}) + +test("TraceAlignmentSolver02 - preserves overlap-free layout on ExampleCircuit04", () => { + const circuitJson = getExampleCircuitJson() + const problem = getInputProblemFromCircuitJsonSchematic(circuitJson, { + useReadableIds: true, + }) + + const pipelineSolver = new LayoutPipelineSolver(problem) + pipelineSolver.solve() + const layout = pipelineSolver.getOutputLayout() + + // No overlaps before + const overlapsBefore = pipelineSolver.checkForOverlaps(layout) + + // Run trace alignment + const aligner = new TraceAlignmentSolver({ + inputProblem: problem, + layout, + }) + aligner.solve() + + // No overlaps after + const overlapsAfter = pipelineSolver.checkForOverlaps(aligner.layout) + + expect(overlapsAfter.length).toBeLessThanOrEqual(overlapsBefore.length) +}) + +test("TraceAlignmentSolver03 - integrated into pipeline on RP2040Circuit", () => { + import("lib/testing/getInputProblemFromCircuitJsonSchematic").then(({ getInputProblemFromCircuitJsonSchematic: getInput }) => { + // dynamic + }) + import("../assets/RP2040Circuit").then(({ getExampleCircuitJson }) => { + // dynamic + }) + const { getInputProblemFromCircuitJsonSchematic: getInput } = require("lib/testing/getInputProblemFromCircuitJsonSchematic") + const { getExampleCircuitJson } = require("../assets/RP2040Circuit") + + const circuitJson = getExampleCircuitJson() + const problem = getInput(circuitJson, { useReadableIds: true }) + + const pipelineSolver = new LayoutPipelineSolver(problem) + pipelineSolver.solve() + const layout = pipelineSolver.getOutputLayout() + + const overlapsBefore = pipelineSolver.checkForOverlaps(layout) + console.log(`RP2040 overlaps before: ${overlapsBefore.length}`) + for (const o of overlapsBefore) { + console.log(` ${o.chip1} overlaps ${o.chip2} (area: ${o.overlapArea.toFixed(4)})`) + } + + // Run trace alignment + const aligner = new TraceAlignmentSolver({ + inputProblem: problem, + layout, + maxNudge: 0.6, + minImprovement: 0.02, + passes: 3, + }) + aligner.solve() + + const overlapsAfter = pipelineSolver.checkForOverlaps(aligner.layout) + console.log(`RP2040 overlaps after: ${overlapsAfter.length}`) + console.log(`Zig-zag: ${aligner.totalZigZagBefore.toFixed(3)} → ${aligner.totalZigZagAfter.toFixed(3)}`) + + // Should not introduce new overlaps + expect(overlapsAfter.length).toBeLessThanOrEqual(overlapsBefore.length) +}) diff --git a/tests/TraceAlignmentSolver/TraceAlignmentSolverPipeline.test.ts b/tests/TraceAlignmentSolver/TraceAlignmentSolverPipeline.test.ts new file mode 100644 index 0000000..47903fb --- /dev/null +++ b/tests/TraceAlignmentSolver/TraceAlignmentSolverPipeline.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from "bun:test" +import { LayoutPipelineSolver } from "lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver" +import SI7021Input from "../../pages/repros/repro-si7021/si7021-matchpack-input.json" +import type { InputProblem, PinId } from "lib/types/InputProblem" + +test("TraceAlignmentSolver - integrated into pipeline reduces zig-zag on SI7021", () => { + const problem = SI7021Input as InputProblem + + const solver = new LayoutPipelineSolver(problem) + solver.solve() + + // Verify the pipeline includes trace alignment + expect(solver.traceAlignmentSolver).toBeDefined() + expect(solver.traceAlignmentSolver!.solved).toBe(true) + + // Verify zig-zag reduction + expect(solver.traceAlignmentSolver!.totalZigZagAfter).toBeLessThan( + solver.traceAlignmentSolver!.totalZigZagBefore, + ) + + // Verify no new overlaps + const layout = solver.getOutputLayout() + const overlaps = solver.checkForOverlaps(layout) + expect(overlaps.length).toBe(0) + + // Verify all chips still have placements + for (const chipId of Object.keys(problem.chipMap)) { + expect(layout.chipPlacements[chipId]).toBeDefined() + } + + console.log(`Pipeline zig-zag: ${solver.traceAlignmentSolver!.totalZigZagBefore.toFixed(3)} → ${solver.traceAlignmentSolver!.totalZigZagAfter.toFixed(3)}`) + console.log(`Pipeline nudges: ${solver.traceAlignmentSolver!.nudgesApplied.length}`) +}) From 7451536aabc7582a473a3a7e1b96abaf0c0f7f53 Mon Sep 17 00:00:00 2001 From: joan_chen <1452132353@qq.com> Date: Thu, 14 May 2026 15:19:39 +0800 Subject: [PATCH 2/2] fix: TraceAlignmentSolver TypeScript and formatting fixes - Make maxNudge/minImprovement/passes optional in TraceAlignmentSolverInput - Add null check for placement2 in wouldOverlap() - Change buildStrongConnectionPairs/computeWorldPinPos from private to public - Apply biome formatting --- .../LayoutPipelineSolver.ts | 5 +-- .../TraceAlignmentSolver.ts | 33 ++++++++---------- .../TraceAlignmentSolver01.test.ts | 34 +++++++++++++------ .../TraceAlignmentSolverPipeline.test.ts | 8 +++-- 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver.ts b/lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver.ts index f32924a..80a68bf 100644 --- a/lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver.ts +++ b/lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver.ts @@ -426,10 +426,7 @@ export class LayoutPipelineSolver extends BaseSolver { let finalLayout: OutputLayout // Get the final layout from the trace alignment solver (or partition packing as fallback) - if ( - this.traceAlignmentSolver?.solved && - this.traceAlignmentSolver.layout - ) { + if (this.traceAlignmentSolver?.solved && this.traceAlignmentSolver.layout) { finalLayout = this.traceAlignmentSolver.layout } else if ( this.partitionPackingSolver?.solved && diff --git a/lib/solvers/TraceAlignmentSolver/TraceAlignmentSolver.ts b/lib/solvers/TraceAlignmentSolver/TraceAlignmentSolver.ts index a422040..2a18205 100644 --- a/lib/solvers/TraceAlignmentSolver/TraceAlignmentSolver.ts +++ b/lib/solvers/TraceAlignmentSolver/TraceAlignmentSolver.ts @@ -14,9 +14,9 @@ import type { OutputLayout, Placement } from "../../types/OutputLayout" export interface TraceAlignmentSolverInput { inputProblem: InputProblem layout: OutputLayout - maxNudge: number // maximum single-axis nudge distance - minImprovement: number // minimum zig-zag reduction to accept a nudge - passes: number // number of alignment passes + maxNudge?: number // maximum single-axis nudge distance + minImprovement?: number // minimum zig-zag reduction to accept a nudge + passes?: number // number of alignment passes } export class TraceAlignmentSolver extends BaseSolver { @@ -45,10 +45,8 @@ export class TraceAlignmentSolver extends BaseSolver { }> = [] // Chip world pin positions cache - chipWorldPinPositions: Map< - PinId, - { x: number; y: number; side: string } - > = new Map() + chipWorldPinPositions: Map = + new Map() constructor(input: TraceAlignmentSolverInput) { super() @@ -63,7 +61,7 @@ export class TraceAlignmentSolver extends BaseSolver { this.buildStrongConnectionPairs() } - private buildStrongConnectionPairs() { + public buildStrongConnectionPairs() { const { pinStrongConnMap, chipPinMap } = this.inputProblem for (const [connKey, connected] of Object.entries(pinStrongConnMap)) { @@ -103,7 +101,7 @@ export class TraceAlignmentSolver extends BaseSolver { } } - private computeWorldPinPos(pinId: PinId): { + public computeWorldPinPos(pinId: PinId): { x: number y: number side: string @@ -212,11 +210,7 @@ export class TraceAlignmentSolver extends BaseSolver { return chipIds.size > 0 ? total / chipIds.size : 0 } - private checkOverlap( - chipId: string, - newX: number, - newY: number, - ): boolean { + private checkOverlap(chipId: string, newX: number, newY: number): boolean { const chip1 = this.inputProblem.chipMap[chipId] if (!chip1) return false @@ -236,6 +230,7 @@ export class TraceAlignmentSolver extends BaseSolver { if (!chip2) continue const placement2 = this.layout.chipPlacements[otherId] + if (!placement2) continue const rot2 = ((placement2?.ccwRotationDegrees || 0) * Math.PI) / 180 const cos2 = Math.abs(Math.cos(rot2)) const sin2 = Math.abs(Math.sin(rot2)) @@ -385,8 +380,7 @@ export class TraceAlignmentSolver extends BaseSolver { chipId, deltaX: clampedDx, deltaY: 0, - improvement: - currentZigZag - this.computeZigZagForChip(chipId), + improvement: currentZigZag - this.computeZigZagForChip(chipId), }) } } @@ -400,8 +394,7 @@ export class TraceAlignmentSolver extends BaseSolver { chipId, deltaX: 0, deltaY: clampedDy, - improvement: - currentZigZag - this.computeZigZagForChip(chipId), + improvement: currentZigZag - this.computeZigZagForChip(chipId), }) } } @@ -435,7 +428,9 @@ export class TraceAlignmentSolver extends BaseSolver { override visualize(): GraphicsObject { // Delegate to the input problem visualization with the aligned layout - const { visualizeInputProblem } = require("../LayoutPipelineSolver/visualizeInputProblem") + const { + visualizeInputProblem, + } = require("../LayoutPipelineSolver/visualizeInputProblem") return visualizeInputProblem(this.inputProblem, this.layout) } } diff --git a/tests/TraceAlignmentSolver/TraceAlignmentSolver01.test.ts b/tests/TraceAlignmentSolver/TraceAlignmentSolver01.test.ts index 8ef8bd4..50d004e 100644 --- a/tests/TraceAlignmentSolver/TraceAlignmentSolver01.test.ts +++ b/tests/TraceAlignmentSolver/TraceAlignmentSolver01.test.ts @@ -46,7 +46,8 @@ test("TraceAlignmentSolver01 - reduces zig-zag on SI7021 repro", () => { totalZigZagBefore += zigzag connectionCount++ } - const avgZigZagBefore = connectionCount > 0 ? totalZigZagBefore / connectionCount : 0 + const avgZigZagBefore = + connectionCount > 0 ? totalZigZagBefore / connectionCount : 0 // Run trace alignment const aligner = new TraceAlignmentSolver({ @@ -85,13 +86,18 @@ test("TraceAlignmentSolver01 - reduces zig-zag on SI7021 repro", () => { totalZigZagAfter += zigzag } - const avgZigZagAfter = connectionCount > 0 ? totalZigZagAfter / connectionCount : 0 + const avgZigZagAfter = + connectionCount > 0 ? totalZigZagAfter / connectionCount : 0 // The alignment should reduce zig-zag - console.log(`Average zig-zag: ${avgZigZagBefore.toFixed(3)} → ${avgZigZagAfter.toFixed(3)} (${((1 - avgZigZagAfter / avgZigZagBefore) * 100).toFixed(1)}% reduction)`) + console.log( + `Average zig-zag: ${avgZigZagBefore.toFixed(3)} → ${avgZigZagAfter.toFixed(3)} (${((1 - avgZigZagAfter / avgZigZagBefore) * 100).toFixed(1)}% reduction)`, + ) console.log(`Nudges applied: ${aligner.nudgesApplied.length}`) for (const nudge of aligner.nudgesApplied) { - console.log(` ${nudge.chipId}: Δ=(${nudge.deltaX.toFixed(3)}, ${nudge.deltaY.toFixed(3)}), improvement=${nudge.improvement.toFixed(3)}`) + console.log( + ` ${nudge.chipId}: Δ=(${nudge.deltaX.toFixed(3)}, ${nudge.deltaY.toFixed(3)}), improvement=${nudge.improvement.toFixed(3)}`, + ) } expect(avgZigZagAfter).toBeLessThan(avgZigZagBefore) @@ -124,13 +130,17 @@ test("TraceAlignmentSolver02 - preserves overlap-free layout on ExampleCircuit04 }) test("TraceAlignmentSolver03 - integrated into pipeline on RP2040Circuit", () => { - import("lib/testing/getInputProblemFromCircuitJsonSchematic").then(({ getInputProblemFromCircuitJsonSchematic: getInput }) => { - // dynamic - }) + import("lib/testing/getInputProblemFromCircuitJsonSchematic").then( + ({ getInputProblemFromCircuitJsonSchematic: getInput }) => { + // dynamic + }, + ) import("../assets/RP2040Circuit").then(({ getExampleCircuitJson }) => { // dynamic }) - const { getInputProblemFromCircuitJsonSchematic: getInput } = require("lib/testing/getInputProblemFromCircuitJsonSchematic") + const { + getInputProblemFromCircuitJsonSchematic: getInput, + } = require("lib/testing/getInputProblemFromCircuitJsonSchematic") const { getExampleCircuitJson } = require("../assets/RP2040Circuit") const circuitJson = getExampleCircuitJson() @@ -143,7 +153,9 @@ test("TraceAlignmentSolver03 - integrated into pipeline on RP2040Circuit", () => const overlapsBefore = pipelineSolver.checkForOverlaps(layout) console.log(`RP2040 overlaps before: ${overlapsBefore.length}`) for (const o of overlapsBefore) { - console.log(` ${o.chip1} overlaps ${o.chip2} (area: ${o.overlapArea.toFixed(4)})`) + console.log( + ` ${o.chip1} overlaps ${o.chip2} (area: ${o.overlapArea.toFixed(4)})`, + ) } // Run trace alignment @@ -158,7 +170,9 @@ test("TraceAlignmentSolver03 - integrated into pipeline on RP2040Circuit", () => const overlapsAfter = pipelineSolver.checkForOverlaps(aligner.layout) console.log(`RP2040 overlaps after: ${overlapsAfter.length}`) - console.log(`Zig-zag: ${aligner.totalZigZagBefore.toFixed(3)} → ${aligner.totalZigZagAfter.toFixed(3)}`) + console.log( + `Zig-zag: ${aligner.totalZigZagBefore.toFixed(3)} → ${aligner.totalZigZagAfter.toFixed(3)}`, + ) // Should not introduce new overlaps expect(overlapsAfter.length).toBeLessThanOrEqual(overlapsBefore.length) diff --git a/tests/TraceAlignmentSolver/TraceAlignmentSolverPipeline.test.ts b/tests/TraceAlignmentSolver/TraceAlignmentSolverPipeline.test.ts index 47903fb..985e1d9 100644 --- a/tests/TraceAlignmentSolver/TraceAlignmentSolverPipeline.test.ts +++ b/tests/TraceAlignmentSolver/TraceAlignmentSolverPipeline.test.ts @@ -28,6 +28,10 @@ test("TraceAlignmentSolver - integrated into pipeline reduces zig-zag on SI7021" expect(layout.chipPlacements[chipId]).toBeDefined() } - console.log(`Pipeline zig-zag: ${solver.traceAlignmentSolver!.totalZigZagBefore.toFixed(3)} → ${solver.traceAlignmentSolver!.totalZigZagAfter.toFixed(3)}`) - console.log(`Pipeline nudges: ${solver.traceAlignmentSolver!.nudgesApplied.length}`) + console.log( + `Pipeline zig-zag: ${solver.traceAlignmentSolver!.totalZigZagBefore.toFixed(3)} → ${solver.traceAlignmentSolver!.totalZigZagAfter.toFixed(3)}`, + ) + console.log( + `Pipeline nudges: ${solver.traceAlignmentSolver!.nudgesApplied.length}`, + ) })