@mcmcjs/julia 0.13.0 → 0.14.1
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/index.cjs +159 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +188 -38
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/driver.jl +0 -18
- package/dist/fitlib.jl +0 -757
- package/dist/julia-env/Manifest.toml +0 -1840
- package/dist/julia-env/Project.toml +0 -12
- package/dist/worker.jl +0 -102
package/dist/fitlib.jl
DELETED
|
@@ -1,757 +0,0 @@
|
|
|
1
|
-
# MCMC.js fit library, shared by the one-shot driver (driver.jl) and the
|
|
2
|
-
# persistent worker (worker.jl). handle_request runs one fit or predict request
|
|
3
|
-
# (Turing or JuliaBUGS) and writes the draws as the @mcmcjs/core samples
|
|
4
|
-
# artifact (a flat JSON wire). This is the only place that touches the backend
|
|
5
|
-
# chain type (FlexiChains), so a chain-type change is contained here.
|
|
6
|
-
using JSON
|
|
7
|
-
using Logging
|
|
8
|
-
using Random
|
|
9
|
-
using StableRNGs
|
|
10
|
-
using Turing
|
|
11
|
-
using Turing: VarName
|
|
12
|
-
using SHA
|
|
13
|
-
using Pkg
|
|
14
|
-
# Imported (not `using`) so they do not pull `@model` etc. into Main and clash
|
|
15
|
-
# with Turing's exports, which would break Turing model files on include.
|
|
16
|
-
import JuliaBUGS
|
|
17
|
-
import AdvancedHMC
|
|
18
|
-
import ForwardDiff
|
|
19
|
-
# Loading Mooncake activates AbstractPPL's native extension for AutoMooncake.
|
|
20
|
-
import Mooncake
|
|
21
|
-
# Loading ReverseDiff activates the DynamicPPL extension for AutoReverseDiff.
|
|
22
|
-
import ReverseDiff
|
|
23
|
-
import FlexiChains
|
|
24
|
-
import DimensionalData
|
|
25
|
-
|
|
26
|
-
# AbstractMCMC reports sampling progress through ProgressLogging (reachable via
|
|
27
|
-
# Turing's dependency, no extra package in the managed env).
|
|
28
|
-
const ProgressLogging = Turing.AbstractMCMC.ProgressLogging
|
|
29
|
-
|
|
30
|
-
# Where progress JSON lines go: the driver leaves this on stderr; the worker
|
|
31
|
-
# points it at the client socket for the duration of a request.
|
|
32
|
-
const PROGRESS_IO = Ref{IO}(stderr)
|
|
33
|
-
|
|
34
|
-
# Translates ProgressLogging records into one JSON line per update
|
|
35
|
-
# ({"mcmcjs":"progress",...}). The TypeScript side filters these out of the
|
|
36
|
-
# failure protocol's stderr buffer. Info-and-above records pass through to a
|
|
37
|
-
# plain console logger; Debug records (AdvancedHMC step-size search) are dropped.
|
|
38
|
-
struct JSONProgressLogger <: Logging.AbstractLogger
|
|
39
|
-
fallback::Logging.AbstractLogger
|
|
40
|
-
last::Dict{Int,Float64}
|
|
41
|
-
end
|
|
42
|
-
JSONProgressLogger() =
|
|
43
|
-
JSONProgressLogger(Logging.ConsoleLogger(stderr, Logging.Info), Dict{Int,Float64}())
|
|
44
|
-
|
|
45
|
-
Logging.min_enabled_level(::JSONProgressLogger) = Logging.BelowMinLevel
|
|
46
|
-
Logging.shouldlog(::JSONProgressLogger, args...) = true
|
|
47
|
-
Logging.catch_exceptions(::JSONProgressLogger) = true
|
|
48
|
-
|
|
49
|
-
function emit_progress(logger::JSONProgressLogger, name, fraction, done)
|
|
50
|
-
m = match(r"Chain (\d+) of (\d+)", String(name))
|
|
51
|
-
chain = m === nothing ? 1 : parse(Int, m.captures[1])
|
|
52
|
-
of = m === nothing ? 1 : parse(Int, m.captures[2])
|
|
53
|
-
f = done ? 1.0 : fraction === nothing ? 0.0 : Float64(fraction)
|
|
54
|
-
# AbstractMCMC already steps by 1%; the dedupe guards chattier emitters.
|
|
55
|
-
if done || f - get(logger.last, chain, -1.0) >= 0.01
|
|
56
|
-
logger.last[chain] = f
|
|
57
|
-
println(
|
|
58
|
-
PROGRESS_IO[],
|
|
59
|
-
JSON.json(
|
|
60
|
-
Dict(
|
|
61
|
-
"mcmcjs" => "progress",
|
|
62
|
-
"chain" => chain,
|
|
63
|
-
"of" => of,
|
|
64
|
-
"fraction" => round(f; digits = 4),
|
|
65
|
-
"done" => done,
|
|
66
|
-
),
|
|
67
|
-
),
|
|
68
|
-
)
|
|
69
|
-
flush(PROGRESS_IO[])
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
function Logging.handle_message(
|
|
74
|
-
logger::JSONProgressLogger, level, message, _module, group, id, file, line; kwargs...
|
|
75
|
-
)
|
|
76
|
-
progress = message isa ProgressLogging.ProgressString ? message.progress :
|
|
77
|
-
message isa ProgressLogging.Progress ? message : nothing
|
|
78
|
-
if progress !== nothing
|
|
79
|
-
emit_progress(logger, progress.name, progress.fraction, progress.done)
|
|
80
|
-
return
|
|
81
|
-
end
|
|
82
|
-
if haskey(kwargs, :progress) && (kwargs[:progress] isa Real || kwargs[:progress] == "done")
|
|
83
|
-
value = kwargs[:progress]
|
|
84
|
-
emit_progress(logger, string(message), value == "done" ? nothing : value, value == "done")
|
|
85
|
-
return
|
|
86
|
-
end
|
|
87
|
-
level >= Logging.Info || return
|
|
88
|
-
Logging.handle_message(logger.fallback, level, message, _module, group, id, file, line; kwargs...)
|
|
89
|
-
end
|
|
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
|
-
|
|
98
|
-
function build_sampler(sampler, backend)
|
|
99
|
-
algorithm = get(sampler, "algorithm", "NUTS")
|
|
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
|
|
103
|
-
warmup = Int(get(sampler, "warmup", 1000))
|
|
104
|
-
adapt_delta = Float64(get(sampler, "adapt_delta", 0.8))
|
|
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
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
to_namedtuple(data) = (; (Symbol(k) => v for (k, v) in data)...)
|
|
138
|
-
|
|
139
|
-
# Binds the data so a model can read a variable as a property (`data.y`) or by
|
|
140
|
-
# index (`data["y"]` / `data[:y]`), with `haskey`/`keys` supported, so models
|
|
141
|
-
# written in either idiom run unchanged. The underlying NamedTuple is reached
|
|
142
|
-
# via getfield to avoid colliding with a data variable also named `nt`.
|
|
143
|
-
struct ModelData
|
|
144
|
-
nt::NamedTuple
|
|
145
|
-
end
|
|
146
|
-
Base.getproperty(d::ModelData, k::Symbol) = getproperty(getfield(d, :nt), k)
|
|
147
|
-
Base.getindex(d::ModelData, k::Symbol) = getproperty(getfield(d, :nt), k)
|
|
148
|
-
Base.getindex(d::ModelData, k::AbstractString) = getproperty(getfield(d, :nt), Symbol(k))
|
|
149
|
-
Base.haskey(d::ModelData, k::Symbol) = haskey(getfield(d, :nt), k)
|
|
150
|
-
Base.haskey(d::ModelData, k::AbstractString) = haskey(getfield(d, :nt), Symbol(k))
|
|
151
|
-
Base.keys(d::ModelData) = keys(getfield(d, :nt))
|
|
152
|
-
Base.propertynames(d::ModelData) = propertynames(getfield(d, :nt))
|
|
153
|
-
|
|
154
|
-
# JuliaBUGS requires dense arrays with a concrete numeric eltype, but JSON parses
|
|
155
|
-
# them as Vector{Any} (and nested arrays as vectors of vectors). narrow promotes a
|
|
156
|
-
# numeric array to a concrete eltype and stacks equal-sized nested arrays into a
|
|
157
|
-
# dense array; an empty array becomes Float64[].
|
|
158
|
-
function narrow(v::AbstractVector)
|
|
159
|
-
isempty(v) && return Float64[]
|
|
160
|
-
elems = [narrow(x) for x in v]
|
|
161
|
-
if all(e -> e isa AbstractArray, elems) && allequal(size.(elems))
|
|
162
|
-
return stack(elems; dims = 1)
|
|
163
|
-
end
|
|
164
|
-
return eltype(elems) <: Real && !isconcretetype(eltype(elems)) ? float.(elems) : elems
|
|
165
|
-
end
|
|
166
|
-
narrow(x) = x
|
|
167
|
-
bugs_namedtuple(data) = (; (Symbol(k) => narrow(v) for (k, v) in data)...)
|
|
168
|
-
|
|
169
|
-
# JSON null arrives as `nothing`; map it to `missing` so a blanked outcome becomes
|
|
170
|
-
# a sampling (predictive) statement in the model.
|
|
171
|
-
to_missing(x) = x === nothing ? missing : (x isa AbstractVector ? map(to_missing, x) : x)
|
|
172
|
-
predict_namedtuple(data) = (; (Symbol(k) => to_missing(v) for (k, v) in data)...)
|
|
173
|
-
|
|
174
|
-
# A blanked target compiles to a latent node; JuliaBUGS drops scalar missings.
|
|
175
|
-
bugs_predict_namedtuple(data) = (; (Symbol(k) => narrow(to_missing(v)) for (k, v) in data)...)
|
|
176
|
-
|
|
177
|
-
# Rebuild a posterior VNChain (latents only) from our samples wire so it can feed
|
|
178
|
-
# Turing's predict. Scalar leaves are regrouped into their array-valued VarName
|
|
179
|
-
# (theta[1..8] -> @varname(theta) holding an 8-vector), so predict matches the
|
|
180
|
-
# model's parameters exactly instead of relying on leaf reconstruction. Target
|
|
181
|
-
# outcome columns are excluded.
|
|
182
|
-
function wire_to_vnchain(wire, targets)
|
|
183
|
-
sz = wire["size"]
|
|
184
|
-
nIter, total, nChains = Int(sz[1]), Int(sz[2]), Int(sz[3])
|
|
185
|
-
flat = wire["value_flat"]
|
|
186
|
-
index = Dict(n => i for (i, n) in enumerate(wire["parameters"]))
|
|
187
|
-
is_target(n) = any(t -> n == t || startswith(n, t * "["), targets)
|
|
188
|
-
keep = [n for n in wire["name_map"]["parameters"] if !is_target(n)]
|
|
189
|
-
cell(name, i, c) = begin
|
|
190
|
-
p = index[name]
|
|
191
|
-
v = flat[i + (p - 1) * nIter + (c - 1) * nIter * total]
|
|
192
|
-
v === nothing ? NaN : Float64(v)
|
|
193
|
-
end
|
|
194
|
-
base_of(n) = (b = findfirst('[', n); b === nothing ? n : n[1:prevind(n, b)])
|
|
195
|
-
idx_of(n) = begin
|
|
196
|
-
b = findfirst('[', n)
|
|
197
|
-
b === nothing && return Int[]
|
|
198
|
-
inner = n[nextind(n, b):prevind(n, findlast(']', n))]
|
|
199
|
-
parse.(Int, split(inner, ','))
|
|
200
|
-
end
|
|
201
|
-
groups = Dict{String,Vector{String}}()
|
|
202
|
-
for n in keep
|
|
203
|
-
push!(get!(groups, base_of(n), String[]), n)
|
|
204
|
-
end
|
|
205
|
-
dict = Dict{FlexiChains.ParameterOrExtra{<:VarName},Array}()
|
|
206
|
-
for (base, names) in groups
|
|
207
|
-
vn = eval(:(Turing.@varname($(Meta.parse(base)))))
|
|
208
|
-
if length(names) == 1 && isempty(idx_of(names[1]))
|
|
209
|
-
m = Matrix{Float64}(undef, nIter, nChains)
|
|
210
|
-
for c in 1:nChains, i in 1:nIter
|
|
211
|
-
m[i, c] = cell(names[1], i, c)
|
|
212
|
-
end
|
|
213
|
-
dict[FlexiChains.Parameter(vn)] = m
|
|
214
|
-
else
|
|
215
|
-
idxs = [idx_of(n) for n in names]
|
|
216
|
-
D = length(idxs[1])
|
|
217
|
-
dims = ntuple(d -> maximum(ix[d] for ix in idxs), D)
|
|
218
|
-
m = Matrix{Array{Float64,D}}(undef, nIter, nChains)
|
|
219
|
-
for c in 1:nChains, i in 1:nIter
|
|
220
|
-
a = Array{Float64,D}(undef, dims...)
|
|
221
|
-
for (n, ix) in zip(names, idxs)
|
|
222
|
-
a[ix...] = cell(n, i, c)
|
|
223
|
-
end
|
|
224
|
-
m[i, c] = a
|
|
225
|
-
end
|
|
226
|
-
dict[FlexiChains.Parameter(vn)] = m
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
return FlexiChains.FlexiChain{VarName}(nIter, nChains, dict)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# Expand one sampler transition to canonical scalar leaves (theta -> theta[1], ...),
|
|
233
|
-
# the same names the final samples file uses, so streamed batches reconstruct it.
|
|
234
|
-
# Sampler statistics (acceptance rate, tree depth, ...) ride along under the same
|
|
235
|
-
# names the samples file records as internals, so consumers get per-draw
|
|
236
|
-
# diagnostics without a second channel; Bool stats become 0/1.
|
|
237
|
-
function flatten_draw(transition)
|
|
238
|
-
out = Pair{String,Float64}[]
|
|
239
|
-
for (vn, val) in pairs(transition.params)
|
|
240
|
-
for (leaf, leafval) in Turing.DynamicPPL.varname_and_value_leaves(vn, val)
|
|
241
|
-
push!(out, string(leaf) => Float64(leafval))
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
if hasproperty(transition, :stats)
|
|
245
|
-
for (name, value) in pairs(transition.stats)
|
|
246
|
-
value isa Real && push!(out, string(name) => Float64(value))
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
return out
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
# A per-draw callback that emits {"mcmcjs":"draws",...} batches on PROGRESS_IO as
|
|
253
|
-
# sampling proceeds. AbstractMCMC passes the 1-based chain index as `chain_number`;
|
|
254
|
-
# each batch carries a per-chain monotonic seq, and a chain's batches concatenate
|
|
255
|
-
# (by leaf name) to the final samples file. Assumes the callback is not invoked
|
|
256
|
-
# concurrently across chains (true for the MCMCSerial path build_and_sample uses).
|
|
257
|
-
function draw_streamer(draws_per_chain::Int, batch_size::Int)
|
|
258
|
-
chain = Ref(-1)
|
|
259
|
-
seq = Ref(0)
|
|
260
|
-
names = String[]
|
|
261
|
-
buffer = Dict{String,Vector{Float64}}()
|
|
262
|
-
filled = Ref(0)
|
|
263
|
-
function emit(iteration)
|
|
264
|
-
filled[] == 0 && return
|
|
265
|
-
cols = Dict{String,Vector{Float64}}(n => copy(buffer[n]) for n in names)
|
|
266
|
-
println(
|
|
267
|
-
PROGRESS_IO[],
|
|
268
|
-
JSON.json(
|
|
269
|
-
Dict(
|
|
270
|
-
"mcmcjs" => "draws",
|
|
271
|
-
"chain" => chain[],
|
|
272
|
-
"seq" => seq[],
|
|
273
|
-
"iteration" => iteration,
|
|
274
|
-
"draws" => cols,
|
|
275
|
-
),
|
|
276
|
-
),
|
|
277
|
-
)
|
|
278
|
-
flush(PROGRESS_IO[])
|
|
279
|
-
seq[] += 1
|
|
280
|
-
for n in names
|
|
281
|
-
empty!(buffer[n])
|
|
282
|
-
end
|
|
283
|
-
filled[] = 0
|
|
284
|
-
end
|
|
285
|
-
return function (_rng, _model, _sampler, transition, _state, iteration; chain_number = 1, _kwargs...)
|
|
286
|
-
c = Int(chain_number) - 1
|
|
287
|
-
if c != chain[]
|
|
288
|
-
chain[] = c
|
|
289
|
-
seq[] = 0
|
|
290
|
-
end
|
|
291
|
-
leaves = flatten_draw(transition)
|
|
292
|
-
if isempty(names)
|
|
293
|
-
for (n, _) in leaves
|
|
294
|
-
push!(names, n)
|
|
295
|
-
buffer[n] = Float64[]
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
|
-
for (n, v) in leaves
|
|
299
|
-
push!(get!(buffer, n, Float64[]), v)
|
|
300
|
-
end
|
|
301
|
-
filled[] += 1
|
|
302
|
-
if filled[] >= batch_size || iteration >= draws_per_chain
|
|
303
|
-
emit(iteration)
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
# Turing observes a variable only when its name is a model argument. A dict-reading
|
|
309
|
-
# model (`y = data["y"]; y[i] ~ dist`) would otherwise SAMPLE its outcome instead of
|
|
310
|
-
# observing it, so condition the built model on the data columns to turn each such
|
|
311
|
-
# `~` into an observation. Columns that are already model arguments are skipped: they
|
|
312
|
-
# are observed by construction, and conditioning one that is also an argument errors.
|
|
313
|
-
# Do not probe the model to find latent columns (e.g. via VarInfo): evaluating a
|
|
314
|
-
# dict-reading model assigns prior draws into the array the data is read from,
|
|
315
|
-
# corrupting it before we condition. Predictor columns carry no `~`, so conditioning
|
|
316
|
-
# on them is a harmless no-op.
|
|
317
|
-
function condition_on_data(model, data)
|
|
318
|
-
nt = data isa ModelData ? getfield(data, :nt) : data
|
|
319
|
-
nt isa NamedTuple || return model
|
|
320
|
-
argnames = keys(model.args)
|
|
321
|
-
pairs = [k => getproperty(nt, k) for k in keys(nt) if Base.isidentifier(string(k)) && !(k in argnames)]
|
|
322
|
-
isempty(pairs) && return model
|
|
323
|
-
return Turing.DynamicPPL.condition(model, (; pairs...))
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
# Build the model and sample in one call so both run in the latest world age:
|
|
327
|
-
# the model methods are defined by `include` at runtime, so a plain call from
|
|
328
|
-
# this (older) world would fail with "method too new". invokelatest fixes that.
|
|
329
|
-
# Returns Turing's default chain type, a FlexiChain. With a callback, draws stream.
|
|
330
|
-
function build_and_sample(entry, data, sampler, draws, chains, rng; callback = nothing, extra = (;))
|
|
331
|
-
model = condition_on_data(entry(data), data)
|
|
332
|
-
kw = callback === nothing ? extra : merge(extra, (; callback))
|
|
333
|
-
return Logging.with_logger(JSONProgressLogger()) do
|
|
334
|
-
sample(rng, model, sampler, MCMCSerial(), draws, chains; progress = true, kw...)
|
|
335
|
-
end
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
# FlexiChains-native wire writer. DimArray(chn) splits array-valued parameters
|
|
339
|
-
# into scalar leaves (theta -> theta[1], theta[2]) in the (iter, chain, param)
|
|
340
|
-
# orientation; Real-valued extras become the internals section.
|
|
341
|
-
function vnchain_to_wire(chn)
|
|
342
|
-
da = DimensionalData.DimArray(chn)
|
|
343
|
-
pnames = string.(collect(DimensionalData.lookup(da, :param)))
|
|
344
|
-
arr = parent(da)
|
|
345
|
-
nIter, nChains, nParams = size(arr)
|
|
346
|
-
|
|
347
|
-
extras = [e for e in FlexiChains.extras(chn) if eltype(chn[e]) <: Union{Missing,Real}]
|
|
348
|
-
enames = [string(Symbol(e)) for e in extras]
|
|
349
|
-
total = nParams + length(extras)
|
|
350
|
-
|
|
351
|
-
flat = Vector{Union{Float64,Nothing}}(undef, nIter * total * nChains)
|
|
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)
|
|
355
|
-
for c in 1:nChains, p in 1:nParams, i in 1:nIter
|
|
356
|
-
flat[i + (p - 1) * nIter + (c - 1) * nIter * total] = cell(arr[i, c, p])
|
|
357
|
-
end
|
|
358
|
-
for (k, e) in enumerate(extras)
|
|
359
|
-
m = chn[e]
|
|
360
|
-
p = nParams + k
|
|
361
|
-
for c in 1:nChains, i in 1:nIter
|
|
362
|
-
flat[i + (p - 1) * nIter + (c - 1) * nIter * total] = cell(m[i, c])
|
|
363
|
-
end
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
return Dict(
|
|
367
|
-
"size" => [nIter, total, nChains],
|
|
368
|
-
"value_flat" => flat,
|
|
369
|
-
"parameters" => vcat(pnames, enames),
|
|
370
|
-
"name_map" => Dict("parameters" => pnames, "internals" => enames),
|
|
371
|
-
)
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
# JuliaBUGS compiles the model at runtime, so building and sampling must run in
|
|
375
|
-
# separate latest-world frames; merging them reintroduces a world-age error.
|
|
376
|
-
# Returns a FlexiChain like the Turing path; gen_chains recovers generated
|
|
377
|
-
# quantities (unobserved nodes with no observed descendants) after sampling.
|
|
378
|
-
# With a callback, draws stream (see bugs_draw_streamer).
|
|
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)))
|
|
382
|
-
return Logging.with_logger(JSONProgressLogger()) do
|
|
383
|
-
JuliaBUGS.AbstractMCMC.sample(
|
|
384
|
-
rng, model, sampler, JuliaBUGS.AbstractMCMC.MCMCSerial(), draws, chains;
|
|
385
|
-
chain_type = FlexiChains.VNChain, n_adapts = warmup, discard_initial = warmup,
|
|
386
|
-
progress = true, kw...,
|
|
387
|
-
)
|
|
388
|
-
end
|
|
389
|
-
end
|
|
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
|
-
|
|
416
|
-
# The JuliaBUGS counterpart to draw_streamer. A JuliaBUGS transition is a raw
|
|
417
|
-
# AdvancedHMC.Transition whose params are an unconstrained vector, so (unlike the
|
|
418
|
-
# Turing path) draws cannot be read off it directly. Per batch we rebuild a small
|
|
419
|
-
# FlexiChain through the same gen_chains + vnchain_to_wire path the final file
|
|
420
|
-
# uses, so the streamed leaf names (parameters, generated quantities, and sampler
|
|
421
|
-
# stats) match the file exactly. Reconstruction runs on a copy of the model so it
|
|
422
|
-
# never perturbs the sampler's state. AbstractMCMC passes the 1-based chain index
|
|
423
|
-
# as chain_number; each chain's batches carry a per-chain monotonic seq.
|
|
424
|
-
# TODO: once the pinned JuliaBUGS carries TuringLang/JuliaBUGS.jl#517 (with
|
|
425
|
-
# AbstractMCMC#212 and AbstractPPL#178), a transition exposes named constrained
|
|
426
|
-
# params (a VarNamedTuple) plus generated quantities, so this collapses to a
|
|
427
|
-
# per-draw flatten like draw_streamer, dropping the deepcopy and reconstruction.
|
|
428
|
-
function bugs_draw_streamer(model, draws_per_chain::Int, batch_size::Int)
|
|
429
|
-
recon = deepcopy(model)
|
|
430
|
-
side_rng = StableRNG(0)
|
|
431
|
-
chain = Ref(-1)
|
|
432
|
-
seq = Ref(0)
|
|
433
|
-
θs = Vector{Vector{Float64}}()
|
|
434
|
-
lps = Float64[]
|
|
435
|
-
stats = Any[]
|
|
436
|
-
function emit(iteration)
|
|
437
|
-
isempty(θs) && return
|
|
438
|
-
stats_names = collect(keys(merge((; lp = lps[1]), stats[1])))
|
|
439
|
-
stats_values = [vcat(lps[i], collect(values(stats[i]))) for i in eachindex(θs)]
|
|
440
|
-
chn = JuliaBUGS.gen_chains(FlexiChains.VNChain, recon, θs, stats_names, stats_values; rng = side_rng)
|
|
441
|
-
wire = vnchain_to_wire(chn)
|
|
442
|
-
nIter, total, _ = wire["size"]
|
|
443
|
-
flat = wire["value_flat"]
|
|
444
|
-
params = wire["parameters"]
|
|
445
|
-
cols = Dict(params[p] => [flat[i + (p - 1) * nIter] for i in 1:nIter] for p in 1:total)
|
|
446
|
-
println(
|
|
447
|
-
PROGRESS_IO[],
|
|
448
|
-
JSON.json(
|
|
449
|
-
Dict(
|
|
450
|
-
"mcmcjs" => "draws",
|
|
451
|
-
"chain" => chain[],
|
|
452
|
-
"seq" => seq[],
|
|
453
|
-
"iteration" => iteration,
|
|
454
|
-
"draws" => cols,
|
|
455
|
-
),
|
|
456
|
-
),
|
|
457
|
-
)
|
|
458
|
-
flush(PROGRESS_IO[])
|
|
459
|
-
seq[] += 1
|
|
460
|
-
empty!(θs)
|
|
461
|
-
empty!(lps)
|
|
462
|
-
empty!(stats)
|
|
463
|
-
end
|
|
464
|
-
return function (_rng, _model, _sampler, transition, _state, iteration; chain_number = 1, _kwargs...)
|
|
465
|
-
c = Int(chain_number) - 1
|
|
466
|
-
if c != chain[]
|
|
467
|
-
chain[] = c
|
|
468
|
-
seq[] = 0
|
|
469
|
-
empty!(θs)
|
|
470
|
-
empty!(lps)
|
|
471
|
-
empty!(stats)
|
|
472
|
-
end
|
|
473
|
-
push!(θs, copy(transition.z.θ))
|
|
474
|
-
push!(lps, transition.z.ℓπ.value)
|
|
475
|
-
push!(stats, AdvancedHMC.stat(transition))
|
|
476
|
-
if length(θs) >= batch_size || iteration >= draws_per_chain
|
|
477
|
-
emit(iteration)
|
|
478
|
-
end
|
|
479
|
-
end
|
|
480
|
-
end
|
|
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
|
-
|
|
545
|
-
# JuliaBUGS posterior prediction: condition the (target-blanked) model on the
|
|
546
|
-
# posterior parameter columns, then evaluate!! ancestral-samples the remaining
|
|
547
|
-
# latents once per posterior draw. No sampler or gradient runs.
|
|
548
|
-
function predict_bugs(model, posterior, targets, rng)
|
|
549
|
-
model isa JuliaBUGS.BUGSModelWithGradient && (model = model.base_model)
|
|
550
|
-
APPL = JuliaBUGS.AbstractPPL
|
|
551
|
-
target_syms = Set(Symbol.(targets))
|
|
552
|
-
is_target(vn) = APPL.getsym(vn) in target_syms
|
|
553
|
-
|
|
554
|
-
sz = posterior["size"]
|
|
555
|
-
nIter, nCols, nChains = Int(sz[1]), Int(sz[2]), Int(sz[3])
|
|
556
|
-
flat = posterior["value_flat"]
|
|
557
|
-
col = Dict(n => i for (i, n) in enumerate(posterior["parameters"]))
|
|
558
|
-
posterior_names = Set(posterior["name_map"]["parameters"])
|
|
559
|
-
cell(i, p, c) = begin
|
|
560
|
-
v = flat[i + (p - 1) * nIter + (c - 1) * nIter * nCols]
|
|
561
|
-
v === nothing ? NaN : Float64(v)
|
|
562
|
-
end
|
|
563
|
-
|
|
564
|
-
# Nodes fully covered by posterior columns are conditioned; anything
|
|
565
|
-
# uncovered stays latent and is forward-sampled, like Turing.predict.
|
|
566
|
-
gd = model.graph_evaluation_data
|
|
567
|
-
conditioned = APPL.VarName[]
|
|
568
|
-
restore = Tuple{APPL.VarName,Int}[]
|
|
569
|
-
for (i, vn) in enumerate(gd.sorted_nodes)
|
|
570
|
-
(gd.is_stochastic_vals[i] && !gd.is_observed_vals[i] && !is_target(vn)) || continue
|
|
571
|
-
leaves = collect(APPL.varname_leaves(vn, APPL.getvalue(model.evaluation_env, vn)))
|
|
572
|
-
found = count(l -> string(l) in posterior_names, leaves)
|
|
573
|
-
if found == length(leaves)
|
|
574
|
-
push!(conditioned, vn)
|
|
575
|
-
append!(restore, [(l, col[string(l)]) for l in leaves])
|
|
576
|
-
elseif found > 0
|
|
577
|
-
error("the posterior samples cover $(vn) only partially; refit before predicting")
|
|
578
|
-
end
|
|
579
|
-
end
|
|
580
|
-
isempty(conditioned) &&
|
|
581
|
-
error("the posterior samples share no parameters with the model; were they produced by fitting this spec?")
|
|
582
|
-
cond = APPL.condition(model, conditioned)
|
|
583
|
-
|
|
584
|
-
any(is_target, cond.graph_evaluation_data.sorted_nodes) ||
|
|
585
|
-
error("no variables predicted; check that [predict].targets name model variables")
|
|
586
|
-
|
|
587
|
-
out_leaves = APPL.VarName[]
|
|
588
|
-
arr = Array{Float64,3}(undef, 0, 0, 0)
|
|
589
|
-
for c in 1:nChains, i in 1:nIter
|
|
590
|
-
env = cond.evaluation_env
|
|
591
|
-
for (leaf, p) in restore
|
|
592
|
-
env = JuliaBUGS.BangBang.setindex!!(env, cell(i, p, c), leaf)
|
|
593
|
-
end
|
|
594
|
-
cond = JuliaBUGS.BangBang.setproperty!!(cond, :evaluation_env, env)
|
|
595
|
-
drawn, _ = APPL.evaluate!!(rng, cond)
|
|
596
|
-
if isempty(out_leaves)
|
|
597
|
-
out_leaves = [
|
|
598
|
-
l for vn in cond.graph_evaluation_data.sorted_nodes if is_target(vn)
|
|
599
|
-
for l in APPL.varname_leaves(vn, APPL.getvalue(drawn, vn))
|
|
600
|
-
]
|
|
601
|
-
arr = Array{Float64,3}(undef, nIter, length(out_leaves), nChains)
|
|
602
|
-
end
|
|
603
|
-
for (k, leaf) in enumerate(out_leaves)
|
|
604
|
-
arr[i, k, c] = Float64(APPL.getvalue(drawn, leaf))
|
|
605
|
-
end
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
pnames = string.(out_leaves)
|
|
609
|
-
n = length(pnames)
|
|
610
|
-
flat_out = Vector{Union{Float64,Nothing}}(undef, nIter * n * nChains)
|
|
611
|
-
for c in 1:nChains, p in 1:n, i in 1:nIter
|
|
612
|
-
v = arr[i, p, c]
|
|
613
|
-
flat_out[i + (p - 1) * nIter + (c - 1) * nIter * n] = isfinite(v) ? v : nothing
|
|
614
|
-
end
|
|
615
|
-
return Dict(
|
|
616
|
-
"size" => [nIter, n, nChains],
|
|
617
|
-
"value_flat" => flat_out,
|
|
618
|
-
"parameters" => pnames,
|
|
619
|
-
"name_map" => Dict("parameters" => pnames, "internals" => String[]),
|
|
620
|
-
)
|
|
621
|
-
end
|
|
622
|
-
|
|
623
|
-
function provenance()
|
|
624
|
-
packages = Dict{String,String}()
|
|
625
|
-
for (_, info) in Pkg.dependencies()
|
|
626
|
-
if info.name in
|
|
627
|
-
("Turing", "FlexiChains", "DynamicPPL", "AbstractMCMC", "JuliaBUGS", "AdvancedHMC", "ForwardDiff", "Mooncake", "ADTypes") &&
|
|
628
|
-
info.version !== nothing
|
|
629
|
-
packages[info.name] = string(info.version)
|
|
630
|
-
end
|
|
631
|
-
end
|
|
632
|
-
manifest = joinpath(dirname(Base.active_project()), "Manifest.toml")
|
|
633
|
-
return Dict(
|
|
634
|
-
"julia_version" => string(VERSION),
|
|
635
|
-
"packages" => packages,
|
|
636
|
-
"manifest_sha256" => isfile(manifest) ? bytes2hex(sha256(read(manifest))) : "",
|
|
637
|
-
)
|
|
638
|
-
end
|
|
639
|
-
|
|
640
|
-
# Resolve the model entry function: use the requested name, falling back to the
|
|
641
|
-
# conventional build_model. This lets a request leave the entry implicit, or name a
|
|
642
|
-
# custom entry that a given model file does not define (then build_model is used).
|
|
643
|
-
# Load the user's model file into a throwaway module. Isolation keeps repeated
|
|
644
|
-
# requests in the persistent worker from colliding on names (e.g. two models each
|
|
645
|
-
# defining `const model_def`), and confines the model's globals to their own scope.
|
|
646
|
-
function load_model_module(path)
|
|
647
|
-
mod = Module(gensym(:UserModel))
|
|
648
|
-
Base.include(mod, abspath(path))
|
|
649
|
-
return mod
|
|
650
|
-
end
|
|
651
|
-
|
|
652
|
-
function resolve_entry(mod, requested)
|
|
653
|
-
for name in (requested, "build_model")
|
|
654
|
-
name === nothing && continue
|
|
655
|
-
sym = Symbol(name)
|
|
656
|
-
# The model was just include'd, so its bindings live in a newer world than
|
|
657
|
-
# this frame; resolve them in the latest world to avoid a Julia 1.12
|
|
658
|
-
# world-age warning on the access.
|
|
659
|
-
Base.invokelatest(isdefined, mod, sym) && return Base.invokelatest(getfield, mod, sym)
|
|
660
|
-
end
|
|
661
|
-
error("model file defines no entry function (looked for $(requested) and build_model)")
|
|
662
|
-
end
|
|
663
|
-
|
|
664
|
-
# Runs one request end to end. Returns {"ok": true, "provenance": ...} or
|
|
665
|
-
# {"ok": false, "error": ..., "stage": ...}; never throws.
|
|
666
|
-
function handle_request(request)
|
|
667
|
-
out = request["out"]
|
|
668
|
-
mode = get(request, "mode", "fit")
|
|
669
|
-
backend = get(get(request, "backend", Dict()), "id", "turing")
|
|
670
|
-
stage = "compile"
|
|
671
|
-
try
|
|
672
|
-
Random.seed!(Int(request["seed"]))
|
|
673
|
-
rng = StableRNG(Int(request["seed"]))
|
|
674
|
-
modelmod = load_model_module(request["model"]["file"])
|
|
675
|
-
entry = resolve_entry(modelmod, get(request["model"], "entry", nothing))
|
|
676
|
-
|
|
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"
|
|
697
|
-
if backend == "juliabugs"
|
|
698
|
-
model = Base.invokelatest(entry, bugs_predict_namedtuple(request["data"]))
|
|
699
|
-
stage = "load_samples"
|
|
700
|
-
posterior = JSON.parsefile(request["samples"])
|
|
701
|
-
stage = "predict"
|
|
702
|
-
Base.invokelatest(predict_bugs, model, posterior, request["predict"]["targets"], rng)
|
|
703
|
-
else
|
|
704
|
-
model = Base.invokelatest(entry, ModelData(predict_namedtuple(request["data"])))
|
|
705
|
-
stage = "load_samples"
|
|
706
|
-
rchn = wire_to_vnchain(JSON.parsefile(request["samples"]), request["predict"]["targets"])
|
|
707
|
-
stage = "predict"
|
|
708
|
-
pp = Base.invokelatest(predict, rng, model, rchn; include_all = false)
|
|
709
|
-
isempty(FlexiChains.parameters(pp)) &&
|
|
710
|
-
error("no variables predicted; check that [predict].targets name unconditioned outcomes")
|
|
711
|
-
vnchain_to_wire(pp)
|
|
712
|
-
end
|
|
713
|
-
else
|
|
714
|
-
data = backend == "juliabugs" ? bugs_namedtuple(request["data"]) : ModelData(to_namedtuple(request["data"]))
|
|
715
|
-
sampler, warmup = build_sampler(request["sampler"], backend)
|
|
716
|
-
draws = Int(request["sampler"]["draws"])
|
|
717
|
-
chains = Int(get(request["sampler"], "chains", 4))
|
|
718
|
-
stage = "sample"
|
|
719
|
-
if backend == "juliabugs"
|
|
720
|
-
model = Base.invokelatest(entry, data)
|
|
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
|
|
733
|
-
vnchain_to_wire(chn)
|
|
734
|
-
else
|
|
735
|
-
cb = get(request, "stream_draws", false) ?
|
|
736
|
-
draw_streamer(draws, Int(get(request, "draw_batch_size", 25))) : nothing
|
|
737
|
-
extra = sampling_kwargs(request["sampler"], warmup, chains)
|
|
738
|
-
chn = Base.invokelatest(
|
|
739
|
-
build_and_sample, entry, data, sampler, draws, chains, rng;
|
|
740
|
-
callback = cb, extra,
|
|
741
|
-
)
|
|
742
|
-
vnchain_to_wire(chn)
|
|
743
|
-
end
|
|
744
|
-
end
|
|
745
|
-
|
|
746
|
-
stage = "write"
|
|
747
|
-
tmp = out * ".tmp"
|
|
748
|
-
open(tmp, "w") do io
|
|
749
|
-
JSON.print(io, wire)
|
|
750
|
-
end
|
|
751
|
-
mv(tmp, out; force = true)
|
|
752
|
-
|
|
753
|
-
return Dict("ok" => true, "provenance" => provenance())
|
|
754
|
-
catch err
|
|
755
|
-
return Dict("ok" => false, "error" => sprint(showerror, err), "stage" => stage)
|
|
756
|
-
end
|
|
757
|
-
end
|