Skip to content

Commit 3be3cdf

Browse files
fix: enforce Pipeline9 bus layers per connection
1 parent 4417f0e commit 3be3cdf

14 files changed

Lines changed: 352 additions & 50 deletions
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type {
2+
AllowedZByConnectionName,
3+
NodeWithPortPoints,
4+
PortPoint,
5+
} from "lib/types/high-density-types"
6+
7+
const addAllowedZToPortPoint = ({
8+
portPoint,
9+
allowedZByConnectionName,
10+
}: {
11+
portPoint: PortPoint
12+
allowedZByConnectionName: AllowedZByConnectionName
13+
}): PortPoint => {
14+
const allowedZ = allowedZByConnectionName[portPoint.connectionName]
15+
return allowedZ ? { ...portPoint, allowedZ } : portPoint
16+
}
17+
18+
export const addPipeline9ConnectionAllowedZToPortPoints = ({
19+
nodes,
20+
allowedZByConnectionName,
21+
}: {
22+
nodes: NodeWithPortPoints[]
23+
allowedZByConnectionName: AllowedZByConnectionName
24+
}): NodeWithPortPoints[] =>
25+
nodes.map((node) => ({
26+
...node,
27+
portPoints: node.portPoints.map((portPoint) =>
28+
addAllowedZToPortPoint({ portPoint, allowedZByConnectionName }),
29+
),
30+
portPointsInPairs: node.portPointsInPairs?.map(([start, end]) => [
31+
addAllowedZToPortPoint({ portPoint: start, allowedZByConnectionName }),
32+
addAllowedZToPortPoint({ portPoint: end, allowedZByConnectionName }),
33+
]),
34+
}))
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { SimplifiedPcbTraces } from "lib/types"
2+
import type { AllowedZByConnectionName } from "lib/types/high-density-types"
3+
import { mapLayerNameToZ } from "lib/utils/mapLayerNameToZ"
4+
5+
export const assertPipeline9TracesUseAllowedZ = ({
6+
traces,
7+
allowedZByConnectionName,
8+
layerCount,
9+
}: {
10+
traces: SimplifiedPcbTraces
11+
allowedZByConnectionName: AllowedZByConnectionName
12+
layerCount: number
13+
}): void => {
14+
for (const trace of traces) {
15+
const allowedZ = allowedZByConnectionName[trace.connection_name]
16+
if (!allowedZ) continue
17+
const disallowedWire = trace.route.find(
18+
(routePoint) =>
19+
routePoint.route_type === "wire" &&
20+
!allowedZ.includes(mapLayerNameToZ(routePoint.layer, layerCount)),
21+
)
22+
if (disallowedWire?.route_type !== "wire") continue
23+
throw new Error(
24+
`Pipeline9 routed "${trace.connection_name}" on disallowed layer "${disallowedWire.layer}"`,
25+
)
26+
}
27+
}

lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/autorouting-pipeline-solver9-preloaded-trace-graph.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
SimplifiedPcbTraces,
2828
} from "lib/types"
2929
import {
30+
AllowedZByConnectionName,
3031
HighDensityRoute,
3132
NodeWithPortPoints,
3233
} from "lib/types/high-density-types"
@@ -66,8 +67,11 @@ import { SingleLayerNodeMergerSolver } from "../../solvers/SingleLayerNodeMerger
6667
import { StrawSolver } from "../../solvers/StrawSolver/StrawSolver"
6768
import { TraceSimplificationSolver } from "../../solvers/TraceSimplificationSolver/TraceSimplificationSolver"
6869
import { TraceWidthSolver } from "../../solvers/TraceWidthSolver/TraceWidthSolver"
70+
import { addPipeline9ConnectionAllowedZToPortPoints } from "./add-pipeline9-connection-allowed-z-to-port-points"
6971
import { applyFixedRouteReplacementsToPreloadedTraces } from "./apply-fixed-route-replacements-to-preloaded-traces"
72+
import { assertPipeline9TracesUseAllowedZ } from "./assert-pipeline9-traces-use-allowed-z"
7073
import { assignUniquePcbTraceIdsToNewTraces } from "./assign-unique-pcb-trace-ids-to-new-traces"
74+
import { getPipeline9AllowedZByConnectionName } from "./get-pipeline9-allowed-z-by-connection-name"
7175
import { getPipeline9NetByConnectionName } from "./get-pipeline9-net-by-connection-name"
7276
import {
7377
getMaterializedPreloadedSectionHdRoutes,
@@ -298,6 +302,7 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver {
298302
/** Available segment points after non-component cramped points are filtered. */
299303
sharedEdgeSegmentsWithNecessaryCrampedPortPoints?: SharedEdgeSegment[]
300304
highDensityNodePortPoints?: NodeWithPortPoints[]
305+
allowedZByConnectionName: AllowedZByConnectionName = {}
301306

302307
cacheProvider: CacheProvider | null = null
303308
pipelineDef = [
@@ -354,6 +359,13 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver {
354359
onSolved: (cms) => {
355360
cms.srjWithPointPairs =
356361
cms.netToPointPairsSolver?.getNewSimpleRouteJson()
362+
cms.allowedZByConnectionName = getPipeline9AllowedZByConnectionName({
363+
srj: cms.originalSrj,
364+
connections: [
365+
...cms.originalSrj.connections,
366+
...cms.srjWithPointPairs!.connections,
367+
],
368+
})
357369
cms.colorMap = getColorMap(cms.srjWithPointPairs!, cms.connMap)
358370
cms.connMap = getConnectivityMapFromSimpleRouteJson(
359371
cms.srjWithPointPairs!,
@@ -527,6 +539,7 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver {
527539
effort: cms.effort,
528540
preserveTerminalPcbPortIds: true,
529541
minViaPadDiameter: cms.viaDiameter,
542+
allowedZByConnectionName: cms.allowedZByConnectionName,
530543
flags: {
531544
FORCE_CENTER_FIRST: true,
532545
RIPPING_ENABLED: true,
@@ -587,8 +600,14 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver {
587600
const uniformNodes =
588601
cms.uniformPortDistributionSolver?.getOutput() ?? []
589602
const fallbackNodes = portPointPathingOutput.nodesWithPortPoints
590-
const nodePortPointsSource =
603+
const unconstrainedNodePortPointsSource =
591604
uniformNodes.length > 0 ? uniformNodes : fallbackNodes
605+
const nodePortPointsSource = addPipeline9ConnectionAllowedZToPortPoints(
606+
{
607+
nodes: unconstrainedNodePortPointsSource,
608+
allowedZByConnectionName: cms.allowedZByConnectionName,
609+
},
610+
)
592611

593612
cms.highDensityNodePortPoints = structuredClone(nodePortPointsSource)
594613
const originalFixedHdRoutes = (cms.originalSrj.traces ?? []).flatMap(
@@ -1458,6 +1477,11 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver {
14581477
defaultViaHoleDiameter: this.viaHoleDiameter,
14591478
connMap: this.connMap,
14601479
})
1480+
assertPipeline9TracesUseAllowedZ({
1481+
traces: routedTraces,
1482+
allowedZByConnectionName: this.allowedZByConnectionName,
1483+
layerCount: this.srj.layerCount,
1484+
})
14611485
return assignUniquePcbTraceIdsToNewTraces(
14621486
routedTraces,
14631487
this.originalSrj.traces ?? [],
@@ -1485,12 +1509,18 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver {
14851509
"Pipeline9 invariant violated: solved pipeline is missing the unconditional power-trace expansion solver",
14861510
)
14871511
}
1488-
return [
1512+
const traces = [
14891513
...this.getPowerTraceExpansionFixedTraces().filter(
14901514
(trace) => trace.__replaces_pcb_trace_id !== undefined,
14911515
),
14921516
...this.powerTraceExpansionSolver.getOutput(),
14931517
]
1518+
assertPipeline9TracesUseAllowedZ({
1519+
traces,
1520+
allowedZByConnectionName: this.allowedZByConnectionName,
1521+
layerCount: this.srj.layerCount,
1522+
})
1523+
return traces
14941524
}
14951525

14961526
getOutputSimpleRouteJson(): SimpleRouteJson {
@@ -1506,6 +1536,11 @@ export class AutoroutingPipelineSolver9_PreloadedTraceGraph extends BaseSolver {
15061536
...this.getPowerTraceExpansionFixedTraces(),
15071537
...this.powerTraceExpansionSolver.getOutput(),
15081538
]
1539+
assertPipeline9TracesUseAllowedZ({
1540+
traces,
1541+
allowedZByConnectionName: this.allowedZByConnectionName,
1542+
layerCount: this.srj.layerCount,
1543+
})
15091544
return {
15101545
...this.originalSrj,
15111546
traces,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { SimpleRouteConnection, SimpleRouteJson } from "lib/types"
2+
import type { AllowedZByConnectionName } from "lib/types/high-density-types"
3+
import type { ConnectionName } from "lib/types/srj-types"
4+
import { getUniqueValidZLayersFromLayerNames } from "lib/utils/mapLayerNameToZ"
5+
6+
export const getPipeline9AllowedZByConnectionName = ({
7+
srj,
8+
connections,
9+
}: {
10+
srj: SimpleRouteJson
11+
connections: SimpleRouteConnection[]
12+
}): AllowedZByConnectionName => {
13+
const busAllowedZByConnectionName = new Map<ConnectionName, number[][]>()
14+
for (const bus of srj.buses ?? []) {
15+
if (!bus.allowedLayers) continue
16+
const allowedZ = getUniqueValidZLayersFromLayerNames(
17+
bus.allowedLayers,
18+
srj.layerCount,
19+
)
20+
if (allowedZ.length === 0) {
21+
throw new Error(`Bus "${bus.busId}" does not allow a valid board layer`)
22+
}
23+
for (const connectionName of bus.connectionNames) {
24+
const constraints = busAllowedZByConnectionName.get(connectionName) ?? []
25+
constraints.push(allowedZ)
26+
busAllowedZByConnectionName.set(connectionName, constraints)
27+
}
28+
}
29+
30+
const allowedZByConnectionName: Record<ConnectionName, readonly number[]> = {}
31+
for (const connection of connections) {
32+
const rootConnectionNames = new Set([
33+
connection.name,
34+
connection.rootConnectionName,
35+
...(connection.__rootConnectionNames ?? []),
36+
])
37+
const constraints = [...rootConnectionNames].flatMap((connectionName) =>
38+
connectionName
39+
? (busAllowedZByConnectionName.get(connectionName) ?? [])
40+
: [],
41+
)
42+
if (constraints.length === 0) continue
43+
44+
const allowedZ = constraints
45+
.slice(1)
46+
.reduce(
47+
(intersection, current) =>
48+
intersection.filter((z) => current.includes(z)),
49+
constraints[0]!,
50+
)
51+
if (allowedZ.length === 0) {
52+
throw new Error(
53+
`Connection "${connection.name}" has incompatible bus layer constraints`,
54+
)
55+
}
56+
allowedZByConnectionName[connection.name] = allowedZ
57+
}
58+
return allowedZByConnectionName
59+
}

lib/autorouter-pipelines/AutoroutingPipeline9_PreloadedTraceGraph/pipeline9-high-density-solver.ts

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,42 @@ type NodeBounds = {
6161

6262
const PRELOADED_TRACE_CLEARANCE = 0.15
6363

64+
type NodeLayerConstraint =
65+
| { kind: "none" }
66+
| { kind: "shared"; allowedZ: readonly number[] }
67+
| { kind: "mixed" }
68+
69+
const getNodeLayerConstraint = (
70+
node: NodeWithPortPoints,
71+
): NodeLayerConstraint => {
72+
const connectionNames = [
73+
...new Set(node.portPoints.map((portPoint) => portPoint.connectionName)),
74+
]
75+
const allowedZByConnection = connectionNames.map(
76+
(connectionName) =>
77+
node.portPoints.find(
78+
(portPoint) =>
79+
portPoint.connectionName === connectionName && portPoint.allowedZ,
80+
)?.allowedZ,
81+
)
82+
if (allowedZByConnection.every((allowedZ) => allowedZ === undefined)) {
83+
return { kind: "none" }
84+
}
85+
const firstAllowedZ = allowedZByConnection[0]
86+
if (
87+
!firstAllowedZ ||
88+
allowedZByConnection.some(
89+
(allowedZ) =>
90+
!allowedZ ||
91+
allowedZ.length !== firstAllowedZ.length ||
92+
allowedZ.some((z, index) => z !== firstAllowedZ[index]),
93+
)
94+
) {
95+
return { kind: "mixed" }
96+
}
97+
return { kind: "shared", allowedZ: firstAllowedZ }
98+
}
99+
64100
const getNodeBounds = (
65101
node: NodeWithPortPoints,
66102
margin: number,
@@ -460,13 +496,22 @@ export class Pipeline9HighDensitySolver extends BaseSolver {
460496
this.activeNode,
461497
this.connMap,
462498
)
499+
const layerConstraint = getNodeLayerConstraint(normalizedNode)
500+
if (layerConstraint.kind === "mixed") {
501+
throw new Error(
502+
`Pipeline9 cannot use regional fallback for node "${normalizedNode.capacityMeshNodeId}" with mixed per-connection layer constraints`,
503+
)
504+
}
463505
const regionalNode = {
464506
...normalizedNode,
465507
// The capacity path fixes each ordinary node to its assigned layers.
466508
// Once B01 has proved that assignment unroutable, the regional repair
467509
// must be able to add a legal layer transition; board obstacles still
468510
// constrain which of these layers it can actually use.
469-
availableZ: Array.from({ length: this.layerCount }, (_, z) => z),
511+
availableZ:
512+
layerConstraint.kind === "shared"
513+
? [...layerConstraint.allowedZ]
514+
: Array.from({ length: this.layerCount }, (_, z) => z),
470515
}
471516
const fallbackProblem = createRegionalFallbackProblem(
472517
regionalNode,
@@ -907,17 +952,27 @@ export class Pipeline9HighDensitySolver extends BaseSolver {
907952
return
908953
}
909954

910-
const nodeBounds = getNodeBounds(node, this.obstacleMargin)
955+
const layerConstraint = getNodeLayerConstraint(node)
956+
const routableNode =
957+
layerConstraint.kind === "shared"
958+
? { ...node, availableZ: [...layerConstraint.allowedZ] }
959+
: node
960+
const nodeBounds = getNodeBounds(routableNode, this.obstacleMargin)
911961
const routedCopperRadius = Math.max(this.traceWidth, this.viaDiameter) / 2
912962
const fixedObstacles = this.getUpdatedFixedHdRoutes()
913963
.filter((route) =>
914-
routeOverlapsNode(route, node, nodeBounds, routedCopperRadius),
964+
routeOverlapsNode(route, routableNode, nodeBounds, routedCopperRadius),
915965
)
916-
.flatMap((route) => convertFixedRouteToB01Obstacles(route, node))
966+
.flatMap((route) => convertFixedRouteToB01Obstacles(route, routableNode))
917967
this.stats.fixedObstacleUses =
918968
Number(this.stats.fixedObstacleUses ?? 0) + fixedObstacles.length
919969
if (fixedObstacles.length === 0) {
920-
this.startRegularSolver(node)
970+
this.startRegularSolver(routableNode)
971+
return
972+
}
973+
if (layerConstraint.kind === "mixed") {
974+
this.error = `Pipeline9 cannot route node "${node.capacityMeshNodeId}" with mixed per-connection layer constraints around fixed copper`
975+
this.failed = true
921976
return
922977
}
923978

@@ -926,7 +981,7 @@ export class Pipeline9HighDensitySolver extends BaseSolver {
926981
.map((obstacle) =>
927982
convertObstacleToB01Obstacle({
928983
obstacle,
929-
node,
984+
node: routableNode,
930985
connMap: this.connMap,
931986
layerCount: this.layerCount,
932987
}),
@@ -938,14 +993,17 @@ export class Pipeline9HighDensitySolver extends BaseSolver {
938993
this.stats.boardObstacleUses =
939994
Number(this.stats.boardObstacleUses ?? 0) + boardObstacles.length
940995

941-
this.activeNode = node
942-
if (node.width > 15 || node.height > 15) {
943-
this.activeFallbackReason = `B01 node "${node.capacityMeshNodeId}" exceeds the 15x15mm routing limit (${node.width}x${node.height}mm)`
996+
this.activeNode = routableNode
997+
if (routableNode.width > 15 || routableNode.height > 15) {
998+
this.activeFallbackReason = `B01 node "${routableNode.capacityMeshNodeId}" exceeds the 15x15mm routing limit (${routableNode.width}x${routableNode.height}mm)`
944999
this.startRegionalFallback()
9451000
return
9461001
}
9471002

948-
const normalizedNode = normalizeNodeRootConnectionNames(node, this.connMap)
1003+
const normalizedNode = normalizeNodeRootConnectionNames(
1004+
routableNode,
1005+
this.connMap,
1006+
)
9491007
this.stats.b01NodeCount = Number(this.stats.b01NodeCount ?? 0) + 1
9501008
this.activeB01Solver = new HighDensitySolverB01({
9511009
...defaultB01Params,

lib/solvers/HighDensitySolver/HighDensitySolver.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -388,9 +388,18 @@ export class HighDensitySolver extends BaseSolver {
388388
growShrinkSolutionValidator: this.growShrinkSolutionValidator,
389389
captureSearchDebug: this.captureSearchDebug,
390390
}
391-
this.activeSubSolver = this.useGrowShrinkHighDensityIntraNodeSolver
392-
? new GrowShrinkHighDensityIntraNodeSolver(intraNodeSolverParams)
393-
: new PortfolioSingleIntraNodeSolver(intraNodeSolverParams)
391+
const nodeAllowedZ = node.availableZ ?? []
392+
const requiresPerConnectionLayerConstraints = node.portPoints.some(
393+
(portPoint) =>
394+
portPoint.allowedZ !== undefined &&
395+
(portPoint.allowedZ.length !== nodeAllowedZ.length ||
396+
portPoint.allowedZ.some((z, index) => z !== nodeAllowedZ[index])),
397+
)
398+
this.activeSubSolver = requiresPerConnectionLayerConstraints
399+
? new IntraNodeRouteSolver(intraNodeSolverParams)
400+
: this.useGrowShrinkHighDensityIntraNodeSolver
401+
? new GrowShrinkHighDensityIntraNodeSolver(intraNodeSolverParams)
402+
: new PortfolioSingleIntraNodeSolver(intraNodeSolverParams)
394403
this.updateCacheStats()
395404
}
396405

0 commit comments

Comments
 (0)