diff --git a/tests/LayoutPipelineSolver/__snapshots__/layout-pipeline-solver06.test.ts.snap b/tests/LayoutPipelineSolver/__snapshots__/layout-pipeline-solver06.test.ts.snap new file mode 100644 index 0000000..86950df --- /dev/null +++ b/tests/LayoutPipelineSolver/__snapshots__/layout-pipeline-solver06.test.ts.snap @@ -0,0 +1,83 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`LayoutPipelineSolver06 - decoupling caps visual snapshot 1`] = ` +{ + "capPlacements": [ + { + "id": "C10", + "rot": 0, + "x": -2.025, + "y": 1.45, + }, + { + "id": "C11", + "rot": 0, + "x": -2.025, + "y": -2.15, + }, + { + "id": "C12", + "rot": 0, + "x": -1.568, + "y": 5.955, + }, + { + "id": "C13", + "rot": 0, + "x": 0.622, + "y": 5.955, + }, + { + "id": "C14", + "rot": 0, + "x": -0.838, + "y": 5.955, + }, + { + "id": "C15", + "rot": 0, + "x": -2.298, + "y": 5.955, + }, + { + "id": "C18", + "rot": 0, + "x": -4.485, + "y": 3.218, + }, + { + "id": "C19", + "rot": 0, + "x": 1.353, + "y": 5.955, + }, + { + "id": "C7", + "rot": 0, + "x": -3.755, + "y": 3.218, + }, + { + "id": "C8", + "rot": 0, + "x": -0.108, + "y": 5.955, + }, + { + "id": "C9", + "rot": 0, + "x": -6.215, + "y": 3.218, + }, + ], + "circleCount": 0, + "lineCount": 133, + "pointCount": 79, + "rectCount": 12, + "u3": { + "rot": 0, + "x": 0.79, + "y": 0, + }, +} +`; diff --git a/tests/LayoutPipelineSolver/layout-pipeline-solver06.test.ts b/tests/LayoutPipelineSolver/layout-pipeline-solver06.test.ts new file mode 100644 index 0000000..bf2d41b --- /dev/null +++ b/tests/LayoutPipelineSolver/layout-pipeline-solver06.test.ts @@ -0,0 +1,129 @@ +import { expect, test } from "bun:test" +import { LayoutPipelineSolver } from "lib/solvers/LayoutPipelineSolver/LayoutPipelineSolver" +import { problem } from "../../pages/LayoutPipelineSolver/LayoutPipelineSolver06.page" + +test("LayoutPipelineSolver06 - decoupling caps arranged in linear rows with no overlaps", () => { + const solver = new LayoutPipelineSolver(problem) + solver.solve() + + // Verify the pipeline completed successfully + expect(solver.solved).toBe(true) + expect(solver.error).toBeNull() + + // Verify all phases ran + const phases = Object.keys(solver.timeSpentOnPhase) + expect(phases).toContain("identifyDecouplingCapsSolver") + expect(phases).toContain("chipPartitionsSolver") + expect(phases).toContain("packInnerPartitionsSolver") + expect(phases).toContain("partitionPackingSolver") + + const layout = solver.getOutputLayout() + expect(layout).toBeDefined() + + // All chips should have valid numeric placements + const chipIds = Object.keys(layout.chipPlacements) + expect(chipIds.length).toBeGreaterThan(0) + + for (const chipId of chipIds) { + const placement = layout.chipPlacements[chipId] + expect(typeof placement.x).toBe("number") + expect(typeof placement.y).toBe("number") + expect(typeof placement.ccwRotationDegrees).toBe("number") + expect(Number.isFinite(placement.x)).toBe(true) + expect(Number.isFinite(placement.y)).toBe(true) + } + + // Verify no overlaps in final layout + const overlaps = solver.checkForOverlaps(layout) + expect(overlaps).toEqual([]) + + // Visualization should be valid + const viz = solver.visualize() + expect(viz).toBeDefined() + expect(viz.rects?.length).toBeGreaterThan(0) +}) + +test("LayoutPipelineSolver06 - decoupling caps form sorted linear rows per net group", () => { + const solver = new LayoutPipelineSolver(problem) + solver.solve() + + const layout = solver.getOutputLayout() + + // Identify cap chips + const capIds = Object.keys(problem.chipMap).filter((id) => id.startsWith("C")) + expect(capIds.length).toBeGreaterThan(0) + + // All caps should be placed + for (const capId of capIds) { + expect(layout.chipPlacements[capId]).toBeDefined() + } + + // Group caps by y-coordinate (same y = same row, rounded to 1 decimal) + const capPositions = capIds.map((id) => ({ + id, + x: layout.chipPlacements[id].x, + y: layout.chipPlacements[id].y, + })) + + const yGroups = new Map() + for (const cap of capPositions) { + const yKey = cap.y.toFixed(1) + const group = yGroups.get(yKey) || [] + group.push(cap) + yGroups.set(yKey, group) + } + + // Each multi-cap row should be sorted left-to-right by x + for (const [_yKey, group] of yGroups) { + if (group.length > 1) { + const sorted = [...group].sort((a, b) => a.x - b.x) + for (let i = 1; i < sorted.length; i++) { + expect(sorted[i]!.x).toBeGreaterThan(sorted[i - 1]!.x) + } + // Verify minimum gap between adjacent caps (decouplingCapsGap = 0.2) + for (let i = 1; i < sorted.length; i++) { + const gap = sorted[i]!.x - sorted[i - 1]!.x + // Caps should have at least a small gap (not overlapping) + expect(gap).toBeGreaterThan(0.1) + } + } + } + + // Verify decoupling caps are grouped into expected number of rows + // (should have at least 2 rows for the 2 voltage groups in LayoutPipelineSolver06) + expect(yGroups.size).toBeGreaterThanOrEqual(2) +}) + +test("LayoutPipelineSolver06 - decoupling caps visual snapshot", () => { + const solver = new LayoutPipelineSolver(problem) + solver.solve() + + const layout = solver.getOutputLayout() + const viz = solver.visualize() + expect(viz).toBeDefined() + + // Extract cap placement positions for visual regression + const capIds = Object.keys(problem.chipMap) + .filter((id) => id.startsWith("C")) + .sort() + const capPlacements = capIds.map((id) => ({ + id, + x: Number(layout.chipPlacements[id].x.toFixed(3)), + y: Number(layout.chipPlacements[id].y.toFixed(3)), + rot: layout.chipPlacements[id].ccwRotationDegrees, + })) + + // Snapshot includes element counts AND actual cap positions + expect({ + rectCount: viz.rects?.length ?? 0, + lineCount: viz.lines?.length ?? 0, + circleCount: viz.circles?.length ?? 0, + pointCount: viz.points?.length ?? 0, + capPlacements, + u3: { + x: Number(layout.chipPlacements["U3"].x.toFixed(3)), + y: Number(layout.chipPlacements["U3"].y.toFixed(3)), + rot: layout.chipPlacements["U3"].ccwRotationDegrees, + }, + }).toMatchSnapshot() +})