f4de30dSimplify the solver to run-per-solve; drop the uihtml event bridgeJeremy Magland 1// Run-per-solve engine: each solve boots a fresh numbl/browser session that
2// runs matlab/main.m standalone (mip load + solve_pde), reads result.json
3// back from the session VFS, and disposes the worker. numbl persists the
4// installed packages in IndexedDB, so only the first-ever run downloads them;
5// prewarm() triggers that download at page load.
7import { createNumblSession, type NumblSession } from 'numbl/browser'
f4de30dSimplify the solver to run-per-solve; drop the uihtml event bridgeJeremy Magland 8import main from '../../matlab/main.m?raw'
9import solvePde from '../../matlab/solve_pde.m?raw'
11const SOLVE_TIMEOUT_MS = 300_000
f4de30dSimplify the solver to run-per-solve; drop the uihtml event bridgeJeremy Magland 13export interface SolveParams {
14 pde: 'poisson' | 'helmholtz'
15 /** RHS f(x,y,z), a MATLAB expression */
16 f: string
17 /** zeroth-order coefficient c(x,y,z) (helmholtz only) */
18 c: string
19 /** polynomial order per patch */
20 p: number
21 /** every mesh edge shared by exactly two quads (from edgeClassification) */
22 closed: boolean
23}
f4de30dSimplify the solver to run-per-solve; drop the uihtml event bridgeJeremy Magland 25/** Per-patch solution grids, as packed by matlab/solve_pde.m. */
26export interface SolutionData {
27 type: 'solution'
28 /** points per patch edge (p + 1) */
29 n: number
30 npatches: number
31 x: number[][]
32 y: number[][]
33 z: number[][]
34 u: number[][]
35 umin: number
36 umax: number
37 pde: string
38}
f4de30dSimplify the solver to run-per-solve; drop the uihtml event bridgeJeremy Magland 40export interface EngineHooks {
41 /** Boot progress (package downloads, engine start). */
42 onProgress?: (message: string) => void
43 /** MATLAB console output (mip install logs etc.). */
44 onOutput?: (text: string) => void
45}
f4de30dSimplify the solver to run-per-solve; drop the uihtml event bridgeJeremy Magland 47/** Install the MATLAB packages ahead of the first solve (fire at page load). */
48export async function prewarm(hooks: EngineHooks = {}): Promise<void> {
49 const session = await createNumblSession({
50 files: [{ path: 'main.m', content: 'mip load --install surfacefun;\n' }],
51 mainFile: 'main.m',
52 onProgress: hooks.onProgress,
53 onOutput: hooks.onOutput,
54 })
55 session.dispose()
56}
59 * Solve params.pde on the mesh in a fresh session. Rejects with the MATLAB
60 * error message if the solve fails (bad expression, degenerate mesh, ...).
61 */
62export async function solve(
63 meshBytes: Uint8Array,
64 params: SolveParams,
65 hooks: EngineHooks = {},
66): Promise<SolutionData> {
67 let session: NumblSession | null = null
68 let timeoutId: ReturnType<typeof setTimeout> | undefined
69 try {
70 return await Promise.race([
71 new Promise<never>((_, reject) => {
72 timeoutId = setTimeout(() => reject(new Error('solve timed out')), SOLVE_TIMEOUT_MS)
73 }),
74 (async () => {
75 session = await createNumblSession({
76 files: [
77 { path: 'main.m', content: main },
78 { path: 'solve_pde.m', content: solvePde },
79 { path: 'params.json', content: JSON.stringify(params) },
80 { path: 'mesh.msh', content: meshBytes },
81 ],
82 mainFile: 'main.m',
83 onProgress: hooks.onProgress,
84 onOutput: hooks.onOutput,
85 })
86 const bytes = await session.readFile('result.json')
87 return JSON.parse(new TextDecoder().decode(bytes)) as SolutionData
88 })(),
89 ])
90 } finally {
91 clearTimeout(timeoutId)
92 ;(session as NumblSession | null)?.dispose()
94}