import { analyzeCfarActivity, estimateCfarActivityWorkload, } from "/packages/acoustics-core/activity-monitor.js"; function sinusoid(sampleCount, sampleRate, frequencyHz, amplitude = 1) { return Float64Array.from( { length: sampleCount }, (_, index) => amplitude * Math.sin((2 * Math.PI * frequencyHz * index) / sampleRate), ); } function detectorOptions(overrides = {}) { return { samples: sinusoid(128, 6_400, 1_200), sampleRate: 6_400, fftSize: 64, hopSize: 64, window: "hann", channelId: "mic-reference", detector: "os-cfar", trainingCellsPerSide: 4, guardCellsPerSide: 1, rank: 6, nominalFalseAlarmProbability: 0.01, ...overrides, }; } test("CFAR activity workload counts complete frames and every one-sided bin", () => { const workload = estimateCfarActivityWorkload({ sampleCount: 160, fftSize: 64, hopSize: 32, }); assertEqual(workload.frameCount, 4, "complete frame count"); assertEqual(workload.binCount, 33, "one-sided bin count"); assertEqual(workload.binDecisionCount, 132, "all frame/bin records"); assertEqual(workload.fftButterflyCount, 768, "radix-2 butterfly count"); }); test("CFAR activity monitor preserves raw bins without time, microphone, or band fusion", async () => { const result = await analyzeCfarActivity(detectorOptions()); assertEqual(result.kind, "micarrsim.cfar-activity-analysis/1", "result kind"); assertEqual(result.channel.channelId, "mic-reference", "explicit channel"); assertEqual(result.channel.microphoneFusion, "none", "no microphone fusion"); assertEqual(result.frames.length, 2, "every complete frame retained"); assertEqual(result.frames[0].bins.length, 33, "every one-sided bin retained"); assertEqual(result.frames[0].bins[0].status, "untested", "DC remains untested"); assertEqual( result.frames[0].bins[32].status, "untested", "Nyquist remains untested", ); assertEqual(result.summary.selectionApplied, false, "no selection"); assertEqual(result.summary.clusteringApplied, false, "no clustering"); assertEqual(result.summary.localizationApplied, false, "no localization"); assertEqual( result.detectorModel.processing.bandFusion, false, "no band fusion", ); assert( result.frames.some((frame) => frame.bins[12].status === "detected"), "the exact 1.2 kHz ADC bin is reported as detected", ); }); test("above-Nyquist analog tone is reported only at its observable ADC alias", async () => { const sampleRate = 6_400; const analogFrequencyHz = 7_600; const result = await analyzeCfarActivity(detectorOptions({ samples: sinusoid(128, sampleRate, analogFrequencyHz), })); assert( result.frames.some((frame) => frame.bins[12].status === "detected"), "7.6 kHz samples expose the 1.2 kHz ADC bin", ); assertEqual( result.input.analogFrequencyOrAliasOrderInferred, false, "monitor does not infer analog alias order", ); assertEqual(result.input.sceneTruthUsed, false, "Scene truth is not an input"); assertEqual( Object.prototype.hasOwnProperty.call(result.input, "analogFrequencyHz"), false, "the result does not invent an analog carrier field", ); assertEqual( Object.prototype.hasOwnProperty.call(result.frames[0].bins[12], "analogFrequencyHz"), false, "the detected ADC bin contains no hidden analog carrier", ); }); test("CA and OS activity analyses stay separate algorithms", async () => { const ca = await analyzeCfarActivity(detectorOptions({ detector: "ca-cfar", rank: undefined, })); const os = await analyzeCfarActivity(detectorOptions()); assertEqual(ca.detectorModel.detector, "ca-cfar", "CA identity"); assertEqual(os.detectorModel.detector, "os-cfar", "OS identity"); assertEqual( ca.detectorModel.processing.detectorFusion, false, "CA has no detector fusion", ); assertEqual( os.detectorModel.processing.detectorFusion, false, "OS has no detector fusion", ); }); test("CFAR activity monitor is deterministic and preserves raw powers", async () => { const first = await analyzeCfarActivity(detectorOptions()); const second = await analyzeCfarActivity(detectorOptions()); for (let frameIndex = 0; frameIndex < first.frames.length; frameIndex += 1) { for (let bin = 0; bin < first.frames[frameIndex].bins.length; bin += 1) { const left = first.frames[frameIndex].bins[bin]; const right = second.frames[frameIndex].bins[bin]; assertEqual(left.status, right.status, "deterministic status"); assertEqual(left.cutPower, right.cutPower, "deterministic raw CUT power"); assertEqual( left.referencePower, right.referencePower, "deterministic reference power", ); assertEqual( left.thresholdPower, right.thresholdPower, "deterministic threshold", ); } } }); test("CFAR activity monitor rejects implicit channel fusion and hard-cap overflow", async () => { let channelError = null; try { await analyzeCfarActivity(detectorOptions({ samples: [ sinusoid(64, 6_400, 1_200), sinusoid(64, 6_400, 1_200), ], })); } catch (error) { channelError = error; } assert(channelError instanceof Error, "nested channel input is rejected"); let capError = null; try { await analyzeCfarActivity(detectorOptions({ maximumBinDecisionCount: 65, })); } catch (error) { capError = error; } assert( capError?.message.includes("maximumBinDecisionCount"), "bin decision cap rejects work instead of truncating results", ); }); test("CFAR activity monitor honors an already-aborted signal", async () => { const controller = new AbortController(); controller.abort(); let abortError = null; try { await analyzeCfarActivity(detectorOptions({ signal: controller.signal })); } catch (error) { abortError = error; } assertEqual(abortError?.name, "AbortError", "typed abort"); });