diff --git a/README.md b/README.md index 59fc3af..3a829e0 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ and treats each bus-layer decision atomically. - Chamfers orthogonal routing corners into 45° segments before validating and emitting the fanout. - Verifies pad, via, trace, and already-routed fanout clearance. +- Treats an obstacle whose `connectedTo` list contains the connection name as + electrically connected copper rather than a foreign keepout. - Emits supplied fanout traces, via obstacles, and moved breakout endpoints in a new `SimpleRouteJson`. The returned problem is ready for a downstream autorouter to finish. @@ -296,6 +298,22 @@ and clearance values as the JLCPCB regressions. | ----------- | ------------ | ----: | -----------------: | ---------------: | -----: | | `sample001` | FCBGA1088L | 1088 | 589 | 499 | 6 | +## Dataset 06 + +`datasets/dataset06.ts` preserves the exact 132-connection, 265-obstacle, +single-layer mixed-footprint input from the clad1 RP2040 board. The checked-in +snapshot is intentionally labeled with its routed count: + +![clad1 RP2040 fanout reproduction](docs/images/dataset06/sample001.png) + +Same-connection copper awareness improves the deterministic result from 35 to +37 routed connections. The remaining repro is not presented as solved: it +contains escapes that must first move away from their requested boundary before +turning around the RP2040 pad field and large exposed pad. Completing it +requires a non-monotone single-layer search. Treating repeated terminals of one +electrical net as a mergeable multi-terminal tree would also remove artificial +same-net congestion. + ## Development ```sh @@ -312,7 +330,10 @@ connection, routing, and layer-assignment metrics. `bun run start` opens the datasets in the standard tscircuit solver debugger. `bun run render:dataset` writes `graphics-debug` PNGs under one subdirectory per dataset, with a red shared boundary, gray component courtyards, and green fanout-exit -markers. +markers. Pass an output directory and dataset id to render one dataset, for +example `bun scripts/render-dataset-pngs.ts docs/images dataset06`. Failed +regressions render their best partial attempt with a visible `INCOMPLETE` +label. ## Scope diff --git a/datasets/dataset06.ts b/datasets/dataset06.ts index b6127c8..7c81266 100644 --- a/datasets/dataset06.ts +++ b/datasets/dataset06.ts @@ -20,7 +20,7 @@ export const fanoutDataset06: FanoutDatasetSample[] = [ id: "sample001", name: "clad1 RP2040 shared-boundary reproduction", description: - "Exact serialized single-layer fanout input from the clad1 RP2040 assembly. The current solver routes 35 of 132 connections through the shared component-area boundary.", + "Exact serialized single-layer fanout input from the clad1 RP2040 assembly. Same-connection copper awareness raises the partial route from 35 to 37 of 132 connections; completing the fixture still requires non-monotone escapes and electrically connected multi-terminal merging.", footprintCount: componentIds.size, footprinterStrings: ["clad1 RP2040 assembly (serialized SRJ)"], simpleRouteJson: fixture.simpleRouteJson, diff --git a/docs/images/dataset06/sample001.png b/docs/images/dataset06/sample001.png new file mode 100644 index 0000000..98f5a92 Binary files /dev/null and b/docs/images/dataset06/sample001.png differ diff --git a/lib/route-bus.ts b/lib/route-bus.ts index 4dd44b3..ce7f1e4 100644 --- a/lib/route-bus.ts +++ b/lib/route-bus.ts @@ -646,6 +646,7 @@ function segmentIsClearOfObstacles(params: { const { segment, plan, segmentIndex, obstacles, clearance } = params for (const obstacle of obstacles) { if (!obstacle.layers.includes(segment.layer)) continue + if (obstacle.connectedTo.includes(plan.connectionName)) continue if ( segmentIndex === 0 && obstacle === plan.sourceObstacle && diff --git a/scripts/render-dataset-pngs.ts b/scripts/render-dataset-pngs.ts index 335efc5..bf97b94 100644 --- a/scripts/render-dataset-pngs.ts +++ b/scripts/render-dataset-pngs.ts @@ -31,6 +31,7 @@ function createVerificationOverlay( srj: SimpleRouteJson, componentBounds: ComponentBounds[], sharedBoundary: Bounds, + statusLabel?: string, ): GraphicsObject { return { rects: [ @@ -55,6 +56,21 @@ function createVerificationOverlay( fill: "#ef444408", stroke: "#dc2626", }, + ...(statusLabel + ? [ + { + center: { + x: (sharedBoundary.minX + sharedBoundary.maxX) / 2, + y: sharedBoundary.maxY + 0.65, + }, + width: 20, + height: 0.8, + fill: "#ffffff", + stroke: "#dc2626", + label: statusLabel, + }, + ] + : []), ], points: srj.connections.flatMap((connection) => connection.pointsToConnect.flatMap((point) => @@ -96,9 +112,12 @@ function formatFootprinterStrings(strings: string[]): string { } const outputDirectory = process.argv[2] ?? "verification-pngs" +const requestedDatasetId = process.argv[3] await mkdir(outputDirectory, { recursive: true }) -for (const dataset of fanoutDatasets) { +for (const dataset of fanoutDatasets.filter( + (candidate) => !requestedDatasetId || candidate.id === requestedDatasetId, +)) { const datasetDirectory = `${outputDirectory}/${dataset.id}` await mkdir(datasetDirectory, { recursive: true }) @@ -108,23 +127,26 @@ for (const dataset of fanoutDatasets) { sample.solverOptions, ) solver.solve() - if (solver.failed) { - throw new Error( - `${dataset.id}/${sample.id} failed: ${solver.error ?? "unknown solver error"}`, - ) - } - const output = solver.getOutput() + const visualizedSrj = solver.solved + ? solver.getOutput().simpleRouteJson + : sample.simpleRouteJson const componentBounds = getComponentBounds(sample.componentBounds) + const routeStatus = solver.failed + ? ` · incomplete: ${solver.attempts[0]?.routedConnectionCount ?? 0}/${sample.simpleRouteJson.connections.length}` + : "" const graphics = mergeGraphics( { ...solver.visualize(), - title: `${dataset.id}/${sample.id}: ${sample.name} · ${formatFootprinterStrings(sample.footprinterStrings)}`, + title: `${dataset.id}/${sample.id}: ${sample.name} · ${formatFootprinterStrings(sample.footprinterStrings)}${routeStatus}`, }, createVerificationOverlay( - output.simpleRouteJson, + visualizedSrj, componentBounds, sample.sharedBoundary, + solver.failed + ? `INCOMPLETE ${solver.attempts[0]?.routedConnectionCount ?? 0}/${sample.simpleRouteJson.connections.length}` + : undefined, ), ) const png = await getPngBufferFromGraphicsObject(graphics, { @@ -135,7 +157,7 @@ for (const dataset of fanoutDatasets) { pngWidth: 1400, viewbox: getVerificationViewbox( sample.sharedBoundary, - output.simpleRouteJson.bounds, + visualizedSrj.bounds, ), }) const outputPath = `${datasetDirectory}/${sample.id}.png` diff --git a/tests/connected-copper-obstacles.test.ts b/tests/connected-copper-obstacles.test.ts new file mode 100644 index 0000000..9b35749 --- /dev/null +++ b/tests/connected-copper-obstacles.test.ts @@ -0,0 +1,78 @@ +import { expect, test } from "bun:test" +import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter" +import { FanoutSolver } from "lib/fanout-solver" +import type { FanoutBusSpec } from "lib/types" + +test("a fanout trace may cross copper connected to the same connection", () => { + const simpleRouteJson: SimpleRouteJson = { + layerCount: 1, + minTraceWidth: 0.1, + nominalTraceWidth: 0.1, + minViaPadDiameter: 0.25, + minViaHoleDiameter: 0.15, + minTraceToPadEdgeClearance: 0.1, + minViaEdgeToPadEdgeClearance: 0.1, + defaultObstacleMargin: 0.1, + bounds: { minX: -1, maxX: 4, minY: -0.3, maxY: 0.3 }, + obstacles: [ + { + obstacleId: "source-pad", + componentId: "source-component", + type: "rect", + center: { x: 0, y: 0 }, + width: 0.2, + height: 0.2, + layers: ["top"], + connectedTo: ["SIGNAL", "source-pad"], + }, + { + obstacleId: "connected-copper", + componentId: "connected-component", + type: "rect", + center: { x: 1, y: 0 }, + width: 0.8, + height: 0.4, + layers: ["top"], + connectedTo: ["SIGNAL"], + }, + ], + connections: [ + { + name: "SIGNAL", + pointsToConnect: [ + { + x: 0, + y: 0, + layer: "top", + pointId: "source-pad", + pcb_port_id: "source-pad", + }, + { x: 4, y: 0, layer: "top" }, + ], + }, + ], + buses: [ + { + busId: "signal-bus", + connectionNames: ["SIGNAL"], + sourceComponentId: "source-component", + direction: "right", + }, + ] as FanoutBusSpec[], + } + + const solver = new FanoutSolver(simpleRouteJson, { + sharedBoundary: { minX: -0.5, maxX: 3, minY: -0.3, maxY: 0.3 }, + escapeLayers: ["top"], + }) + solver.solve() + + expect(solver.failed).toBe(false) + expect(solver.getOutput().fanoutTraces).toHaveLength(1) + expect(solver.getOutput().fanoutTraces[0]!.route.at(-1)).toMatchObject({ + route_type: "wire", + x: 3, + y: 0, + layer: "top", + }) +}) diff --git a/tests/dataset06-clad1-rp2040-repro.test.ts b/tests/dataset06-clad1-rp2040-repro.test.ts index 1cabbb7..c1a47da 100644 --- a/tests/dataset06-clad1-rp2040-repro.test.ts +++ b/tests/dataset06-clad1-rp2040-repro.test.ts @@ -2,7 +2,7 @@ import { expect, test } from "bun:test" import { FanoutSolver } from "lib/fanout-solver" import { fanoutDataset06 } from "../datasets/dataset06" -test("dataset06 reproduces the incomplete clad1 RP2040 shared-boundary fanout", () => { +test("dataset06 records the remaining clad1 RP2040 fanout gap", () => { const sample = fanoutDataset06[0]! expect(sample.simpleRouteJson.layerCount).toBe(1) @@ -22,11 +22,11 @@ test("dataset06 reproduces the incomplete clad1 RP2040 shared-boundary fanout", expect(solver.failed).toBe(true) expect(solver.error).toBe( - "FanoutSolver: best layer assignment routed 35/132 connections", + "FanoutSolver: best layer assignment routed 37/132 connections", ) expect(solver.attempts).toHaveLength(1) expect(solver.attempts[0]).toMatchObject({ - routedConnectionCount: 35, - routedBusCount: 35, + routedConnectionCount: 37, + routedBusCount: 37, }) -}, 20_000) +}, 60_000)