/ concept-collection / fastandaccurate
Sign in
concept-collection / fastandaccurate
fastandaccurate / src / cli / main.ts
222 lines · 7.5 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"),
3677e1fAdd star-branch instance: branch-point data family alongside the log-charge familyJeremy Magland 113 bdataBranch: readSrc("problems/laplace2d/matlab/laplace2d_bdata_branch.m"),
116 let solverList: { manifest: SolverManifest; sources: MatlabSources; source: string }[];
117 if (flags["solver-file"]) {
118 const file = resolve(flags["solver-file"]);
119 const id = flags["solver-id"];
120 if (!id) throw new Error("--solver-file requires --solver-id");
121 const manifest: SolverManifest = {
122 id,
123 name: id,
124 description: `custom solver from ${file}`,
125 version: flags["solver-version"] ?? "0.0.0",
126 backend: "cpu",
127 sweepN: getSolver("nystrom-dlp").sweepN,
128 };
129 solverList = [
130 {
131 manifest,
132 sources: { ...base, solver: readFileSync(file, "utf-8") },
133 source: file,
134 },
135 ];
136 } else {
137 const wanted = flags.solver ? [getSolver(flags.solver)] : SOLVERS;
138 solverList = wanted.map((manifest) => ({
139 manifest,
140 sources: { ...base, solver: readSrc(`solvers/${manifest.id}/solver.m`) },
141 source: "builtin",
142 }));
143 }
145 mkdirSync(outDir, { recursive: true });
146 const env = environment(flags.label, !flags["solver-file"]);
148 for (const inst of instances) {
149 for (const { manifest, sources, source } of solverList) {
150 console.log(`\n${inst.id} / ${manifest.id}`);
151 console.log(" n relMax relL2 solve(s)");
152 const points = runSweep({
153 instance: inst,
154 solver: manifest,
155 sources,
156 repeats,
157 maxN,
158 onPoint: (p) => {
159 console.log(
160 ` ${String(p.n).padStart(4)} ${p.relMax.toExponential(2)} ` +
161 `${p.relL2.toExponential(2)} ${p.solveSeconds.toFixed(4)}`
162 );
163 },
164 });
165 const result = await buildResultFile({
166 instance: inst,
167 solver: {
168 id: manifest.id,
169 version: manifest.version,
170 backend: manifest.backend,
171 source,
172 },
173 environment: env,
174 repeats,
175 points: points.map(toResultPoint),
176 });
177 const name = `laplace-dirichlet-2d.${inst.id}.${manifest.id}.json`;
178 const path = join(outDir, name);
179 writeFileSync(path, JSON.stringify(result, null, 2) + "\n");
180 console.log(` wrote ${path}`);
181 }
182 }
183 console.log(
184 "\nTo publish: open a pull request adding these files under results/ in " +
185 "https://github.com/concept-collection/fastandaccurate-results"
186 );
189async function main() {
190 const args = parseArgs(process.argv.slice(2));
191 if (args.command === "list") {
192 listCommand();
193 } else if (args.command === "run") {
194 await runCommand(args.flags);
195 } else {
196 console.log(
197 [
198 "fastandaccurate - PDE solver benchmarks (https://concept-collection.github.io/fastandaccurate/)",
199 "",
200 "Commands:",
201 " list List problems, instances, and solvers",
202 " run Run work-precision sweeps and write result JSON files",
203 "",
204 "Run flags:",
205 " --instance <id> One instance (default: all)",
206 " --solver <id> One built-in solver (default: all)",
207 " --solver-file <f.m> A custom solver file (requires --solver-id)",
208 " --solver-id <name> Identifier for the custom solver",
209 " --solver-version <v> Version string for the custom solver",
210 " --repeats <N> Timed repeats per point (default 3)",
211 " --max-n <N> Restrict the sweep to n <= N",
212 " --label <text> Free-text machine label recorded in results",
213 " --out <dir> Output directory (default fastandaccurate-results-out)",
214 ].join("\n")
215 );
216 }
219main().catch((err) => {
220 console.error(err instanceof Error ? err.message : err);
221 process.exit(1);
222});
moveopenescclose