Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
ComponentDetectionSolverParams,
DetectedComponent,
} from "./ComponentDetectionSolver"
import { getBgaLikeObstacleSubset } from "./detectors/bga/BgaComponentDetector"
import { detectComponentKind, type ComponentKind } from "./detectors"

/**
Expand Down Expand Up @@ -243,7 +244,7 @@ export class RectBoundsComponentDetectionStage extends BaseSolver {
grouped[obstacle.componentId].push(obstacle)
}

const detectedEntries = Object.entries(grouped).filter(
const detectedEntries = Object.entries(grouped).flatMap(
([componentId, memberObstacles]) => {
const boardBounds = this.inputSrj.bounds
const hasOutOfBoundsObstacle = memberObstacles.some((obstacle) => {
Expand All @@ -257,17 +258,22 @@ export class RectBoundsComponentDetectionStage extends BaseSolver {
})

if (hasOutOfBoundsObstacle) {
return false
return []
}

const componentKind = detectComponentKind({
memberObstacles,
inputSrj: this.inputSrj,
})
if (!componentKind) return false
if (!componentKind) return []

componentKinds[componentId] = componentKind
return true
const detectedObstacles =
componentKind === "bga"
? getBgaLikeObstacleSubset(memberObstacles)!
: memberObstacles

return [[componentId, detectedObstacles] as const]
},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ const MAX_BGA_PAD_ASPECT_RATIO = 1.5
const MIN_INTERIOR_PAD_COUNT = 1
const AXIS_CLUSTER_EPSILON = 1e-3

export function clusterAxisValues(values: number[]) {
type BgaGridRow = {
y: number
obstaclesByX: Map<number, Obstacle>
}

export function clusterAxisValues(values: number[]): number[] {
const sortedValues = [...values].sort((a, b) => a - b)
const clustered: number[] = []

Expand All @@ -25,7 +30,7 @@ export function clusterAxisValues(values: number[]) {
return clustered
}

function hasUniformDimensionWithinTolerance(values: number[]) {
function hasUniformDimensionWithinTolerance(values: number[]): boolean {
const minValue = Math.min(...values)
const maxValue = Math.max(...values)

Expand All @@ -34,7 +39,7 @@ function hasUniformDimensionWithinTolerance(values: number[]) {
return maxValue / minValue <= 1 + MAX_BGA_PAD_SIZE_VARIANCE
}

function hasUniformPadDimensions(memberObstacles: Obstacle[]) {
function hasUniformPadDimensions(memberObstacles: Obstacle[]): boolean {
return (
hasUniformDimensionWithinTolerance(
memberObstacles.map((obstacle) => obstacle.width),
Expand All @@ -58,7 +63,7 @@ function hasInteriorPads(
memberObstacles: Obstacle[],
rowAxisValues: number[],
columnAxisValues: number[],
) {
): boolean {
if (rowAxisValues.length < MIN_BGA_AXIS_COUNT) return false
if (columnAxisValues.length < MIN_BGA_AXIS_COUNT) return false

Expand All @@ -79,7 +84,7 @@ function hasInteriorPads(
return interiorPadCount >= MIN_INTERIOR_PAD_COUNT
}

export function isBgaLikeComponent(memberObstacles: Obstacle[]) {
function isDirectBgaLikeComponent(memberObstacles: Obstacle[]): boolean {
if (!hasUniformPadDimensions(memberObstacles)) return false
if (!hasBgaLikePadAspectRatio(memberObstacles)) return false

Expand Down Expand Up @@ -109,6 +114,96 @@ export function isBgaLikeComponent(memberObstacles: Obstacle[]) {
return true
}

function getPadGeometryKey(obstacle: Obstacle): string {
return [
Math.round(obstacle.width / AXIS_CLUSTER_EPSILON),
Math.round(obstacle.height / AXIS_CLUSTER_EPSILON),
obstacle.layers.join(","),
].join(":")
}

function hasUniformPitch(values: number[]): boolean {
if (values.length < MIN_BGA_AXIS_COUNT) return false

const sortedValues = [...values].sort((a, b) => a - b)
const pitch = sortedValues[1]! - sortedValues[0]!
if (pitch <= AXIS_CLUSTER_EPSILON) return false

return sortedValues.every(
(value, index) =>
index === 0 ||
Math.abs(value - sortedValues[index - 1]! - pitch) <=
AXIS_CLUSTER_EPSILON,
)
}

function getRowsForPadGeometry(obstacles: Obstacle[]): BgaGridRow[] {
const rowsByY = new Map<number, BgaGridRow>()

for (const obstacle of obstacles) {
const yKey = Math.round(obstacle.center.y / AXIS_CLUSTER_EPSILON)
const xKey = Math.round(obstacle.center.x / AXIS_CLUSTER_EPSILON)
const row = rowsByY.get(yKey) ?? {
y: obstacle.center.y,
obstaclesByX: new Map<number, Obstacle>(),
}
row.obstaclesByX.set(xKey, obstacle)
rowsByY.set(yKey, row)
}

return [...rowsByY.values()]
}

function findLargestCompleteGrid(obstacles: Obstacle[]): Obstacle[] {
const rows = getRowsForPadGeometry(obstacles)
let largestGrid: Obstacle[] = []

for (const candidateRow of rows) {
const xKeys = [...candidateRow.obstaclesByX.keys()]
if (
!hasUniformPitch(
[...candidateRow.obstaclesByX.values()].map(
(obstacle) => obstacle.center.x,
),
)
) {
continue
}

const matchingRows = rows.filter((row) =>
xKeys.every((xKey) => row.obstaclesByX.has(xKey)),
)
if (!hasUniformPitch(matchingRows.map((row) => row.y))) continue

const grid = matchingRows.flatMap((row) =>
xKeys.map((xKey) => row.obstaclesByX.get(xKey)!),
)
if (grid.length > largestGrid.length) largestGrid = grid
}

return largestGrid
}

export function getBgaLikeObstacleSubset(
memberObstacles: Obstacle[],
): Obstacle[] | null {
if (isDirectBgaLikeComponent(memberObstacles)) return memberObstacles

const geometryGroups = Map.groupBy(memberObstacles, getPadGeometryKey)
let largestGrid: Obstacle[] = []

for (const geometryGroup of geometryGroups.values()) {
const grid = findLargestCompleteGrid(geometryGroup)
if (grid.length > largestGrid.length) largestGrid = grid
}

return largestGrid.length > 0 ? largestGrid : null
}

export function isBgaLikeComponent(memberObstacles: Obstacle[]): boolean {
return getBgaLikeObstacleSubset(memberObstacles) !== null
}

export class BgaComponentDetector implements ComponentDetector {
static readonly componentKind = "bga"
readonly componentKind = BgaComponentDetector.componentKind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,30 @@ export interface ComponentTopologyGeneratorSolverParams {

export type ComponentTopologyGeneratorSolverOutput = CapacityMeshNode[]

export function isObstacleInDetectedComponent(
obstacle: Obstacle,
detectedComponent: DetectedComponent,
): boolean {
if (obstacle.componentId !== detectedComponent.componentId) return false

const { bounds } = detectedComponent
return (
obstacle.center.x >= bounds.minX &&
obstacle.center.x <= bounds.maxX &&
obstacle.center.y >= bounds.minY &&
obstacle.center.y <= bounds.maxY
)
}

export function createReplacementObstacleForComponent({
detectedComponent,
inputSrj,
}: {
detectedComponent: DetectedComponent
inputSrj: SimpleRouteJson
}): Obstacle & { obstacleId: string } {
const memberObstacles = inputSrj.obstacles.filter(
(obstacle) => obstacle.componentId === detectedComponent.componentId,
const memberObstacles = inputSrj.obstacles.filter((obstacle) =>
isObstacleInDetectedComponent(obstacle, detectedComponent),
)
const zLayers = Array.from({ length: inputSrj.layerCount }, (_, z) => z)
const layers = zLayers.map((z) => mapZToLayerName(z, inputSrj.layerCount))
Expand Down Expand Up @@ -62,17 +77,14 @@ export function createComponentObstacleSrj({
detectedComponents: DetectedComponent[]
inputSrj: SimpleRouteJson
}): SimpleRouteJson {
const detectedComponentIds = new Set(
detectedComponents.map((component) => component.componentId),
)

return {
...structuredClone(inputSrj),
obstacles: [
...inputSrj.obstacles.filter(
(obstacle) =>
!obstacle.componentId ||
!detectedComponentIds.has(obstacle.componentId),
!detectedComponents.some((detectedComponent) =>
isObstacleInDetectedComponent(obstacle, detectedComponent),
),
),
...detectedComponents.map((detectedComponent) =>
createReplacementObstacleForComponent({
Expand Down
5 changes: 3 additions & 2 deletions lib/solvers/TopologyPlanningSolver/topologyPlanningShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { ComponentKind } from "lib/solvers/ComponentDetectionSolver/detecto
import {
createComponentObstacleSrj,
createReplacementObstacleForComponent,
isObstacleInDetectedComponent,
} from "lib/solvers/ComponentTopologyGeneratorSolver/ComponentTopologyGeneratorSolver"
import type { DetectedComponent } from "lib/solvers/ComponentDetectionSolver/ComponentDetectionSolver"
import {
Expand Down Expand Up @@ -158,8 +159,8 @@ function serializeDetectedComponents({
inputSrj: SimpleRouteJson
}): SerializedTopologyComponentInput[] {
return detectedComponents.map((detectedComponent) => {
const memberObstacles = inputSrj.obstacles.filter(
(obstacle) => obstacle.componentId === detectedComponent.componentId,
const memberObstacles = inputSrj.obstacles.filter((obstacle) =>
isObstacleInDetectedComponent(obstacle, detectedComponent),
)

return {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 36 additions & 11 deletions tests/features/topology/fixtures/srj24-sample4-topology.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ export interface Srj24Sample4TopologyFixture {
inputSrj: SimpleRouteJson
capacityNodes: CapacityMeshNode[]
bgaCoreObstacleIds: ReadonlySet<string>
nonCoreObstacleIds: ReadonlySet<string>
}

type VisualizedCapacityMeshNode = CapacityMeshNode & {
_allowsViaInPad?: boolean
_isBgaViaRegion?: boolean
}

const BGA_COMPONENT_ID = "mixed-pad-bga"
Expand Down Expand Up @@ -94,6 +95,29 @@ function createSpecializedPerimeterPads(): Obstacle[] {
return [...horizontalPads, ...verticalPads]
}

function createIrregularSquarePerimeterPads(): Obstacle[] {
const positions = [
[-2.6, -2.6],
[-1.95, -2.3],
[-2.3, -1.95],
[1.95, 2.3],
[2.3, 1.95],
[2.6, 2.6],
[-2.6, 2.6],
[2.6, -2.6],
]

return positions.map(([x, y], index) =>
createPad({
obstacleId: `irregular-square-${index}`,
x: x!,
y: y!,
width: 0.254,
height: 0.254,
}),
)
}

function createCapacityNode(
capacityMeshNodeId: string,
availableZ: number[],
Expand All @@ -112,6 +136,10 @@ function createCapacityNode(

export function createSrj24Sample4TopologyFixture(): Srj24Sample4TopologyFixture {
const bgaCore = createBgaCore()
const nonCoreObstacles = [
...createSpecializedPerimeterPads(),
...createIrregularSquarePerimeterPads(),
]
const bottomPad = createPad({
obstacleId: "stacked-bottom-pad",
componentId: "bottom-component",
Expand All @@ -129,7 +157,7 @@ export function createSrj24Sample4TopologyFixture(): Srj24Sample4TopologyFixture
minTraceWidth: 0.1,
minViaPadDiameter: 0.3,
minViaHoleDiameter: 0.15,
obstacles: [...bgaCore, ...createSpecializedPerimeterPads(), bottomPad],
obstacles: [...bgaCore, ...nonCoreObstacles, bottomPad],
connections: [
{
name: "stacked-terminal-net",
Expand Down Expand Up @@ -167,6 +195,9 @@ export function createSrj24Sample4TopologyFixture(): Srj24Sample4TopologyFixture
bgaCoreObstacleIds: new Set(
bgaCore.map((obstacle) => obstacle.obstacleId!),
),
nonCoreObstacleIds: new Set(
nonCoreObstacles.map((obstacle) => obstacle.obstacleId!),
),
}
}

Expand Down Expand Up @@ -206,14 +237,8 @@ export function visualizeLayerAccess({
nodes: VisualizedCapacityMeshNode[]
edges: CapacityMeshEdge[]
}): GraphicsObject {
const orderedNodeIds = [
"target-top",
"free-top",
"target-bottom",
"free-bottom",
]
const xByNodeId = new Map(
orderedNodeIds.map((nodeId, index) => [nodeId, index * 1.4]),
nodes.map((node, index) => [node.capacityMeshNodeId, index * 1.4]),
)
const nodeById = new Map(nodes.map((node) => [node.capacityMeshNodeId, node]))
const rects: NonNullable<GraphicsObject["rects"]> = []
Expand Down Expand Up @@ -245,10 +270,10 @@ export function visualizeLayerAccess({
width: 0.8,
height: 0.46,
fill: node._containsTarget ? "#fca5a5" : "#bae6fd",
stroke: node._allowsViaInPad ? "#15803d" : "#475569",
stroke: node._isBgaViaRegion ? "#15803d" : "#475569",
})
}
if (node._allowsViaInPad && node.availableZ.length > 1) {
if (node._isBgaViaRegion && node.availableZ.length > 1) {
lines.push({
points: [
{ x, y: -2.23 },
Expand Down
Loading
Loading