@mcmcjs/julia 0.11.2 → 0.12.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/fitlib.jl +127 -10
- package/dist/index.cjs +140 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +129 -39
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/fitlib.jl
CHANGED
|
@@ -88,6 +88,9 @@ end
|
|
|
88
88
|
|
|
89
89
|
function build_sampler(sampler, backend)
|
|
90
90
|
algorithm = get(sampler, "algorithm", "NUTS")
|
|
91
|
+
# Prior draws are iid: no warmup, no adaptation. The JuliaBUGS path samples
|
|
92
|
+
# ancestrally (sample_bugs_prior) instead of going through a sampler object.
|
|
93
|
+
algorithm == "Prior" && return Turing.Prior(), 0
|
|
91
94
|
algorithm == "NUTS" || error("unsupported sampler algorithm: $algorithm")
|
|
92
95
|
warmup = Int(get(sampler, "warmup", 1000))
|
|
93
96
|
adapt_delta = Float64(get(sampler, "adapt_delta", 0.8))
|
|
@@ -310,7 +313,9 @@ function vnchain_to_wire(chn)
|
|
|
310
313
|
total = nParams + length(extras)
|
|
311
314
|
|
|
312
315
|
flat = Vector{Union{Float64,Nothing}}(undef, nIter * total * nChains)
|
|
313
|
-
|
|
316
|
+
# JSON has no Inf/NaN; a non-finite draw (e.g. 1/sqrt(tau) under a diffuse
|
|
317
|
+
# prior) becomes null, which the samples parser reads back as NaN.
|
|
318
|
+
cell(v) = v === missing || !isfinite(v) ? nothing : Float64(v)
|
|
314
319
|
for c in 1:nChains, p in 1:nParams, i in 1:nIter
|
|
315
320
|
flat[i + (p - 1) * nIter + (c - 1) * nIter * total] = cell(arr[i, c, p])
|
|
316
321
|
end
|
|
@@ -346,6 +351,31 @@ function sample_bugs(model, sampler, warmup, draws, chains, rng; callback = noth
|
|
|
346
351
|
end
|
|
347
352
|
end
|
|
348
353
|
|
|
354
|
+
# JuliaBUGS prior sampling: each draw is one ancestral pass (evaluate!! with an
|
|
355
|
+
# rng samples every unobserved stochastic node from its prior and recomputes the
|
|
356
|
+
# deterministic ones; observed data stays fixed). The collected variables are the
|
|
357
|
+
# model parameters plus generated quantities, the same columns a posterior fit
|
|
358
|
+
# produces through gen_chains.
|
|
359
|
+
function sample_bugs_prior(model, draws, chains, rng)
|
|
360
|
+
model isa JuliaBUGS.BUGSModelWithGradient && (model = model.base_model)
|
|
361
|
+
APPL = JuliaBUGS.AbstractPPL
|
|
362
|
+
vars = vcat(JuliaBUGS.model_parameters(model), JuliaBUGS.generated_quantities(model))
|
|
363
|
+
isempty(vars) && error("the model has no parameters or generated quantities to sample")
|
|
364
|
+
grab(env, vn) = (v = APPL.getvalue(env, vn); v isa AbstractArray ? copy(v) : v)
|
|
365
|
+
columns = Dict(vn => Vector{Any}(undef, draws * chains) for vn in vars)
|
|
366
|
+
for c in 1:chains, i in 1:draws
|
|
367
|
+
env, _ = APPL.evaluate!!(rng, model)
|
|
368
|
+
for vn in vars
|
|
369
|
+
columns[vn][i + (c - 1) * draws] = grab(env, vn)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
dict = Dict{FlexiChains.ParameterOrExtra{<:VarName},Array}(
|
|
373
|
+
FlexiChains.Parameter(vn) =>
|
|
374
|
+
[columns[vn][i + (c - 1) * draws] for i in 1:draws, c in 1:chains] for vn in vars
|
|
375
|
+
)
|
|
376
|
+
return FlexiChains.FlexiChain{VarName}(draws, chains, dict)
|
|
377
|
+
end
|
|
378
|
+
|
|
349
379
|
# The JuliaBUGS counterpart to draw_streamer. A JuliaBUGS transition is a raw
|
|
350
380
|
# AdvancedHMC.Transition whose params are an unconstrained vector, so (unlike the
|
|
351
381
|
# Turing path) draws cannot be read off it directly. Per batch we rebuild a small
|
|
@@ -412,6 +442,69 @@ function bugs_draw_streamer(model, draws_per_chain::Int, batch_size::Int)
|
|
|
412
442
|
end
|
|
413
443
|
end
|
|
414
444
|
|
|
445
|
+
# JuliaBUGS pointwise log-likelihood: restore each posterior draw into the
|
|
446
|
+
# evaluation environment, recompute the deterministic nodes in topological
|
|
447
|
+
# order, and record logpdf per observed node. Every unobserved stochastic node
|
|
448
|
+
# must be covered by the posterior columns, or log p(y | theta) would not be a
|
|
449
|
+
# function of the draw.
|
|
450
|
+
function loglik_bugs(model, posterior)
|
|
451
|
+
model isa JuliaBUGS.BUGSModelWithGradient && (model = model.base_model)
|
|
452
|
+
APPL = JuliaBUGS.AbstractPPL
|
|
453
|
+
gd = model.graph_evaluation_data
|
|
454
|
+
|
|
455
|
+
sz = posterior["size"]
|
|
456
|
+
nIter, nCols, nChains = Int(sz[1]), Int(sz[2]), Int(sz[3])
|
|
457
|
+
flat = posterior["value_flat"]
|
|
458
|
+
col = Dict(n => i for (i, n) in enumerate(posterior["parameters"]))
|
|
459
|
+
posterior_names = Set(posterior["name_map"]["parameters"])
|
|
460
|
+
cell(i, p, c) = begin
|
|
461
|
+
v = flat[i + (p - 1) * nIter + (c - 1) * nIter * nCols]
|
|
462
|
+
v === nothing ? NaN : Float64(v)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
restore = Tuple{APPL.VarName,Int}[]
|
|
466
|
+
for vn in JuliaBUGS.model_parameters(model)
|
|
467
|
+
leaves = collect(APPL.varname_leaves(vn, APPL.getvalue(model.evaluation_env, vn)))
|
|
468
|
+
found = count(l -> string(l) in posterior_names, leaves)
|
|
469
|
+
found == length(leaves) ||
|
|
470
|
+
error("the posterior samples do not cover $(vn); were they produced by fitting this spec?")
|
|
471
|
+
append!(restore, [(l, col[string(l)]) for l in leaves])
|
|
472
|
+
end
|
|
473
|
+
observed = [
|
|
474
|
+
(vn, i) for (i, vn) in enumerate(gd.sorted_nodes) if
|
|
475
|
+
gd.is_stochastic_vals[i] && gd.is_observed_vals[i]
|
|
476
|
+
]
|
|
477
|
+
isempty(observed) && error("the model has no observed variables to compute a log-likelihood for")
|
|
478
|
+
|
|
479
|
+
pnames = string.(first.(observed))
|
|
480
|
+
n = length(pnames)
|
|
481
|
+
flat_out = Vector{Union{Float64,Nothing}}(undef, nIter * n * nChains)
|
|
482
|
+
for c in 1:nChains, i in 1:nIter
|
|
483
|
+
env = model.evaluation_env
|
|
484
|
+
for (leaf, p) in restore
|
|
485
|
+
env = JuliaBUGS.BangBang.setindex!!(env, cell(i, p, c), leaf)
|
|
486
|
+
end
|
|
487
|
+
for (k, node) in enumerate(gd.sorted_nodes)
|
|
488
|
+
if !gd.is_stochastic_vals[k]
|
|
489
|
+
env = JuliaBUGS.BangBang.setindex!!(
|
|
490
|
+
env, gd.node_function_vals[k](env, gd.loop_vars_vals[k]), node,
|
|
491
|
+
)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
for (k, (vn, idx)) in enumerate(observed)
|
|
495
|
+
dist = gd.node_function_vals[idx](env, gd.loop_vars_vals[idx])
|
|
496
|
+
v = logpdf(dist, APPL.getvalue(env, vn))
|
|
497
|
+
flat_out[i + (k - 1) * nIter + (c - 1) * nIter * n] = isfinite(v) ? Float64(v) : nothing
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
return Dict(
|
|
501
|
+
"size" => [nIter, n, nChains],
|
|
502
|
+
"value_flat" => flat_out,
|
|
503
|
+
"parameters" => pnames,
|
|
504
|
+
"name_map" => Dict("parameters" => pnames, "internals" => String[]),
|
|
505
|
+
)
|
|
506
|
+
end
|
|
507
|
+
|
|
415
508
|
# JuliaBUGS posterior prediction: condition the (target-blanked) model on the
|
|
416
509
|
# posterior parameter columns, then evaluate!! ancestral-samples the remaining
|
|
417
510
|
# latents once per posterior draw. No sampler or gradient runs.
|
|
@@ -479,7 +572,8 @@ function predict_bugs(model, posterior, targets, rng)
|
|
|
479
572
|
n = length(pnames)
|
|
480
573
|
flat_out = Vector{Union{Float64,Nothing}}(undef, nIter * n * nChains)
|
|
481
574
|
for c in 1:nChains, p in 1:n, i in 1:nIter
|
|
482
|
-
|
|
575
|
+
v = arr[i, p, c]
|
|
576
|
+
flat_out[i + (p - 1) * nIter + (c - 1) * nIter * n] = isfinite(v) ? v : nothing
|
|
483
577
|
end
|
|
484
578
|
return Dict(
|
|
485
579
|
"size" => [nIter, n, nChains],
|
|
@@ -543,7 +637,26 @@ function handle_request(request)
|
|
|
543
637
|
modelmod = load_model_module(request["model"]["file"])
|
|
544
638
|
entry = resolve_entry(modelmod, get(request["model"], "entry", nothing))
|
|
545
639
|
|
|
546
|
-
wire = if mode == "
|
|
640
|
+
wire = if mode == "loglik"
|
|
641
|
+
if backend == "juliabugs"
|
|
642
|
+
model = Base.invokelatest(entry, bugs_namedtuple(request["data"]))
|
|
643
|
+
stage = "load_samples"
|
|
644
|
+
posterior = JSON.parsefile(request["samples"])
|
|
645
|
+
stage = "loglik"
|
|
646
|
+
Base.invokelatest(loglik_bugs, model, posterior)
|
|
647
|
+
else
|
|
648
|
+
data = ModelData(to_namedtuple(request["data"]))
|
|
649
|
+
model = Base.invokelatest(entry, data)
|
|
650
|
+
model = Base.invokelatest(condition_on_data, model, data)
|
|
651
|
+
stage = "load_samples"
|
|
652
|
+
chn = wire_to_vnchain(JSON.parsefile(request["samples"]), String[])
|
|
653
|
+
stage = "loglik"
|
|
654
|
+
pll = Base.invokelatest(Turing.DynamicPPL.pointwise_loglikelihoods, model, chn)
|
|
655
|
+
isempty(FlexiChains.parameters(pll)) &&
|
|
656
|
+
error("the model has no observed variables to compute a log-likelihood for")
|
|
657
|
+
vnchain_to_wire(pll)
|
|
658
|
+
end
|
|
659
|
+
elseif mode == "predict"
|
|
547
660
|
if backend == "juliabugs"
|
|
548
661
|
model = Base.invokelatest(entry, bugs_predict_namedtuple(request["data"]))
|
|
549
662
|
stage = "load_samples"
|
|
@@ -568,13 +681,17 @@ function handle_request(request)
|
|
|
568
681
|
stage = "sample"
|
|
569
682
|
if backend == "juliabugs"
|
|
570
683
|
model = Base.invokelatest(entry, data)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
684
|
+
chn = if get(request["sampler"], "algorithm", "NUTS") == "Prior"
|
|
685
|
+
Base.invokelatest(sample_bugs_prior, model, draws, chains, rng)
|
|
686
|
+
else
|
|
687
|
+
# Reconstruction reads the plain BUGSModel; entry may return a gradient wrapper.
|
|
688
|
+
base = hasproperty(model, :base_model) ? model.base_model : model
|
|
689
|
+
cb = get(request, "stream_draws", false) ?
|
|
690
|
+
bugs_draw_streamer(base, draws, Int(get(request, "draw_batch_size", 25))) : nothing
|
|
691
|
+
Base.invokelatest(
|
|
692
|
+
sample_bugs, model, sampler, warmup, draws, chains, rng; callback = cb,
|
|
693
|
+
)
|
|
694
|
+
end
|
|
578
695
|
vnchain_to_wire(chn)
|
|
579
696
|
else
|
|
580
697
|
cb = get(request, "stream_draws", false) ?
|
package/dist/index.cjs
CHANGED
|
@@ -40,6 +40,7 @@ __export(index_exports, {
|
|
|
40
40
|
runDoctor: () => runDoctor,
|
|
41
41
|
runFit: () => runFit,
|
|
42
42
|
runFitAuto: () => runFitAuto,
|
|
43
|
+
runLogLik: () => runLogLik,
|
|
43
44
|
runMatrix: () => runMatrix,
|
|
44
45
|
runPredict: () => runPredict,
|
|
45
46
|
runSetup: () => runSetup,
|
|
@@ -287,10 +288,95 @@ async function runFit(spec, resolved, io) {
|
|
|
287
288
|
return finalizeOkFit(spec, resolved, io, lastJsonLine(stdout), startedAt, elapsedMs);
|
|
288
289
|
}
|
|
289
290
|
|
|
290
|
-
// src/
|
|
291
|
+
// src/loglik.ts
|
|
291
292
|
var import_node_fs3 = require("fs");
|
|
292
293
|
var import_node_path4 = require("path");
|
|
293
294
|
var import_core2 = require("@mcmcjs/core");
|
|
295
|
+
async function runLogLik(spec, resolved, io) {
|
|
296
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
297
|
+
const start = performance.now();
|
|
298
|
+
const runtimeRequested = spec.backend.version;
|
|
299
|
+
const ownTmp = io.tmpDir === void 0;
|
|
300
|
+
const tmp = io.tmpDir ?? makeRequestDir("loglik");
|
|
301
|
+
const requestPath = (0, import_node_path4.join)(tmp, "request.json");
|
|
302
|
+
(0, import_node_fs3.writeFileSync)(
|
|
303
|
+
requestPath,
|
|
304
|
+
JSON.stringify({
|
|
305
|
+
mode: "loglik",
|
|
306
|
+
schema_version: spec.schema_version,
|
|
307
|
+
backend: { id: spec.backend.id },
|
|
308
|
+
model: { file: spec.modelPath, entry: spec.model.entry },
|
|
309
|
+
data: spec.data,
|
|
310
|
+
samples: io.samplesPath,
|
|
311
|
+
seed: spec.seed,
|
|
312
|
+
out: io.outPath
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
const args = [
|
|
316
|
+
...resolved.args,
|
|
317
|
+
"--startup-file=no",
|
|
318
|
+
`--project=${io.projectDir}`,
|
|
319
|
+
driverPath(),
|
|
320
|
+
requestPath
|
|
321
|
+
];
|
|
322
|
+
let stdout;
|
|
323
|
+
let stderr;
|
|
324
|
+
let code;
|
|
325
|
+
try {
|
|
326
|
+
({ stdout, stderr, code } = await io.spawn(resolved.command, args));
|
|
327
|
+
} finally {
|
|
328
|
+
if (ownTmp) (0, import_node_fs3.rmSync)(tmp, { recursive: true, force: true });
|
|
329
|
+
}
|
|
330
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
331
|
+
if (code !== 0 || !(0, import_node_fs3.existsSync)(io.outPath)) {
|
|
332
|
+
const failure = lastJsonLine(stderr);
|
|
333
|
+
const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
|
|
334
|
+
return { status: "error", runtimeRequested, elapsedMs, stage: toStage(failure?.stage), error };
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
(0, import_core2.parseSamples)((0, import_node_fs3.readFileSync)(io.outPath, "utf8"));
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return {
|
|
340
|
+
status: "error",
|
|
341
|
+
runtimeRequested,
|
|
342
|
+
elapsedMs,
|
|
343
|
+
stage: "write",
|
|
344
|
+
error: `log-likelihood file did not parse: ${error.message}`
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const provenance = lastJsonLine(stdout);
|
|
348
|
+
const julia = provenance?.julia_version;
|
|
349
|
+
const record = {
|
|
350
|
+
schema_version: import_core2.RUN_RECORD_SCHEMA_VERSION,
|
|
351
|
+
spec_hash: spec.specHash,
|
|
352
|
+
seed: spec.seed,
|
|
353
|
+
backend: { id: spec.backend.id, runtime: spec.backend.runtime },
|
|
354
|
+
runtime: { requested: runtimeRequested, actual: julia, path: resolved.command },
|
|
355
|
+
manifest_sha256: provenance?.manifest_sha256,
|
|
356
|
+
packages: provenance?.packages,
|
|
357
|
+
model_sha256: (0, import_node_fs3.existsSync)(spec.modelPath) ? sha256((0, import_node_fs3.readFileSync)(spec.modelPath, "utf8")) : void 0,
|
|
358
|
+
data_sha256: sha256((0, import_core2.canonicalJson)(spec.data)),
|
|
359
|
+
posterior_samples: io.samplesPath,
|
|
360
|
+
posterior_samples_sha256: (0, import_node_fs3.existsSync)(io.samplesPath) ? sha256((0, import_node_fs3.readFileSync)(io.samplesPath, "utf8")) : void 0,
|
|
361
|
+
samples_file: io.outPath,
|
|
362
|
+
started_at: startedAt,
|
|
363
|
+
elapsed_ms: elapsedMs
|
|
364
|
+
};
|
|
365
|
+
(0, import_node_fs3.writeFileSync)(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
|
|
366
|
+
`);
|
|
367
|
+
return {
|
|
368
|
+
status: "ok",
|
|
369
|
+
samplesFile: io.outPath,
|
|
370
|
+
runtimeRequested,
|
|
371
|
+
runtimeActual: julia,
|
|
372
|
+
elapsedMs
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/predict.ts
|
|
377
|
+
var import_node_fs4 = require("fs");
|
|
378
|
+
var import_node_path5 = require("path");
|
|
379
|
+
var import_core3 = require("@mcmcjs/core");
|
|
294
380
|
function predictData(spec) {
|
|
295
381
|
const predict = spec.predict;
|
|
296
382
|
if (!predict) throw new Error("spec has no [predict] block");
|
|
@@ -308,8 +394,8 @@ async function runPredict(spec, resolved, io) {
|
|
|
308
394
|
const data = predictData(spec);
|
|
309
395
|
const ownTmp = io.tmpDir === void 0;
|
|
310
396
|
const tmp = io.tmpDir ?? makeRequestDir("predict");
|
|
311
|
-
const requestPath = (0,
|
|
312
|
-
(0,
|
|
397
|
+
const requestPath = (0, import_node_path5.join)(tmp, "request.json");
|
|
398
|
+
(0, import_node_fs4.writeFileSync)(
|
|
313
399
|
requestPath,
|
|
314
400
|
JSON.stringify({
|
|
315
401
|
mode: "predict",
|
|
@@ -336,16 +422,16 @@ async function runPredict(spec, resolved, io) {
|
|
|
336
422
|
try {
|
|
337
423
|
({ stdout, stderr, code } = await io.spawn(resolved.command, args));
|
|
338
424
|
} finally {
|
|
339
|
-
if (ownTmp) (0,
|
|
425
|
+
if (ownTmp) (0, import_node_fs4.rmSync)(tmp, { recursive: true, force: true });
|
|
340
426
|
}
|
|
341
427
|
const elapsedMs = Math.round(performance.now() - start);
|
|
342
|
-
if (code !== 0 || !(0,
|
|
428
|
+
if (code !== 0 || !(0, import_node_fs4.existsSync)(io.outPath)) {
|
|
343
429
|
const failure = lastJsonLine(stderr);
|
|
344
430
|
const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
|
|
345
431
|
return { status: "error", runtimeRequested, elapsedMs, stage: toStage(failure?.stage), error };
|
|
346
432
|
}
|
|
347
433
|
try {
|
|
348
|
-
(0,
|
|
434
|
+
(0, import_core3.parseSamples)((0, import_node_fs4.readFileSync)(io.outPath, "utf8"));
|
|
349
435
|
} catch (error) {
|
|
350
436
|
return {
|
|
351
437
|
status: "error",
|
|
@@ -358,22 +444,22 @@ async function runPredict(spec, resolved, io) {
|
|
|
358
444
|
const provenance = lastJsonLine(stdout);
|
|
359
445
|
const julia = provenance?.julia_version;
|
|
360
446
|
const record = {
|
|
361
|
-
schema_version:
|
|
447
|
+
schema_version: import_core3.RUN_RECORD_SCHEMA_VERSION,
|
|
362
448
|
spec_hash: spec.specHash,
|
|
363
449
|
seed: spec.seed,
|
|
364
450
|
backend: { id: spec.backend.id, runtime: spec.backend.runtime },
|
|
365
451
|
runtime: { requested: runtimeRequested, actual: julia, path: resolved.command },
|
|
366
452
|
manifest_sha256: provenance?.manifest_sha256,
|
|
367
453
|
packages: provenance?.packages,
|
|
368
|
-
model_sha256: (0,
|
|
369
|
-
data_sha256: sha256((0,
|
|
454
|
+
model_sha256: (0, import_node_fs4.existsSync)(spec.modelPath) ? sha256((0, import_node_fs4.readFileSync)(spec.modelPath, "utf8")) : void 0,
|
|
455
|
+
data_sha256: sha256((0, import_core3.canonicalJson)(data)),
|
|
370
456
|
posterior_samples: io.samplesPath,
|
|
371
|
-
posterior_samples_sha256: (0,
|
|
457
|
+
posterior_samples_sha256: (0, import_node_fs4.existsSync)(io.samplesPath) ? sha256((0, import_node_fs4.readFileSync)(io.samplesPath, "utf8")) : void 0,
|
|
372
458
|
samples_file: io.outPath,
|
|
373
459
|
started_at: startedAt,
|
|
374
460
|
elapsed_ms: elapsedMs
|
|
375
461
|
};
|
|
376
|
-
(0,
|
|
462
|
+
(0, import_node_fs4.writeFileSync)(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
|
|
377
463
|
`);
|
|
378
464
|
return {
|
|
379
465
|
status: "ok",
|
|
@@ -385,10 +471,10 @@ async function runPredict(spec, resolved, io) {
|
|
|
385
471
|
}
|
|
386
472
|
|
|
387
473
|
// src/project.ts
|
|
388
|
-
var
|
|
474
|
+
var import_node_fs5 = require("fs");
|
|
389
475
|
var import_node_os3 = require("os");
|
|
390
|
-
var
|
|
391
|
-
var
|
|
476
|
+
var import_node_path6 = require("path");
|
|
477
|
+
var import_core4 = require("@mcmcjs/core");
|
|
392
478
|
var import_engine3 = require("@mcmcjs/engine");
|
|
393
479
|
var PACKAGES = [
|
|
394
480
|
"Turing",
|
|
@@ -425,19 +511,19 @@ function hasPins(pins) {
|
|
|
425
511
|
return pins !== void 0 && Object.keys(pins).length > 0;
|
|
426
512
|
}
|
|
427
513
|
function managedProjectDir(version, pins) {
|
|
428
|
-
const base = process.env.XDG_DATA_HOME || (0,
|
|
429
|
-
const root = (0,
|
|
514
|
+
const base = process.env.XDG_DATA_HOME || (0, import_node_path6.join)((0, import_node_os3.homedir)(), ".local", "share");
|
|
515
|
+
const root = (0, import_node_path6.join)(base, "mcmcjs", "julia", "env");
|
|
430
516
|
if (!version && !hasPins(pins)) return root;
|
|
431
517
|
let leaf = (version ?? "default").replace(/[^A-Za-z0-9._-]/g, "_");
|
|
432
|
-
if (hasPins(pins)) leaf += `-${sha256((0,
|
|
433
|
-
return (0,
|
|
518
|
+
if (hasPins(pins)) leaf += `-${sha256((0, import_core4.canonicalJson)(pins)).slice(0, 8)}`;
|
|
519
|
+
return (0, import_node_path6.join)(root, leaf);
|
|
434
520
|
}
|
|
435
521
|
function sentinelPath(dir) {
|
|
436
|
-
return (0,
|
|
522
|
+
return (0, import_node_path6.join)(dir, ".mcmcjs-packages.json");
|
|
437
523
|
}
|
|
438
524
|
var PROVISION_GENERATION = 1;
|
|
439
525
|
function expectedSentinel(pins) {
|
|
440
|
-
return (0,
|
|
526
|
+
return (0, import_core4.canonicalJson)({
|
|
441
527
|
gen: PROVISION_GENERATION,
|
|
442
528
|
packages: [...PACKAGES].sort(),
|
|
443
529
|
pins: pins ?? {}
|
|
@@ -445,13 +531,13 @@ function expectedSentinel(pins) {
|
|
|
445
531
|
}
|
|
446
532
|
function provisioned(dir, pins) {
|
|
447
533
|
try {
|
|
448
|
-
return (0,
|
|
534
|
+
return (0, import_node_fs5.readFileSync)(sentinelPath(dir), "utf8").trim() === expectedSentinel(pins);
|
|
449
535
|
} catch {
|
|
450
536
|
return false;
|
|
451
537
|
}
|
|
452
538
|
}
|
|
453
539
|
function managedProjectReady(dir = managedProjectDir(), pins) {
|
|
454
|
-
return (0,
|
|
540
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path6.join)(dir, "Project.toml")) && provisioned(dir, pins);
|
|
455
541
|
}
|
|
456
542
|
function packageSpecsCode(pins) {
|
|
457
543
|
const specs = PACKAGES.map((name) => {
|
|
@@ -461,15 +547,15 @@ function packageSpecsCode(pins) {
|
|
|
461
547
|
return `[${specs.join(", ")}]`;
|
|
462
548
|
}
|
|
463
549
|
function usesPinnedEnv(version, pins) {
|
|
464
|
-
return !hasPins(pins) && version ===
|
|
550
|
+
return !hasPins(pins) && version === import_core4.DEFAULT_JULIA_CHANNEL && (0, import_node_fs5.existsSync)(pinnedEnvDir());
|
|
465
551
|
}
|
|
466
552
|
async function ensureProject(juliaBin, run = (0, import_engine3.createRunner)(30 * 6e4), dir = managedProjectDir(), pins, opts) {
|
|
467
553
|
validatePins(pins);
|
|
468
554
|
if (managedProjectReady(dir, pins)) return dir;
|
|
469
|
-
(0,
|
|
555
|
+
(0, import_node_fs5.mkdirSync)(dir, { recursive: true });
|
|
470
556
|
if (usesPinnedEnv(opts?.version, pins)) {
|
|
471
|
-
(0,
|
|
472
|
-
(0,
|
|
557
|
+
(0, import_node_fs5.copyFileSync)((0, import_node_path6.join)(pinnedEnvDir(), "Project.toml"), (0, import_node_path6.join)(dir, "Project.toml"));
|
|
558
|
+
(0, import_node_fs5.copyFileSync)((0, import_node_path6.join)(pinnedEnvDir(), "Manifest.toml"), (0, import_node_path6.join)(dir, "Manifest.toml"));
|
|
473
559
|
await run(juliaBin, [
|
|
474
560
|
"--startup-file=no",
|
|
475
561
|
`--project=${dir}`,
|
|
@@ -480,12 +566,12 @@ async function ensureProject(juliaBin, run = (0, import_engine3.createRunner)(30
|
|
|
480
566
|
const code = `using Pkg; Pkg.add(${packageSpecsCode(pins)}); Pkg.precompile()`;
|
|
481
567
|
await run(juliaBin, ["--startup-file=no", `--project=${dir}`, "-e", code]);
|
|
482
568
|
}
|
|
483
|
-
(0,
|
|
569
|
+
(0, import_node_fs5.writeFileSync)(sentinelPath(dir), expectedSentinel(pins));
|
|
484
570
|
return dir;
|
|
485
571
|
}
|
|
486
572
|
|
|
487
573
|
// src/run-matrix.ts
|
|
488
|
-
var
|
|
574
|
+
var import_node_path7 = require("path");
|
|
489
575
|
async function runMatrix(spec, versions, io) {
|
|
490
576
|
const entries = [];
|
|
491
577
|
for (const version of versions) {
|
|
@@ -497,7 +583,7 @@ async function runMatrix(spec, versions, io) {
|
|
|
497
583
|
const result = await runFit(versioned, resolved, {
|
|
498
584
|
spawn: io.spawn,
|
|
499
585
|
projectDir,
|
|
500
|
-
outPath: (0,
|
|
586
|
+
outPath: (0, import_node_path7.join)(io.outDir, `${version}.samples.json`),
|
|
501
587
|
dataFile: io.dataFile,
|
|
502
588
|
dataSha256: io.dataSha256
|
|
503
589
|
});
|
|
@@ -524,7 +610,7 @@ async function runMatrix(spec, versions, io) {
|
|
|
524
610
|
}
|
|
525
611
|
|
|
526
612
|
// src/setup.ts
|
|
527
|
-
var
|
|
613
|
+
var import_core5 = require("@mcmcjs/core");
|
|
528
614
|
var JULIAUP_INSTALL_URL = "https://install.julialang.org";
|
|
529
615
|
function juliaupInstallCommand(platform) {
|
|
530
616
|
if (platform === "win32") {
|
|
@@ -534,20 +620,20 @@ function juliaupInstallCommand(platform) {
|
|
|
534
620
|
command: "sh",
|
|
535
621
|
args: [
|
|
536
622
|
"-c",
|
|
537
|
-
`curl -fsSL ${JULIAUP_INSTALL_URL} | sh -s -- --yes --default-channel ${
|
|
623
|
+
`curl -fsSL ${JULIAUP_INSTALL_URL} | sh -s -- --yes --default-channel ${import_core5.DEFAULT_JULIA_CHANNEL}`
|
|
538
624
|
]
|
|
539
625
|
};
|
|
540
626
|
}
|
|
541
627
|
function planSetup(report, platform) {
|
|
542
|
-
if (report.julia.found && report.julia.version ===
|
|
628
|
+
if (report.julia.found && report.julia.version === import_core5.DEFAULT_JULIA_CHANNEL) return [];
|
|
543
629
|
if (report.juliaup.found) {
|
|
544
630
|
return [
|
|
545
631
|
{
|
|
546
632
|
tool: "julia",
|
|
547
|
-
label: `install the pinned Julia ${
|
|
633
|
+
label: `install the pinned Julia ${import_core5.DEFAULT_JULIA_CHANNEL} via juliaup`,
|
|
548
634
|
command: {
|
|
549
635
|
command: report.juliaup.path ?? "juliaup",
|
|
550
|
-
args: ["add",
|
|
636
|
+
args: ["add", import_core5.DEFAULT_JULIA_CHANNEL]
|
|
551
637
|
}
|
|
552
638
|
}
|
|
553
639
|
];
|
|
@@ -555,7 +641,7 @@ function planSetup(report, platform) {
|
|
|
555
641
|
return [
|
|
556
642
|
{
|
|
557
643
|
tool: "juliaup",
|
|
558
|
-
label: `install juliaup with the pinned Julia ${
|
|
644
|
+
label: `install juliaup with the pinned Julia ${import_core5.DEFAULT_JULIA_CHANNEL}`,
|
|
559
645
|
command: juliaupInstallCommand(platform)
|
|
560
646
|
}
|
|
561
647
|
];
|
|
@@ -674,9 +760,9 @@ function assertVersionsInstalled(channels, installed) {
|
|
|
674
760
|
|
|
675
761
|
// src/worker.ts
|
|
676
762
|
var import_node_child_process = require("child_process");
|
|
677
|
-
var
|
|
763
|
+
var import_node_fs6 = require("fs");
|
|
678
764
|
var import_node_net = require("net");
|
|
679
|
-
var
|
|
765
|
+
var import_node_path8 = require("path");
|
|
680
766
|
var import_engine4 = require("@mcmcjs/engine");
|
|
681
767
|
var START_WAIT_MS = 18e4;
|
|
682
768
|
var CONNECT_TIMEOUT_MS = 1e3;
|
|
@@ -695,7 +781,7 @@ function workerPidPath(socket) {
|
|
|
695
781
|
function killWorker(socket) {
|
|
696
782
|
const pidPath = workerPidPath(socket);
|
|
697
783
|
try {
|
|
698
|
-
const pid = Number((0,
|
|
784
|
+
const pid = Number((0, import_node_fs6.readFileSync)(pidPath, "utf8").trim());
|
|
699
785
|
if (Number.isInteger(pid) && pid > 0) {
|
|
700
786
|
try {
|
|
701
787
|
process.kill(-pid, "SIGKILL");
|
|
@@ -705,17 +791,17 @@ function killWorker(socket) {
|
|
|
705
791
|
}
|
|
706
792
|
} catch {
|
|
707
793
|
}
|
|
708
|
-
(0,
|
|
709
|
-
(0,
|
|
794
|
+
(0, import_node_fs6.rmSync)(socket, { force: true });
|
|
795
|
+
(0, import_node_fs6.rmSync)(pidPath, { force: true });
|
|
710
796
|
}
|
|
711
797
|
function workersDir() {
|
|
712
798
|
const runtime = process.env.XDG_RUNTIME_DIR;
|
|
713
|
-
const dir = runtime ? (0,
|
|
714
|
-
(0,
|
|
799
|
+
const dir = runtime ? (0, import_node_path8.join)(runtime, "mcmcjs") : (0, import_node_path8.join)(sharedTmpParent(), "workers");
|
|
800
|
+
(0, import_node_fs6.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
715
801
|
return dir;
|
|
716
802
|
}
|
|
717
803
|
function workerSocketPath(command, projectDir) {
|
|
718
|
-
return (0,
|
|
804
|
+
return (0, import_node_path8.join)(workersDir(), `worker-${sha256(`${command}
|
|
719
805
|
${projectDir}`).slice(0, 12)}.sock`);
|
|
720
806
|
}
|
|
721
807
|
function connectOnce(path) {
|
|
@@ -739,13 +825,13 @@ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
739
825
|
function acquireStartLock(socketPath) {
|
|
740
826
|
const lock = `${socketPath}.start`;
|
|
741
827
|
try {
|
|
742
|
-
(0,
|
|
828
|
+
(0, import_node_fs6.mkdirSync)(lock);
|
|
743
829
|
return true;
|
|
744
830
|
} catch {
|
|
745
831
|
try {
|
|
746
|
-
if (Date.now() - (0,
|
|
747
|
-
(0,
|
|
748
|
-
(0,
|
|
832
|
+
if (Date.now() - (0, import_node_fs6.statSync)(lock).mtimeMs > START_WAIT_MS) {
|
|
833
|
+
(0, import_node_fs6.rmdirSync)(lock);
|
|
834
|
+
(0, import_node_fs6.mkdirSync)(lock);
|
|
749
835
|
return true;
|
|
750
836
|
}
|
|
751
837
|
} catch {
|
|
@@ -755,7 +841,7 @@ function acquireStartLock(socketPath) {
|
|
|
755
841
|
}
|
|
756
842
|
function releaseStartLock(socketPath) {
|
|
757
843
|
try {
|
|
758
|
-
(0,
|
|
844
|
+
(0, import_node_fs6.rmdirSync)(`${socketPath}.start`);
|
|
759
845
|
} catch {
|
|
760
846
|
}
|
|
761
847
|
}
|
|
@@ -770,7 +856,7 @@ async function ensureWorker(resolved, projectDir, notify, signal) {
|
|
|
770
856
|
const startedHere = acquireStartLock(path);
|
|
771
857
|
if (startedHere) {
|
|
772
858
|
try {
|
|
773
|
-
(0,
|
|
859
|
+
(0, import_node_fs6.rmSync)(path, { force: true });
|
|
774
860
|
notify?.("starting the Julia worker (first use loads Turing, can take a minute)...");
|
|
775
861
|
const child = (0, import_node_child_process.spawn)(
|
|
776
862
|
resolved.command,
|
|
@@ -962,10 +1048,10 @@ async function runFitAuto(spec, resolved, io) {
|
|
|
962
1048
|
}
|
|
963
1049
|
async function listWorkers() {
|
|
964
1050
|
const dir = workersDir();
|
|
965
|
-
const sockets = (0,
|
|
1051
|
+
const sockets = (0, import_node_fs6.readdirSync)(dir).filter((f) => f.startsWith("worker-") && f.endsWith(".sock"));
|
|
966
1052
|
const statuses = [];
|
|
967
1053
|
for (const name of sockets) {
|
|
968
|
-
const socket = (0,
|
|
1054
|
+
const socket = (0, import_node_path8.join)(dir, name);
|
|
969
1055
|
let alive = false;
|
|
970
1056
|
try {
|
|
971
1057
|
const sock = await connectOnce(socket);
|
|
@@ -982,7 +1068,7 @@ async function stopWorker(socket) {
|
|
|
982
1068
|
try {
|
|
983
1069
|
sock = await connectOnce(socket);
|
|
984
1070
|
} catch {
|
|
985
|
-
(0,
|
|
1071
|
+
(0, import_node_fs6.rmSync)(socket, { force: true });
|
|
986
1072
|
return "stale";
|
|
987
1073
|
}
|
|
988
1074
|
const acked = await new Promise((resolve) => {
|
|
@@ -998,7 +1084,7 @@ async function stopWorker(socket) {
|
|
|
998
1084
|
});
|
|
999
1085
|
sock.destroy();
|
|
1000
1086
|
if (!acked) return "pending";
|
|
1001
|
-
(0,
|
|
1087
|
+
(0, import_node_fs6.rmSync)(socket, { force: true });
|
|
1002
1088
|
return "stopped";
|
|
1003
1089
|
}
|
|
1004
1090
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1023,6 +1109,7 @@ async function stopWorker(socket) {
|
|
|
1023
1109
|
runDoctor,
|
|
1024
1110
|
runFit,
|
|
1025
1111
|
runFitAuto,
|
|
1112
|
+
runLogLik,
|
|
1026
1113
|
runMatrix,
|
|
1027
1114
|
runPredict,
|
|
1028
1115
|
runSetup,
|