@tscircuit/fanout-solver 0.0.35 → 0.0.37

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.
@@ -1,10 +1,11 @@
1
1
  import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
2
2
  import { BaseSolver } from "@tscircuit/solver-utils"
3
3
  import type { GraphicsObject } from "graphics-debug"
4
+ import { getCornerBandSide } from "./boundary-exit"
4
5
  import { buildOutputSimpleRouteJson } from "./build-output"
5
6
  import {
6
- completeOriginalEndpoints,
7
7
  type CompleteOriginalEndpointsResult,
8
+ completeOriginalEndpoints,
8
9
  } from "./complete-original-endpoints"
9
10
  import { generateLayerAssignments, getCopperLayerNames } from "./layer-names"
10
11
  import {
@@ -12,14 +13,12 @@ import {
12
13
  resolveAvailableBoundaryRegions,
13
14
  } from "./prepare-buses"
14
15
  import {
16
+ type RouteBusStaticClearanceCache,
15
17
  routeBus,
16
18
  routeBusAlternatives,
17
- type RouteBusStaticClearanceCache,
18
19
  } from "./route-bus"
19
20
  import { routeSingleLayerWithAdaptiveExits } from "./route-single-layer-adaptive-exits"
20
21
  import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
21
- import { validateFanoutSolution } from "./validate-fanout-solution"
22
- import { visualizeSimpleRouteJson } from "./visualize-simple-route-json"
23
22
  import type {
24
23
  AssignmentAttempt,
25
24
  Bounds,
@@ -30,6 +29,8 @@ import type {
30
29
  FanoutSolverOutput,
31
30
  PreparedBus,
32
31
  } from "./types"
32
+ import { validateFanoutSolution } from "./validate-fanout-solution"
33
+ import { visualizeSimpleRouteJson } from "./visualize-simple-route-json"
33
34
 
34
35
  interface ResolvedFanoutConfig {
35
36
  traceWidth: number
@@ -146,6 +147,45 @@ function resolveConfig(
146
147
  }
147
148
  }
148
149
 
150
+ function validateCornerBandCapacities(
151
+ buses: readonly PreparedBus[],
152
+ config: ResolvedFanoutConfig,
153
+ ): void {
154
+ const checkedBands = new Set<string>()
155
+ const exitPitch = Math.max(
156
+ config.traceWidth + config.clearance,
157
+ config.viaDiameter + config.clearance,
158
+ )
159
+ // Keep the block clear of the physical end of the edge and leave one
160
+ // unoccupied via-pitch between the minimum and maximum quarter bands.
161
+ const endInset = Math.max(
162
+ config.viaDiameter / 2 + config.clearance,
163
+ exitPitch,
164
+ )
165
+
166
+ for (const bus of buses) {
167
+ const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
168
+ if (!bus.exitEdge || !side) continue
169
+ const bandKey = `${bus.exitEdge}:${side}`
170
+ if (checkedBands.has(bandKey)) continue
171
+ checkedBands.add(bandKey)
172
+
173
+ const edgeLength =
174
+ bus.exitEdge === "left" || bus.exitEdge === "right"
175
+ ? bus.sharedBoundary.maxY - bus.sharedBoundary.minY
176
+ : bus.sharedBoundary.maxX - bus.sharedBoundary.minX
177
+ const connectionCount =
178
+ bus.cornerBandConnectionCount ?? bus.connections.length
179
+ const halfTrackSpan = ((connectionCount - 1) * exitPitch) / 2
180
+ const availableHalfTrackSpan = edgeLength / 4 - endInset
181
+ if (halfTrackSpan > availableHalfTrackSpan + 1e-6) {
182
+ throw new Error(
183
+ `FanoutSolver: ${side} band on the ${bus.exitEdge} edge cannot fit ${connectionCount} via-safe exits`,
184
+ )
185
+ }
186
+ }
187
+ }
188
+
149
189
  function assignmentLoadPenalty(
150
190
  assignment: Readonly<Record<string, string>>,
151
191
  buses: readonly PreparedBus[],
@@ -179,6 +219,7 @@ function getPlanViaCount(plans: readonly FanoutRoutePlan[]): number {
179
219
  (count, plan) =>
180
220
  count +
181
221
  Number(Boolean(plan.via)) +
222
+ (plan.additionalVias?.length ?? 0) +
182
223
  Number(Boolean(plan.planeEndpointVia)),
183
224
  0,
184
225
  )
@@ -217,6 +258,27 @@ function busUsesDestinationGuidedTracks(bus: PreparedBus): boolean {
217
258
  })
218
259
  }
219
260
 
261
+ function getCommonExplicitExitTargetLayer(
262
+ bus: PreparedBus,
263
+ ): string | undefined {
264
+ if (
265
+ bus.connections.length === 0 ||
266
+ bus.connections.some(
267
+ (connection) =>
268
+ !connection.hasExplicitLayeredExitTarget ||
269
+ !connection.exitTargetPoint?.layer,
270
+ )
271
+ ) {
272
+ return undefined
273
+ }
274
+ const targetLayers = new Set(
275
+ bus.connections.map((connection) => connection.exitTargetPoint!.layer!),
276
+ )
277
+ if (targetLayers.size !== 1) return undefined
278
+ const [targetLayer] = targetLayers
279
+ return targetLayer
280
+ }
281
+
220
282
  function busIsOnOutwardComponentEdge(bus: PreparedBus): boolean {
221
283
  const isHorizontal = bus.direction === "left" || bus.direction === "right"
222
284
  const directionalCoordinates = isHorizontal
@@ -287,7 +349,14 @@ function createInitialLayerAssignment(params: {
287
349
  const viaLayers = routableEscapeLayers.filter(
288
350
  (layer) => layer !== sourceLayer,
289
351
  )
352
+ const commonExitTargetLayer = getCommonExplicitExitTargetLayer(bus)
290
353
  if (
354
+ commonExitTargetLayer &&
355
+ routableEscapeLayers.includes(commonExitTargetLayer)
356
+ ) {
357
+ assignment[bus.busId] = commonExitTargetLayer
358
+ } else if (
359
+ !busUsesCoordinatedWinding(bus) &&
291
360
  routableEscapeLayers.includes(sourceLayer) &&
292
361
  (busUsesDestinationGuidedTracks(bus) || busIsOnOutwardComponentEdge(bus))
293
362
  ) {
@@ -328,6 +397,17 @@ function prioritizeLayerAssignment(params: {
328
397
  ].slice(0, maxAssignments)
329
398
  }
330
399
 
400
+ function busUsesCoordinatedWinding(bus: PreparedBus): boolean {
401
+ return Boolean(
402
+ bus.exitEdge &&
403
+ bus.termination.type === "boundary" &&
404
+ bus.connections.length > 0 &&
405
+ bus.connections.every(
406
+ (connection) => connection.hasExplicitLayeredExitTarget === true,
407
+ ),
408
+ )
409
+ }
410
+
331
411
  function getCandidateEscapeLayersForBus(params: {
332
412
  bus: PreparedBus
333
413
  srj: SimpleRouteJson
@@ -340,6 +420,10 @@ function getCandidateEscapeLayersForBus(params: {
340
420
  busAllowedLayers === undefined
341
421
  ? config.escapeLayers
342
422
  : config.escapeLayers.filter((layer) => busAllowedLayers.includes(layer))
423
+ // A coordinated winding route is deliberately planned with the other buses'
424
+ // committed escape vias present. Testing it in isolation is both expensive
425
+ // and can reject a layer whose shared via field guides a valid bus ordering.
426
+ if (busUsesCoordinatedWinding(bus)) return allowedEscapeLayers
343
427
  const individuallyRoutableLayers = allowedEscapeLayers.filter(
344
428
  (targetLayer) =>
345
429
  routeBus({
@@ -411,7 +495,21 @@ export class FanoutSolver extends BaseSolver {
411
495
  }
412
496
  this.config = resolveConfig(inputSrj, options)
413
497
  this.preparedBuses = prepareFanoutBuses(this.routingSrj, options)
498
+ validateCornerBandCapacities(this.preparedBuses, this.config)
414
499
  for (const bus of this.preparedBuses) {
500
+ for (const connection of bus.connections) {
501
+ if (!connection.hasExplicitLayeredExitTarget) continue
502
+ const targetLayer = connection.exitTargetPoint?.layer
503
+ if (
504
+ typeof targetLayer !== "string" ||
505
+ targetLayer.length === 0 ||
506
+ !this.config.layerNames.includes(targetLayer)
507
+ ) {
508
+ throw new Error(
509
+ `FanoutSolver: connection exit target for "${connection.connection.name}" uses unavailable layer "${String(targetLayer)}"`,
510
+ )
511
+ }
512
+ }
415
513
  for (const allowedLayer of bus.allowedLayers ?? []) {
416
514
  if (!this.config.layerNames.includes(allowedLayer)) {
417
515
  throw new Error(
@@ -430,6 +528,9 @@ export class FanoutSolver extends BaseSolver {
430
528
  `FanoutSolver: bus "${bus.busId}" has no allowed layer in escapeLayers`,
431
529
  )
432
530
  }
531
+ bus.routableEscapeLayers = this.config.escapeLayers.filter(
532
+ (layer) => bus.allowedLayers?.includes(layer) ?? true,
533
+ )
433
534
  if (bus.termination.type !== "plane") continue
434
535
  const planeLayer = bus.termination.layer
435
536
  if (!this.config.layerNames.includes(planeLayer)) {
@@ -566,7 +667,13 @@ export class FanoutSolver extends BaseSolver {
566
667
  let failedBusIds: string[] = []
567
668
  let blockingBusCounts = new Map<string, number>()
568
669
  const isSingleLayerFanout = this.config.escapeLayers.length === 1
569
- if (isSingleLayerFanout && this.config.singleLayerPushAndShove) {
670
+ const useSingleLayerPushAndShove =
671
+ isSingleLayerFanout &&
672
+ this.config.singleLayerPushAndShove &&
673
+ !this.preparedBuses.some(
674
+ (bus) => bus.exitEdge && bus.preferredExit?.includes("-"),
675
+ )
676
+ if (useSingleLayerPushAndShove) {
570
677
  const singleLayerParams = {
571
678
  srj: this.routingSrj,
572
679
  buses: this.preparedBuses,
@@ -590,10 +697,22 @@ export class FanoutSolver extends BaseSolver {
590
697
  failedBusIds.push(...this.preparedBuses.map((bus) => bus.busId))
591
698
  }
592
699
  }
593
- const busesInRoutingOrder = [...this.preparedBuses].sort(
594
- (a, b) =>
700
+ const busesInRoutingOrder = [...this.preparedBuses].sort((a, b) => {
701
+ const aUsesCoordinatedWinding = busUsesCoordinatedWinding(a)
702
+ const bUsesCoordinatedWinding = busUsesCoordinatedWinding(b)
703
+ const aLayerIndex = this.config.layerNames.indexOf(
704
+ busLayerAssignments[a.busId] ?? "",
705
+ )
706
+ const bLayerIndex = this.config.layerNames.indexOf(
707
+ busLayerAssignments[b.busId] ?? "",
708
+ )
709
+ return (
595
710
  Number(b.termination.type === "plane") -
596
711
  Number(a.termination.type === "plane") ||
712
+ Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) ||
713
+ (aUsesCoordinatedWinding && bUsesCoordinatedWinding
714
+ ? bLayerIndex - aLayerIndex
715
+ : 0) ||
597
716
  (routingStrategy === "group-by-layer"
598
717
  ? (busLayerAssignments[a.busId] ?? "").localeCompare(
599
718
  busLayerAssignments[b.busId] ?? "",
@@ -605,13 +724,12 @@ export class FanoutSolver extends BaseSolver {
605
724
  : b.connections.length - a.connections.length ||
606
725
  (routingStrategy === "deep-first"
607
726
  ? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a)
608
- : getBusDistanceToBoundary(a) - getBusDistanceToBoundary(b))),
609
- )
727
+ : getBusDistanceToBoundary(a) - getBusDistanceToBoundary(b)))
728
+ )
729
+ })
610
730
 
611
731
  let routingPrefixKey = `${routingStrategy}|`
612
- for (const bus of isSingleLayerFanout && this.config.singleLayerPushAndShove
613
- ? []
614
- : busesInRoutingOrder) {
732
+ for (const bus of useSingleLayerPushAndShove ? [] : busesInRoutingOrder) {
615
733
  const targetLayer = busLayerAssignments[bus.busId]
616
734
  if (!targetLayer) {
617
735
  throw new Error(
@@ -735,7 +853,8 @@ export class FanoutSolver extends BaseSolver {
735
853
  )
736
854
  if (
737
855
  bestAttempt.summary.routedConnectionCount ===
738
- this.inputSrj.connections.length
856
+ this.inputSrj.connections.length &&
857
+ this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0
739
858
  ) {
740
859
  return bestAttempt
741
860
  }
@@ -746,12 +865,13 @@ export class FanoutSolver extends BaseSolver {
746
865
  busLayerAssignments,
747
866
  routingStrategy,
748
867
  )
749
- if (attempt.summary.score < bestAttempt.summary.score) {
868
+ if (this.isAttemptBetter(attempt, bestAttempt)) {
750
869
  bestAttempt = attempt
751
870
  }
752
871
  if (
753
872
  bestAttempt.summary.routedConnectionCount ===
754
- this.inputSrj.connections.length
873
+ this.inputSrj.connections.length &&
874
+ this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0
755
875
  ) {
756
876
  return bestAttempt
757
877
  }
@@ -784,7 +904,24 @@ export class FanoutSolver extends BaseSolver {
784
904
  return null
785
905
  }
786
906
 
907
+ const getMaximumViaSpan = (bus: PreparedBus): number => {
908
+ const sourceLayerIndex = this.config.layerNames.indexOf(
909
+ bus.connections[0]?.sourceLayer ?? "",
910
+ )
911
+ const candidateLayers =
912
+ bus.termination.type === "plane"
913
+ ? [bus.termination.layer]
914
+ : (this.escapeLayersByBusId[bus.busId] ?? this.config.escapeLayers)
915
+ return Math.max(
916
+ 0,
917
+ ...candidateLayers.map((layer) =>
918
+ Math.abs(this.config.layerNames.indexOf(layer) - sourceLayerIndex),
919
+ ),
920
+ )
921
+ }
787
922
  const busesInSearchOrder = [...this.preparedBuses].sort((a, b) => {
923
+ const aUsesCoordinatedWinding = busUsesCoordinatedWinding(a)
924
+ const bUsesCoordinatedWinding = busUsesCoordinatedWinding(b)
788
925
  const aLayerCount =
789
926
  a.termination.type === "plane"
790
927
  ? 1
@@ -798,6 +935,10 @@ export class FanoutSolver extends BaseSolver {
798
935
  return (
799
936
  Number(b.termination.type === "plane") -
800
937
  Number(a.termination.type === "plane") ||
938
+ Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) ||
939
+ (aUsesCoordinatedWinding && bUsesCoordinatedWinding
940
+ ? getMaximumViaSpan(b) - getMaximumViaSpan(a)
941
+ : 0) ||
801
942
  (groupByDirection ? a.direction.localeCompare(b.direction) : 0) ||
802
943
  aLayerCount - bLayerCount ||
803
944
  b.componentObstacles.length - a.componentObstacles.length ||
@@ -835,7 +976,9 @@ export class FanoutSolver extends BaseSolver {
835
976
  return count
836
977
  }
837
978
  const sourceLayer = bus.connections[0]?.sourceLayer
838
- return state.assignment[bus.busId] === sourceLayer
979
+ const preferredLayer =
980
+ getCommonExplicitExitTargetLayer(bus) ?? sourceLayer
981
+ return state.assignment[bus.busId] === preferredLayer
839
982
  ? count
840
983
  : count + bus.connections.length
841
984
  },
@@ -875,10 +1018,13 @@ export class FanoutSolver extends BaseSolver {
875
1018
  )
876
1019
  }
877
1020
  const sourceLayer = bus.connections[0]?.sourceLayer
1021
+ const commonExitTargetLayer = getCommonExplicitExitTargetLayer(bus)
878
1022
  const preferSourceLayer = busUsesDestinationGuidedTracks(bus)
879
1023
  const orderedLayers = candidateLayers.toSorted(
880
1024
  (first, second) =>
881
1025
  (layerLoads.get(first) ?? 0) - (layerLoads.get(second) ?? 0) ||
1026
+ Number(second === commonExitTargetLayer) -
1027
+ Number(first === commonExitTargetLayer) ||
882
1028
  (preferSourceLayer
883
1029
  ? Number(second === sourceLayer) - Number(first === sourceLayer)
884
1030
  : Number(first === sourceLayer) -
@@ -918,6 +1064,10 @@ export class FanoutSolver extends BaseSolver {
918
1064
 
919
1065
  if (nextStates.length === 0) return null
920
1066
  nextStates.sort((first, second) => {
1067
+ const additionalViaDifference =
1068
+ this.getCoordinatedAdditionalViaCount(first.plans) -
1069
+ this.getCoordinatedAdditionalViaCount(second.plans)
1070
+ if (additionalViaDifference !== 0) return additionalViaDifference
921
1071
  const scoreDifference = getStateScore(first) - getStateScore(second)
922
1072
  if (Math.abs(scoreDifference) > 1e-9) return scoreDifference
923
1073
  return JSON.stringify(first.assignment).localeCompare(
@@ -936,19 +1086,23 @@ export class FanoutSolver extends BaseSolver {
936
1086
  }
937
1087
  }
938
1088
 
939
- const bestState = states[0]
940
- if (!bestState) return null
941
- const outputSrj = buildOutputSimpleRouteJson({
942
- inputSrj: this.inputSrj,
943
- plans: bestState.plans,
944
- layerNames: this.config.layerNames,
945
- })
946
- if (
947
- bestState.plans.length === this.inputSrj.connections.length &&
948
- !this.validateCompletePlans(bestState.plans, outputSrj).valid
949
- ) {
950
- return null
1089
+ let bestState: GroupedBeamState | undefined
1090
+ let outputSrj: SimpleRouteJson | undefined
1091
+ for (const state of states) {
1092
+ if (state.plans.length !== this.inputSrj.connections.length) continue
1093
+ const candidateOutput = buildOutputSimpleRouteJson({
1094
+ inputSrj: this.inputSrj,
1095
+ plans: state.plans,
1096
+ layerNames: this.config.layerNames,
1097
+ })
1098
+ if (!this.validateCompletePlans(state.plans, candidateOutput).valid) {
1099
+ continue
1100
+ }
1101
+ bestState = state
1102
+ outputSrj = candidateOutput
1103
+ break
951
1104
  }
1105
+ if (!bestState || !outputSrj) return null
952
1106
  const score =
953
1107
  bestState.plans.length === this.inputSrj.connections.length
954
1108
  ? bestState.plans.reduce((total, plan) => total + plan.length, 0) +
@@ -1071,6 +1225,66 @@ export class FanoutSolver extends BaseSolver {
1071
1225
  )
1072
1226
  }
1073
1227
 
1228
+ private getCoordinatedAdditionalViaCount(
1229
+ plans: readonly FanoutRoutePlan[],
1230
+ ): number {
1231
+ const coordinatedBusIds = new Set(
1232
+ this.preparedBuses
1233
+ .filter(busUsesCoordinatedWinding)
1234
+ .map((bus) => bus.busId),
1235
+ )
1236
+ return plans.reduce(
1237
+ (count, plan) =>
1238
+ count +
1239
+ (coordinatedBusIds.has(plan.busId)
1240
+ ? (plan.additionalVias?.length ?? 0)
1241
+ : 0),
1242
+ 0,
1243
+ )
1244
+ }
1245
+
1246
+ private isAttemptBetter(
1247
+ candidate: AssignmentAttempt,
1248
+ current: AssignmentAttempt,
1249
+ ): boolean {
1250
+ if (
1251
+ candidate.summary.routedConnectionCount !==
1252
+ current.summary.routedConnectionCount
1253
+ ) {
1254
+ return (
1255
+ candidate.summary.routedConnectionCount >
1256
+ current.summary.routedConnectionCount
1257
+ )
1258
+ }
1259
+ if (candidate.summary.routedBusCount !== current.summary.routedBusCount) {
1260
+ return candidate.summary.routedBusCount > current.summary.routedBusCount
1261
+ }
1262
+ const candidateAdditionalVias = this.getCoordinatedAdditionalViaCount(
1263
+ candidate.plans,
1264
+ )
1265
+ const currentAdditionalVias = this.getCoordinatedAdditionalViaCount(
1266
+ current.plans,
1267
+ )
1268
+ if (candidateAdditionalVias !== currentAdditionalVias) {
1269
+ return candidateAdditionalVias < currentAdditionalVias
1270
+ }
1271
+ return candidate.summary.score < current.summary.score
1272
+ }
1273
+
1274
+ private hasGloballyViaMinimalBestAttempt(): boolean {
1275
+ if (!this.hasCompleteBestAttempt() || !this.bestAttempt) return false
1276
+ if (
1277
+ this.preparedBuses.length === 0 ||
1278
+ !this.preparedBuses.every(busUsesCoordinatedWinding)
1279
+ ) {
1280
+ return false
1281
+ }
1282
+ return this.bestAttempt.plans.every(
1283
+ (plan) =>
1284
+ plan.via !== undefined && (plan.additionalVias?.length ?? 0) === 0,
1285
+ )
1286
+ }
1287
+
1074
1288
  private shouldEvaluateGroupedBeam(): boolean {
1075
1289
  if (this.groupedBeamEvaluated || this.nextAssignmentIndex === 0) {
1076
1290
  return false
@@ -1085,6 +1299,14 @@ export class FanoutSolver extends BaseSolver {
1085
1299
  }
1086
1300
 
1087
1301
  override _step(): void {
1302
+ if (
1303
+ this.nextAssignmentIndex > 0 &&
1304
+ this.hasGloballyViaMinimalBestAttempt()
1305
+ ) {
1306
+ this.completeBestAttemptEndpoints()
1307
+ this.solved = true
1308
+ return
1309
+ }
1088
1310
  // Try the deterministic assignment and only its targeted repair queue
1089
1311
  // before paying for the grouped beam. If the beam cannot solve, continue
1090
1312
  // with the broader generated-assignment search below.
@@ -1098,7 +1320,7 @@ export class FanoutSolver extends BaseSolver {
1098
1320
  this.attempts.push(beamAttempt.summary)
1099
1321
  if (
1100
1322
  !this.bestAttempt ||
1101
- beamAttempt.summary.score < this.bestAttempt.summary.score
1323
+ this.isAttemptBetter(beamAttempt, this.bestAttempt)
1102
1324
  ) {
1103
1325
  this.bestAttempt = beamAttempt
1104
1326
  }
@@ -1114,11 +1336,19 @@ export class FanoutSolver extends BaseSolver {
1114
1336
  failedBuses: "none",
1115
1337
  bestScore: bestSummary.score,
1116
1338
  }
1117
- this.completeBestAttemptEndpoints()
1118
- this.solved = true
1119
- return
1339
+ if (
1340
+ this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0
1341
+ ) {
1342
+ this.completeBestAttemptEndpoints()
1343
+ this.solved = true
1344
+ return
1345
+ }
1120
1346
  }
1121
- if (this.hasCompleteBestAttempt()) {
1347
+ if (
1348
+ this.hasCompleteBestAttempt() &&
1349
+ this.bestAttempt &&
1350
+ this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0
1351
+ ) {
1122
1352
  this.completeBestAttemptEndpoints()
1123
1353
  this.solved = true
1124
1354
  return
@@ -1192,10 +1422,7 @@ export class FanoutSolver extends BaseSolver {
1192
1422
  )
1193
1423
  }
1194
1424
  this.attempts.push(attempt.summary)
1195
- if (
1196
- !this.bestAttempt ||
1197
- attempt.summary.score < this.bestAttempt.summary.score
1198
- ) {
1425
+ if (!this.bestAttempt || this.isAttemptBetter(attempt, this.bestAttempt)) {
1199
1426
  this.bestAttempt = attempt
1200
1427
  }
1201
1428
  this.stats = {
@@ -1208,7 +1435,9 @@ export class FanoutSolver extends BaseSolver {
1208
1435
  }
1209
1436
  if (
1210
1437
  this.groupedBeamEvaluated &&
1211
- attempt.summary.routedConnectionCount === this.inputSrj.connections.length
1438
+ attempt.summary.routedConnectionCount ===
1439
+ this.inputSrj.connections.length &&
1440
+ this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0
1212
1441
  ) {
1213
1442
  this.completeBestAttemptEndpoints()
1214
1443
  this.solved = true
package/lib/index.ts CHANGED
@@ -1,19 +1,8 @@
1
- export { FanoutSolver } from "./fanout-solver"
2
1
  export { completeOriginalEndpoints } from "./complete-original-endpoints"
2
+ export { getFanoutExitPositionConfig } from "./fanout-exit-position"
3
+ export { FanoutSolver } from "./fanout-solver"
3
4
  export { getCopperLayerColor } from "./layer-colors"
4
5
  export { getCopperLayerNames } from "./layer-names"
5
- export { validateOriginalEndpointConnectivity } from "./validate-original-endpoint-connectivity"
6
- export { validateRoutedCopperDrc } from "./validate-routed-copper-drc"
7
- export { validateFanoutSolution } from "./validate-fanout-solution"
8
- export type {
9
- OriginalEndpointConnectivityIssue,
10
- OriginalEndpointConnectivityReport,
11
- } from "./validate-original-endpoint-connectivity"
12
- export type {
13
- RoutedCopperDrcIssue,
14
- RoutedCopperDrcIssueCode,
15
- RoutedCopperDrcReport,
16
- } from "./validate-routed-copper-drc"
17
6
  export type {
18
7
  Bounds,
19
8
  FanoutAttemptSummary,
@@ -30,6 +19,8 @@ export type {
30
19
  FanoutDownstreamRouterOptions,
31
20
  FanoutEdge,
32
21
  FanoutEndpointCompletionReport,
22
+ FanoutExitPosition,
23
+ FanoutExitPositionConfig,
33
24
  FanoutPlaneConnectivity,
34
25
  FanoutPlaneTermination,
35
26
  FanoutRoutePlan,
@@ -41,3 +32,15 @@ export type {
41
32
  PreparedBus,
42
33
  SimpleRouteJsonWithFanoutPlanes,
43
34
  } from "./types"
35
+ export { validateFanoutSolution } from "./validate-fanout-solution"
36
+ export type {
37
+ OriginalEndpointConnectivityIssue,
38
+ OriginalEndpointConnectivityReport,
39
+ } from "./validate-original-endpoint-connectivity"
40
+ export { validateOriginalEndpointConnectivity } from "./validate-original-endpoint-connectivity"
41
+ export type {
42
+ RoutedCopperDrcIssue,
43
+ RoutedCopperDrcIssueCode,
44
+ RoutedCopperDrcReport,
45
+ } from "./validate-routed-copper-drc"
46
+ export { validateRoutedCopperDrc } from "./validate-routed-copper-drc"