@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 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
- cell(v) = v === missing ? nothing : Float64(v)
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
- flat_out[i + (p - 1) * nIter + (c - 1) * nIter * n] = arr[i, p, c]
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 == "predict"
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
- # Reconstruction reads the plain BUGSModel; entry may return a gradient wrapper.
572
- base = hasproperty(model, :base_model) ? model.base_model : model
573
- cb = get(request, "stream_draws", false) ?
574
- bugs_draw_streamer(base, draws, Int(get(request, "draw_batch_size", 25))) : nothing
575
- chn = Base.invokelatest(
576
- sample_bugs, model, sampler, warmup, draws, chains, rng; callback = cb,
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/predict.ts
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, import_node_path4.join)(tmp, "request.json");
312
- (0, import_node_fs3.writeFileSync)(
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, import_node_fs3.rmSync)(tmp, { recursive: true, force: true });
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, import_node_fs3.existsSync)(io.outPath)) {
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, import_core2.parseSamples)((0, import_node_fs3.readFileSync)(io.outPath, "utf8"));
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: import_core2.RUN_RECORD_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, import_node_fs3.existsSync)(spec.modelPath) ? sha256((0, import_node_fs3.readFileSync)(spec.modelPath, "utf8")) : void 0,
369
- data_sha256: sha256((0, import_core2.canonicalJson)(data)),
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, import_node_fs3.existsSync)(io.samplesPath) ? sha256((0, import_node_fs3.readFileSync)(io.samplesPath, "utf8")) : void 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, import_node_fs3.writeFileSync)(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
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 import_node_fs4 = require("fs");
474
+ var import_node_fs5 = require("fs");
389
475
  var import_node_os3 = require("os");
390
- var import_node_path5 = require("path");
391
- var import_core3 = require("@mcmcjs/core");
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, import_node_path5.join)((0, import_node_os3.homedir)(), ".local", "share");
429
- const root = (0, import_node_path5.join)(base, "mcmcjs", "julia", "env");
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, import_core3.canonicalJson)(pins)).slice(0, 8)}`;
433
- return (0, import_node_path5.join)(root, leaf);
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, import_node_path5.join)(dir, ".mcmcjs-packages.json");
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, import_core3.canonicalJson)({
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, import_node_fs4.readFileSync)(sentinelPath(dir), "utf8").trim() === expectedSentinel(pins);
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, import_node_fs4.existsSync)((0, import_node_path5.join)(dir, "Project.toml")) && provisioned(dir, pins);
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 === import_core3.DEFAULT_JULIA_CHANNEL && (0, import_node_fs4.existsSync)(pinnedEnvDir());
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, import_node_fs4.mkdirSync)(dir, { recursive: true });
555
+ (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
470
556
  if (usesPinnedEnv(opts?.version, pins)) {
471
- (0, import_node_fs4.copyFileSync)((0, import_node_path5.join)(pinnedEnvDir(), "Project.toml"), (0, import_node_path5.join)(dir, "Project.toml"));
472
- (0, import_node_fs4.copyFileSync)((0, import_node_path5.join)(pinnedEnvDir(), "Manifest.toml"), (0, import_node_path5.join)(dir, "Manifest.toml"));
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, import_node_fs4.writeFileSync)(sentinelPath(dir), expectedSentinel(pins));
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 import_node_path6 = require("path");
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, import_node_path6.join)(io.outDir, `${version}.samples.json`),
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 import_core4 = require("@mcmcjs/core");
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 ${import_core4.DEFAULT_JULIA_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 === import_core4.DEFAULT_JULIA_CHANNEL) return [];
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 ${import_core4.DEFAULT_JULIA_CHANNEL} via juliaup`,
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", import_core4.DEFAULT_JULIA_CHANNEL]
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 ${import_core4.DEFAULT_JULIA_CHANNEL}`,
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 import_node_fs5 = require("fs");
763
+ var import_node_fs6 = require("fs");
678
764
  var import_node_net = require("net");
679
- var import_node_path7 = require("path");
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, import_node_fs5.readFileSync)(pidPath, "utf8").trim());
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, import_node_fs5.rmSync)(socket, { force: true });
709
- (0, import_node_fs5.rmSync)(pidPath, { force: true });
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, import_node_path7.join)(runtime, "mcmcjs") : (0, import_node_path7.join)(sharedTmpParent(), "workers");
714
- (0, import_node_fs5.mkdirSync)(dir, { recursive: true, mode: 448 });
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, import_node_path7.join)(workersDir(), `worker-${sha256(`${command}
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, import_node_fs5.mkdirSync)(lock);
828
+ (0, import_node_fs6.mkdirSync)(lock);
743
829
  return true;
744
830
  } catch {
745
831
  try {
746
- if (Date.now() - (0, import_node_fs5.statSync)(lock).mtimeMs > START_WAIT_MS) {
747
- (0, import_node_fs5.rmdirSync)(lock);
748
- (0, import_node_fs5.mkdirSync)(lock);
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, import_node_fs5.rmdirSync)(`${socketPath}.start`);
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, import_node_fs5.rmSync)(path, { force: true });
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, import_node_fs5.readdirSync)(dir).filter((f) => f.startsWith("worker-") && f.endsWith(".sock"));
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, import_node_path7.join)(dir, name);
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, import_node_fs5.rmSync)(socket, { force: true });
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, import_node_fs5.rmSync)(socket, { force: true });
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,