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