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