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