@mcmcjs/julia 0.11.3 → 0.13.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
@@ -18,6 +18,8 @@ import AdvancedHMC
18
18
  import ForwardDiff
19
19
  # Loading Mooncake activates AbstractPPL's native extension for AutoMooncake.
20
20
  import Mooncake
21
+ # Loading ReverseDiff activates the DynamicPPL extension for AutoReverseDiff.
22
+ import ReverseDiff
21
23
  import FlexiChains
22
24
  import DimensionalData
23
25
 
@@ -86,13 +88,50 @@ function Logging.handle_message(
86
88
  Logging.handle_message(logger.fallback, level, message, _module, group, id, file, line; kwargs...)
87
89
  end
88
90
 
91
+ function build_adtype(name)
92
+ name == "forwarddiff" && return Turing.ADTypes.AutoForwardDiff()
93
+ name == "reversediff" && return Turing.ADTypes.AutoReverseDiff()
94
+ name == "mooncake" && return Turing.ADTypes.AutoMooncake(; config = nothing)
95
+ error("unsupported adtype: $name")
96
+ end
97
+
89
98
  function build_sampler(sampler, backend)
90
99
  algorithm = get(sampler, "algorithm", "NUTS")
91
- algorithm == "NUTS" || error("unsupported sampler algorithm: $algorithm")
100
+ # Prior draws are iid: no warmup, no adaptation. The JuliaBUGS path samples
101
+ # ancestrally (sample_bugs_prior) instead of going through a sampler object.
102
+ algorithm == "Prior" && return Turing.Prior(), 0
92
103
  warmup = Int(get(sampler, "warmup", 1000))
93
104
  adapt_delta = Float64(get(sampler, "adapt_delta", 0.8))
94
- nuts = backend == "juliabugs" ? AdvancedHMC.NUTS(adapt_delta) : Turing.NUTS(warmup, adapt_delta)
95
- return nuts, warmup
105
+ if backend == "juliabugs"
106
+ algorithm == "NUTS" || error("the juliabugs backend supports NUTS and Prior only")
107
+ return AdvancedHMC.NUTS(adapt_delta), warmup
108
+ end
109
+ adkw = haskey(sampler, "adtype") ? (; adtype = build_adtype(sampler["adtype"])) : (;)
110
+ algorithm == "NUTS" && return Turing.NUTS(warmup, adapt_delta; adkw...), warmup
111
+ algorithm == "HMC" &&
112
+ return Turing.HMC(Float64(sampler["step_size"]), Int(sampler["leapfrog_steps"]); adkw...),
113
+ warmup
114
+ algorithm == "HMCDA" &&
115
+ return Turing.HMCDA(warmup, adapt_delta, Float64(sampler["lambda"]); adkw...), warmup
116
+ algorithm == "MH" && return Turing.MH(), warmup
117
+ error("unsupported sampler algorithm: $algorithm")
118
+ end
119
+
120
+ # Extra keyword arguments for AbstractMCMC.sample, from the request's sampler
121
+ # table: thinning, burn-in for the non-adaptive samplers (NUTS and HMCDA discard
122
+ # their own adaptation), and named starting values replicated across chains.
123
+ function sampling_kwargs(sampler, warmup, chains)
124
+ kw = (;)
125
+ thin = Int(get(sampler, "thin", 1))
126
+ thin > 1 && (kw = merge(kw, (; thinning = thin)))
127
+ algorithm = get(sampler, "algorithm", "NUTS")
128
+ algorithm in ("MH", "HMC") && warmup > 0 && (kw = merge(kw, (; discard_initial = warmup)))
129
+ if haskey(sampler, "initial_params")
130
+ nt = (; (Symbol(k) => narrow(v) for (k, v) in sampler["initial_params"])...)
131
+ strategy = Turing.DynamicPPL.InitFromParams(nt)
132
+ kw = merge(kw, (; initial_params = fill(strategy, chains)))
133
+ end
134
+ return kw
96
135
  end
97
136
 
98
137
  to_namedtuple(data) = (; (Symbol(k) => v for (k, v) in data)...)
@@ -288,9 +327,9 @@ end
288
327
  # the model methods are defined by `include` at runtime, so a plain call from
289
328
  # this (older) world would fail with "method too new". invokelatest fixes that.
290
329
  # Returns Turing's default chain type, a FlexiChain. With a callback, draws stream.
291
- function build_and_sample(entry, data, sampler, draws, chains, rng; callback = nothing)
330
+ function build_and_sample(entry, data, sampler, draws, chains, rng; callback = nothing, extra = (;))
292
331
  model = condition_on_data(entry(data), data)
293
- kw = callback === nothing ? NamedTuple() : (; callback)
332
+ kw = callback === nothing ? extra : merge(extra, (; callback))
294
333
  return Logging.with_logger(JSONProgressLogger()) do
295
334
  sample(rng, model, sampler, MCMCSerial(), draws, chains; progress = true, kw...)
296
335
  end
@@ -310,7 +349,9 @@ function vnchain_to_wire(chn)
310
349
  total = nParams + length(extras)
311
350
 
312
351
  flat = Vector{Union{Float64,Nothing}}(undef, nIter * total * nChains)
313
- cell(v) = v === missing ? nothing : Float64(v)
352
+ # JSON has no Inf/NaN; a non-finite draw (e.g. 1/sqrt(tau) under a diffuse
353
+ # prior) becomes null, which the samples parser reads back as NaN.
354
+ cell(v) = v === missing || !isfinite(v) ? nothing : Float64(v)
314
355
  for c in 1:nChains, p in 1:nParams, i in 1:nIter
315
356
  flat[i + (p - 1) * nIter + (c - 1) * nIter * total] = cell(arr[i, c, p])
316
357
  end
@@ -335,8 +376,9 @@ end
335
376
  # Returns a FlexiChain like the Turing path; gen_chains recovers generated
336
377
  # quantities (unobserved nodes with no observed descendants) after sampling.
337
378
  # With a callback, draws stream (see bugs_draw_streamer).
338
- function sample_bugs(model, sampler, warmup, draws, chains, rng; callback = nothing)
339
- kw = callback === nothing ? NamedTuple() : (; callback)
379
+ function sample_bugs(model, sampler, warmup, draws, chains, rng; callback = nothing, thin = 1)
380
+ kw = callback === nothing ? (;) : (; callback)
381
+ thin > 1 && (kw = merge(kw, (; thinning = thin)))
340
382
  return Logging.with_logger(JSONProgressLogger()) do
341
383
  JuliaBUGS.AbstractMCMC.sample(
342
384
  rng, model, sampler, JuliaBUGS.AbstractMCMC.MCMCSerial(), draws, chains;
@@ -346,6 +388,31 @@ function sample_bugs(model, sampler, warmup, draws, chains, rng; callback = noth
346
388
  end
347
389
  end
348
390
 
391
+ # JuliaBUGS prior sampling: each draw is one ancestral pass (evaluate!! with an
392
+ # rng samples every unobserved stochastic node from its prior and recomputes the
393
+ # deterministic ones; observed data stays fixed). The collected variables are the
394
+ # model parameters plus generated quantities, the same columns a posterior fit
395
+ # produces through gen_chains.
396
+ function sample_bugs_prior(model, draws, chains, rng)
397
+ model isa JuliaBUGS.BUGSModelWithGradient && (model = model.base_model)
398
+ APPL = JuliaBUGS.AbstractPPL
399
+ vars = vcat(JuliaBUGS.model_parameters(model), JuliaBUGS.generated_quantities(model))
400
+ isempty(vars) && error("the model has no parameters or generated quantities to sample")
401
+ grab(env, vn) = (v = APPL.getvalue(env, vn); v isa AbstractArray ? copy(v) : v)
402
+ columns = Dict(vn => Vector{Any}(undef, draws * chains) for vn in vars)
403
+ for c in 1:chains, i in 1:draws
404
+ env, _ = APPL.evaluate!!(rng, model)
405
+ for vn in vars
406
+ columns[vn][i + (c - 1) * draws] = grab(env, vn)
407
+ end
408
+ end
409
+ dict = Dict{FlexiChains.ParameterOrExtra{<:VarName},Array}(
410
+ FlexiChains.Parameter(vn) =>
411
+ [columns[vn][i + (c - 1) * draws] for i in 1:draws, c in 1:chains] for vn in vars
412
+ )
413
+ return FlexiChains.FlexiChain{VarName}(draws, chains, dict)
414
+ end
415
+
349
416
  # The JuliaBUGS counterpart to draw_streamer. A JuliaBUGS transition is a raw
350
417
  # AdvancedHMC.Transition whose params are an unconstrained vector, so (unlike the
351
418
  # Turing path) draws cannot be read off it directly. Per batch we rebuild a small
@@ -412,6 +479,69 @@ function bugs_draw_streamer(model, draws_per_chain::Int, batch_size::Int)
412
479
  end
413
480
  end
414
481
 
482
+ # JuliaBUGS pointwise log-likelihood: restore each posterior draw into the
483
+ # evaluation environment, recompute the deterministic nodes in topological
484
+ # order, and record logpdf per observed node. Every unobserved stochastic node
485
+ # must be covered by the posterior columns, or log p(y | theta) would not be a
486
+ # function of the draw.
487
+ function loglik_bugs(model, posterior)
488
+ model isa JuliaBUGS.BUGSModelWithGradient && (model = model.base_model)
489
+ APPL = JuliaBUGS.AbstractPPL
490
+ gd = model.graph_evaluation_data
491
+
492
+ sz = posterior["size"]
493
+ nIter, nCols, nChains = Int(sz[1]), Int(sz[2]), Int(sz[3])
494
+ flat = posterior["value_flat"]
495
+ col = Dict(n => i for (i, n) in enumerate(posterior["parameters"]))
496
+ posterior_names = Set(posterior["name_map"]["parameters"])
497
+ cell(i, p, c) = begin
498
+ v = flat[i + (p - 1) * nIter + (c - 1) * nIter * nCols]
499
+ v === nothing ? NaN : Float64(v)
500
+ end
501
+
502
+ restore = Tuple{APPL.VarName,Int}[]
503
+ for vn in JuliaBUGS.model_parameters(model)
504
+ leaves = collect(APPL.varname_leaves(vn, APPL.getvalue(model.evaluation_env, vn)))
505
+ found = count(l -> string(l) in posterior_names, leaves)
506
+ found == length(leaves) ||
507
+ error("the posterior samples do not cover $(vn); were they produced by fitting this spec?")
508
+ append!(restore, [(l, col[string(l)]) for l in leaves])
509
+ end
510
+ observed = [
511
+ (vn, i) for (i, vn) in enumerate(gd.sorted_nodes) if
512
+ gd.is_stochastic_vals[i] && gd.is_observed_vals[i]
513
+ ]
514
+ isempty(observed) && error("the model has no observed variables to compute a log-likelihood for")
515
+
516
+ pnames = string.(first.(observed))
517
+ n = length(pnames)
518
+ flat_out = Vector{Union{Float64,Nothing}}(undef, nIter * n * nChains)
519
+ for c in 1:nChains, i in 1:nIter
520
+ env = model.evaluation_env
521
+ for (leaf, p) in restore
522
+ env = JuliaBUGS.BangBang.setindex!!(env, cell(i, p, c), leaf)
523
+ end
524
+ for (k, node) in enumerate(gd.sorted_nodes)
525
+ if !gd.is_stochastic_vals[k]
526
+ env = JuliaBUGS.BangBang.setindex!!(
527
+ env, gd.node_function_vals[k](env, gd.loop_vars_vals[k]), node,
528
+ )
529
+ end
530
+ end
531
+ for (k, (vn, idx)) in enumerate(observed)
532
+ dist = gd.node_function_vals[idx](env, gd.loop_vars_vals[idx])
533
+ v = logpdf(dist, APPL.getvalue(env, vn))
534
+ flat_out[i + (k - 1) * nIter + (c - 1) * nIter * n] = isfinite(v) ? Float64(v) : nothing
535
+ end
536
+ end
537
+ return Dict(
538
+ "size" => [nIter, n, nChains],
539
+ "value_flat" => flat_out,
540
+ "parameters" => pnames,
541
+ "name_map" => Dict("parameters" => pnames, "internals" => String[]),
542
+ )
543
+ end
544
+
415
545
  # JuliaBUGS posterior prediction: condition the (target-blanked) model on the
416
546
  # posterior parameter columns, then evaluate!! ancestral-samples the remaining
417
547
  # latents once per posterior draw. No sampler or gradient runs.
@@ -479,7 +609,8 @@ function predict_bugs(model, posterior, targets, rng)
479
609
  n = length(pnames)
480
610
  flat_out = Vector{Union{Float64,Nothing}}(undef, nIter * n * nChains)
481
611
  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]
612
+ v = arr[i, p, c]
613
+ flat_out[i + (p - 1) * nIter + (c - 1) * nIter * n] = isfinite(v) ? v : nothing
483
614
  end
484
615
  return Dict(
485
616
  "size" => [nIter, n, nChains],
@@ -543,7 +674,26 @@ function handle_request(request)
543
674
  modelmod = load_model_module(request["model"]["file"])
544
675
  entry = resolve_entry(modelmod, get(request["model"], "entry", nothing))
545
676
 
546
- wire = if mode == "predict"
677
+ wire = if mode == "loglik"
678
+ if backend == "juliabugs"
679
+ model = Base.invokelatest(entry, bugs_namedtuple(request["data"]))
680
+ stage = "load_samples"
681
+ posterior = JSON.parsefile(request["samples"])
682
+ stage = "loglik"
683
+ Base.invokelatest(loglik_bugs, model, posterior)
684
+ else
685
+ data = ModelData(to_namedtuple(request["data"]))
686
+ model = Base.invokelatest(entry, data)
687
+ model = Base.invokelatest(condition_on_data, model, data)
688
+ stage = "load_samples"
689
+ chn = wire_to_vnchain(JSON.parsefile(request["samples"]), String[])
690
+ stage = "loglik"
691
+ pll = Base.invokelatest(Turing.DynamicPPL.pointwise_loglikelihoods, model, chn)
692
+ isempty(FlexiChains.parameters(pll)) &&
693
+ error("the model has no observed variables to compute a log-likelihood for")
694
+ vnchain_to_wire(pll)
695
+ end
696
+ elseif mode == "predict"
547
697
  if backend == "juliabugs"
548
698
  model = Base.invokelatest(entry, bugs_predict_namedtuple(request["data"]))
549
699
  stage = "load_samples"
@@ -568,19 +718,26 @@ function handle_request(request)
568
718
  stage = "sample"
569
719
  if backend == "juliabugs"
570
720
  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
- )
721
+ chn = if get(request["sampler"], "algorithm", "NUTS") == "Prior"
722
+ Base.invokelatest(sample_bugs_prior, model, draws, chains, rng)
723
+ else
724
+ # Reconstruction reads the plain BUGSModel; entry may return a gradient wrapper.
725
+ base = hasproperty(model, :base_model) ? model.base_model : model
726
+ cb = get(request, "stream_draws", false) ?
727
+ bugs_draw_streamer(base, draws, Int(get(request, "draw_batch_size", 25))) : nothing
728
+ Base.invokelatest(
729
+ sample_bugs, model, sampler, warmup, draws, chains, rng;
730
+ callback = cb, thin = Int(get(request["sampler"], "thin", 1)),
731
+ )
732
+ end
578
733
  vnchain_to_wire(chn)
579
734
  else
580
735
  cb = get(request, "stream_draws", false) ?
581
736
  draw_streamer(draws, Int(get(request, "draw_batch_size", 25))) : nothing
737
+ extra = sampling_kwargs(request["sampler"], warmup, chains)
582
738
  chn = Base.invokelatest(
583
- build_and_sample, entry, data, sampler, draws, chains, rng; callback = cb,
739
+ build_and_sample, entry, data, sampler, draws, chains, rng;
740
+ callback = cb, extra,
584
741
  )
585
742
  vnchain_to_wire(chn)
586
743
  end