/ concept-collection / fastandaccurate
Sign in
concept-collection / fastandaccurate
fastandaccurate / src / cli / main.ts
357 lines · 12.0 KBCodeBlameHistory
5127df5fastandaccurate: PDE solver benchmarks, laplace-dirichlet-2d with MFS and Nystrom DLP solversJeremy Magland 1// The fastandaccurate command line: run work-precision sweeps outside the
2// browser, through the same harness the site uses, and write result JSON
3// files ready to submit to the fastandaccurate-results repository by PR.
4//
5// fastandaccurate list
6// fastandaccurate run [--instance <id>] [--solver <id>]
7// [--solver-file f.m --solver-id name]
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 8// [--repeats N] [--time-budget S] [--max-n N]
9// [--label "text"]
11//
12// In development: npx tsx src/cli/main.ts run ...
14import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
15import { dirname, join, resolve } from "path";
16import { fileURLToPath } from "url";
17import os from "os";
18import { INSTANCES, getInstance } from "../problems/laplace2d/spec";
59058e5Add chunkie-dlp solver: mip package support in the harness, curl-backed file I/O for nodeJeremy Magland 19import { setNumblFileIO } from "../harness/numblRun";
20import { NodeFileIOAdapter } from "./nodeFileIO";
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 21import { matlabAvailable, matlabSetup, runMatlabSweep } from "./matlabRun";
acdea26Add mfs-gpu: the MFS on WebGPU, and a TypeScript form of the solver interfaceJeremy Magland 22import { gpuUnavailableReason } from "../harness/webgpuDevice";
23import { GPU_TIMER, runSweepGpu } from "../harness/webgpuRun";
24import { getWebgpuSolver } from "../solvers/webgpuSolvers";
26setNumblFileIO((vfs) => new NodeFileIOAdapter(vfs));
28 SOLVERS,
29 getSolver,
30 solverSourceDir,
31 sweepNFor,
32 type SolverManifest,
33} from "../solvers";
5127df5fastandaccurate: PDE solver benchmarks, laplace-dirichlet-2d with MFS and Nystrom DLP solversJeremy Magland 34import { runSweep } from "../harness/sweep";
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 35import { DEFAULT_TIMING, type TimingPolicy } from "../harness/timing";
5127df5fastandaccurate: PDE solver benchmarks, laplace-dirichlet-2d with MFS and Nystrom DLP solversJeremy Magland 36import type { MatlabSources } from "../harness/runner";
37import {
38 buildResultFile,
39 toResultPoint,
40 type ResultEnvironment,
41} from "../harness/resultSchema";
43const moduleDir = dirname(fileURLToPath(import.meta.url));
45/** Locate the directory holding the .m sources: the repo's src/ in
46 * development, the bundle's own src/ in the packed CLI. */
47function findSrcRoot(): string {
48 const candidates = [join(moduleDir, "..", ".."), join(moduleDir)];
49 for (const c of candidates) {
50 if (existsSync(join(c, "src", "problems", "laplace2d", "matlab", "build_problem.m"))) {
51 return join(c, "src");
52 }
53 }
54 throw new Error("cannot locate MATLAB sources next to the CLI");
57const srcRoot = findSrcRoot();
58const readSrc = (p: string) => readFileSync(join(srcRoot, p), "utf-8");
60function numblVersion(): string {
61 // The packed CLI has numbl bundled in; the version is stamped at build
62 // time. In development (tsx), read it from node_modules instead.
63 if (typeof __NUMBL_VERSION__ !== "undefined") return __NUMBL_VERSION__;
64 const c = join(srcRoot, "..", "node_modules", "numbl", "package.json");
65 if (existsSync(c)) {
66 return (JSON.parse(readFileSync(c, "utf-8")) as { version: string }).version;
67 }
68 return "unknown";
71function environment(machineLabel: string | undefined, builtin: boolean): ResultEnvironment {
72 return {
73 kind: "node",
74 runtime: `node ${process.version}`,
75 numblVersion: numblVersion(),
76 os: `${os.platform()} ${os.release()}`,
77 cpu: os.cpus()[0]?.model?.trim() ?? "unknown",
78 machineLabel,
79 browserReproducible: builtin,
80 };
acdea26Add mfs-gpu: the MFS on WebGPU, and a TypeScript form of the solver interfaceJeremy Magland 83/** A WebGPU run records its adapter, and no numbl version: numbl is not
84 * involved. */
85function gpuEnvironment(
86 machineLabel: string | undefined,
87 gpu: string
88): ResultEnvironment {
89 return {
90 kind: "node",
91 runtime: `node ${process.version}`,
92 gpu,
93 os: `${os.platform()} ${os.release()}`,
94 cpu: os.cpus()[0]?.model?.trim() ?? "unknown",
95 machineLabel,
96 browserReproducible: true,
97 };
101 machineLabel: string | undefined,
102 matlabVersion: string
103): ResultEnvironment {
104 return {
105 kind: "matlab",
106 runtime: `MATLAB ${matlabVersion}`,
107 os: `${os.platform()} ${os.release()}`,
108 cpu: os.cpus()[0]?.model?.trim() ?? "unknown",
109 machineLabel,
110 browserReproducible: false,
111 };
115 command: string;
116 flags: Record<string, string>;
119function parseArgs(argv: string[]): Args {
120 const [command = "help", ...rest] = argv;
121 const flags: Record<string, string> = {};
122 for (let i = 0; i < rest.length; i++) {
123 const a = rest[i];
124 if (!a.startsWith("--")) throw new Error(`unexpected argument: ${a}`);
125 const key = a.slice(2);
126 const val = rest[i + 1];
127 if (val === undefined || val.startsWith("--")) {
128 flags[key] = "true";
129 } else {
130 flags[key] = val;
131 i++;
132 }
133 }
134 return { command, flags };
137function listCommand() {
138 console.log("Problem: laplace-dirichlet-2d (v1)\n");
139 console.log("Instances:");
140 for (const inst of INSTANCES) {
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 141 console.log(` ${inst.id.padEnd(16)} ${inst.label}`);
143 console.log("\nSolvers:");
144 for (const s of SOLVERS) {
146 ` ${s.id.padEnd(16)} ${s.name} ` +
147 `(v${s.version}, ${s.backend}, ${s.runtime})`
148 );
152async function runCommand(flags: Record<string, string>) {
154 minTimedRuns: flags.repeats
155 ? parseInt(flags.repeats, 10)
156 : DEFAULT_TIMING.minTimedRuns,
157 timeBudgetSeconds: flags["time-budget"]
158 ? parseFloat(flags["time-budget"])
159 : DEFAULT_TIMING.timeBudgetSeconds,
160 maxTimedRuns: flags["max-repeats"]
161 ? parseInt(flags["max-repeats"], 10)
162 : DEFAULT_TIMING.maxTimedRuns,
163 };
5127df5fastandaccurate: PDE solver benchmarks, laplace-dirichlet-2d with MFS and Nystrom DLP solversJeremy Magland 164 const maxN = flags["max-n"] ? parseInt(flags["max-n"], 10) : undefined;
165 const outDir = resolve(flags.out ?? "fastandaccurate-results-out");
166 const instances = flags.instance
167 ? [getInstance(flags.instance)]
168 : INSTANCES;
170 const base = {
171 buildProblem: readSrc("problems/laplace2d/matlab/build_problem.m"),
172 bdata: readSrc("problems/laplace2d/matlab/laplace2d_bdata.m"),
173 };
175 let solverList: { manifest: SolverManifest; sources: MatlabSources; source: string }[];
176 if (flags["solver-file"]) {
177 const file = resolve(flags["solver-file"]);
178 const id = flags["solver-id"];
179 if (!id) throw new Error("--solver-file requires --solver-id");
180 const manifest: SolverManifest = {
181 id,
182 name: id,
183 description: `custom solver from ${file}`,
184 version: flags["solver-version"] ?? "0.0.0",
185 backend: "cpu",
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 187 // A submitted solver sweeps the same resolutions as the reference
188 // Nystrom solver, per-instance lists included, so its curve lands on
189 // the same points as the committed ones.
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 191 sweepNByInstance: getSolver("nystrom-dlp").sweepNByInstance,
193 solverList = [
194 {
195 manifest,
196 sources: { ...base, solver: readFileSync(file, "utf-8") },
197 source: file,
198 },
199 ];
200 } else {
e58e208chunkie-dlp runs in real MATLAB via the CLI; solver runtime field, matlab -batch harnessJeremy Magland 201 let wanted = flags.solver ? [getSolver(flags.solver)] : SOLVERS;
202 if (wanted.some((s) => s.runtime === "matlab") && !matlabAvailable()) {
203 if (flags.solver) {
204 throw new Error(
205 `${flags.solver} runs in real MATLAB, and no matlab was found on the PATH`
206 );
207 }
208 for (const s of wanted.filter((x) => x.runtime === "matlab")) {
209 console.log(`skipping ${s.id}: runs in real MATLAB, and no matlab was found on the PATH`);
210 }
211 wanted = wanted.filter((s) => s.runtime !== "matlab");
212 }
acdea26Add mfs-gpu: the MFS on WebGPU, and a TypeScript form of the solver interfaceJeremy Magland 213 if (wanted.some((s) => s.runtime === "webgpu")) {
214 const why = await gpuUnavailableReason();
215 if (why !== null) {
216 if (flags.solver) {
217 throw new Error(`${flags.solver} runs on WebGPU: ${why}`);
218 }
219 for (const s of wanted.filter((x) => x.runtime === "webgpu")) {
220 console.log(`skipping ${s.id}: runs on WebGPU. ${why}`);
221 }
222 wanted = wanted.filter((s) => s.runtime !== "webgpu");
223 }
224 }
226 manifest,
acdea26Add mfs-gpu: the MFS on WebGPU, and a TypeScript form of the solver interfaceJeremy Magland 227 // A WebGPU solver has no MATLAB source; the problem files are still
228 // read so that the numbl and MATLAB entries share one code path.
230 ...base,
232 manifest.runtime === "webgpu"
233 ? ""
234 : readSrc(`solvers/${solverSourceDir(manifest)}/solver.m`),
237 }));
238 }
240 mkdirSync(outDir, { recursive: true });
241 const env = environment(flags.label, !flags["solver-file"]);
243 for (const inst of instances) {
244 for (const { manifest, sources, source } of solverList) {
245 console.log(`\n${inst.id} / ${manifest.id}`);
246 console.log(" n relMax relL2 solve(s)");
e58e208chunkie-dlp runs in real MATLAB via the CLI; solver runtime field, matlab -batch harnessJeremy Magland 247 const printPoint = (p: { n: number; relMax: number; relL2: number; solveSeconds: number }) => {
248 console.log(
249 ` ${String(p.n).padStart(4)} ${p.relMax.toExponential(2)} ` +
250 `${p.relL2.toExponential(2)} ${p.solveSeconds.toFixed(4)}`
251 );
252 };
253 let resultPoints;
254 let runEnv = env;
255 let timer: string | undefined;
acdea26Add mfs-gpu: the MFS on WebGPU, and a TypeScript form of the solver interfaceJeremy Magland 256 if (manifest.runtime === "webgpu") {
257 const points = await runSweepGpu({
258 instance: inst,
259 solver: manifest,
260 timing,
261 maxN,
262 onPoint: printPoint,
263 });
264 resultPoints = points.map(toResultPoint);
265 const gpu = await getWebgpuSolver(manifest.id);
266 runEnv = gpuEnvironment(flags.label, `${gpu.adapter} (${gpu.via})`);
267 timer = GPU_TIMER;
268 } else if (manifest.runtime === "matlab") {
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 269 const setup = matlabSetup(manifest.id);
270 const ns = sweepNFor(manifest, inst.id).filter(
271 (n) => maxN === undefined || n <= maxN
272 );
e58e208chunkie-dlp runs in real MATLAB via the CLI; solver runtime field, matlab -batch harnessJeremy Magland 273 const { points, matlabVersion } = runMatlabSweep({
274 instance: inst,
275 ns,
278 setup,
279 onPoint: printPoint,
280 });
281 resultPoints = points;
282 runEnv = matlabEnvironment(flags.label, matlabVersion);
283 timer = "matlab tic/toc";
284 } else {
285 const points = runSweep({
286 instance: inst,
287 solver: manifest,
288 sources,
291 onPoint: printPoint,
292 });
293 resultPoints = points.map(toResultPoint);
294 }
296 instance: inst,
297 solver: {
298 id: manifest.id,
299 version: manifest.version,
300 backend: manifest.backend,
301 source,
302 },
306 timer,
308 const name = `laplace-dirichlet-2d.${inst.id}.${manifest.id}.json`;
309 const path = join(outDir, name);
310 writeFileSync(path, JSON.stringify(result, null, 2) + "\n");
311 console.log(` wrote ${path}`);
312 }
313 }
314 console.log(
315 "\nTo publish: open a pull request adding these files under results/ in " +
316 "https://github.com/concept-collection/fastandaccurate-results"
317 );
320async function main() {
321 const args = parseArgs(process.argv.slice(2));
322 if (args.command === "list") {
323 listCommand();
324 } else if (args.command === "run") {
325 await runCommand(args.flags);
326 } else {
327 console.log(
328 [
329 "fastandaccurate - PDE solver benchmarks (https://concept-collection.github.io/fastandaccurate/)",
330 "",
331 "Commands:",
332 " list List problems, instances, and solvers",
333 " run Run work-precision sweeps and write result JSON files",
334 "",
335 "Run flags:",
336 " --instance <id> One instance (default: all)",
337 " --solver <id> One built-in solver (default: all)",
338 " --solver-file <f.m> A custom solver file (requires --solver-id)",
339 " --solver-id <name> Identifier for the custom solver",
340 " --solver-version <v> Version string for the custom solver",
ad5dc23Three new instances, near-field target sets, and an adaptive timing policyJeremy Magland 341 " --repeats <N> Minimum timed runs per point (default 5)",
342 " --time-budget <s> Keep timing a point until it has used this",
343 " many seconds (default 0.5), which is what",
344 " makes cheap points reproducible",
345 " --max-repeats <N> Cap on timed runs per point (default 50)",
5127df5fastandaccurate: PDE solver benchmarks, laplace-dirichlet-2d with MFS and Nystrom DLP solversJeremy Magland 346 " --max-n <N> Restrict the sweep to n <= N",
347 " --label <text> Free-text machine label recorded in results",
348 " --out <dir> Output directory (default fastandaccurate-results-out)",
349 ].join("\n")
350 );
351 }
354main().catch((err) => {
355 console.error(err instanceof Error ? err.message : err);
356 process.exit(1);
357});
moveopenescclose