/ concept-collection / fastandaccurate
Sign in
concept-collection / fastandaccurate
fastandaccurate / src / cli / main.ts
308 lines · 10.3 KBBlameHistoryRaw
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]
8// [--repeats N] [--time-budget S] [--max-n N]
9// [--label "text"]
10// [--out dir]
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";
19import { setNumblFileIO } from "../harness/numblRun";
20import { NodeFileIOAdapter } from "./nodeFileIO";
21import { matlabAvailable, matlabSetup, runMatlabSweep } from "./matlabRun";
23setNumblFileIO((vfs) => new NodeFileIOAdapter(vfs));
24import {
25 SOLVERS,
26 getSolver,
27 solverSourceDir,
28 sweepNFor,
29 type SolverManifest,
30} from "../solvers";
31import { runSweep } from "../harness/sweep";
32import { DEFAULT_TIMING, type TimingPolicy } from "../harness/timing";
33import type { MatlabSources } from "../harness/runner";
34import {
35 buildResultFile,
36 toResultPoint,
37 type ResultEnvironment,
38} from "../harness/resultSchema";
40const moduleDir = dirname(fileURLToPath(import.meta.url));
42/** Locate the directory holding the .m sources: the repo's src/ in
43 * development, the bundle's own src/ in the packed CLI. */
44function findSrcRoot(): string {
45 const candidates = [join(moduleDir, "..", ".."), join(moduleDir)];
46 for (const c of candidates) {
47 if (existsSync(join(c, "src", "problems", "laplace2d", "matlab", "build_problem.m"))) {
48 return join(c, "src");
49 }
50 }
51 throw new Error("cannot locate MATLAB sources next to the CLI");
54const srcRoot = findSrcRoot();
55const readSrc = (p: string) => readFileSync(join(srcRoot, p), "utf-8");
57function numblVersion(): string {
58 // The packed CLI has numbl bundled in; the version is stamped at build
59 // time. In development (tsx), read it from node_modules instead.
60 if (typeof __NUMBL_VERSION__ !== "undefined") return __NUMBL_VERSION__;
61 const c = join(srcRoot, "..", "node_modules", "numbl", "package.json");
62 if (existsSync(c)) {
63 return (JSON.parse(readFileSync(c, "utf-8")) as { version: string }).version;
64 }
65 return "unknown";
68function environment(machineLabel: string | undefined, builtin: boolean): ResultEnvironment {
69 return {
70 kind: "node",
71 runtime: `node ${process.version}`,
72 numblVersion: numblVersion(),
73 os: `${os.platform()} ${os.release()}`,
74 cpu: os.cpus()[0]?.model?.trim() ?? "unknown",
75 machineLabel,
76 browserReproducible: builtin,
77 };
80function matlabEnvironment(
81 machineLabel: string | undefined,
82 matlabVersion: string
83): ResultEnvironment {
84 return {
85 kind: "matlab",
86 runtime: `MATLAB ${matlabVersion}`,
87 os: `${os.platform()} ${os.release()}`,
88 cpu: os.cpus()[0]?.model?.trim() ?? "unknown",
89 machineLabel,
90 browserReproducible: false,
91 };
94interface Args {
95 command: string;
96 flags: Record<string, string>;
99function parseArgs(argv: string[]): Args {
100 const [command = "help", ...rest] = argv;
101 const flags: Record<string, string> = {};
102 for (let i = 0; i < rest.length; i++) {
103 const a = rest[i];
104 if (!a.startsWith("--")) throw new Error(`unexpected argument: ${a}`);
105 const key = a.slice(2);
106 const val = rest[i + 1];
107 if (val === undefined || val.startsWith("--")) {
108 flags[key] = "true";
109 } else {
110 flags[key] = val;
111 i++;
112 }
113 }
114 return { command, flags };
117function listCommand() {
118 console.log("Problem: laplace-dirichlet-2d (v1)\n");
119 console.log("Instances:");
120 for (const inst of INSTANCES) {
121 console.log(` ${inst.id.padEnd(16)} ${inst.label}`);
122 }
123 console.log("\nSolvers:");
124 for (const s of SOLVERS) {
125 console.log(
126 ` ${s.id.padEnd(16)} ${s.name} ` +
127 `(v${s.version}, ${s.backend}, ${s.runtime})`
128 );
129 }
132async function runCommand(flags: Record<string, string>) {
133 const timing: TimingPolicy = {
134 minTimedRuns: flags.repeats
135 ? parseInt(flags.repeats, 10)
136 : DEFAULT_TIMING.minTimedRuns,
137 timeBudgetSeconds: flags["time-budget"]
138 ? parseFloat(flags["time-budget"])
139 : DEFAULT_TIMING.timeBudgetSeconds,
140 maxTimedRuns: flags["max-repeats"]
141 ? parseInt(flags["max-repeats"], 10)
142 : DEFAULT_TIMING.maxTimedRuns,
143 };
144 const maxN = flags["max-n"] ? parseInt(flags["max-n"], 10) : undefined;
145 const outDir = resolve(flags.out ?? "fastandaccurate-results-out");
146 const instances = flags.instance
147 ? [getInstance(flags.instance)]
148 : INSTANCES;
150 const base = {
151 buildProblem: readSrc("problems/laplace2d/matlab/build_problem.m"),
152 bdata: readSrc("problems/laplace2d/matlab/laplace2d_bdata.m"),
153 };
155 let solverList: { manifest: SolverManifest; sources: MatlabSources; source: string }[];
156 if (flags["solver-file"]) {
157 const file = resolve(flags["solver-file"]);
158 const id = flags["solver-id"];
159 if (!id) throw new Error("--solver-file requires --solver-id");
160 const manifest: SolverManifest = {
161 id,
162 name: id,
163 description: `custom solver from ${file}`,
164 version: flags["solver-version"] ?? "0.0.0",
165 backend: "cpu",
166 runtime: "numbl",
167 // A submitted solver sweeps the same resolutions as the reference
168 // Nystrom solver, per-instance lists included, so its curve lands on
169 // the same points as the committed ones.
170 sweepN: getSolver("nystrom-dlp").sweepN,
171 sweepNByInstance: getSolver("nystrom-dlp").sweepNByInstance,
172 };
173 solverList = [
174 {
175 manifest,
176 sources: { ...base, solver: readFileSync(file, "utf-8") },
177 source: file,
178 },
179 ];
180 } else {
181 let wanted = flags.solver ? [getSolver(flags.solver)] : SOLVERS;
182 if (wanted.some((s) => s.runtime === "matlab") && !matlabAvailable()) {
183 if (flags.solver) {
184 throw new Error(
185 `${flags.solver} runs in real MATLAB, and no matlab was found on the PATH`
186 );
187 }
188 for (const s of wanted.filter((x) => x.runtime === "matlab")) {
189 console.log(`skipping ${s.id}: runs in real MATLAB, and no matlab was found on the PATH`);
190 }
191 wanted = wanted.filter((s) => s.runtime !== "matlab");
192 }
193 solverList = wanted.map((manifest) => ({
194 manifest,
195 sources: {
196 ...base,
197 solver: readSrc(`solvers/${solverSourceDir(manifest)}/solver.m`),
198 },
199 source: "builtin",
200 }));
201 }
203 mkdirSync(outDir, { recursive: true });
204 const env = environment(flags.label, !flags["solver-file"]);
206 for (const inst of instances) {
207 for (const { manifest, sources, source } of solverList) {
208 console.log(`\n${inst.id} / ${manifest.id}`);
209 console.log(" n relMax relL2 solve(s)");
210 const printPoint = (p: { n: number; relMax: number; relL2: number; solveSeconds: number }) => {
211 console.log(
212 ` ${String(p.n).padStart(4)} ${p.relMax.toExponential(2)} ` +
213 `${p.relL2.toExponential(2)} ${p.solveSeconds.toFixed(4)}`
214 );
215 };
216 let resultPoints;
217 let runEnv = env;
218 let timer: string | undefined;
219 if (manifest.runtime === "matlab") {
220 const setup = matlabSetup(manifest.id);
221 const ns = sweepNFor(manifest, inst.id).filter(
222 (n) => maxN === undefined || n <= maxN
223 );
224 const { points, matlabVersion } = runMatlabSweep({
225 instance: inst,
226 ns,
227 timing,
228 sources,
229 setup,
230 onPoint: printPoint,
231 });
232 resultPoints = points;
233 runEnv = matlabEnvironment(flags.label, matlabVersion);
234 timer = "matlab tic/toc";
235 } else {
236 const points = runSweep({
237 instance: inst,
238 solver: manifest,
239 sources,
240 timing,
241 maxN,
242 onPoint: printPoint,
243 });
244 resultPoints = points.map(toResultPoint);
245 }
246 const result = await buildResultFile({
247 instance: inst,
248 solver: {
249 id: manifest.id,
250 version: manifest.version,
251 backend: manifest.backend,
252 source,
253 },
254 environment: runEnv,
255 timing,
256 points: resultPoints,
257 timer,
258 });
259 const name = `laplace-dirichlet-2d.${inst.id}.${manifest.id}.json`;
260 const path = join(outDir, name);
261 writeFileSync(path, JSON.stringify(result, null, 2) + "\n");
262 console.log(` wrote ${path}`);
263 }
264 }
265 console.log(
266 "\nTo publish: open a pull request adding these files under results/ in " +
267 "https://github.com/concept-collection/fastandaccurate-results"
268 );
271async function main() {
272 const args = parseArgs(process.argv.slice(2));
273 if (args.command === "list") {
274 listCommand();
275 } else if (args.command === "run") {
276 await runCommand(args.flags);
277 } else {
278 console.log(
279 [
280 "fastandaccurate - PDE solver benchmarks (https://concept-collection.github.io/fastandaccurate/)",
281 "",
282 "Commands:",
283 " list List problems, instances, and solvers",
284 " run Run work-precision sweeps and write result JSON files",
285 "",
286 "Run flags:",
287 " --instance <id> One instance (default: all)",
288 " --solver <id> One built-in solver (default: all)",
289 " --solver-file <f.m> A custom solver file (requires --solver-id)",
290 " --solver-id <name> Identifier for the custom solver",
291 " --solver-version <v> Version string for the custom solver",
292 " --repeats <N> Minimum timed runs per point (default 5)",
293 " --time-budget <s> Keep timing a point until it has used this",
294 " many seconds (default 0.5), which is what",
295 " makes cheap points reproducible",
296 " --max-repeats <N> Cap on timed runs per point (default 50)",
297 " --max-n <N> Restrict the sweep to n <= N",
298 " --label <text> Free-text machine label recorded in results",
299 " --out <dir> Output directory (default fastandaccurate-results-out)",
300 ].join("\n")
301 );
302 }
305main().catch((err) => {
306 console.error(err instanceof Error ? err.message : err);
307 process.exit(1);
308});
moveopenescclose