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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
285 changes: 285 additions & 0 deletions tests/features/topology/fixtures/srj24-sample4-topology.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import type { GraphicsObject } from "graphics-debug"
import type {
CapacityMeshEdge,
CapacityMeshNode,
Obstacle,
SimpleRouteJson,
} from "lib/types"

export interface Srj24Sample4TopologyFixture {
inputSrj: SimpleRouteJson
capacityNodes: CapacityMeshNode[]
bgaCoreObstacleIds: ReadonlySet<string>
}

type VisualizedCapacityMeshNode = CapacityMeshNode & {
_allowsViaInPad?: boolean
}

const BGA_COMPONENT_ID = "mixed-pad-bga"
const BGA_TOP_PORT_ID = "bga-top-port"
const BOTTOM_PORT_ID = "bottom-port"
const GRID_COORDINATES = [-1.3, -0.65, 0, 0.65, 1.3]

function createPad({
obstacleId,
componentId = BGA_COMPONENT_ID,
x,
y,
width,
height,
layer = "top",
connectedTo = [],
}: {
obstacleId: string
componentId?: string
x: number
y: number
width: number
height: number
layer?: string
connectedTo?: string[]
}): Obstacle {
return {
obstacleId,
componentId,
type: "rect",
layers: [layer],
center: { x, y },
width,
height,
connectedTo,
}
}

function createBgaCore(): Obstacle[] {
return GRID_COORDINATES.flatMap((y, row) =>
GRID_COORDINATES.map((x, column) =>
createPad({
obstacleId: `core-${row}-${column}`,
x,
y,
width: 0.254,
height: 0.254,
connectedTo: x === 0 && y === 0 ? [BGA_TOP_PORT_ID] : [],
}),
),
)
}

function createSpecializedPerimeterPads(): Obstacle[] {
const horizontalPads = [-1, 1].flatMap((direction) =>
GRID_COORDINATES.map((x, index) =>
createPad({
obstacleId: `horizontal-${direction}-${index}`,
x,
y: direction * 1.95,
width: 0.3302,
height: 0.1578,
}),
),
)
const verticalPads = [-1, 1].flatMap((direction) =>
GRID_COORDINATES.map((y, index) =>
createPad({
obstacleId: `vertical-${direction}-${index}`,
x: direction * 1.95,
y,
width: 0.1578,
height: 0.3302,
}),
),
)

return [...horizontalPads, ...verticalPads]
}

function createCapacityNode(
capacityMeshNodeId: string,
availableZ: number[],
overrides: Partial<CapacityMeshNode>,
): CapacityMeshNode {
return {
capacityMeshNodeId,
center: { x: 0, y: 0 },
width: 0.254,
height: 0.254,
layer: `z${availableZ.join(",")}`,
availableZ,
...overrides,
}
}

export function createSrj24Sample4TopologyFixture(): Srj24Sample4TopologyFixture {
const bgaCore = createBgaCore()
const bottomPad = createPad({
obstacleId: "stacked-bottom-pad",
componentId: "bottom-component",
x: 0.17,
y: 0,
width: 0.59,
height: 0.64,
layer: "bottom",
connectedTo: [BOTTOM_PORT_ID],
})

return {
inputSrj: {
layerCount: 6,
minTraceWidth: 0.1,
minViaPadDiameter: 0.3,
minViaHoleDiameter: 0.15,
obstacles: [...bgaCore, ...createSpecializedPerimeterPads(), bottomPad],
connections: [
{
name: "stacked-terminal-net",
pointsToConnect: [
{ pointId: BGA_TOP_PORT_ID, x: 0, y: 0, layer: "top" },
{ pointId: BOTTOM_PORT_ID, x: 0.17, y: 0, layer: "bottom" },
],
},
],
bounds: { minX: -4, maxX: 4, minY: -4, maxY: 4 },
},
capacityNodes: [
createCapacityNode("target-top", [0], {
_containsTarget: true,
_containsObstacle: true,
_targetConnectionName: "stacked-terminal-net",
}),
createCapacityNode("target-bottom", [5], {
center: { x: 0.17, y: 0 },
width: 0.59,
height: 0.64,
_containsTarget: true,
_containsObstacle: true,
_targetConnectionName: "stacked-terminal-net",
}),
createCapacityNode("free-top", [0], {
center: { x: 0.452, y: 0 },
width: 0.65,
}),
createCapacityNode("free-bottom", [5], {
center: { x: 0.665, y: 0 },
width: 0.4,
}),
],
bgaCoreObstacleIds: new Set(
bgaCore.map((obstacle) => obstacle.obstacleId!),
),
}
}

export function visualizeMixedComponent({
inputSrj,
selectedObstacleIds = new Set(),
}: {
inputSrj: SimpleRouteJson
selectedObstacleIds?: ReadonlySet<string>
}): GraphicsObject {
return {
rects: inputSrj.obstacles.map((obstacle) => {
const selected =
obstacle.obstacleId !== undefined &&
selectedObstacleIds.has(obstacle.obstacleId)

return {
center: obstacle.center,
width: obstacle.width,
height: obstacle.height,
fill: selected
? "#86efac"
: obstacle.layers.includes("bottom")
? "#fca5a5"
: "#cbd5e1",
stroke: selected ? "#15803d" : "#64748b",
label: obstacle.obstacleId,
}
}),
}
}

export function visualizeLayerAccess({
nodes,
edges,
}: {
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]),
)
const nodeById = new Map(nodes.map((node) => [node.capacityMeshNodeId, node]))
const rects: NonNullable<GraphicsObject["rects"]> = []
const lines: NonNullable<GraphicsObject["lines"]> = []
const texts: NonNullable<GraphicsObject["texts"]> = []

for (let z = 0; z < 6; z++) {
texts.push({
x: -0.6,
y: 2.5 - z,
text: `z${z}`,
anchorSide: "center_right",
fontSize: 0.18,
})
}

for (const node of nodes) {
const x = xByNodeId.get(node.capacityMeshNodeId)!
texts.push({
x,
y: 3.05,
text: node.capacityMeshNodeId,
anchorSide: "bottom_center",
fontSize: 0.13,
})
for (const z of node.availableZ) {
rects.push({
center: { x, y: 2.5 - z },
width: 0.8,
height: 0.46,
fill: node._containsTarget ? "#fca5a5" : "#bae6fd",
stroke: node._allowsViaInPad ? "#15803d" : "#475569",
})
}
if (node._allowsViaInPad && node.availableZ.length > 1) {
lines.push({
points: [
{ x, y: -2.23 },
{ x, y: 2.23 },
],
strokeColor: "#16a34a",
strokeWidth: 0.1,
})
}
}

for (const edge of edges) {
const [firstNodeId, secondNodeId] = edge.nodeIds
const firstNode = nodeById.get(firstNodeId)
const secondNode = nodeById.get(secondNodeId)
if (!firstNode || !secondNode) continue

const sharedLayers = firstNode.availableZ.filter((z) =>
secondNode.availableZ.includes(z),
)
for (const z of sharedLayers) {
lines.push({
points: [
{ x: xByNodeId.get(firstNodeId)!, y: 2.5 - z },
{ x: xByNodeId.get(secondNodeId)!, y: 2.5 - z },
],
strokeColor: "#2563eb",
strokeWidth: 0.08,
})
}
}

return { rects, lines, texts }
}
77 changes: 77 additions & 0 deletions tests/features/topology/srj24-sample4-topology-repro.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { expect, test } from "bun:test"
import { CapacityMeshEdgeSolver } from "lib/solvers/CapacityMeshSolver/CapacityMeshEdgeSolver"
import { ComponentDetectionSolver } from "lib/solvers/ComponentDetectionSolver/ComponentDetectionSolver"
import { getGraphicsSvgFrames } from "tests/fixtures/solver-svg-frames"
import {
createSrj24Sample4TopologyFixture,
visualizeLayerAccess,
visualizeMixedComponent,
} from "./fixtures/srj24-sample4-topology.fixture"

test("reproduces the srj24 sample 4 topology gap", async (): Promise<void> => {
const fixture = createSrj24Sample4TopologyFixture()
const componentDetectionSolver = new ComponentDetectionSolver({
inputSrj: fixture.inputSrj,
})
componentDetectionSolver.solve()
const detectedComponents = componentDetectionSolver.getOutput()

const edgeSolver = new CapacityMeshEdgeSolver(fixture.capacityNodes)
edgeSolver.solve()
const targetTop = fixture.capacityNodes.find(
(node) => node.capacityMeshNodeId === "target-top",
)!
const targetBottom = fixture.capacityNodes.find(
(node) => node.capacityMeshNodeId === "target-bottom",
)!

expect(
detectedComponents.some(
(component) =>
component.componentId === "mixed-pad-bga" &&
component.componentKind === "bga",
),
).toBe(false)
expect(edgeSolver.hasEdgeBetween(targetTop, targetBottom)).toBe(false)

const svg = getGraphicsSvgFrames({
frames: [
{
name: "Before: mixed pad package",
step: 0,
iteration: 0,
graphics: visualizeMixedComponent({ inputSrj: fixture.inputSrj }),
},
{
name: "Hidden regular BGA core",
step: 1,
iteration: 1,
graphics: visualizeMixedComponent({
inputSrj: fixture.inputSrj,
selectedObstacleIds: fixture.bgaCoreObstacleIds,
}),
},
{
name: "Before: isolated terminal layers",
step: 2,
iteration: 2,
graphics: visualizeLayerAccess({
nodes: fixture.capacityNodes,
edges: [],
}),
},
{
name: "Before: no cross-layer edge",
step: 3,
iteration: 3,
graphics: visualizeLayerAccess({
nodes: fixture.capacityNodes,
edges: edgeSolver.edges,
}),
},
],
columns: 2,
})

await expect(svg).toMatchSvgSnapshot(import.meta.path, { scale: 2 })
})
Loading