1/**
2 * Runs a MATLAB-syntax classifier through numbl, with the same interface
3 * the audio-spike-sorting benchmark uses, so an algorithm developed
4 * there drops in here unchanged:
5 *
6 * function [times, labels] = anyname(x, fs, training)
7 *
8 * x is the take (column vector), fs its rate, and training a struct with
9 * snips ({1 x nVoices} cell of [snipLen x nSnips] matrices), names, and
10 * pre (samples before the hit onset within each snippet). It returns
11 * event times in seconds and 1-based voice indices. Imported only by the
12 * worker and the node test, never by the page, so numbl stays out of the
13 * main bundle.
14 */
15import { executeCode, RTV } from 'numbl';
16import type { RuntimeValue } from 'numbl';
17import { extractFunctionName } from './source.ts';
18import type { Detection, TrainingArrays } from './types.ts';
20export function runAlgorithm(
21 source: string,
22 x: Float32Array | Float64Array,
23 fs: number,
24 training: TrainingArrays,
25): { detections: Detection[]; logs: string[] } {
26 const fnName = extractFunctionName(source);
27 if (!fnName) throw new Error('the classifier must define a function [times, labels] = name(x, fs, training)');
29 const xd = x instanceof Float64Array ? x : Float64Array.from(x);
30 const trainingStruct = RTV.struct({
31 snips: RTV.cell(
32 training.snips.map((s) => RTV.tensor(Float64Array.from(s.data), [s.snipLen, Math.max(s.count, 0)])),
33 [1, training.snips.length],
34 ),
35 names: RTV.cell(training.names.map((n) => RTV.char(n)), [1, training.names.length]),
36 pre: RTV.num(training.pre),
37 });
39 const logs: string[] = [];
40 const result = executeCode(
41 `[times__, labels__] = ${fnName}(x, fs, training);\ntimes__ = times__(:); labels__ = labels__(:);`,
42 {
43 initialVariableValues: { x: RTV.tensor(xd, [xd.length, 1]), fs: RTV.num(fs), training: trainingStruct },
44 optimization: '1',
45 displayResults: false,
46 onOutput: (text: string) => logs.push(text),
47 },
48 [{ name: `${fnName}.m`, source }],
49 'main.m',
50 );
52 const times = toVector(result.variableValues.times__);
53 const labels = toVector(result.variableValues.labels__);
54 if (times.length !== labels.length) {
55 throw new Error(`times (${times.length}) and labels (${labels.length}) differ in length`);
56 }
57 const detections: Detection[] = times.map((t, i) => ({ t, typeIndex: Math.round(labels[i]) - 1 }));
58 detections.sort((a, b) => a.t - b.t);
59 return { detections, logs };
60}
62function toVector(v: RuntimeValue | undefined): number[] {
63 if (v === undefined || v === null) throw new Error('the classifier did not return times/labels');
64 if (typeof v === 'number') return [v];
65 const t = v as { data?: Float64Array };
66 if (t.data instanceof Float64Array) return Array.from(t.data);
67 throw new Error('times/labels must be numeric vectors');
68}