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
7// MATLAB's own tic/toc with the same warmup-plus-median protocol.
9import { execFileSync, spawnSync } from "node:child_process";
10import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11import { homedir, tmpdir } from "node:os";
12import { join } from "node:path";
13import type { Laplace2dInstance } from "../problems/laplace2d/spec";
14import { evalErrors } from "../problems/laplace2d/exact";
15import type { ResultPoint } from "../harness/resultSchema";
17const CHUNKIE_GIT = "https://github.com/fastalgorithms/chunkie";
18const depsDir = join(homedir(), ".cache", "fastandaccurate", "matlab-deps");
20export function matlabAvailable(): boolean {
21 try {
22 execFileSync("which", ["matlab"], { stdio: "ignore" });
23 return true;
24 } catch {
25 return false;
26 }
27}
29/** Clone chunkie (with the FLAM submodule) on first use and return the
30 * MATLAB setup lines that put it on the path. chunkie's own startup.m is
31 * deliberately not used: it attempts to compile fmm2d when a Fortran
32 * compiler is present, which an unattended run must not do, and the
33 * direct (accel=false) code path needs only the toolbox and FLAM. */
34export function ensureChunkie(): string[] {
35 const dir = join(depsDir, "chunkie");
36 if (!existsSync(join(dir, "chunkie"))) {
37 mkdirSync(depsDir, { recursive: true });
38 console.log(`fetching chunkie into ${dir}`);
39 execFileSync(
40 "git",
41 ["clone", "--depth", "1", "--recurse-submodules", "--shallow-submodules", CHUNKIE_GIT, dir],
42 { stdio: "inherit" }
43 );
44 }
45 if (!existsSync(join(dir, "chunkie", "FLAM", "startup.m"))) {
46 execFileSync(
47 "git",
48 ["-C", dir, "submodule", "update", "--init", "--depth", "1", "chunkie/FLAM"],
49 { stdio: "inherit" }
50 );
51 }
52 return [
53 `addpath('${join(dir, "chunkie")}');`,
54 `run('${join(dir, "chunkie", "FLAM", "startup.m")}');`,
55 ];
56}
58export interface MatlabSweepOptions {
59 instance: Laplace2dInstance;
60 ns: number[];
61 repeats: number;
62 sources: { buildProblem: string; bdata: string; solver: string };
63 /** MATLAB lines run before anything else (addpath etc.). */
64 setup: string[];
65 onPoint?: (point: ResultPoint, index: number, total: number) => void;
66}
68export interface MatlabSweepResult {
69 points: ResultPoint[];
70 matlabVersion: string;
71}
73function asArray(x: number | number[]): number[] {
74 return Array.isArray(x) ? x : [x];
75}
77export function runMatlabSweep(opts: MatlabSweepOptions): MatlabSweepResult {
78 const { instance, ns, repeats } = opts;
79 const dir = mkdtempSync(join(tmpdir(), "fastandaccurate-matlab-"));
80 try {
81 writeFileSync(join(dir, "build_problem.m"), opts.sources.buildProblem);
82 writeFileSync(join(dir, "laplace2d_bdata.m"), opts.sources.bdata);
83 writeFileSync(join(dir, "solver.m"), opts.sources.solver);
84 const main = [
85 "% generated by the fastandaccurate MATLAB harness",
86 ...opts.setup,
87 `ns = [${ns.join(" ")}];`,
88 `nrep = ${repeats};`,
89 `prob = build_problem(${instance.a}, ${instance.k}, ${instance.d}, 0);`,
90 "results = cell(numel(ns), 1);",
91 "for i = 1:numel(ns)",
92 " n = ns(i);",
93 " tic; out = solver(prob, n); cold = toc;",
94 " times = zeros(nrep, 1);",
95 " for r = 1:nrep",
96 " tic; out = solver(prob, n); times(r) = toc;",
97 " end",
98 " results{i} = struct('n', n, 'cold', cold, 'times', times, 'ueval', out.uEval);",
99 " fprintf('point n=%d done (%.3fs)\\n', n, median(times));",
100 "end",
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 sorted = [...times].sort((x, y) => x - y);
131 const median =
132 sorted.length % 2 === 1
133 ? sorted[(sorted.length - 1) / 2]
134 : (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2;
135 const { relMax, relL2 } = evalErrors(instance, e.ueval);
136 return {
137 n: e.n,
138 solveSeconds: median,
139 solveSecondsAll: times,
140 coldSeconds: e.cold,
141 relMax,
142 relL2,
143 };
144 });
145 points.forEach((p, i) => opts.onPoint?.(p, i, points.length));
146 return { points, matlabVersion: payload.matlabVersion };
147 } finally {
148 rmSync(dir, { recursive: true, force: true });
149 }
150}