@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.
@@ -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.values(assignment)) {
150
- loadByLayer.set(layer, (loadByLayer.get(layer) ?? 0) + 1)
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 { buses, escapeLayers, escapeLayersByBusId } = params
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(a.termination.type === "plane") -
495
- Number(b.termination.type === "plane") ||
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.filter((plan) => plan.via).length * 0.1 +
600
- assignmentLoadPenalty(busLayerAssignments) * 0.01
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(a.termination.type === "plane") -
694
- Number(b.termination.type === "plane") ||
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.filter((plan) => plan.via).length
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
- assignmentLoadPenalty(state.assignment) * 0.01
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.values(state.assignment)) {
739
- layerLoads.set(layer, (layerLoads.get(layer) ?? 0) + 1)
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
- Number(first === sourceLayer) - Number(second === sourceLayer) ||
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.filter((plan) => plan.via).length * 0.1 +
816
- assignmentLoadPenalty(bestState.assignment) * 0.01
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: this.bestAttempt.outputSrj,
1055
- fanoutTraces: this.bestAttempt.plans.map((plan) => plan.trace),
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 = this.bestAttempt?.outputSrj ?? this.inputSrj
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"