1/**
2 * Runs one algorithm on one dataset through numbl. The algorithm is a
3 * MATLAB-syntax file defining
4 *
5 * function [times, labels] = anyname(x, fs, training)
6 *
7 * where x is the recording (column vector), fs the sampling rate, and
8 * training a struct with fields:
9 * snips {1 x nTypes} cell, each [snipLen x nSnips] (columns are snippets)
10 * names {1 x nTypes} cell of type names
11 * pre samples before the event onset within each snippet
12 * It must return event times in seconds and 1-based type labels. This
13 * module is pure of the DOM so the node test and the web worker share it.
14 */
15import { executeCode, RTV } from 'numbl';
16import type { RuntimeValue } from 'numbl';
17import type { Detection } from './types.ts';
18import type { TrainingArrays } from './training.ts';
20/** Name of the first function defined in a .m source, or null for a plain script. */
21export function extractFunctionName(source: string): string | null {
22 const m = source.match(/^\s*function\s+(?:\[[^\]]*\]\s*=\s*|\w+\s*=\s*)?([A-Za-z]\w*)\s*\(/m);
23 return m ? m[1] : null;
24}
26export interface ExecuteOutcome {
27 detections: Detection[];
28 logs: string[];
29 elapsedMs: number;
30}
32export function runAlgorithm(
33 source: string,
34 x: Float32Array | Float64Array,
35 fs: number,
36 training: TrainingArrays,
37): ExecuteOutcome {
38 const fnName = extractFunctionName(source);
39 if (!fnName) throw new Error('algorithm source must define a function [times, labels] = name(x, fs, training)');
41 const xd = x instanceof Float64Array ? x : Float64Array.from(x);
42 const xv = RTV.tensor(xd, [xd.length, 1]);
43 const trainingStruct = RTV.struct({
44 snips: RTV.cell(
45 training.snips.map((s) => RTV.tensor(s.data, [s.snipLen, Math.max(s.count, 0)])),
46 [1, training.snips.length],
47 ),
48 names: RTV.cell(training.names.map((n) => RTV.char(n)), [1, training.names.length]),
49 pre: RTV.num(training.pre),
50 });
52 const logs: string[] = [];
53 const t0 = (typeof performance !== 'undefined' ? performance : Date).now();
54 const result = executeCode(
55 `[times__, labels__] = ${fnName}(x, fs, training);\ntimes__ = times__(:); labels__ = labels__(:);`,
56 {
57 initialVariableValues: { x: xv, fs: RTV.num(fs), training: trainingStruct },
58 optimization: '1',
59 displayResults: false,
60 onOutput: (text: string) => logs.push(text),
61 },
62 [{ name: `${fnName}.m`, source }],
63 'main.m',
64 );
65 const elapsedMs = (typeof performance !== 'undefined' ? performance : Date).now() - t0;
67 const times = toVector(result.variableValues.times__);
68 const labels = toVector(result.variableValues.labels__);
69 if (times.length !== labels.length) {
70 throw new Error(`times (${times.length}) and labels (${labels.length}) differ in length`);
71 }
72 const detections: Detection[] = [];
73 for (let i = 0; i < times.length; i++) {
74 detections.push({ t: times[i], typeIndex: Math.round(labels[i]) - 1 });
75 }
76 detections.sort((a, b) => a.t - b.t);
77 return { detections, logs, elapsedMs };
78}
80function toVector(v: RuntimeValue | undefined): number[] {
81 if (v === undefined || v === null) throw new Error('algorithm did not return times/labels');
82 if (typeof v === 'number') return [v];
83 const t = v as { data?: Float64Array };
84 if (t.data instanceof Float64Array) return Array.from(t.data);
85 throw new Error('times/labels must be numeric vectors');
86}