@tscircuit/fanout-solver 0.0.18 → 0.0.20
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 +41 -22
- package/lib/fanout-solver.ts +111 -18
- package/lib/geometry.ts +38 -19
- package/lib/index.ts +5 -0
- package/lib/net-identity.ts +163 -0
- package/lib/route-bus.ts +108 -3
- package/lib/types.ts +39 -0
- package/lib/validate-fanout-solution.ts +772 -0
- package/package.json +3 -2
package/lib/route-bus.ts
CHANGED
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
segmentsAreClear,
|
|
12
12
|
} from "./geometry"
|
|
13
13
|
import { getLayerSpan } from "./layer-names"
|
|
14
|
+
import {
|
|
15
|
+
connectionsShareElectricalNet,
|
|
16
|
+
obstacleSharesElectricalNet,
|
|
17
|
+
} from "./net-identity"
|
|
14
18
|
import type {
|
|
15
19
|
Bounds,
|
|
16
20
|
FanoutDirection,
|
|
@@ -34,6 +38,7 @@ export interface RouteBusParams {
|
|
|
34
38
|
viaHoleDiameter: number
|
|
35
39
|
clearance: number
|
|
36
40
|
compactBusTracks: boolean
|
|
41
|
+
allowSameNetMerges?: boolean
|
|
37
42
|
staticClearanceCache?: RouteBusStaticClearanceCache
|
|
38
43
|
blockingBusCounts?: Map<string, number>
|
|
39
44
|
}
|
|
@@ -662,13 +667,29 @@ function segmentIsClearOfObstacles(params: {
|
|
|
662
667
|
segment: RoutedSegment
|
|
663
668
|
plan: FanoutRoutePlan
|
|
664
669
|
segmentIndex: number
|
|
670
|
+
srj: SimpleRouteJson
|
|
671
|
+
allowSameNetMerges: boolean
|
|
665
672
|
obstacles: Obstacle[]
|
|
666
673
|
clearance: number
|
|
667
674
|
}): boolean {
|
|
668
|
-
const {
|
|
675
|
+
const {
|
|
676
|
+
segment,
|
|
677
|
+
plan,
|
|
678
|
+
segmentIndex,
|
|
679
|
+
srj,
|
|
680
|
+
allowSameNetMerges,
|
|
681
|
+
obstacles,
|
|
682
|
+
clearance,
|
|
683
|
+
} = params
|
|
669
684
|
for (const obstacle of obstacles) {
|
|
670
685
|
if (!obstacle.layers.includes(segment.layer)) continue
|
|
671
686
|
if (obstacle.connectedTo.includes(plan.connectionName)) continue
|
|
687
|
+
if (
|
|
688
|
+
allowSameNetMerges &&
|
|
689
|
+
obstacleSharesElectricalNet(srj, obstacle, plan.connectionName)
|
|
690
|
+
) {
|
|
691
|
+
continue
|
|
692
|
+
}
|
|
672
693
|
if (
|
|
673
694
|
segmentIndex === 0 &&
|
|
674
695
|
obstacle === plan.sourceObstacle &&
|
|
@@ -691,8 +712,9 @@ function planIsStaticallyClear(params: {
|
|
|
691
712
|
srj: SimpleRouteJson
|
|
692
713
|
sharedBoundary: Bounds
|
|
693
714
|
clearance: number
|
|
715
|
+
allowSameNetMerges: boolean
|
|
694
716
|
}): boolean {
|
|
695
|
-
const { plan, srj, sharedBoundary, clearance } = params
|
|
717
|
+
const { plan, srj, sharedBoundary, clearance, allowSameNetMerges } = params
|
|
696
718
|
const routableBounds = getRoutableBounds(srj.bounds, sharedBoundary)
|
|
697
719
|
if (
|
|
698
720
|
!pointIsInsideBounds(plan.exitPoint, routableBounds) ||
|
|
@@ -710,6 +732,8 @@ function planIsStaticallyClear(params: {
|
|
|
710
732
|
segment: plan.segments[index]!,
|
|
711
733
|
plan,
|
|
712
734
|
segmentIndex: index,
|
|
735
|
+
srj,
|
|
736
|
+
allowSameNetMerges,
|
|
713
737
|
obstacles: srj.obstacles,
|
|
714
738
|
clearance,
|
|
715
739
|
})
|
|
@@ -724,6 +748,12 @@ function planIsStaticallyClear(params: {
|
|
|
724
748
|
) {
|
|
725
749
|
continue
|
|
726
750
|
}
|
|
751
|
+
if (
|
|
752
|
+
allowSameNetMerges &&
|
|
753
|
+
obstacleSharesElectricalNet(srj, obstacle, plan.connectionName)
|
|
754
|
+
) {
|
|
755
|
+
continue
|
|
756
|
+
}
|
|
727
757
|
if (
|
|
728
758
|
distancePointToObstacle(plan.via.center, obstacle) <
|
|
729
759
|
plan.via.diameter / 2 + clearance - 1e-9
|
|
@@ -739,11 +769,30 @@ function planIsStaticallyClear(params: {
|
|
|
739
769
|
function planIsClearOfPlans(params: {
|
|
740
770
|
plan: FanoutRoutePlan
|
|
741
771
|
otherPlans: FanoutRoutePlan[]
|
|
772
|
+
srj: SimpleRouteJson
|
|
773
|
+
allowSameNetMerges: boolean
|
|
742
774
|
clearance: number
|
|
743
775
|
blockingBusCounts?: Map<string, number>
|
|
744
776
|
}): boolean {
|
|
745
|
-
const {
|
|
777
|
+
const {
|
|
778
|
+
plan,
|
|
779
|
+
otherPlans,
|
|
780
|
+
srj,
|
|
781
|
+
allowSameNetMerges,
|
|
782
|
+
clearance,
|
|
783
|
+
blockingBusCounts,
|
|
784
|
+
} = params
|
|
746
785
|
for (const otherPlan of otherPlans) {
|
|
786
|
+
if (
|
|
787
|
+
allowSameNetMerges &&
|
|
788
|
+
connectionsShareElectricalNet(
|
|
789
|
+
srj,
|
|
790
|
+
plan.connectionName,
|
|
791
|
+
otherPlan.connectionName,
|
|
792
|
+
)
|
|
793
|
+
) {
|
|
794
|
+
continue
|
|
795
|
+
}
|
|
747
796
|
const plansShareSourcePort =
|
|
748
797
|
(plan.sourcePoint.pcb_port_id &&
|
|
749
798
|
plan.sourcePoint.pcb_port_id === otherPlan.sourcePoint.pcb_port_id) ||
|
|
@@ -817,6 +866,7 @@ function planIsClear(params: {
|
|
|
817
866
|
srj: SimpleRouteJson
|
|
818
867
|
sharedBoundary: Bounds
|
|
819
868
|
clearance: number
|
|
869
|
+
allowSameNetMerges: boolean
|
|
820
870
|
}): boolean {
|
|
821
871
|
const {
|
|
822
872
|
plan,
|
|
@@ -827,6 +877,7 @@ function planIsClear(params: {
|
|
|
827
877
|
srj,
|
|
828
878
|
sharedBoundary,
|
|
829
879
|
clearance,
|
|
880
|
+
allowSameNetMerges,
|
|
830
881
|
} = params
|
|
831
882
|
let staticallyClear = staticClearanceCache?.get(cacheKey)
|
|
832
883
|
if (staticallyClear === undefined) {
|
|
@@ -835,6 +886,7 @@ function planIsClear(params: {
|
|
|
835
886
|
srj,
|
|
836
887
|
sharedBoundary,
|
|
837
888
|
clearance,
|
|
889
|
+
allowSameNetMerges,
|
|
838
890
|
})
|
|
839
891
|
staticClearanceCache?.set(cacheKey, staticallyClear)
|
|
840
892
|
}
|
|
@@ -843,12 +895,61 @@ function planIsClear(params: {
|
|
|
843
895
|
planIsClearOfPlans({
|
|
844
896
|
plan,
|
|
845
897
|
otherPlans,
|
|
898
|
+
srj,
|
|
899
|
+
allowSameNetMerges,
|
|
846
900
|
clearance,
|
|
847
901
|
blockingBusCounts,
|
|
848
902
|
})
|
|
849
903
|
)
|
|
850
904
|
}
|
|
851
905
|
|
|
906
|
+
/**
|
|
907
|
+
* Validate a complete set of fanout plans against the source SRJ and against
|
|
908
|
+
* one another. This is intentionally separate from route search so every
|
|
909
|
+
* route-producing strategy can share the same final safety invariant.
|
|
910
|
+
*/
|
|
911
|
+
export function fanoutPlansAreClear(params: {
|
|
912
|
+
plans: readonly FanoutRoutePlan[]
|
|
913
|
+
srj: SimpleRouteJson
|
|
914
|
+
sharedBoundary: Bounds
|
|
915
|
+
clearance: number
|
|
916
|
+
allowSameNetMerges?: boolean
|
|
917
|
+
}): boolean {
|
|
918
|
+
const {
|
|
919
|
+
plans,
|
|
920
|
+
srj,
|
|
921
|
+
sharedBoundary,
|
|
922
|
+
clearance,
|
|
923
|
+
allowSameNetMerges = false,
|
|
924
|
+
} = params
|
|
925
|
+
for (let index = 0; index < plans.length; index++) {
|
|
926
|
+
const plan = plans[index]!
|
|
927
|
+
if (
|
|
928
|
+
!planIsStaticallyClear({
|
|
929
|
+
plan,
|
|
930
|
+
srj,
|
|
931
|
+
sharedBoundary,
|
|
932
|
+
clearance,
|
|
933
|
+
allowSameNetMerges,
|
|
934
|
+
})
|
|
935
|
+
) {
|
|
936
|
+
return false
|
|
937
|
+
}
|
|
938
|
+
if (
|
|
939
|
+
!planIsClearOfPlans({
|
|
940
|
+
plan,
|
|
941
|
+
otherPlans: plans.filter((_, otherIndex) => otherIndex !== index),
|
|
942
|
+
srj,
|
|
943
|
+
allowSameNetMerges,
|
|
944
|
+
clearance,
|
|
945
|
+
})
|
|
946
|
+
) {
|
|
947
|
+
return false
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return true
|
|
951
|
+
}
|
|
952
|
+
|
|
852
953
|
function routePlaneTerminatedBus(
|
|
853
954
|
params: RouteBusParams,
|
|
854
955
|
): FanoutRoutePlan[] | null {
|
|
@@ -864,6 +965,7 @@ function routePlaneTerminatedBus(
|
|
|
864
965
|
clearance,
|
|
865
966
|
staticClearanceCache,
|
|
866
967
|
blockingBusCounts,
|
|
968
|
+
allowSameNetMerges = false,
|
|
867
969
|
} = params
|
|
868
970
|
const sourceObstacle = bus.connections[0]?.sourceObstacle
|
|
869
971
|
if (!sourceObstacle || bus.termination.type !== "plane") return null
|
|
@@ -914,6 +1016,7 @@ function routePlaneTerminatedBus(
|
|
|
914
1016
|
srj,
|
|
915
1017
|
sharedBoundary: bus.sharedBoundary,
|
|
916
1018
|
clearance,
|
|
1019
|
+
allowSameNetMerges,
|
|
917
1020
|
})
|
|
918
1021
|
) {
|
|
919
1022
|
orderIsClear = false
|
|
@@ -945,6 +1048,7 @@ export function routeBusAlternatives(
|
|
|
945
1048
|
compactBusTracks,
|
|
946
1049
|
staticClearanceCache,
|
|
947
1050
|
blockingBusCounts,
|
|
1051
|
+
allowSameNetMerges = false,
|
|
948
1052
|
} = params
|
|
949
1053
|
if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
|
|
950
1054
|
throw new Error(
|
|
@@ -1055,6 +1159,7 @@ export function routeBusAlternatives(
|
|
|
1055
1159
|
srj,
|
|
1056
1160
|
sharedBoundary: bus.sharedBoundary,
|
|
1057
1161
|
clearance,
|
|
1162
|
+
allowSameNetMerges,
|
|
1058
1163
|
})
|
|
1059
1164
|
) {
|
|
1060
1165
|
continue
|
package/lib/types.ts
CHANGED
|
@@ -98,6 +98,8 @@ export interface FanoutSolverOptions {
|
|
|
98
98
|
viaHoleDiameter?: number
|
|
99
99
|
clearance?: number
|
|
100
100
|
compactBusTracks?: boolean
|
|
101
|
+
/** Allow branches belonging to the same electrical net to share copper. */
|
|
102
|
+
allowSameNetMerges?: boolean
|
|
101
103
|
singleLayerPushAndShove?: boolean
|
|
102
104
|
/**
|
|
103
105
|
* When preferred single-layer exits cannot coexist, allow a global
|
|
@@ -115,6 +117,7 @@ export interface FanoutAttemptSummary {
|
|
|
115
117
|
routedConnectionCount: number
|
|
116
118
|
failedBusIds: string[]
|
|
117
119
|
score: number
|
|
120
|
+
validationIssues?: FanoutValidationIssue[]
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
export interface FanoutSolverOutput {
|
|
@@ -124,6 +127,42 @@ export interface FanoutSolverOutput {
|
|
|
124
127
|
busLayerAssignments: Readonly<Record<string, string>>
|
|
125
128
|
busDirections: Readonly<Record<string, FanoutDirection>>
|
|
126
129
|
attempts: FanoutAttemptSummary[]
|
|
130
|
+
validation: FanoutValidationReport
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface FanoutValidationIssue {
|
|
134
|
+
code:
|
|
135
|
+
| "missing-plan"
|
|
136
|
+
| "duplicate-plan"
|
|
137
|
+
| "unknown-plan"
|
|
138
|
+
| "connection-mismatch"
|
|
139
|
+
| "source-mismatch"
|
|
140
|
+
| "termination-mismatch"
|
|
141
|
+
| "not-broken-out"
|
|
142
|
+
| "outside-routing-bounds"
|
|
143
|
+
| "disconnected-trace"
|
|
144
|
+
| "unsupported-route-point"
|
|
145
|
+
| "trace-plan-mismatch"
|
|
146
|
+
| "output-connection-missing"
|
|
147
|
+
| "output-exit-mismatch"
|
|
148
|
+
| "downstream-endpoint-lost"
|
|
149
|
+
| "plane-connection-retained"
|
|
150
|
+
| "obstacle-clearance"
|
|
151
|
+
| "via-obstacle-clearance"
|
|
152
|
+
| "different-net-trace-clearance"
|
|
153
|
+
| "different-net-trace-via-clearance"
|
|
154
|
+
| "different-net-via-clearance"
|
|
155
|
+
message: string
|
|
156
|
+
connectionName?: string
|
|
157
|
+
otherConnectionName?: string
|
|
158
|
+
busId?: string
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface FanoutValidationReport {
|
|
162
|
+
valid: boolean
|
|
163
|
+
checkedConnectionCount: number
|
|
164
|
+
brokenOutConnectionCount: number
|
|
165
|
+
issues: FanoutValidationIssue[]
|
|
127
166
|
}
|
|
128
167
|
|
|
129
168
|
export interface Point2D {
|