import { createAnalysisFramePlan, playbackFrameAtTime, sceneHasTrajectories, scenePlaybackState, } from "/apps/acoustic-camera/timeline.js"; import { alignExperimentToScene, canonicalStringify, createResultDocument, estimateExperimentWorkload, hashSimulationInputs, shiftSceneDistance, } from "/apps/acoustic-camera/model.js"; import { validateResult } from "/packages/contracts/validation.js"; function assertNear(actual, expected, tolerance, label) { assert( Math.abs(actual - expected) <= tolerance, `${label}: expected ${expected} ± ${tolerance}, received ${actual}`, ); } function assertThrows(callback, expectedText, label) { let error; try { callback(); } catch (caught) { error = caught; } assert(error instanceof Error, `${label}: expected an error`); assert(error.message.includes(expectedText), `${label}: unexpected message ${error.message}`); } test("Camera detects moving source trajectories", () => { assertEqual(sceneHasTrajectories(null), false, "missing scene"); assertEqual(sceneHasTrajectories({ sources: [{ id: "fixed", position: [0, 0, 2] }] }), false, "fixed scene"); assertEqual(sceneHasTrajectories({ sources: [{ id: "moving", position: [0, 0, 2], trajectory: [ { time: 0, position: [0, 0, 2] }, { time: 1, position: [1, 0, 2] }, ], }], }), true, "moving scene"); }); test("Camera analysis plan covers a scene and respects its frame budget", () => { const scene = { duration: 2, sampleRate: 1000, sources: [] }; const options = { fftSize: 100, targetMapHz: 4, maxFrames: 3 }; const first = createAnalysisFramePlan(scene, options); const second = createAnalysisFramePlan(scene, options); assertEqual(first.frameCount, 3, "frame count is capped"); assertNear(first.minimumWindowDuration, 0.1, 1e-12, "minimum FFT window"); assertNear(first.windowDuration, 2 / 3, 1e-12, "windows grow to cover capped timeline"); assertEqual(first.frames[0].startTime, 0, "first frame begins at zero"); assertEqual(first.frames.at(-1).endTime, 2, "last frame reaches scene end"); first.frames.forEach((frame, index) => { assertEqual(frame.index, index, `frame ${index} index`); assertNear(frame.sampleTime, (frame.startTime + frame.endTime) / 2, 1e-12, `frame ${index} midpoint`); if (index > 0) { assert(frame.startTime <= first.frames[index - 1].endTime, `frame ${index} leaves no timeline gap`); } }); assertEqual(JSON.stringify(second), JSON.stringify(first), "frame planning is deterministic"); }); test("Camera analysis windows obey FFT and short-scene constraints", () => { const fftLimited = createAnalysisFramePlan( { duration: 1, sampleRate: 1000, sources: [] }, { fftSize: 400, targetMapHz: 10, maxFrames: 100 }, ); assertEqual(fftLimited.frameCount, 10, "requested timeline frames"); assertNear(fftLimited.windowDuration, 0.4, 1e-12, "FFT-sized window"); assertEqual(fftLimited.frames[0].startTime, 0, "overlapping first window"); assertEqual(fftLimited.frames.at(-1).endTime, 1, "overlapping last window"); assert(fftLimited.frames.every((frame) => frame.duration >= 0.4 - 1e-12), "no window is shorter than one FFT"); const shortScene = createAnalysisFramePlan( { duration: 0.05, sampleRate: 1000, sources: [] }, { fftSize: 100, targetMapHz: 100, maxFrames: 50 }, ); assertEqual(shortScene.frameCount, 1, "short scene avoids duplicate frames"); assertEqual(shortScene.windowDuration, 0.05, "window is bounded by scene duration"); assertEqual(shortScene.zeroPaddingRequired, true, "short scene reports FFT padding requirement"); assertNear(shortScene.frames[0].sampleTime, 0.025, 1e-12, "short scene midpoint"); }); test("Camera playback selects and interpolates bounded analysis frames", () => { const plan = createAnalysisFramePlan( { duration: 1, sampleRate: 1000, sources: [] }, { fftSize: 100, targetMapHz: 4, maxFrames: 10 }, ); const middle = playbackFrameAtTime(plan, 0.5); assertEqual(middle.leftFrameIndex, 1, "middle left frame"); assertEqual(middle.rightFrameIndex, 2, "middle right frame"); assertEqual(middle.frameIndex, 2, "nearest frame resolves midpoint ties forward"); assertNear(middle.progress, 0.5, 1e-12, "middle interpolation progress"); assertNear(middle.framePosition, 1.5, 1e-12, "fractional frame position"); const before = playbackFrameAtTime(plan, -10); assertEqual(before.time, 0, "negative playback time is clamped"); assertEqual(before.frameIndex, 0, "negative playback selects first frame"); const after = playbackFrameAtTime(plan, 10); assertEqual(after.time, 1, "late playback time is clamped"); assertEqual(after.frameIndex, 3, "late playback selects last frame"); assertEqual(after.sceneProgress, 1, "late playback progress"); }); test("Camera playback resolves moving positions without mutating the scene", () => { const movingSource = { id: "moving", position: [99, 99, 99], trajectory: [ { time: 0, position: [0, 0, 2] }, { time: 1, position: [4, 2, 2] }, ], }; const fixedSource = { id: "fixed", position: [-1, 1, 3] }; const scene = { duration: 1, sampleRate: 1000, sources: [movingSource, fixedSource] }; const plan = createAnalysisFramePlan(scene, { fftSize: 100, targetMapHz: 4 }); const state = scenePlaybackState(scene, plan, 0.25); assertNear(state.scene.sources[0].position[0], 1, 1e-12, "moving x"); assertNear(state.scene.sources[0].position[1], 0.5, 1e-12, "moving y"); assertEqual(state.scene.sources[1].position.join(","), "-1,1,3", "fixed source"); assertEqual(state.sourcePositions[0].sourceId, "moving", "position metadata source id"); assertEqual(state.sourcePositions[0].position.join(","), "1,0.5,2", "position metadata value"); assertEqual(movingSource.position.join(","), "99,99,99", "input source remains unchanged"); assert(state.scene !== scene && state.scene.sources[0] !== movingSource, "sampled scene is immutable"); }); test("Camera timeline rejects non-finite and invalid planning inputs", () => { const scene = { duration: 1, sampleRate: 1000, sources: [] }; assertThrows( () => createAnalysisFramePlan({ ...scene, duration: Number.NaN }, { fftSize: 100 }), "scene.duration", "NaN duration", ); assertThrows( () => createAnalysisFramePlan(scene, { fftSize: Number.POSITIVE_INFINITY }), "fftSize", "infinite FFT size", ); assertThrows( () => createAnalysisFramePlan(scene, { fftSize: 100, targetMapHz: 0 }), "targetMapHz", "zero map rate", ); assertThrows( () => createAnalysisFramePlan(scene, { fftSize: 100, maxFrames: 1.5 }), "maxFrames", "fractional frame limit", ); assertThrows( () => createAnalysisFramePlan(scene, { fftSize: 100, windowDuration: Number.NaN }), "windowDuration", "NaN analysis window", ); const plan = createAnalysisFramePlan(scene, { fftSize: 100 }); assertThrows(() => playbackFrameAtTime(plan, Number.NaN), "time", "NaN playback time"); assertThrows( () => scenePlaybackState({ ...scene, duration: 2 }, plan, 0.5), "scene.duration", "scene and plan mismatch", ); }); test("Camera input hash is canonical across object key order", async () => { const first = { arrayDefinition: { id: "array", microphones: [ { id: "mic-1", position: [0, 0, 0] }, { id: "mic-2", position: [0.1, 0, 0] }, ], }, scene: { id: "scene", sampleRate: 8000, sources: [{ id: "source", position: [0, 0, 2], signal: { type: "tone", frequency: 1000 } }], }, experiment: { id: "experiment", scan: { z: 2, columns: 8, rows: 8 }, frequencyBands: [{ id: "band", minHz: 900, maxHz: 1100 }], }, }; const reordered = { experiment: { frequencyBands: [{ maxHz: 1100, minHz: 900, id: "band" }], scan: { rows: 8, columns: 8, z: 2 }, id: "experiment", }, scene: { sources: [{ signal: { frequency: 1000, type: "tone" }, position: [0, 0, 2], id: "source" }], sampleRate: 8000, id: "scene", }, arrayDefinition: { microphones: [ { position: [0, 0, 0], id: "mic-1" }, { position: [0.1, 0, 0], id: "mic-2" }, ], id: "array", }, }; assertEqual(canonicalStringify({ z: undefined, b: 2, a: { d: 4, c: 3 } }), '{"a":{"c":3,"d":4},"b":2}', "canonical JSON"); const firstHash = await hashSimulationInputs(first); const reorderedHash = await hashSimulationInputs(reordered); assertEqual(firstHash, reorderedHash, "key ordering does not alter hash"); assert(/^[a-f0-9]{64}$/.test(firstHash), "hash is lowercase SHA-256"); reordered.scene.sources[0].position[0] = 0.01; const changedHash = await hashSimulationInputs(reordered); assert(changedHash !== firstHash, "semantic input change alters hash"); }); test("Camera workload counts unique overlapping FFT bins", () => { const arrayDefinition = { microphones: Array.from({ length: 3 }, (_, index) => ({ id: `mic-${index}`, position: [index, 0, 0] })), }; const scene = { sampleRate: 8000, duration: 1 }; const experiment = { scan: { columns: 10, rows: 8 }, stft: { fftSize: 256, hopSize: 128 }, frequencyBands: [ { id: "lower", minHz: 100, maxHz: 200 }, { id: "upper", minHz: 187.5, maxHz: 250 }, ], }; const workload = estimateExperimentWorkload(arrayDefinition, scene, experiment); assertEqual(workload.pointCount, 80, "scan point count"); assertEqual(workload.microphoneCount, 3, "microphone count"); assertEqual(workload.frameCount, 62, "STFT frame count"); assertEqual(JSON.stringify(workload.bandBins), JSON.stringify([[4, 5, 6], [6, 7, 8]]), "bins per band"); assertEqual(workload.selectedBinCount, 5, "overlapping bin is counted once"); assertEqual(workload.complexMacCount, 74_400, "estimated complex MAC count"); assertNear(workload.scalingVs50, 80 / 2500, 1e-15, "50x50 scaling"); assertNear(workload.distanceMiB, (80 * 3 * 4) / 1024 ** 2, 1e-15, "distance table size"); }); test("Camera distance shift keeps the scene and scan on one z plane", () => { const scene = { sources: [ { id: "one", position: [0.1, -0.2, 2] }, { id: "two", position: [-0.3, 0.4, 2.5] }, ], image: { plane: { z: 2, xMin: -1, xMax: 1, yMin: -1, yMax: 1 } }, }; const experiment = { scan: { z: 2 } }; shiftSceneDistance(scene, experiment, 3.25); assertEqual(experiment.scan.z, 3.25, "scan z"); assertEqual(scene.sources[0].position.join(","), "0.1,-0.2,3.25", "first source z shift"); assertEqual(scene.sources[1].position.join(","), "-0.3,0.4,3.75", "relative source depth retained"); assertEqual(scene.image.plane.z, 3.25, "background image plane z shift"); }); test("Camera aligns an experiment to an imported scene without changing grid resolution", () => { const experiment = { scan: { z: 2, xMin: -1, xMax: 1, yMin: -1, yMax: 1, columns: 32, rows: 24 }, }; const imageScene = { sources: [{ id: "one", position: [0.2, -0.1, 4.5] }], image: { plane: { z: 4.5, xMin: -2, xMax: 3, yMin: -1.5, yMax: 2.5 } }, }; assertEqual(alignExperimentToScene(experiment, imageScene), "image-plane", "alignment source"); assertEqual( JSON.stringify(experiment.scan), JSON.stringify({ z: 4.5, xMin: -2, xMax: 3, yMin: -1.5, yMax: 2.5, columns: 32, rows: 24, }), "image plane becomes scan plane while rows and columns stay unchanged", ); const sourceScene = { sources: [ { id: "one", position: [-0.5, 0.1, 3] }, { id: "two", position: [0.5, 0.2, 5] }, ], }; assertEqual(alignExperimentToScene(experiment, sourceScene), "source-bounds", "source fallback"); assertEqual(experiment.scan.z, 4, "median source depth"); assertNear(experiment.scan.xMin, -0.75, 1e-12, "source x minimum with padding"); assertNear(experiment.scan.xMax, 0.75, 1e-12, "source x maximum with padding"); assertNear(experiment.scan.yMin, -0.15, 1e-12, "source y minimum with padding"); assertNear(experiment.scan.yMax, 0.45, 1e-12, "source y maximum with padding"); assertEqual(experiment.scan.columns, 32, "columns retained after source fallback"); assertEqual(experiment.scan.rows, 24, "rows retained after source fallback"); }); test("Camera result document is schema-safe and linked to its snapshot", async () => { const arrayDefinition = { id: "test-array", name: "Test array", microphones: [ { id: "mic-1", position: [0, 0, 0] }, { id: "mic-2", position: [0.1, 0, 0] }, ], }; const scene = { id: "test-scene", sources: [{ id: "source-1", position: [0, 0, 2] }], }; const experiment = { id: "test-experiment", arrayId: arrayDefinition.id, sceneId: scene.id, scan: { rows: 2, columns: 2, xMin: -1, xMax: 1, yMin: -1, yMax: 1, z: 2 }, beamforming: { backend: "auto" }, frequencyBands: [{ id: "band-1", color: "#65f2bf" }], }; const run = { backend: "cpu", signalStats: { preNormalizationPeak: 0.75, appliedScale: 1, clippedSampleCount: 0 }, timingMs: { total: 12.5, signalGeneration: 2.5, beamforming: 10 }, fallback: { code: "webgpu-unavailable", message: "CPU fallback" }, bands: [{ id: "band-1", color: "#65f2bf", rawMap: Float32Array.of(0.1, 0.2, 0.3, 1), displayMap: Float32Array.of(0.1, 0.2, 0.3, 1), displayScale: "normalized", }], }; const evaluations = new Map([["band-1", { peaks: [{ index: 3, sourceId: "source-1", position: [1, -1, 2], value: 1, localizationErrorM: 0.05, localizationErrorPixels: 0.1, beamSpread: { calibrated: false, method: "connected-half-power-power-weighted-covariance", halfPowerRatio: 0.5, thresholdValue: 0.5, pixelCount: 3, centroid: [0.9, -0.9, 2], covarianceM2: { xx: 0.04, xy: 0.01, yy: 0.02 }, ellipse: { axisScale: "rms", angleReference: "counterclockwise-from-positive-x", majorRmsRadiusM: 0.21, minorRmsRadiusM: 0.12, angleDeg: 22.5, }, }, }], metrics: { peakToSidelobeDb: 10, energyInsideTruthRatio: 0.75, precision: 1, recall: 1, mainLobeWidthM: 0.2, ignoredNonFinite: Number.NaN, }, evaluation: { version: "1.0.0", truthRadiusM: 0.1, matchToleranceM: 0.2, detectionThresholdRatio: 0.5, detectionThresholdValue: 0.25, minimumPeakDistanceM: 0.1, sidelobeExclusionRadiusM: 0.1, halfPowerRatio: 0.5, dynamicRangeDb: 30, maximumDetections: 128, }, }]]); const result = createResultDocument({ arrayDefinition, scene, experiment, run, evaluations, inputHash: "a".repeat(64), createdAt: "2026-08-28T00:00:00.000Z", }); assert(Array.isArray(result.bands[0].rawMap.values), "typed raw map becomes JSON array"); assert(!("index" in result.bands[0].peaks[0]), "internal peak index is omitted"); assertEqual( Object.keys(result.bands[0].metrics).join(","), "peakToSidelobeDb,energyInsideTruthRatio,precision,recall,mainLobeWidthM", "only finite schema metrics are exported", ); assertEqual(result.bands[0].peaks[0].localizationErrorPixels, 0.1, "pixel error is exported"); assertEqual(result.bands[0].peaks[0].beamSpread.pixelCount, 3, "beam spread is exported"); assertEqual(result.bands[0].peaks[0].beamSpread.calibrated, false, "beam spread is not probability"); assertEqual(result.bands[0].evaluation.matchToleranceM, 0.2, "evaluation settings are exported"); assertEqual(result.environment.scanXMin, -1, "scan bounds are recorded"); assertEqual(result.environment.fallbackCode, "webgpu-unavailable", "fallback is recorded"); const issues = await validateResult(result, { experiment, scene }); assertEqual(issues.length, 0, `generated result validation: ${JSON.stringify(issues)}`); const wrongExperiment = { ...experiment, arrayId: "another-array" }; const linkageIssues = await validateResult(result, { experiment: wrongExperiment, scene }); assert(linkageIssues.some(({ path }) => path === "$.arrayId"), "validator catches snapshot linkage mismatch"); });