/ concept-collection / fastandaccurate
Sign in
concept-collection / fastandaccurate
fastandaccurate / src / harness / numblRun.ts
124 lines · 4.2 KBBlameHistoryRaw
1// Thin wrapper around numbl's synchronous executeCode for harness runs.
2// Works identically in a browser worker and in node. Plain solvers need
3// no file system at all: the MATLAB side communicates through workspace
4// variables, read back from result.variableValues.
5//
6// Solvers that begin with `mip load --install <pkg>` additionally need
7// the mip package manager, which the wrapper bootstraps on first use: a
8// persistent virtual file system holds /system, the mip core is fetched
9// once and unzipped into it, and mip's own downloads go through the
10// platform's websave. In a browser worker that is numbl's
11// BrowserFileIOAdapter (synchronous XHR, GitHub release URLs routed
12// through numbl's CORS proxy); the node CLI substitutes a curl-backed
13// adapter via setNumblFileIO. Installed packages persist for the
14// lifetime of the worker or process, so a sweep pays the download once.
16import {
17 executeCode,
18 VirtualFileSystem,
19 BrowserFileIOAdapter,
20 BrowserSystemAdapter,
21} from "numbl";
22import { unzipSync } from "fflate";
24const PROJ = "/fastandaccurate";
26const MIP_MHL_URL =
27 "https://github.com/mip-org/mip-core/releases/download/mip-numbl/mip-numbl-any.mhl";
28const MIP_SYSTEM_PREFIX = "/system/mip/packages/gh/mip-org/core/mip/";
29const MIP_SEARCH_PATH = MIP_SYSTEM_PREFIX + "mip";
31type FileIOFactory = (vfs: VirtualFileSystem) => BrowserFileIOAdapter;
33let makeFileIO: FileIOFactory = (vfs) => new BrowserFileIOAdapter(vfs);
35/** Substitute the platform's file I/O adapter (the node CLI installs a
36 * curl-backed one; the browser default needs nothing). Must be called
37 * before the first mip-using run. */
38export function setNumblFileIO(factory: FileIOFactory) {
39 makeFileIO = factory;
42let vfs: VirtualFileSystem | null = null;
43let fileIO: BrowserFileIOAdapter | null = null;
44let system: BrowserSystemAdapter | null = null;
45let mipReady = false;
47function ensureMip() {
48 if (mipReady && vfs && fileIO && system) return;
49 vfs = new VirtualFileSystem();
50 fileIO = makeFileIO(vfs);
51 system = new BrowserSystemAdapter(vfs);
52 const tmp = "/tmp/mip-core.mhl";
53 fileIO.websave(MIP_MHL_URL, tmp);
54 const entries = unzipSync(vfs.readFile(vfs.normalizePath(tmp)));
55 for (const [name, content] of Object.entries(entries)) {
56 if (name.endsWith("/")) continue;
57 vfs.writeFile(MIP_SYSTEM_PREFIX + name, content);
58 }
59 mipReady = true;
62function usesMip(sources: string[]): boolean {
63 return sources.some((s) => /^\s*mip\s+load\b/m.test(s));
66export interface NumblRunResult {
67 /** Console output of the run. */
68 output: string;
69 /** Named numeric results pulled from the final workspace. */
70 vars: Record<string, Float64Array>;
73/**
74 * Run mainSource as the main script with the given auxiliary .m files on
75 * the search path, and extract the requested workspace variables, which
76 * must be real numeric arrays (or scalars, returned as length-1 arrays).
77 * Throws on MATLAB errors and on missing/non-numeric variables.
78 */
79export function runNumblScript(
80 mainSource: string,
81 files: Record<string, string>,
82 wantVars: string[]
83): NumblRunResult {
84 const workspaceFiles = Object.entries(files).map(([name, source]) => ({
85 name: `${PROJ}/${name}`,
86 source,
87 }));
88 const outputs: string[] = [];
89 const mip = usesMip([mainSource, ...Object.values(files)]);
90 if (mip) ensureMip();
91 const result = executeCode(
92 mainSource,
93 {
94 onOutput: (text) => outputs.push(text),
95 displayResults: false,
96 optimization: "1",
97 implicitCwdPath: null,
98 ...(mip && fileIO && system ? { fileIO, system } : {}),
99 },
100 workspaceFiles,
101 `${PROJ}/main.m`,
102 mip ? [PROJ, MIP_SEARCH_PATH] : [PROJ]
103 );
104 const vars: Record<string, Float64Array> = {};
105 for (const name of wantVars) {
106 const v = result.variableValues[name];
107 if (typeof v === "number") {
108 vars[name] = new Float64Array([v]);
109 } else if (
110 v &&
111 typeof v === "object" &&
112 (v as { kind?: string }).kind === "tensor"
113 ) {
114 const tensor = v as { data: Float64Array; imag?: Float64Array };
115 if (tensor.imag) {
116 throw new Error(`variable ${name} is complex; expected real`);
117 }
118 vars[name] = tensor.data;
119 } else {
120 throw new Error(`variable ${name} missing or not numeric after run`);
121 }
122 }
123 return { output: outputs.join(""), vars };
moveopenescclose