@tscircuit/fanout-solver 0.0.39 → 0.0.41
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 +405 -102
- package/lib/match-bus-lengths.ts +114 -21
- package/lib/match-component-dogbone-via-sites.ts +72 -1
- package/lib/route-bus.ts +176 -40
- package/lib/route-via-minimal-winding.ts +41 -10
- package/package.json +1 -1
package/lib/fanout-solver.ts
CHANGED
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
getViaSpanLayers,
|
|
15
15
|
} from "./layer-names"
|
|
16
16
|
import { matchBusPlanLengths } from "./match-bus-lengths"
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
getComponentDogboneViaSiteCandidates,
|
|
19
|
+
matchComponentDogboneViaSites,
|
|
20
|
+
} from "./match-component-dogbone-via-sites"
|
|
18
21
|
import { connectionsShareElectricalNet } from "./net-identity"
|
|
19
22
|
import {
|
|
20
23
|
prepareFanoutBuses,
|
|
@@ -514,10 +517,6 @@ export class FanoutSolver extends BaseSolver {
|
|
|
514
517
|
private bestAttempt: AssignmentAttempt | null = null
|
|
515
518
|
private lengthMatchingFailure: FanoutValidationIssue | null = null
|
|
516
519
|
private endpointCompletion: CompleteOriginalEndpointsResult | null = null
|
|
517
|
-
private denseDogboneViaPoints:
|
|
518
|
-
| Map<number, { x: number; y: number }>
|
|
519
|
-
| null
|
|
520
|
-
| undefined
|
|
521
520
|
|
|
522
521
|
constructor(
|
|
523
522
|
public readonly inputSrj: SimpleRouteJson,
|
|
@@ -723,9 +722,60 @@ export class FanoutSolver extends BaseSolver {
|
|
|
723
722
|
}): MixedTerminationState | null {
|
|
724
723
|
if (this.config.allowBlindAndBuriedVias) return null
|
|
725
724
|
|
|
726
|
-
const
|
|
725
|
+
const unsortedBoundaryBuses = params.busesInRoutingOrder.filter(
|
|
727
726
|
(bus) => bus.termination.type === "boundary",
|
|
728
727
|
)
|
|
728
|
+
const useJointFourBusReservation =
|
|
729
|
+
unsortedBoundaryBuses.length === 4 &&
|
|
730
|
+
new Set(unsortedBoundaryBuses.map((bus) => bus.connections.length)).size >
|
|
731
|
+
1
|
|
732
|
+
const boundaryBuses = unsortedBoundaryBuses.toSorted((first, second) => {
|
|
733
|
+
// Reserve the dense escape field for the widest buses first. Small
|
|
734
|
+
// control groups can usually route around their copper, while routing
|
|
735
|
+
// a two-line corner bus first can consume a critical channel needed by
|
|
736
|
+
// an eight-line winding bus and force the expensive fallback search.
|
|
737
|
+
if (useJointFourBusReservation) {
|
|
738
|
+
const connectionCountDifference =
|
|
739
|
+
second.connections.length - first.connections.length
|
|
740
|
+
if (connectionCountDifference !== 0) return connectionCountDifference
|
|
741
|
+
}
|
|
742
|
+
const cornerBandDifference =
|
|
743
|
+
Number(
|
|
744
|
+
Boolean(getCornerBandSide(second.exitEdge, second.preferredExit)),
|
|
745
|
+
) -
|
|
746
|
+
Number(Boolean(getCornerBandSide(first.exitEdge, first.preferredExit)))
|
|
747
|
+
if (cornerBandDifference !== 0) return cornerBandDifference
|
|
748
|
+
const firstIsCorner = Boolean(
|
|
749
|
+
getCornerBandSide(first.exitEdge, first.preferredExit),
|
|
750
|
+
)
|
|
751
|
+
if (!firstIsCorner) {
|
|
752
|
+
const getSourceSpan = (bus: PreparedBus): number => {
|
|
753
|
+
const xCoordinates = bus.connections.map(
|
|
754
|
+
(connection) => connection.sourcePoint.x,
|
|
755
|
+
)
|
|
756
|
+
const yCoordinates = bus.connections.map(
|
|
757
|
+
(connection) => connection.sourcePoint.y,
|
|
758
|
+
)
|
|
759
|
+
return (
|
|
760
|
+
Math.max(...xCoordinates) -
|
|
761
|
+
Math.min(...xCoordinates) +
|
|
762
|
+
Math.max(...yCoordinates) -
|
|
763
|
+
Math.min(...yCoordinates)
|
|
764
|
+
)
|
|
765
|
+
}
|
|
766
|
+
const sourceSpanDifference =
|
|
767
|
+
getSourceSpan(second) - getSourceSpan(first)
|
|
768
|
+
if (Math.abs(sourceSpanDifference) > 1e-9) {
|
|
769
|
+
return sourceSpanDifference
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const firstLayer = params.busLayerAssignments[first.busId]
|
|
773
|
+
const secondLayer = params.busLayerAssignments[second.busId]
|
|
774
|
+
const layerDifference =
|
|
775
|
+
this.config.layerNames.indexOf(firstLayer ?? "") -
|
|
776
|
+
this.config.layerNames.indexOf(secondLayer ?? "")
|
|
777
|
+
return -layerDifference
|
|
778
|
+
})
|
|
729
779
|
// Preserve the caller/input order for the dense singleton fill. The
|
|
730
780
|
// general routing sort is useful for heterogeneous buses, but ordering a
|
|
731
781
|
// regular BGA power field by obstacle depth creates artificial local
|
|
@@ -763,119 +813,372 @@ export class FanoutSolver extends BaseSolver {
|
|
|
763
813
|
getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
|
|
764
814
|
]),
|
|
765
815
|
)
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
816
|
+
const canShareCopper = (
|
|
817
|
+
firstConnectionIndex: number,
|
|
818
|
+
secondConnectionIndex: number,
|
|
819
|
+
): boolean => {
|
|
820
|
+
if (!this.config.allowSameNetMerges) return false
|
|
821
|
+
const firstConnectionName =
|
|
822
|
+
connectionNameByIndex.get(firstConnectionIndex)
|
|
823
|
+
const secondConnectionName = connectionNameByIndex.get(
|
|
824
|
+
secondConnectionIndex,
|
|
825
|
+
)
|
|
826
|
+
return Boolean(
|
|
827
|
+
firstConnectionName &&
|
|
828
|
+
secondConnectionName &&
|
|
829
|
+
connectionsShareElectricalNet(
|
|
830
|
+
this.routingSrj,
|
|
831
|
+
firstConnectionName,
|
|
832
|
+
secondConnectionName,
|
|
833
|
+
),
|
|
834
|
+
)
|
|
835
|
+
}
|
|
836
|
+
// Four boundary buses leave too little slack for incremental site
|
|
837
|
+
// allocation: a valid early trace can consume the last dogbone site of a
|
|
838
|
+
// later narrow bus. Reserve every physical barrel before routing copper.
|
|
839
|
+
// Plane sites remain provisional and are rematched around the completed
|
|
840
|
+
// boundary plans below.
|
|
841
|
+
const jointViaPoints = useJointFourBusReservation
|
|
842
|
+
? matchComponentDogboneViaSites([...planeBuses, ...boundaryBuses], {
|
|
770
843
|
viaDiameter: this.config.viaDiameter,
|
|
771
844
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
772
845
|
traceWidth: this.config.traceWidth,
|
|
773
846
|
clearance: this.config.clearance,
|
|
774
|
-
maximumSearchStates:
|
|
847
|
+
maximumSearchStates: 100_000,
|
|
775
848
|
preferredBoundaryPerpendicularSideByBusId,
|
|
776
849
|
preferBoundaryOutwardByBusId,
|
|
777
|
-
canShareCopper
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
850
|
+
canShareCopper,
|
|
851
|
+
})
|
|
852
|
+
: null
|
|
853
|
+
const seedViaPoints =
|
|
854
|
+
jointViaPoints ??
|
|
855
|
+
matchComponentDogboneViaSites([...planeBuses, boundaryBuses[0]!], {
|
|
856
|
+
viaDiameter: this.config.viaDiameter,
|
|
857
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
858
|
+
traceWidth: this.config.traceWidth,
|
|
859
|
+
clearance: this.config.clearance,
|
|
860
|
+
maximumSearchStates: 20_000,
|
|
861
|
+
preferredBoundaryPerpendicularSideByBusId,
|
|
862
|
+
preferBoundaryOutwardByBusId,
|
|
863
|
+
canShareCopper,
|
|
864
|
+
})
|
|
865
|
+
if (seedViaPoints) {
|
|
866
|
+
let fixedViaPointsByConnectionIndex: ReadonlyMap<
|
|
867
|
+
number,
|
|
868
|
+
{ x: number; y: number }
|
|
869
|
+
> = seedViaPoints
|
|
870
|
+
let matchedPlans: FanoutRoutePlan[] = []
|
|
871
|
+
let matchedRoutingSucceeded = true
|
|
872
|
+
const getReservedVias = (bus: PreparedBus) => {
|
|
873
|
+
const currentConnectionNames = new Set(
|
|
874
|
+
bus.connections.map((connection) => connection.connection.name),
|
|
875
|
+
)
|
|
876
|
+
return this.preparedBuses.flatMap((preparedBus) => {
|
|
877
|
+
const targetLayer = params.busLayerAssignments[preparedBus.busId]
|
|
878
|
+
if (!targetLayer) return []
|
|
879
|
+
return preparedBus.connections.flatMap((connection) => {
|
|
880
|
+
if (currentConnectionNames.has(connection.connection.name))
|
|
881
|
+
return []
|
|
882
|
+
const center = fixedViaPointsByConnectionIndex.get(
|
|
883
|
+
connection.connectionIndex,
|
|
792
884
|
)
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
>()
|
|
810
|
-
for (const bus of this.preparedBuses) {
|
|
811
|
-
const targetLayer = params.busLayerAssignments[bus.busId]
|
|
812
|
-
if (!targetLayer) continue
|
|
813
|
-
const componentReservedVias =
|
|
814
|
-
reservedViasByComponentId.get(bus.componentId) ?? []
|
|
815
|
-
for (const connection of bus.connections) {
|
|
816
|
-
const center = fixedViaPointsByConnectionIndex.get(
|
|
817
|
-
connection.connectionIndex,
|
|
818
|
-
)
|
|
819
|
-
if (!center) continue
|
|
820
|
-
componentReservedVias.push({
|
|
821
|
-
connectionName: connection.connection.name,
|
|
822
|
-
via: {
|
|
823
|
-
center,
|
|
824
|
-
diameter: this.config.viaDiameter,
|
|
825
|
-
spanLayers: getViaSpanLayers({
|
|
826
|
-
fromLayer: connection.sourceLayer,
|
|
827
|
-
toLayer: targetLayer,
|
|
828
|
-
layerNames: this.config.layerNames,
|
|
829
|
-
allowBlindAndBuriedVias: false,
|
|
830
|
-
}),
|
|
831
|
-
},
|
|
885
|
+
if (!center) return []
|
|
886
|
+
return [
|
|
887
|
+
{
|
|
888
|
+
connectionName: connection.connection.name,
|
|
889
|
+
via: {
|
|
890
|
+
center,
|
|
891
|
+
diameter: this.config.viaDiameter,
|
|
892
|
+
spanLayers: getViaSpanLayers({
|
|
893
|
+
fromLayer: connection.sourceLayer,
|
|
894
|
+
toLayer: targetLayer,
|
|
895
|
+
layerNames: this.config.layerNames,
|
|
896
|
+
allowBlindAndBuriedVias: false,
|
|
897
|
+
}),
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
]
|
|
832
901
|
})
|
|
833
|
-
}
|
|
834
|
-
reservedViasByComponentId.set(bus.componentId, componentReservedVias)
|
|
902
|
+
})
|
|
835
903
|
}
|
|
836
|
-
|
|
837
|
-
const matchedPlans: FanoutRoutePlan[] = []
|
|
838
|
-
let matchedRoutingSucceeded = true
|
|
839
|
-
for (const bus of boundaryBuses) {
|
|
904
|
+
const routeMatchedBoundaryBus = (bus: PreparedBus): boolean => {
|
|
840
905
|
const targetLayer = params.busLayerAssignments[bus.busId]
|
|
841
906
|
if (!targetLayer) {
|
|
907
|
+
return false
|
|
908
|
+
}
|
|
909
|
+
const routeParams = {
|
|
910
|
+
srj: this.routingSrj,
|
|
911
|
+
bus,
|
|
912
|
+
targetLayer,
|
|
913
|
+
acceptedPlans: matchedPlans,
|
|
914
|
+
layerNames: this.config.layerNames,
|
|
915
|
+
traceWidth: this.config.traceWidth,
|
|
916
|
+
viaDiameter: this.config.viaDiameter,
|
|
917
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
918
|
+
clearance: this.config.clearance,
|
|
919
|
+
compactBusTracks: this.config.compactBusTracks,
|
|
920
|
+
allowBlindAndBuriedVias: false,
|
|
921
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
922
|
+
staticClearanceCache: this.routeStaticClearanceCache,
|
|
923
|
+
fixedViaPointsByConnectionIndex,
|
|
924
|
+
reservedVias: getReservedVias(bus),
|
|
925
|
+
viaMinimalOnly: true,
|
|
926
|
+
} as const
|
|
927
|
+
let busPlans = routeBusAlternatives(routeParams, 1)[0]
|
|
928
|
+
if (busPlans && bus.maxLengthSkew !== undefined) {
|
|
929
|
+
const lengths = busPlans.map((plan) => plan.length)
|
|
930
|
+
const rawSkew = Math.max(...lengths) - Math.min(...lengths)
|
|
931
|
+
const needsRouteDiversity =
|
|
932
|
+
rawSkew - bus.maxLengthSkew > Math.max(1, bus.maxLengthSkew * 0.25)
|
|
933
|
+
// Only pay for additional A* variants when the first topology is so
|
|
934
|
+
// skewed that compact meanders are unlikely to absorb the deficit.
|
|
935
|
+
// This keeps already-near-matched buses on the single-attempt path.
|
|
936
|
+
if (needsRouteDiversity) {
|
|
937
|
+
busPlans = routeBusAlternatives(routeParams, 3).toSorted(
|
|
938
|
+
(first, second) => {
|
|
939
|
+
const firstLengths = first.map((plan) => plan.length)
|
|
940
|
+
const secondLengths = second.map((plan) => plan.length)
|
|
941
|
+
return (
|
|
942
|
+
Math.max(...firstLengths) -
|
|
943
|
+
Math.min(...firstLengths) -
|
|
944
|
+
(Math.max(...secondLengths) - Math.min(...secondLengths))
|
|
945
|
+
)
|
|
946
|
+
},
|
|
947
|
+
)[0]
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (!busPlans) {
|
|
951
|
+
return false
|
|
952
|
+
}
|
|
953
|
+
matchedPlans.push(...busPlans)
|
|
954
|
+
return true
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const firstBoundaryBus = boundaryBuses[0]!
|
|
958
|
+
const routedBoundaryBuses: PreparedBus[] = []
|
|
959
|
+
if (routeMatchedBoundaryBus(firstBoundaryBus)) {
|
|
960
|
+
routedBoundaryBuses.push(firstBoundaryBus)
|
|
961
|
+
} else {
|
|
962
|
+
matchedRoutingSucceeded = false
|
|
963
|
+
}
|
|
964
|
+
const remainingBoundaryBuses = boundaryBuses.slice(1)
|
|
965
|
+
while (matchedRoutingSucceeded && remainingBoundaryBuses.length > 0) {
|
|
966
|
+
const blockingSegments = matchedPlans.flatMap((plan) =>
|
|
967
|
+
plan.segments.map((segment) => ({
|
|
968
|
+
connectionIndex: plan.connectionIndex,
|
|
969
|
+
segment,
|
|
970
|
+
})),
|
|
971
|
+
)
|
|
972
|
+
let selectedBusIndex = -1
|
|
973
|
+
for (
|
|
974
|
+
let candidateIndex = 0;
|
|
975
|
+
candidateIndex < remainingBoundaryBuses.length;
|
|
976
|
+
candidateIndex++
|
|
977
|
+
) {
|
|
978
|
+
const candidateBus = remainingBoundaryBuses[candidateIndex]!
|
|
979
|
+
const extendedViaPoints = jointViaPoints
|
|
980
|
+
? // The joint map is deliberately kept intact so getReservedVias()
|
|
981
|
+
// blocks every future through-barrel during A*. Plane dogbones are
|
|
982
|
+
// validated/rematched once the boundary copper is complete.
|
|
983
|
+
new Map(fixedViaPointsByConnectionIndex)
|
|
984
|
+
: matchComponentDogboneViaSites(
|
|
985
|
+
[...planeBuses, ...routedBoundaryBuses, candidateBus],
|
|
986
|
+
{
|
|
987
|
+
viaDiameter: this.config.viaDiameter,
|
|
988
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
989
|
+
traceWidth: this.config.traceWidth,
|
|
990
|
+
clearance: this.config.clearance,
|
|
991
|
+
maximumSearchStates: 100_000,
|
|
992
|
+
preferredBoundaryPerpendicularSideByBusId,
|
|
993
|
+
preferBoundaryOutwardByBusId,
|
|
994
|
+
fixedViaPointsByConnectionIndex:
|
|
995
|
+
fixedViaPointsByConnectionIndex,
|
|
996
|
+
blockingSegments,
|
|
997
|
+
canShareCopper,
|
|
998
|
+
},
|
|
999
|
+
)
|
|
1000
|
+
if (!extendedViaPoints) continue
|
|
1001
|
+
const previousFixedViaPoints = fixedViaPointsByConnectionIndex
|
|
1002
|
+
const previousPlanCount = matchedPlans.length
|
|
1003
|
+
const laterBuses = remainingBoundaryBuses.filter(
|
|
1004
|
+
(_, laterIndex) => laterIndex !== candidateIndex,
|
|
1005
|
+
)
|
|
1006
|
+
let candidateFixedViaPoints: ReadonlyMap<
|
|
1007
|
+
number,
|
|
1008
|
+
{ x: number; y: number }
|
|
1009
|
+
> = extendedViaPoints
|
|
1010
|
+
if (laterBuses.length === 1) {
|
|
1011
|
+
const laterBus = laterBuses[0]!
|
|
1012
|
+
const futureAssignment = matchComponentDogboneViaSites(
|
|
1013
|
+
[...planeBuses, ...routedBoundaryBuses, candidateBus, laterBus],
|
|
1014
|
+
{
|
|
1015
|
+
viaDiameter: this.config.viaDiameter,
|
|
1016
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
1017
|
+
traceWidth: this.config.traceWidth,
|
|
1018
|
+
clearance: this.config.clearance,
|
|
1019
|
+
maximumSearchStates: 100_000,
|
|
1020
|
+
preferredBoundaryPerpendicularSideByBusId,
|
|
1021
|
+
preferBoundaryOutwardByBusId,
|
|
1022
|
+
fixedViaPointsByConnectionIndex: extendedViaPoints,
|
|
1023
|
+
blockingSegments,
|
|
1024
|
+
canShareCopper,
|
|
1025
|
+
},
|
|
1026
|
+
)
|
|
1027
|
+
if (futureAssignment) {
|
|
1028
|
+
const candidateCountByConnectionIndex = new Map<number, number>()
|
|
1029
|
+
for (const candidate of getComponentDogboneViaSiteCandidates(
|
|
1030
|
+
[laterBus],
|
|
1031
|
+
{
|
|
1032
|
+
viaDiameter: this.config.viaDiameter,
|
|
1033
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
1034
|
+
traceWidth: this.config.traceWidth,
|
|
1035
|
+
clearance: this.config.clearance,
|
|
1036
|
+
blockingSegments,
|
|
1037
|
+
canShareCopper,
|
|
1038
|
+
},
|
|
1039
|
+
)) {
|
|
1040
|
+
candidateCountByConnectionIndex.set(
|
|
1041
|
+
candidate.connectionIndex,
|
|
1042
|
+
(candidateCountByConnectionIndex.get(
|
|
1043
|
+
candidate.connectionIndex,
|
|
1044
|
+
) ?? 0) + 1,
|
|
1045
|
+
)
|
|
1046
|
+
}
|
|
1047
|
+
const constrainedConnections = laterBus.connections.toSorted(
|
|
1048
|
+
(first, second) =>
|
|
1049
|
+
(candidateCountByConnectionIndex.get(first.connectionIndex) ??
|
|
1050
|
+
0) -
|
|
1051
|
+
(candidateCountByConnectionIndex.get(
|
|
1052
|
+
second.connectionIndex,
|
|
1053
|
+
) ?? 0) || first.connectionIndex - second.connectionIndex,
|
|
1054
|
+
)
|
|
1055
|
+
const repairedViaPoints = new Map(extendedViaPoints)
|
|
1056
|
+
for (const connection of constrainedConnections) {
|
|
1057
|
+
const criticalPoint = futureAssignment.get(
|
|
1058
|
+
connection.connectionIndex,
|
|
1059
|
+
)
|
|
1060
|
+
if (criticalPoint) {
|
|
1061
|
+
repairedViaPoints.set(
|
|
1062
|
+
connection.connectionIndex,
|
|
1063
|
+
criticalPoint,
|
|
1064
|
+
)
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
candidateFixedViaPoints = repairedViaPoints
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
fixedViaPointsByConnectionIndex = candidateFixedViaPoints
|
|
1071
|
+
if (routeMatchedBoundaryBus(candidateBus)) {
|
|
1072
|
+
const candidateLeavesAFeasibleExtension =
|
|
1073
|
+
laterBuses.length === 0 ||
|
|
1074
|
+
laterBuses.some((laterBus) => {
|
|
1075
|
+
const lookaheadBlockingSegments = matchedPlans.flatMap((plan) =>
|
|
1076
|
+
plan.segments.map((segment) => ({
|
|
1077
|
+
connectionIndex: plan.connectionIndex,
|
|
1078
|
+
segment,
|
|
1079
|
+
})),
|
|
1080
|
+
)
|
|
1081
|
+
return Boolean(
|
|
1082
|
+
matchComponentDogboneViaSites(
|
|
1083
|
+
[
|
|
1084
|
+
...planeBuses,
|
|
1085
|
+
...routedBoundaryBuses,
|
|
1086
|
+
candidateBus,
|
|
1087
|
+
laterBus,
|
|
1088
|
+
],
|
|
1089
|
+
{
|
|
1090
|
+
viaDiameter: this.config.viaDiameter,
|
|
1091
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
1092
|
+
traceWidth: this.config.traceWidth,
|
|
1093
|
+
clearance: this.config.clearance,
|
|
1094
|
+
maximumSearchStates: 100_000,
|
|
1095
|
+
preferredBoundaryPerpendicularSideByBusId,
|
|
1096
|
+
preferBoundaryOutwardByBusId,
|
|
1097
|
+
fixedViaPointsByConnectionIndex:
|
|
1098
|
+
fixedViaPointsByConnectionIndex,
|
|
1099
|
+
blockingSegments: lookaheadBlockingSegments,
|
|
1100
|
+
canShareCopper,
|
|
1101
|
+
},
|
|
1102
|
+
),
|
|
1103
|
+
)
|
|
1104
|
+
})
|
|
1105
|
+
if (candidateLeavesAFeasibleExtension) {
|
|
1106
|
+
selectedBusIndex = candidateIndex
|
|
1107
|
+
routedBoundaryBuses.push(candidateBus)
|
|
1108
|
+
break
|
|
1109
|
+
}
|
|
1110
|
+
matchedPlans.splice(previousPlanCount)
|
|
1111
|
+
}
|
|
1112
|
+
fixedViaPointsByConnectionIndex = previousFixedViaPoints
|
|
1113
|
+
}
|
|
1114
|
+
if (selectedBusIndex < 0) {
|
|
842
1115
|
matchedRoutingSucceeded = false
|
|
843
1116
|
break
|
|
844
1117
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
)
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1118
|
+
remainingBoundaryBuses.splice(selectedBusIndex, 1)
|
|
1119
|
+
}
|
|
1120
|
+
if (matchedRoutingSucceeded) {
|
|
1121
|
+
let feasibleViaPoints: Map<number, { x: number; y: number }> | null =
|
|
1122
|
+
null
|
|
1123
|
+
const matchViaPointsAroundPlans = (
|
|
1124
|
+
candidatePlans: readonly FanoutRoutePlan[],
|
|
1125
|
+
): Map<number, { x: number; y: number }> | null => {
|
|
1126
|
+
const fixedBoundaryViaPoints = new Map(
|
|
1127
|
+
candidatePlans.flatMap((plan) =>
|
|
1128
|
+
plan.via
|
|
1129
|
+
? [[plan.connectionIndex, plan.via.center] as const]
|
|
1130
|
+
: [],
|
|
1131
|
+
),
|
|
1132
|
+
)
|
|
1133
|
+
return matchComponentDogboneViaSites(
|
|
1134
|
+
[...planeBuses, ...boundaryBuses],
|
|
1135
|
+
{
|
|
1136
|
+
viaDiameter: this.config.viaDiameter,
|
|
1137
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
1138
|
+
traceWidth: this.config.traceWidth,
|
|
1139
|
+
clearance: this.config.clearance,
|
|
1140
|
+
maximumSearchStates: 100_000,
|
|
1141
|
+
preferredBoundaryPerpendicularSideByBusId,
|
|
1142
|
+
preferBoundaryOutwardByBusId,
|
|
1143
|
+
fixedViaPointsByConnectionIndex: fixedBoundaryViaPoints,
|
|
1144
|
+
blockingSegments: candidatePlans.flatMap((plan) =>
|
|
1145
|
+
plan.segments.map((segment) => ({
|
|
1146
|
+
connectionIndex: plan.connectionIndex,
|
|
1147
|
+
segment,
|
|
1148
|
+
})),
|
|
1149
|
+
),
|
|
1150
|
+
canShareCopper,
|
|
1151
|
+
},
|
|
1152
|
+
)
|
|
1153
|
+
}
|
|
1154
|
+
const matchedLengthResult = matchBusPlanLengths({
|
|
1155
|
+
plans: matchedPlans,
|
|
1156
|
+
preparedBuses: this.preparedBuses,
|
|
1157
|
+
inputSrj: this.inputSrj,
|
|
1158
|
+
sharedBoundary: this.getValidationBoundary(),
|
|
1159
|
+
clearance: this.config.clearance,
|
|
1160
|
+
allowBlindAndBuriedVias: false,
|
|
1161
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
1162
|
+
allowMatchingInsideDenseBounds: true,
|
|
1163
|
+
candidatePlansAreFeasible: (candidatePlans) => {
|
|
1164
|
+
const candidateViaPoints = matchViaPointsAroundPlans(candidatePlans)
|
|
1165
|
+
if (!candidateViaPoints) return false
|
|
1166
|
+
feasibleViaPoints = candidateViaPoints
|
|
1167
|
+
return true
|
|
871
1168
|
},
|
|
872
|
-
|
|
873
|
-
)
|
|
874
|
-
|
|
1169
|
+
})
|
|
1170
|
+
if (matchedLengthResult.plans) {
|
|
1171
|
+
matchedPlans = matchedLengthResult.plans
|
|
1172
|
+
const rematchedViaPoints =
|
|
1173
|
+
feasibleViaPoints ?? matchViaPointsAroundPlans(matchedPlans)
|
|
1174
|
+
if (rematchedViaPoints) {
|
|
1175
|
+
fixedViaPointsByConnectionIndex = rematchedViaPoints
|
|
1176
|
+
} else {
|
|
1177
|
+
matchedRoutingSucceeded = false
|
|
1178
|
+
}
|
|
1179
|
+
} else {
|
|
875
1180
|
matchedRoutingSucceeded = false
|
|
876
|
-
break
|
|
877
1181
|
}
|
|
878
|
-
matchedPlans.push(...busPlans)
|
|
879
1182
|
}
|
|
880
1183
|
if (matchedRoutingSucceeded) {
|
|
881
1184
|
for (const bus of planeBuses) {
|
package/lib/match-bus-lengths.ts
CHANGED
|
@@ -448,6 +448,7 @@ function createTunedPlanCandidates(params: {
|
|
|
448
448
|
targetAddedLength: number
|
|
449
449
|
clearance: number
|
|
450
450
|
sharedBoundary: Bounds
|
|
451
|
+
allowInsideDenseBounds?: boolean
|
|
451
452
|
denseBoundarySplitApplied?: boolean
|
|
452
453
|
}): FanoutRoutePlan[] {
|
|
453
454
|
const {
|
|
@@ -456,6 +457,7 @@ function createTunedPlanCandidates(params: {
|
|
|
456
457
|
targetAddedLength,
|
|
457
458
|
clearance,
|
|
458
459
|
sharedBoundary,
|
|
460
|
+
allowInsideDenseBounds = false,
|
|
459
461
|
denseBoundarySplitApplied = false,
|
|
460
462
|
} = params
|
|
461
463
|
const candidates: FanoutRoutePlan[] = []
|
|
@@ -497,6 +499,7 @@ function createTunedPlanCandidates(params: {
|
|
|
497
499
|
continue
|
|
498
500
|
}
|
|
499
501
|
if (
|
|
502
|
+
!allowInsideDenseBounds &&
|
|
500
503
|
points
|
|
501
504
|
.slice(1, -1)
|
|
502
505
|
.some(
|
|
@@ -575,6 +578,17 @@ export function matchBusPlanLengths(params: {
|
|
|
575
578
|
clearance: number
|
|
576
579
|
allowBlindAndBuriedVias?: boolean
|
|
577
580
|
allowSameNetMerges?: boolean
|
|
581
|
+
/**
|
|
582
|
+
* Allows tuning on target-layer copper inside the component pad envelope.
|
|
583
|
+
* Intended only for coordinated dense routing whose complete copper is
|
|
584
|
+
* revalidated and whose remaining dogbone capacity is checked atomically.
|
|
585
|
+
*/
|
|
586
|
+
allowMatchingInsideDenseBounds?: boolean
|
|
587
|
+
/**
|
|
588
|
+
* Rejects a geometrically clear candidate when it would make a caller-owned
|
|
589
|
+
* downstream assignment (such as pending plane dogbones) infeasible.
|
|
590
|
+
*/
|
|
591
|
+
candidatePlansAreFeasible?: (plans: readonly FanoutRoutePlan[]) => boolean
|
|
578
592
|
}):
|
|
579
593
|
| { plans: FanoutRoutePlan[]; failedBus?: never }
|
|
580
594
|
| { plans: null; failedBus: PreparedBus } {
|
|
@@ -585,6 +599,8 @@ export function matchBusPlanLengths(params: {
|
|
|
585
599
|
clearance,
|
|
586
600
|
allowBlindAndBuriedVias = true,
|
|
587
601
|
allowSameNetMerges = false,
|
|
602
|
+
allowMatchingInsideDenseBounds = false,
|
|
603
|
+
candidatePlansAreFeasible,
|
|
588
604
|
} = params
|
|
589
605
|
let matchedPlans = [...params.plans]
|
|
590
606
|
const constrainedBuses = preparedBuses.filter(
|
|
@@ -633,6 +649,95 @@ export function matchBusPlanLengths(params: {
|
|
|
633
649
|
)
|
|
634
650
|
.toSorted((first, second) => first - second)
|
|
635
651
|
let acceptedPlans: FanoutRoutePlan[] | null = null
|
|
652
|
+
const acceptCandidate = (
|
|
653
|
+
candidate: FanoutRoutePlan,
|
|
654
|
+
): FanoutRoutePlan[] | null => {
|
|
655
|
+
const nextPlans = matchedPlans.map((plan) =>
|
|
656
|
+
plan === shortest ? candidate : plan,
|
|
657
|
+
)
|
|
658
|
+
const nextBusPlans = nextPlans.filter(
|
|
659
|
+
(plan) => plan.busId === bus.busId,
|
|
660
|
+
)
|
|
661
|
+
if (getBusSkew(nextBusPlans) > skew + EPSILON) return null
|
|
662
|
+
if (
|
|
663
|
+
!fanoutPlansAreClear({
|
|
664
|
+
plans: nextPlans,
|
|
665
|
+
srj: inputSrj,
|
|
666
|
+
sharedBoundary,
|
|
667
|
+
clearance,
|
|
668
|
+
allowBlindAndBuriedVias,
|
|
669
|
+
allowSameNetMerges,
|
|
670
|
+
})
|
|
671
|
+
) {
|
|
672
|
+
return null
|
|
673
|
+
}
|
|
674
|
+
if (
|
|
675
|
+
candidatePlansAreFeasible &&
|
|
676
|
+
!candidatePlansAreFeasible(nextPlans)
|
|
677
|
+
) {
|
|
678
|
+
return null
|
|
679
|
+
}
|
|
680
|
+
return nextPlans
|
|
681
|
+
}
|
|
682
|
+
const findMultiSpanCandidate = (
|
|
683
|
+
targetAddedLength: number,
|
|
684
|
+
): FanoutRoutePlan[] | null => {
|
|
685
|
+
const maximumSearchStates = 320
|
|
686
|
+
const maximumCandidatesPerState = 48
|
|
687
|
+
let searchedStateCount = 0
|
|
688
|
+
const sampleCandidates = (
|
|
689
|
+
candidates: readonly FanoutRoutePlan[],
|
|
690
|
+
): FanoutRoutePlan[] => {
|
|
691
|
+
if (candidates.length <= maximumCandidatesPerState) {
|
|
692
|
+
return [...candidates]
|
|
693
|
+
}
|
|
694
|
+
return Array.from(
|
|
695
|
+
{ length: maximumCandidatesPerState },
|
|
696
|
+
(_, sampleIndex) =>
|
|
697
|
+
candidates[
|
|
698
|
+
Math.floor(
|
|
699
|
+
(sampleIndex * candidates.length) / maximumCandidatesPerState,
|
|
700
|
+
)
|
|
701
|
+
]!,
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
const search = (
|
|
705
|
+
currentPlan: FanoutRoutePlan,
|
|
706
|
+
stagesRemaining: number,
|
|
707
|
+
): FanoutRoutePlan[] | null => {
|
|
708
|
+
const addedLength = currentPlan.length - shortest.length
|
|
709
|
+
const remainingAddition = targetAddedLength - addedLength
|
|
710
|
+
if (remainingAddition <= EPSILON) {
|
|
711
|
+
return acceptCandidate(currentPlan)
|
|
712
|
+
}
|
|
713
|
+
if (stagesRemaining <= 0) return null
|
|
714
|
+
const stageAddedLength = remainingAddition / stagesRemaining
|
|
715
|
+
const candidates = sampleCandidates(
|
|
716
|
+
createTunedPlanCandidates({
|
|
717
|
+
plan: currentPlan,
|
|
718
|
+
bus,
|
|
719
|
+
targetAddedLength: stageAddedLength,
|
|
720
|
+
clearance,
|
|
721
|
+
sharedBoundary: bus.sharedBoundary,
|
|
722
|
+
allowInsideDenseBounds: allowMatchingInsideDenseBounds,
|
|
723
|
+
}),
|
|
724
|
+
)
|
|
725
|
+
for (const candidate of candidates) {
|
|
726
|
+
searchedStateCount++
|
|
727
|
+
if (searchedStateCount > maximumSearchStates) return null
|
|
728
|
+
if (!acceptCandidate(candidate)) continue
|
|
729
|
+
const result = search(candidate, stagesRemaining - 1)
|
|
730
|
+
if (result) return result
|
|
731
|
+
}
|
|
732
|
+
return null
|
|
733
|
+
}
|
|
734
|
+
for (let stageCount = 2; stageCount <= 8; stageCount++) {
|
|
735
|
+
const result = search(shortest, stageCount)
|
|
736
|
+
if (result) return result
|
|
737
|
+
if (searchedStateCount > maximumSearchStates) break
|
|
738
|
+
}
|
|
739
|
+
return null
|
|
740
|
+
}
|
|
636
741
|
for (const targetAddedLength of targetAddedLengths) {
|
|
637
742
|
const candidates = createTunedPlanCandidates({
|
|
638
743
|
plan: shortest,
|
|
@@ -640,31 +745,19 @@ export function matchBusPlanLengths(params: {
|
|
|
640
745
|
targetAddedLength,
|
|
641
746
|
clearance,
|
|
642
747
|
sharedBoundary: bus.sharedBoundary,
|
|
748
|
+
allowInsideDenseBounds: allowMatchingInsideDenseBounds,
|
|
643
749
|
})
|
|
644
750
|
for (const candidate of candidates) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
)
|
|
648
|
-
const nextBusPlans = nextPlans.filter(
|
|
649
|
-
(plan) => plan.busId === bus.busId,
|
|
650
|
-
)
|
|
651
|
-
const nextSkew = getBusSkew(nextBusPlans)
|
|
652
|
-
if (nextSkew > skew + EPSILON) continue
|
|
653
|
-
if (
|
|
654
|
-
!fanoutPlansAreClear({
|
|
655
|
-
plans: nextPlans,
|
|
656
|
-
srj: inputSrj,
|
|
657
|
-
sharedBoundary,
|
|
658
|
-
clearance,
|
|
659
|
-
allowBlindAndBuriedVias,
|
|
660
|
-
allowSameNetMerges,
|
|
661
|
-
})
|
|
662
|
-
) {
|
|
663
|
-
continue
|
|
664
|
-
}
|
|
665
|
-
acceptedPlans = nextPlans
|
|
751
|
+
acceptedPlans = acceptCandidate(candidate)
|
|
752
|
+
if (!acceptedPlans) continue
|
|
666
753
|
break
|
|
667
754
|
}
|
|
755
|
+
if (
|
|
756
|
+
!acceptedPlans &&
|
|
757
|
+
Math.abs(targetAddedLength - minimumRequiredAddition) <= EPSILON
|
|
758
|
+
) {
|
|
759
|
+
acceptedPlans = findMultiSpanCandidate(targetAddedLength)
|
|
760
|
+
}
|
|
668
761
|
if (acceptedPlans) break
|
|
669
762
|
}
|
|
670
763
|
if (!acceptedPlans) return { plans: null, failedBus: bus }
|
|
@@ -33,6 +33,13 @@ export interface DogboneViaSiteGeometryRules {
|
|
|
33
33
|
preferredBoundaryPerpendicularSideByBusId?: ReadonlyMap<string, -1 | 1>
|
|
34
34
|
/** Prefer the local outward or inward half-pitch row for a boundary bus. */
|
|
35
35
|
preferBoundaryOutwardByBusId?: ReadonlyMap<string, boolean>
|
|
36
|
+
/** Existing assignments that must be preserved while matching other pads. */
|
|
37
|
+
fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
|
|
38
|
+
/** Routed copper that every newly assigned through-via/dogbone must clear. */
|
|
39
|
+
blockingSegments?: readonly {
|
|
40
|
+
connectionIndex: number
|
|
41
|
+
segment: RoutedSegment
|
|
42
|
+
}[]
|
|
36
43
|
/** True only when the two connections are allowed to merge copper. */
|
|
37
44
|
canShareCopper?: (
|
|
38
45
|
firstConnectionIndex: number,
|
|
@@ -69,6 +76,11 @@ interface ConnectionCandidates {
|
|
|
69
76
|
candidates: ViaSiteCandidate[]
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
export interface ComponentDogboneViaSiteCandidate {
|
|
80
|
+
connectionIndex: number
|
|
81
|
+
point: Point2D
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
function assertGeometryRules(rules: DogboneViaSiteGeometryRules): number {
|
|
73
85
|
for (const [name, value] of [
|
|
74
86
|
["viaDiameter", rules.viaDiameter],
|
|
@@ -340,9 +352,13 @@ function getConnectionCandidates(params: {
|
|
|
340
352
|
uniquePoints.push(point)
|
|
341
353
|
}
|
|
342
354
|
}
|
|
355
|
+
const fixedViaPoint = rules.fixedViaPointsByConnectionIndex?.get(
|
|
356
|
+
preparedConnection.connectionIndex,
|
|
357
|
+
)
|
|
358
|
+
const candidatePoints = fixedViaPoint ? [fixedViaPoint] : uniquePoints
|
|
343
359
|
|
|
344
360
|
const candidates: ViaSiteCandidate[] = []
|
|
345
|
-
for (const point of
|
|
361
|
+
for (const point of candidatePoints) {
|
|
346
362
|
if (
|
|
347
363
|
connection.terminationType === "plane" &&
|
|
348
364
|
!directSegmentIsStraightOr45(source, point)
|
|
@@ -375,6 +391,38 @@ function getConnectionCandidates(params: {
|
|
|
375
391
|
) {
|
|
376
392
|
continue
|
|
377
393
|
}
|
|
394
|
+
const candidateClearsRoutedCopper = (rules.blockingSegments ?? []).every(
|
|
395
|
+
(blocker) => {
|
|
396
|
+
if (blocker.connectionIndex === preparedConnection.connectionIndex) {
|
|
397
|
+
return true
|
|
398
|
+
}
|
|
399
|
+
if (
|
|
400
|
+
rules.canShareCopper?.(
|
|
401
|
+
preparedConnection.connectionIndex,
|
|
402
|
+
blocker.connectionIndex,
|
|
403
|
+
)
|
|
404
|
+
) {
|
|
405
|
+
return true
|
|
406
|
+
}
|
|
407
|
+
const viaToTraceClearance =
|
|
408
|
+
rules.viaDiameter / 2 + blocker.segment.width / 2 + rules.clearance
|
|
409
|
+
if (
|
|
410
|
+
distancePointToSegment(
|
|
411
|
+
point,
|
|
412
|
+
blocker.segment.start,
|
|
413
|
+
blocker.segment.end,
|
|
414
|
+
) <
|
|
415
|
+
viaToTraceClearance - EPSILON
|
|
416
|
+
) {
|
|
417
|
+
return false
|
|
418
|
+
}
|
|
419
|
+
return (
|
|
420
|
+
blocker.segment.layer !== sourceSegment.layer ||
|
|
421
|
+
segmentsAreClear(sourceSegment, blocker.segment, rules.clearance)
|
|
422
|
+
)
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
if (!candidateClearsRoutedCopper) continue
|
|
378
426
|
candidates.push({
|
|
379
427
|
connectionIndex: preparedConnection.connectionIndex,
|
|
380
428
|
point,
|
|
@@ -582,3 +630,26 @@ export function matchComponentDogboneViaSites(
|
|
|
582
630
|
}
|
|
583
631
|
return result
|
|
584
632
|
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Enumerates the same statically legal sites used by the component matcher.
|
|
636
|
+
* This is useful to preserve future dogbone capacity while another bus is
|
|
637
|
+
* being routed; callers must still run the full matcher afterward because
|
|
638
|
+
* these candidates are not mutually assigned.
|
|
639
|
+
*/
|
|
640
|
+
export function getComponentDogboneViaSiteCandidates(
|
|
641
|
+
preparedBuses: readonly PreparedBus[],
|
|
642
|
+
rules: DogboneViaSiteGeometryRules,
|
|
643
|
+
): ComponentDogboneViaSiteCandidate[] {
|
|
644
|
+
assertGeometryRules(rules)
|
|
645
|
+
return getComponentMatchingInputs(preparedBuses).flatMap((component) =>
|
|
646
|
+
component.connections.flatMap((connection) =>
|
|
647
|
+
getConnectionCandidates({ connection, component, rules }).map(
|
|
648
|
+
(candidate) => ({
|
|
649
|
+
connectionIndex: candidate.connectionIndex,
|
|
650
|
+
point: { ...candidate.point },
|
|
651
|
+
}),
|
|
652
|
+
),
|
|
653
|
+
),
|
|
654
|
+
)
|
|
655
|
+
}
|
package/lib/route-bus.ts
CHANGED
|
@@ -131,14 +131,22 @@ function getStableConnectionIdentity(
|
|
|
131
131
|
return connection.name
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
function
|
|
134
|
+
function getWindingTargetOrders(params: {
|
|
135
135
|
bus: PreparedBus
|
|
136
|
-
connection: PreparedConnection
|
|
137
136
|
boundaryDirection: FanoutDirection
|
|
138
137
|
layerNames: readonly string[]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
targetLayer: string
|
|
139
|
+
}): {
|
|
140
|
+
orders: PreparedConnection[][]
|
|
141
|
+
legacyOrder: PreparedConnection[]
|
|
142
|
+
} {
|
|
143
|
+
const { bus, boundaryDirection, layerNames, targetLayer } = params
|
|
144
|
+
const getTargetLayer = (candidate: PreparedConnection): string =>
|
|
145
|
+
candidate.exitTargetPoint?.layer ?? getPointLayer(candidate.targetPoint)
|
|
146
|
+
const compareWithinLayer = (
|
|
147
|
+
first: PreparedConnection,
|
|
148
|
+
second: PreparedConnection,
|
|
149
|
+
): number => {
|
|
142
150
|
const axisDifference =
|
|
143
151
|
getPerpendicularAxis(
|
|
144
152
|
first.exitTargetPoint ?? first.targetPoint,
|
|
@@ -150,28 +158,114 @@ function getWindingTargetRank(params: {
|
|
|
150
158
|
)
|
|
151
159
|
if (axisDifference !== 0) return axisDifference
|
|
152
160
|
|
|
153
|
-
// Equal target coordinates came from distinct target layers and do not
|
|
154
|
-
// impose a physical order after this bus is collapsed onto one escape
|
|
155
|
-
// layer. Keep that free choice independent of allowed-layer ordering so
|
|
156
|
-
// the local fanout can choose a deterministic, via-minimal permutation.
|
|
157
|
-
const identityDifference = first.connection.name.localeCompare(
|
|
158
|
-
second.connection.name,
|
|
159
|
-
)
|
|
160
|
-
if (identityDifference !== 0) return identityDifference
|
|
161
|
-
|
|
162
161
|
const firstStableId = getStableConnectionIdentity(first.connection)
|
|
163
162
|
const secondStableId = getStableConnectionIdentity(second.connection)
|
|
164
163
|
const stableIdentityDifference = firstStableId.localeCompare(secondStableId)
|
|
165
164
|
if (stableIdentityDifference !== 0) return stableIdentityDifference
|
|
166
165
|
|
|
167
|
-
const firstLayer = first.exitTargetPoint?.layer
|
|
168
|
-
const secondLayer = second.exitTargetPoint?.layer
|
|
169
166
|
return (
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
first.connection.name.localeCompare(second.connection.name) ||
|
|
168
|
+
first.connectionIndex - second.connectionIndex
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
const legacyOrderedConnections = bus.connections.toSorted((first, second) => {
|
|
172
|
+
const axisDifference =
|
|
173
|
+
getPerpendicularAxis(
|
|
174
|
+
first.exitTargetPoint ?? first.targetPoint,
|
|
175
|
+
boundaryDirection,
|
|
176
|
+
) -
|
|
177
|
+
getPerpendicularAxis(
|
|
178
|
+
second.exitTargetPoint ?? second.targetPoint,
|
|
179
|
+
boundaryDirection,
|
|
180
|
+
)
|
|
181
|
+
if (axisDifference !== 0) return axisDifference
|
|
182
|
+
return (
|
|
183
|
+
first.connection.name.localeCompare(second.connection.name) ||
|
|
184
|
+
getStableConnectionIdentity(first.connection).localeCompare(
|
|
185
|
+
getStableConnectionIdentity(second.connection),
|
|
186
|
+
) ||
|
|
187
|
+
layerNames.indexOf(getTargetLayer(first)) -
|
|
188
|
+
layerNames.indexOf(getTargetLayer(second)) ||
|
|
172
189
|
first.connectionIndex - second.connectionIndex
|
|
173
190
|
)
|
|
174
191
|
})
|
|
192
|
+
const connectionsByLayer = new Map<string, PreparedConnection[]>()
|
|
193
|
+
for (const candidate of bus.connections) {
|
|
194
|
+
const layer = getTargetLayer(candidate)
|
|
195
|
+
const layerConnections = connectionsByLayer.get(layer) ?? []
|
|
196
|
+
layerConnections.push(candidate)
|
|
197
|
+
connectionsByLayer.set(layer, layerConnections)
|
|
198
|
+
}
|
|
199
|
+
const orderedLayers = [...connectionsByLayer.keys()].toSorted(
|
|
200
|
+
(first, second) =>
|
|
201
|
+
Number(second === targetLayer) - Number(first === targetLayer) ||
|
|
202
|
+
layerNames.indexOf(first) - layerNames.indexOf(second) ||
|
|
203
|
+
first.localeCompare(second),
|
|
204
|
+
)
|
|
205
|
+
const layerOrderByName = new Map(
|
|
206
|
+
orderedLayers.map((layer, layerOrder) => [layer, layerOrder]),
|
|
207
|
+
)
|
|
208
|
+
const rankWithinLayerByConnectionIndex = new Map<number, number>()
|
|
209
|
+
for (const layer of orderedLayers) {
|
|
210
|
+
for (const [rank, candidate] of connectionsByLayer
|
|
211
|
+
.get(layer)!
|
|
212
|
+
.toSorted(compareWithinLayer)
|
|
213
|
+
.entries()) {
|
|
214
|
+
rankWithinLayerByConnectionIndex.set(candidate.connectionIndex, rank)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Winding targets define an order within each copper layer. Their absolute
|
|
218
|
+
// offsets across different layers are not an ordering constraint: unrelated
|
|
219
|
+
// buses can move those layer bands without changing this bus's topology.
|
|
220
|
+
// Build a canonical interleave plus its adjacent linear extensions so a
|
|
221
|
+
// via-minimal search can choose a locally routable merge without violating
|
|
222
|
+
// any same-layer winding order.
|
|
223
|
+
const canonicalOrderedConnections = bus.connections.toSorted(
|
|
224
|
+
(first, second) =>
|
|
225
|
+
(rankWithinLayerByConnectionIndex.get(first.connectionIndex) ?? 0) -
|
|
226
|
+
(rankWithinLayerByConnectionIndex.get(second.connectionIndex) ?? 0) ||
|
|
227
|
+
(layerOrderByName.get(getTargetLayer(first)) ?? 0) -
|
|
228
|
+
(layerOrderByName.get(getTargetLayer(second)) ?? 0) ||
|
|
229
|
+
compareWithinLayer(first, second),
|
|
230
|
+
)
|
|
231
|
+
const candidateOrders: PreparedConnection[][] = [canonicalOrderedConnections]
|
|
232
|
+
for (let index = 0; index + 1 < canonicalOrderedConnections.length; index++) {
|
|
233
|
+
const first = canonicalOrderedConnections[index]!
|
|
234
|
+
const second = canonicalOrderedConnections[index + 1]!
|
|
235
|
+
if (getTargetLayer(first) === getTargetLayer(second)) continue
|
|
236
|
+
const adjacentExtension = [...canonicalOrderedConnections]
|
|
237
|
+
adjacentExtension[index] = second
|
|
238
|
+
adjacentExtension[index + 1] = first
|
|
239
|
+
candidateOrders.push(adjacentExtension)
|
|
240
|
+
}
|
|
241
|
+
// Retain the coordinate-total-order behavior as a compatibility fallback,
|
|
242
|
+
// but do not let sub-nanometer noise between unrelated layer bands choose
|
|
243
|
+
// the primary topology.
|
|
244
|
+
candidateOrders.push(legacyOrderedConnections)
|
|
245
|
+
const seenOrders = new Set<string>()
|
|
246
|
+
const orders = candidateOrders.filter((order) => {
|
|
247
|
+
const key = order.map((candidate) => candidate.connectionIndex).join(",")
|
|
248
|
+
if (seenOrders.has(key)) return false
|
|
249
|
+
seenOrders.add(key)
|
|
250
|
+
return true
|
|
251
|
+
})
|
|
252
|
+
return { orders, legacyOrder: legacyOrderedConnections }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getWindingTargetRank(params: {
|
|
256
|
+
bus: PreparedBus
|
|
257
|
+
connection: PreparedConnection
|
|
258
|
+
boundaryDirection: FanoutDirection
|
|
259
|
+
layerNames: readonly string[]
|
|
260
|
+
targetLayer: string
|
|
261
|
+
windingOrderIndex?: number
|
|
262
|
+
}): { rank: number; connectionCount: number } {
|
|
263
|
+
const { connection, windingOrderIndex = 0 } = params
|
|
264
|
+
const { orders, legacyOrder } = getWindingTargetOrders(params)
|
|
265
|
+
const orderedConnections =
|
|
266
|
+
params.windingOrderIndex === undefined
|
|
267
|
+
? legacyOrder
|
|
268
|
+
: (orders[windingOrderIndex] ?? orders[0] ?? legacyOrder)
|
|
175
269
|
const rank = orderedConnections.findIndex(
|
|
176
270
|
(candidate) => candidate.connectionIndex === connection.connectionIndex,
|
|
177
271
|
)
|
|
@@ -201,6 +295,8 @@ function getCornerTargetTrack(params: {
|
|
|
201
295
|
viaDiameter: number
|
|
202
296
|
clearance: number
|
|
203
297
|
layerNames: readonly string[]
|
|
298
|
+
targetLayer: string
|
|
299
|
+
windingOrderIndex?: number
|
|
204
300
|
}): number {
|
|
205
301
|
const {
|
|
206
302
|
bus,
|
|
@@ -210,6 +306,8 @@ function getCornerTargetTrack(params: {
|
|
|
210
306
|
viaDiameter,
|
|
211
307
|
clearance,
|
|
212
308
|
layerNames,
|
|
309
|
+
targetLayer,
|
|
310
|
+
windingOrderIndex,
|
|
213
311
|
} = params
|
|
214
312
|
const side = getCornerSide(bus)
|
|
215
313
|
if (!side || !bus.exitEdge) {
|
|
@@ -235,6 +333,8 @@ function getCornerTargetTrack(params: {
|
|
|
235
333
|
connection,
|
|
236
334
|
boundaryDirection,
|
|
237
335
|
layerNames,
|
|
336
|
+
targetLayer,
|
|
337
|
+
windingOrderIndex,
|
|
238
338
|
})
|
|
239
339
|
: undefined
|
|
240
340
|
const bandConnectionCount = Math.max(
|
|
@@ -947,6 +1047,7 @@ function buildPlan(params: {
|
|
|
947
1047
|
viaDiameter,
|
|
948
1048
|
clearance,
|
|
949
1049
|
layerNames,
|
|
1050
|
+
targetLayer,
|
|
950
1051
|
})
|
|
951
1052
|
: usesLayeredWindingChannel
|
|
952
1053
|
? getPerpendicularAxis(
|
|
@@ -2376,8 +2477,18 @@ export function routeBusAlternatives(
|
|
|
2376
2477
|
) => ViaHandedness
|
|
2377
2478
|
getViaPoint?: (preparedConnection: PreparedConnection) => Point2D
|
|
2378
2479
|
maximumRouteOrderAttempts?: number
|
|
2480
|
+
windingOrderIndex?: number
|
|
2481
|
+
preferTargetDirectedLaneBias?: boolean
|
|
2379
2482
|
}
|
|
2380
2483
|
const maximumThroughAllRouteOrderAttempts = 24
|
|
2484
|
+
const windingTargetOrderCount = cornerSide
|
|
2485
|
+
? getWindingTargetOrders({
|
|
2486
|
+
bus,
|
|
2487
|
+
boundaryDirection,
|
|
2488
|
+
layerNames,
|
|
2489
|
+
targetLayer,
|
|
2490
|
+
}).orders.length
|
|
2491
|
+
: 1
|
|
2381
2492
|
const uniformDogboneTerminalPatterns: CoordinatedTerminalPattern[] =
|
|
2382
2493
|
viaHandednesses.map((viaHandedness) => ({
|
|
2383
2494
|
label: `uniform-${viaHandedness}`,
|
|
@@ -2519,21 +2630,40 @@ export function routeBusAlternatives(
|
|
|
2519
2630
|
useViaInPad: true,
|
|
2520
2631
|
getViaHandedness: () => 0 as const,
|
|
2521
2632
|
}
|
|
2522
|
-
const
|
|
2633
|
+
const fixedViaTerminalPatterns: CoordinatedTerminalPattern[] =
|
|
2523
2634
|
fixedViaPointsByConnectionIndex
|
|
2524
|
-
?
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2635
|
+
? [
|
|
2636
|
+
...Array.from(
|
|
2637
|
+
{ length: windingTargetOrderCount },
|
|
2638
|
+
(_, windingOrderIndex) => ({
|
|
2639
|
+
label: `component-matched-vias-winding-${windingOrderIndex}`,
|
|
2640
|
+
useViaInPad: false,
|
|
2641
|
+
getViaHandedness: () => 0 as const,
|
|
2642
|
+
getViaPoint: (connection: PreparedConnection) =>
|
|
2643
|
+
fixedViaPointsByConnectionIndex.get(
|
|
2644
|
+
connection.connectionIndex,
|
|
2645
|
+
)!,
|
|
2646
|
+
maximumRouteOrderAttempts: 1,
|
|
2647
|
+
windingOrderIndex,
|
|
2648
|
+
preferTargetDirectedLaneBias: true,
|
|
2649
|
+
}),
|
|
2650
|
+
),
|
|
2651
|
+
{
|
|
2652
|
+
label: "component-matched-vias-fallback",
|
|
2653
|
+
useViaInPad: false,
|
|
2654
|
+
getViaHandedness: () => 0,
|
|
2655
|
+
getViaPoint: (connection) =>
|
|
2656
|
+
fixedViaPointsByConnectionIndex.get(
|
|
2657
|
+
connection.connectionIndex,
|
|
2658
|
+
)!,
|
|
2659
|
+
// Preserve the existing bounded search after every inexpensive
|
|
2660
|
+
// layer-interleave candidate has had one deterministic attempt.
|
|
2661
|
+
maximumRouteOrderAttempts: maximumThroughAllRouteOrderAttempts,
|
|
2662
|
+
windingOrderIndex: 0,
|
|
2663
|
+
preferTargetDirectedLaneBias: true,
|
|
2664
|
+
},
|
|
2665
|
+
]
|
|
2666
|
+
: []
|
|
2537
2667
|
const planeTerminationsAlreadyOccupyTheFanout = acceptedPlans.some(
|
|
2538
2668
|
(plan) => plan.termination.type === "plane",
|
|
2539
2669
|
)
|
|
@@ -2545,8 +2675,8 @@ export function routeBusAlternatives(
|
|
|
2545
2675
|
? [...mixedDogboneTerminalPatterns, ...uniformDogboneTerminalPatterns]
|
|
2546
2676
|
: [...uniformDogboneTerminalPatterns, ...mixedDogboneTerminalPatterns]
|
|
2547
2677
|
const terminalPatterns: CoordinatedTerminalPattern[] =
|
|
2548
|
-
|
|
2549
|
-
?
|
|
2678
|
+
fixedViaTerminalPatterns.length > 0
|
|
2679
|
+
? fixedViaTerminalPatterns
|
|
2550
2680
|
: canUseViaInPadTerminals
|
|
2551
2681
|
? planeTerminationsAlreadyOccupyTheFanout
|
|
2552
2682
|
? [viaInPadTerminalPattern, ...dogboneTerminalPatterns]
|
|
@@ -2566,6 +2696,8 @@ export function routeBusAlternatives(
|
|
|
2566
2696
|
viaDiameter,
|
|
2567
2697
|
clearance,
|
|
2568
2698
|
layerNames,
|
|
2699
|
+
targetLayer,
|
|
2700
|
+
windingOrderIndex: terminalPattern.windingOrderIndex,
|
|
2569
2701
|
})
|
|
2570
2702
|
: getPerpendicularAxis(
|
|
2571
2703
|
preparedConnection.exitTargetPoint ??
|
|
@@ -2597,12 +2729,12 @@ export function routeBusAlternatives(
|
|
|
2597
2729
|
),
|
|
2598
2730
|
}
|
|
2599
2731
|
})
|
|
2600
|
-
const terminalSignature = terminals
|
|
2732
|
+
const terminalSignature = `${terminals
|
|
2601
2733
|
.map(
|
|
2602
2734
|
(terminal) =>
|
|
2603
|
-
`${terminal.connection.connectionIndex}:${terminal.viaPoint.x}:${terminal.viaPoint.y}`,
|
|
2735
|
+
`${terminal.connection.connectionIndex}:${terminal.viaPoint.x}:${terminal.viaPoint.y}:${terminal.exitPoint.x}:${terminal.exitPoint.y}`,
|
|
2604
2736
|
)
|
|
2605
|
-
.join("|")
|
|
2737
|
+
.join("|")}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}`
|
|
2606
2738
|
if (seenTerminalSignatures.has(terminalSignature)) continue
|
|
2607
2739
|
seenTerminalSignatures.add(terminalSignature)
|
|
2608
2740
|
const viaMinimalAlternatives = routeViaMinimalWindingAlternatives(
|
|
@@ -2628,10 +2760,14 @@ export function routeBusAlternatives(
|
|
|
2628
2760
|
traceWidth + clearance
|
|
2629
2761
|
? 2
|
|
2630
2762
|
: 1,
|
|
2763
|
+
preferTargetDirectedLaneBias:
|
|
2764
|
+
terminalPattern.preferTargetDirectedLaneBias,
|
|
2631
2765
|
},
|
|
2632
|
-
|
|
2633
|
-
? Math.min(2, maxAlternatives - alternatives.length)
|
|
2634
|
-
:
|
|
2766
|
+
fixedViaPointsByConnectionIndex && viaMinimalOnly
|
|
2767
|
+
? Math.min(2, Math.max(1, maxAlternatives - alternatives.length))
|
|
2768
|
+
: terminalPattern.maximumRouteOrderAttempts === undefined
|
|
2769
|
+
? Math.min(2, maxAlternatives - alternatives.length)
|
|
2770
|
+
: 2,
|
|
2635
2771
|
)
|
|
2636
2772
|
for (const viaMinimalPlans of viaMinimalAlternatives) {
|
|
2637
2773
|
const combinedPlansAreClear = fanoutPlansAreClear({
|
|
@@ -60,6 +60,8 @@ export interface RouteViaMinimalWindingParams {
|
|
|
60
60
|
reservedVias?: readonly ViaMinimalWindingReservedVia[]
|
|
61
61
|
/** Use a finer uniform grid for narrow channels between reserved vias. */
|
|
62
62
|
gridStepDivisor?: 1 | 2
|
|
63
|
+
/** Bias bounded fixed-site searches toward the remote target band. */
|
|
64
|
+
preferTargetDirectedLaneBias?: boolean
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
interface GridNode {
|
|
@@ -600,6 +602,7 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
600
602
|
maximumRouteOrderAttempts,
|
|
601
603
|
reservedVias = [],
|
|
602
604
|
gridStepDivisor = 1,
|
|
605
|
+
preferTargetDirectedLaneBias = false,
|
|
603
606
|
} = params
|
|
604
607
|
if (
|
|
605
608
|
maximumRouteOrderAttempts !== undefined &&
|
|
@@ -723,7 +726,6 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
723
726
|
}
|
|
724
727
|
return low
|
|
725
728
|
}
|
|
726
|
-
|
|
727
729
|
const segmentIsClear = (params: {
|
|
728
730
|
segment: RoutedSegment
|
|
729
731
|
terminal: ViaMinimalWindingTerminal
|
|
@@ -842,7 +844,7 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
842
844
|
nodeIndex,
|
|
843
845
|
points,
|
|
844
846
|
radialDistance: connectorDistance,
|
|
845
|
-
length:
|
|
847
|
+
length: segments.reduce(
|
|
846
848
|
(total, segment) => total + distance(segment.start, segment.end),
|
|
847
849
|
0,
|
|
848
850
|
),
|
|
@@ -1073,15 +1075,31 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
1073
1075
|
const targetTracks = targetOrderedTerminals.map((terminal) =>
|
|
1074
1076
|
getPerpendicularAxis(terminal.exitPoint, boundaryDirection),
|
|
1075
1077
|
)
|
|
1078
|
+
const meanViaTrack =
|
|
1079
|
+
viaTracks.reduce((sum, track) => sum + track, 0) / viaTracks.length
|
|
1080
|
+
const meanTargetTrack =
|
|
1081
|
+
targetTracks.reduce((sum, track) => sum + track, 0) / targetTracks.length
|
|
1076
1082
|
const viasAreBeforeTargets =
|
|
1077
1083
|
Math.max(...viaTracks) < Math.min(...targetTracks) - EPSILON
|
|
1078
1084
|
const viasAreAfterTargets =
|
|
1079
1085
|
Math.min(...viaTracks) > Math.max(...targetTracks) + EPSILON
|
|
1080
|
-
const laneBiases =
|
|
1081
|
-
?
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1086
|
+
const laneBiases = preferTargetDirectedLaneBias
|
|
1087
|
+
? viasAreBeforeTargets
|
|
1088
|
+
? ([0, 1, -1] as const)
|
|
1089
|
+
: viasAreAfterTargets
|
|
1090
|
+
? ([0, -1, 1] as const)
|
|
1091
|
+
: bus.direction === boundaryDirection &&
|
|
1092
|
+
meanTargetTrack > meanViaTrack + EPSILON
|
|
1093
|
+
? ([1, 0, -1] as const)
|
|
1094
|
+
: bus.direction === boundaryDirection &&
|
|
1095
|
+
meanTargetTrack < meanViaTrack - EPSILON
|
|
1096
|
+
? ([-1, 0, 1] as const)
|
|
1097
|
+
: ([0, 1, -1] as const)
|
|
1098
|
+
: viasAreBeforeTargets
|
|
1099
|
+
? ([1, 0, -1] as const)
|
|
1100
|
+
: viasAreAfterTargets
|
|
1101
|
+
? ([-1, 0, 1] as const)
|
|
1102
|
+
: ([0, 1, -1] as const)
|
|
1085
1103
|
const initialRouteOrderFactories: Array<
|
|
1086
1104
|
() => readonly ViaMinimalWindingTerminal[]
|
|
1087
1105
|
> = []
|
|
@@ -1094,8 +1112,19 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
1094
1112
|
initialRouteOrderFactories.push(() => [...targetOrderedTerminals].reverse())
|
|
1095
1113
|
}
|
|
1096
1114
|
initialRouteOrderFactories.push(
|
|
1097
|
-
(
|
|
1098
|
-
|
|
1115
|
+
...(preferTargetDirectedLaneBias &&
|
|
1116
|
+
bus.direction === boundaryDirection &&
|
|
1117
|
+
meanTargetTrack < meanViaTrack - EPSILON &&
|
|
1118
|
+
!viasAreBeforeTargets &&
|
|
1119
|
+
!viasAreAfterTargets
|
|
1120
|
+
? [
|
|
1121
|
+
() => [...targetOrderedTerminals].reverse(),
|
|
1122
|
+
() => targetOrderedTerminals,
|
|
1123
|
+
]
|
|
1124
|
+
: [
|
|
1125
|
+
() => targetOrderedTerminals,
|
|
1126
|
+
() => [...targetOrderedTerminals].reverse(),
|
|
1127
|
+
]),
|
|
1099
1128
|
() =>
|
|
1100
1129
|
terminals.toSorted(
|
|
1101
1130
|
(first, second) =>
|
|
@@ -1201,7 +1230,9 @@ export function routeViaMinimalWindingAlternatives(
|
|
|
1201
1230
|
if (seenAlternativeKeys.has(alternativeKey)) continue
|
|
1202
1231
|
seenAlternativeKeys.add(alternativeKey)
|
|
1203
1232
|
alternatives.push(plans)
|
|
1204
|
-
if (alternatives.length >= maximumAlternatives)
|
|
1233
|
+
if (alternatives.length >= maximumAlternatives) {
|
|
1234
|
+
return alternatives
|
|
1235
|
+
}
|
|
1205
1236
|
}
|
|
1206
1237
|
}
|
|
1207
1238
|
return alternatives
|