From 505ea31cf5521683e2d9090b5b10d2dcad95159a Mon Sep 17 00:00:00 2001 From: ShiboSoftwareDev Date: Thu, 30 Jul 2026 18:17:37 +0200 Subject: [PATCH] test(pipeline7): reproduce srj24 sample 4 topology gap --- .../srj24-sample4-topology-repro.snap.svg | 44 +++ .../srj24-sample4-topology.fixture.ts | 285 ++++++++++++++++++ .../srj24-sample4-topology-repro.test.ts | 77 +++++ 3 files changed, 406 insertions(+) create mode 100644 tests/features/topology/__snapshots__/srj24-sample4-topology-repro.snap.svg create mode 100644 tests/features/topology/fixtures/srj24-sample4-topology.fixture.ts create mode 100644 tests/features/topology/srj24-sample4-topology-repro.test.ts diff --git a/tests/features/topology/__snapshots__/srj24-sample4-topology-repro.snap.svg b/tests/features/topology/__snapshots__/srj24-sample4-topology-repro.snap.svg new file mode 100644 index 000000000..497443e52 --- /dev/null +++ b/tests/features/topology/__snapshots__/srj24-sample4-topology-repro.snap.svg @@ -0,0 +1,44 @@ +Before: mixed pad packagestep:0 layer:allHidden regular BGA corestep:1 layer:allz0z1z2z3z4z5target-toptarget-bottomfree-topfree-bottomBefore: isolated terminal layersstep:2 layer:allz0z1z2z3z4z5target-toptarget-bottomfree-topfree-bottomBefore: no cross-layer edgestep:3 layer:all \ No newline at end of file diff --git a/tests/features/topology/fixtures/srj24-sample4-topology.fixture.ts b/tests/features/topology/fixtures/srj24-sample4-topology.fixture.ts new file mode 100644 index 000000000..f3d229aeb --- /dev/null +++ b/tests/features/topology/fixtures/srj24-sample4-topology.fixture.ts @@ -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 +} + +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 { + 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 +}): 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 = [] + const lines: NonNullable = [] + const texts: NonNullable = [] + + 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 } +} diff --git a/tests/features/topology/srj24-sample4-topology-repro.test.ts b/tests/features/topology/srj24-sample4-topology-repro.test.ts new file mode 100644 index 000000000..e96d9724f --- /dev/null +++ b/tests/features/topology/srj24-sample4-topology-repro.test.ts @@ -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 => { + 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 }) +})