Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion datasets/dataset06.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Binary file added docs/images/dataset06/sample001.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions lib/route-bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
42 changes: 32 additions & 10 deletions scripts/render-dataset-pngs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function createVerificationOverlay(
srj: SimpleRouteJson,
componentBounds: ComponentBounds[],
sharedBoundary: Bounds,
statusLabel?: string,
): GraphicsObject {
return {
rects: [
Expand All @@ -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) =>
Expand Down Expand Up @@ -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 })

Expand All @@ -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, {
Expand All @@ -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`
Expand Down
78 changes: 78 additions & 0 deletions tests/connected-copper-obstacles.test.ts
Original file line number Diff line number Diff line change
@@ -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",
})
})
10 changes: 5 additions & 5 deletions tests/dataset06-clad1-rp2040-repro.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Loading