concept-collection / mesh-pde-solver
mesh-pde-solver / src / engine / engine.ts
113 lines · 3.8 KBBlameHistoryRaw
1// Run-per-solve engine: each solve fills matlab/solve_template.m with the
2// parameters, boots a fresh numbl/browser session that runs it standalone,
3// reads result.json back from the session VFS, and disposes the worker.
4// numbl persists the installed packages in IndexedDB, so only the first-ever
5// run downloads them; prewarm() triggers that download at page load.
7import { createNumblSession, type NumblSession } from 'numbl/browser'
8import solveTemplate from '../../matlab/solve_template.m?raw'
10const SOLVE_TIMEOUT_MS = 300_000
12export interface SolveParams {
13 pde: 'poisson' | 'helmholtz'
14 /** RHS f(x,y,z), a MATLAB expression */
15 f: string
16 /** zeroth-order coefficient c(x,y,z) (helmholtz only) */
17 c: string
18 /** polynomial order per patch */
19 p: number
20 /** every mesh edge shared by exactly two cells (from edgeClassification) */
21 closed: boolean
22 /** filename the mesh is staged under, referenced by the generated script */
23 meshFile: string
26/**
27 * Fill matlab/solve_template.m with actual parameter values. The result is
28 * the exact script a solve runs, and what the UI offers for download — it
29 * also runs in desktop MATLAB with surfacefun on the path.
30 */
31export function buildSolveScript(params: SolveParams): string {
32 const fills: Record<string, string> = {
33 MESHFILE: params.meshFile,
34 PDE: params.pde,
35 F_EXPR: params.f,
36 C_EXPR: params.pde === 'helmholtz' ? params.c : '0',
37 ORDER: String(params.p),
38 CLOSED: params.closed ? 'true' : 'false',
39 }
40 return solveTemplate.replace(/\{\{(\w+)\}\}/g, (token, key) => fills[key] ?? token)
43/** Per-patch solution data, as packed by matlab/solve_template.m. */
44export interface SolutionData {
45 type: 'solution'
46 /** points per patch edge (p + 1); quad patches carry n*n points
47 * (column-major grid), triangle patches n*(n+1)/2 (trianglepts order) */
48 n: number
49 /** patch type of the mesh — never mixed */
50 ptype: 'quad' | 'tri'
51 npatches: number
52 x: number[][]
53 y: number[][]
54 z: number[][]
55 u: number[][]
56 umin: number
57 umax: number
58 pde: string
61export interface EngineHooks {
62 /** Boot progress (package downloads, engine start). */
63 onProgress?: (message: string) => void
64 /** MATLAB console output (mip install logs etc.). */
65 onOutput?: (text: string) => void
68/** Install the MATLAB packages ahead of the first solve (fire at page load). */
69export async function prewarm(hooks: EngineHooks = {}): Promise<void> {
70 const session = await createNumblSession({
71 files: [{ path: 'main.m', content: 'mip load --install surfacefun;\n' }],
72 mainFile: 'main.m',
73 onProgress: hooks.onProgress,
74 onOutput: hooks.onOutput,
75 })
76 session.dispose()
79/**
80 * Solve params.pde on the mesh in a fresh session. Rejects with the MATLAB
81 * error message if the solve fails (bad expression, degenerate mesh, ...).
82 */
83export async function solve(
84 meshBytes: Uint8Array,
85 params: SolveParams,
86 hooks: EngineHooks = {},
87): Promise<SolutionData> {
88 let session: NumblSession | null = null
89 let timeoutId: ReturnType<typeof setTimeout> | undefined
90 try {
91 return await Promise.race([
92 new Promise<never>((_, reject) => {
93 timeoutId = setTimeout(() => reject(new Error('solve timed out')), SOLVE_TIMEOUT_MS)
94 }),
95 (async () => {
96 session = await createNumblSession({
97 files: [
98 { path: 'solve_pde.m', content: buildSolveScript(params) },
99 { path: params.meshFile, content: meshBytes },
100 ],
101 mainFile: 'solve_pde.m',
102 onProgress: hooks.onProgress,
103 onOutput: hooks.onOutput,
104 })
105 const bytes = await session.readFile('result.json')
106 return JSON.parse(new TextDecoder().decode(bytes)) as SolutionData
107 })(),
108 ])
109 } finally {
110 clearTimeout(timeoutId)
111 ;(session as NumblSession | null)?.dispose()
112 }