@tscircuit/fanout-solver 0.0.40 → 0.0.42
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/lib/fanout-solver.ts +148 -61
- package/package.json +1 -1
package/lib/fanout-solver.ts
CHANGED
|
@@ -440,6 +440,36 @@ function busUsesCoordinatedWinding(bus: PreparedBus): boolean {
|
|
|
440
440
|
)
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
export function shouldUseJointBoundaryViaReservation(
|
|
444
|
+
boundaryBusConnectionCounts: readonly number[],
|
|
445
|
+
): boolean {
|
|
446
|
+
return (
|
|
447
|
+
boundaryBusConnectionCounts.length === 5 ||
|
|
448
|
+
(boundaryBusConnectionCounts.length === 4 &&
|
|
449
|
+
new Set(boundaryBusConnectionCounts).size > 1)
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function shouldDeferSingletonBoundaryViaReservation(
|
|
454
|
+
boundaryBusConnectionCounts: readonly number[],
|
|
455
|
+
): boolean {
|
|
456
|
+
return (
|
|
457
|
+
boundaryBusConnectionCounts.length === 5 &&
|
|
458
|
+
boundaryBusConnectionCounts.filter((count) => count === 1).length === 1
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function shouldSearchAdditionalBoundaryRouteTopologies(params: {
|
|
463
|
+
boundaryBusCount: number
|
|
464
|
+
rawSkew: number
|
|
465
|
+
maximumSkew: number
|
|
466
|
+
}): boolean {
|
|
467
|
+
if (params.boundaryBusCount >= 5) return false
|
|
468
|
+
return (
|
|
469
|
+
params.rawSkew - params.maximumSkew > Math.max(1, params.maximumSkew * 0.25)
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
443
473
|
function getCandidateEscapeLayersForBus(params: {
|
|
444
474
|
bus: PreparedBus
|
|
445
475
|
srj: SimpleRouteJson
|
|
@@ -722,48 +752,59 @@ export class FanoutSolver extends BaseSolver {
|
|
|
722
752
|
}): MixedTerminationState | null {
|
|
723
753
|
if (this.config.allowBlindAndBuriedVias) return null
|
|
724
754
|
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
755
|
+
const unsortedBoundaryBuses = params.busesInRoutingOrder.filter(
|
|
756
|
+
(bus) => bus.termination.type === "boundary",
|
|
757
|
+
)
|
|
758
|
+
const useJointBoundaryViaReservation = shouldUseJointBoundaryViaReservation(
|
|
759
|
+
unsortedBoundaryBuses.map((bus) => bus.connections.length),
|
|
760
|
+
)
|
|
761
|
+
const boundaryBuses = unsortedBoundaryBuses.toSorted((first, second) => {
|
|
762
|
+
// Reserve the dense escape field for the widest buses first. Small
|
|
763
|
+
// control groups can usually route around their copper, while routing
|
|
764
|
+
// a two-line corner bus first can consume a critical channel needed by
|
|
765
|
+
// an eight-line winding bus and force the expensive fallback search.
|
|
766
|
+
if (useJointBoundaryViaReservation) {
|
|
767
|
+
const connectionCountDifference =
|
|
768
|
+
second.connections.length - first.connections.length
|
|
769
|
+
if (connectionCountDifference !== 0) return connectionCountDifference
|
|
770
|
+
}
|
|
771
|
+
const cornerBandDifference =
|
|
772
|
+
Number(
|
|
773
|
+
Boolean(getCornerBandSide(second.exitEdge, second.preferredExit)),
|
|
774
|
+
) -
|
|
775
|
+
Number(Boolean(getCornerBandSide(first.exitEdge, first.preferredExit)))
|
|
776
|
+
if (cornerBandDifference !== 0) return cornerBandDifference
|
|
777
|
+
const firstIsCorner = Boolean(
|
|
778
|
+
getCornerBandSide(first.exitEdge, first.preferredExit),
|
|
779
|
+
)
|
|
780
|
+
if (!firstIsCorner) {
|
|
781
|
+
const getSourceSpan = (bus: PreparedBus): number => {
|
|
782
|
+
const xCoordinates = bus.connections.map(
|
|
783
|
+
(connection) => connection.sourcePoint.x,
|
|
784
|
+
)
|
|
785
|
+
const yCoordinates = bus.connections.map(
|
|
786
|
+
(connection) => connection.sourcePoint.y,
|
|
787
|
+
)
|
|
788
|
+
return (
|
|
789
|
+
Math.max(...xCoordinates) -
|
|
790
|
+
Math.min(...xCoordinates) +
|
|
791
|
+
Math.max(...yCoordinates) -
|
|
792
|
+
Math.min(...yCoordinates)
|
|
734
793
|
)
|
|
735
|
-
if (cornerBandDifference !== 0) return cornerBandDifference
|
|
736
|
-
const firstIsCorner = Boolean(
|
|
737
|
-
getCornerBandSide(first.exitEdge, first.preferredExit),
|
|
738
|
-
)
|
|
739
|
-
if (!firstIsCorner) {
|
|
740
|
-
const getSourceSpan = (bus: PreparedBus): number => {
|
|
741
|
-
const xCoordinates = bus.connections.map(
|
|
742
|
-
(connection) => connection.sourcePoint.x,
|
|
743
|
-
)
|
|
744
|
-
const yCoordinates = bus.connections.map(
|
|
745
|
-
(connection) => connection.sourcePoint.y,
|
|
746
|
-
)
|
|
747
|
-
return (
|
|
748
|
-
Math.max(...xCoordinates) -
|
|
749
|
-
Math.min(...xCoordinates) +
|
|
750
|
-
Math.max(...yCoordinates) -
|
|
751
|
-
Math.min(...yCoordinates)
|
|
752
|
-
)
|
|
753
|
-
}
|
|
754
|
-
const sourceSpanDifference =
|
|
755
|
-
getSourceSpan(second) - getSourceSpan(first)
|
|
756
|
-
if (Math.abs(sourceSpanDifference) > 1e-9) {
|
|
757
|
-
return sourceSpanDifference
|
|
758
|
-
}
|
|
759
794
|
}
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
795
|
+
const sourceSpanDifference =
|
|
796
|
+
getSourceSpan(second) - getSourceSpan(first)
|
|
797
|
+
if (Math.abs(sourceSpanDifference) > 1e-9) {
|
|
798
|
+
return sourceSpanDifference
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const firstLayer = params.busLayerAssignments[first.busId]
|
|
802
|
+
const secondLayer = params.busLayerAssignments[second.busId]
|
|
803
|
+
const layerDifference =
|
|
804
|
+
this.config.layerNames.indexOf(firstLayer ?? "") -
|
|
805
|
+
this.config.layerNames.indexOf(secondLayer ?? "")
|
|
806
|
+
return -layerDifference
|
|
807
|
+
})
|
|
767
808
|
// Preserve the caller/input order for the dense singleton fill. The
|
|
768
809
|
// general routing sort is useful for heterogeneous buses, but ordering a
|
|
769
810
|
// regular BGA power field by obstacle depth creates artificial local
|
|
@@ -774,7 +815,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
774
815
|
)
|
|
775
816
|
if (
|
|
776
817
|
boundaryBuses.length === 0 ||
|
|
777
|
-
boundaryBuses.length >
|
|
818
|
+
boundaryBuses.length > 5 ||
|
|
778
819
|
planeBuses.length < 8 ||
|
|
779
820
|
boundaryBuses.some((bus) => !busUsesCoordinatedWinding(bus)) ||
|
|
780
821
|
planeBuses.some((bus) => bus.connections.length !== 1) ||
|
|
@@ -821,9 +862,41 @@ export class FanoutSolver extends BaseSolver {
|
|
|
821
862
|
),
|
|
822
863
|
)
|
|
823
864
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
865
|
+
// Five boundary buses, and heterogeneous four-bus groups, leave too little
|
|
866
|
+
// slack for incremental site allocation: a valid early trace can consume
|
|
867
|
+
// the last dogbone site of a later narrow bus. Reserve the multi-line bus
|
|
868
|
+
// barrels before routing copper. A single one-line bus stays provisional
|
|
869
|
+
// because its flexible site can be rematched around the wide-bus copper.
|
|
870
|
+
// Plane sites are likewise rematched around completed boundary plans.
|
|
871
|
+
const boundaryBusConnectionCounts = boundaryBuses.map(
|
|
872
|
+
(bus) => bus.connections.length,
|
|
873
|
+
)
|
|
874
|
+
const provisionalSingletonBus = shouldDeferSingletonBoundaryViaReservation(
|
|
875
|
+
boundaryBusConnectionCounts,
|
|
876
|
+
)
|
|
877
|
+
? boundaryBuses.find((bus) => bus.connections.length === 1)
|
|
878
|
+
: undefined
|
|
879
|
+
const initiallyMatchedBoundaryBuses = provisionalSingletonBus
|
|
880
|
+
? boundaryBuses.filter((bus) => bus !== provisionalSingletonBus)
|
|
881
|
+
: boundaryBuses
|
|
882
|
+
const jointViaPoints = useJointBoundaryViaReservation
|
|
883
|
+
? matchComponentDogboneViaSites(
|
|
884
|
+
[...planeBuses, ...initiallyMatchedBoundaryBuses],
|
|
885
|
+
{
|
|
886
|
+
viaDiameter: this.config.viaDiameter,
|
|
887
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
888
|
+
traceWidth: this.config.traceWidth,
|
|
889
|
+
clearance: this.config.clearance,
|
|
890
|
+
maximumSearchStates: 100_000,
|
|
891
|
+
preferredBoundaryPerpendicularSideByBusId,
|
|
892
|
+
preferBoundaryOutwardByBusId,
|
|
893
|
+
canShareCopper,
|
|
894
|
+
},
|
|
895
|
+
)
|
|
896
|
+
: null
|
|
897
|
+
const seedViaPoints =
|
|
898
|
+
jointViaPoints ??
|
|
899
|
+
matchComponentDogboneViaSites([...planeBuses, boundaryBuses[0]!], {
|
|
827
900
|
viaDiameter: this.config.viaDiameter,
|
|
828
901
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
829
902
|
traceWidth: this.config.traceWidth,
|
|
@@ -832,8 +905,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
832
905
|
preferredBoundaryPerpendicularSideByBusId,
|
|
833
906
|
preferBoundaryOutwardByBusId,
|
|
834
907
|
canShareCopper,
|
|
835
|
-
}
|
|
836
|
-
)
|
|
908
|
+
})
|
|
837
909
|
if (seedViaPoints) {
|
|
838
910
|
let fixedViaPointsByConnectionIndex: ReadonlyMap<
|
|
839
911
|
number,
|
|
@@ -901,7 +973,11 @@ export class FanoutSolver extends BaseSolver {
|
|
|
901
973
|
const lengths = busPlans.map((plan) => plan.length)
|
|
902
974
|
const rawSkew = Math.max(...lengths) - Math.min(...lengths)
|
|
903
975
|
const needsRouteDiversity =
|
|
904
|
-
|
|
976
|
+
shouldSearchAdditionalBoundaryRouteTopologies({
|
|
977
|
+
boundaryBusCount: boundaryBuses.length,
|
|
978
|
+
rawSkew,
|
|
979
|
+
maximumSkew: bus.maxLengthSkew,
|
|
980
|
+
})
|
|
905
981
|
// Only pay for additional A* variants when the first topology is so
|
|
906
982
|
// skewed that compact meanders are unlikely to absorb the deficit.
|
|
907
983
|
// This keeps already-near-matched buses on the single-attempt path.
|
|
@@ -948,21 +1024,32 @@ export class FanoutSolver extends BaseSolver {
|
|
|
948
1024
|
candidateIndex++
|
|
949
1025
|
) {
|
|
950
1026
|
const candidateBus = remainingBoundaryBuses[candidateIndex]!
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
viaDiameter: this.config.viaDiameter,
|
|
955
|
-
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
956
|
-
traceWidth: this.config.traceWidth,
|
|
957
|
-
clearance: this.config.clearance,
|
|
958
|
-
maximumSearchStates: 100_000,
|
|
959
|
-
preferredBoundaryPerpendicularSideByBusId,
|
|
960
|
-
preferBoundaryOutwardByBusId,
|
|
961
|
-
fixedViaPointsByConnectionIndex: fixedViaPointsByConnectionIndex,
|
|
962
|
-
blockingSegments,
|
|
963
|
-
canShareCopper,
|
|
964
|
-
},
|
|
1027
|
+
const candidateHasFixedViaPoints = candidateBus.connections.every(
|
|
1028
|
+
(connection) =>
|
|
1029
|
+
fixedViaPointsByConnectionIndex.has(connection.connectionIndex),
|
|
965
1030
|
)
|
|
1031
|
+
const extendedViaPoints =
|
|
1032
|
+
jointViaPoints && candidateHasFixedViaPoints
|
|
1033
|
+
? // The joint map is deliberately kept intact so getReservedVias()
|
|
1034
|
+
// blocks every already-reserved future through-barrel during A*.
|
|
1035
|
+
// Provisional singleton and plane dogbones are rematched later.
|
|
1036
|
+
new Map(fixedViaPointsByConnectionIndex)
|
|
1037
|
+
: matchComponentDogboneViaSites(
|
|
1038
|
+
[...planeBuses, ...routedBoundaryBuses, candidateBus],
|
|
1039
|
+
{
|
|
1040
|
+
viaDiameter: this.config.viaDiameter,
|
|
1041
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
1042
|
+
traceWidth: this.config.traceWidth,
|
|
1043
|
+
clearance: this.config.clearance,
|
|
1044
|
+
maximumSearchStates: 100_000,
|
|
1045
|
+
preferredBoundaryPerpendicularSideByBusId,
|
|
1046
|
+
preferBoundaryOutwardByBusId,
|
|
1047
|
+
fixedViaPointsByConnectionIndex:
|
|
1048
|
+
fixedViaPointsByConnectionIndex,
|
|
1049
|
+
blockingSegments,
|
|
1050
|
+
canShareCopper,
|
|
1051
|
+
},
|
|
1052
|
+
)
|
|
966
1053
|
if (!extendedViaPoints) continue
|
|
967
1054
|
const previousFixedViaPoints = fixedViaPointsByConnectionIndex
|
|
968
1055
|
const previousPlanCount = matchedPlans.length
|