/ concept-collection / fastandaccurate
Sign in
concept-collection / fastandaccurate
chunkie-dlp runs in real MATLAB via the CLI; solver runtime field, matlab -batch harness
Jeremy Magland <jmagland@flatironinstitute.org> committed commit e58e208cd5a6 parent 59058e5 Browse files
13 changed files+347−59
README.mdmodified+7−5View file
@@ -13,11 +13,13 @@ problem-specified evaluation points. The central object is the
1313 resolution varies. No single ranking is presented; which curve wins can
1414 differ by accuracy regime, instance, and machine.
1515
16-Solvers are MATLAB function files run by [numbl](https://numbl.org)
17-(MATLAB syntax in the browser and in node), so every run on this site
18-happens client side, and the identical harness runs from the command
19-line. Each problem defines its own interface and instances in a written
20-specification; interfaces are per problem rather than shared.
16+Solvers are MATLAB function files. Most run via
17+[numbl](https://numbl.org) (MATLAB syntax in the browser and in node),
18+both on the site and from the command line; some run only in real
19+MATLAB through the command line, and their results are marked as not
20+reproducible in the browser. Each problem defines its own interface and
21+instances in a written specification; interfaces are per problem rather
22+than shared.
2123
2224 ## Problems
2325
package.jsonmodified+2−1View file
@@ -9,7 +9,8 @@
99 "build:cli": "vite build --config vite.cli.config.ts && node scripts/pack-cli.mjs",
1010 "preview": "vite preview",
1111 "test": "tsx test/solver-test.ts",
12- "check-app": "node scripts/check-app.mjs"
12+ "check-app": "node scripts/check-app.mjs",
13+ "test:matlab": "tsx test/matlab-test.ts"
1314 },
1415 "dependencies": {
1516 "fflate": "^0.8.3",
src/app/components/SolutionSection.tsxmodified+6−3View file
@@ -33,10 +33,13 @@ interface Computed {
3333 point: ResultPoint;
3434 }
3535
36+// Only solvers that run in the browser can compute a field here.
37+const BROWSER_SOLVERS = SOLVERS.filter((s) => s.runtime === "numbl");
38+
3639 export function SolutionSection({ inst }: { inst: Laplace2dInstance }) {
37- const [solverId, setSolverId] = useState(SOLVERS[0].id);
40+ const [solverId, setSolverId] = useState(BROWSER_SOLVERS[0].id);
3841 const [n, setN] = useState<number>(
39- SOLVERS[0].sweepN[Math.floor(SOLVERS[0].sweepN.length * 0.7)]
42+ BROWSER_SOLVERS[0].sweepN[Math.floor(BROWSER_SOLVERS[0].sweepN.length * 0.7)]
4043 );
4144 const [busy, setBusy] = useState(false);
4245 const [error, setError] = useState<string | null>(null);
@@ -94,7 +97,7 @@ export function SolutionSection({ inst }: { inst: Laplace2dInstance }) {
9497 setN(sw[Math.floor(sw.length * 0.7)]);
9598 }}
9699 >
97- {SOLVERS.map((s) => (
100+ {BROWSER_SOLVERS.map((s) => (
98101 <option key={s.id} value={s.id}>
99102 {s.name}
100103 </option>
src/app/pages/AboutPage.tsxmodified+5−3View file
@@ -18,9 +18,11 @@ export function AboutPage() {
1818 points. A problem defines its own solver interface and a short list
1919 of official <strong>instances</strong> (parameter combinations) in a
2020 written specification, so every solver is compared on identical
21- inputs. The solvers on this site are MATLAB function files run by{" "}
22- <a href="https://numbl.org">numbl</a>, client side; the identical
23- harness runs from the command line.
21+ inputs. Solvers are MATLAB function files. Most run via{" "}
22+ <a href="https://numbl.org">numbl</a>, in the browser and from the
23+ command line alike; some run only in real MATLAB through the command
24+ line, and their results are marked as not reproducible in the
25+ browser.
2426 </p>
2527
2628 <h2>Measurement</h2>
src/app/pages/ProblemPage.tsxmodified+15−8View file
@@ -262,7 +262,10 @@ export function ProblemPage({ problemId }: { problemId: string }) {
262262 />
263263 <strong>{s.name}</strong>{" "}
264264 <span className="small muted">
265- {s.id} v{s.version} · {s.backend}
265+ {s.id} v{s.version} · {s.backend} ·{" "}
266+ {s.runtime === "matlab"
267+ ? "runs in MATLAB via the command line"
268+ : "runs via numbl in the browser and command line"}
266269 </span>
267270 </div>
268271 <p className="small" style={{ color: "var(--text-2)" }}>
@@ -315,13 +318,17 @@ export function ProblemPage({ problemId }: { problemId: string }) {
315318 />
316319 {s.name}
317320 </label>{" "}
318- <button
319- onClick={() => runSolver(s.id)}
320- disabled={running !== null}
321- title={`Run the full ${s.id} sweep on ${instanceId} in this browser`}
322- >
323- {running === s.id ? "running…" : "Run in this browser"}
324- </button>
321+ {s.runtime === "numbl" ? (
322+ <button
323+ onClick={() => runSolver(s.id)}
324+ disabled={running !== null}
325+ title={`Run the full ${s.id} sweep on ${instanceId} in this browser`}
326+ >
327+ {running === s.id ? "running…" : "Run in this browser"}
328+ </button>
329+ ) : (
330+ <span className="small muted">MATLAB only (via the CLI)</span>
331+ )}
325332 </span>
326333 ))}
327334 <label>
src/app/results.tsmodified+2−2View file
@@ -45,6 +45,6 @@ export async function fetchCommittedResults(): Promise<ResultFile[]> {
4545 export function environmentLabel(r: ResultFile): string {
4646 const env = r.environment;
4747 if (env.machineLabel) return `${env.machineLabel} (${env.kind})`;
48- if (env.kind === "node") return `${env.cpu ?? "unknown cpu"} (node)`;
49- return "browser";
48+ if (env.kind === "browser") return "browser";
49+ return `${env.cpu ?? "unknown cpu"} (${env.kind})`;
5050 }
src/cli/main.tsmodified+65−16View file
@@ -17,6 +17,7 @@ import os from "os";
1717 import { INSTANCES, getInstance } from "../problems/laplace2d/spec";
1818 import { setNumblFileIO } from "../harness/numblRun";
1919 import { NodeFileIOAdapter } from "./nodeFileIO";
20+import { ensureChunkie, matlabAvailable, runMatlabSweep } from "./matlabRun";
2021
2122 setNumblFileIO((vfs) => new NodeFileIOAdapter(vfs));
2223 import { SOLVERS, getSolver, type SolverManifest } from "../solvers";
@@ -68,6 +69,20 @@ function environment(machineLabel: string | undefined, builtin: boolean): Result
6869 };
6970 }
7071
72+function 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+ };
84+}
85+
7186 interface Args {
7287 command: string;
7388 flags: Record<string, string>;
@@ -127,6 +142,7 @@ async function runCommand(flags: Record<string, string>) {
127142 description: `custom solver from ${file}`,
128143 version: flags["solver-version"] ?? "0.0.0",
129144 backend: "cpu",
145+ runtime: "numbl",
130146 sweepN: getSolver("nystrom-dlp").sweepN,
131147 };
132148 solverList = [
@@ -137,7 +153,18 @@ async function runCommand(flags: Record<string, string>) {
137153 },
138154 ];
139155 } else {
140- const wanted = flags.solver ? [getSolver(flags.solver)] : SOLVERS;
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+ }
141168 solverList = wanted.map((manifest) => ({
142169 manifest,
143170 sources: { ...base, solver: readSrc(`solvers/${manifest.id}/solver.m`) },
@@ -152,19 +179,40 @@ async function runCommand(flags: Record<string, string>) {
152179 for (const { manifest, sources, source } of solverList) {
153180 console.log(`\n${inst.id} / ${manifest.id}`);
154181 console.log(" n relMax relL2 solve(s)");
155- const points = runSweep({
156- instance: inst,
157- solver: manifest,
158- sources,
159- repeats,
160- maxN,
161- onPoint: (p) => {
162- console.log(
163- ` ${String(p.n).padStart(4)} ${p.relMax.toExponential(2)} ` +
164- `${p.relL2.toExponential(2)} ${p.solveSeconds.toFixed(4)}`
165- );
166- },
167- });
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+ }
168216 const result = await buildResultFile({
169217 instance: inst,
170218 solver: {
@@ -173,9 +221,10 @@ async function runCommand(flags: Record<string, string>) {
173221 backend: manifest.backend,
174222 source,
175223 },
176- environment: env,
224+ environment: runEnv,
177225 repeats,
178- points: points.map(toResultPoint),
226+ points: resultPoints,
227+ timer,
179228 });
180229 const name = `laplace-dirichlet-2d.${inst.id}.${manifest.id}.json`;
181230 const path = join(outDir, name);
src/cli/matlabRun.tsadded+150−0View file
@@ -0,0 +1,150 @@
1+// Runs a work-precision sweep in real MATLAB. Used for solvers whose
2+// manifest declares runtime "matlab": the harness writes the problem
3+// files, the solver, and a generated driver into a temp directory, runs
4+// `matlab -batch` once for the whole sweep (one MATLAB startup per
5+// instance), and reads a JSON payload back. Errors are computed on the
6+// node side against the exact solution, as for numbl runs; timing is
7+// MATLAB's own tic/toc with the same warmup-plus-median protocol.
8+
9+import { execFileSync, spawnSync } from "node:child_process";
10+import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11+import { homedir, tmpdir } from "node:os";
12+import { join } from "node:path";
13+import type { Laplace2dInstance } from "../problems/laplace2d/spec";
14+import { evalErrors } from "../problems/laplace2d/exact";
15+import type { ResultPoint } from "../harness/resultSchema";
16+
17+const CHUNKIE_GIT = "https://github.com/fastalgorithms/chunkie";
18+const depsDir = join(homedir(), ".cache", "fastandaccurate", "matlab-deps");
19+
20+export function matlabAvailable(): boolean {
21+ try {
22+ execFileSync("which", ["matlab"], { stdio: "ignore" });
23+ return true;
24+ } catch {
25+ return false;
26+ }
27+}
28+
29+/** Clone chunkie (with the FLAM submodule) on first use and return the
30+ * MATLAB setup lines that put it on the path. chunkie's own startup.m is
31+ * deliberately not used: it attempts to compile fmm2d when a Fortran
32+ * compiler is present, which an unattended run must not do, and the
33+ * direct (accel=false) code path needs only the toolbox and FLAM. */
34+export function ensureChunkie(): string[] {
35+ const dir = join(depsDir, "chunkie");
36+ if (!existsSync(join(dir, "chunkie"))) {
37+ mkdirSync(depsDir, { recursive: true });
38+ console.log(`fetching chunkie into ${dir}`);
39+ execFileSync(
40+ "git",
41+ ["clone", "--depth", "1", "--recurse-submodules", "--shallow-submodules", CHUNKIE_GIT, dir],
42+ { stdio: "inherit" }
43+ );
44+ }
45+ if (!existsSync(join(dir, "chunkie", "FLAM", "startup.m"))) {
46+ execFileSync(
47+ "git",
48+ ["-C", dir, "submodule", "update", "--init", "--depth", "1", "chunkie/FLAM"],
49+ { stdio: "inherit" }
50+ );
51+ }
52+ return [
53+ `addpath('${join(dir, "chunkie")}');`,
54+ `run('${join(dir, "chunkie", "FLAM", "startup.m")}');`,
55+ ];
56+}
57+
58+export interface MatlabSweepOptions {
59+ instance: Laplace2dInstance;
60+ ns: number[];
61+ repeats: number;
62+ sources: { buildProblem: string; bdata: string; solver: string };
63+ /** MATLAB lines run before anything else (addpath etc.). */
64+ setup: string[];
65+ onPoint?: (point: ResultPoint, index: number, total: number) => void;
66+}
67+
68+export interface MatlabSweepResult {
69+ points: ResultPoint[];
70+ matlabVersion: string;
71+}
72+
73+function asArray(x: number | number[]): number[] {
74+ return Array.isArray(x) ? x : [x];
75+}
76+
77+export function runMatlabSweep(opts: MatlabSweepOptions): MatlabSweepResult {
78+ const { instance, ns, repeats } = opts;
79+ const dir = mkdtempSync(join(tmpdir(), "fastandaccurate-matlab-"));
80+ try {
81+ writeFileSync(join(dir, "build_problem.m"), opts.sources.buildProblem);
82+ writeFileSync(join(dir, "laplace2d_bdata.m"), opts.sources.bdata);
83+ writeFileSync(join(dir, "solver.m"), opts.sources.solver);
84+ const main = [
85+ "% generated by the fastandaccurate MATLAB harness",
86+ ...opts.setup,
87+ `ns = [${ns.join(" ")}];`,
88+ `nrep = ${repeats};`,
89+ `prob = build_problem(${instance.a}, ${instance.k}, ${instance.d}, 0);`,
90+ "results = cell(numel(ns), 1);",
91+ "for i = 1:numel(ns)",
92+ " n = ns(i);",
93+ " tic; out = solver(prob, n); cold = toc;",
94+ " times = zeros(nrep, 1);",
95+ " for r = 1:nrep",
96+ " tic; out = solver(prob, n); times(r) = toc;",
97+ " end",
98+ " results{i} = struct('n', n, 'cold', cold, 'times', times, 'ueval', out.uEval);",
99+ " fprintf('point n=%d done (%.3fs)\\n', n, median(times));",
100+ "end",
101+ "payload = struct('matlabVersion', version, 'results', {results});",
102+ "fid = fopen('out_results.json', 'w');",
103+ "fwrite(fid, jsonencode(payload));",
104+ "fclose(fid);",
105+ "",
106+ ].join("\n");
107+ writeFileSync(join(dir, "main.m"), main);
108+ const proc = spawnSync("matlab", ["-batch", "main"], {
109+ cwd: dir,
110+ encoding: "utf8",
111+ timeout: 60 * 60 * 1000,
112+ });
113+ const outPath = join(dir, "out_results.json");
114+ if (!existsSync(outPath)) {
115+ throw new Error(
116+ `MATLAB run failed (exit ${proc.status}):\n${(proc.stdout ?? "").slice(-2000)}\n${(proc.stderr ?? "").slice(-2000)}`
117+ );
118+ }
119+ const payload = JSON.parse(readFileSync(outPath, "utf8")) as {
120+ matlabVersion: string;
121+ results:
122+ | { n: number; cold: number; times: number | number[]; ueval: number[] }[]
123+ | { n: number; cold: number; times: number | number[]; ueval: number[] };
124+ };
125+ const entries = Array.isArray(payload.results)
126+ ? payload.results
127+ : [payload.results];
128+ const points = entries.map((e) => {
129+ const times = asArray(e.times);
130+ const sorted = [...times].sort((x, y) => x - y);
131+ const median =
132+ sorted.length % 2 === 1
133+ ? sorted[(sorted.length - 1) / 2]
134+ : (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2;
135+ const { relMax, relL2 } = evalErrors(instance, e.ueval);
136+ return {
137+ n: e.n,
138+ solveSeconds: median,
139+ solveSecondsAll: times,
140+ coldSeconds: e.cold,
141+ relMax,
142+ relL2,
143+ };
144+ });
145+ points.forEach((p, i) => opts.onPoint?.(p, i, points.length));
146+ return { points, matlabVersion: payload.matlabVersion };
147+ } finally {
148+ rmSync(dir, { recursive: true, force: true });
149+ }
150+}
src/harness/resultSchema.tsmodified+7−4View file
@@ -15,10 +15,11 @@ export const RESULT_FORMAT = "fastandaccurate-result";
1515 export const RESULT_FORMAT_VERSION = 1;
1616
1717 export interface ResultEnvironment {
18- kind: "browser" | "node";
19- /** User agent string (browser) or node version (node). */
18+ kind: "browser" | "node" | "matlab";
19+ /** User agent (browser), node version (node), or MATLAB version. */
2020 runtime: string;
21- numblVersion: string;
21+ /** Absent for runs outside numbl (e.g. real MATLAB). */
22+ numblVersion?: string;
2223 os?: string;
2324 cpu?: string;
2425 /** Free-text label a human recognizes ("office workstation"). */
@@ -90,6 +91,8 @@ export async function buildResultFile(opts: {
9091 environment: ResultEnvironment;
9192 repeats: number;
9293 points: ResultPoint[];
94+ /** What measured the times (default numbl tic/toc). */
95+ timer?: string;
9396 }): Promise<ResultFile> {
9497 return {
9598 format: RESULT_FORMAT,
@@ -104,7 +107,7 @@ export async function buildResultFile(opts: {
104107 protocol: {
105108 warmupRuns: 1,
106109 timedRuns: opts.repeats,
107- timer: "numbl tic/toc",
110+ timer: opts.timer ?? "numbl tic/toc",
108111 },
109112 createdUtc: new Date().toISOString(),
110113 points: opts.points,
src/solvers/chunkie-dlp/solver.mmodified+7−7View file
@@ -8,13 +8,14 @@ function out = solver(prob, n)
88 % panelized into n uniform 16th-order Gauss-Legendre chunks, chunkermat
99 % assembles the system with high-order singular quadrature, the dense
1010 % system is solved directly, and chunkerkerneval evaluates the potential
11-% with corrected quadrature for targets near the boundary. The package
12-% is fetched by mip on first use.
11+% with corrected quadrature for targets near the boundary.
12+%
13+% This solver runs in real MATLAB only: the command-line harness invokes
14+% it through `matlab -batch` with chunkie on the path (fetched on first
15+% use). It is not runnable in the browser.
1316 %
1417 % n : number of chunks (16 points each).
1518
16-mip load --install magland/magland/chunkie;
17-
1819 chnkr = chunkerfuncuni(@(t) fcurve(t, prob), n);
1920
2021 % Dirichlet data at the nodes: for this problem family the curve
@@ -41,9 +42,8 @@ end
4142
4243 function u = eval_targets(chnkr, fkern, sigma, XY)
4344 % Direct (unaccelerated) evaluation, in blocks to bound memory. accel is
44-% disabled because chunkie's FMM acceleration binds to the fmm2d
45-% library, which is not available in this embedded numbl runtime; at
46-% these sizes direct evaluation is cheap anyway.
45+% disabled so that the FLAM/fmm2d submodules are not required; at these
46+% sizes direct evaluation is cheap anyway.
4747 opts = struct();
4848 opts.accel = false;
4949 m = size(XY, 1);
src/solvers/index.tsmodified+12−7View file
@@ -13,6 +13,9 @@ export interface SolverManifest {
1313 * alter results. */
1414 version: string;
1515 backend: "cpu" | "gpu";
16+ /** What executes the solver: "numbl" solvers run in the browser and in
17+ * the CLI; "matlab" solvers run only in real MATLAB via the CLI. */
18+ runtime: "numbl" | "matlab";
1619 /** The resolution values a standard work-precision sweep runs. */
1720 sweepN: number[];
1821 }
@@ -30,6 +33,7 @@ export const SOLVERS: SolverManifest[] = [
3033 "attainable accuracy near 1e-10 in exchange for very small n.",
3134 version: "1.0.0",
3235 backend: "cpu",
36+ runtime: "numbl",
3337 sweepN: [8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256],
3438 },
3539 {
@@ -43,6 +47,7 @@ export const SOLVERS: SolverManifest[] = [
4347 "instance costs more nodes rather than a lost method assumption.",
4448 version: "1.0.0",
4549 backend: "cpu",
50+ runtime: "numbl",
4651 sweepN: [16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768],
4752 },
4853 {
@@ -53,14 +58,14 @@ export const SOLVERS: SolverManifest[] = [
5358 "discretized by chunkie, a production MATLAB boundary-integral " +
5459 "toolbox: n uniform 16th-order Gauss-Legendre panels, high-order " +
5560 "singular quadrature in the assembly, a direct dense solve, and " +
56- "near-corrected evaluation of the potential. chunkie's default " +
57- "quadrature tolerances cap the attainable accuracy near 1e-11, and " +
58- "the timings include the cost of running a general-purpose library " +
59- "through numbl. The package is fetched by mip on first use, so the " +
60- "first run in a session spends tens of seconds downloading it; " +
61- "later runs do not.",
62- version: "1.0.0",
61+ "near-corrected evaluation of the potential. Runs in real MATLAB " +
62+ "only: the command line invokes matlab -batch and fetches chunkie " +
63+ "on first use, so its results appear here but cannot be rerun in " +
64+ "the browser. chunkie's default quadrature tolerances cap the " +
65+ "attainable accuracy near 1e-11.",
66+ version: "2.0.0",
6367 backend: "cpu",
68+ runtime: "matlab",
6469 sweepN: [2, 3, 4, 6, 8, 12, 16, 24, 32, 48],
6570 },
6671 ];
test/matlab-test.tsadded+66−0View file
@@ -0,0 +1,66 @@
1+// Convergence test for the MATLAB-runtime solvers, run where real MATLAB
2+// exists (not in CI): npx tsx test/matlab-test.ts
3+// Exits quietly with a notice when no matlab is on the PATH.
4+
5+import { readFileSync } from "fs";
6+import { fileURLToPath } from "url";
7+import { dirname, join } from "path";
8+import { getInstance } from "../src/problems/laplace2d/spec";
9+import { getSolver } from "../src/solvers";
10+import {
11+ ensureChunkie,
12+ matlabAvailable,
13+ runMatlabSweep,
14+} from "../src/cli/matlabRun";
15+
16+if (!matlabAvailable()) {
17+ console.log("matlab not found on PATH; skipping MATLAB solver tests");
18+ process.exit(0);
19+}
20+
21+const root = join(dirname(fileURLToPath(import.meta.url)), "..");
22+const read = (p: string) => readFileSync(join(root, p), "utf-8");
23+const sources = {
24+ buildProblem: read("src/problems/laplace2d/matlab/build_problem.m"),
25+ bdata: read("src/problems/laplace2d/matlab/laplace2d_bdata.m"),
26+ solver: read("src/solvers/chunkie-dlp/solver.m"),
27+};
28+
29+const mustReach: Record<string, number> = {
30+ "disk-easy": 1e-10,
31+ "star-hard": 1e-9,
32+};
33+
34+let failures = 0;
35+for (const [instId, reach] of Object.entries(mustReach)) {
36+ console.log(`\n== ${instId} / chunkie-dlp (MATLAB)`);
37+ console.log(" n relMax relL2 solve(s)");
38+ let best = Infinity;
39+ const { points, matlabVersion } = runMatlabSweep({
40+ instance: getInstance(instId),
41+ ns: getSolver("chunkie-dlp").sweepN,
42+ repeats: 1,
43+ sources,
44+ setup: ensureChunkie(),
45+ });
46+ for (const p of points) {
47+ best = Math.min(best, p.relMax);
48+ console.log(
49+ ` ${String(p.n).padStart(4)} ${p.relMax.toExponential(3)} ` +
50+ `${p.relL2.toExponential(3)} ${p.solveSeconds.toFixed(4)}`
51+ );
52+ }
53+ console.log(` (MATLAB ${matlabVersion})`);
54+ if (best > reach) {
55+ console.log(` FAIL: best relMax ${best.toExponential(2)} > ${reach}`);
56+ failures++;
57+ } else {
58+ console.log(` ok (best relMax ${best.toExponential(2)})`);
59+ }
60+}
61+
62+if (failures > 0) {
63+ console.error(`\n${failures} failure(s)`);
64+ process.exit(1);
65+}
66+console.log("\nall MATLAB checks passed");
test/solver-test.tsmodified+3−3View file
@@ -26,7 +26,6 @@ const base = {
2626 const solverSources: Record<string, MatlabSources> = {
2727 mfs: { ...base, solver: read("src/solvers/mfs/solver.m") },
2828 "nystrom-dlp": { ...base, solver: read("src/solvers/nystrom-dlp/solver.m") },
29- "chunkie-dlp": { ...base, solver: read("src/solvers/chunkie-dlp/solver.m") },
3029 };
3130
3231 // Best relMax each solver must reach over its full sweep. On star-hard,
@@ -36,7 +35,6 @@ const solverSources: Record<string, MatlabSources> = {
3635 const mustReach: Record<string, Record<string, number>> = {
3736 mfs: { "disk-easy": 1e-12, "star-medium": 1e-12, "star-hard": 1e-2 },
3837 "nystrom-dlp": { "disk-easy": 1e-10, "star-medium": 1e-10, "star-hard": 1e-8 },
39- "chunkie-dlp": { "disk-easy": 1e-10, "star-medium": 1e-10, "star-hard": 1e-9 },
4038 };
4139 const mustNotReach: Record<string, Record<string, number>> = {
4240 mfs: { "star-hard": 1e-8 },
@@ -44,8 +42,10 @@ const mustNotReach: Record<string, Record<string, number>> = {
4442
4543 let failures = 0;
4644
45+// MATLAB-runtime solvers are covered by test/matlab-test.ts, run locally
46+// where MATLAB exists; this suite tests the numbl solvers.
4747 for (const inst of INSTANCES) {
48- for (const solver of SOLVERS) {
48+ for (const solver of SOLVERS.filter((s) => s.runtime === "numbl")) {
4949 console.log(`\n== ${inst.id} / ${solver.id}`);
5050 console.log(" n relMax relL2 solve(s) cold(s)");
5151 let best = Infinity;
moveopenescclose