@tscircuit/fanout-solver 0.0.21 → 0.0.22
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 +15 -0
- package/lib/build-output.ts +30 -12
- package/lib/complete-original-endpoints.ts +1208 -0
- package/lib/fanout-solver.ts +150 -20
- package/lib/index.ts +4 -0
- package/lib/route-bus.ts +339 -87
- package/lib/route-single-layer-adaptive-exits.ts +1 -0
- package/lib/route-single-layer-push-shove.ts +1 -0
- package/lib/types.ts +50 -0
- package/lib/validate-fanout-solution.ts +152 -79
- package/lib/validate-original-endpoint-connectivity.ts +42 -1
- package/lib/validate-routed-copper-drc.ts +23 -2
- package/package.json +2 -2
package/lib/fanout-solver.ts
CHANGED
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
import { BaseSolver } from "@tscircuit/solver-utils"
|
|
6
6
|
import type { GraphicsObject } from "graphics-debug"
|
|
7
7
|
import { buildOutputSimpleRouteJson } from "./build-output"
|
|
8
|
+
import {
|
|
9
|
+
completeOriginalEndpoints,
|
|
10
|
+
type CompleteOriginalEndpointsResult,
|
|
11
|
+
} from "./complete-original-endpoints"
|
|
8
12
|
import { getCopperLayerColor } from "./layer-colors"
|
|
9
13
|
import { generateLayerAssignments, getCopperLayerNames } from "./layer-names"
|
|
10
14
|
import {
|
|
@@ -36,6 +40,7 @@ interface ResolvedFanoutConfig {
|
|
|
36
40
|
viaHoleDiameter: number
|
|
37
41
|
clearance: number
|
|
38
42
|
compactBusTracks: boolean
|
|
43
|
+
preferOriginalEndpointTracks: boolean
|
|
39
44
|
allowSameNetMerges: boolean
|
|
40
45
|
singleLayerPushAndShove: boolean
|
|
41
46
|
singleLayerAdaptiveExits: boolean
|
|
@@ -43,6 +48,7 @@ interface ResolvedFanoutConfig {
|
|
|
43
48
|
layerNames: string[]
|
|
44
49
|
escapeLayers: string[]
|
|
45
50
|
maxLayerCombinations: number
|
|
51
|
+
balanceLayerLoadByConnectionCount: boolean
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
interface EvaluatedAssignment extends AssignmentAttempt {
|
|
@@ -126,6 +132,7 @@ function resolveConfig(
|
|
|
126
132
|
viaHoleDiameter,
|
|
127
133
|
clearance,
|
|
128
134
|
compactBusTracks: options.compactBusTracks ?? false,
|
|
135
|
+
preferOriginalEndpointTracks: options.preferOriginalEndpointTracks ?? false,
|
|
129
136
|
allowSameNetMerges: options.allowSameNetMerges ?? false,
|
|
130
137
|
singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
|
|
131
138
|
singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
|
|
@@ -139,15 +146,28 @@ function resolveConfig(
|
|
|
139
146
|
"maxLayerCombinations",
|
|
140
147
|
options.maxLayerCombinations,
|
|
141
148
|
),
|
|
149
|
+
balanceLayerLoadByConnectionCount:
|
|
150
|
+
options.balanceLayerLoadByConnectionCount ?? false,
|
|
142
151
|
}
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
function assignmentLoadPenalty(
|
|
146
155
|
assignment: Readonly<Record<string, string>>,
|
|
156
|
+
buses: readonly PreparedBus[],
|
|
157
|
+
weightByConnectionCount: boolean,
|
|
147
158
|
): number {
|
|
159
|
+
const connectionCountByBusId = new Map(
|
|
160
|
+
buses.map((bus) => [bus.busId, bus.connections.length]),
|
|
161
|
+
)
|
|
148
162
|
const loadByLayer = new Map<string, number>()
|
|
149
|
-
for (const layer of Object.
|
|
150
|
-
loadByLayer.set(
|
|
163
|
+
for (const [busId, layer] of Object.entries(assignment)) {
|
|
164
|
+
loadByLayer.set(
|
|
165
|
+
layer,
|
|
166
|
+
(loadByLayer.get(layer) ?? 0) +
|
|
167
|
+
(weightByConnectionCount
|
|
168
|
+
? (connectionCountByBusId.get(busId) ?? 1)
|
|
169
|
+
: 1),
|
|
170
|
+
)
|
|
151
171
|
}
|
|
152
172
|
return [...loadByLayer.values()].reduce(
|
|
153
173
|
(penalty, load) => penalty + load * load,
|
|
@@ -155,6 +175,20 @@ function assignmentLoadPenalty(
|
|
|
155
175
|
)
|
|
156
176
|
}
|
|
157
177
|
|
|
178
|
+
function getLayerLoadPenaltyWeight(config: ResolvedFanoutConfig): number {
|
|
179
|
+
return config.balanceLayerLoadByConnectionCount ? 0.25 : 0.01
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getPlanViaCount(plans: readonly FanoutRoutePlan[]): number {
|
|
183
|
+
return plans.reduce(
|
|
184
|
+
(count, plan) =>
|
|
185
|
+
count +
|
|
186
|
+
Number(Boolean(plan.via)) +
|
|
187
|
+
Number(Boolean(plan.planeEndpointVia)),
|
|
188
|
+
0,
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
158
192
|
function getBusDistanceToBoundary(bus: PreparedBus): number {
|
|
159
193
|
const averageSource =
|
|
160
194
|
bus.connections.reduce((sum, connection) => {
|
|
@@ -222,8 +256,14 @@ function createPreferredLayerAssignment(params: {
|
|
|
222
256
|
buses: PreparedBus[]
|
|
223
257
|
escapeLayers: string[]
|
|
224
258
|
escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
|
|
259
|
+
preferOriginalEndpointTracks: boolean
|
|
225
260
|
}): Readonly<Record<string, string>> {
|
|
226
|
-
const {
|
|
261
|
+
const {
|
|
262
|
+
buses,
|
|
263
|
+
escapeLayers,
|
|
264
|
+
escapeLayersByBusId,
|
|
265
|
+
preferOriginalEndpointTracks,
|
|
266
|
+
} = params
|
|
227
267
|
const assignment: Record<string, string> = {}
|
|
228
268
|
const directionsByComponent = new Map<string, Set<PreparedBus["direction"]>>()
|
|
229
269
|
let nextViaLayerIndex = 0
|
|
@@ -248,7 +288,7 @@ function createPreferredLayerAssignment(params: {
|
|
|
248
288
|
)
|
|
249
289
|
if (
|
|
250
290
|
routableEscapeLayers.includes(sourceLayer) &&
|
|
251
|
-
busIsOnOutwardComponentEdge(bus)
|
|
291
|
+
(preferOriginalEndpointTracks || busIsOnOutwardComponentEdge(bus))
|
|
252
292
|
) {
|
|
253
293
|
assignment[bus.busId] = sourceLayer
|
|
254
294
|
} else if (viaLayers.length > 0) {
|
|
@@ -307,6 +347,7 @@ function getCandidateEscapeLayersForBus(params: {
|
|
|
307
347
|
viaHoleDiameter: config.viaHoleDiameter,
|
|
308
348
|
clearance: config.clearance,
|
|
309
349
|
compactBusTracks: config.compactBusTracks,
|
|
350
|
+
preferOriginalEndpointTracks: config.preferOriginalEndpointTracks,
|
|
310
351
|
allowSameNetMerges: config.allowSameNetMerges,
|
|
311
352
|
staticClearanceCache,
|
|
312
353
|
}) !== null,
|
|
@@ -349,6 +390,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
349
390
|
private nextAssignmentIndex = 0
|
|
350
391
|
private nextGeneratedAssignmentIndex = 0
|
|
351
392
|
private bestAttempt: AssignmentAttempt | null = null
|
|
393
|
+
private endpointCompletion: CompleteOriginalEndpointsResult | null = null
|
|
352
394
|
|
|
353
395
|
constructor(
|
|
354
396
|
public readonly inputSrj: SimpleRouteJson,
|
|
@@ -416,6 +458,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
416
458
|
buses: this.preparedBuses,
|
|
417
459
|
escapeLayers: this.config.escapeLayers,
|
|
418
460
|
escapeLayersByBusId,
|
|
461
|
+
preferOriginalEndpointTracks: this.config.preferOriginalEndpointTracks,
|
|
419
462
|
}),
|
|
420
463
|
generatedAssignments,
|
|
421
464
|
maxAssignments: this.config.maxLayerCombinations,
|
|
@@ -427,6 +470,26 @@ export class FanoutSolver extends BaseSolver {
|
|
|
427
470
|
return "FanoutSolver"
|
|
428
471
|
}
|
|
429
472
|
|
|
473
|
+
private completeBestAttemptEndpoints(): void {
|
|
474
|
+
if (
|
|
475
|
+
!this.options.completeOriginalEndpoints ||
|
|
476
|
+
this.endpointCompletion ||
|
|
477
|
+
!this.bestAttempt
|
|
478
|
+
) {
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
this.endpointCompletion = completeOriginalEndpoints({
|
|
482
|
+
inputSrj: this.inputSrj,
|
|
483
|
+
fanoutSrj: this.bestAttempt.outputSrj,
|
|
484
|
+
plans: this.bestAttempt.plans,
|
|
485
|
+
traceWidth: this.config.traceWidth,
|
|
486
|
+
viaDiameter: this.config.viaDiameter,
|
|
487
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
488
|
+
clearance: this.config.clearance,
|
|
489
|
+
effort: this.options.endpointCompletionEffort,
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
430
493
|
private getValidationBoundary(): Bounds {
|
|
431
494
|
if (this.options.sharedBoundary) return this.options.sharedBoundary
|
|
432
495
|
const firstBoundary = this.preparedBuses[0]?.sharedBoundary
|
|
@@ -491,8 +554,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
491
554
|
}
|
|
492
555
|
const busesInRoutingOrder = [...this.preparedBuses].sort(
|
|
493
556
|
(a, b) =>
|
|
494
|
-
Number(
|
|
495
|
-
Number(
|
|
557
|
+
Number(b.termination.type === "plane") -
|
|
558
|
+
Number(a.termination.type === "plane") ||
|
|
496
559
|
(routingStrategy === "group-by-layer"
|
|
497
560
|
? (busLayerAssignments[a.busId] ?? "").localeCompare(
|
|
498
561
|
busLayerAssignments[b.busId] ?? "",
|
|
@@ -542,6 +605,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
542
605
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
543
606
|
clearance: this.config.clearance,
|
|
544
607
|
compactBusTracks: this.config.compactBusTracks,
|
|
608
|
+
preferOriginalEndpointTracks: this.config.preferOriginalEndpointTracks,
|
|
545
609
|
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
546
610
|
staticClearanceCache: this.routeStaticClearanceCache,
|
|
547
611
|
blockingBusCounts: currentBusBlockingCounts,
|
|
@@ -596,8 +660,13 @@ export class FanoutSolver extends BaseSolver {
|
|
|
596
660
|
unroutedConnectionCount * 1_000_000 +
|
|
597
661
|
failedBusIds.length * 100_000 +
|
|
598
662
|
routeLength +
|
|
599
|
-
plans
|
|
600
|
-
assignmentLoadPenalty(
|
|
663
|
+
getPlanViaCount(plans) * 0.1 +
|
|
664
|
+
assignmentLoadPenalty(
|
|
665
|
+
busLayerAssignments,
|
|
666
|
+
this.preparedBuses,
|
|
667
|
+
this.config.balanceLayerLoadByConnectionCount,
|
|
668
|
+
) *
|
|
669
|
+
getLayerLoadPenaltyWeight(this.config)
|
|
601
670
|
const summary: FanoutAttemptSummary = {
|
|
602
671
|
assignmentIndex,
|
|
603
672
|
busLayerAssignments,
|
|
@@ -690,8 +759,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
690
759
|
: (this.escapeLayersByBusId[b.busId]?.length ??
|
|
691
760
|
this.config.escapeLayers.length)
|
|
692
761
|
return (
|
|
693
|
-
Number(
|
|
694
|
-
Number(
|
|
762
|
+
Number(b.termination.type === "plane") -
|
|
763
|
+
Number(a.termination.type === "plane") ||
|
|
695
764
|
(groupByDirection ? a.direction.localeCompare(b.direction) : 0) ||
|
|
696
765
|
aLayerCount - bLayerCount ||
|
|
697
766
|
b.componentObstacles.length - a.componentObstacles.length ||
|
|
@@ -719,11 +788,29 @@ export class FanoutSolver extends BaseSolver {
|
|
|
719
788
|
(total, plan) => total + plan.length,
|
|
720
789
|
0,
|
|
721
790
|
)
|
|
722
|
-
const viaCount = state.plans
|
|
791
|
+
const viaCount = getPlanViaCount(state.plans)
|
|
792
|
+
const offEndpointLayerConnectionCount = this.preparedBuses.reduce(
|
|
793
|
+
(count, bus) => {
|
|
794
|
+
if (bus.termination.type !== "boundary") return count
|
|
795
|
+
const sourceLayer = bus.connections[0]?.sourceLayer
|
|
796
|
+
return state.assignment[bus.busId] === sourceLayer
|
|
797
|
+
? count
|
|
798
|
+
: count + bus.connections.length
|
|
799
|
+
},
|
|
800
|
+
0,
|
|
801
|
+
)
|
|
723
802
|
return (
|
|
724
803
|
routeLength +
|
|
725
804
|
viaCount * 0.1 +
|
|
726
|
-
|
|
805
|
+
(this.config.preferOriginalEndpointTracks
|
|
806
|
+
? offEndpointLayerConnectionCount * 10_000
|
|
807
|
+
: 0) +
|
|
808
|
+
assignmentLoadPenalty(
|
|
809
|
+
state.assignment,
|
|
810
|
+
this.preparedBuses,
|
|
811
|
+
this.config.balanceLayerLoadByConnectionCount,
|
|
812
|
+
) *
|
|
813
|
+
getLayerLoadPenaltyWeight(this.config)
|
|
727
814
|
)
|
|
728
815
|
}
|
|
729
816
|
|
|
@@ -735,14 +822,26 @@ export class FanoutSolver extends BaseSolver {
|
|
|
735
822
|
? [bus.termination.layer]
|
|
736
823
|
: (this.escapeLayersByBusId[bus.busId] ?? this.config.escapeLayers)
|
|
737
824
|
const layerLoads = new Map<string, number>()
|
|
738
|
-
for (const layer of Object.
|
|
739
|
-
|
|
825
|
+
for (const [assignedBusId, layer] of Object.entries(state.assignment)) {
|
|
826
|
+
const assignedBus = this.preparedBuses.find(
|
|
827
|
+
(candidate) => candidate.busId === assignedBusId,
|
|
828
|
+
)
|
|
829
|
+
layerLoads.set(
|
|
830
|
+
layer,
|
|
831
|
+
(layerLoads.get(layer) ?? 0) +
|
|
832
|
+
(this.config.balanceLayerLoadByConnectionCount
|
|
833
|
+
? (assignedBus?.connections.length ?? 1)
|
|
834
|
+
: 1),
|
|
835
|
+
)
|
|
740
836
|
}
|
|
741
837
|
const sourceLayer = bus.connections[0]?.sourceLayer
|
|
742
838
|
const orderedLayers = candidateLayers.toSorted(
|
|
743
839
|
(first, second) =>
|
|
744
840
|
(layerLoads.get(first) ?? 0) - (layerLoads.get(second) ?? 0) ||
|
|
745
|
-
|
|
841
|
+
(this.config.preferOriginalEndpointTracks
|
|
842
|
+
? Number(second === sourceLayer) - Number(first === sourceLayer)
|
|
843
|
+
: Number(first === sourceLayer) -
|
|
844
|
+
Number(second === sourceLayer)) ||
|
|
746
845
|
first.localeCompare(second),
|
|
747
846
|
)
|
|
748
847
|
|
|
@@ -759,6 +858,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
759
858
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
760
859
|
clearance: this.config.clearance,
|
|
761
860
|
compactBusTracks: this.config.compactBusTracks,
|
|
861
|
+
preferOriginalEndpointTracks:
|
|
862
|
+
this.config.preferOriginalEndpointTracks,
|
|
762
863
|
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
763
864
|
staticClearanceCache: this.routeStaticClearanceCache,
|
|
764
865
|
},
|
|
@@ -812,8 +913,13 @@ export class FanoutSolver extends BaseSolver {
|
|
|
812
913
|
const score =
|
|
813
914
|
bestState.plans.length === this.inputSrj.connections.length
|
|
814
915
|
? bestState.plans.reduce((total, plan) => total + plan.length, 0) +
|
|
815
|
-
bestState.plans
|
|
816
|
-
assignmentLoadPenalty(
|
|
916
|
+
getPlanViaCount(bestState.plans) * 0.1 +
|
|
917
|
+
assignmentLoadPenalty(
|
|
918
|
+
bestState.assignment,
|
|
919
|
+
this.preparedBuses,
|
|
920
|
+
this.config.balanceLayerLoadByConnectionCount,
|
|
921
|
+
) *
|
|
922
|
+
getLayerLoadPenaltyWeight(this.config)
|
|
817
923
|
: Number.POSITIVE_INFINITY
|
|
818
924
|
if (!Number.isFinite(score)) return null
|
|
819
925
|
|
|
@@ -937,6 +1043,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
937
1043
|
failedBuses: "none",
|
|
938
1044
|
bestScore: beamAttempt.summary.score,
|
|
939
1045
|
}
|
|
1046
|
+
this.completeBestAttemptEndpoints()
|
|
940
1047
|
this.solved = true
|
|
941
1048
|
return
|
|
942
1049
|
}
|
|
@@ -977,6 +1084,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
977
1084
|
this.bestAttempt.summary.routedConnectionCount ===
|
|
978
1085
|
this.inputSrj.connections.length
|
|
979
1086
|
) {
|
|
1087
|
+
this.completeBestAttemptEndpoints()
|
|
980
1088
|
this.solved = true
|
|
981
1089
|
} else {
|
|
982
1090
|
this.failed = true
|
|
@@ -1022,6 +1130,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1022
1130
|
if (
|
|
1023
1131
|
attempt.summary.routedConnectionCount === this.inputSrj.connections.length
|
|
1024
1132
|
) {
|
|
1133
|
+
this.completeBestAttemptEndpoints()
|
|
1025
1134
|
this.solved = true
|
|
1026
1135
|
}
|
|
1027
1136
|
}
|
|
@@ -1050,9 +1159,27 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1050
1159
|
`FanoutSolver: completed output failed validation: ${validation.issues[0]?.message ?? "unknown validation error"}`,
|
|
1051
1160
|
)
|
|
1052
1161
|
}
|
|
1162
|
+
const finalSrj =
|
|
1163
|
+
this.endpointCompletion?.simpleRouteJson ?? this.bestAttempt.outputSrj
|
|
1164
|
+
const finalTraceById = new Map(
|
|
1165
|
+
(finalSrj.traces ?? []).map((trace) => [trace.pcb_trace_id, trace]),
|
|
1166
|
+
)
|
|
1053
1167
|
return {
|
|
1054
|
-
simpleRouteJson:
|
|
1055
|
-
|
|
1168
|
+
simpleRouteJson:
|
|
1169
|
+
this.endpointCompletion?.simpleRouteJson ?? this.bestAttempt.outputSrj,
|
|
1170
|
+
fanoutTraces: this.bestAttempt.plans.flatMap((plan) => [
|
|
1171
|
+
finalTraceById.get(plan.trace.pcb_trace_id) ?? plan.trace,
|
|
1172
|
+
...(plan.planeEndpointTrace
|
|
1173
|
+
? [
|
|
1174
|
+
finalTraceById.get(plan.planeEndpointTrace.pcb_trace_id) ??
|
|
1175
|
+
plan.planeEndpointTrace,
|
|
1176
|
+
]
|
|
1177
|
+
: []),
|
|
1178
|
+
]),
|
|
1179
|
+
completionTraces: this.endpointCompletion?.traces ?? [],
|
|
1180
|
+
...(this.endpointCompletion
|
|
1181
|
+
? { endpointCompletion: this.endpointCompletion.report }
|
|
1182
|
+
: {}),
|
|
1056
1183
|
planeTerminations: this.bestAttempt.plans.flatMap((plan) =>
|
|
1057
1184
|
plan.termination.type === "plane" && plan.via
|
|
1058
1185
|
? [
|
|
@@ -1079,7 +1206,10 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1079
1206
|
}
|
|
1080
1207
|
|
|
1081
1208
|
override visualize(): GraphicsObject {
|
|
1082
|
-
const visualizedSrj =
|
|
1209
|
+
const visualizedSrj =
|
|
1210
|
+
this.endpointCompletion?.simpleRouteJson ??
|
|
1211
|
+
this.bestAttempt?.outputSrj ??
|
|
1212
|
+
this.inputSrj
|
|
1083
1213
|
const graphics = convertSrjToGraphicsObject(visualizedSrj)
|
|
1084
1214
|
const circularPadKeys = new Set(
|
|
1085
1215
|
visualizedSrj.obstacles
|
package/lib/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { FanoutSolver } from "./fanout-solver"
|
|
2
|
+
export { completeOriginalEndpoints } from "./complete-original-endpoints"
|
|
2
3
|
export { getCopperLayerColor } from "./layer-colors"
|
|
3
4
|
export { getCopperLayerNames } from "./layer-names"
|
|
4
5
|
export { validateOriginalEndpointConnectivity } from "./validate-original-endpoint-connectivity"
|
|
@@ -26,6 +27,8 @@ export type {
|
|
|
26
27
|
FanoutCorner,
|
|
27
28
|
FanoutDirection,
|
|
28
29
|
FanoutEdge,
|
|
30
|
+
FanoutEndpointCompletionReport,
|
|
31
|
+
FanoutPlaneConnectivity,
|
|
29
32
|
FanoutPlaneTermination,
|
|
30
33
|
FanoutRoutePlan,
|
|
31
34
|
FanoutSolverOptions,
|
|
@@ -33,4 +36,5 @@ export type {
|
|
|
33
36
|
FanoutValidationIssue,
|
|
34
37
|
FanoutValidationReport,
|
|
35
38
|
PreparedBus,
|
|
39
|
+
SimpleRouteJsonWithFanoutPlanes,
|
|
36
40
|
} from "./types"
|