@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 +175 -18
- package/dist/index.cjs +141 -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 +130 -39
- package/dist/index.js.map +1 -1
- package/dist/julia-env/Manifest.toml +8 -6
- package/dist/julia-env/Project.toml +1 -0
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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 ?
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
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 == "
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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;
|
|
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
|