/ concept-collection / fastandaccurate
Sign in
concept-collection / fastandaccurate
fastandaccurate / src / cli / main.ts
221 lines · 7.4 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]
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 { SOLVERS, getSolver, type SolverManifest } from "../solvers";
19import { runSweep } from "../harness/sweep";
20import type { MatlabSources } from "../harness/runner";
21import {
22 buildResultFile,
23 toResultPoint,
24 type ResultEnvironment,
25} from "../harness/resultSchema";
27const moduleDir = dirname(fileURLToPath(import.meta.url));
29/** Locate the directory holding the .m sources: the repo's src/ in
30 * development, the bundle's own src/ in the packed CLI. */
31function findSrcRoot(): string {
32 const candidates = [join(moduleDir, "..", ".."), join(moduleDir)];
33 for (const c of candidates) {
34 if (existsSync(join(c, "src", "problems", "laplace2d", "matlab", "build_problem.m"))) {
35 return join(c, "src");
36 }
37 }
38 throw new Error("cannot locate MATLAB sources next to the CLI");
41const srcRoot = findSrcRoot();
42const readSrc = (p: string) => readFileSync(join(srcRoot, p), "utf-8");
44function numblVersion(): string {
45 // The packed CLI has numbl bundled in; the version is stamped at build
46 // time. In development (tsx), read it from node_modules instead.
47 if (typeof __NUMBL_VERSION__ !== "undefined") return __NUMBL_VERSION__;
48 const c = join(srcRoot, "..", "node_modules", "numbl", "package.json");
49 if (existsSync(c)) {
50 return (JSON.parse(readFileSync(c, "utf-8")) as { version: string }).version;
51 }
52 return "unknown";
55function environment(machineLabel: string | undefined, builtin: boolean): ResultEnvironment {
56 return {
57 kind: "node",
58 runtime: `node ${process.version}`,
59 numblVersion: numblVersion(),
60 os: `${os.platform()} ${os.release()}`,
61 cpu: os.cpus()[0]?.model?.trim() ?? "unknown",
62 machineLabel,
63 browserReproducible: builtin,
64 };
67interface Args {
68 command: string;
69 flags: Record<string, string>;
72function parseArgs(argv: string[]): Args {
73 const [command = "help", ...rest] = argv;
74 const flags: Record<string, string> = {};
75 for (let i = 0; i < rest.length; i++) {
76 const a = rest[i];
77 if (!a.startsWith("--")) throw new Error(`unexpected argument: ${a}`);
78 const key = a.slice(2);
79 const val = rest[i + 1];
80 if (val === undefined || val.startsWith("--")) {
81 flags[key] = "true";
82 } else {
83 flags[key] = val;
84 i++;
85 }
86 }
87 return { command, flags };
90function listCommand() {
91 console.log("Problem: laplace-dirichlet-2d (v1)\n");
92 console.log("Instances:");
93 for (const inst of INSTANCES) {
94 console.log(` ${inst.id.padEnd(14)} ${inst.label}`);
95 }
96 console.log("\nSolvers:");
97 for (const s of SOLVERS) {
98 console.log(` ${s.id.padEnd(14)} ${s.name} (v${s.version}, ${s.backend})`);
99 }
102async function runCommand(flags: Record<string, string>) {
103 const repeats = flags.repeats ? parseInt(flags.repeats, 10) : 3;
104 const maxN = flags["max-n"] ? parseInt(flags["max-n"], 10) : undefined;
105 const outDir = resolve(flags.out ?? "fastandaccurate-results-out");
106 const instances = flags.instance
107 ? [getInstance(flags.instance)]
108 : INSTANCES;
110 const base = {
111 buildProblem: readSrc("problems/laplace2d/matlab/build_problem.m"),
112 bdata: readSrc("problems/laplace2d/matlab/laplace2d_bdata.m"),
113 };
115 let solverList: { manifest: SolverManifest; sources: MatlabSources; source: string }[];
116 if (flags["solver-file"]) {
117 const file = resolve(flags["solver-file"]);
118 const id = flags["solver-id"];
119 if (!id) throw new Error("--solver-file requires --solver-id");
120 const manifest: SolverManifest = {
121 id,
122 name: id,
123 description: `custom solver from ${file}`,
124 version: flags["solver-version"] ?? "0.0.0",
125 backend: "cpu",
126 sweepN: getSolver("nystrom-dlp").sweepN,
127 };
128 solverList = [
129 {
130 manifest,
131 sources: { ...base, solver: readFileSync(file, "utf-8") },
132 source: file,
133 },
134 ];
135 } else {
136 const wanted = flags.solver ? [getSolver(flags.solver)] : SOLVERS;
137 solverList = wanted.map((manifest) => ({
138 manifest,
139 sources: { ...base, solver: readSrc(`solvers/${manifest.id}/solver.m`) },
140 source: "builtin",
141 }));
142 }
144 mkdirSync(outDir, { recursive: true });
145 const env = environment(flags.label, !flags["solver-file"]);
147 for (const inst of instances) {
148 for (const { manifest, sources, source } of solverList) {
149 console.log(`\n${inst.id} / ${manifest.id}`);
150 console.log(" n relMax relL2 solve(s)");
151 const points = runSweep({
152 instance: inst,
153 solver: manifest,
154 sources,
155 repeats,
156 maxN,
157 onPoint: (p) => {
158 console.log(
159 ` ${String(p.n).padStart(4)} ${p.relMax.toExponential(2)} ` +
160 `${p.relL2.toExponential(2)} ${p.solveSeconds.toFixed(4)}`
161 );
162 },
163 });
164 const result = await buildResultFile({
165 instance: inst,
166 solver: {
167 id: manifest.id,
168 version: manifest.version,
169 backend: manifest.backend,
170 source,
171 },
172 environment: env,
173 repeats,
174 points: points.map(toResultPoint),
175 });
176 const name = `laplace-dirichlet-2d.${inst.id}.${manifest.id}.json`;
177 const path = join(outDir, name);
178 writeFileSync(path, JSON.stringify(result, null, 2) + "\n");
179 console.log(` wrote ${path}`);
180 }
181 }
182 console.log(
183 "\nTo publish: open a pull request adding these files under results/ in " +
184 "https://github.com/concept-collection/fastandaccurate-results"
185 );
188async function main() {
189 const args = parseArgs(process.argv.slice(2));
190 if (args.command === "list") {
191 listCommand();
192 } else if (args.command === "run") {
193 await runCommand(args.flags);
194 } else {
195 console.log(
196 [
197 "fastandaccurate - PDE solver benchmarks (https://concept-collection.github.io/fastandaccurate/)",
198 "",
199 "Commands:",
200 " list List problems, instances, and solvers",
201 " run Run work-precision sweeps and write result JSON files",
202 "",
203 "Run flags:",
204 " --instance <id> One instance (default: all)",
205 " --solver <id> One built-in solver (default: all)",
206 " --solver-file <f.m> A custom solver file (requires --solver-id)",
207 " --solver-id <name> Identifier for the custom solver",
208 " --solver-version <v> Version string for the custom solver",
209 " --repeats <N> Timed repeats per point (default 3)",
210 " --max-n <N> Restrict the sweep to n <= N",
211 " --label <text> Free-text machine label recorded in results",
212 " --out <dir> Output directory (default fastandaccurate-results-out)",
213 ].join("\n")
214 );
215 }
218main().catch((err) => {
219 console.error(err instanceof Error ? err.message : err);
220 process.exit(1);
221});
moveopenescclose