Skip to content
Draft
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
3 changes: 3 additions & 0 deletions experiments/ddr-escape-planning/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
vendor
results
node_modules
68 changes: 68 additions & 0 deletions experiments/ddr-escape-planning/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Proposal: plan DDR escapes together with movable decouplers

Status: experimental RFC; escape improvement observed, complete-board proof outstanding.

## Problem and intended behavior

A placement can pass pad/footprint overlap checks and still leave no legal signal via beside a BGA pad. Starting an expensive carrier search cannot repair that missing escape. Decouplers can block these sites on the opposite side of the board because through vias occupy every copper layer.

Before carrier routing, attempt a collective escape assignment against all physical pads, holes, vias and fixed copper. If it fails, identify blocking geometry and search a bounded set of permitted capacitor moves. Only return a placement when a joint assignment is feasible. Show a concrete proposed move and the affected signal names when automatic movement is not enabled.

## Proposed contract and integration

1. Add a planning step after physical placement geometry exists and before the first DDR phase in `Group_getRoutingPhasePlans`. Reuse the phase preparation boundary and placement-error reporting. Do not mutate `_parsedProps` or user props; derived placements must be separate data and regenerate the native geometry.
2. Require explicit movable components and a displacement limit. User-locked parts, the processor and memory stay fixed. Add per-decoupler electrical constraints (power-pin association and maximum loop/connection length) before enabling automatic moves in production. Physical placement validity alone does not establish acceptable decoupling.
3. Allocate all DDR escape sites jointly. Reserve copper geometry with net ownership, layer span, clearance and via technology. Pass those exact reservations to downstream routing; invalidate and replan if placement changes. Individually escapable pins are not proof of a jointly feasible fanout.
4. Rank candidates by feasibility and escaped connections, then movement. The prototype checks cardinal offsets 0.1–0.6 mm, with at most 128 assessments. Production needs spatial indexing, incremental regeneration, a hard wall deadline and an early exit once an acceptable assignment exists.
5. Emit readable diagnostics such as “C_DDR11 blocks a DDR escape; move 0.5 mm upward.” Keep component IDs in structured references only. Show the via/clearance region on the PCB and offer the bounded move as an edit.

Board geometry uses right-handed board-world millimetres, +X right and +Y up; positions are points and offsets are directions. Layer top/bottom refer to the PCB Z axis, not the XY direction names.

## Executable prototype

`plan.ts` implements the bounded search with an injected physical/escape assessor. It preserves the input, respects locked positions, rejects invalid placements and returns `escape_feasible` separately from routing success. `plan-board.ts` applies it to the native fixture, regenerating translated capacitor geometry and testing collective dogbones with the common solver. This translation-only adapter is not a general footprint placement engine and does not yet persist the escape reservations into carrier routing.

The small unit test checks search behavior and immutability; it is not a routing benchmark. The board trial used RAM at (0, -25) mm, 2 mm closer to the processor than the earlier proof. A first control retaining the earlier manual capacitor nudges already had 47/47 feasible escapes; it showed no benefit. The committed stress variant removes those two manual nudges while keeping the new RAM position. This is a targeted regression, not an unbiased random-layout sample.

Observed on that variant:

| Measure | Before planning | After planning |
| --- | --- | --- |
| Physical placement check | valid | valid |
| Individually escapable DDR connections | 45/47 | 47/47 |
| Collective escape assignment | failed | feasible |
| Automatic moves | none | C_U1_K8 +0.3 mm Y; C_DDR11 +0.5 mm Y |

The planner used 97 assessments and 176.185 seconds. The two moves match the earlier manual repair without hard-coding their identities or offsets. The original complete-DDR pipeline failed local dogbone assignment in 1.559 seconds; the post-planning run advances to shared-layer routing. It then reached the 120-second external timeout in `route_shared_layers`; all 47 DDR connections were not completed. See `evidence.json`. A fresh byte-0 route passed the solver's clearance/timing screen in 7.206 seconds, then all 11 native checks passed for that prefix. This is 11/47 connections, not a complete DDR bus.

## Reproduce

Run `bash setup.sh` in this directory with Bun and Git installed, then:

```sh
bun test plan.test.ts
mkdir -p results/planning
cp fixture/*.json results/planning/
bun plan-board.ts results/planning
PROOF_ROOT=results/before timeout 120s bun route-all.ts
PROOF_INPUT=results/planning/planned-input.json PROOF_ROOT=results/after timeout 120s bun route-all.ts
```

The 120-second limit is an external experimental budget for each routing trial, not a measured time-to-completion target. Inspect the exit status and reports; timeout is not success. The default complete-DDR attempt keeps all 47 requests together. `route-group.ts` is available to diagnose selected groups without crediting them as whole-board success.

## Evidence and reproducibility limits

These are experimental adapters, outside the production router. Both proposals start from core commit `8b46e058e746d635c07db1b859d182683e8c92c2`; neither branch includes the other proposal. The captured board was rendered with core `a165eb88a9403c6d7855a2c8d2d054eaef9d53ac`, from the local AM3352 board experiment based on `bc85a52da8dcad023a3721ca9c90f7a9d29ea3d6`. It has 221 components, 135 capacitors, DDR3, two USB-A connectors, USB-C power/PD, and additional peripherals. These are new placements of that circuit, not independently designed schematics.

The committed fixture is native pre-routing circuit JSON plus a 47-connection DDR routing input, with zero supplied traces/vias. It is sufficient to repeat the DDR experiment, not to rerun full source rendering. Six physical layers are retained: top/inner2/inner3/bottom for signals, inner1/inner4 reserved for ground. The proof uses 0.1 mm traces/clearance and 0.3/0.15 mm through vias, without via-in-pad. DDR bus skew is at most 0.635 mm and pair skew at most 0.127 mm; the 0.12 mm pair gap is a geometric test setting, not an impedance qualification.

Both experiments use bus-lanes-solver `992e0448aee2c40b0d8fa1d3f0537f364b0e60cc` (0.0.35), with the same `common-solver.patch` applied to both control and treatment. That common investigation patch fixes hole-in-pad rejection, propagates/checks maximum lengths, and caches native copper geometry. Those changes are **not** effects of either proposal. Fanout is the solver's pinned `15857f17beed0f228b2e29544f7ce393af297c86`; native checks are 0.0.264. Results were observed with Bun 1.4.0 on one development machine. Transitive dependencies are not fully locked here; repeated runs and pinned CI dependencies remain required.

The reports distinguish escape feasibility, a validated DDR prefix, all 47 DDR connections, and whole-board completion. No whole-board completion or signal-integrity signoff is claimed. These experiments do not yet satisfy the requested independent complete-board proof.

## Merge gates

- Commit/reproduce complete, native-validated routes for the new layout and an additional held-out placement, including all peripheral/power nets. Do not accept partial copper as a completed board.
- Compare clean base and head using identical inputs, constraints, budgets, and hardware, then repeat with a dependency lock. This common-patch experiment is not a clean-upstream performance benchmark.
- Record success rate, elapsed time including validation, retained valid work, retries, unresolved nets and timing/DRC violations. Publish failures as well as successes.
- Keep behavior opt-in until regressions across the existing DDR corpus and the new boards pass. No merge or production default change is proposed in this PR.
158 changes: 158 additions & 0 deletions experiments/ddr-escape-planning/common-solver.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
diff --git a/lib/match-native-carrier-signals.ts b/lib/match-native-carrier-signals.ts
index 533d60c..8f53501 100644
--- a/lib/match-native-carrier-signals.ts
+++ b/lib/match-native-carrier-signals.ts
@@ -55,7 +55,6 @@ export function* matchNativeCarrierSignals(
connections: original.connections.filter((c) => !pairedNames.has(c.name)),
traces: [...(original.traces ?? []), ...paired],
}
- if (!input.connections.length) return paired
const minimums = new Map<string, number>(),
maximums = new Map<string, number>()
for (const bus of original.buses ?? []) {
@@ -163,6 +162,7 @@ export function* matchNativeCarrierSignals(
allowBlindAndBuriedVias: false,
} as unknown as Parameters<typeof validateRoutedCopperDrc>[0]).valid
}
+ if (!input.connections.length) return valid([]) ? paired : null
function* negotiate(
seed: Trace[],
frozen: Trace[],
diff --git a/lib/native-carrier-router.ts b/lib/native-carrier-router.ts
index 4ee6533..b394f07 100644
--- a/lib/native-carrier-router.ts
+++ b/lib/native-carrier-router.ts
@@ -124,6 +124,36 @@ export class NativeCarrierRouter {
N = this.cellCount
const width = input.minTraceWidth,
diameter = input.minViaPadDiameter ?? 0.3
+ // A same-net pad is transparent to copper clearance, but not to a drill.
+ // Keep its physical hole out of SMT pads unless fabrication permits it.
+ const ownPads =
+ input.allowViaInPad === true
+ ? []
+ : input.obstacles.filter(
+ (o) =>
+ o.componentId &&
+ (o.connectedTo.includes(c.name) ||
+ c.pointsToConnect.some(
+ (p) => p.pcb_port_id && o.connectedTo.includes(p.pcb_port_id),
+ )),
+ )
+ const holeRadius = (input.minViaHoleDiameter ?? 0.15) / 2
+ const holeClearsOwnPads = (p: Point) =>
+ ownPads.every((o) => {
+ const angle = (-(o.ccwRotationDegrees ?? 0) * Math.PI) / 180
+ const dx = p.x - o.center.x,
+ dy = p.y - o.center.y
+ const x = dx * Math.cos(angle) - dy * Math.sin(angle),
+ y = dx * Math.sin(angle) + dy * Math.cos(angle)
+ const distance =
+ o.shape === "circle"
+ ? Math.hypot(dx, dy) - o.width / 2
+ : Math.hypot(
+ Math.max(0, Math.abs(x) - o.width / 2),
+ Math.max(0, Math.abs(y) - o.height / 2),
+ )
+ return distance >= holeRadius + 1e-5
+ })
const wireMargin = width / 2 + (input.minBoardEdgeClearance ?? 0)
const viaMargin = diameter / 2 + (input.minBoardEdgeClearance ?? 0)
const softGroups = (options.softTraces ?? []).map((t) =>
@@ -257,7 +287,8 @@ export class NativeCarrierRouter {
a.x <= bounds.maxX - viaMargin &&
a.y >= bounds.minY + viaMargin &&
a.y <= bounds.maxY - viaMargin &&
- vias.every((v) => !v.isBlocked(cell))
+ vias.every((v) => !v.isBlocked(cell)) &&
+ holeClearsOwnPads(a)
) {
const cost = softVia.reduce(
(sum, v) => sum + (v.softEdgeIsClear(a, a) ? 0 : penalty * 4),
diff --git a/lib/plan-native-paired-carriers.ts b/lib/plan-native-paired-carriers.ts
index 2045e3e..823c493 100644
--- a/lib/plan-native-paired-carriers.ts
+++ b/lib/plan-native-paired-carriers.ts
@@ -466,7 +466,16 @@ export function* planNativePairedCarriers(
.map((pair) => ({
busId: `native_${bus.busId}`,
connectionNames: pair.connectionNames,
- minLength: Math.max(target, bus.minLength ?? 0),
+ minLength: Math.max(
+ options.busTargetLength === undefined
+ ? Math.min(
+ target,
+ (bus.maxLength ?? Infinity) - (pair.lengthTolerance ?? 0),
+ )
+ : target,
+ bus.minLength ?? 0,
+ ),
+ maxLength: bus.maxLength,
maxLengthSkew: pair.lengthTolerance,
})),
)
diff --git a/lib/types.ts b/lib/types.ts
index 9a64cb0..018b35f 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -74,6 +74,8 @@ export interface SimpleRouteJson {
minViaPadDiameter?: number
minViaHoleDiameter?: number
minViaHoleEdgeToViaHoleEdgeClearance?: number
+ /** Whether drilled holes may overlap SMT pads (requires suitable fabrication). */
+ allowViaInPad?: boolean
allowBlindAndBuriedVias?: boolean
bounds: { minX: number; maxX: number; minY: number; maxY: number }
connections: Connection[]
diff --git a/lib/vector-scene.ts b/lib/vector-scene.ts
index 15e370f..f2e9de6 100644
--- a/lib/vector-scene.ts
+++ b/lib/vector-scene.ts
@@ -14,6 +14,18 @@ export interface Copper {
owners: string[]
rect?: { minX: number; maxX: number; minY: number; maxY: number }
}
+// Investigation-only optimization. The scene's copper ownership is immutable.
+const ownerIndexes = new WeakMap<string[], Set<string>>()
+function sharesOwner(aliases: string[], owners: Set<string>): boolean {
+ if (aliases.length <= 16) return aliases.some((alias) => owners.has(alias))
+ let index = ownerIndexes.get(aliases)
+ if (!index) {
+ index = new Set(aliases)
+ ownerIndexes.set(aliases, index)
+ }
+ for (const owner of owners) if (index.has(owner)) return true
+ return false
+}
export function fixedCopper(input: SimpleRouteJson): Copper[] {
const result: Copper[] = []
for (const o of input.obstacles) {
@@ -316,7 +328,7 @@ export class VectorScene {
this.copper = all.filter(
(c) =>
c.layer === connection.pointsToConnect[0].layer &&
- !c.owners.some((n) => this.owners.has(n)),
+ !sharesOwner(c.owners, this.owners),
)
this.margin =
width / 2 +
diff --git a/tests/native-carrier-router.test.ts b/tests/native-carrier-router.test.ts
index 038d3e3..90c2a54 100644
--- a/tests/native-carrier-router.test.ts
+++ b/tests/native-carrier-router.test.ts
@@ -97,3 +97,15 @@ test("via placement checks copper on unused physical planes", () => {
})
expect(route(input)).toBeNull()
})
+
+test("native carrier keeps drill holes outside its own pads by default", () => {
+ const input = nativeInput()
+ input.allowViaInPad = false
+ const trace = route(input)!
+ expect(trace).not.toBeNull()
+ for (const via of trace.route.filter((p) => p.route_type === "via"))
+ for (const pad of input.obstacles)
+ expect(
+ Math.hypot(via.x - pad.center.x, via.y - pad.center.y),
+ ).toBeGreaterThanOrEqual(pad.width / 2 + input.minViaHoleDiameter! / 2)
+})
95 changes: 95 additions & 0 deletions experiments/ddr-escape-planning/evidence.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"status": "partial_validation_only",
"wholeBoardComplete": false,
"completeDdr47": false,
"coreProposalBase": "8b46e058e746d635c07db1b859d182683e8c92c2",
"coreCaptureRevision": "a165eb88a9403c6d7855a2c8d2d054eaef9d53ac",
"solverBase": "992e0448aee2c40b0d8fa1d3f0537f364b0e60cc",
"commonSolverPatchSha256": "c19f9f05142feeb97c2da875936fcf4ae6dbf5fdebb1d0b4abf63a4508044218",
"bunVersion": "1.4.0",
"nativeChecksVersion": "0.0.264",
"connectivityMapVersion": "0.0.33",
"fixtureSha256": {
"input.json": "b337b5d138281abe3d433c90ade3f34ee9144f11413401684fce607ed863befc",
"native.circuit.json": "3278f452523f9367f6223537e894fc85d09a648f95935294df775a505eeb4ee2",
"capture.json": "9bfcfb0102d18c6fce6f60e29d2cb59772948434a3a72b28074afc4501aa9281"
},
"layerCount": 6,
"timingCaveat": "Single observations on a development machine; some early trials overlapped other diagnostic work. Not a statistical speed benchmark.",
"planning": {
"before": {
"placementValid": true,
"escapedConnections": 45,
"requiredConnections": 47,
"jointlyFeasible": false,
"blockedBy": [
"pcb_component_42",
"pcb_component_97",
"pcb_component_101"
]
},
"status": "escape_feasible",
"evaluations": 97,
"assessment": {
"placementValid": true,
"escapedConnections": 47,
"requiredConnections": 47,
"jointlyFeasible": true,
"blockedBy": []
},
"changes": [
{
"componentId": "pcb_component_42",
"from": {
"x": -2.2,
"y": -3.71
},
"to": {
"x": -2.2,
"y": -3.41
}
},
{
"componentId": "pcb_component_101",
"from": {
"x": 3.110046,
"y": -26.400023
},
"to": {
"x": 3.110046,
"y": -25.900023
}
}
],
"milliseconds": 176185.052952
},
"baselineDdr": {
"solved": false,
"failed": true,
"error": "No collision-free local dogbone assignment; stage=preferred_assignment; attempt=5; fixedTraces=0; unavailableLocalVia=[]; nativeFallbacks=[]; cohortTrials=0",
"phase": "failed",
"seconds": 1.558831621,
"stats": {
"carrierStrategy": "preferred"
}
},
"afterDdr": {
"status": "timed_out",
"externalBudgetSeconds": 120,
"exitCode": 124,
"lastProgress": {
"seconds": 117.21377526399999,
"phase": "route_shared_layers",
"iterations": 265013
}
},
"unmodifiedNudgedControl": {
"jointEscapeFeasible": true,
"escapedConnections": 47,
"benefitObserved": false,
"ramPositionMm": {
"x": 0,
"y": -25
}
}
}
Loading
Loading