/** * Runs one algorithm on one dataset through numbl. The algorithm is a * MATLAB-syntax file defining * * function [times, labels] = anyname(x, fs, training) * * where x is the recording (column vector), fs the sampling rate, and * training a struct with fields: * snips {1 x nTypes} cell, each [snipLen x nSnips] (columns are snippets) * names {1 x nTypes} cell of type names * pre samples before the event onset within each snippet * It must return event times in seconds and 1-based type labels. This * module is pure of the DOM so the node test and the web worker share it. */ import { executeCode, RTV } from 'numbl'; import type { RuntimeValue } from 'numbl'; import type { Detection } from './types.ts'; import type { TrainingArrays } from './training.ts'; /** Name of the first function defined in a .m source, or null for a plain script. */ export function extractFunctionName(source: string): string | null { const m = source.match(/^\s*function\s+(?:\[[^\]]*\]\s*=\s*|\w+\s*=\s*)?([A-Za-z]\w*)\s*\(/m); return m ? m[1] : null; } export interface ExecuteOutcome { detections: Detection[]; logs: string[]; elapsedMs: number; } export function runAlgorithm( source: string, x: Float32Array | Float64Array, fs: number, training: TrainingArrays, ): ExecuteOutcome { const fnName = extractFunctionName(source); if (!fnName) throw new Error('algorithm source must define a function [times, labels] = name(x, fs, training)'); const xd = x instanceof Float64Array ? x : Float64Array.from(x); const xv = RTV.tensor(xd, [xd.length, 1]); const trainingStruct = RTV.struct({ snips: RTV.cell( training.snips.map((s) => RTV.tensor(s.data, [s.snipLen, Math.max(s.count, 0)])), [1, training.snips.length], ), names: RTV.cell(training.names.map((n) => RTV.char(n)), [1, training.names.length]), pre: RTV.num(training.pre), }); const logs: string[] = []; const t0 = (typeof performance !== 'undefined' ? performance : Date).now(); const result = executeCode( `[times__, labels__] = ${fnName}(x, fs, training);\ntimes__ = times__(:); labels__ = labels__(:);`, { initialVariableValues: { x: xv, fs: RTV.num(fs), training: trainingStruct }, optimization: '1', displayResults: false, onOutput: (text: string) => logs.push(text), }, [{ name: `${fnName}.m`, source }], 'main.m', ); const elapsedMs = (typeof performance !== 'undefined' ? performance : Date).now() - t0; const times = toVector(result.variableValues.times__); const labels = toVector(result.variableValues.labels__); if (times.length !== labels.length) { throw new Error(`times (${times.length}) and labels (${labels.length}) differ in length`); } const detections: Detection[] = []; for (let i = 0; i < times.length; i++) { detections.push({ t: times[i], typeIndex: Math.round(labels[i]) - 1 }); } detections.sort((a, b) => a.t - b.t); return { detections, logs, elapsedMs }; } function toVector(v: RuntimeValue | undefined): number[] { if (v === undefined || v === null) throw new Error('algorithm did not return times/labels'); if (typeof v === 'number') return [v]; const t = v as { data?: Float64Array }; if (t.data instanceof Float64Array) return Array.from(t.data); throw new Error('times/labels must be numeric vectors'); }