From 76385f1864a226006426e5a25aff0221f3385dc6 Mon Sep 17 00:00:00 2001 From: ShiboSoftwareDev Date: Sun, 2 Aug 2026 11:16:26 +0200 Subject: [PATCH 1/4] test: reproduce synthetic duplicate lane placement --- ...sted-port-physical-placement-repro.test.ts | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 tests/solver/duplicate-congested-port-physical-placement-repro.test.ts diff --git a/tests/solver/duplicate-congested-port-physical-placement-repro.test.ts b/tests/solver/duplicate-congested-port-physical-placement-repro.test.ts new file mode 100644 index 0000000..7b9050d --- /dev/null +++ b/tests/solver/duplicate-congested-port-physical-placement-repro.test.ts @@ -0,0 +1,158 @@ +import "bun-match-svg" +import { expect, test } from "bun:test" +import type { SerializedHyperGraph } from "@tscircuit/hypergraph" +import { + getSvgFromGraphicsObject, + stackGraphicsVertically, + type GraphicsObject, +} from "graphics-debug" +import { DuplicateCongestedPortSolver } from "lib/index" + +const REQUIRED_CENTER_SPACING = 0.2 +const TRACE_WIDTH = 0.1 + +const createRegion = ( + regionId: string, + center: { x: number; y: number }, + width: number, + height: number, + pointIds: string[], +): SerializedHyperGraph["regions"][number] => ({ + regionId, + pointIds, + d: { center, width, height }, +}) + +const createPort = ( + portId: string, + region1Id: string, + region2Id: string, + x: number, + y: number, +): SerializedHyperGraph["ports"][number] => ({ + portId, + region1Id, + region2Id, + d: { x, y, z: 0 }, +}) + +const createFixture = (): SerializedHyperGraph => ({ + regions: [ + createRegion("start-a", { x: -4, y: 0 }, 2, 2, ["start-a-port"]), + createRegion("start-b", { x: -4, y: -0.2 }, 2, 2, ["start-b-port"]), + createRegion("left", { x: -1, y: 0 }, 2, 10, [ + "start-a-port", + "start-b-port", + "shared", + "neighbor", + ]), + createRegion("right", { x: 1, y: 0 }, 2, 10, [ + "shared", + "neighbor", + "end-a-port", + "end-b-port", + ]), + createRegion("end-a", { x: 4, y: 0 }, 2, 2, ["end-a-port"]), + createRegion("end-b", { x: 4, y: -0.2 }, 2, 2, ["end-b-port"]), + ], + ports: [ + createPort("start-a-port", "start-a", "left", -3, 0), + createPort("start-b-port", "start-b", "left", -3, -0.2), + createPort("shared", "left", "right", 0, 0), + createPort("neighbor", "left", "right", 0, 4), + createPort("end-a-port", "right", "end-a", 3, 0), + createPort("end-b-port", "right", "end-b", 3, -0.2), + ], + connections: [ + { + connectionId: "connection-a", + startRegionId: "start-a", + endRegionId: "end-a", + mutuallyConnectedNetworkId: "net-a", + }, + { + connectionId: "connection-b", + startRegionId: "start-b", + endRegionId: "end-b", + mutuallyConnectedNetworkId: "net-b", + }, + ], +}) + +test("visualizes physical duplicate lanes when the boundary has sufficient capacity", () => { + const solver = new DuplicateCongestedPortSolver(createFixture(), { + duplicatePortProximity: 0.05, + minimumDuplicatePortSpacing: REQUIRED_CENTER_SPACING, + duplicatePortWidth: TRACE_WIDTH, + }) + solver.solve() + + const lanePorts = solver + .getOutput() + .ports.filter( + (port) => + port.portId === "shared" || port.d?.duplicatedFromPortId === "shared", + ) + .sort((a, b) => Number(a.d?.y) - Number(b.d?.y)) + const actualCenterSpacing = Math.abs( + Number(lanePorts[1]?.d?.y) - Number(lanePorts[0]?.d?.y), + ) + + const graphics: GraphicsObject = { + rects: [ + { + center: { x: -1, y: 0 }, + width: 2, + height: 10, + fill: "rgba(80, 140, 220, 0.12)", + stroke: "rgb(80, 140, 220)", + label: "left graph region", + }, + { + center: { x: 1, y: 0 }, + width: 2, + height: 10, + fill: "rgba(80, 140, 220, 0.12)", + stroke: "rgb(80, 140, 220)", + label: "right graph region", + }, + ], + lines: lanePorts.flatMap((port, index) => [ + { + points: [ + { x: -0.8, y: Number(port.d?.y) }, + { x: 0.8, y: Number(port.d?.y) }, + ], + strokeColor: "rgba(245, 158, 11, 0.25)", + strokeWidth: REQUIRED_CENTER_SPACING, + label: `lane ${index + 1}: trace plus clearance`, + }, + { + points: [ + { x: -0.8, y: Number(port.d?.y) }, + { x: 0.8, y: Number(port.d?.y) }, + ], + strokeColor: "rgb(220, 38, 38)", + strokeWidth: TRACE_WIDTH, + label: `lane ${index + 1}: physical trace`, + }, + ]), + points: lanePorts.map((port, index) => ({ + x: Number(port.d?.x), + y: Number(port.d?.y), + color: "rgb(17, 24, 39)", + label: `graph port ${index + 1}`, + })), + } + + expect(lanePorts).toHaveLength(2) + expect( + getSvgFromGraphicsObject( + stackGraphicsVertically([graphics], { + titles: [ + `Actual lane spacing: ${actualCenterSpacing.toFixed(3)} mm / ${REQUIRED_CENTER_SPACING.toFixed(3)} mm required`, + ], + }), + ), + ).toMatchSvgSnapshot(import.meta.path) +}) From 61ce0eeba32f69dcc6251abb09623dc33926edc9 Mon Sep 17 00:00:00 2001 From: ShiboSoftwareDev Date: Sun, 2 Aug 2026 11:18:21 +0200 Subject: [PATCH 2/4] ci: upload generated lane placement snapshot --- .github/workflows/bun-test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/bun-test.yml b/.github/workflows/bun-test.yml index ff846d8..c72faeb 100644 --- a/.github/workflows/bun-test.yml +++ b/.github/workflows/bun-test.yml @@ -36,5 +36,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: test-snapshots - path: tests/**/__snapshots__/*.diff.png + path: | + tests/**/__snapshots__/*.diff.png + tests/solver/__snapshots__/duplicate-congested-port-physical-placement-repro.snap.svg if-no-files-found: ignore From 71f1808c1bcfa477d264efa315a158a745a77dc7 Mon Sep 17 00:00:00 2001 From: ShiboSoftwareDev Date: Sun, 2 Aug 2026 11:21:09 +0200 Subject: [PATCH 3/4] test: clarify physical lane placement snapshot --- ...sted-port-physical-placement-repro.test.ts | 103 +++++++++++------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/tests/solver/duplicate-congested-port-physical-placement-repro.test.ts b/tests/solver/duplicate-congested-port-physical-placement-repro.test.ts index 7b9050d..655a900 100644 --- a/tests/solver/duplicate-congested-port-physical-placement-repro.test.ts +++ b/tests/solver/duplicate-congested-port-physical-placement-repro.test.ts @@ -3,7 +3,6 @@ import { expect, test } from "bun:test" import type { SerializedHyperGraph } from "@tscircuit/hypergraph" import { getSvgFromGraphicsObject, - stackGraphicsVertically, type GraphicsObject, } from "graphics-debug" import { DuplicateCongestedPortSolver } from "lib/index" @@ -38,30 +37,32 @@ const createPort = ( const createFixture = (): SerializedHyperGraph => ({ regions: [ - createRegion("start-a", { x: -4, y: 0 }, 2, 2, ["start-a-port"]), - createRegion("start-b", { x: -4, y: -0.2 }, 2, 2, ["start-b-port"]), - createRegion("left", { x: -1, y: 0 }, 2, 10, [ + createRegion("start-a", { x: -1.5, y: 0 }, 1, 1, ["start-a-port"]), + createRegion("start-b", { x: -1.5, y: -0.2 }, 1, 1, [ + "start-b-port", + ]), + createRegion("left", { x: -0.5, y: 0 }, 1, 1, [ "start-a-port", "start-b-port", "shared", "neighbor", ]), - createRegion("right", { x: 1, y: 0 }, 2, 10, [ + createRegion("right", { x: 0.5, y: 0 }, 1, 1, [ "shared", "neighbor", "end-a-port", "end-b-port", ]), - createRegion("end-a", { x: 4, y: 0 }, 2, 2, ["end-a-port"]), - createRegion("end-b", { x: 4, y: -0.2 }, 2, 2, ["end-b-port"]), + createRegion("end-a", { x: 1.5, y: 0 }, 1, 1, ["end-a-port"]), + createRegion("end-b", { x: 1.5, y: -0.2 }, 1, 1, ["end-b-port"]), ], ports: [ - createPort("start-a-port", "start-a", "left", -3, 0), - createPort("start-b-port", "start-b", "left", -3, -0.2), + createPort("start-a-port", "start-a", "left", -1, 0), + createPort("start-b-port", "start-b", "left", -1, -0.2), createPort("shared", "left", "right", 0, 0), - createPort("neighbor", "left", "right", 0, 4), - createPort("end-a-port", "right", "end-a", 3, 0), - createPort("end-b-port", "right", "end-b", 3, -0.2), + createPort("neighbor", "left", "right", 0, 0.4), + createPort("end-a-port", "right", "end-a", 1, 0), + createPort("end-b-port", "right", "end-b", 1, -0.2), ], connections: [ { @@ -101,58 +102,78 @@ test("visualizes physical duplicate lanes when the boundary has sufficient capac const graphics: GraphicsObject = { rects: [ { - center: { x: -1, y: 0 }, - width: 2, - height: 10, + center: { x: -0.5, y: 0 }, + width: 1, + height: 1, fill: "rgba(80, 140, 220, 0.12)", stroke: "rgb(80, 140, 220)", label: "left graph region", }, { - center: { x: 1, y: 0 }, - width: 2, - height: 10, + center: { x: 0.5, y: 0 }, + width: 1, + height: 1, fill: "rgba(80, 140, 220, 0.12)", stroke: "rgb(80, 140, 220)", label: "right graph region", }, ], - lines: lanePorts.flatMap((port, index) => [ + lines: [ { points: [ - { x: -0.8, y: Number(port.d?.y) }, - { x: 0.8, y: Number(port.d?.y) }, + { x: 0, y: -0.5 }, + { x: 0, y: 0.5 }, ], - strokeColor: "rgba(245, 158, 11, 0.25)", - strokeWidth: REQUIRED_CENTER_SPACING, - label: `lane ${index + 1}: trace plus clearance`, + strokeColor: "rgb(13, 148, 136)", + strokeWidth: 0.012, + label: "shared graph boundary", }, - { - points: [ - { x: -0.8, y: Number(port.d?.y) }, - { x: 0.8, y: Number(port.d?.y) }, - ], - strokeColor: "rgb(220, 38, 38)", - strokeWidth: TRACE_WIDTH, - label: `lane ${index + 1}: physical trace`, - }, - ]), + ...lanePorts.flatMap((port, index) => [ + { + points: [ + { x: -0.35, y: Number(port.d?.y) }, + { x: 0.35, y: Number(port.d?.y) }, + ], + strokeColor: "rgba(245, 158, 11, 0.25)", + strokeWidth: REQUIRED_CENTER_SPACING, + label: `lane ${index + 1}: trace plus clearance`, + }, + { + points: [ + { x: -0.35, y: Number(port.d?.y) }, + { x: 0.35, y: Number(port.d?.y) }, + ], + strokeColor: "rgb(220, 38, 38)", + strokeWidth: TRACE_WIDTH, + label: `lane ${index + 1}: physical trace`, + }, + ]), + ], points: lanePorts.map((port, index) => ({ x: Number(port.d?.x), y: Number(port.d?.y), color: "rgb(17, 24, 39)", label: `graph port ${index + 1}`, })), + texts: [ + { + x: -0.95, + y: 0.62, + text: `Actual lane spacing: ${actualCenterSpacing.toFixed(3)} mm / ${REQUIRED_CENTER_SPACING.toFixed(3)} mm required`, + fontSize: 0.045, + }, + { + x: -0.95, + y: 0.55, + text: "Red = 0.1 mm trace Orange = trace + clearance Teal = shared boundary", + fontSize: 0.032, + color: "rgb(75, 85, 99)", + }, + ], } expect(lanePorts).toHaveLength(2) expect( - getSvgFromGraphicsObject( - stackGraphicsVertically([graphics], { - titles: [ - `Actual lane spacing: ${actualCenterSpacing.toFixed(3)} mm / ${REQUIRED_CENTER_SPACING.toFixed(3)} mm required`, - ], - }), - ), + getSvgFromGraphicsObject(graphics), ).toMatchSvgSnapshot(import.meta.path) }) From f242af206fecf1102fb00f1c2de9a34b067e2604 Mon Sep 17 00:00:00 2001 From: ShiboSoftwareDev Date: Sun, 2 Aug 2026 11:23:23 +0200 Subject: [PATCH 4/4] test: add hosted duplicate lane snapshot --- .github/workflows/bun-test.yml | 4 +- ...ted-port-physical-placement-repro.snap.svg | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 tests/solver/__snapshots__/duplicate-congested-port-physical-placement-repro.snap.svg diff --git a/.github/workflows/bun-test.yml b/.github/workflows/bun-test.yml index c72faeb..ff846d8 100644 --- a/.github/workflows/bun-test.yml +++ b/.github/workflows/bun-test.yml @@ -36,7 +36,5 @@ jobs: uses: actions/upload-artifact@v4 with: name: test-snapshots - path: | - tests/**/__snapshots__/*.diff.png - tests/solver/__snapshots__/duplicate-congested-port-physical-placement-repro.snap.svg + path: tests/**/__snapshots__/*.diff.png if-no-files-found: ignore diff --git a/tests/solver/__snapshots__/duplicate-congested-port-physical-placement-repro.snap.svg b/tests/solver/__snapshots__/duplicate-congested-port-physical-placement-repro.snap.svg new file mode 100644 index 0000000..abe98e5 --- /dev/null +++ b/tests/solver/__snapshots__/duplicate-congested-port-physical-placement-repro.snap.svg @@ -0,0 +1,44 @@ +Actual lane spacing: 0.025 mm / 0.200 mm requiredRed = 0.1 mm trace Orange = trace + clearance Teal = shared boundary