d8098edAn interactive tutorial on the wave equation in acoustics: strings, drums and rooms, measured by earJeremy Magland 1// Runs a lab's MATLAB layer in a numbl session.
2//
3// One session per (driver, script) pair, booted on first use and then reused:
4// changing a control only rewrites params.json and re-runs main.m, which is
5// fast enough to drive sliders. Editing the script needs a fresh session,
6// because the script is compiled into main.m at boot.
7import { createNumblSession, type BootFile, type NumblSession } from 'numbl/browser'
8import type { Params, RunResult } from './types.ts'
9import { LIB_FILES } from './lib.ts'
11function bootFiles(driver: string, script: string): BootFile[] {
12 return [
13 ...LIB_FILES,
14 { path: 'main.m', content: `${driver}\n${script}` },
15 // params.json is overwritten before every run; it only needs to exist so
16 // that the first fileread has something to find
17 { path: 'params.json', content: '{}' },
18 ]
19}
21export class Engine {
22 private session: NumblSession | null = null
23 private bootedFor: string | null = null
24 private queue: Promise<unknown> = Promise.resolve()
25 private chunks: string[] = []
27 /** Resolves when the run finishes; runs are serialised in call order. */
28 run<T>(driver: string, script: string, params: Params): Promise<RunResult<T>> {
29 const task = this.queue.then(
30 () => this.exec<T>(driver, script, params),
31 () => this.exec<T>(driver, script, params),
32 )
33 this.queue = task.catch(() => undefined)
34 return task
35 }
37 private async exec<T>(driver: string, script: string, params: Params): Promise<RunResult<T>> {
38 const t0 = performance.now()
39 const fail = (error: string): RunResult<T> => ({
40 ok: false,
41 error,
42 output: this.chunks.join(''),
43 ms: performance.now() - t0,
44 })
46 const key = `${driver}\n---\n${script}`
47 try {
48 if (!this.session || this.bootedFor !== key) {
49 this.session?.dispose()
50 this.session = null
51 this.bootedFor = null
52 this.chunks = []
53 const session = await createNumblSession({
54 files: bootFiles(driver, script),
55 mip: false,
56 persistSystem: false,
57 optimization: '1',
58 onOutput: (text) => {
59 this.chunks.push(text)
60 },
61 })
62 this.session = session
63 this.bootedFor = key
64 }
66 this.chunks = []
67 this.session.writeFile('params.json', JSON.stringify(params))
68 // `run` rather than a bare `main;`: the file is a script with local
69 // functions, and this says so at the call site.
70 const res = await this.session.execute("run('main.m');")
71 if (!res.ok) return fail(res.error ?? 'the script failed')
73 const bytes = await this.session.readFile('out.json')
74 return {
75 ok: true,
76 data: JSON.parse(new TextDecoder().decode(bytes)) as T,
77 output: this.chunks.join(''),
78 ms: performance.now() - t0,
79 }
80 } catch (err) {
81 // a boot failure leaves nothing usable behind
82 this.session?.dispose()
83 this.session = null
84 this.bootedFor = null
85 return fail(err instanceof Error ? err.message : String(err))
86 }
87 }
89 dispose() {
90 this.session?.dispose()
91 this.session = null
92 this.bootedFor = null
93 }
94}