@mcmcjs/julia 0.9.1 → 0.11.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 +181 -16
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/julia-env/Manifest.toml +194 -118
- package/dist/julia-env/Project.toml +1 -0
- package/package.json +2 -2
package/dist/fitlib.jl
CHANGED
|
@@ -17,6 +17,8 @@ using Pkg
|
|
|
17
17
|
import JuliaBUGS
|
|
18
18
|
import AdvancedHMC
|
|
19
19
|
import ForwardDiff
|
|
20
|
+
# Loading Mooncake activates AbstractPPL's native extension for AutoMooncake.
|
|
21
|
+
import Mooncake
|
|
20
22
|
import FlexiChains
|
|
21
23
|
import DimensionalData
|
|
22
24
|
|
|
@@ -131,6 +133,9 @@ bugs_namedtuple(data) = (; (Symbol(k) => narrow(v) for (k, v) in data)...)
|
|
|
131
133
|
to_missing(x) = x === nothing ? missing : (x isa AbstractVector ? map(to_missing, x) : x)
|
|
132
134
|
predict_namedtuple(data) = (; (Symbol(k) => to_missing(v) for (k, v) in data)...)
|
|
133
135
|
|
|
136
|
+
# A blanked target compiles to a latent node; JuliaBUGS drops scalar missings.
|
|
137
|
+
bugs_predict_namedtuple(data) = (; (Symbol(k) => narrow(to_missing(v)) for (k, v) in data)...)
|
|
138
|
+
|
|
134
139
|
# Rebuild a posterior Chains (latents only) from our MCMCChains-JSON wire object so
|
|
135
140
|
# it can feed Turing.predict. The varname_to_symbol map is required for VarName
|
|
136
141
|
# indexing; target outcome columns are excluded.
|
|
@@ -297,17 +302,164 @@ function vnchain_to_wire(chn)
|
|
|
297
302
|
end
|
|
298
303
|
|
|
299
304
|
# JuliaBUGS compiles the model at runtime, so building and sampling must run in
|
|
300
|
-
# separate latest-world frames; merging them reintroduces a world-age error.
|
|
301
|
-
#
|
|
302
|
-
|
|
305
|
+
# separate latest-world frames; merging them reintroduces a world-age error.
|
|
306
|
+
# Returns a FlexiChain like the Turing path; gen_chains recovers generated
|
|
307
|
+
# quantities (unobserved nodes with no observed descendants) after sampling.
|
|
308
|
+
# With a callback, draws stream (see bugs_draw_streamer).
|
|
309
|
+
function sample_bugs(model, sampler, warmup, draws, chains, rng; callback = nothing)
|
|
310
|
+
kw = callback === nothing ? NamedTuple() : (; callback)
|
|
303
311
|
return Logging.with_logger(JSONProgressLogger()) do
|
|
304
312
|
JuliaBUGS.AbstractMCMC.sample(
|
|
305
313
|
rng, model, sampler, JuliaBUGS.AbstractMCMC.MCMCSerial(), draws, chains;
|
|
306
|
-
chain_type =
|
|
314
|
+
chain_type = FlexiChains.VNChain, n_adapts = warmup, discard_initial = warmup,
|
|
315
|
+
progress = true, kw...,
|
|
307
316
|
)
|
|
308
317
|
end
|
|
309
318
|
end
|
|
310
319
|
|
|
320
|
+
# The JuliaBUGS counterpart to draw_streamer. A JuliaBUGS transition is a raw
|
|
321
|
+
# AdvancedHMC.Transition whose params are an unconstrained vector, so (unlike the
|
|
322
|
+
# Turing path) draws cannot be read off it directly. Per batch we rebuild a small
|
|
323
|
+
# FlexiChain through the same gen_chains + vnchain_to_wire path the final file
|
|
324
|
+
# uses, so the streamed leaf names (parameters, generated quantities, and sampler
|
|
325
|
+
# stats) match the file exactly. Reconstruction runs on a copy of the model so it
|
|
326
|
+
# never perturbs the sampler's state. AbstractMCMC passes the 1-based chain index
|
|
327
|
+
# as chain_number; each chain's batches carry a per-chain monotonic seq.
|
|
328
|
+
# TODO: once the pinned JuliaBUGS carries TuringLang/JuliaBUGS.jl#517 (with
|
|
329
|
+
# AbstractMCMC#212 and AbstractPPL#178), a transition exposes named constrained
|
|
330
|
+
# params (a VarNamedTuple) plus generated quantities, so this collapses to a
|
|
331
|
+
# per-draw flatten like draw_streamer, dropping the deepcopy and reconstruction.
|
|
332
|
+
function bugs_draw_streamer(model, draws_per_chain::Int, batch_size::Int)
|
|
333
|
+
recon = deepcopy(model)
|
|
334
|
+
side_rng = StableRNG(0)
|
|
335
|
+
chain = Ref(-1)
|
|
336
|
+
seq = Ref(0)
|
|
337
|
+
θs = Vector{Vector{Float64}}()
|
|
338
|
+
lps = Float64[]
|
|
339
|
+
stats = Any[]
|
|
340
|
+
function emit(iteration)
|
|
341
|
+
isempty(θs) && return
|
|
342
|
+
stats_names = collect(keys(merge((; lp = lps[1]), stats[1])))
|
|
343
|
+
stats_values = [vcat(lps[i], collect(values(stats[i]))) for i in eachindex(θs)]
|
|
344
|
+
chn = JuliaBUGS.gen_chains(FlexiChains.VNChain, recon, θs, stats_names, stats_values; rng = side_rng)
|
|
345
|
+
wire = vnchain_to_wire(chn)
|
|
346
|
+
nIter, total, _ = wire["size"]
|
|
347
|
+
flat = wire["value_flat"]
|
|
348
|
+
params = wire["parameters"]
|
|
349
|
+
cols = Dict(params[p] => [flat[i + (p - 1) * nIter] for i in 1:nIter] for p in 1:total)
|
|
350
|
+
println(
|
|
351
|
+
PROGRESS_IO[],
|
|
352
|
+
JSON.json(
|
|
353
|
+
Dict(
|
|
354
|
+
"mcmcjs" => "draws",
|
|
355
|
+
"chain" => chain[],
|
|
356
|
+
"seq" => seq[],
|
|
357
|
+
"iteration" => iteration,
|
|
358
|
+
"draws" => cols,
|
|
359
|
+
),
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
flush(PROGRESS_IO[])
|
|
363
|
+
seq[] += 1
|
|
364
|
+
empty!(θs)
|
|
365
|
+
empty!(lps)
|
|
366
|
+
empty!(stats)
|
|
367
|
+
end
|
|
368
|
+
return function (_rng, _model, _sampler, transition, _state, iteration; chain_number = 1, _kwargs...)
|
|
369
|
+
c = Int(chain_number) - 1
|
|
370
|
+
if c != chain[]
|
|
371
|
+
chain[] = c
|
|
372
|
+
seq[] = 0
|
|
373
|
+
empty!(θs)
|
|
374
|
+
empty!(lps)
|
|
375
|
+
empty!(stats)
|
|
376
|
+
end
|
|
377
|
+
push!(θs, copy(transition.z.θ))
|
|
378
|
+
push!(lps, transition.z.ℓπ.value)
|
|
379
|
+
push!(stats, AdvancedHMC.stat(transition))
|
|
380
|
+
if length(θs) >= batch_size || iteration >= draws_per_chain
|
|
381
|
+
emit(iteration)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# JuliaBUGS posterior prediction: condition the (target-blanked) model on the
|
|
387
|
+
# posterior parameter columns, then evaluate!! ancestral-samples the remaining
|
|
388
|
+
# latents once per posterior draw. No sampler or gradient runs.
|
|
389
|
+
function predict_bugs(model, posterior, targets, rng)
|
|
390
|
+
model isa JuliaBUGS.BUGSModelWithGradient && (model = model.base_model)
|
|
391
|
+
APPL = JuliaBUGS.AbstractPPL
|
|
392
|
+
target_syms = Set(Symbol.(targets))
|
|
393
|
+
is_target(vn) = APPL.getsym(vn) in target_syms
|
|
394
|
+
|
|
395
|
+
sz = posterior["size"]
|
|
396
|
+
nIter, nCols, nChains = Int(sz[1]), Int(sz[2]), Int(sz[3])
|
|
397
|
+
flat = posterior["value_flat"]
|
|
398
|
+
col = Dict(n => i for (i, n) in enumerate(posterior["parameters"]))
|
|
399
|
+
posterior_names = Set(posterior["name_map"]["parameters"])
|
|
400
|
+
cell(i, p, c) = begin
|
|
401
|
+
v = flat[i + (p - 1) * nIter + (c - 1) * nIter * nCols]
|
|
402
|
+
v === nothing ? NaN : Float64(v)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Nodes fully covered by posterior columns are conditioned; anything
|
|
406
|
+
# uncovered stays latent and is forward-sampled, like Turing.predict.
|
|
407
|
+
gd = model.graph_evaluation_data
|
|
408
|
+
conditioned = APPL.VarName[]
|
|
409
|
+
restore = Tuple{APPL.VarName,Int}[]
|
|
410
|
+
for (i, vn) in enumerate(gd.sorted_nodes)
|
|
411
|
+
(gd.is_stochastic_vals[i] && !gd.is_observed_vals[i] && !is_target(vn)) || continue
|
|
412
|
+
leaves = collect(APPL.varname_leaves(vn, APPL.getvalue(model.evaluation_env, vn)))
|
|
413
|
+
found = count(l -> string(l) in posterior_names, leaves)
|
|
414
|
+
if found == length(leaves)
|
|
415
|
+
push!(conditioned, vn)
|
|
416
|
+
append!(restore, [(l, col[string(l)]) for l in leaves])
|
|
417
|
+
elseif found > 0
|
|
418
|
+
error("the posterior samples cover $(vn) only partially; refit before predicting")
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
isempty(conditioned) &&
|
|
422
|
+
error("the posterior samples share no parameters with the model; were they produced by fitting this spec?")
|
|
423
|
+
cond = APPL.condition(model, conditioned)
|
|
424
|
+
|
|
425
|
+
any(is_target, cond.graph_evaluation_data.sorted_nodes) ||
|
|
426
|
+
error("no variables predicted; check that [predict].targets name model variables")
|
|
427
|
+
|
|
428
|
+
out_leaves = APPL.VarName[]
|
|
429
|
+
arr = Array{Float64,3}(undef, 0, 0, 0)
|
|
430
|
+
for c in 1:nChains, i in 1:nIter
|
|
431
|
+
env = cond.evaluation_env
|
|
432
|
+
for (leaf, p) in restore
|
|
433
|
+
env = JuliaBUGS.BangBang.setindex!!(env, cell(i, p, c), leaf)
|
|
434
|
+
end
|
|
435
|
+
cond = JuliaBUGS.BangBang.setproperty!!(cond, :evaluation_env, env)
|
|
436
|
+
drawn, _ = APPL.evaluate!!(rng, cond)
|
|
437
|
+
if isempty(out_leaves)
|
|
438
|
+
out_leaves = [
|
|
439
|
+
l for vn in cond.graph_evaluation_data.sorted_nodes if is_target(vn)
|
|
440
|
+
for l in APPL.varname_leaves(vn, APPL.getvalue(drawn, vn))
|
|
441
|
+
]
|
|
442
|
+
arr = Array{Float64,3}(undef, nIter, length(out_leaves), nChains)
|
|
443
|
+
end
|
|
444
|
+
for (k, leaf) in enumerate(out_leaves)
|
|
445
|
+
arr[i, k, c] = Float64(APPL.getvalue(drawn, leaf))
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
pnames = string.(out_leaves)
|
|
450
|
+
n = length(pnames)
|
|
451
|
+
flat_out = Vector{Union{Float64,Nothing}}(undef, nIter * n * nChains)
|
|
452
|
+
for c in 1:nChains, p in 1:n, i in 1:nIter
|
|
453
|
+
flat_out[i + (p - 1) * nIter + (c - 1) * nIter * n] = arr[i, p, c]
|
|
454
|
+
end
|
|
455
|
+
return Dict(
|
|
456
|
+
"size" => [nIter, n, nChains],
|
|
457
|
+
"value_flat" => flat_out,
|
|
458
|
+
"parameters" => pnames,
|
|
459
|
+
"name_map" => Dict("parameters" => pnames, "internals" => String[]),
|
|
460
|
+
)
|
|
461
|
+
end
|
|
462
|
+
|
|
311
463
|
# Build the exact wire object parseMCMCChainsJson consumes: value_flat is indexed
|
|
312
464
|
# i + p*nIter + c*nIter*nParams (column-major over [iteration, parameter, chain]).
|
|
313
465
|
function chains_to_wire(chn)
|
|
@@ -333,7 +485,7 @@ function provenance()
|
|
|
333
485
|
packages = Dict{String,String}()
|
|
334
486
|
for (_, info) in Pkg.dependencies()
|
|
335
487
|
if info.name in
|
|
336
|
-
("Turing", "FlexiChains", "MCMCChains", "DynamicPPL", "AbstractMCMC", "JuliaBUGS", "AdvancedHMC", "ForwardDiff", "ADTypes") &&
|
|
488
|
+
("Turing", "FlexiChains", "MCMCChains", "DynamicPPL", "AbstractMCMC", "JuliaBUGS", "AdvancedHMC", "ForwardDiff", "Mooncake", "ADTypes") &&
|
|
337
489
|
info.version !== nothing
|
|
338
490
|
packages[info.name] = string(info.version)
|
|
339
491
|
end
|
|
@@ -372,15 +524,22 @@ function handle_request(request)
|
|
|
372
524
|
entry = resolve_entry(Main, get(request["model"], "entry", nothing))
|
|
373
525
|
|
|
374
526
|
wire = if mode == "predict"
|
|
375
|
-
backend == "
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
527
|
+
if backend == "juliabugs"
|
|
528
|
+
model = Base.invokelatest(entry, bugs_predict_namedtuple(request["data"]))
|
|
529
|
+
stage = "load_samples"
|
|
530
|
+
posterior = JSON.parsefile(request["samples"])
|
|
531
|
+
stage = "predict"
|
|
532
|
+
Base.invokelatest(predict_bugs, model, posterior, request["predict"]["targets"], rng)
|
|
533
|
+
else
|
|
534
|
+
model = Base.invokelatest(entry, ModelData(predict_namedtuple(request["data"])))
|
|
535
|
+
stage = "load_samples"
|
|
536
|
+
rchn = chains_from_wire(JSON.parsefile(request["samples"]), request["predict"]["targets"])
|
|
537
|
+
stage = "predict"
|
|
538
|
+
pp = Base.invokelatest(predict, rng, model, rchn)
|
|
539
|
+
isempty(names(pp)) &&
|
|
540
|
+
error("no variables predicted; check that [predict].targets name unconditioned outcomes")
|
|
541
|
+
chains_to_wire(pp)
|
|
542
|
+
end
|
|
384
543
|
else
|
|
385
544
|
data = backend == "juliabugs" ? bugs_namedtuple(request["data"]) : ModelData(to_namedtuple(request["data"]))
|
|
386
545
|
sampler, warmup = build_sampler(request["sampler"], backend)
|
|
@@ -389,8 +548,14 @@ function handle_request(request)
|
|
|
389
548
|
stage = "sample"
|
|
390
549
|
if backend == "juliabugs"
|
|
391
550
|
model = Base.invokelatest(entry, data)
|
|
392
|
-
|
|
393
|
-
|
|
551
|
+
# Reconstruction reads the plain BUGSModel; entry may return a gradient wrapper.
|
|
552
|
+
base = hasproperty(model, :base_model) ? model.base_model : model
|
|
553
|
+
cb = get(request, "stream_draws", false) ?
|
|
554
|
+
bugs_draw_streamer(base, draws, Int(get(request, "draw_batch_size", 25))) : nothing
|
|
555
|
+
chn = Base.invokelatest(
|
|
556
|
+
sample_bugs, model, sampler, warmup, draws, chains, rng; callback = cb,
|
|
557
|
+
)
|
|
558
|
+
vnchain_to_wire(chn)
|
|
394
559
|
else
|
|
395
560
|
cb = get(request, "stream_draws", false) ?
|
|
396
561
|
draw_streamer(draws, Int(get(request, "draw_batch_size", 25))) : nothing
|
package/dist/index.cjs
CHANGED
|
@@ -268,6 +268,7 @@ async function runFit(spec, resolved, io) {
|
|
|
268
268
|
({ stdout, stderr, code, cancelled } = await io.spawn(resolved.command, args, {
|
|
269
269
|
onProgress: io.onProgress,
|
|
270
270
|
onDraws: io.onDraws,
|
|
271
|
+
onLog: io.onLog,
|
|
271
272
|
signal: io.signal
|
|
272
273
|
}));
|
|
273
274
|
} finally {
|
|
@@ -397,6 +398,7 @@ var PACKAGES = [
|
|
|
397
398
|
"JuliaBUGS",
|
|
398
399
|
"AdvancedHMC",
|
|
399
400
|
"ForwardDiff",
|
|
401
|
+
"Mooncake",
|
|
400
402
|
"JSON",
|
|
401
403
|
"StableRNGs",
|
|
402
404
|
"StatsFuns"
|