@mcmcjs/julia 0.2.0 → 0.4.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/driver.jl ADDED
@@ -0,0 +1,222 @@
1
+ # MCMC.js fit driver. Reads a run-request JSON, runs the spec's backend (Turing or
2
+ # JuliaBUGS), and writes the draws as MCMCChains-JSON (the @mcmcjs/core samples
3
+ # artifact). On success a single line of provenance JSON is printed to stdout;
4
+ # failures print one-line JSON to stderr ({error, stage}) and exit nonzero. This is
5
+ # the only place that touches the backend chain types, so a chain-type change is
6
+ # contained here.
7
+ using JSON
8
+ using Random
9
+ using StableRNGs
10
+ using Turing
11
+ using Turing: VarName
12
+ using MCMCChains
13
+ using SHA
14
+ using Pkg
15
+ # Imported (not `using`) so they do not pull `@model` etc. into Main and clash
16
+ # with Turing's exports, which would break Turing model files on include.
17
+ import JuliaBUGS
18
+ import AdvancedHMC
19
+ import ForwardDiff
20
+ import FlexiChains
21
+ import DimensionalData
22
+
23
+ function build_sampler(sampler, backend)
24
+ algorithm = get(sampler, "algorithm", "NUTS")
25
+ algorithm == "NUTS" || error("unsupported sampler algorithm: $algorithm")
26
+ warmup = Int(get(sampler, "warmup", 1000))
27
+ adapt_delta = Float64(get(sampler, "adapt_delta", 0.8))
28
+ nuts = backend == "juliabugs" ? AdvancedHMC.NUTS(adapt_delta) : Turing.NUTS(warmup, adapt_delta)
29
+ return nuts, warmup
30
+ end
31
+
32
+ to_namedtuple(data) = (; (Symbol(k) => v for (k, v) in data)...)
33
+
34
+ # JuliaBUGS requires dense arrays with a concrete numeric eltype, but JSON parses
35
+ # them as Vector{Any} (and nested arrays as vectors of vectors). narrow promotes a
36
+ # numeric array to a concrete eltype and stacks equal-sized nested arrays into a
37
+ # dense array; an empty array becomes Float64[].
38
+ function narrow(v::AbstractVector)
39
+ isempty(v) && return Float64[]
40
+ elems = [narrow(x) for x in v]
41
+ if all(e -> e isa AbstractArray, elems) && allequal(size.(elems))
42
+ return stack(elems; dims = 1)
43
+ end
44
+ return eltype(elems) <: Real && !isconcretetype(eltype(elems)) ? float.(elems) : elems
45
+ end
46
+ narrow(x) = x
47
+ bugs_namedtuple(data) = (; (Symbol(k) => narrow(v) for (k, v) in data)...)
48
+
49
+ # JSON null arrives as `nothing`; map it to `missing` so a blanked outcome becomes
50
+ # a sampling (predictive) statement in the model.
51
+ to_missing(x) = x === nothing ? missing : (x isa AbstractVector ? map(to_missing, x) : x)
52
+ predict_namedtuple(data) = (; (Symbol(k) => to_missing(v) for (k, v) in data)...)
53
+
54
+ # Rebuild a posterior Chains (latents only) from our MCMCChains-JSON wire object so
55
+ # it can feed Turing.predict. The varname_to_symbol map is required for VarName
56
+ # indexing; target outcome columns are excluded.
57
+ function chains_from_wire(wire, targets)
58
+ sz = wire["size"]
59
+ nIter, nParams, nChains = Int(sz[1]), Int(sz[2]), Int(sz[3])
60
+ flat = wire["value_flat"]
61
+ allnames = wire["parameters"]
62
+ is_target(n) = any(t -> n == t || startswith(n, t * "["), targets)
63
+ keep = [n for n in wire["name_map"]["parameters"] if !is_target(n)]
64
+ index = Dict(n => i for (i, n) in enumerate(allnames))
65
+ arr = Array{Float64,3}(undef, nIter, length(keep), nChains)
66
+ for (newp, n) in enumerate(keep)
67
+ p = index[n]
68
+ for c in 1:nChains, i in 1:nIter
69
+ v = flat[i + (p - 1) * nIter + (c - 1) * nIter * nParams]
70
+ arr[i, newp, c] = v === nothing ? NaN : Float64(v)
71
+ end
72
+ end
73
+ syms = Symbol.(keep)
74
+ vmap = Dict{VarName,Symbol}(eval(:(Turing.@varname($(Meta.parse(n))))) => Symbol(n) for n in keep)
75
+ return MCMCChains.Chains(arr, syms, Dict(:parameters => syms); info = (; varname_to_symbol = vmap))
76
+ end
77
+
78
+ # Build the model and sample in one call so both run in the latest world age:
79
+ # the model methods are defined by `include` at runtime, so a plain call from
80
+ # this (older) world would fail with "method too new". invokelatest fixes that.
81
+ # Returns Turing's default chain type, a FlexiChain.
82
+ function build_and_sample(entry, data, sampler, draws, chains, rng)
83
+ model = entry(data)
84
+ return sample(rng, model, sampler, MCMCSerial(), draws, chains; progress = false)
85
+ end
86
+
87
+ # FlexiChains-native wire writer. DimArray(chn) splits array-valued parameters
88
+ # into scalar leaves (theta -> theta[1], theta[2]) in the (iter, chain, param)
89
+ # orientation; Real-valued extras become the internals section, matching what
90
+ # the MCMCChains bridge produced.
91
+ function vnchain_to_wire(chn)
92
+ da = DimensionalData.DimArray(chn)
93
+ pnames = string.(collect(DimensionalData.lookup(da, :param)))
94
+ arr = parent(da)
95
+ nIter, nChains, nParams = size(arr)
96
+
97
+ extras = [e for e in FlexiChains.extras(chn) if eltype(chn[e]) <: Union{Missing,Real}]
98
+ enames = [string(Symbol(e)) for e in extras]
99
+ total = nParams + length(extras)
100
+
101
+ flat = Vector{Union{Float64,Nothing}}(undef, nIter * total * nChains)
102
+ cell(v) = v === missing ? nothing : Float64(v)
103
+ for c in 1:nChains, p in 1:nParams, i in 1:nIter
104
+ flat[i + (p - 1) * nIter + (c - 1) * nIter * total] = cell(arr[i, c, p])
105
+ end
106
+ for (k, e) in enumerate(extras)
107
+ m = chn[e]
108
+ p = nParams + k
109
+ for c in 1:nChains, i in 1:nIter
110
+ flat[i + (p - 1) * nIter + (c - 1) * nIter * total] = cell(m[i, c])
111
+ end
112
+ end
113
+
114
+ return Dict(
115
+ "size" => [nIter, total, nChains],
116
+ "value_flat" => flat,
117
+ "parameters" => vcat(pnames, enames),
118
+ "name_map" => Dict("parameters" => pnames, "internals" => enames),
119
+ )
120
+ end
121
+
122
+ # JuliaBUGS compiles the model at runtime, so building and sampling must run in
123
+ # separate latest-world frames; merging them reintroduces a world-age error. The
124
+ # result is already an MCMCChains.Chains, so it is not re-wrapped.
125
+ function sample_bugs(model, sampler, warmup, draws, chains, rng)
126
+ return JuliaBUGS.AbstractMCMC.sample(
127
+ rng, model, sampler, JuliaBUGS.AbstractMCMC.MCMCSerial(), draws, chains;
128
+ chain_type = MCMCChains.Chains, n_adapts = warmup, discard_initial = warmup, progress = false,
129
+ )
130
+ end
131
+
132
+ # Build the exact wire object parseMCMCChainsJson consumes: value_flat is indexed
133
+ # i + p*nIter + c*nIter*nParams (column-major over [iteration, parameter, chain]).
134
+ function chains_to_wire(chn)
135
+ arr = Array(chn.value)
136
+ nIter, nParams, nChains = size(arr)
137
+ flat = Vector{Union{Float64,Nothing}}(undef, nIter * nParams * nChains)
138
+ for c in 1:nChains, p in 1:nParams, i in 1:nIter
139
+ v = arr[i, p, c]
140
+ flat[i + (p - 1) * nIter + (c - 1) * nIter * nParams] = v === missing ? nothing : Float64(v)
141
+ end
142
+ return Dict(
143
+ "size" => [nIter, nParams, nChains],
144
+ "value_flat" => flat,
145
+ "parameters" => string.(names(chn)),
146
+ "name_map" => Dict(
147
+ "parameters" => string.(get(chn.name_map, :parameters, Symbol[])),
148
+ "internals" => string.(get(chn.name_map, :internals, Symbol[])),
149
+ ),
150
+ )
151
+ end
152
+
153
+ function provenance()
154
+ packages = Dict{String,String}()
155
+ for (_, info) in Pkg.dependencies()
156
+ if info.name in
157
+ ("Turing", "FlexiChains", "MCMCChains", "DynamicPPL", "AbstractMCMC", "JuliaBUGS", "AdvancedHMC", "ForwardDiff", "ADTypes") &&
158
+ info.version !== nothing
159
+ packages[info.name] = string(info.version)
160
+ end
161
+ end
162
+ manifest = joinpath(dirname(Base.active_project()), "Manifest.toml")
163
+ return Dict(
164
+ "julia_version" => string(VERSION),
165
+ "packages" => packages,
166
+ "manifest_sha256" => isfile(manifest) ? bytes2hex(sha256(read(manifest))) : "",
167
+ )
168
+ end
169
+
170
+ function main()
171
+ request = JSON.parsefile(ARGS[1])
172
+ out = request["out"]
173
+ mode = get(request, "mode", "fit")
174
+ backend = get(get(request, "backend", Dict()), "id", "turing")
175
+ stage = "compile"
176
+ try
177
+ Random.seed!(Int(request["seed"]))
178
+ rng = StableRNG(Int(request["seed"]))
179
+ include(abspath(request["model"]["file"]))
180
+ entry = getfield(Main, Symbol(request["model"]["entry"]))
181
+
182
+ wire = if mode == "predict"
183
+ backend == "turing" || error("predict is not yet supported for backend $backend")
184
+ model = Base.invokelatest(entry, predict_namedtuple(request["data"]))
185
+ stage = "load_samples"
186
+ rchn = chains_from_wire(JSON.parsefile(request["samples"]), request["predict"]["targets"])
187
+ stage = "predict"
188
+ pp = Base.invokelatest(predict, rng, model, rchn)
189
+ isempty(names(pp)) &&
190
+ error("no variables predicted; check that [predict].targets name unconditioned outcomes")
191
+ chains_to_wire(pp)
192
+ else
193
+ data = backend == "juliabugs" ? bugs_namedtuple(request["data"]) : to_namedtuple(request["data"])
194
+ sampler, warmup = build_sampler(request["sampler"], backend)
195
+ draws = Int(request["sampler"]["draws"])
196
+ chains = Int(get(request["sampler"], "chains", 4))
197
+ stage = "sample"
198
+ if backend == "juliabugs"
199
+ model = Base.invokelatest(entry, data)
200
+ chn = Base.invokelatest(sample_bugs, model, sampler, warmup, draws, chains, rng)
201
+ chains_to_wire(chn)
202
+ else
203
+ chn = Base.invokelatest(build_and_sample, entry, data, sampler, draws, chains, rng)
204
+ vnchain_to_wire(chn)
205
+ end
206
+ end
207
+
208
+ stage = "write"
209
+ tmp = out * ".tmp"
210
+ open(tmp, "w") do io
211
+ JSON.print(io, wire)
212
+ end
213
+ mv(tmp, out; force = true)
214
+
215
+ println(JSON.json(provenance()))
216
+ catch err
217
+ println(stderr, JSON.json(Dict("error" => sprint(showerror, err), "stage" => stage)))
218
+ exit(1)
219
+ end
220
+ end
221
+
222
+ main()
package/dist/index.cjs CHANGED
@@ -20,31 +20,41 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ addVersion: () => addVersion,
24
+ assertVersionsInstalled: () => assertVersionsInstalled,
23
25
  detectJulia: () => detectJulia,
24
26
  detectJuliaup: () => detectJuliaup,
27
+ ensureProject: () => ensureProject,
28
+ gcVersions: () => gcVersions,
29
+ juliaEngine: () => juliaEngine,
25
30
  juliaupInstallCommand: () => juliaupInstallCommand,
31
+ listVersions: () => listVersions,
32
+ managedProjectDir: () => managedProjectDir,
33
+ managedProjectReady: () => managedProjectReady,
26
34
  planSetup: () => planSetup,
35
+ predictData: () => predictData,
36
+ removeVersion: () => removeVersion,
37
+ resolveVersion: () => resolveVersion,
27
38
  runDoctor: () => runDoctor,
28
- runSetup: () => runSetup
39
+ runFit: () => runFit,
40
+ runMatrix: () => runMatrix,
41
+ runPredict: () => runPredict,
42
+ runSetup: () => runSetup,
43
+ setDefaultVersion: () => setDefaultVersion,
44
+ updateVersion: () => updateVersion
29
45
  });
30
46
  module.exports = __toCommonJS(index_exports);
31
47
 
48
+ // ../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.15_typescript@6.0.3/node_modules/tsup/assets/cjs_shims.js
49
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
50
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
51
+
32
52
  // src/environment.ts
33
- var import_node_child_process = require("child_process");
34
53
  var import_node_os = require("os");
35
54
  var import_node_path = require("path");
36
- var import_node_util = require("util");
37
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
38
- function createRunner(timeoutMs = 1e4) {
39
- return async (command, args) => {
40
- const { stdout } = await execFileAsync(command, args, {
41
- timeout: timeoutMs,
42
- maxBuffer: 16 * 1024 * 1024
43
- });
44
- return stdout;
45
- };
46
- }
47
- var defaultRunner = createRunner();
55
+ var import_engine = require("@mcmcjs/engine");
56
+ var import_engine2 = require("@mcmcjs/engine");
57
+ var defaultRunner = (0, import_engine.createRunner)();
48
58
  function candidates(binary) {
49
59
  return [(0, import_node_path.join)((0, import_node_os.homedir)(), ".juliaup", "bin", binary), binary];
50
60
  }
@@ -72,6 +82,305 @@ async function runDoctor(runner) {
72
82
  return { juliaup, julia, ready: julia.found };
73
83
  }
74
84
 
85
+ // src/engine.ts
86
+ var JULIAUP_URL = "https://github.com/JuliaLang/juliaup";
87
+ var juliaEngine = {
88
+ id: "julia",
89
+ displayName: "Julia",
90
+ capabilities: { setup: true, versions: true, fit: true, predict: true },
91
+ async doctor(ctx) {
92
+ const report = await runDoctor(ctx.run);
93
+ return {
94
+ engineId: "julia",
95
+ ready: report.ready,
96
+ tools: [
97
+ { name: "juliaup", ...report.juliaup },
98
+ { name: "julia", ...report.julia }
99
+ ],
100
+ hint: report.ready ? void 0 : `Julia not found. Install it with juliaup (${JULIAUP_URL}), or run \`mcmc setup\`.`
101
+ };
102
+ }
103
+ };
104
+
105
+ // src/fit.ts
106
+ var import_node_fs = require("fs");
107
+ var import_node_os2 = require("os");
108
+ var import_node_path3 = require("path");
109
+ var import_core = require("@mcmcjs/core");
110
+
111
+ // src/runner-common.ts
112
+ var import_node_crypto = require("crypto");
113
+ var import_node_path2 = require("path");
114
+ var import_node_url = require("url");
115
+ function driverPath() {
116
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl)), "driver.jl");
117
+ }
118
+ function sha256(text) {
119
+ return (0, import_node_crypto.createHash)("sha256").update(text).digest("hex");
120
+ }
121
+ function lastJsonLine(text) {
122
+ for (const line of text.trimEnd().split("\n").reverse()) {
123
+ const trimmed = line.trim();
124
+ if (!trimmed.startsWith("{")) continue;
125
+ try {
126
+ return JSON.parse(trimmed);
127
+ } catch {
128
+ return void 0;
129
+ }
130
+ }
131
+ return void 0;
132
+ }
133
+ var STAGES = /* @__PURE__ */ new Set([
134
+ "compile",
135
+ "sample",
136
+ "load_samples",
137
+ "predict",
138
+ "write",
139
+ "spawn"
140
+ ]);
141
+ function toStage(value) {
142
+ return typeof value === "string" && STAGES.has(value) ? value : void 0;
143
+ }
144
+
145
+ // src/fit.ts
146
+ async function runFit(spec, resolved, io) {
147
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
148
+ const start = performance.now();
149
+ const runtimeRequested = spec.backend.version;
150
+ const tmp = io.tmpDir ?? (0, import_node_fs.mkdtempSync)((0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "mcmcjs-fit-"));
151
+ const requestPath = (0, import_node_path3.join)(tmp, "request.json");
152
+ (0, import_node_fs.writeFileSync)(
153
+ requestPath,
154
+ JSON.stringify({
155
+ schema_version: spec.schema_version,
156
+ backend: { id: spec.backend.id },
157
+ model: { file: spec.modelPath, entry: spec.model.entry },
158
+ data: spec.data,
159
+ sampler: spec.sampler,
160
+ seed: spec.seed,
161
+ out: io.outPath
162
+ })
163
+ );
164
+ const args = [
165
+ ...resolved.args,
166
+ "--startup-file=no",
167
+ `--project=${io.projectDir}`,
168
+ driverPath(),
169
+ requestPath
170
+ ];
171
+ const { stdout, stderr, code } = await io.spawn(resolved.command, args);
172
+ const elapsedMs = Math.round(performance.now() - start);
173
+ if (code !== 0 || !(0, import_node_fs.existsSync)(io.outPath)) {
174
+ const failure = lastJsonLine(stderr);
175
+ const stage = toStage(failure?.stage);
176
+ const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
177
+ return { status: "error", runtimeRequested, elapsedMs, stage, error };
178
+ }
179
+ try {
180
+ (0, import_core.parseSamples)((0, import_node_fs.readFileSync)(io.outPath, "utf8"));
181
+ } catch (error) {
182
+ return {
183
+ status: "error",
184
+ runtimeRequested,
185
+ elapsedMs,
186
+ stage: "write",
187
+ error: `samples file did not parse: ${error.message}`
188
+ };
189
+ }
190
+ const provenance = lastJsonLine(stdout);
191
+ const julia = provenance?.julia_version;
192
+ const record = {
193
+ schema_version: import_core.RUN_RECORD_SCHEMA_VERSION,
194
+ spec_hash: spec.specHash,
195
+ seed: spec.seed,
196
+ backend: { id: spec.backend.id, runtime: spec.backend.runtime },
197
+ runtime: { requested: runtimeRequested, actual: julia, path: resolved.command },
198
+ manifest_sha256: provenance?.manifest_sha256,
199
+ packages: provenance?.packages,
200
+ model_sha256: (0, import_node_fs.existsSync)(spec.modelPath) ? sha256((0, import_node_fs.readFileSync)(spec.modelPath, "utf8")) : void 0,
201
+ data_sha256: sha256((0, import_core.canonicalJson)(spec.data)),
202
+ samples_file: io.outPath,
203
+ started_at: startedAt,
204
+ elapsed_ms: elapsedMs
205
+ };
206
+ (0, import_node_fs.writeFileSync)(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
207
+ `);
208
+ return {
209
+ status: "ok",
210
+ samplesFile: io.outPath,
211
+ runtimeRequested,
212
+ runtimeActual: julia,
213
+ elapsedMs
214
+ };
215
+ }
216
+
217
+ // src/predict.ts
218
+ var import_node_fs2 = require("fs");
219
+ var import_node_os3 = require("os");
220
+ var import_node_path4 = require("path");
221
+ var import_core2 = require("@mcmcjs/core");
222
+ function predictData(spec) {
223
+ const predict = spec.predict;
224
+ if (!predict) throw new Error("spec has no [predict] block");
225
+ const data = { ...spec.data, ...predict.data ?? {} };
226
+ const blank = (value) => Array.isArray(value) ? value.map(blank) : null;
227
+ for (const target of predict.targets) {
228
+ data[target] = blank(data[target]);
229
+ }
230
+ return data;
231
+ }
232
+ async function runPredict(spec, resolved, io) {
233
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
234
+ const start = performance.now();
235
+ const runtimeRequested = spec.backend.version;
236
+ const data = predictData(spec);
237
+ const tmp = io.tmpDir ?? (0, import_node_fs2.mkdtempSync)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "mcmcjs-predict-"));
238
+ const requestPath = (0, import_node_path4.join)(tmp, "request.json");
239
+ (0, import_node_fs2.writeFileSync)(
240
+ requestPath,
241
+ JSON.stringify({
242
+ mode: "predict",
243
+ schema_version: spec.schema_version,
244
+ backend: { id: spec.backend.id },
245
+ model: { file: spec.modelPath, entry: spec.model.entry },
246
+ data,
247
+ predict: { targets: spec.predict?.targets ?? [] },
248
+ samples: io.samplesPath,
249
+ seed: spec.seed,
250
+ out: io.outPath
251
+ })
252
+ );
253
+ const args = [
254
+ ...resolved.args,
255
+ "--startup-file=no",
256
+ `--project=${io.projectDir}`,
257
+ driverPath(),
258
+ requestPath
259
+ ];
260
+ const { stdout, stderr, code } = await io.spawn(resolved.command, args);
261
+ const elapsedMs = Math.round(performance.now() - start);
262
+ if (code !== 0 || !(0, import_node_fs2.existsSync)(io.outPath)) {
263
+ const failure = lastJsonLine(stderr);
264
+ const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
265
+ return { status: "error", runtimeRequested, elapsedMs, stage: toStage(failure?.stage), error };
266
+ }
267
+ try {
268
+ (0, import_core2.parseSamples)((0, import_node_fs2.readFileSync)(io.outPath, "utf8"));
269
+ } catch (error) {
270
+ return {
271
+ status: "error",
272
+ runtimeRequested,
273
+ elapsedMs,
274
+ stage: "write",
275
+ error: `samples file did not parse: ${error.message}`
276
+ };
277
+ }
278
+ const provenance = lastJsonLine(stdout);
279
+ const julia = provenance?.julia_version;
280
+ const record = {
281
+ schema_version: import_core2.RUN_RECORD_SCHEMA_VERSION,
282
+ spec_hash: spec.specHash,
283
+ seed: spec.seed,
284
+ backend: { id: spec.backend.id, runtime: spec.backend.runtime },
285
+ runtime: { requested: runtimeRequested, actual: julia, path: resolved.command },
286
+ manifest_sha256: provenance?.manifest_sha256,
287
+ packages: provenance?.packages,
288
+ model_sha256: (0, import_node_fs2.existsSync)(spec.modelPath) ? sha256((0, import_node_fs2.readFileSync)(spec.modelPath, "utf8")) : void 0,
289
+ data_sha256: sha256((0, import_core2.canonicalJson)(data)),
290
+ posterior_samples: io.samplesPath,
291
+ posterior_samples_sha256: (0, import_node_fs2.existsSync)(io.samplesPath) ? sha256((0, import_node_fs2.readFileSync)(io.samplesPath, "utf8")) : void 0,
292
+ samples_file: io.outPath,
293
+ started_at: startedAt,
294
+ elapsed_ms: elapsedMs
295
+ };
296
+ (0, import_node_fs2.writeFileSync)(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
297
+ `);
298
+ return {
299
+ status: "ok",
300
+ samplesFile: io.outPath,
301
+ runtimeRequested,
302
+ runtimeActual: julia,
303
+ elapsedMs
304
+ };
305
+ }
306
+
307
+ // src/project.ts
308
+ var import_node_fs3 = require("fs");
309
+ var import_node_os4 = require("os");
310
+ var import_node_path5 = require("path");
311
+ var import_engine3 = require("@mcmcjs/engine");
312
+ var PACKAGES = [
313
+ "Turing",
314
+ "FlexiChains",
315
+ "DimensionalData",
316
+ "MCMCChains",
317
+ "JuliaBUGS",
318
+ "AdvancedHMC",
319
+ "ForwardDiff",
320
+ "JSON",
321
+ "StableRNGs"
322
+ ];
323
+ function managedProjectDir() {
324
+ const base = process.env.XDG_DATA_HOME || (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".local", "share");
325
+ return (0, import_node_path5.join)(base, "mcmcjs", "julia", "env");
326
+ }
327
+ function sentinelPath(dir) {
328
+ return (0, import_node_path5.join)(dir, ".mcmcjs-packages.json");
329
+ }
330
+ function provisioned(dir) {
331
+ try {
332
+ return (0, import_node_fs3.readFileSync)(sentinelPath(dir), "utf8").trim() === JSON.stringify([...PACKAGES].sort());
333
+ } catch {
334
+ return false;
335
+ }
336
+ }
337
+ function managedProjectReady(dir = managedProjectDir()) {
338
+ return (0, import_node_fs3.existsSync)((0, import_node_path5.join)(dir, "Project.toml")) && provisioned(dir);
339
+ }
340
+ async function ensureProject(juliaBin, run = (0, import_engine3.createRunner)(30 * 6e4), dir = managedProjectDir()) {
341
+ if (managedProjectReady(dir)) return dir;
342
+ (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
343
+ const code = `using Pkg; Pkg.add(${JSON.stringify(PACKAGES)}); Pkg.precompile()`;
344
+ await run(juliaBin, ["--startup-file=no", `--project=${dir}`, "-e", code]);
345
+ (0, import_node_fs3.writeFileSync)(sentinelPath(dir), JSON.stringify([...PACKAGES].sort()));
346
+ return dir;
347
+ }
348
+
349
+ // src/run-matrix.ts
350
+ var import_node_path6 = require("path");
351
+ async function runMatrix(spec, versions, io) {
352
+ const entries = [];
353
+ for (const version of versions) {
354
+ let entry;
355
+ try {
356
+ const resolved = await io.resolve(version);
357
+ const versioned = { ...spec, backend: { ...spec.backend, version } };
358
+ const result = await runFit(versioned, resolved, {
359
+ spawn: io.spawn,
360
+ projectDir: io.projectDir,
361
+ outPath: (0, import_node_path6.join)(io.outDir, `${version}.samples.json`)
362
+ });
363
+ entry = {
364
+ version,
365
+ status: result.status,
366
+ samplesFile: result.samplesFile,
367
+ runtimeActual: result.runtimeActual,
368
+ elapsedMs: result.elapsedMs,
369
+ stage: result.stage,
370
+ error: result.error
371
+ };
372
+ } catch (error) {
373
+ entry = { version, status: "error", elapsedMs: 0, error: error.message };
374
+ }
375
+ entries.push(entry);
376
+ if (entry.status === "error" && !io.keepGoing) break;
377
+ }
378
+ return {
379
+ entries,
380
+ ok: entries.length === versions.length && entries.every((e) => e.status === "ok")
381
+ };
382
+ }
383
+
75
384
  // src/setup.ts
76
385
  var JULIAUP_INSTALL_URL = "https://install.julialang.org";
77
386
  function juliaupInstallCommand(platform) {
@@ -102,7 +411,7 @@ function planSetup(report, platform) {
102
411
  async function runSetup(options = {}) {
103
412
  const {
104
413
  runner,
105
- installer = createRunner(15 * 6e4),
414
+ installer = (0, import_engine2.createRunner)(15 * 6e4),
106
415
  platform = process.platform,
107
416
  dryRun = false
108
417
  } = options;
@@ -132,13 +441,107 @@ async function runSetup(options = {}) {
132
441
  const after = await runDoctor(runner);
133
442
  return { juliaup: after.juliaup, julia: after.julia, ready: after.ready, steps };
134
443
  }
444
+
445
+ // src/versions.ts
446
+ function parseChannel(value) {
447
+ const c = value;
448
+ if (!c || typeof c.Name !== "string" || typeof c.File !== "string") {
449
+ throw new Error("unexpected `juliaup api getconfig1` channel shape");
450
+ }
451
+ return {
452
+ Name: c.Name,
453
+ File: c.File,
454
+ Args: Array.isArray(c.Args) ? c.Args : void 0,
455
+ Version: typeof c.Version === "string" ? c.Version : void 0,
456
+ Arch: typeof c.Arch === "string" ? c.Arch : void 0
457
+ };
458
+ }
459
+ function parseGetconfig1(raw) {
460
+ let data;
461
+ try {
462
+ data = JSON.parse(raw);
463
+ } catch {
464
+ throw new Error("could not parse `juliaup api getconfig1` output as JSON");
465
+ }
466
+ const obj = data;
467
+ if (!obj || typeof obj !== "object" || !Array.isArray(obj.OtherChannels)) {
468
+ throw new Error("unexpected `juliaup api getconfig1` output");
469
+ }
470
+ return {
471
+ DefaultChannel: obj.DefaultChannel ? parseChannel(obj.DefaultChannel) : null,
472
+ OtherChannels: obj.OtherChannels.map(parseChannel)
473
+ };
474
+ }
475
+ function toVersion(channel, isDefault) {
476
+ return { id: channel.Name, version: channel.Version, path: channel.File || void 0, isDefault };
477
+ }
478
+ async function listVersions(bin, run) {
479
+ const config = parseGetconfig1(await run(bin, ["api", "getconfig1"]));
480
+ const versions = [];
481
+ if (config.DefaultChannel) versions.push(toVersion(config.DefaultChannel, true));
482
+ for (const channel of config.OtherChannels) versions.push(toVersion(channel, false));
483
+ return versions;
484
+ }
485
+ async function addVersion(bin, channel, opts, run) {
486
+ await run(bin, ["add", channel]);
487
+ if (opts.default) await run(bin, ["default", channel]);
488
+ }
489
+ async function removeVersion(bin, channel, run) {
490
+ await run(bin, ["remove", channel]);
491
+ }
492
+ async function setDefaultVersion(bin, channel, run) {
493
+ await run(bin, ["default", channel]);
494
+ }
495
+ async function updateVersion(bin, channel, run) {
496
+ await run(bin, channel ? ["update", channel] : ["update"]);
497
+ }
498
+ async function gcVersions(bin, run) {
499
+ await run(bin, ["gc"]);
500
+ }
501
+ async function resolveVersion(bin, channel, run) {
502
+ const config = parseGetconfig1(await run(bin, ["api", "getconfig1"]));
503
+ const all = config.DefaultChannel ? [config.DefaultChannel, ...config.OtherChannels] : config.OtherChannels;
504
+ const match = all.find((c) => c.Name === channel);
505
+ if (!match?.File) {
506
+ throw new Error(
507
+ `Julia version "${channel}" is not installed. Add it with: mcmc julia version add ${channel}`
508
+ );
509
+ }
510
+ return { command: match.File, args: match.Args ?? [] };
511
+ }
512
+ function assertVersionsInstalled(channels, installed) {
513
+ const have = new Set(installed.map((v) => v.id));
514
+ const missing = channels.filter((c) => !have.has(c));
515
+ if (missing.length > 0) {
516
+ const fixes = missing.map((m) => `mcmc julia version add ${m}`).join("; ");
517
+ throw new Error(
518
+ `Julia version(s) not installed: ${missing.join(", ")}. Install with: ${fixes}`
519
+ );
520
+ }
521
+ }
135
522
  // Annotate the CommonJS export names for ESM import in node:
136
523
  0 && (module.exports = {
524
+ addVersion,
525
+ assertVersionsInstalled,
137
526
  detectJulia,
138
527
  detectJuliaup,
528
+ ensureProject,
529
+ gcVersions,
530
+ juliaEngine,
139
531
  juliaupInstallCommand,
532
+ listVersions,
533
+ managedProjectDir,
534
+ managedProjectReady,
140
535
  planSetup,
536
+ predictData,
537
+ removeVersion,
538
+ resolveVersion,
141
539
  runDoctor,
142
- runSetup
540
+ runFit,
541
+ runMatrix,
542
+ runPredict,
543
+ runSetup,
544
+ setDefaultVersion,
545
+ updateVersion
143
546
  });
144
547
  //# sourceMappingURL=index.cjs.map