diff --git a/algos/infinite-grid-ijump-astar/tests/__snapshots__/remove-path-loops-1.snap.svg b/algos/infinite-grid-ijump-astar/tests/__snapshots__/remove-path-loops-1.snap.svg new file mode 100644 index 0000000..b10f377 --- /dev/null +++ b/algos/infinite-grid-ijump-astar/tests/__snapshots__/remove-path-loops-1.snap.svg @@ -0,0 +1 @@ +01234012pathWithLoopsimplifiedPath \ No newline at end of file diff --git a/algos/infinite-grid-ijump-astar/tests/__snapshots__/remove-path-loops-2.snap.svg b/algos/infinite-grid-ijump-astar/tests/__snapshots__/remove-path-loops-2.snap.svg new file mode 100644 index 0000000..c92de33 --- /dev/null +++ b/algos/infinite-grid-ijump-astar/tests/__snapshots__/remove-path-loops-2.snap.svg @@ -0,0 +1 @@ +0123456012345pathWithLoopsimplifiedPath \ No newline at end of file diff --git a/algos/infinite-grid-ijump-astar/tests/fixtures/get-path-comparison-svg.ts b/algos/infinite-grid-ijump-astar/tests/fixtures/get-path-comparison-svg.ts new file mode 100644 index 0000000..e05e2dd --- /dev/null +++ b/algos/infinite-grid-ijump-astar/tests/fixtures/get-path-comparison-svg.ts @@ -0,0 +1,93 @@ +import type { Point } from "@tscircuit/math-utils" +import type { PointWithLayer } from "solver-utils" +import { + scale, + fromTriangles, + translate, + compose, + applyToPoint, +} from "transformation-matrix" + +export const getPathComparisonSvg = ( + pathMap: Record, +) => { + const svgWidth = 640 + const svgHeight = 480 + + // Find min and max coordinates + let minX = Infinity, + maxX = -Infinity, + minY = Infinity, + maxY = -Infinity + Object.values(pathMap) + .flat() + .forEach((point) => { + minX = Math.min(minX, point.x) + maxX = Math.max(maxX, point.x) + minY = Math.min(minY, point.y) + maxY = Math.max(maxY, point.y) + }) + + // Compute scale and translation + const padding = 20 // Padding around the edges + + // Define triangles in path coordinate space and SVG coordinate space + const pathTriangle = [ + { x: minX, y: minY }, + { x: maxX, y: minY }, + { x: minX, y: maxY }, + ] + const svgTriangle = [ + { x: padding, y: padding }, + { x: svgWidth - padding, y: padding }, + { x: padding, y: svgHeight - padding }, + ] + + // Compute the transform using fromTriangles + const transform = fromTriangles(pathTriangle, svgTriangle) + + let svg = `` + + const legendItems: string[] = [] + + Object.entries(pathMap).forEach(([pathName, points], pathIndex) => { + const color = `hsl(${pathIndex * 137.5}, 70%, 40%)` + + // Draw lines between adjacent points + for (let i = 0; i < points.length - 1; i++) { + const start = applyToPoint(transform, points[i]) + const end = applyToPoint(transform, points[i + 1]) + start.x -= 8 * pathIndex + start.y -= 8 * pathIndex + end.x -= 8 * pathIndex + end.y -= 8 * pathIndex + const isDashed = + points[i].layer === "bottom" || points[i + 1].layer === "bottom" + svg += `` + } + + // Draw points and add index numbers + points.forEach((point, index) => { + let { x, y } = applyToPoint(transform, point) + x -= 8 * pathIndex + y -= 8 * pathIndex + + svg += `` + svg += `${index}` + }) + + // Add legend item + legendItems.push( + `${pathName}`, + ) + }) + + // Add legend + svg += `` + svg += `` + svg += legendItems.join("") + svg += "" + + svg += "" + return svg +} diff --git a/algos/infinite-grid-ijump-astar/tests/remove-path-loops-1.test.tsx b/algos/infinite-grid-ijump-astar/tests/remove-path-loops-1.test.tsx new file mode 100644 index 0000000..1200f59 --- /dev/null +++ b/algos/infinite-grid-ijump-astar/tests/remove-path-loops-1.test.tsx @@ -0,0 +1,36 @@ +import { Circuit } from "@tscircuit/core" +import { expect, test } from "bun:test" +import type { AnyCircuitElement } from "circuit-json" +import { getSimpleRouteJson } from "solver-utils" +import { IJumpMultiMarginAutorouter } from "../v2/lib/IJumpMultiMarginAutorouter" +import { getDebugSvg } from "./fixtures/get-debug-svg" +import { removePathLoops } from "solver-postprocessing/remove-path-loops" +import { getPathComparisonSvg } from "./fixtures/get-path-comparison-svg" + +test("remove-path-loops 1: simple loop", () => { + /** + * Ascii art of the path: + * ...... + * . . + * .......... + * . + * . + */ + // Create a path with an intentional loop + const pathWithLoop: Array<{ x: number; y: number; layer: string }> = [ + { x: 0, y: 0, layer: "top" }, + { x: 5, y: 0, layer: "top" }, + { x: 5, y: 3, layer: "top" }, + { x: 3, y: 3, layer: "top" }, + { x: 3, y: -3, layer: "top" }, + ] + + const simplifiedPath = removePathLoops(pathWithLoop) + + expect( + getPathComparisonSvg({ + pathWithLoop, + simplifiedPath, + }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/algos/infinite-grid-ijump-astar/tests/remove-path-loops-2.test.tsx b/algos/infinite-grid-ijump-astar/tests/remove-path-loops-2.test.tsx new file mode 100644 index 0000000..a5a0fcf --- /dev/null +++ b/algos/infinite-grid-ijump-astar/tests/remove-path-loops-2.test.tsx @@ -0,0 +1,38 @@ +import { Circuit } from "@tscircuit/core" +import { expect, test } from "bun:test" +import type { AnyCircuitElement } from "circuit-json" +import { getSimpleRouteJson } from "solver-utils" +import { IJumpMultiMarginAutorouter } from "../v2/lib/IJumpMultiMarginAutorouter" +import { getDebugSvg } from "./fixtures/get-debug-svg" +import { removePathLoops } from "solver-postprocessing/remove-path-loops" +import { getPathComparisonSvg } from "./fixtures/get-path-comparison-svg" + +test("remove-path-loops 2: simple loop", () => { + /** + * Ascii art of the path: + * ...... + * . . + * .......... + * . + * . + */ + // Create a path with an intentional loop + const pathWithLoop: Array<{ x: number; y: number; layer: string }> = [ + { x: 0, y: 0, layer: "top" }, + { x: 5, y: 0, layer: "top" }, + { x: 5, y: 3, layer: "top" }, + { x: 5, y: 3, layer: "top" }, + { x: 3, y: 3, layer: "top" }, + { x: 3, y: 3, layer: "bottom" }, + { x: 3, y: -3, layer: "bottom" }, + ] + + const simplifiedPath = removePathLoops(pathWithLoop) + + expect( + getPathComparisonSvg({ + pathWithLoop, + simplifiedPath, + }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/algos/multi-layer-ijump/index.ts b/algos/multi-layer-ijump/index.ts index 31fdef4..152ea17 100644 --- a/algos/multi-layer-ijump/index.ts +++ b/algos/multi-layer-ijump/index.ts @@ -14,7 +14,7 @@ export function autoroute(soup: AnySoupElement[]): SolutionWithDebugInfo { const autorouter = new MultilayerIjump({ input, connMap, - // isRemovePathLoopsEnabled: true, + isRemovePathLoopsEnabled: true, optimizeWithGoalBoxes: true, }) diff --git a/algos/multi-layer-ijump/tests/repros/__snapshots__/repro2-path-loop-fails.test.tsx-no-loop-removal.snap.svg b/algos/multi-layer-ijump/tests/repros/__snapshots__/repro2-path-loop-fails.test.tsx-no-loop-removal.snap.svg new file mode 100644 index 0000000..1498a6f --- /dev/null +++ b/algos/multi-layer-ijump/tests/repros/__snapshots__/repro2-path-loop-fails.test.tsx-no-loop-removal.snap.svg @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/algos/multi-layer-ijump/tests/repros/__snapshots__/repro2-path-loop-fails.test.tsx-with-loop-removal.snap.svg b/algos/multi-layer-ijump/tests/repros/__snapshots__/repro2-path-loop-fails.test.tsx-with-loop-removal.snap.svg new file mode 100644 index 0000000..1498a6f --- /dev/null +++ b/algos/multi-layer-ijump/tests/repros/__snapshots__/repro2-path-loop-fails.test.tsx-with-loop-removal.snap.svg @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/algos/multi-layer-ijump/tests/repros/repro2-path-loop-fails.test.tsx b/algos/multi-layer-ijump/tests/repros/repro2-path-loop-fails.test.tsx new file mode 100644 index 0000000..2bc18aa --- /dev/null +++ b/algos/multi-layer-ijump/tests/repros/repro2-path-loop-fails.test.tsx @@ -0,0 +1,39 @@ +import { MultilayerIjump } from "algos/multi-layer-ijump/MultilayerIjump" +import { test, expect } from "bun:test" +import { getDatasetGenerator } from "autorouting-dataset/lib/generators" +import { getSimpleRouteJson } from "solver-utils" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" + +test("repro2 path-loop-fails on single-trace sample 9", async () => { + const generator = getDatasetGenerator("single-trace") + const circuitJson = await generator.getExample({ seed: 9 }) + const input = getSimpleRouteJson(circuitJson, { layerCount: 2 }) + + // Run with path loop removal disabled + const autorouter1 = new MultilayerIjump({ + input, + isRemovePathLoopsEnabled: false, + debug: true, + }) + const solution1 = autorouter1.solveAndMapToTraces() + + // Run with path loop removal enabled + const autorouter2 = new MultilayerIjump({ + input, + isRemovePathLoopsEnabled: true, + debug: true, + }) + const solution2 = autorouter2.solveAndMapToTraces() + + // Compare results + expect(solution1).toHaveLength(1) + expect(solution2).toHaveLength(1) + + expect( + convertCircuitJsonToPcbSvg(circuitJson.concat(solution1 as any) as any), + ).toMatchSvgSnapshot(import.meta.path + "-no-loop-removal") + + expect( + convertCircuitJsonToPcbSvg(circuitJson.concat(solution2 as any) as any), + ).toMatchSvgSnapshot(import.meta.path + "-with-loop-removal") +}) diff --git a/module/lib/solver-postprocessing/remove-path-loops.ts b/module/lib/solver-postprocessing/remove-path-loops.ts index a97220b..bbd0e78 100644 --- a/module/lib/solver-postprocessing/remove-path-loops.ts +++ b/module/lib/solver-postprocessing/remove-path-loops.ts @@ -4,15 +4,26 @@ interface PointWithLayer { x: number y: number layer: string + route_type?: string } export function removePathLoops(path: T[]): T[] { if (path.length < 4) return path // No loops possible with less than 4 points - const result: PointWithLayer[] = [path[0]] + const result: PointWithLayer[] = [{ ...path[0] }] + let currentLayer = path[0].layer for (let i = 1; i < path.length; i++) { const currentSegment = { start: path[i - 1], end: path[i] } + const isVia = + path[i].route_type === "via" || path[i - 1].route_type === "via" + + // Handle layer changes + if (path[i].layer !== currentLayer || isVia) { + result.push({ ...path[i] }) + currentLayer = path[i].layer + continue + } let intersectionFound = false let intersectionPoint: PointWithLayer | null = null @@ -24,16 +35,18 @@ export function removePathLoops(path: T[]): T[] { if (previousSegment.start.layer !== currentSegment.start.layer) { continue } - const intersection = findIntersection(previousSegment, currentSegment) - - if (intersection) { - intersectionFound = true - intersectionPoint = { - ...intersection, - layer: previousSegment.start.layer, + // Only check intersections on the same layer + if (previousSegment.start.layer === currentSegment.start.layer) { + const intersection = findIntersection(previousSegment, currentSegment) + if (intersection) { + intersectionFound = true + intersectionPoint = { + ...intersection, + layer: currentLayer, + } + intersectionIndex = j + break } - intersectionIndex = j - break } }