@tscircuit/fanout-solver 0.0.38 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/lib/add-via-layer-metadata.ts +48 -0
- package/lib/complete-original-endpoints.ts +30 -2
- package/lib/fanout-solver.ts +636 -14
- package/lib/geometry.ts +21 -0
- package/lib/get-routed-trace-copper.ts +15 -6
- package/lib/index.ts +3 -0
- package/lib/layer-names.ts +40 -0
- package/lib/match-bus-lengths.ts +124 -14
- package/lib/match-component-dogbone-via-sites.ts +584 -0
- package/lib/route-bus.ts +872 -112
- package/lib/route-via-minimal-winding.ts +390 -74
- package/lib/types.ts +35 -7
- package/lib/validate-fanout-solution.ts +34 -6
- package/lib/validate-routed-copper-drc.ts +47 -8
- package/package.json +1 -1
package/lib/fanout-solver.ts
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
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 {
|
|
4
|
+
import { addViaLayerMetadataToSrj } from "./add-via-layer-metadata"
|
|
5
|
+
import { getCornerBandSide, getExitEdgeForDirection } from "./boundary-exit"
|
|
5
6
|
import { buildOutputSimpleRouteJson } from "./build-output"
|
|
6
7
|
import {
|
|
7
8
|
type CompleteOriginalEndpointsResult,
|
|
8
9
|
completeOriginalEndpoints,
|
|
9
10
|
} from "./complete-original-endpoints"
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
generateLayerAssignments,
|
|
13
|
+
getCopperLayerNames,
|
|
14
|
+
getViaSpanLayers,
|
|
15
|
+
} from "./layer-names"
|
|
11
16
|
import { matchBusPlanLengths } from "./match-bus-lengths"
|
|
17
|
+
import { matchComponentDogboneViaSites } from "./match-component-dogbone-via-sites"
|
|
18
|
+
import { connectionsShareElectricalNet } from "./net-identity"
|
|
12
19
|
import {
|
|
13
20
|
prepareFanoutBuses,
|
|
14
21
|
resolveAvailableBoundaryRegions,
|
|
15
22
|
} from "./prepare-buses"
|
|
16
23
|
import {
|
|
24
|
+
fanoutPlansAreClear,
|
|
17
25
|
type RouteBusStaticClearanceCache,
|
|
18
26
|
routeBus,
|
|
19
27
|
routeBusAlternatives,
|
|
@@ -30,6 +38,7 @@ import type {
|
|
|
30
38
|
FanoutSolverOutput,
|
|
31
39
|
FanoutValidationIssue,
|
|
32
40
|
PreparedBus,
|
|
41
|
+
SimpleRouteJsonWithFanoutPlanes,
|
|
33
42
|
} from "./types"
|
|
34
43
|
import { validateFanoutSolution } from "./validate-fanout-solution"
|
|
35
44
|
import { visualizeSimpleRouteJson } from "./visualize-simple-route-json"
|
|
@@ -40,6 +49,7 @@ interface ResolvedFanoutConfig {
|
|
|
40
49
|
viaHoleDiameter: number
|
|
41
50
|
clearance: number
|
|
42
51
|
compactBusTracks: boolean
|
|
52
|
+
allowBlindAndBuriedVias: boolean
|
|
43
53
|
allowSameNetMerges: boolean
|
|
44
54
|
singleLayerPushAndShove: boolean
|
|
45
55
|
singleLayerAdaptiveExits: boolean
|
|
@@ -59,6 +69,11 @@ interface GroupedBeamState {
|
|
|
59
69
|
plans: FanoutRoutePlan[]
|
|
60
70
|
}
|
|
61
71
|
|
|
72
|
+
interface MixedTerminationState {
|
|
73
|
+
plans: FanoutRoutePlan[]
|
|
74
|
+
failedBusIds: string[]
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
|
|
63
78
|
|
|
64
79
|
function resolvePositiveNumber(label: string, value: number): number {
|
|
@@ -131,6 +146,7 @@ function resolveConfig(
|
|
|
131
146
|
viaHoleDiameter,
|
|
132
147
|
clearance,
|
|
133
148
|
compactBusTracks: options.compactBusTracks ?? false,
|
|
149
|
+
allowBlindAndBuriedVias: options.allowBlindAndBuriedVias ?? true,
|
|
134
150
|
allowSameNetMerges: options.allowSameNetMerges ?? false,
|
|
135
151
|
singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
|
|
136
152
|
singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
|
|
@@ -216,6 +232,17 @@ function getLayerLoadPenaltyWeight(config: ResolvedFanoutConfig): number {
|
|
|
216
232
|
return config.balanceLayerLoadByConnectionCount ? 0.25 : 0.01
|
|
217
233
|
}
|
|
218
234
|
|
|
235
|
+
function comparePlaneRoutingPriority(
|
|
236
|
+
first: PreparedBus,
|
|
237
|
+
second: PreparedBus,
|
|
238
|
+
allowBlindAndBuriedVias: boolean,
|
|
239
|
+
): number {
|
|
240
|
+
const planeDifference =
|
|
241
|
+
Number(first.termination.type === "plane") -
|
|
242
|
+
Number(second.termination.type === "plane")
|
|
243
|
+
return allowBlindAndBuriedVias ? -planeDifference : planeDifference
|
|
244
|
+
}
|
|
245
|
+
|
|
219
246
|
function getPlanViaCount(plans: readonly FanoutRoutePlan[]): number {
|
|
220
247
|
return plans.reduce(
|
|
221
248
|
(count, plan) =>
|
|
@@ -439,6 +466,7 @@ function getCandidateEscapeLayersForBus(params: {
|
|
|
439
466
|
viaHoleDiameter: config.viaHoleDiameter,
|
|
440
467
|
clearance: config.clearance,
|
|
441
468
|
compactBusTracks: config.compactBusTracks,
|
|
469
|
+
allowBlindAndBuriedVias: config.allowBlindAndBuriedVias,
|
|
442
470
|
allowSameNetMerges: config.allowSameNetMerges,
|
|
443
471
|
staticClearanceCache,
|
|
444
472
|
}) !== null,
|
|
@@ -486,6 +514,10 @@ export class FanoutSolver extends BaseSolver {
|
|
|
486
514
|
private bestAttempt: AssignmentAttempt | null = null
|
|
487
515
|
private lengthMatchingFailure: FanoutValidationIssue | null = null
|
|
488
516
|
private endpointCompletion: CompleteOriginalEndpointsResult | null = null
|
|
517
|
+
private denseDogboneViaPoints:
|
|
518
|
+
| Map<number, { x: number; y: number }>
|
|
519
|
+
| null
|
|
520
|
+
| undefined
|
|
489
521
|
|
|
490
522
|
constructor(
|
|
491
523
|
public readonly inputSrj: SimpleRouteJson,
|
|
@@ -627,6 +659,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
627
659
|
viaDiameter: this.config.viaDiameter,
|
|
628
660
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
629
661
|
clearance: this.config.clearance,
|
|
662
|
+
allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
|
|
630
663
|
effort: this.options.endpointCompletionEffort,
|
|
631
664
|
routeDownstreamConnections: this.options.routeDownstreamConnections,
|
|
632
665
|
})
|
|
@@ -658,6 +691,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
658
691
|
preparedBuses: this.preparedBuses,
|
|
659
692
|
sharedBoundary: this.getValidationBoundary(),
|
|
660
693
|
clearance: this.config.clearance,
|
|
694
|
+
allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
|
|
661
695
|
})
|
|
662
696
|
}
|
|
663
697
|
|
|
@@ -670,10 +704,571 @@ export class FanoutSolver extends BaseSolver {
|
|
|
670
704
|
inputSrj: this.inputSrj,
|
|
671
705
|
sharedBoundary: this.getValidationBoundary(),
|
|
672
706
|
clearance: this.config.clearance,
|
|
707
|
+
allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
|
|
673
708
|
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
674
709
|
})
|
|
675
710
|
}
|
|
676
711
|
|
|
712
|
+
/**
|
|
713
|
+
* Through-all source vias from a wide boundary bus can consume the only
|
|
714
|
+
* legal dogbone channel for nearby plane pads. Conversely, routing hundreds
|
|
715
|
+
* of singleton plane drops first can strand the boundary bus. Search a tiny
|
|
716
|
+
* number of whole-bus boundary alternatives, then fill the remaining plane
|
|
717
|
+
* dogbones. This is intentionally bounded independently of the number of
|
|
718
|
+
* plane drops so dense power fields cannot explode the general beam search.
|
|
719
|
+
*/
|
|
720
|
+
private routeDenseThroughAllMixedTerminations(params: {
|
|
721
|
+
busLayerAssignments: Readonly<Record<string, string>>
|
|
722
|
+
busesInRoutingOrder: readonly PreparedBus[]
|
|
723
|
+
}): MixedTerminationState | null {
|
|
724
|
+
if (this.config.allowBlindAndBuriedVias) return null
|
|
725
|
+
|
|
726
|
+
const boundaryBuses = params.busesInRoutingOrder.filter(
|
|
727
|
+
(bus) => bus.termination.type === "boundary",
|
|
728
|
+
)
|
|
729
|
+
// Preserve the caller/input order for the dense singleton fill. The
|
|
730
|
+
// general routing sort is useful for heterogeneous buses, but ordering a
|
|
731
|
+
// regular BGA power field by obstacle depth creates artificial local
|
|
732
|
+
// dead-ends and needlessly triggers the widened boundary beam. The local
|
|
733
|
+
// rip-up/retry below handles the genuinely constrained drops.
|
|
734
|
+
const planeBuses = this.preparedBuses.filter(
|
|
735
|
+
(bus) => bus.termination.type === "plane",
|
|
736
|
+
)
|
|
737
|
+
if (
|
|
738
|
+
boundaryBuses.length === 0 ||
|
|
739
|
+
boundaryBuses.length > 4 ||
|
|
740
|
+
planeBuses.length < 8 ||
|
|
741
|
+
boundaryBuses.some((bus) => !busUsesCoordinatedWinding(bus)) ||
|
|
742
|
+
planeBuses.some((bus) => bus.connections.length !== 1) ||
|
|
743
|
+
boundaryBuses.length + planeBuses.length !==
|
|
744
|
+
params.busesInRoutingOrder.length
|
|
745
|
+
) {
|
|
746
|
+
return null
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const connectionNameByIndex = new Map(
|
|
750
|
+
this.preparedBuses.flatMap((bus) =>
|
|
751
|
+
bus.connections.map(
|
|
752
|
+
(connection) =>
|
|
753
|
+
[connection.connectionIndex, connection.connection.name] as const,
|
|
754
|
+
),
|
|
755
|
+
),
|
|
756
|
+
)
|
|
757
|
+
const preferredBoundaryPerpendicularSideByBusId = new Map(
|
|
758
|
+
boundaryBuses.map((bus) => [bus.busId, 1 as const]),
|
|
759
|
+
)
|
|
760
|
+
const preferBoundaryOutwardByBusId = new Map(
|
|
761
|
+
boundaryBuses.map((bus) => [
|
|
762
|
+
bus.busId,
|
|
763
|
+
getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
|
|
764
|
+
]),
|
|
765
|
+
)
|
|
766
|
+
if (this.denseDogboneViaPoints === undefined) {
|
|
767
|
+
this.denseDogboneViaPoints = matchComponentDogboneViaSites(
|
|
768
|
+
this.preparedBuses,
|
|
769
|
+
{
|
|
770
|
+
viaDiameter: this.config.viaDiameter,
|
|
771
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
772
|
+
traceWidth: this.config.traceWidth,
|
|
773
|
+
clearance: this.config.clearance,
|
|
774
|
+
maximumSearchStates: 20_000,
|
|
775
|
+
preferredBoundaryPerpendicularSideByBusId,
|
|
776
|
+
preferBoundaryOutwardByBusId,
|
|
777
|
+
canShareCopper: (firstConnectionIndex, secondConnectionIndex) => {
|
|
778
|
+
if (!this.config.allowSameNetMerges) return false
|
|
779
|
+
const firstConnectionName =
|
|
780
|
+
connectionNameByIndex.get(firstConnectionIndex)
|
|
781
|
+
const secondConnectionName = connectionNameByIndex.get(
|
|
782
|
+
secondConnectionIndex,
|
|
783
|
+
)
|
|
784
|
+
return Boolean(
|
|
785
|
+
firstConnectionName &&
|
|
786
|
+
secondConnectionName &&
|
|
787
|
+
connectionsShareElectricalNet(
|
|
788
|
+
this.routingSrj,
|
|
789
|
+
firstConnectionName,
|
|
790
|
+
secondConnectionName,
|
|
791
|
+
),
|
|
792
|
+
)
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
)
|
|
796
|
+
}
|
|
797
|
+
const fixedViaPointsByConnectionIndex = this.denseDogboneViaPoints
|
|
798
|
+
if (fixedViaPointsByConnectionIndex) {
|
|
799
|
+
const reservedViasByComponentId = new Map<
|
|
800
|
+
string,
|
|
801
|
+
Array<{
|
|
802
|
+
connectionName: string
|
|
803
|
+
via: {
|
|
804
|
+
center: { x: number; y: number }
|
|
805
|
+
diameter: number
|
|
806
|
+
spanLayers: string[]
|
|
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
|
+
},
|
|
832
|
+
})
|
|
833
|
+
}
|
|
834
|
+
reservedViasByComponentId.set(bus.componentId, componentReservedVias)
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const matchedPlans: FanoutRoutePlan[] = []
|
|
838
|
+
let matchedRoutingSucceeded = true
|
|
839
|
+
for (const bus of boundaryBuses) {
|
|
840
|
+
const targetLayer = params.busLayerAssignments[bus.busId]
|
|
841
|
+
if (!targetLayer) {
|
|
842
|
+
matchedRoutingSucceeded = false
|
|
843
|
+
break
|
|
844
|
+
}
|
|
845
|
+
const currentConnectionNames = new Set(
|
|
846
|
+
bus.connections.map((connection) => connection.connection.name),
|
|
847
|
+
)
|
|
848
|
+
const reservedVias = (
|
|
849
|
+
reservedViasByComponentId.get(bus.componentId) ?? []
|
|
850
|
+
).filter(
|
|
851
|
+
({ connectionName }) => !currentConnectionNames.has(connectionName),
|
|
852
|
+
)
|
|
853
|
+
const busPlans = routeBusAlternatives(
|
|
854
|
+
{
|
|
855
|
+
srj: this.routingSrj,
|
|
856
|
+
bus,
|
|
857
|
+
targetLayer,
|
|
858
|
+
acceptedPlans: matchedPlans,
|
|
859
|
+
layerNames: this.config.layerNames,
|
|
860
|
+
traceWidth: this.config.traceWidth,
|
|
861
|
+
viaDiameter: this.config.viaDiameter,
|
|
862
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
863
|
+
clearance: this.config.clearance,
|
|
864
|
+
compactBusTracks: this.config.compactBusTracks,
|
|
865
|
+
allowBlindAndBuriedVias: false,
|
|
866
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
867
|
+
staticClearanceCache: this.routeStaticClearanceCache,
|
|
868
|
+
fixedViaPointsByConnectionIndex,
|
|
869
|
+
reservedVias,
|
|
870
|
+
viaMinimalOnly: true,
|
|
871
|
+
},
|
|
872
|
+
1,
|
|
873
|
+
)[0]
|
|
874
|
+
if (!busPlans) {
|
|
875
|
+
matchedRoutingSucceeded = false
|
|
876
|
+
break
|
|
877
|
+
}
|
|
878
|
+
matchedPlans.push(...busPlans)
|
|
879
|
+
}
|
|
880
|
+
if (matchedRoutingSucceeded) {
|
|
881
|
+
for (const bus of planeBuses) {
|
|
882
|
+
const targetLayer = params.busLayerAssignments[bus.busId]
|
|
883
|
+
const busPlans = targetLayer
|
|
884
|
+
? routeBus({
|
|
885
|
+
srj: this.routingSrj,
|
|
886
|
+
bus,
|
|
887
|
+
targetLayer,
|
|
888
|
+
acceptedPlans: matchedPlans,
|
|
889
|
+
layerNames: this.config.layerNames,
|
|
890
|
+
traceWidth: this.config.traceWidth,
|
|
891
|
+
viaDiameter: this.config.viaDiameter,
|
|
892
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
893
|
+
clearance: this.config.clearance,
|
|
894
|
+
compactBusTracks: this.config.compactBusTracks,
|
|
895
|
+
allowBlindAndBuriedVias: false,
|
|
896
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
897
|
+
staticClearanceCache: this.routeStaticClearanceCache,
|
|
898
|
+
fixedViaPointsByConnectionIndex,
|
|
899
|
+
})
|
|
900
|
+
: null
|
|
901
|
+
if (!busPlans) {
|
|
902
|
+
matchedRoutingSucceeded = false
|
|
903
|
+
break
|
|
904
|
+
}
|
|
905
|
+
matchedPlans.push(...busPlans)
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (
|
|
909
|
+
matchedRoutingSucceeded &&
|
|
910
|
+
fanoutPlansAreClear({
|
|
911
|
+
plans: matchedPlans,
|
|
912
|
+
srj: this.routingSrj,
|
|
913
|
+
sharedBoundary: boundaryBuses[0]!.sharedBoundary,
|
|
914
|
+
clearance: this.config.clearance,
|
|
915
|
+
allowBlindAndBuriedVias: false,
|
|
916
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
917
|
+
})
|
|
918
|
+
) {
|
|
919
|
+
return { plans: matchedPlans, failedBusIds: [] }
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const maximumStates = 8
|
|
924
|
+
const getBoundaryStates = (
|
|
925
|
+
alternativesPerBoundaryBus: number,
|
|
926
|
+
initialPlans: readonly FanoutRoutePlan[] = [],
|
|
927
|
+
): MixedTerminationState[] | null => {
|
|
928
|
+
let states: MixedTerminationState[] = [
|
|
929
|
+
{ plans: [...initialPlans], failedBusIds: [] },
|
|
930
|
+
]
|
|
931
|
+
for (const bus of boundaryBuses) {
|
|
932
|
+
const targetLayer = params.busLayerAssignments[bus.busId]
|
|
933
|
+
if (!targetLayer) return null
|
|
934
|
+
const nextStates: MixedTerminationState[] = []
|
|
935
|
+
const alternativesByState = states.map((state) => ({
|
|
936
|
+
state,
|
|
937
|
+
alternatives: routeBusAlternatives(
|
|
938
|
+
{
|
|
939
|
+
srj: this.routingSrj,
|
|
940
|
+
bus,
|
|
941
|
+
targetLayer,
|
|
942
|
+
acceptedPlans: state.plans,
|
|
943
|
+
layerNames: this.config.layerNames,
|
|
944
|
+
traceWidth: this.config.traceWidth,
|
|
945
|
+
viaDiameter: this.config.viaDiameter,
|
|
946
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
947
|
+
clearance: this.config.clearance,
|
|
948
|
+
compactBusTracks: this.config.compactBusTracks,
|
|
949
|
+
allowBlindAndBuriedVias: false,
|
|
950
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
951
|
+
staticClearanceCache: this.routeStaticClearanceCache,
|
|
952
|
+
},
|
|
953
|
+
alternativesPerBoundaryBus,
|
|
954
|
+
),
|
|
955
|
+
}))
|
|
956
|
+
for (
|
|
957
|
+
let alternativeIndex = 0;
|
|
958
|
+
alternativeIndex < alternativesPerBoundaryBus;
|
|
959
|
+
alternativeIndex++
|
|
960
|
+
) {
|
|
961
|
+
for (const { state, alternatives } of alternativesByState) {
|
|
962
|
+
const alternative = alternatives[alternativeIndex]
|
|
963
|
+
if (!alternative) continue
|
|
964
|
+
nextStates.push({
|
|
965
|
+
plans: [...state.plans, ...alternative],
|
|
966
|
+
failedBusIds: [],
|
|
967
|
+
})
|
|
968
|
+
if (nextStates.length >= maximumStates) break
|
|
969
|
+
}
|
|
970
|
+
if (nextStates.length >= maximumStates) break
|
|
971
|
+
}
|
|
972
|
+
if (nextStates.length === 0) return null
|
|
973
|
+
states = nextStates
|
|
974
|
+
}
|
|
975
|
+
return states
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const getJointReservedBoundaryState = (
|
|
979
|
+
initialPlans: readonly FanoutRoutePlan[],
|
|
980
|
+
): MixedTerminationState | null => {
|
|
981
|
+
if (boundaryBuses.length !== 2) return null
|
|
982
|
+
const [firstBus, secondBus] = boundaryBuses
|
|
983
|
+
if (!firstBus || !secondBus) return null
|
|
984
|
+
const routeBoundaryBus = (
|
|
985
|
+
bus: PreparedBus,
|
|
986
|
+
acceptedPlans: FanoutRoutePlan[],
|
|
987
|
+
rejectedViaMinimalCandidates?: FanoutRoutePlan[][],
|
|
988
|
+
): FanoutRoutePlan[] | null => {
|
|
989
|
+
const targetLayer = params.busLayerAssignments[bus.busId]
|
|
990
|
+
if (!targetLayer) return null
|
|
991
|
+
return (
|
|
992
|
+
routeBusAlternatives(
|
|
993
|
+
{
|
|
994
|
+
srj: this.routingSrj,
|
|
995
|
+
bus,
|
|
996
|
+
targetLayer,
|
|
997
|
+
acceptedPlans,
|
|
998
|
+
layerNames: this.config.layerNames,
|
|
999
|
+
traceWidth: this.config.traceWidth,
|
|
1000
|
+
viaDiameter: this.config.viaDiameter,
|
|
1001
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
1002
|
+
clearance: this.config.clearance,
|
|
1003
|
+
compactBusTracks: this.config.compactBusTracks,
|
|
1004
|
+
allowBlindAndBuriedVias: false,
|
|
1005
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
1006
|
+
staticClearanceCache: this.routeStaticClearanceCache,
|
|
1007
|
+
rejectedViaMinimalCandidates,
|
|
1008
|
+
stopAfterFirstRejectedViaMinimalCandidate:
|
|
1009
|
+
rejectedViaMinimalCandidates !== undefined,
|
|
1010
|
+
},
|
|
1011
|
+
1,
|
|
1012
|
+
)[0] ?? null
|
|
1013
|
+
)
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const firstPlans = routeBoundaryBus(firstBus, [...initialPlans])
|
|
1017
|
+
if (!firstPlans) return null
|
|
1018
|
+
const rejectedSecondCandidates: FanoutRoutePlan[][] = []
|
|
1019
|
+
const secondPlans = routeBoundaryBus(
|
|
1020
|
+
secondBus,
|
|
1021
|
+
[...initialPlans, ...firstPlans],
|
|
1022
|
+
rejectedSecondCandidates,
|
|
1023
|
+
)
|
|
1024
|
+
if (secondPlans) {
|
|
1025
|
+
return {
|
|
1026
|
+
plans: [...initialPlans, ...firstPlans, ...secondPlans],
|
|
1027
|
+
failedBusIds: [],
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const rejectedSecondPlans = rejectedSecondCandidates[0]
|
|
1032
|
+
if (!rejectedSecondPlans) return null
|
|
1033
|
+
// Keep the candidate's exact copper/vias as immutable blockers while
|
|
1034
|
+
// rerouting the earlier bus, but exclude it from corner-slot allocation.
|
|
1035
|
+
const reservedSecondPlans = rejectedSecondPlans.map((plan) => ({
|
|
1036
|
+
...plan,
|
|
1037
|
+
termination: {
|
|
1038
|
+
type: "plane" as const,
|
|
1039
|
+
layer: plan.targetLayer,
|
|
1040
|
+
},
|
|
1041
|
+
}))
|
|
1042
|
+
const reroutedFirstPlans = routeBoundaryBus(firstBus, [
|
|
1043
|
+
...initialPlans,
|
|
1044
|
+
...reservedSecondPlans,
|
|
1045
|
+
])
|
|
1046
|
+
if (!reroutedFirstPlans) return null
|
|
1047
|
+
const combinedPlans = [
|
|
1048
|
+
...initialPlans,
|
|
1049
|
+
...reroutedFirstPlans,
|
|
1050
|
+
...rejectedSecondPlans,
|
|
1051
|
+
]
|
|
1052
|
+
if (
|
|
1053
|
+
!fanoutPlansAreClear({
|
|
1054
|
+
plans: combinedPlans,
|
|
1055
|
+
srj: this.routingSrj,
|
|
1056
|
+
sharedBoundary: firstBus.sharedBoundary,
|
|
1057
|
+
clearance: this.config.clearance,
|
|
1058
|
+
allowBlindAndBuriedVias: false,
|
|
1059
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
1060
|
+
})
|
|
1061
|
+
) {
|
|
1062
|
+
return null
|
|
1063
|
+
}
|
|
1064
|
+
return { plans: combinedPlans, failedBusIds: [] }
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const planeBusById = new Map(planeBuses.map((bus) => [bus.busId, bus]))
|
|
1068
|
+
const routePlaneBus = (
|
|
1069
|
+
bus: PreparedBus,
|
|
1070
|
+
acceptedPlans: FanoutRoutePlan[],
|
|
1071
|
+
blockingBusCounts?: Map<string, number>,
|
|
1072
|
+
): FanoutRoutePlan[] | null => {
|
|
1073
|
+
const targetLayer = params.busLayerAssignments[bus.busId]
|
|
1074
|
+
if (!targetLayer) return null
|
|
1075
|
+
return routeBus({
|
|
1076
|
+
srj: this.routingSrj,
|
|
1077
|
+
bus,
|
|
1078
|
+
targetLayer,
|
|
1079
|
+
acceptedPlans,
|
|
1080
|
+
layerNames: this.config.layerNames,
|
|
1081
|
+
traceWidth: this.config.traceWidth,
|
|
1082
|
+
viaDiameter: this.config.viaDiameter,
|
|
1083
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
1084
|
+
clearance: this.config.clearance,
|
|
1085
|
+
compactBusTracks: this.config.compactBusTracks,
|
|
1086
|
+
allowBlindAndBuriedVias: false,
|
|
1087
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
1088
|
+
staticClearanceCache: this.routeStaticClearanceCache,
|
|
1089
|
+
blockingBusCounts,
|
|
1090
|
+
})
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const routePlaneOrder = (
|
|
1094
|
+
boundaryPlans: readonly FanoutRoutePlan[],
|
|
1095
|
+
planeOrder: readonly PreparedBus[],
|
|
1096
|
+
): MixedTerminationState => {
|
|
1097
|
+
const state: MixedTerminationState = {
|
|
1098
|
+
plans: [...boundaryPlans],
|
|
1099
|
+
failedBusIds: [],
|
|
1100
|
+
}
|
|
1101
|
+
for (const bus of planeOrder) {
|
|
1102
|
+
const blockingBusCounts = new Map<string, number>()
|
|
1103
|
+
let busPlans = routePlaneBus(bus, state.plans, blockingBusCounts)
|
|
1104
|
+
|
|
1105
|
+
// A plane dogbone can lose its only local channel to one or two
|
|
1106
|
+
// earlier singleton drops. Rip up only the strongest local blockers,
|
|
1107
|
+
// route the constrained drop first, then put the displaced drops back.
|
|
1108
|
+
// Boundary buses are immutable here and the search is capped at 36
|
|
1109
|
+
// blocker pairs, so dense fields remain predictable.
|
|
1110
|
+
if (!busPlans) {
|
|
1111
|
+
const blockerIds = [...blockingBusCounts.entries()]
|
|
1112
|
+
.filter(([busId]) =>
|
|
1113
|
+
state.plans.some((plan) => plan.busId === busId),
|
|
1114
|
+
)
|
|
1115
|
+
.filter(([busId]) => planeBusById.has(busId))
|
|
1116
|
+
.toSorted(
|
|
1117
|
+
([, firstCount], [, secondCount]) => secondCount - firstCount,
|
|
1118
|
+
)
|
|
1119
|
+
.slice(0, 8)
|
|
1120
|
+
.map(([busId]) => busId)
|
|
1121
|
+
const ripupSets = [
|
|
1122
|
+
...blockerIds.map((busId) => [busId]),
|
|
1123
|
+
...blockerIds.flatMap((first, firstIndex) =>
|
|
1124
|
+
blockerIds.slice(firstIndex + 1).map((second) => [first, second]),
|
|
1125
|
+
),
|
|
1126
|
+
]
|
|
1127
|
+
for (const ripupIds of ripupSets) {
|
|
1128
|
+
const ripupIdSet = new Set(ripupIds)
|
|
1129
|
+
const candidatePlans = state.plans.filter(
|
|
1130
|
+
(plan) => !ripupIdSet.has(plan.busId),
|
|
1131
|
+
)
|
|
1132
|
+
const constrainedPlans = routePlaneBus(bus, candidatePlans)
|
|
1133
|
+
if (!constrainedPlans) continue
|
|
1134
|
+
candidatePlans.push(...constrainedPlans)
|
|
1135
|
+
let repairSucceeded = true
|
|
1136
|
+
for (const blockerId of ripupIds) {
|
|
1137
|
+
const blockerBus = planeBusById.get(blockerId)
|
|
1138
|
+
const replacementPlans = blockerBus
|
|
1139
|
+
? routePlaneBus(blockerBus, candidatePlans)
|
|
1140
|
+
: null
|
|
1141
|
+
if (!replacementPlans) {
|
|
1142
|
+
repairSucceeded = false
|
|
1143
|
+
break
|
|
1144
|
+
}
|
|
1145
|
+
candidatePlans.push(...replacementPlans)
|
|
1146
|
+
}
|
|
1147
|
+
if (!repairSucceeded) continue
|
|
1148
|
+
state.plans = candidatePlans
|
|
1149
|
+
busPlans = []
|
|
1150
|
+
break
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (busPlans) state.plans.push(...busPlans)
|
|
1155
|
+
else state.failedBusIds.push(bus.busId)
|
|
1156
|
+
}
|
|
1157
|
+
return state
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
let bestState: MixedTerminationState | null = null
|
|
1161
|
+
let mostRecentBoundaryBestState: MixedTerminationState | null = null
|
|
1162
|
+
const evaluateBoundaryStates = (
|
|
1163
|
+
states: readonly MixedTerminationState[],
|
|
1164
|
+
initialPlaneOrder: readonly PreparedBus[],
|
|
1165
|
+
): MixedTerminationState | null => {
|
|
1166
|
+
let localBestState: MixedTerminationState | null = null
|
|
1167
|
+
for (const boundaryState of states) {
|
|
1168
|
+
let planeOrder = [...initialPlaneOrder]
|
|
1169
|
+
const seenPlaneOrders = new Set<string>()
|
|
1170
|
+
for (let retryIndex = 0; retryIndex < 3; retryIndex++) {
|
|
1171
|
+
const orderKey = planeOrder.map((bus) => bus.busId).join("\u0000")
|
|
1172
|
+
if (seenPlaneOrders.has(orderKey)) break
|
|
1173
|
+
seenPlaneOrders.add(orderKey)
|
|
1174
|
+
const state = routePlaneOrder(boundaryState.plans, planeOrder)
|
|
1175
|
+
if (
|
|
1176
|
+
!bestState ||
|
|
1177
|
+
state.plans.length > bestState.plans.length ||
|
|
1178
|
+
(state.plans.length === bestState.plans.length &&
|
|
1179
|
+
state.failedBusIds.length < bestState.failedBusIds.length)
|
|
1180
|
+
) {
|
|
1181
|
+
bestState = state
|
|
1182
|
+
}
|
|
1183
|
+
if (
|
|
1184
|
+
!localBestState ||
|
|
1185
|
+
state.plans.length > localBestState.plans.length ||
|
|
1186
|
+
(state.plans.length === localBestState.plans.length &&
|
|
1187
|
+
state.failedBusIds.length < localBestState.failedBusIds.length)
|
|
1188
|
+
) {
|
|
1189
|
+
localBestState = state
|
|
1190
|
+
}
|
|
1191
|
+
if (state.failedBusIds.length === 0) return state
|
|
1192
|
+
const failedBusIds = new Set(state.failedBusIds)
|
|
1193
|
+
planeOrder = [
|
|
1194
|
+
...state.failedBusIds.flatMap((busId) => {
|
|
1195
|
+
const bus = planeBusById.get(busId)
|
|
1196
|
+
return bus ? [bus] : []
|
|
1197
|
+
}),
|
|
1198
|
+
...planeOrder.filter((bus) => !failedBusIds.has(bus.busId)),
|
|
1199
|
+
]
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
mostRecentBoundaryBestState = localBestState
|
|
1203
|
+
return null
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Most dense packages route with the first deterministic boundary choice.
|
|
1207
|
+
// Try that cheap path before widening the beam; eagerly generating four
|
|
1208
|
+
// complete A* variants per state can otherwise dominate runtime and memory.
|
|
1209
|
+
for (const alternativesPerBoundaryBus of [1, 4]) {
|
|
1210
|
+
const states = getBoundaryStates(alternativesPerBoundaryBus)
|
|
1211
|
+
if (!states) continue
|
|
1212
|
+
const completeState = evaluateBoundaryStates(states, planeBuses)
|
|
1213
|
+
if (completeState) return completeState
|
|
1214
|
+
|
|
1215
|
+
// If a completed signal escape encloses one especially constrained
|
|
1216
|
+
// power pad, seed just that failed singleton first and recompute the
|
|
1217
|
+
// small boundary-bus set around its physical through barrel. This keeps
|
|
1218
|
+
// the search local without routing hundreds of plane drops before the
|
|
1219
|
+
// signal buses.
|
|
1220
|
+
const seedFailureIds = (
|
|
1221
|
+
bestState as MixedTerminationState | null
|
|
1222
|
+
)?.failedBusIds.slice(0, 3)
|
|
1223
|
+
if (alternativesPerBoundaryBus === 1 && seedFailureIds) {
|
|
1224
|
+
for (const failedBusId of seedFailureIds) {
|
|
1225
|
+
const seededPlanePlans: FanoutRoutePlan[] = []
|
|
1226
|
+
const seededPlaneBusIds = new Set<string>()
|
|
1227
|
+
let nextFailedBusId: string | undefined = failedBusId
|
|
1228
|
+
for (
|
|
1229
|
+
let seedDepth = 0;
|
|
1230
|
+
seedDepth < 3 && nextFailedBusId;
|
|
1231
|
+
seedDepth++
|
|
1232
|
+
) {
|
|
1233
|
+
const failedPlaneBus = planeBusById.get(nextFailedBusId)
|
|
1234
|
+
if (!failedPlaneBus) break
|
|
1235
|
+
const nextSeedPlans = routePlaneBus(
|
|
1236
|
+
failedPlaneBus,
|
|
1237
|
+
seededPlanePlans,
|
|
1238
|
+
)
|
|
1239
|
+
if (!nextSeedPlans) break
|
|
1240
|
+
seededPlanePlans.push(...nextSeedPlans)
|
|
1241
|
+
seededPlaneBusIds.add(nextFailedBusId)
|
|
1242
|
+
const jointReservedBoundaryState =
|
|
1243
|
+
getJointReservedBoundaryState(seededPlanePlans)
|
|
1244
|
+
if (!jointReservedBoundaryState) break
|
|
1245
|
+
const jointCompleteState = evaluateBoundaryStates(
|
|
1246
|
+
[jointReservedBoundaryState],
|
|
1247
|
+
planeBuses.filter((bus) => !seededPlaneBusIds.has(bus.busId)),
|
|
1248
|
+
)
|
|
1249
|
+
if (jointCompleteState) return jointCompleteState
|
|
1250
|
+
const recentFailedBusIds = (
|
|
1251
|
+
mostRecentBoundaryBestState as MixedTerminationState | null
|
|
1252
|
+
)?.failedBusIds
|
|
1253
|
+
nextFailedBusId = recentFailedBusIds?.find(
|
|
1254
|
+
(busId) => !seededPlaneBusIds.has(busId),
|
|
1255
|
+
)
|
|
1256
|
+
}
|
|
1257
|
+
if (seededPlanePlans.length === 0) continue
|
|
1258
|
+
const seededBoundaryStates = getBoundaryStates(1, seededPlanePlans)
|
|
1259
|
+
if (!seededBoundaryStates) continue
|
|
1260
|
+
const seededCompleteState = evaluateBoundaryStates(
|
|
1261
|
+
seededBoundaryStates,
|
|
1262
|
+
planeBuses.filter((bus) => !seededPlaneBusIds.has(bus.busId)),
|
|
1263
|
+
)
|
|
1264
|
+
if (seededCompleteState) return seededCompleteState
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
return bestState
|
|
1270
|
+
}
|
|
1271
|
+
|
|
677
1272
|
private evaluateAssignmentWithStrategy(
|
|
678
1273
|
assignmentIndex: number,
|
|
679
1274
|
busLayerAssignments: Readonly<Record<string, string>>,
|
|
@@ -723,8 +1318,11 @@ export class FanoutSolver extends BaseSolver {
|
|
|
723
1318
|
busLayerAssignments[b.busId] ?? "",
|
|
724
1319
|
)
|
|
725
1320
|
return (
|
|
726
|
-
|
|
727
|
-
|
|
1321
|
+
comparePlaneRoutingPriority(
|
|
1322
|
+
a,
|
|
1323
|
+
b,
|
|
1324
|
+
this.config.allowBlindAndBuriedVias,
|
|
1325
|
+
) ||
|
|
728
1326
|
Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) ||
|
|
729
1327
|
(aUsesCoordinatedWinding && bUsesCoordinatedWinding
|
|
730
1328
|
? bLayerIndex - aLayerIndex
|
|
@@ -744,8 +1342,23 @@ export class FanoutSolver extends BaseSolver {
|
|
|
744
1342
|
)
|
|
745
1343
|
})
|
|
746
1344
|
|
|
1345
|
+
const mixedTerminationState =
|
|
1346
|
+
!useSingleLayerPushAndShove && routingStrategy === "default"
|
|
1347
|
+
? this.routeDenseThroughAllMixedTerminations({
|
|
1348
|
+
busLayerAssignments,
|
|
1349
|
+
busesInRoutingOrder,
|
|
1350
|
+
})
|
|
1351
|
+
: null
|
|
1352
|
+
|
|
1353
|
+
if (mixedTerminationState) {
|
|
1354
|
+
plans = mixedTerminationState.plans
|
|
1355
|
+
failedBusIds = mixedTerminationState.failedBusIds
|
|
1356
|
+
}
|
|
1357
|
+
|
|
747
1358
|
let routingPrefixKey = `${routingStrategy}|`
|
|
748
|
-
for (const bus of useSingleLayerPushAndShove
|
|
1359
|
+
for (const bus of useSingleLayerPushAndShove || mixedTerminationState
|
|
1360
|
+
? []
|
|
1361
|
+
: busesInRoutingOrder) {
|
|
749
1362
|
const targetLayer = busLayerAssignments[bus.busId]
|
|
750
1363
|
if (!targetLayer) {
|
|
751
1364
|
throw new Error(
|
|
@@ -777,6 +1390,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
777
1390
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
778
1391
|
clearance: this.config.clearance,
|
|
779
1392
|
compactBusTracks: this.config.compactBusTracks,
|
|
1393
|
+
allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
|
|
780
1394
|
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
781
1395
|
staticClearanceCache: this.routeStaticClearanceCache,
|
|
782
1396
|
blockingBusCounts: currentBusBlockingCounts,
|
|
@@ -870,7 +1484,6 @@ export class FanoutSolver extends BaseSolver {
|
|
|
870
1484
|
score,
|
|
871
1485
|
...(validationIssues ? { validationIssues } : {}),
|
|
872
1486
|
}
|
|
873
|
-
|
|
874
1487
|
return {
|
|
875
1488
|
summary,
|
|
876
1489
|
plans,
|
|
@@ -972,8 +1585,11 @@ export class FanoutSolver extends BaseSolver {
|
|
|
972
1585
|
: (this.escapeLayersByBusId[b.busId]?.length ??
|
|
973
1586
|
this.config.escapeLayers.length)
|
|
974
1587
|
return (
|
|
975
|
-
|
|
976
|
-
|
|
1588
|
+
comparePlaneRoutingPriority(
|
|
1589
|
+
a,
|
|
1590
|
+
b,
|
|
1591
|
+
this.config.allowBlindAndBuriedVias,
|
|
1592
|
+
) ||
|
|
977
1593
|
Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) ||
|
|
978
1594
|
(aUsesCoordinatedWinding && bUsesCoordinatedWinding
|
|
979
1595
|
? getMaximumViaSpan(b) - getMaximumViaSpan(a)
|
|
@@ -1084,6 +1700,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1084
1700
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
1085
1701
|
clearance: this.config.clearance,
|
|
1086
1702
|
compactBusTracks: this.config.compactBusTracks,
|
|
1703
|
+
allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
|
|
1087
1704
|
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
1088
1705
|
staticClearanceCache: this.routeStaticClearanceCache,
|
|
1089
1706
|
},
|
|
@@ -1534,14 +2151,17 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1534
2151
|
`FanoutSolver: completed output failed validation: ${validation.issues[0]?.message ?? "unknown validation error"}`,
|
|
1535
2152
|
)
|
|
1536
2153
|
}
|
|
1537
|
-
const finalSrj =
|
|
1538
|
-
|
|
2154
|
+
const finalSrj = addViaLayerMetadataToSrj({
|
|
2155
|
+
srj:
|
|
2156
|
+
this.endpointCompletion?.simpleRouteJson ?? this.bestAttempt.outputSrj,
|
|
2157
|
+
layerNames: this.config.layerNames,
|
|
2158
|
+
allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
|
|
2159
|
+
})
|
|
1539
2160
|
const finalTraceById = new Map(
|
|
1540
2161
|
(finalSrj.traces ?? []).map((trace) => [trace.pcb_trace_id, trace]),
|
|
1541
2162
|
)
|
|
1542
2163
|
return {
|
|
1543
|
-
simpleRouteJson:
|
|
1544
|
-
this.endpointCompletion?.simpleRouteJson ?? this.bestAttempt.outputSrj,
|
|
2164
|
+
simpleRouteJson: finalSrj,
|
|
1545
2165
|
fanoutTraces: this.bestAttempt.plans.flatMap((plan) => [
|
|
1546
2166
|
finalTraceById.get(plan.trace.pcb_trace_id) ?? plan.trace,
|
|
1547
2167
|
...(plan.planeEndpointTrace
|
|
@@ -1551,7 +2171,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1551
2171
|
]
|
|
1552
2172
|
: []),
|
|
1553
2173
|
]),
|
|
1554
|
-
completionTraces: this.endpointCompletion?.traces ?? []
|
|
2174
|
+
completionTraces: (this.endpointCompletion?.traces ?? []).map(
|
|
2175
|
+
(trace) => finalTraceById.get(trace.pcb_trace_id) ?? trace,
|
|
2176
|
+
),
|
|
1555
2177
|
...(this.endpointCompletion
|
|
1556
2178
|
? { endpointCompletion: this.endpointCompletion.report }
|
|
1557
2179
|
: {}),
|
|
@@ -1576,7 +2198,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1576
2198
|
}
|
|
1577
2199
|
}
|
|
1578
2200
|
|
|
1579
|
-
getOutputSimpleRouteJson():
|
|
2201
|
+
getOutputSimpleRouteJson(): SimpleRouteJsonWithFanoutPlanes {
|
|
1580
2202
|
return this.getOutput().simpleRouteJson
|
|
1581
2203
|
}
|
|
1582
2204
|
|