/ concept-collection / fastandaccurate
concept-collection / fastandaccurate
fastandaccurate / src / cli / matlabRun.ts
145 lines · 5.8 KBCodeBlameHistory
e58e208chunkie-dlp runs in real MATLAB via the CLI; solver runtime field, matlab -batch harnessJeremy Magland 1// Runs a work-precision sweep in real MATLAB. Used for solvers whose
2// manifest declares runtime "matlab": the harness writes the problem
3// files, the solver, and a generated driver into a temp directory, runs
4// `matlab -batch` once for the whole sweep (one MATLAB startup per
5// instance), and reads a JSON payload back. Errors are computed on the
6// node side against the exact solution, as for numbl runs; timing is
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 7// MATLAB's own tic/toc under the same policy as the numbl runner.
9import { execFileSync, spawnSync } from "node:child_process";
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 10import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11import { tmpdir } from "node:os";
13import type { Laplace2dInstance } from "../problems/laplace2d/spec";
14import { evalErrors } from "../problems/laplace2d/exact";
15import type { ResultPoint } from "../harness/resultSchema";
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 16import { timedRunLines, type TimingPolicy } from "../harness/timing";
18export function matlabAvailable(): boolean {
19 try {
20 execFileSync("which", ["matlab"], { stdio: "ignore" });
21 return true;
22 } catch {
23 return false;
24 }
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 27/** The MATLAB setup lines a solver needs before its sweep. Only
28 * chunkie-dlp has an external dependency; the -mat solvers are plain
29 * MATLAB and need nothing.
30 *
31 * chunkie comes from mip (https://mip.sh), which the harness expects to
32 * find on the MATLAB path; --install fetches chunkie, and with it FLAM
33 * and fmm2d, on first use. Taking chunkie from mip rather than from a
34 * git clone is what makes the accelerated code path available: the mip
35 * fmm2d package ships a compiled MEX binary for the platform, so
36 * chunkie's FMM evaluation runs without a Fortran compiler on the
37 * machine. chunkie's own startup.m is not used; mip puts the three
38 * packages on the path itself. */
39export function matlabSetup(solverId: string): string[] {
40 if (solverId !== "chunkie-dlp") return [];
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 42 // which('/mip') is mip's own idiom for "a non-builtin function named
43 // mip", so a variable of that name in a user startup cannot mask the
44 // check.
45 "if isempty(which('/mip'))",
46 " error(['chunkie-dlp needs the mip package manager on the MATLAB ' ...",
47 " 'path. Install it from inside MATLAB with ' ...",
48 " 'eval(webread(''https://mip.sh/install.txt''))']);",
49 "end",
50 "mip load --install chunkie;",
54export interface MatlabSweepOptions {
55 instance: Laplace2dInstance;
56 ns: number[];
e58e208chunkie-dlp runs in real MATLAB via the CLI; solver runtime field, matlab -batch harnessJeremy Magland 58 sources: { buildProblem: string; bdata: string; solver: string };
59 /** MATLAB lines run before anything else (addpath etc.). */
60 setup: string[];
61 onPoint?: (point: ResultPoint, index: number, total: number) => void;
64export interface MatlabSweepResult {
65 points: ResultPoint[];
66 matlabVersion: string;
69function asArray(x: number | number[]): number[] {
70 return Array.isArray(x) ? x : [x];
73export function runMatlabSweep(opts: MatlabSweepOptions): MatlabSweepResult {
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 74 const { instance, ns, timing } = opts;
e58e208chunkie-dlp runs in real MATLAB via the CLI; solver runtime field, matlab -batch harnessJeremy Magland 75 const dir = mkdtempSync(join(tmpdir(), "fastandaccurate-matlab-"));
76 try {
77 writeFileSync(join(dir, "build_problem.m"), opts.sources.buildProblem);
78 writeFileSync(join(dir, "laplace2d_bdata.m"), opts.sources.bdata);
79 writeFileSync(join(dir, "solver.m"), opts.sources.solver);
80 const main = [
81 "% generated by the fastandaccurate MATLAB harness",
82 ...opts.setup,
83 `ns = [${ns.join(" ")}];`,
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 84 `prob = build_problem(${instance.a}, ${instance.k}, ${instance.p ?? 0}, ` +
85 `${instance.d}, 0, ${instance.nearBoundary ? 1 : 0});`,
2f05269Connect work-precision curves by resolution, not time; strengthen the timing protocolJeremy Magland 87 "% Session-level warmup: the whole sweep shares one MATLAB process, so",
88 "% without this the first resolution absorbs all of the one-time cost",
89 "% (loading the solver's dependencies, quadrature tables, JIT).",
90 "for w = 1:2",
91 " solver(prob, ns(max(1, floor(numel(ns)/2))));",
92 "end",
94 " n = ns(i);",
95 " tic; out = solver(prob, n); cold = toc;",
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 97 ...timedRunLines("out = solver(prob, n)", "times", timing).map((l) => ` ${l}`),
e58e208chunkie-dlp runs in real MATLAB via the CLI; solver runtime field, matlab -batch harnessJeremy Magland 98 " results{i} = struct('n', n, 'cold', cold, 'times', times, 'ueval', out.uEval);",
2f05269Connect work-precision curves by resolution, not time; strengthen the timing protocolJeremy Magland 99 " fprintf('point n=%d done (%.3fs)\\n', n, min(times));",
101 "payload = struct('matlabVersion', version, 'results', {results});",
102 "fid = fopen('out_results.json', 'w');",
103 "fwrite(fid, jsonencode(payload));",
104 "fclose(fid);",
105 "",
106 ].join("\n");
107 writeFileSync(join(dir, "main.m"), main);
108 const proc = spawnSync("matlab", ["-batch", "main"], {
109 cwd: dir,
110 encoding: "utf8",
111 timeout: 60 * 60 * 1000,
112 });
113 const outPath = join(dir, "out_results.json");
114 if (!existsSync(outPath)) {
115 throw new Error(
116 `MATLAB run failed (exit ${proc.status}):\n${(proc.stdout ?? "").slice(-2000)}\n${(proc.stderr ?? "").slice(-2000)}`
117 );
118 }
119 const payload = JSON.parse(readFileSync(outPath, "utf8")) as {
120 matlabVersion: string;
121 results:
122 | { n: number; cold: number; times: number | number[]; ueval: number[] }[]
123 | { n: number; cold: number; times: number | number[]; ueval: number[] };
124 };
125 const entries = Array.isArray(payload.results)
126 ? payload.results
127 : [payload.results];
128 const points = entries.map((e) => {
129 const times = asArray(e.times);
130 const { relMax, relL2 } = evalErrors(instance, e.ueval);
131 return {
132 n: e.n,
135 coldSeconds: e.cold,
136 relMax,
137 relL2,
138 };
139 });
140 points.forEach((p, i) => opts.onPoint?.(p, i, points.length));
141 return { points, matlabVersion: payload.matlabVersion };
142 } finally {
143 rmSync(dir, { recursive: true, force: true });
144 }