@tscircuit/fanout-solver 0.0.18 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -22
- package/lib/fanout-solver.ts +111 -18
- package/lib/geometry.ts +38 -19
- package/lib/index.ts +5 -0
- package/lib/net-identity.ts +163 -0
- package/lib/route-bus.ts +108 -3
- package/lib/types.ts +39 -0
- package/lib/validate-fanout-solution.ts +772 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -42,6 +42,11 @@ and treats each bus-layer decision atomically.
|
|
|
42
42
|
alternate instead of favoring one axis; square grids distribute equally
|
|
43
43
|
across north, south, east, and west.
|
|
44
44
|
- Enumerates combinations of the copper layers implied by `layerCount`.
|
|
45
|
+
- Keeps a bounded beam of route alternatives for multi-connection buses, so
|
|
46
|
+
grouped power/signal lanes can backtrack across layer and track choices
|
|
47
|
+
before committing a prefix.
|
|
48
|
+
- Keys route-prefix caches by both bus and layer, preserving plan uniqueness
|
|
49
|
+
when grouped-layer search changes bus order.
|
|
45
50
|
- Prefers depth-cycled layer assignments: matching north/south (or east/west)
|
|
46
51
|
bus depths share a layer, and deeper pairs cycle through every available
|
|
47
52
|
escape layer. This forces a small stackup to reuse routing channels.
|
|
@@ -61,9 +66,17 @@ and treats each bus-layer decision atomically.
|
|
|
61
66
|
breakout corridor.
|
|
62
67
|
- Chamfers orthogonal routing corners into 45° segments before validating and
|
|
63
68
|
emitting the fanout.
|
|
64
|
-
- Verifies pad, via, trace, and already-routed fanout clearance
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
- Verifies oriented-pad, via, trace, and already-routed fanout clearance on
|
|
70
|
+
every complete candidate, independent of the routing strategy that produced
|
|
71
|
+
it.
|
|
72
|
+
- Resolves `netConnectionName`, connection, port, trace, and obstacle metadata
|
|
73
|
+
into electrical-net identities. Same-net copper may merge; different-net
|
|
74
|
+
pads, traces, and vias must retain clearance on every layer they occupy.
|
|
75
|
+
- `allowSameNetMerges` lets grouped branches such as VCC or GND reuse connected
|
|
76
|
+
copper instead of reserving artificial clearance from one another. It is
|
|
77
|
+
opt-in; different electrical nets remain hard obstacles.
|
|
78
|
+
- Audits route continuity, unique connection coverage, boundary exits, and
|
|
79
|
+
retained downstream endpoints before marking a solution complete.
|
|
67
80
|
- Emits supplied fanout traces, via obstacles, and moved breakout endpoints in a
|
|
68
81
|
new `SimpleRouteJson`. The returned problem is ready for a downstream
|
|
69
82
|
autorouter to finish.
|
|
@@ -177,6 +190,8 @@ bus-layer combination search.
|
|
|
177
190
|
- `busLayerAssignments`: the selected layer for every bus
|
|
178
191
|
- `busDirections`: the direction shared by each bus
|
|
179
192
|
- `attempts`: score and success metadata for every tried layer combination
|
|
193
|
+
- `validation`: the final geometry/connectivity report, including the number of
|
|
194
|
+
independently validated breakouts
|
|
180
195
|
|
|
181
196
|
## Dataset 01
|
|
182
197
|
|
|
@@ -198,15 +213,18 @@ parameters. Each sample has one shared boundary around all of its footprints,
|
|
|
198
213
|
and component bounds come from the exact footprinter-generated copper pad
|
|
199
214
|
extents.
|
|
200
215
|
|
|
201
|
-
##
|
|
216
|
+
## SRJ29 benchmark
|
|
202
217
|
|
|
203
|
-
The repository
|
|
204
|
-
[`tscircuit/dataset-
|
|
205
|
-
pinned development dependency. The adapter keeps the complete obstacle
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
218
|
+
The repository loads all 200 samples from the derivative
|
|
219
|
+
[`tscircuit/dataset-srj29-bga-decoupling`](https://github.com/tscircuit/dataset-srj29-bga-decoupling)
|
|
220
|
+
as a pinned development dependency. The adapter keeps the complete obstacle
|
|
221
|
+
field, including opposite-layer capacitor pads and bodies. VCC and GND are
|
|
222
|
+
grouped onto opposite boundary corridors, while the capacitor pad remains the
|
|
223
|
+
downstream endpoint of every power connection; a local capacitor or plane via
|
|
224
|
+
alone cannot count as a solved BGA pin. Remaining edge signals are grouped by
|
|
225
|
+
direction. Every adapted problem uses the same six-layer stackup (`top`,
|
|
226
|
+
`inner1` through `inner4`, and `bottom`) so benchmark improvements are directly
|
|
227
|
+
comparable.
|
|
210
228
|
|
|
211
229
|
Run the full benchmark with:
|
|
212
230
|
|
|
@@ -219,22 +237,23 @@ Use `--sample sample001`, `--limit 10`, or
|
|
|
219
237
|
default and print progress as they finish. `--concurrency 8` runs isolated
|
|
220
238
|
samples in parallel, and `--sample-timeout-seconds 600` prevents a difficult
|
|
221
239
|
sample from blocking the remaining work. Each run writes the full ordered
|
|
222
|
-
results to `benchmark-results/
|
|
223
|
-
`benchmark-results/
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
240
|
+
results to `benchmark-results/srj29.json` and
|
|
241
|
+
`benchmark-results/srj29.md`. A row is marked solved only when every input
|
|
242
|
+
connection has a validated breakout, all downstream endpoints remain attached,
|
|
243
|
+
and the independent DRC audit passes. Partial solutions are reported as
|
|
244
|
+
benchmark results instead of failing the command, making current completion
|
|
245
|
+
rates a baseline for solver improvements. `bun run benchmark:srj29` is an alias
|
|
246
|
+
for the same command.
|
|
247
|
+
|
|
248
|
+
The `SRJ29 Benchmark` GitHub Actions workflow runs the complete dataset on a
|
|
229
249
|
Blacksmith 32-vCPU ARM runner with 32 sample processes by default. It can be
|
|
230
250
|
started manually with an optional sample id, or for a pull request by adding
|
|
231
251
|
`[BENCHMARK TEST]` to its title. The workflow publishes the Markdown summary and
|
|
232
252
|
uploads both reports as an artifact.
|
|
233
253
|
|
|
234
|
-
Run `bun run start` and
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
URL parameter.
|
|
254
|
+
Run `bun run start` and inspect the SRJ29 fixtures to step through the selected
|
|
255
|
+
sample. The derivative dataset also publishes dedicated Cosmos pages for the
|
|
256
|
+
first ten samples.
|
|
238
257
|
|
|
239
258
|
## Dataset 02
|
|
240
259
|
|
package/lib/fanout-solver.ts
CHANGED
|
@@ -18,8 +18,10 @@ import {
|
|
|
18
18
|
} from "./route-bus"
|
|
19
19
|
import { routeSingleLayerWithAdaptiveExits } from "./route-single-layer-adaptive-exits"
|
|
20
20
|
import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
|
|
21
|
+
import { validateFanoutSolution } from "./validate-fanout-solution"
|
|
21
22
|
import type {
|
|
22
23
|
AssignmentAttempt,
|
|
24
|
+
Bounds,
|
|
23
25
|
FanoutAttemptSummary,
|
|
24
26
|
FanoutBorderDistribution,
|
|
25
27
|
FanoutRoutePlan,
|
|
@@ -34,6 +36,7 @@ interface ResolvedFanoutConfig {
|
|
|
34
36
|
viaHoleDiameter: number
|
|
35
37
|
clearance: number
|
|
36
38
|
compactBusTracks: boolean
|
|
39
|
+
allowSameNetMerges: boolean
|
|
37
40
|
singleLayerPushAndShove: boolean
|
|
38
41
|
singleLayerAdaptiveExits: boolean
|
|
39
42
|
borderDistribution: FanoutBorderDistribution
|
|
@@ -123,6 +126,7 @@ function resolveConfig(
|
|
|
123
126
|
viaHoleDiameter,
|
|
124
127
|
clearance,
|
|
125
128
|
compactBusTracks: options.compactBusTracks ?? false,
|
|
129
|
+
allowSameNetMerges: options.allowSameNetMerges ?? false,
|
|
126
130
|
singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
|
|
127
131
|
singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
|
|
128
132
|
borderDistribution,
|
|
@@ -303,6 +307,7 @@ function getCandidateEscapeLayersForBus(params: {
|
|
|
303
307
|
viaHoleDiameter: config.viaHoleDiameter,
|
|
304
308
|
clearance: config.clearance,
|
|
305
309
|
compactBusTracks: config.compactBusTracks,
|
|
310
|
+
allowSameNetMerges: config.allowSameNetMerges,
|
|
306
311
|
staticClearanceCache,
|
|
307
312
|
}) !== null,
|
|
308
313
|
)
|
|
@@ -422,6 +427,35 @@ export class FanoutSolver extends BaseSolver {
|
|
|
422
427
|
return "FanoutSolver"
|
|
423
428
|
}
|
|
424
429
|
|
|
430
|
+
private getValidationBoundary(): Bounds {
|
|
431
|
+
if (this.options.sharedBoundary) return this.options.sharedBoundary
|
|
432
|
+
const firstBoundary = this.preparedBuses[0]?.sharedBoundary
|
|
433
|
+
if (!firstBoundary) return this.inputSrj.bounds
|
|
434
|
+
return this.preparedBuses.slice(1).reduce<Bounds>(
|
|
435
|
+
(boundary, bus) => ({
|
|
436
|
+
minX: Math.min(boundary.minX, bus.sharedBoundary.minX),
|
|
437
|
+
maxX: Math.max(boundary.maxX, bus.sharedBoundary.maxX),
|
|
438
|
+
minY: Math.min(boundary.minY, bus.sharedBoundary.minY),
|
|
439
|
+
maxY: Math.max(boundary.maxY, bus.sharedBoundary.maxY),
|
|
440
|
+
}),
|
|
441
|
+
{ ...firstBoundary },
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private validateCompletePlans(
|
|
446
|
+
plans: readonly FanoutRoutePlan[],
|
|
447
|
+
outputSrj: SimpleRouteJson,
|
|
448
|
+
) {
|
|
449
|
+
return validateFanoutSolution({
|
|
450
|
+
inputSrj: this.inputSrj,
|
|
451
|
+
outputSrj,
|
|
452
|
+
plans,
|
|
453
|
+
preparedBuses: this.preparedBuses,
|
|
454
|
+
sharedBoundary: this.getValidationBoundary(),
|
|
455
|
+
clearance: this.config.clearance,
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
|
|
425
459
|
private evaluateAssignmentWithStrategy(
|
|
426
460
|
assignmentIndex: number,
|
|
427
461
|
busLayerAssignments: Readonly<Record<string, string>>,
|
|
@@ -483,7 +517,12 @@ export class FanoutSolver extends BaseSolver {
|
|
|
483
517
|
`FanoutSolver: assignment ${assignmentIndex} has no layer for bus "${bus.busId}"`,
|
|
484
518
|
)
|
|
485
519
|
}
|
|
486
|
-
|
|
520
|
+
// The routing order can change between assignments (notably for
|
|
521
|
+
// group-by-layer search). A layer-only key can therefore replay a
|
|
522
|
+
// prefix belonging to a different bus and duplicate or drop plans when
|
|
523
|
+
// buses contain multiple connections. Include the bus identity so the
|
|
524
|
+
// cache remains valid for grouped power/signal lanes.
|
|
525
|
+
routingPrefixKey += `${bus.busId.length}:${bus.busId};${targetLayer.length}:${targetLayer};`
|
|
487
526
|
const cachedPrefix = this.routingPrefixCache.get(routingPrefixKey)
|
|
488
527
|
if (cachedPrefix) {
|
|
489
528
|
plans = [...cachedPrefix.plans]
|
|
@@ -503,6 +542,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
503
542
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
504
543
|
clearance: this.config.clearance,
|
|
505
544
|
compactBusTracks: this.config.compactBusTracks,
|
|
545
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
506
546
|
staticClearanceCache: this.routeStaticClearanceCache,
|
|
507
547
|
blockingBusCounts: currentBusBlockingCounts,
|
|
508
548
|
})
|
|
@@ -524,6 +564,30 @@ export class FanoutSolver extends BaseSolver {
|
|
|
524
564
|
})
|
|
525
565
|
}
|
|
526
566
|
|
|
567
|
+
let validationIssues: FanoutAttemptSummary["validationIssues"]
|
|
568
|
+
let outputSrj = buildOutputSimpleRouteJson({
|
|
569
|
+
inputSrj: this.inputSrj,
|
|
570
|
+
plans,
|
|
571
|
+
layerNames: this.config.layerNames,
|
|
572
|
+
})
|
|
573
|
+
const validation =
|
|
574
|
+
plans.length === this.inputSrj.connections.length
|
|
575
|
+
? this.validateCompletePlans(plans, outputSrj)
|
|
576
|
+
: null
|
|
577
|
+
if (validation && !validation.valid) {
|
|
578
|
+
// Every route-producing strategy must pass the same final layer-aware,
|
|
579
|
+
// same-net-aware copper validation before it can be scored as complete.
|
|
580
|
+
validationIssues = validation.issues
|
|
581
|
+
plans = []
|
|
582
|
+
failedBusIds = this.preparedBuses.map((bus) => bus.busId)
|
|
583
|
+
blockingBusCounts.clear()
|
|
584
|
+
outputSrj = buildOutputSimpleRouteJson({
|
|
585
|
+
inputSrj: this.inputSrj,
|
|
586
|
+
plans,
|
|
587
|
+
layerNames: this.config.layerNames,
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
|
|
527
591
|
const routedBusCount = this.preparedBuses.length - failedBusIds.length
|
|
528
592
|
const routeLength = plans.reduce((total, plan) => total + plan.length, 0)
|
|
529
593
|
const unroutedConnectionCount =
|
|
@@ -541,6 +605,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
541
605
|
routedConnectionCount: plans.length,
|
|
542
606
|
failedBusIds,
|
|
543
607
|
score,
|
|
608
|
+
...(validationIssues ? { validationIssues } : {}),
|
|
544
609
|
}
|
|
545
610
|
|
|
546
611
|
return {
|
|
@@ -549,11 +614,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
549
614
|
blockingBusIds: [...blockingBusCounts.entries()]
|
|
550
615
|
.toSorted(([, firstCount], [, secondCount]) => secondCount - firstCount)
|
|
551
616
|
.map(([busId]) => busId),
|
|
552
|
-
outputSrj
|
|
553
|
-
inputSrj: this.inputSrj,
|
|
554
|
-
plans,
|
|
555
|
-
layerNames: this.config.layerNames,
|
|
556
|
-
}),
|
|
617
|
+
outputSrj,
|
|
557
618
|
}
|
|
558
619
|
}
|
|
559
620
|
|
|
@@ -596,8 +657,10 @@ export class FanoutSolver extends BaseSolver {
|
|
|
596
657
|
* Search layer assignments and track alternatives together. The regular
|
|
597
658
|
* assignment loop commits to one route per bus before the next bus is
|
|
598
659
|
* considered, so a locally-valid track can still starve a later bus. A
|
|
599
|
-
* bounded beam keeps several grouped-layer route prefixes alive
|
|
600
|
-
*
|
|
660
|
+
* bounded beam keeps several grouped-layer route prefixes alive. It also
|
|
661
|
+
* evaluates multi-connection buses atomically, so one promising route for a
|
|
662
|
+
* power or signal lane cannot starve a later bus before the solver explores
|
|
663
|
+
* an alternate layer/track combination.
|
|
601
664
|
*/
|
|
602
665
|
private evaluateGroupedBeam(
|
|
603
666
|
assignmentIndex: number,
|
|
@@ -605,9 +668,12 @@ export class FanoutSolver extends BaseSolver {
|
|
|
605
668
|
): EvaluatedAssignment | null {
|
|
606
669
|
if (this.config.escapeLayers.length < 2) return null
|
|
607
670
|
if (this.preparedBuses.length > 56) return null
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
671
|
+
const totalConnections = this.inputSrj.connections.length
|
|
672
|
+
// Multi-pin alternatives grow with both the bus width and the number of
|
|
673
|
+
// layer prefixes. Keep the new search bounded on the small/medium grouped
|
|
674
|
+
// problems it can improve, then let the regular assignment/repair search
|
|
675
|
+
// handle the very large benchmark samples without starving them.
|
|
676
|
+
if (totalConnections > 64) return null
|
|
611
677
|
if (new Set(this.preparedBuses.map((bus) => bus.componentId)).size !== 1) {
|
|
612
678
|
return null
|
|
613
679
|
}
|
|
@@ -635,8 +701,17 @@ export class FanoutSolver extends BaseSolver {
|
|
|
635
701
|
)
|
|
636
702
|
})
|
|
637
703
|
|
|
638
|
-
const
|
|
639
|
-
const
|
|
704
|
+
const isSmallProblem = totalConnections <= 24
|
|
705
|
+
const hasMultiConnectionBus = this.preparedBuses.some(
|
|
706
|
+
(bus) => bus.connections.length > 1,
|
|
707
|
+
)
|
|
708
|
+
// Preserve the broad track search for small singleton problems. Grouped
|
|
709
|
+
// power buses already branch across every candidate layer and make each
|
|
710
|
+
// route-alternative expansion combinatorial, so retain layer diversity but
|
|
711
|
+
// only one atomic route per layer for those buses.
|
|
712
|
+
const beamWidth = isSmallProblem ? 48 : totalConnections <= 32 ? 24 : 12
|
|
713
|
+
const alternativesPerLayer =
|
|
714
|
+
isSmallProblem && !hasMultiConnectionBus ? 4 : 1
|
|
640
715
|
let states: GroupedBeamState[] = [{ assignment: {}, plans: [] }]
|
|
641
716
|
|
|
642
717
|
const getStateScore = (state: GroupedBeamState): number => {
|
|
@@ -684,6 +759,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
684
759
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
685
760
|
clearance: this.config.clearance,
|
|
686
761
|
compactBusTracks: this.config.compactBusTracks,
|
|
762
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
687
763
|
staticClearanceCache: this.routeStaticClearanceCache,
|
|
688
764
|
},
|
|
689
765
|
alternativesPerLayer,
|
|
@@ -722,6 +798,17 @@ export class FanoutSolver extends BaseSolver {
|
|
|
722
798
|
|
|
723
799
|
const bestState = states[0]
|
|
724
800
|
if (!bestState) return null
|
|
801
|
+
const outputSrj = buildOutputSimpleRouteJson({
|
|
802
|
+
inputSrj: this.inputSrj,
|
|
803
|
+
plans: bestState.plans,
|
|
804
|
+
layerNames: this.config.layerNames,
|
|
805
|
+
})
|
|
806
|
+
if (
|
|
807
|
+
bestState.plans.length === this.inputSrj.connections.length &&
|
|
808
|
+
!this.validateCompletePlans(bestState.plans, outputSrj).valid
|
|
809
|
+
) {
|
|
810
|
+
return null
|
|
811
|
+
}
|
|
725
812
|
const score =
|
|
726
813
|
bestState.plans.length === this.inputSrj.connections.length
|
|
727
814
|
? bestState.plans.reduce((total, plan) => total + plan.length, 0) +
|
|
@@ -742,11 +829,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
742
829
|
summary,
|
|
743
830
|
plans: bestState.plans,
|
|
744
831
|
blockingBusIds: [],
|
|
745
|
-
outputSrj
|
|
746
|
-
inputSrj: this.inputSrj,
|
|
747
|
-
plans: bestState.plans,
|
|
748
|
-
layerNames: this.config.layerNames,
|
|
749
|
-
}),
|
|
832
|
+
outputSrj,
|
|
750
833
|
}
|
|
751
834
|
}
|
|
752
835
|
|
|
@@ -958,6 +1041,15 @@ export class FanoutSolver extends BaseSolver {
|
|
|
958
1041
|
"FanoutSolver: getOutput() called before a complete fanout was solved",
|
|
959
1042
|
)
|
|
960
1043
|
}
|
|
1044
|
+
const validation = this.validateCompletePlans(
|
|
1045
|
+
this.bestAttempt.plans,
|
|
1046
|
+
this.bestAttempt.outputSrj,
|
|
1047
|
+
)
|
|
1048
|
+
if (!validation.valid) {
|
|
1049
|
+
throw new Error(
|
|
1050
|
+
`FanoutSolver: completed output failed validation: ${validation.issues[0]?.message ?? "unknown validation error"}`,
|
|
1051
|
+
)
|
|
1052
|
+
}
|
|
961
1053
|
return {
|
|
962
1054
|
simpleRouteJson: this.bestAttempt.outputSrj,
|
|
963
1055
|
fanoutTraces: this.bestAttempt.plans.map((plan) => plan.trace),
|
|
@@ -978,6 +1070,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
978
1070
|
this.preparedBuses.map((bus) => [bus.busId, bus.direction]),
|
|
979
1071
|
),
|
|
980
1072
|
attempts: [...this.attempts],
|
|
1073
|
+
validation,
|
|
981
1074
|
}
|
|
982
1075
|
}
|
|
983
1076
|
|
package/lib/geometry.ts
CHANGED
|
@@ -3,12 +3,27 @@ import type { Point2D, RoutedSegment } from "./types"
|
|
|
3
3
|
|
|
4
4
|
const EPSILON = 1e-9
|
|
5
5
|
|
|
6
|
-
type ShapeAwareObstacle = Obstacle & {
|
|
6
|
+
type ShapeAwareObstacle = Obstacle & {
|
|
7
|
+
shape?: "circle"
|
|
8
|
+
ccwRotationDegrees?: number
|
|
9
|
+
}
|
|
7
10
|
|
|
8
11
|
function obstacleIsCircular(obstacle: Obstacle): boolean {
|
|
9
12
|
return (obstacle as ShapeAwareObstacle).shape === "circle"
|
|
10
13
|
}
|
|
11
14
|
|
|
15
|
+
function toObstacleLocalPoint(point: Point2D, obstacle: Obstacle): Point2D {
|
|
16
|
+
const rotationRadians =
|
|
17
|
+
(-((obstacle as ShapeAwareObstacle).ccwRotationDegrees ?? 0) * Math.PI) /
|
|
18
|
+
180
|
|
19
|
+
const dx = point.x - obstacle.center.x
|
|
20
|
+
const dy = point.y - obstacle.center.y
|
|
21
|
+
return {
|
|
22
|
+
x: dx * Math.cos(rotationRadians) - dy * Math.sin(rotationRadians),
|
|
23
|
+
y: dx * Math.sin(rotationRadians) + dy * Math.cos(rotationRadians),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
export function distance(a: Point2D, b: Point2D): number {
|
|
13
28
|
return Math.hypot(a.x - b.x, a.y - b.y)
|
|
14
29
|
}
|
|
@@ -76,9 +91,10 @@ export function pointIsInsideObstacle(
|
|
|
76
91
|
if (obstacleIsCircular(obstacle)) {
|
|
77
92
|
return distance(point, obstacle.center) <= obstacle.width / 2 + tolerance
|
|
78
93
|
}
|
|
94
|
+
const localPoint = toObstacleLocalPoint(point, obstacle)
|
|
79
95
|
return (
|
|
80
|
-
Math.abs(
|
|
81
|
-
Math.abs(
|
|
96
|
+
Math.abs(localPoint.x) <= obstacle.width / 2 + tolerance &&
|
|
97
|
+
Math.abs(localPoint.y) <= obstacle.height / 2 + tolerance
|
|
82
98
|
)
|
|
83
99
|
}
|
|
84
100
|
|
|
@@ -89,14 +105,9 @@ export function distancePointToObstacle(
|
|
|
89
105
|
if (obstacleIsCircular(obstacle)) {
|
|
90
106
|
return Math.max(0, distance(point, obstacle.center) - obstacle.width / 2)
|
|
91
107
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
const dy = Math.max(
|
|
97
|
-
Math.abs(point.y - obstacle.center.y) - obstacle.height / 2,
|
|
98
|
-
0,
|
|
99
|
-
)
|
|
108
|
+
const localPoint = toObstacleLocalPoint(point, obstacle)
|
|
109
|
+
const dx = Math.max(Math.abs(localPoint.x) - obstacle.width / 2, 0)
|
|
110
|
+
const dy = Math.max(Math.abs(localPoint.y) - obstacle.height / 2, 0)
|
|
100
111
|
return Math.hypot(dx, dy)
|
|
101
112
|
}
|
|
102
113
|
|
|
@@ -111,16 +122,24 @@ export function distanceSegmentToObstacle(
|
|
|
111
122
|
obstacle.width / 2,
|
|
112
123
|
)
|
|
113
124
|
}
|
|
125
|
+
const localStart = toObstacleLocalPoint(segment.start, obstacle)
|
|
126
|
+
const localEnd = toObstacleLocalPoint(segment.end, obstacle)
|
|
127
|
+
if (
|
|
128
|
+
Math.abs(localStart.x) <= obstacle.width / 2 + EPSILON &&
|
|
129
|
+
Math.abs(localStart.y) <= obstacle.height / 2 + EPSILON
|
|
130
|
+
) {
|
|
131
|
+
return 0
|
|
132
|
+
}
|
|
114
133
|
if (
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
Math.abs(localEnd.x) <= obstacle.width / 2 + EPSILON &&
|
|
135
|
+
Math.abs(localEnd.y) <= obstacle.height / 2 + EPSILON
|
|
117
136
|
) {
|
|
118
137
|
return 0
|
|
119
138
|
}
|
|
120
|
-
const minX =
|
|
121
|
-
const maxX = obstacle.
|
|
122
|
-
const minY =
|
|
123
|
-
const maxY = obstacle.
|
|
139
|
+
const minX = -obstacle.width / 2
|
|
140
|
+
const maxX = obstacle.width / 2
|
|
141
|
+
const minY = -obstacle.height / 2
|
|
142
|
+
const maxY = obstacle.height / 2
|
|
124
143
|
const corners = [
|
|
125
144
|
{ x: minX, y: minY },
|
|
126
145
|
{ x: maxX, y: minY },
|
|
@@ -133,8 +152,8 @@ export function distanceSegmentToObstacle(
|
|
|
133
152
|
minimumDistance = Math.min(
|
|
134
153
|
minimumDistance,
|
|
135
154
|
distanceSegmentToSegment(
|
|
136
|
-
|
|
137
|
-
|
|
155
|
+
localStart,
|
|
156
|
+
localEnd,
|
|
138
157
|
corners[index]!,
|
|
139
158
|
corners[(index + 1) % corners.length]!,
|
|
140
159
|
),
|
package/lib/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { FanoutSolver } from "./fanout-solver"
|
|
2
2
|
export { getCopperLayerColor } from "./layer-colors"
|
|
3
3
|
export { getCopperLayerNames } from "./layer-names"
|
|
4
|
+
export { validateFanoutSolution } from "./validate-fanout-solution"
|
|
4
5
|
export type {
|
|
5
6
|
Bounds,
|
|
6
7
|
FanoutAttemptSummary,
|
|
@@ -15,6 +16,10 @@ export type {
|
|
|
15
16
|
FanoutDirection,
|
|
16
17
|
FanoutEdge,
|
|
17
18
|
FanoutPlaneTermination,
|
|
19
|
+
FanoutRoutePlan,
|
|
18
20
|
FanoutSolverOptions,
|
|
19
21
|
FanoutSolverOutput,
|
|
22
|
+
FanoutValidationIssue,
|
|
23
|
+
FanoutValidationReport,
|
|
24
|
+
PreparedBus,
|
|
20
25
|
} from "./types"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Obstacle,
|
|
3
|
+
SimpleRouteConnection,
|
|
4
|
+
SimpleRouteJson,
|
|
5
|
+
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
|
|
7
|
+
interface ElectricalNetIdentity {
|
|
8
|
+
connectionNetKeys: Map<string, string>
|
|
9
|
+
tokenNetKeys: Map<string, Set<string>>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const identityCache = new WeakMap<SimpleRouteJson, ElectricalNetIdentity>()
|
|
13
|
+
|
|
14
|
+
export function getConnectionNetKey(connection: SimpleRouteConnection): string {
|
|
15
|
+
return (
|
|
16
|
+
connection.netConnectionName ??
|
|
17
|
+
connection.rootConnectionName ??
|
|
18
|
+
connection.name
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function addTokenNet(
|
|
23
|
+
tokenNetKeys: Map<string, Set<string>>,
|
|
24
|
+
token: string | undefined,
|
|
25
|
+
netKey: string,
|
|
26
|
+
): boolean {
|
|
27
|
+
if (!token) return false
|
|
28
|
+
const keys = tokenNetKeys.get(token) ?? new Set<string>()
|
|
29
|
+
const sizeBefore = keys.size
|
|
30
|
+
keys.add(netKey)
|
|
31
|
+
tokenNetKeys.set(token, keys)
|
|
32
|
+
return keys.size !== sizeBefore
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getKnownNetKeys(
|
|
36
|
+
tokenNetKeys: Map<string, Set<string>>,
|
|
37
|
+
tokens: readonly string[],
|
|
38
|
+
): Set<string> {
|
|
39
|
+
const keys = new Set<string>()
|
|
40
|
+
for (const token of tokens) {
|
|
41
|
+
for (const key of tokenNetKeys.get(token) ?? []) keys.add(key)
|
|
42
|
+
}
|
|
43
|
+
return keys
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createElectricalNetIdentity(
|
|
47
|
+
srj: SimpleRouteJson,
|
|
48
|
+
): ElectricalNetIdentity {
|
|
49
|
+
const connectionNetKeys = new Map<string, string>()
|
|
50
|
+
const tokenNetKeys = new Map<string, Set<string>>()
|
|
51
|
+
|
|
52
|
+
for (const connection of srj.connections) {
|
|
53
|
+
const netKey = getConnectionNetKey(connection)
|
|
54
|
+
connectionNetKeys.set(connection.name, netKey)
|
|
55
|
+
addTokenNet(tokenNetKeys, connection.name, netKey)
|
|
56
|
+
addTokenNet(tokenNetKeys, connection.rootConnectionName, netKey)
|
|
57
|
+
addTokenNet(tokenNetKeys, connection.netConnectionName, netKey)
|
|
58
|
+
for (const point of connection.pointsToConnect) {
|
|
59
|
+
addTokenNet(tokenNetKeys, point.pointId, netKey)
|
|
60
|
+
addTokenNet(tokenNetKeys, point.pcb_port_id, netKey)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const trace of srj.traces ?? []) {
|
|
65
|
+
const netKey = trace.connection_name
|
|
66
|
+
? connectionNetKeys.get(trace.connection_name)
|
|
67
|
+
: undefined
|
|
68
|
+
if (!netKey) continue
|
|
69
|
+
addTokenNet(tokenNetKeys, trace.pcb_trace_id, netKey)
|
|
70
|
+
for (const token of trace.connectsTo ?? []) {
|
|
71
|
+
addTokenNet(tokenNetKeys, token, netKey)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Obstacle metadata often contains both a connection id and a lower-level
|
|
76
|
+
// connectivity id. Propagate the known net across that metadata so another
|
|
77
|
+
// pad that only names the connectivity id is still recognized as same-net.
|
|
78
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
79
|
+
let changed = false
|
|
80
|
+
for (const obstacle of srj.obstacles) {
|
|
81
|
+
const netKeys = getKnownNetKeys(tokenNetKeys, obstacle.connectedTo)
|
|
82
|
+
if (netKeys.size !== 1) continue
|
|
83
|
+
const netKey = [...netKeys][0]!
|
|
84
|
+
for (const token of obstacle.connectedTo) {
|
|
85
|
+
changed = addTokenNet(tokenNetKeys, token, netKey) || changed
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!changed) break
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const parentByNetKey = new Map<string, string>()
|
|
92
|
+
const findRoot = (netKey: string): string => {
|
|
93
|
+
const parent = parentByNetKey.get(netKey) ?? netKey
|
|
94
|
+
parentByNetKey.set(netKey, parent)
|
|
95
|
+
if (parent === netKey) return netKey
|
|
96
|
+
const root = findRoot(parent)
|
|
97
|
+
parentByNetKey.set(netKey, root)
|
|
98
|
+
return root
|
|
99
|
+
}
|
|
100
|
+
const union = (first: string, second: string): void => {
|
|
101
|
+
const firstRoot = findRoot(first)
|
|
102
|
+
const secondRoot = findRoot(second)
|
|
103
|
+
if (firstRoot !== secondRoot) parentByNetKey.set(secondRoot, firstRoot)
|
|
104
|
+
}
|
|
105
|
+
for (const netKeys of tokenNetKeys.values()) {
|
|
106
|
+
const [firstNetKey, ...otherNetKeys] = [...netKeys]
|
|
107
|
+
if (!firstNetKey) continue
|
|
108
|
+
for (const otherNetKey of otherNetKeys) union(firstNetKey, otherNetKey)
|
|
109
|
+
}
|
|
110
|
+
for (const connectedTokens of [
|
|
111
|
+
...srj.obstacles.map((obstacle) => obstacle.connectedTo),
|
|
112
|
+
...(srj.traces ?? []).map((trace) => trace.connectsTo ?? []),
|
|
113
|
+
]) {
|
|
114
|
+
const [firstNetKey, ...otherNetKeys] = [
|
|
115
|
+
...getKnownNetKeys(tokenNetKeys, connectedTokens),
|
|
116
|
+
]
|
|
117
|
+
if (!firstNetKey) continue
|
|
118
|
+
for (const otherNetKey of otherNetKeys) union(firstNetKey, otherNetKey)
|
|
119
|
+
}
|
|
120
|
+
for (const [connectionName, netKey] of connectionNetKeys) {
|
|
121
|
+
connectionNetKeys.set(connectionName, findRoot(netKey))
|
|
122
|
+
}
|
|
123
|
+
for (const [token, netKeys] of tokenNetKeys) {
|
|
124
|
+
tokenNetKeys.set(
|
|
125
|
+
token,
|
|
126
|
+
new Set([...netKeys].map((netKey) => findRoot(netKey))),
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { connectionNetKeys, tokenNetKeys }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getElectricalNetIdentity(srj: SimpleRouteJson): ElectricalNetIdentity {
|
|
134
|
+
const cached = identityCache.get(srj)
|
|
135
|
+
if (cached) return cached
|
|
136
|
+
const identity = createElectricalNetIdentity(srj)
|
|
137
|
+
identityCache.set(srj, identity)
|
|
138
|
+
return identity
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function connectionsShareElectricalNet(
|
|
142
|
+
srj: SimpleRouteJson,
|
|
143
|
+
firstConnectionName: string,
|
|
144
|
+
secondConnectionName: string,
|
|
145
|
+
): boolean {
|
|
146
|
+
const identity = getElectricalNetIdentity(srj)
|
|
147
|
+
const firstNet = identity.connectionNetKeys.get(firstConnectionName)
|
|
148
|
+
const secondNet = identity.connectionNetKeys.get(secondConnectionName)
|
|
149
|
+
return firstNet !== undefined && firstNet === secondNet
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function obstacleSharesElectricalNet(
|
|
153
|
+
srj: SimpleRouteJson,
|
|
154
|
+
obstacle: Obstacle,
|
|
155
|
+
connectionName: string,
|
|
156
|
+
): boolean {
|
|
157
|
+
const identity = getElectricalNetIdentity(srj)
|
|
158
|
+
const connectionNet = identity.connectionNetKeys.get(connectionName)
|
|
159
|
+
if (!connectionNet) return false
|
|
160
|
+
return obstacle.connectedTo.some((token) =>
|
|
161
|
+
identity.tokenNetKeys.get(token)?.has(connectionNet),
|
|
162
|
+
)
|
|
163
|
+
}
|