/ concept-collection / fastandaccurate
Sign in
concept-collection / fastandaccurate
fastandaccurate / test / solver-test.ts
142 lines · 4.9 KBBlameHistoryRaw
1// Convergence test: run the numbl solvers on the official instances
2// through numbl in node and check that errors behave as the theory says
3// they should. The MATLAB-runtime solvers are covered by
4// test/matlab-test.ts, run locally where MATLAB exists, against the same
5// expectations in test/expected.ts.
6// Run with: npx tsx test/solver-test.ts
8import { existsSync, readFileSync } from "fs";
9import { fileURLToPath } from "url";
10import { dirname, join } from "path";
11import { INSTANCES, getInstance } from "../src/problems/laplace2d/spec";
12import {
13 SOLVERS,
14 getSolver,
15 solverSourceDir,
16 type SolverManifest,
17} from "../src/solvers";
18import { runPoint, type MatlabSources } from "../src/harness/runner";
19import { runSweep } from "../src/harness/sweep";
20import { setNumblFileIO } from "../src/harness/numblRun";
21import { DEFAULT_TIMING } from "../src/harness/timing";
22import { NodeFileIOAdapter } from "../src/cli/nodeFileIO";
23import { MUST_NOT_REACH, MUST_REACH } from "./expected";
25// Solvers that mip-install packages (chunkie-dlp) need file I/O; in node
26// that is the curl-backed adapter.
27setNumblFileIO((vfs) => new NodeFileIOAdapter(vfs));
29const root = join(dirname(fileURLToPath(import.meta.url)), "..");
30const read = (p: string) => readFileSync(join(root, p), "utf-8");
32const base = {
33 buildProblem: read("src/problems/laplace2d/matlab/build_problem.m"),
34 bdata: read("src/problems/laplace2d/matlab/laplace2d_bdata.m"),
35};
36const solverSources = (solver: SolverManifest): MatlabSources => ({
37 ...base,
38 solver: read(`src/solvers/${solverSourceDir(solver)}/solver.m`),
39});
41// The suite checks accuracy, not speed, so one timed run per point is
42// enough; the committed results are what the full timing policy is for.
43const TEST_TIMING = { ...DEFAULT_TIMING, minTimedRuns: 1, timeBudgetSeconds: 0 };
45let failures = 0;
47// Registry consistency, checked for every entry including the ones this
48// suite does not run: the solver file must exist, it must have stated
49// expectations, and an entry that borrows another entry's solver.m must
50// carry the same version, so that two results claiming the same solver
51// version really did run the same code.
52for (const s of SOLVERS) {
53 const dir = solverSourceDir(s);
54 const path = `src/solvers/${dir}/solver.m`;
55 if (!existsSync(join(root, path))) {
56 console.log(`FAIL: ${s.id} has no ${path}`);
57 failures++;
58 }
59 const expected = MUST_REACH[dir];
60 if (!expected) {
61 console.log(`FAIL: ${s.id} has no entry in test/expected.ts`);
62 failures++;
63 } else if (s.runtime === "numbl") {
64 for (const inst of INSTANCES) {
65 if (expected[inst.id] === undefined) {
66 console.log(`FAIL: ${s.id} has no expectation on ${inst.id}`);
67 failures++;
68 }
69 }
70 }
71 const twin = s.sourceDir && SOLVERS.find((x) => x.id === s.sourceDir);
72 if (s.sourceDir && !twin) {
73 console.log(
74 `FAIL: ${s.id} names sourceDir ${s.sourceDir}, which is not a solver`
75 );
76 failures++;
77 } else if (twin && twin.version !== s.version) {
78 console.log(
79 `FAIL: ${s.id} v${s.version} shares solver.m with ${twin.id} v${twin.version}`
80 );
81 failures++;
82 }
85for (const inst of INSTANCES) {
86 for (const solver of SOLVERS.filter((s) => s.runtime === "numbl")) {
87 console.log(`\n== ${inst.id} / ${solver.id}`);
88 console.log(" n relMax relL2 solve(s) cold(s)");
89 let best = Infinity;
90 runSweep({
91 instance: inst,
92 solver,
93 sources: solverSources(solver),
94 timing: TEST_TIMING,
95 onPoint: (p) => {
96 best = Math.min(best, p.relMax);
97 console.log(
98 ` ${String(p.n).padStart(4)} ${p.relMax.toExponential(3)} ` +
99 `${p.relL2.toExponential(3)} ${p.solveSeconds.toFixed(4)} ${p.coldSeconds.toFixed(4)}`
100 );
101 },
102 });
103 const dir = solverSourceDir(solver);
104 const reach = MUST_REACH[dir][inst.id];
105 const notReach = MUST_NOT_REACH[dir]?.[inst.id];
106 if (best > reach) {
107 console.log(` FAIL: best relMax ${best.toExponential(2)} > ${reach}`);
108 failures++;
109 } else if (notReach !== undefined && best < notReach) {
110 console.log(
111 ` FAIL: best relMax ${best.toExponential(2)} < ${notReach} ` +
112 "(instance no longer defeats this method)"
113 );
114 failures++;
115 } else {
116 console.log(` ok (best relMax ${best.toExponential(2)})`);
117 }
118 }
121// The grid path: one run with wantGrid on the medium instance.
123 const p = runPoint({
124 instance: getInstance("star-medium"),
125 n: 64,
126 timing: TEST_TIMING,
127 wantGrid: true,
128 sources: solverSources(getSolver("nystrom-dlp")),
129 });
130 if (!p.uGrid || p.uGrid.length !== 200 * 200) {
131 console.log(`\nFAIL: grid has ${p.uGrid?.length ?? 0} values, expected 40000`);
132 failures++;
133 } else {
134 console.log(`\ngrid ok (${p.uGrid.length} values)`);
135 }
138if (failures > 0) {
139 console.error(`\n${failures} failure(s)`);
140 process.exit(1);
142console.log("\nall checks passed");
moveopenescclose