@mcmcjs/stan 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/index.cjs +1000 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +263 -0
- package/dist/index.d.ts +263 -0
- package/dist/index.js +976 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
// src/compile.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4
|
+
import { dirname, isAbsolute, join as join2 } from "path";
|
|
5
|
+
import { createRunner as createRunner3 } from "@mcmcjs/engine";
|
|
6
|
+
|
|
7
|
+
// src/environment.ts
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { basename, join } from "path";
|
|
11
|
+
import { createRunner } from "@mcmcjs/engine";
|
|
12
|
+
import { createRunner as createRunner2 } from "@mcmcjs/engine";
|
|
13
|
+
import { DEFAULT_CMDSTAN_CHANNEL } from "@mcmcjs/core";
|
|
14
|
+
var defaultRunner = createRunner();
|
|
15
|
+
var INSTALLED_CMDSTAN_CHANNEL = DEFAULT_CMDSTAN_CHANNEL;
|
|
16
|
+
var PINNED_CMDSTAN_VERSION = "2.39.0";
|
|
17
|
+
function managedStanRoot() {
|
|
18
|
+
const home = process.env.HOME || homedir();
|
|
19
|
+
const dataHome = process.env.XDG_DATA_HOME || join(home, ".local", "share");
|
|
20
|
+
return join(dataHome, "mcmcjs", "stan");
|
|
21
|
+
}
|
|
22
|
+
function isCmdStanHome(dir) {
|
|
23
|
+
return existsSync(join(dir, "makefile")) && existsSync(join(dir, "bin", "stanc"));
|
|
24
|
+
}
|
|
25
|
+
function versionFromDirName(dir) {
|
|
26
|
+
return basename(dir).match(/^cmdstan-(\d+\.\d+\.\d+(?:-rc\d+)?)$/)?.[1];
|
|
27
|
+
}
|
|
28
|
+
function versionFromMakefile(home) {
|
|
29
|
+
try {
|
|
30
|
+
const makefile = readFileSync(join(home, "makefile"), "utf8");
|
|
31
|
+
return makefile.match(/CMDSTAN_VERSION\s*:?=\s*(\S+)/)?.[1];
|
|
32
|
+
} catch {
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function installAt(home) {
|
|
37
|
+
if (!isCmdStanHome(home)) return void 0;
|
|
38
|
+
const version = versionFromDirName(home) ?? versionFromMakefile(home);
|
|
39
|
+
return version ? { version, home } : void 0;
|
|
40
|
+
}
|
|
41
|
+
function scanRoot(root) {
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = readdirSync(root);
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const installs = [];
|
|
49
|
+
for (const name of entries) {
|
|
50
|
+
if (!name.startsWith("cmdstan-")) continue;
|
|
51
|
+
const install = installAt(join(root, name));
|
|
52
|
+
if (install) installs.push(install);
|
|
53
|
+
}
|
|
54
|
+
return installs;
|
|
55
|
+
}
|
|
56
|
+
function compareVersions(a, b) {
|
|
57
|
+
const parse = (v) => {
|
|
58
|
+
const [core, rc] = v.split("-rc");
|
|
59
|
+
const nums = (core ?? "").split(".").map((p) => Number.parseInt(p, 10) || 0);
|
|
60
|
+
return { nums, rc: rc === void 0 ? Number.POSITIVE_INFINITY : Number.parseInt(rc, 10) || 0 };
|
|
61
|
+
};
|
|
62
|
+
const pa = parse(a);
|
|
63
|
+
const pb = parse(b);
|
|
64
|
+
for (let i = 0; i < Math.max(pa.nums.length, pb.nums.length); i++) {
|
|
65
|
+
const d = (pa.nums[i] ?? 0) - (pb.nums[i] ?? 0);
|
|
66
|
+
if (d !== 0) return d;
|
|
67
|
+
}
|
|
68
|
+
if (pa.rc === pb.rc) return 0;
|
|
69
|
+
return pa.rc < pb.rc ? -1 : 1;
|
|
70
|
+
}
|
|
71
|
+
function listCmdStanInstalls() {
|
|
72
|
+
const explicit = process.env.MCMCJS_CMDSTAN || process.env.CMDSTAN;
|
|
73
|
+
if (explicit) {
|
|
74
|
+
const install = installAt(explicit);
|
|
75
|
+
if (!install) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`MCMCJS_CMDSTAN/CMDSTAN points at ${explicit}, which is not a CmdStan directory (expected a makefile and bin/stanc); unset it or point it at a CmdStan home.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return [install];
|
|
81
|
+
}
|
|
82
|
+
const home = process.env.HOME || homedir();
|
|
83
|
+
const installs = [...scanRoot(managedStanRoot()), ...scanRoot(join(home, ".cmdstan"))];
|
|
84
|
+
return installs.sort((a, b) => compareVersions(b.version, a.version));
|
|
85
|
+
}
|
|
86
|
+
function resolveCmdStan(requested = INSTALLED_CMDSTAN_CHANNEL) {
|
|
87
|
+
const installs = listCmdStanInstalls();
|
|
88
|
+
if (requested === INSTALLED_CMDSTAN_CHANNEL) {
|
|
89
|
+
const newest = installs[0];
|
|
90
|
+
if (newest) return newest;
|
|
91
|
+
throw new Error(
|
|
92
|
+
"CmdStan not found. Run `mcmc setup --engine stan`, or point MCMCJS_CMDSTAN at a CmdStan directory."
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const match = installs.find((i) => i.version === requested);
|
|
96
|
+
if (match) return match;
|
|
97
|
+
throw new Error(
|
|
98
|
+
`CmdStan ${requested} not found. Run \`mcmc setup --engine stan --stan-version ${requested}\`, or point MCMCJS_CMDSTAN at a CmdStan directory.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
async function detect(command, args, parseVersion, runner) {
|
|
102
|
+
try {
|
|
103
|
+
const version = parseVersion(await runner(command, args));
|
|
104
|
+
if (version) return { found: true, version, path: command };
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
return { found: false };
|
|
108
|
+
}
|
|
109
|
+
var versionNumber = (stdout) => stdout.match(/(\d+\.\d+(?:\.\d+)?)/)?.[1];
|
|
110
|
+
function detectMake(runner = defaultRunner) {
|
|
111
|
+
return detect("make", ["--version"], versionNumber, runner);
|
|
112
|
+
}
|
|
113
|
+
async function detectCxx(runner = defaultRunner) {
|
|
114
|
+
for (const command of ["g++", "clang++"]) {
|
|
115
|
+
const info = await detect(command, ["--version"], versionNumber, runner);
|
|
116
|
+
if (info.found) return info;
|
|
117
|
+
}
|
|
118
|
+
return { found: false };
|
|
119
|
+
}
|
|
120
|
+
async function detectStanc(install, runner = defaultRunner) {
|
|
121
|
+
if (!install) return { found: false };
|
|
122
|
+
const stanc = join(install.home, "bin", "stanc");
|
|
123
|
+
return detect(stanc, ["--version"], versionNumber, runner);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/compile.ts
|
|
127
|
+
function cacheKeySource(source, modelDir, depth = 4) {
|
|
128
|
+
if (depth === 0) return source;
|
|
129
|
+
const parts = [source];
|
|
130
|
+
for (const match of source.matchAll(/^\s*#include\s+[<"']?([^>"'\s]+)[>"']?/gm)) {
|
|
131
|
+
const file = match[1];
|
|
132
|
+
if (!file) continue;
|
|
133
|
+
try {
|
|
134
|
+
const included = readFileSync2(isAbsolute(file) ? file : join2(modelDir, file), "utf8");
|
|
135
|
+
parts.push(cacheKeySource(included, modelDir, depth - 1));
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return parts.join("\n");
|
|
140
|
+
}
|
|
141
|
+
function modelCacheDir(source, version, cacheRoot) {
|
|
142
|
+
const key = createHash("sha256").update(`${version}
|
|
143
|
+
${source}`).digest("hex").slice(0, 16);
|
|
144
|
+
return join2(cacheRoot ?? join2(managedStanRoot(), "models"), key);
|
|
145
|
+
}
|
|
146
|
+
async function compileModel(install, modelPath, options = {}) {
|
|
147
|
+
const { runner = createRunner3(10 * 6e4), cacheRoot } = options;
|
|
148
|
+
const source = readFileSync2(modelPath, "utf8");
|
|
149
|
+
const modelDir = dirname(modelPath);
|
|
150
|
+
const dir = modelCacheDir(cacheKeySource(source, modelDir), install.version, cacheRoot);
|
|
151
|
+
const binaryPath = join2(dir, "model");
|
|
152
|
+
const okPath = join2(dir, ".ok");
|
|
153
|
+
if (existsSync2(okPath) && existsSync2(binaryPath)) return { binaryPath, cached: true };
|
|
154
|
+
mkdirSync(dir, { recursive: true });
|
|
155
|
+
writeFileSync(join2(dir, "model.stan"), source);
|
|
156
|
+
await runner("make", ["-C", install.home, `STANCFLAGS=--include-paths=${modelDir}`, binaryPath]);
|
|
157
|
+
if (!existsSync2(binaryPath)) {
|
|
158
|
+
throw new Error("make reported success but produced no model executable");
|
|
159
|
+
}
|
|
160
|
+
writeFileSync(okPath, "");
|
|
161
|
+
return { binaryPath, cached: false };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/csv.ts
|
|
165
|
+
import { closeSync, openSync, readSync } from "fs";
|
|
166
|
+
import { fromStanName } from "@mcmcjs/core";
|
|
167
|
+
var STAT_RENAME = {
|
|
168
|
+
lp__: "lp",
|
|
169
|
+
energy__: "energy",
|
|
170
|
+
divergent__: "diverging",
|
|
171
|
+
treedepth__: "tree_depth",
|
|
172
|
+
n_leapfrog__: "n_steps",
|
|
173
|
+
accept_stat__: "acceptance_rate",
|
|
174
|
+
stepsize__: "step_size"
|
|
175
|
+
};
|
|
176
|
+
function columnToLeaf(column) {
|
|
177
|
+
if (column.endsWith("__")) return STAT_RENAME[column] ?? null;
|
|
178
|
+
return fromStanName(column);
|
|
179
|
+
}
|
|
180
|
+
function parseStanNumber(field) {
|
|
181
|
+
const value = Number(field);
|
|
182
|
+
if (!Number.isNaN(value) || field === "nan") return value;
|
|
183
|
+
if (field === "inf" || field === "+inf") return Number.POSITIVE_INFINITY;
|
|
184
|
+
if (field === "-inf") return Number.NEGATIVE_INFINITY;
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
function createStanCsvTail(filePath, opts) {
|
|
188
|
+
let position = 0;
|
|
189
|
+
let pending = "";
|
|
190
|
+
let leaves;
|
|
191
|
+
let seq = 0;
|
|
192
|
+
const rows = [];
|
|
193
|
+
const emit = (count) => {
|
|
194
|
+
if (count === 0 || !leaves) return;
|
|
195
|
+
const draws = {};
|
|
196
|
+
for (let c = 0; c < leaves.length; c++) {
|
|
197
|
+
const leaf = leaves[c];
|
|
198
|
+
if (!leaf) continue;
|
|
199
|
+
const values = new Array(count);
|
|
200
|
+
for (let r = 0; r < count; r++) values[r] = rows[r]?.[c] ?? Number.NaN;
|
|
201
|
+
draws[leaf] = values;
|
|
202
|
+
}
|
|
203
|
+
rows.splice(0, count);
|
|
204
|
+
opts.onBatch({ chain: opts.chain, seq, iteration: null, draws });
|
|
205
|
+
seq += 1;
|
|
206
|
+
};
|
|
207
|
+
const consume = (line) => {
|
|
208
|
+
const trimmed = line.trim();
|
|
209
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) return;
|
|
210
|
+
if (!leaves) {
|
|
211
|
+
leaves = trimmed.split(",").map(columnToLeaf);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const fields = trimmed.split(",");
|
|
215
|
+
if (fields.length !== leaves.length) return;
|
|
216
|
+
rows.push(fields.map(parseStanNumber));
|
|
217
|
+
if (rows.length >= opts.batchSize) emit(opts.batchSize);
|
|
218
|
+
};
|
|
219
|
+
const poll = () => {
|
|
220
|
+
let fd;
|
|
221
|
+
try {
|
|
222
|
+
fd = openSync(filePath, "r");
|
|
223
|
+
} catch {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const chunk = Buffer.alloc(1 << 16);
|
|
228
|
+
for (; ; ) {
|
|
229
|
+
const read = readSync(fd, chunk, 0, chunk.length, position);
|
|
230
|
+
if (read <= 0) break;
|
|
231
|
+
position += read;
|
|
232
|
+
pending += chunk.toString("utf8", 0, read);
|
|
233
|
+
for (; ; ) {
|
|
234
|
+
const nl = pending.indexOf("\n");
|
|
235
|
+
if (nl < 0) break;
|
|
236
|
+
consume(pending.slice(0, nl));
|
|
237
|
+
pending = pending.slice(nl + 1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} finally {
|
|
241
|
+
closeSync(fd);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
return {
|
|
245
|
+
poll,
|
|
246
|
+
finish() {
|
|
247
|
+
poll();
|
|
248
|
+
emit(rows.length);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/doctor.ts
|
|
254
|
+
async function runDoctor(runner) {
|
|
255
|
+
const install = listCmdStanInstalls()[0];
|
|
256
|
+
const cmdstan = install ? { found: true, version: install.version, path: install.home } : { found: false };
|
|
257
|
+
const [stanc, make, cxx] = await Promise.all([
|
|
258
|
+
detectStanc(install, runner),
|
|
259
|
+
detectMake(runner),
|
|
260
|
+
detectCxx(runner)
|
|
261
|
+
]);
|
|
262
|
+
return {
|
|
263
|
+
cmdstan,
|
|
264
|
+
stanc,
|
|
265
|
+
make,
|
|
266
|
+
cxx,
|
|
267
|
+
install,
|
|
268
|
+
ready: cmdstan.found && make.found && cxx.found
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/engine.ts
|
|
273
|
+
var stanEngine = {
|
|
274
|
+
id: "stan",
|
|
275
|
+
displayName: "Stan (CmdStan)",
|
|
276
|
+
capabilities: { setup: true, versions: true, fit: true, predict: true },
|
|
277
|
+
async doctor(ctx) {
|
|
278
|
+
const report = await runDoctor(ctx.run);
|
|
279
|
+
const missingToolchain = !report.make.found || !report.cxx.found;
|
|
280
|
+
return {
|
|
281
|
+
engineId: "stan",
|
|
282
|
+
ready: report.ready,
|
|
283
|
+
tools: [
|
|
284
|
+
{ name: "cmdstan", ...report.cmdstan },
|
|
285
|
+
{ name: "stanc", ...report.stanc },
|
|
286
|
+
{ name: "make", ...report.make },
|
|
287
|
+
{ name: "c++", ...report.cxx }
|
|
288
|
+
],
|
|
289
|
+
hint: report.ready ? void 0 : missingToolchain ? "Stan models compile with the system toolchain; install make and a C++ compiler (g++ or clang++), then run `mcmc setup --engine stan`." : "CmdStan not found. Run `mcmc setup --engine stan`."
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// src/fit.ts
|
|
295
|
+
import { createHash as createHash2 } from "crypto";
|
|
296
|
+
import {
|
|
297
|
+
existsSync as existsSync3,
|
|
298
|
+
mkdirSync as mkdirSync2,
|
|
299
|
+
mkdtempSync,
|
|
300
|
+
readFileSync as readFileSync3,
|
|
301
|
+
renameSync,
|
|
302
|
+
rmSync,
|
|
303
|
+
writeFileSync as writeFileSync2
|
|
304
|
+
} from "fs";
|
|
305
|
+
import { tmpdir } from "os";
|
|
306
|
+
import { join as join3 } from "path";
|
|
307
|
+
import {
|
|
308
|
+
canonicalJson,
|
|
309
|
+
fromStanCSVFiles,
|
|
310
|
+
parseSamples,
|
|
311
|
+
RUN_RECORD_SCHEMA_VERSION,
|
|
312
|
+
toMCMCChainsJson
|
|
313
|
+
} from "@mcmcjs/core";
|
|
314
|
+
|
|
315
|
+
// src/runner.ts
|
|
316
|
+
import { spawn } from "child_process";
|
|
317
|
+
import { killTree } from "@mcmcjs/engine";
|
|
318
|
+
var DETACHED = process.platform !== "win32";
|
|
319
|
+
var STDERR_LIMIT = 1 << 22;
|
|
320
|
+
function createStanSpawn(defaultTimeoutMs = 30 * 6e4) {
|
|
321
|
+
return (command, args, opts = {}) => new Promise((resolve) => {
|
|
322
|
+
const child = spawn(command, args, {
|
|
323
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
324
|
+
detached: DETACHED
|
|
325
|
+
});
|
|
326
|
+
let stderr = "";
|
|
327
|
+
let stdoutPending = "";
|
|
328
|
+
let cancelled = false;
|
|
329
|
+
let settled = false;
|
|
330
|
+
const timeoutMs = opts.timeoutMs ?? defaultTimeoutMs;
|
|
331
|
+
const timer = setTimeout(() => {
|
|
332
|
+
stderr += "process timed out and was killed\n";
|
|
333
|
+
killTree(child);
|
|
334
|
+
}, timeoutMs);
|
|
335
|
+
timer.unref?.();
|
|
336
|
+
const onAbort = () => {
|
|
337
|
+
cancelled = true;
|
|
338
|
+
killTree(child);
|
|
339
|
+
};
|
|
340
|
+
if (opts.signal) {
|
|
341
|
+
if (opts.signal.aborted) onAbort();
|
|
342
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
343
|
+
}
|
|
344
|
+
child.stdout?.on("data", (data) => {
|
|
345
|
+
stdoutPending += data.toString("utf8");
|
|
346
|
+
for (; ; ) {
|
|
347
|
+
const nl = stdoutPending.indexOf("\n");
|
|
348
|
+
if (nl < 0) break;
|
|
349
|
+
opts.onStdoutLine?.(stdoutPending.slice(0, nl));
|
|
350
|
+
stdoutPending = stdoutPending.slice(nl + 1);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
child.stderr?.on("data", (data) => {
|
|
354
|
+
if (stderr.length < STDERR_LIMIT) stderr += data.toString("utf8");
|
|
355
|
+
});
|
|
356
|
+
const settle = (code) => {
|
|
357
|
+
if (settled) return;
|
|
358
|
+
settled = true;
|
|
359
|
+
clearTimeout(timer);
|
|
360
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
361
|
+
if (stdoutPending.length > 0) opts.onStdoutLine?.(stdoutPending);
|
|
362
|
+
resolve({ code, stderr, cancelled: cancelled || void 0 });
|
|
363
|
+
};
|
|
364
|
+
child.on("error", (error) => {
|
|
365
|
+
stderr = stderr || error.message;
|
|
366
|
+
settle(1);
|
|
367
|
+
});
|
|
368
|
+
child.on("close", (code) => settle(code ?? 1));
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function parseIterationLine(line) {
|
|
372
|
+
const match = line.match(/Iteration:\s*(\d+)\s*\/\s*(\d+)\s*\[\s*\d+%\]\s*\((Warmup|Sampling)\)/);
|
|
373
|
+
if (!match) return null;
|
|
374
|
+
return {
|
|
375
|
+
iteration: Number.parseInt(match[1] ?? "0", 10),
|
|
376
|
+
total: Number.parseInt(match[2] ?? "0", 10),
|
|
377
|
+
warmup: match[3] === "Warmup"
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/fit.ts
|
|
382
|
+
function sha256(text) {
|
|
383
|
+
return createHash2("sha256").update(text).digest("hex");
|
|
384
|
+
}
|
|
385
|
+
function tmpParent() {
|
|
386
|
+
const uid = typeof process.getuid === "function" ? `-${process.getuid()}` : "";
|
|
387
|
+
return join3(tmpdir(), `mcmcjs${uid}`);
|
|
388
|
+
}
|
|
389
|
+
function chainArgs(spec, chain1, dataPath, csvPath) {
|
|
390
|
+
const s = spec.sampler;
|
|
391
|
+
const total = s.draws + s.warmup;
|
|
392
|
+
const refresh = Math.max(1, Math.floor(total / 100));
|
|
393
|
+
return [
|
|
394
|
+
"sample",
|
|
395
|
+
`num_samples=${s.draws}`,
|
|
396
|
+
`num_warmup=${s.warmup}`,
|
|
397
|
+
"adapt",
|
|
398
|
+
`delta=${s.adapt_delta}`,
|
|
399
|
+
"data",
|
|
400
|
+
`file=${dataPath}`,
|
|
401
|
+
"random",
|
|
402
|
+
`seed=${spec.seed}`,
|
|
403
|
+
`id=${chain1}`,
|
|
404
|
+
"output",
|
|
405
|
+
`file=${csvPath}`,
|
|
406
|
+
`refresh=${refresh}`
|
|
407
|
+
];
|
|
408
|
+
}
|
|
409
|
+
function errorTail(stderr) {
|
|
410
|
+
const lines = stderr.trim().split("\n").map((l) => l.trim()).filter(Boolean);
|
|
411
|
+
return lines.at(-1);
|
|
412
|
+
}
|
|
413
|
+
async function runFit(spec, install, io) {
|
|
414
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
415
|
+
const start = performance.now();
|
|
416
|
+
const runtimeRequested = spec.backend.version;
|
|
417
|
+
const fail = (stage, error) => ({
|
|
418
|
+
status: "error",
|
|
419
|
+
runtimeRequested,
|
|
420
|
+
elapsedMs: Math.round(performance.now() - start),
|
|
421
|
+
stage,
|
|
422
|
+
error
|
|
423
|
+
});
|
|
424
|
+
if (io.signal?.aborted) {
|
|
425
|
+
return { status: "cancelled", runtimeRequested, elapsedMs: 0 };
|
|
426
|
+
}
|
|
427
|
+
let binaryPath;
|
|
428
|
+
try {
|
|
429
|
+
({ binaryPath } = await compileModel(install, spec.modelPath, io.compile));
|
|
430
|
+
} catch (error) {
|
|
431
|
+
return fail("compile", error.message);
|
|
432
|
+
}
|
|
433
|
+
if (io.signal?.aborted) {
|
|
434
|
+
return {
|
|
435
|
+
status: "cancelled",
|
|
436
|
+
runtimeRequested,
|
|
437
|
+
elapsedMs: Math.round(performance.now() - start)
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const ownTmp = io.tmpDir === void 0;
|
|
441
|
+
let tmp;
|
|
442
|
+
if (io.tmpDir === void 0) {
|
|
443
|
+
mkdirSync2(tmpParent(), { recursive: true });
|
|
444
|
+
tmp = mkdtempSync(join3(tmpParent(), "stan-fit-"));
|
|
445
|
+
} else {
|
|
446
|
+
tmp = io.tmpDir;
|
|
447
|
+
}
|
|
448
|
+
try {
|
|
449
|
+
const dataPath = join3(tmp, "data.json");
|
|
450
|
+
writeFileSync2(dataPath, JSON.stringify(spec.data));
|
|
451
|
+
const chains = spec.sampler.chains;
|
|
452
|
+
const spawn2 = io.spawn ?? createStanSpawn();
|
|
453
|
+
const csvPaths = Array.from({ length: chains }, (_, i) => join3(tmp, `chain_${i + 1}.csv`));
|
|
454
|
+
const tails = io.onDraws ? csvPaths.map(
|
|
455
|
+
(path, i) => createStanCsvTail(path, {
|
|
456
|
+
chain: i,
|
|
457
|
+
batchSize: io.drawBatchSize ?? 25,
|
|
458
|
+
onBatch: io.onDraws
|
|
459
|
+
})
|
|
460
|
+
) : [];
|
|
461
|
+
const pollTimer = tails.length > 0 ? setInterval(() => {
|
|
462
|
+
for (const tail of tails) tail.poll();
|
|
463
|
+
}, 150) : void 0;
|
|
464
|
+
pollTimer?.unref?.();
|
|
465
|
+
let results;
|
|
466
|
+
try {
|
|
467
|
+
results = await Promise.all(
|
|
468
|
+
csvPaths.map((csvPath, i) => {
|
|
469
|
+
const onProgress = io.onProgress;
|
|
470
|
+
return spawn2(binaryPath, chainArgs(spec, i + 1, dataPath, csvPath), {
|
|
471
|
+
signal: io.signal,
|
|
472
|
+
onStdoutLine: onProgress ? (line) => {
|
|
473
|
+
const p = parseIterationLine(line);
|
|
474
|
+
if (!p) return;
|
|
475
|
+
onProgress({
|
|
476
|
+
chain: i + 1,
|
|
477
|
+
of: chains,
|
|
478
|
+
fraction: p.total > 0 ? p.iteration / p.total : 0,
|
|
479
|
+
done: p.iteration === p.total
|
|
480
|
+
});
|
|
481
|
+
} : void 0
|
|
482
|
+
});
|
|
483
|
+
})
|
|
484
|
+
);
|
|
485
|
+
} finally {
|
|
486
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
487
|
+
}
|
|
488
|
+
for (const tail of tails) tail.finish();
|
|
489
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
490
|
+
if (io.signal?.aborted || results.some((r) => r.cancelled)) {
|
|
491
|
+
return { status: "cancelled", runtimeRequested, elapsedMs };
|
|
492
|
+
}
|
|
493
|
+
const failed = results.findIndex((r) => r.code !== 0);
|
|
494
|
+
if (failed >= 0) {
|
|
495
|
+
const r = results[failed];
|
|
496
|
+
return fail(
|
|
497
|
+
"sample",
|
|
498
|
+
`chain ${failed + 1} exited with code ${r?.code}${r && errorTail(r.stderr) ? `: ${errorTail(r.stderr)}` : ""}`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
let samplesJson;
|
|
502
|
+
try {
|
|
503
|
+
const texts = csvPaths.map((path) => readFileSync3(path, "utf8"));
|
|
504
|
+
samplesJson = JSON.stringify(toMCMCChainsJson(fromStanCSVFiles(texts)));
|
|
505
|
+
parseSamples(samplesJson);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
return fail("load_samples", `could not read CmdStan output: ${error.message}`);
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const partial = `${io.outPath}.tmp`;
|
|
511
|
+
writeFileSync2(partial, samplesJson);
|
|
512
|
+
renameSync(partial, io.outPath);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return fail("write", error.message);
|
|
515
|
+
}
|
|
516
|
+
const record = {
|
|
517
|
+
schema_version: RUN_RECORD_SCHEMA_VERSION,
|
|
518
|
+
spec_hash: spec.specHash,
|
|
519
|
+
seed: spec.seed,
|
|
520
|
+
backend: { id: spec.backend.id, runtime: spec.backend.runtime },
|
|
521
|
+
runtime: { requested: runtimeRequested, actual: install.version, path: install.home },
|
|
522
|
+
model_sha256: existsSync3(spec.modelPath) ? sha256(readFileSync3(spec.modelPath, "utf8")) : void 0,
|
|
523
|
+
data_sha256: io.dataSha256 ?? sha256(canonicalJson(spec.data)),
|
|
524
|
+
...io.dataFile ? { data_file: io.dataFile } : {},
|
|
525
|
+
samples_file: io.outPath,
|
|
526
|
+
started_at: startedAt,
|
|
527
|
+
elapsed_ms: Math.round(performance.now() - start)
|
|
528
|
+
};
|
|
529
|
+
try {
|
|
530
|
+
writeFileSync2(
|
|
531
|
+
io.recordPath ?? `${io.outPath}.run.json`,
|
|
532
|
+
`${JSON.stringify(record, null, 2)}
|
|
533
|
+
`
|
|
534
|
+
);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
return fail("write", `could not write the run record: ${error.message}`);
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
status: "ok",
|
|
540
|
+
samplesFile: io.outPath,
|
|
541
|
+
runtimeRequested,
|
|
542
|
+
runtimeActual: install.version,
|
|
543
|
+
elapsedMs: Math.round(performance.now() - start)
|
|
544
|
+
};
|
|
545
|
+
} finally {
|
|
546
|
+
if (ownTmp) rmSync(tmp, { recursive: true, force: true });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/matrix.ts
|
|
551
|
+
import { join as join4 } from "path";
|
|
552
|
+
async function runMatrix(spec, versions, io) {
|
|
553
|
+
const entries = [];
|
|
554
|
+
for (const version of versions) {
|
|
555
|
+
let entry;
|
|
556
|
+
try {
|
|
557
|
+
const install = resolveCmdStan(version);
|
|
558
|
+
const result = await runFit({ ...spec, backend: { ...spec.backend, version } }, install, {
|
|
559
|
+
...io.fit,
|
|
560
|
+
outPath: join4(io.outDir, `${version}.samples.json`),
|
|
561
|
+
dataFile: io.dataFile,
|
|
562
|
+
dataSha256: io.dataSha256,
|
|
563
|
+
signal: io.signal
|
|
564
|
+
});
|
|
565
|
+
entry = {
|
|
566
|
+
version,
|
|
567
|
+
status: result.status,
|
|
568
|
+
samplesFile: result.samplesFile,
|
|
569
|
+
runtimeActual: result.runtimeActual,
|
|
570
|
+
elapsedMs: result.elapsedMs,
|
|
571
|
+
stage: result.stage,
|
|
572
|
+
error: result.error
|
|
573
|
+
};
|
|
574
|
+
} catch (error) {
|
|
575
|
+
entry = { version, status: "error", elapsedMs: 0, error: error.message };
|
|
576
|
+
}
|
|
577
|
+
entries.push(entry);
|
|
578
|
+
if (entry.status === "cancelled") break;
|
|
579
|
+
if (entry.status === "error" && !io.keepGoing) break;
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
entries,
|
|
583
|
+
ok: entries.length === versions.length && entries.every((e) => e.status === "ok")
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/predict.ts
|
|
588
|
+
import { createHash as createHash3 } from "crypto";
|
|
589
|
+
import {
|
|
590
|
+
existsSync as existsSync4,
|
|
591
|
+
mkdirSync as mkdirSync3,
|
|
592
|
+
mkdtempSync as mkdtempSync2,
|
|
593
|
+
readFileSync as readFileSync4,
|
|
594
|
+
renameSync as renameSync2,
|
|
595
|
+
rmSync as rmSync2,
|
|
596
|
+
writeFileSync as writeFileSync3
|
|
597
|
+
} from "fs";
|
|
598
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
599
|
+
import { join as join5 } from "path";
|
|
600
|
+
import {
|
|
601
|
+
canonicalJson as canonicalJson2,
|
|
602
|
+
fromStanCSVFiles as fromStanCSVFiles2,
|
|
603
|
+
parseSamples as parseSamples2,
|
|
604
|
+
RUN_RECORD_SCHEMA_VERSION as RUN_RECORD_SCHEMA_VERSION2,
|
|
605
|
+
toMCMCChainsJson as toMCMCChainsJson2,
|
|
606
|
+
toStanName
|
|
607
|
+
} from "@mcmcjs/core";
|
|
608
|
+
function predictData(spec) {
|
|
609
|
+
if (!spec.predict) throw new Error("spec has no [predict] block");
|
|
610
|
+
return { ...spec.data, ...spec.predict.data ?? {} };
|
|
611
|
+
}
|
|
612
|
+
function fittedParamsCsv(samples, chain) {
|
|
613
|
+
const names = samples.variables;
|
|
614
|
+
const header = names.map(toStanName).join(",");
|
|
615
|
+
const columns = names.map((name) => samples.draws.get(name));
|
|
616
|
+
const start = chain * samples.nDraws;
|
|
617
|
+
const rows = [header];
|
|
618
|
+
for (let i = 0; i < samples.nDraws; i++) {
|
|
619
|
+
rows.push(columns.map((col) => stanCell(col[start + i] ?? Number.NaN)).join(","));
|
|
620
|
+
}
|
|
621
|
+
return `${rows.join("\n")}
|
|
622
|
+
`;
|
|
623
|
+
}
|
|
624
|
+
function stanCell(value) {
|
|
625
|
+
if (Number.isFinite(value)) return String(value);
|
|
626
|
+
if (Number.isNaN(value)) return "nan";
|
|
627
|
+
return value > 0 ? "inf" : "-inf";
|
|
628
|
+
}
|
|
629
|
+
function matchesTarget(name, targets) {
|
|
630
|
+
return targets.some((t) => name === t || name.startsWith(`${t}[`));
|
|
631
|
+
}
|
|
632
|
+
function filterToTargets(all, targets) {
|
|
633
|
+
const draws = /* @__PURE__ */ new Map();
|
|
634
|
+
for (const name of all.variables) {
|
|
635
|
+
if (matchesTarget(name, targets)) {
|
|
636
|
+
draws.set(name, all.draws.get(name));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
variables: [...draws.keys()],
|
|
641
|
+
nChains: all.nChains,
|
|
642
|
+
nDraws: all.nDraws,
|
|
643
|
+
draws,
|
|
644
|
+
sampleStats: /* @__PURE__ */ new Map()
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function baseNames(variables) {
|
|
648
|
+
return [...new Set(variables.map((v) => v.replace(/\[.*$/, "")))];
|
|
649
|
+
}
|
|
650
|
+
function sha2562(text) {
|
|
651
|
+
return createHash3("sha256").update(text).digest("hex");
|
|
652
|
+
}
|
|
653
|
+
function tmpParent2() {
|
|
654
|
+
const uid = typeof process.getuid === "function" ? `-${process.getuid()}` : "";
|
|
655
|
+
return join5(tmpdir2(), `mcmcjs${uid}`);
|
|
656
|
+
}
|
|
657
|
+
async function runPredict(spec, install, io) {
|
|
658
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
659
|
+
const start = performance.now();
|
|
660
|
+
const runtimeRequested = spec.backend.version;
|
|
661
|
+
const elapsed = () => Math.round(performance.now() - start);
|
|
662
|
+
const fail = (stage, error) => ({
|
|
663
|
+
status: "error",
|
|
664
|
+
runtimeRequested,
|
|
665
|
+
elapsedMs: elapsed(),
|
|
666
|
+
stage,
|
|
667
|
+
error
|
|
668
|
+
});
|
|
669
|
+
const targets = spec.predict?.targets;
|
|
670
|
+
if (!targets || targets.length === 0) {
|
|
671
|
+
return fail("predict", "spec has no [predict] block");
|
|
672
|
+
}
|
|
673
|
+
if (io.signal?.aborted) {
|
|
674
|
+
return { status: "cancelled", runtimeRequested, elapsedMs: 0 };
|
|
675
|
+
}
|
|
676
|
+
let posterior;
|
|
677
|
+
try {
|
|
678
|
+
posterior = parseSamples2(readFileSync4(io.samplesPath, "utf8"));
|
|
679
|
+
} catch (error) {
|
|
680
|
+
return fail("load_samples", `posterior samples did not parse: ${error.message}`);
|
|
681
|
+
}
|
|
682
|
+
let binaryPath;
|
|
683
|
+
try {
|
|
684
|
+
({ binaryPath } = await compileModel(install, spec.modelPath, io.compile));
|
|
685
|
+
} catch (error) {
|
|
686
|
+
return fail("compile", error.message);
|
|
687
|
+
}
|
|
688
|
+
const ownTmp = io.tmpDir === void 0;
|
|
689
|
+
let tmp;
|
|
690
|
+
if (io.tmpDir === void 0) {
|
|
691
|
+
mkdirSync3(tmpParent2(), { recursive: true });
|
|
692
|
+
tmp = mkdtempSync2(join5(tmpParent2(), "stan-predict-"));
|
|
693
|
+
} else {
|
|
694
|
+
tmp = io.tmpDir;
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
const data = predictData(spec);
|
|
698
|
+
const dataPath = join5(tmp, "data.json");
|
|
699
|
+
writeFileSync3(dataPath, JSON.stringify(data));
|
|
700
|
+
const spawn2 = io.spawn ?? createStanSpawn();
|
|
701
|
+
const chains = posterior.nChains;
|
|
702
|
+
const results = await Promise.all(
|
|
703
|
+
Array.from({ length: chains }, (_, i) => {
|
|
704
|
+
const fittedPath = join5(tmp, `fitted_${i + 1}.csv`);
|
|
705
|
+
writeFileSync3(fittedPath, fittedParamsCsv(posterior, i));
|
|
706
|
+
return spawn2(
|
|
707
|
+
binaryPath,
|
|
708
|
+
[
|
|
709
|
+
"generate_quantities",
|
|
710
|
+
`fitted_params=${fittedPath}`,
|
|
711
|
+
"data",
|
|
712
|
+
`file=${dataPath}`,
|
|
713
|
+
"random",
|
|
714
|
+
`seed=${spec.seed}`,
|
|
715
|
+
`id=${i + 1}`,
|
|
716
|
+
"output",
|
|
717
|
+
`file=${join5(tmp, `gq_${i + 1}.csv`)}`
|
|
718
|
+
],
|
|
719
|
+
{ signal: io.signal }
|
|
720
|
+
);
|
|
721
|
+
})
|
|
722
|
+
);
|
|
723
|
+
if (io.signal?.aborted || results.some((r) => r.cancelled)) {
|
|
724
|
+
return { status: "cancelled", runtimeRequested, elapsedMs: elapsed() };
|
|
725
|
+
}
|
|
726
|
+
const failed = results.findIndex((r) => r.code !== 0);
|
|
727
|
+
if (failed >= 0) {
|
|
728
|
+
const stderr = results[failed]?.stderr ?? "";
|
|
729
|
+
if (/doesn't generate any quantities/i.test(stderr)) {
|
|
730
|
+
return fail(
|
|
731
|
+
"predict",
|
|
732
|
+
`the Stan model has no generated quantities block; add one that computes ${targets.join(", ")}`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
const tail = stderr.trim().split("\n").filter(Boolean).at(-1);
|
|
736
|
+
return fail(
|
|
737
|
+
"predict",
|
|
738
|
+
`generate_quantities failed for chain ${failed + 1}${tail ? `: ${tail}` : ""}`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
let predictive;
|
|
742
|
+
try {
|
|
743
|
+
const texts = Array.from(
|
|
744
|
+
{ length: chains },
|
|
745
|
+
(_, i) => readFileSync4(join5(tmp, `gq_${i + 1}.csv`), "utf8")
|
|
746
|
+
);
|
|
747
|
+
predictive = filterToTargets(fromStanCSVFiles2(texts), targets);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
return fail(
|
|
750
|
+
"load_samples",
|
|
751
|
+
`could not read generate_quantities output: ${error.message}`
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
if (predictive.variables.length === 0) {
|
|
755
|
+
const available = baseNames(fromStanCSVFilesNames(tmp, chains));
|
|
756
|
+
return fail(
|
|
757
|
+
"predict",
|
|
758
|
+
`no generated quantity matches targets ${targets.join(", ")}; the model generates: ${available.length > 0 ? available.join(", ") : "(nothing)"}`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
const json = JSON.stringify(toMCMCChainsJson2(predictive));
|
|
763
|
+
parseSamples2(json);
|
|
764
|
+
const partial = `${io.outPath}.tmp`;
|
|
765
|
+
writeFileSync3(partial, json);
|
|
766
|
+
renameSync2(partial, io.outPath);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
return fail("write", error.message);
|
|
769
|
+
}
|
|
770
|
+
const record = {
|
|
771
|
+
schema_version: RUN_RECORD_SCHEMA_VERSION2,
|
|
772
|
+
spec_hash: spec.specHash,
|
|
773
|
+
seed: spec.seed,
|
|
774
|
+
backend: { id: spec.backend.id, runtime: spec.backend.runtime },
|
|
775
|
+
runtime: { requested: runtimeRequested, actual: install.version, path: install.home },
|
|
776
|
+
model_sha256: existsSync4(spec.modelPath) ? sha2562(readFileSync4(spec.modelPath, "utf8")) : void 0,
|
|
777
|
+
data_sha256: sha2562(canonicalJson2(data)),
|
|
778
|
+
posterior_samples: io.samplesPath,
|
|
779
|
+
posterior_samples_sha256: sha2562(readFileSync4(io.samplesPath, "utf8")),
|
|
780
|
+
samples_file: io.outPath,
|
|
781
|
+
started_at: startedAt,
|
|
782
|
+
elapsed_ms: elapsed()
|
|
783
|
+
};
|
|
784
|
+
try {
|
|
785
|
+
writeFileSync3(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
|
|
786
|
+
`);
|
|
787
|
+
} catch (error) {
|
|
788
|
+
return fail("write", `could not write the run record: ${error.message}`);
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
status: "ok",
|
|
792
|
+
samplesFile: io.outPath,
|
|
793
|
+
runtimeRequested,
|
|
794
|
+
runtimeActual: install.version,
|
|
795
|
+
elapsedMs: elapsed()
|
|
796
|
+
};
|
|
797
|
+
} finally {
|
|
798
|
+
if (ownTmp) rmSync2(tmp, { recursive: true, force: true });
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function fromStanCSVFilesNames(tmp, chains) {
|
|
802
|
+
try {
|
|
803
|
+
const texts = Array.from(
|
|
804
|
+
{ length: chains },
|
|
805
|
+
(_, i) => readFileSync4(join5(tmp, `gq_${i + 1}.csv`), "utf8")
|
|
806
|
+
);
|
|
807
|
+
return [...fromStanCSVFiles2(texts).variables];
|
|
808
|
+
} catch {
|
|
809
|
+
return [];
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/setup.ts
|
|
814
|
+
import { existsSync as existsSync5 } from "fs";
|
|
815
|
+
import { cpus } from "os";
|
|
816
|
+
import { join as join6 } from "path";
|
|
817
|
+
import { createRunner as createRunner4 } from "@mcmcjs/engine";
|
|
818
|
+
function releaseUrl(version) {
|
|
819
|
+
return `https://github.com/stan-dev/cmdstan/releases/download/v${version}/cmdstan-${version}.tar.gz`;
|
|
820
|
+
}
|
|
821
|
+
function shq(value) {
|
|
822
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
823
|
+
}
|
|
824
|
+
function managedCmdStanHome(version) {
|
|
825
|
+
return join6(managedStanRoot(), `cmdstan-${version}`);
|
|
826
|
+
}
|
|
827
|
+
function planSetup(report, platform, version = PINNED_CMDSTAN_VERSION) {
|
|
828
|
+
const steps = [];
|
|
829
|
+
if (!report.make.found || !report.cxx.found) {
|
|
830
|
+
const missing = [
|
|
831
|
+
...report.make.found ? [] : ["make"],
|
|
832
|
+
...report.cxx.found ? [] : ["a C++ compiler (g++ or clang++)"]
|
|
833
|
+
].join(" and ");
|
|
834
|
+
steps.push({
|
|
835
|
+
tool: "toolchain",
|
|
836
|
+
label: `install ${missing} with your system package manager`,
|
|
837
|
+
command: null
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
if (report.cmdstan.found && report.cmdstan.version === version) return steps;
|
|
841
|
+
const home = managedCmdStanHome(version);
|
|
842
|
+
const posix = platform !== "win32";
|
|
843
|
+
if (!existsSync5(join6(home, "makefile"))) {
|
|
844
|
+
const root = shq(managedStanRoot());
|
|
845
|
+
const staging = shq(join6(managedStanRoot(), `.download-${version}`));
|
|
846
|
+
const finalHome = shq(home);
|
|
847
|
+
const extracted = shq(join6(managedStanRoot(), `.download-${version}`, `cmdstan-${version}`));
|
|
848
|
+
steps.push({
|
|
849
|
+
tool: "cmdstan",
|
|
850
|
+
label: `download CmdStan ${version}`,
|
|
851
|
+
command: posix ? {
|
|
852
|
+
command: "sh",
|
|
853
|
+
args: [
|
|
854
|
+
"-c",
|
|
855
|
+
`rm -rf ${staging} && mkdir -p ${staging} && curl -fsSL ${shq(releaseUrl(version))} | tar -xz -C ${staging} && rm -rf ${finalHome} && mv ${extracted} ${finalHome} && rm -rf ${staging} && mkdir -p ${root}`
|
|
856
|
+
]
|
|
857
|
+
} : null
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
if (!existsSync5(join6(home, "bin", "stansummary"))) {
|
|
861
|
+
steps.push({
|
|
862
|
+
tool: "build",
|
|
863
|
+
label: `build CmdStan ${version} (one-time, a few minutes)`,
|
|
864
|
+
command: posix ? { command: "make", args: ["-C", home, "build", `-j${cpus().length}`] } : null
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
return steps;
|
|
868
|
+
}
|
|
869
|
+
function validateVersion(version) {
|
|
870
|
+
if (!/^\d+\.\d+\.\d+(-rc\d+)?$/.test(version)) {
|
|
871
|
+
throw new Error(
|
|
872
|
+
`invalid CmdStan version: "${version}" (expected e.g. ${PINNED_CMDSTAN_VERSION})`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async function runSetup(options = {}) {
|
|
877
|
+
const {
|
|
878
|
+
runner,
|
|
879
|
+
installer = createRunner4(30 * 6e4),
|
|
880
|
+
platform = process.platform,
|
|
881
|
+
dryRun = false,
|
|
882
|
+
version = PINNED_CMDSTAN_VERSION
|
|
883
|
+
} = options;
|
|
884
|
+
validateVersion(version);
|
|
885
|
+
const before = await runDoctor(runner);
|
|
886
|
+
const plan = planSetup(before, platform, version);
|
|
887
|
+
const strip = ({ install: _install, ...report }) => report;
|
|
888
|
+
if (plan.length === 0) return { ...strip(before), steps: [] };
|
|
889
|
+
if (dryRun) {
|
|
890
|
+
return {
|
|
891
|
+
...strip(before),
|
|
892
|
+
steps: plan.map((step) => ({ ...step, status: step.command ? "skipped" : "unsupported" }))
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const steps = [];
|
|
896
|
+
let failed = false;
|
|
897
|
+
for (const step of plan) {
|
|
898
|
+
if (!step.command) {
|
|
899
|
+
steps.push({ ...step, status: "unsupported" });
|
|
900
|
+
failed = failed || step.tool === "toolchain";
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (failed) {
|
|
904
|
+
steps.push({ ...step, status: "skipped" });
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
try {
|
|
908
|
+
await installer(step.command.command, step.command.args);
|
|
909
|
+
steps.push({ ...step, status: "ran" });
|
|
910
|
+
} catch (error) {
|
|
911
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
912
|
+
steps.push({ ...step, status: "failed", detail });
|
|
913
|
+
failed = true;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const after = await runDoctor(runner);
|
|
917
|
+
return { ...strip(after), steps };
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/versions.ts
|
|
921
|
+
import { rmSync as rmSync3 } from "fs";
|
|
922
|
+
import { join as join7 } from "path";
|
|
923
|
+
function listVersions() {
|
|
924
|
+
return listCmdStanInstalls().map((install, index) => ({
|
|
925
|
+
id: install.version,
|
|
926
|
+
version: install.version,
|
|
927
|
+
path: install.home,
|
|
928
|
+
isDefault: index === 0
|
|
929
|
+
}));
|
|
930
|
+
}
|
|
931
|
+
function addVersion(version, options = {}) {
|
|
932
|
+
return runSetup({ ...options, version });
|
|
933
|
+
}
|
|
934
|
+
function removeVersion(version) {
|
|
935
|
+
const install = listCmdStanInstalls().find((i) => i.version === version);
|
|
936
|
+
if (!install) {
|
|
937
|
+
throw new Error(`CmdStan ${version} is not installed`);
|
|
938
|
+
}
|
|
939
|
+
const managedHome = join7(managedStanRoot(), `cmdstan-${version}`);
|
|
940
|
+
if (install.home !== managedHome) {
|
|
941
|
+
throw new Error(
|
|
942
|
+
`CmdStan ${version} at ${install.home} is not managed by mcmcjs; remove it yourself if intended`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
rmSync3(managedHome, { recursive: true, force: true });
|
|
946
|
+
}
|
|
947
|
+
export {
|
|
948
|
+
INSTALLED_CMDSTAN_CHANNEL,
|
|
949
|
+
PINNED_CMDSTAN_VERSION,
|
|
950
|
+
addVersion,
|
|
951
|
+
chainArgs,
|
|
952
|
+
columnToLeaf,
|
|
953
|
+
compileModel,
|
|
954
|
+
createStanCsvTail,
|
|
955
|
+
createStanSpawn,
|
|
956
|
+
fittedParamsCsv,
|
|
957
|
+
isCmdStanHome,
|
|
958
|
+
listCmdStanInstalls,
|
|
959
|
+
listVersions,
|
|
960
|
+
managedCmdStanHome,
|
|
961
|
+
managedStanRoot,
|
|
962
|
+
matchesTarget,
|
|
963
|
+
modelCacheDir,
|
|
964
|
+
parseIterationLine,
|
|
965
|
+
planSetup,
|
|
966
|
+
predictData,
|
|
967
|
+
removeVersion,
|
|
968
|
+
resolveCmdStan,
|
|
969
|
+
runDoctor,
|
|
970
|
+
runFit,
|
|
971
|
+
runMatrix,
|
|
972
|
+
runPredict,
|
|
973
|
+
runSetup,
|
|
974
|
+
stanEngine
|
|
975
|
+
};
|
|
976
|
+
//# sourceMappingURL=index.js.map
|