/ proof-of-concept / audio-spike-sorting
Sign in
proof-of-concept / audio-spike-sorting
audio-spike-sorting / scripts / test-node.ts
99 lines · 4.1 KBBlameHistoryRaw
1/**
2 * End-to-end pipeline test without a microphone: three synthetic event
3 * types (a low ringing tone, a high short tone, a noise burst) stand in
4 * for recorded taps. Builds a snippet library, synthesizes datasets across
5 * runs, executes each baseline algorithm through numbl, and prints scores.
6 * The matched filter and the kNN sorter should sit far above the naive
7 * floor; the run fails loudly if they do not.
8 */
9import { FS, type SynthConfig } from '../src/bench/types.ts';
10import { synthesize } from '../src/bench/synthesize.ts';
11import { score } from '../src/bench/score.ts';
12import { buildTraining } from '../src/bench/training.ts';
13import { runAlgorithm } from '../src/bench/execute.ts';
14import { makeSyntheticLibrary } from './synthetic-lib.ts';
15import matchedFilterSrc from '../src/algorithms/template_matched_filter.m?raw';
16import onsetKnnSrc from '../src/algorithms/onset_knn.m?raw';
17import silenceGapKnnSrc from '../src/algorithms/silence_gap_knn.m?raw';
18import naiveSrc from '../src/algorithms/naive_threshold.m?raw';
20const lib = makeSyntheticLibrary();
22const cfg: SynthConfig = {
23 durationS: 30,
24 eventsPerType: 30,
25 gainMinDb: -12,
26 overlap: 0.2,
27 coincidence: 0,
28 coincidenceMs: 5,
29 trainFraction: 0.3,
30 seed: 1,
31};
33const algos = [
34 { name: 'template_matched_filter', source: matchedFilterSrc },
35 { name: 'onset_knn', source: onsetKnnSrc },
36 { name: 'silence_gap_knn', source: silenceGapKnnSrc },
37 { name: 'naive_threshold', source: naiveSrc },
38];
40const NRUNS = 2;
41const TOL = 0.02;
42const acc: Record<string, number[]> = {};
43for (let r = 0; r < NRUNS; r++) {
44 const ds = synthesize(lib, cfg, r);
45 const training = buildTraining(lib, ds.trainIndices);
46 console.log(`run ${r}: ${ds.truth.length} ground-truth events, ${ds.x.length} samples`);
47 for (const a of algos) {
48 const t0 = Date.now();
49 try {
50 const out = runAlgorithm(a.source, ds.x, FS, training);
51 const s = score(ds.truth, out.detections, lib.names.length, TOL);
52 (acc[a.name] ??= []).push(s.accuracy);
53 console.log(
54 ` ${a.name.padEnd(26)} acc=${s.accuracy.toFixed(3)} correct=${s.correct}` +
55 ` miss=${s.missed} fp=${s.falsePositives} misclass=${s.misclassified}` +
56 ` (${Date.now() - t0} ms)`,
57 );
58 } catch (err) {
59 console.log(` ${a.name.padEnd(26)} ERROR: ${(err as Error).message}`);
60 }
61 }
64const mean = (v: number[]) => v.reduce((a, b) => a + b, 0) / Math.max(v.length, 1);
65console.log('\nmean accuracy over runs:');
66for (const a of algos) console.log(` ${a.name.padEnd(26)} ${mean(acc[a.name] ?? []).toFixed(3)}`);
68// The coincidence knob must actually place events near-simultaneously:
69// with every overlapper aimed at its host's onset, most consecutive
70// ground-truth pairs closer than a snippet length should be inside the
71// coincidence window, where they are outside it when the knob is off.
72function nearPairs(coincidence: number, windowMs: number): { close: number; overlapping: number } {
73 const ds = synthesize(lib, { ...cfg, overlap: 0.5, coincidence, coincidenceMs: windowMs }, 0);
74 let close = 0, overlapping = 0;
75 for (let i = 1; i < ds.truth.length; i++) {
76 const dtMs = (ds.truth[i].t - ds.truth[i - 1].t) * 1000;
77 if (dtMs < 200) overlapping++;
78 if (dtMs <= windowMs) close++;
79 }
80 return { close, overlapping };
82const off = nearPairs(0, 5);
83const on = nearPairs(1, 5);
84console.log(`\ncoincidence off: ${off.close}/${off.overlapping} overlapping pairs within 5 ms`);
85console.log(`coincidence on: ${on.close}/${on.overlapping} overlapping pairs within 5 ms`);
86if (!(on.close > 0.6 * on.overlapping)) {
87 console.error('FAIL: coincidence knob did not place events near-simultaneously');
88 process.exit(1);
90if (!(off.close < 0.35 * off.overlapping)) {
91 console.error('FAIL: events cluster within the coincidence window with the knob off');
92 process.exit(1);
95const mf = mean(acc['template_matched_filter'] ?? []);
96const knn = mean(acc['onset_knn'] ?? []);
97if (!(mf > 0.7)) { console.error('FAIL: matched filter accuracy too low'); process.exit(1); }
98if (!(knn > 0.5)) { console.error('FAIL: onset_knn accuracy too low'); process.exit(1); }
99console.log('PASS');
↑↓ move↵openescclose