/ concept-collection / fastandaccurate
Sign in
concept-collection / fastandaccurate
fastandaccurate / test / solver-test.ts
95 lines · 3.3 KBBlameHistoryRaw
1// Convergence test: run both solvers on the official instances through
2// numbl in node and check that errors behave as the theory says they
3// should. Run with: npx tsx test/solver-test.ts
5import { readFileSync } from "fs";
6import { fileURLToPath } from "url";
7import { dirname, join } from "path";
8import { INSTANCES, getInstance } from "../src/problems/laplace2d/spec";
9import { SOLVERS } from "../src/solvers";
10import { runPoint, type MatlabSources } from "../src/harness/runner";
11import { runSweep } from "../src/harness/sweep";
13const root = join(dirname(fileURLToPath(import.meta.url)), "..");
14const read = (p: string) => readFileSync(join(root, p), "utf-8");
16const base = {
17 buildProblem: read("src/problems/laplace2d/matlab/build_problem.m"),
18 bdata: read("src/problems/laplace2d/matlab/laplace2d_bdata.m"),
19};
20const solverSources: Record<string, MatlabSources> = {
21 mfs: { ...base, solver: read("src/solvers/mfs/solver.m") },
22 "nystrom-dlp": { ...base, solver: read("src/solvers/nystrom-dlp/solver.m") },
23};
25// Best relMax each solver must reach over its full sweep. On star-hard,
26// MFS is additionally required NOT to do well: its charge curve lies
27// beyond the data's singularities there, and if it suddenly reached high
28// accuracy the instance would no longer be testing what the spec says.
29const mustReach: Record<string, Record<string, number>> = {
30 mfs: { "disk-easy": 1e-12, "star-medium": 1e-12, "star-hard": 1e-2 },
31 "nystrom-dlp": { "disk-easy": 1e-10, "star-medium": 1e-10, "star-hard": 1e-8 },
32};
33const mustNotReach: Record<string, Record<string, number>> = {
34 mfs: { "star-hard": 1e-8 },
35};
37let failures = 0;
39for (const inst of INSTANCES) {
40 for (const solver of SOLVERS) {
41 console.log(`\n== ${inst.id} / ${solver.id}`);
42 console.log(" n relMax relL2 solve(s) cold(s)");
43 let best = Infinity;
44 runSweep({
45 instance: inst,
46 solver,
47 sources: solverSources[solver.id],
48 repeats: 1,
49 onPoint: (p) => {
50 best = Math.min(best, p.relMax);
51 console.log(
52 ` ${String(p.n).padStart(4)} ${p.relMax.toExponential(3)} ` +
53 `${p.relL2.toExponential(3)} ${p.solveSeconds.toFixed(4)} ${p.coldSeconds.toFixed(4)}`
54 );
55 },
56 });
57 const reach = mustReach[solver.id][inst.id];
58 const notReach = mustNotReach[solver.id]?.[inst.id];
59 if (best > reach) {
60 console.log(` FAIL: best relMax ${best.toExponential(2)} > ${reach}`);
61 failures++;
62 } else if (notReach !== undefined && best < notReach) {
63 console.log(
64 ` FAIL: best relMax ${best.toExponential(2)} < ${notReach} ` +
65 "(instance no longer defeats this method)"
66 );
67 failures++;
68 } else {
69 console.log(` ok (best relMax ${best.toExponential(2)})`);
70 }
71 }
74// The grid path: one run with wantGrid on the medium instance.
76 const p = runPoint({
77 instance: getInstance("star-medium"),
78 n: 64,
79 repeats: 1,
80 wantGrid: true,
81 sources: solverSources["nystrom-dlp"],
82 });
83 if (!p.uGrid || p.uGrid.length !== 200 * 200) {
84 console.log(`\nFAIL: grid has ${p.uGrid?.length ?? 0} values, expected 40000`);
85 failures++;
86 } else {
87 console.log(`\ngrid ok (${p.uGrid.length} values)`);
88 }
91if (failures > 0) {
92 console.error(`\n${failures} failure(s)`);
93 process.exit(1);
95console.log("\nall checks passed");
moveopenescclose