@mcmcjs/julia 0.4.0 → 0.6.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/driver.jl +10 -214
- package/dist/fitlib.jl +289 -0
- package/dist/index.cjs +403 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -16
- package/dist/index.d.ts +82 -16
- package/dist/index.js +399 -59
- package/dist/index.js.map +1 -1
- package/dist/worker.jl +95 -0
- package/package.json +3 -3
package/dist/driver.jl
CHANGED
|
@@ -1,220 +1,16 @@
|
|
|
1
|
-
# MCMC.js fit driver. Reads a run-request JSON
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
# contained here.
|
|
7
|
-
using JSON
|
|
8
|
-
using Random
|
|
9
|
-
using StableRNGs
|
|
10
|
-
using Turing
|
|
11
|
-
using Turing: VarName
|
|
12
|
-
using MCMCChains
|
|
13
|
-
using SHA
|
|
14
|
-
using Pkg
|
|
15
|
-
# Imported (not `using`) so they do not pull `@model` etc. into Main and clash
|
|
16
|
-
# with Turing's exports, which would break Turing model files on include.
|
|
17
|
-
import JuliaBUGS
|
|
18
|
-
import AdvancedHMC
|
|
19
|
-
import ForwardDiff
|
|
20
|
-
import FlexiChains
|
|
21
|
-
import DimensionalData
|
|
22
|
-
|
|
23
|
-
function build_sampler(sampler, backend)
|
|
24
|
-
algorithm = get(sampler, "algorithm", "NUTS")
|
|
25
|
-
algorithm == "NUTS" || error("unsupported sampler algorithm: $algorithm")
|
|
26
|
-
warmup = Int(get(sampler, "warmup", 1000))
|
|
27
|
-
adapt_delta = Float64(get(sampler, "adapt_delta", 0.8))
|
|
28
|
-
nuts = backend == "juliabugs" ? AdvancedHMC.NUTS(adapt_delta) : Turing.NUTS(warmup, adapt_delta)
|
|
29
|
-
return nuts, warmup
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
to_namedtuple(data) = (; (Symbol(k) => v for (k, v) in data)...)
|
|
33
|
-
|
|
34
|
-
# JuliaBUGS requires dense arrays with a concrete numeric eltype, but JSON parses
|
|
35
|
-
# them as Vector{Any} (and nested arrays as vectors of vectors). narrow promotes a
|
|
36
|
-
# numeric array to a concrete eltype and stacks equal-sized nested arrays into a
|
|
37
|
-
# dense array; an empty array becomes Float64[].
|
|
38
|
-
function narrow(v::AbstractVector)
|
|
39
|
-
isempty(v) && return Float64[]
|
|
40
|
-
elems = [narrow(x) for x in v]
|
|
41
|
-
if all(e -> e isa AbstractArray, elems) && allequal(size.(elems))
|
|
42
|
-
return stack(elems; dims = 1)
|
|
43
|
-
end
|
|
44
|
-
return eltype(elems) <: Real && !isconcretetype(eltype(elems)) ? float.(elems) : elems
|
|
45
|
-
end
|
|
46
|
-
narrow(x) = x
|
|
47
|
-
bugs_namedtuple(data) = (; (Symbol(k) => narrow(v) for (k, v) in data)...)
|
|
48
|
-
|
|
49
|
-
# JSON null arrives as `nothing`; map it to `missing` so a blanked outcome becomes
|
|
50
|
-
# a sampling (predictive) statement in the model.
|
|
51
|
-
to_missing(x) = x === nothing ? missing : (x isa AbstractVector ? map(to_missing, x) : x)
|
|
52
|
-
predict_namedtuple(data) = (; (Symbol(k) => to_missing(v) for (k, v) in data)...)
|
|
53
|
-
|
|
54
|
-
# Rebuild a posterior Chains (latents only) from our MCMCChains-JSON wire object so
|
|
55
|
-
# it can feed Turing.predict. The varname_to_symbol map is required for VarName
|
|
56
|
-
# indexing; target outcome columns are excluded.
|
|
57
|
-
function chains_from_wire(wire, targets)
|
|
58
|
-
sz = wire["size"]
|
|
59
|
-
nIter, nParams, nChains = Int(sz[1]), Int(sz[2]), Int(sz[3])
|
|
60
|
-
flat = wire["value_flat"]
|
|
61
|
-
allnames = wire["parameters"]
|
|
62
|
-
is_target(n) = any(t -> n == t || startswith(n, t * "["), targets)
|
|
63
|
-
keep = [n for n in wire["name_map"]["parameters"] if !is_target(n)]
|
|
64
|
-
index = Dict(n => i for (i, n) in enumerate(allnames))
|
|
65
|
-
arr = Array{Float64,3}(undef, nIter, length(keep), nChains)
|
|
66
|
-
for (newp, n) in enumerate(keep)
|
|
67
|
-
p = index[n]
|
|
68
|
-
for c in 1:nChains, i in 1:nIter
|
|
69
|
-
v = flat[i + (p - 1) * nIter + (c - 1) * nIter * nParams]
|
|
70
|
-
arr[i, newp, c] = v === nothing ? NaN : Float64(v)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
syms = Symbol.(keep)
|
|
74
|
-
vmap = Dict{VarName,Symbol}(eval(:(Turing.@varname($(Meta.parse(n))))) => Symbol(n) for n in keep)
|
|
75
|
-
return MCMCChains.Chains(arr, syms, Dict(:parameters => syms); info = (; varname_to_symbol = vmap))
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Build the model and sample in one call so both run in the latest world age:
|
|
79
|
-
# the model methods are defined by `include` at runtime, so a plain call from
|
|
80
|
-
# this (older) world would fail with "method too new". invokelatest fixes that.
|
|
81
|
-
# Returns Turing's default chain type, a FlexiChain.
|
|
82
|
-
function build_and_sample(entry, data, sampler, draws, chains, rng)
|
|
83
|
-
model = entry(data)
|
|
84
|
-
return sample(rng, model, sampler, MCMCSerial(), draws, chains; progress = false)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# FlexiChains-native wire writer. DimArray(chn) splits array-valued parameters
|
|
88
|
-
# into scalar leaves (theta -> theta[1], theta[2]) in the (iter, chain, param)
|
|
89
|
-
# orientation; Real-valued extras become the internals section, matching what
|
|
90
|
-
# the MCMCChains bridge produced.
|
|
91
|
-
function vnchain_to_wire(chn)
|
|
92
|
-
da = DimensionalData.DimArray(chn)
|
|
93
|
-
pnames = string.(collect(DimensionalData.lookup(da, :param)))
|
|
94
|
-
arr = parent(da)
|
|
95
|
-
nIter, nChains, nParams = size(arr)
|
|
96
|
-
|
|
97
|
-
extras = [e for e in FlexiChains.extras(chn) if eltype(chn[e]) <: Union{Missing,Real}]
|
|
98
|
-
enames = [string(Symbol(e)) for e in extras]
|
|
99
|
-
total = nParams + length(extras)
|
|
100
|
-
|
|
101
|
-
flat = Vector{Union{Float64,Nothing}}(undef, nIter * total * nChains)
|
|
102
|
-
cell(v) = v === missing ? nothing : Float64(v)
|
|
103
|
-
for c in 1:nChains, p in 1:nParams, i in 1:nIter
|
|
104
|
-
flat[i + (p - 1) * nIter + (c - 1) * nIter * total] = cell(arr[i, c, p])
|
|
105
|
-
end
|
|
106
|
-
for (k, e) in enumerate(extras)
|
|
107
|
-
m = chn[e]
|
|
108
|
-
p = nParams + k
|
|
109
|
-
for c in 1:nChains, i in 1:nIter
|
|
110
|
-
flat[i + (p - 1) * nIter + (c - 1) * nIter * total] = cell(m[i, c])
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
return Dict(
|
|
115
|
-
"size" => [nIter, total, nChains],
|
|
116
|
-
"value_flat" => flat,
|
|
117
|
-
"parameters" => vcat(pnames, enames),
|
|
118
|
-
"name_map" => Dict("parameters" => pnames, "internals" => enames),
|
|
119
|
-
)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# JuliaBUGS compiles the model at runtime, so building and sampling must run in
|
|
123
|
-
# separate latest-world frames; merging them reintroduces a world-age error. The
|
|
124
|
-
# result is already an MCMCChains.Chains, so it is not re-wrapped.
|
|
125
|
-
function sample_bugs(model, sampler, warmup, draws, chains, rng)
|
|
126
|
-
return JuliaBUGS.AbstractMCMC.sample(
|
|
127
|
-
rng, model, sampler, JuliaBUGS.AbstractMCMC.MCMCSerial(), draws, chains;
|
|
128
|
-
chain_type = MCMCChains.Chains, n_adapts = warmup, discard_initial = warmup, progress = false,
|
|
129
|
-
)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Build the exact wire object parseMCMCChainsJson consumes: value_flat is indexed
|
|
133
|
-
# i + p*nIter + c*nIter*nParams (column-major over [iteration, parameter, chain]).
|
|
134
|
-
function chains_to_wire(chn)
|
|
135
|
-
arr = Array(chn.value)
|
|
136
|
-
nIter, nParams, nChains = size(arr)
|
|
137
|
-
flat = Vector{Union{Float64,Nothing}}(undef, nIter * nParams * nChains)
|
|
138
|
-
for c in 1:nChains, p in 1:nParams, i in 1:nIter
|
|
139
|
-
v = arr[i, p, c]
|
|
140
|
-
flat[i + (p - 1) * nIter + (c - 1) * nIter * nParams] = v === missing ? nothing : Float64(v)
|
|
141
|
-
end
|
|
142
|
-
return Dict(
|
|
143
|
-
"size" => [nIter, nParams, nChains],
|
|
144
|
-
"value_flat" => flat,
|
|
145
|
-
"parameters" => string.(names(chn)),
|
|
146
|
-
"name_map" => Dict(
|
|
147
|
-
"parameters" => string.(get(chn.name_map, :parameters, Symbol[])),
|
|
148
|
-
"internals" => string.(get(chn.name_map, :internals, Symbol[])),
|
|
149
|
-
),
|
|
150
|
-
)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
function provenance()
|
|
154
|
-
packages = Dict{String,String}()
|
|
155
|
-
for (_, info) in Pkg.dependencies()
|
|
156
|
-
if info.name in
|
|
157
|
-
("Turing", "FlexiChains", "MCMCChains", "DynamicPPL", "AbstractMCMC", "JuliaBUGS", "AdvancedHMC", "ForwardDiff", "ADTypes") &&
|
|
158
|
-
info.version !== nothing
|
|
159
|
-
packages[info.name] = string(info.version)
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
manifest = joinpath(dirname(Base.active_project()), "Manifest.toml")
|
|
163
|
-
return Dict(
|
|
164
|
-
"julia_version" => string(VERSION),
|
|
165
|
-
"packages" => packages,
|
|
166
|
-
"manifest_sha256" => isfile(manifest) ? bytes2hex(sha256(read(manifest))) : "",
|
|
167
|
-
)
|
|
168
|
-
end
|
|
1
|
+
# MCMC.js one-shot fit driver. Reads a run-request JSON file and runs it via
|
|
2
|
+
# fitlib's handle_request. On success a single line of provenance JSON is
|
|
3
|
+
# printed to stdout; failures print one-line JSON to stderr ({error, stage})
|
|
4
|
+
# and exit nonzero. Progress streams to stderr as {"mcmcjs":"progress"} lines.
|
|
5
|
+
include(joinpath(@__DIR__, "fitlib.jl"))
|
|
169
6
|
|
|
170
7
|
function main()
|
|
171
8
|
request = JSON.parsefile(ARGS[1])
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
Random.seed!(Int(request["seed"]))
|
|
178
|
-
rng = StableRNG(Int(request["seed"]))
|
|
179
|
-
include(abspath(request["model"]["file"]))
|
|
180
|
-
entry = getfield(Main, Symbol(request["model"]["entry"]))
|
|
181
|
-
|
|
182
|
-
wire = if mode == "predict"
|
|
183
|
-
backend == "turing" || error("predict is not yet supported for backend $backend")
|
|
184
|
-
model = Base.invokelatest(entry, predict_namedtuple(request["data"]))
|
|
185
|
-
stage = "load_samples"
|
|
186
|
-
rchn = chains_from_wire(JSON.parsefile(request["samples"]), request["predict"]["targets"])
|
|
187
|
-
stage = "predict"
|
|
188
|
-
pp = Base.invokelatest(predict, rng, model, rchn)
|
|
189
|
-
isempty(names(pp)) &&
|
|
190
|
-
error("no variables predicted; check that [predict].targets name unconditioned outcomes")
|
|
191
|
-
chains_to_wire(pp)
|
|
192
|
-
else
|
|
193
|
-
data = backend == "juliabugs" ? bugs_namedtuple(request["data"]) : to_namedtuple(request["data"])
|
|
194
|
-
sampler, warmup = build_sampler(request["sampler"], backend)
|
|
195
|
-
draws = Int(request["sampler"]["draws"])
|
|
196
|
-
chains = Int(get(request["sampler"], "chains", 4))
|
|
197
|
-
stage = "sample"
|
|
198
|
-
if backend == "juliabugs"
|
|
199
|
-
model = Base.invokelatest(entry, data)
|
|
200
|
-
chn = Base.invokelatest(sample_bugs, model, sampler, warmup, draws, chains, rng)
|
|
201
|
-
chains_to_wire(chn)
|
|
202
|
-
else
|
|
203
|
-
chn = Base.invokelatest(build_and_sample, entry, data, sampler, draws, chains, rng)
|
|
204
|
-
vnchain_to_wire(chn)
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
stage = "write"
|
|
209
|
-
tmp = out * ".tmp"
|
|
210
|
-
open(tmp, "w") do io
|
|
211
|
-
JSON.print(io, wire)
|
|
212
|
-
end
|
|
213
|
-
mv(tmp, out; force = true)
|
|
214
|
-
|
|
215
|
-
println(JSON.json(provenance()))
|
|
216
|
-
catch err
|
|
217
|
-
println(stderr, JSON.json(Dict("error" => sprint(showerror, err), "stage" => stage)))
|
|
9
|
+
result = handle_request(request)
|
|
10
|
+
if result["ok"]
|
|
11
|
+
println(JSON.json(result["provenance"]))
|
|
12
|
+
else
|
|
13
|
+
println(stderr, JSON.json(Dict("error" => result["error"], "stage" => result["stage"])))
|
|
218
14
|
exit(1)
|
|
219
15
|
end
|
|
220
16
|
end
|
package/dist/fitlib.jl
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
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 MCMCChains-JSON (the
|
|
4
|
+
# @mcmcjs/core samples artifact). This is the only place that touches the
|
|
5
|
+
# backend chain types, 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 MCMCChains
|
|
13
|
+
using SHA
|
|
14
|
+
using Pkg
|
|
15
|
+
# Imported (not `using`) so they do not pull `@model` etc. into Main and clash
|
|
16
|
+
# with Turing's exports, which would break Turing model files on include.
|
|
17
|
+
import JuliaBUGS
|
|
18
|
+
import AdvancedHMC
|
|
19
|
+
import ForwardDiff
|
|
20
|
+
import FlexiChains
|
|
21
|
+
import DimensionalData
|
|
22
|
+
|
|
23
|
+
# AbstractMCMC reports sampling progress through ProgressLogging (reachable via
|
|
24
|
+
# Turing's dependency, no extra package in the managed env).
|
|
25
|
+
const ProgressLogging = Turing.AbstractMCMC.ProgressLogging
|
|
26
|
+
|
|
27
|
+
# Where progress JSON lines go: the driver leaves this on stderr; the worker
|
|
28
|
+
# points it at the client socket for the duration of a request.
|
|
29
|
+
const PROGRESS_IO = Ref{IO}(stderr)
|
|
30
|
+
|
|
31
|
+
# Translates ProgressLogging records into one JSON line per update
|
|
32
|
+
# ({"mcmcjs":"progress",...}). The TypeScript side filters these out of the
|
|
33
|
+
# failure protocol's stderr buffer. Info-and-above records pass through to a
|
|
34
|
+
# plain console logger; Debug records (AdvancedHMC step-size search) are dropped.
|
|
35
|
+
struct JSONProgressLogger <: Logging.AbstractLogger
|
|
36
|
+
fallback::Logging.AbstractLogger
|
|
37
|
+
last::Dict{Int,Float64}
|
|
38
|
+
end
|
|
39
|
+
JSONProgressLogger() =
|
|
40
|
+
JSONProgressLogger(Logging.ConsoleLogger(stderr, Logging.Info), Dict{Int,Float64}())
|
|
41
|
+
|
|
42
|
+
Logging.min_enabled_level(::JSONProgressLogger) = Logging.BelowMinLevel
|
|
43
|
+
Logging.shouldlog(::JSONProgressLogger, args...) = true
|
|
44
|
+
Logging.catch_exceptions(::JSONProgressLogger) = true
|
|
45
|
+
|
|
46
|
+
function emit_progress(logger::JSONProgressLogger, name, fraction, done)
|
|
47
|
+
m = match(r"Chain (\d+) of (\d+)", String(name))
|
|
48
|
+
chain = m === nothing ? 1 : parse(Int, m.captures[1])
|
|
49
|
+
of = m === nothing ? 1 : parse(Int, m.captures[2])
|
|
50
|
+
f = done ? 1.0 : fraction === nothing ? 0.0 : Float64(fraction)
|
|
51
|
+
# AbstractMCMC already steps by 1%; the dedupe guards chattier emitters.
|
|
52
|
+
if done || f - get(logger.last, chain, -1.0) >= 0.01
|
|
53
|
+
logger.last[chain] = f
|
|
54
|
+
println(
|
|
55
|
+
PROGRESS_IO[],
|
|
56
|
+
JSON.json(
|
|
57
|
+
Dict(
|
|
58
|
+
"mcmcjs" => "progress",
|
|
59
|
+
"chain" => chain,
|
|
60
|
+
"of" => of,
|
|
61
|
+
"fraction" => round(f; digits = 4),
|
|
62
|
+
"done" => done,
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
flush(PROGRESS_IO[])
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
function Logging.handle_message(
|
|
71
|
+
logger::JSONProgressLogger, level, message, _module, group, id, file, line; kwargs...
|
|
72
|
+
)
|
|
73
|
+
progress = message isa ProgressLogging.ProgressString ? message.progress :
|
|
74
|
+
message isa ProgressLogging.Progress ? message : nothing
|
|
75
|
+
if progress !== nothing
|
|
76
|
+
emit_progress(logger, progress.name, progress.fraction, progress.done)
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
if haskey(kwargs, :progress) && (kwargs[:progress] isa Real || kwargs[:progress] == "done")
|
|
80
|
+
value = kwargs[:progress]
|
|
81
|
+
emit_progress(logger, string(message), value == "done" ? nothing : value, value == "done")
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
level >= Logging.Info || return
|
|
85
|
+
Logging.handle_message(logger.fallback, level, message, _module, group, id, file, line; kwargs...)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
function build_sampler(sampler, backend)
|
|
89
|
+
algorithm = get(sampler, "algorithm", "NUTS")
|
|
90
|
+
algorithm == "NUTS" || error("unsupported sampler algorithm: $algorithm")
|
|
91
|
+
warmup = Int(get(sampler, "warmup", 1000))
|
|
92
|
+
adapt_delta = Float64(get(sampler, "adapt_delta", 0.8))
|
|
93
|
+
nuts = backend == "juliabugs" ? AdvancedHMC.NUTS(adapt_delta) : Turing.NUTS(warmup, adapt_delta)
|
|
94
|
+
return nuts, warmup
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
to_namedtuple(data) = (; (Symbol(k) => v for (k, v) in data)...)
|
|
98
|
+
|
|
99
|
+
# JuliaBUGS requires dense arrays with a concrete numeric eltype, but JSON parses
|
|
100
|
+
# them as Vector{Any} (and nested arrays as vectors of vectors). narrow promotes a
|
|
101
|
+
# numeric array to a concrete eltype and stacks equal-sized nested arrays into a
|
|
102
|
+
# dense array; an empty array becomes Float64[].
|
|
103
|
+
function narrow(v::AbstractVector)
|
|
104
|
+
isempty(v) && return Float64[]
|
|
105
|
+
elems = [narrow(x) for x in v]
|
|
106
|
+
if all(e -> e isa AbstractArray, elems) && allequal(size.(elems))
|
|
107
|
+
return stack(elems; dims = 1)
|
|
108
|
+
end
|
|
109
|
+
return eltype(elems) <: Real && !isconcretetype(eltype(elems)) ? float.(elems) : elems
|
|
110
|
+
end
|
|
111
|
+
narrow(x) = x
|
|
112
|
+
bugs_namedtuple(data) = (; (Symbol(k) => narrow(v) for (k, v) in data)...)
|
|
113
|
+
|
|
114
|
+
# JSON null arrives as `nothing`; map it to `missing` so a blanked outcome becomes
|
|
115
|
+
# a sampling (predictive) statement in the model.
|
|
116
|
+
to_missing(x) = x === nothing ? missing : (x isa AbstractVector ? map(to_missing, x) : x)
|
|
117
|
+
predict_namedtuple(data) = (; (Symbol(k) => to_missing(v) for (k, v) in data)...)
|
|
118
|
+
|
|
119
|
+
# Rebuild a posterior Chains (latents only) from our MCMCChains-JSON wire object so
|
|
120
|
+
# it can feed Turing.predict. The varname_to_symbol map is required for VarName
|
|
121
|
+
# indexing; target outcome columns are excluded.
|
|
122
|
+
function chains_from_wire(wire, targets)
|
|
123
|
+
sz = wire["size"]
|
|
124
|
+
nIter, nParams, nChains = Int(sz[1]), Int(sz[2]), Int(sz[3])
|
|
125
|
+
flat = wire["value_flat"]
|
|
126
|
+
allnames = wire["parameters"]
|
|
127
|
+
is_target(n) = any(t -> n == t || startswith(n, t * "["), targets)
|
|
128
|
+
keep = [n for n in wire["name_map"]["parameters"] if !is_target(n)]
|
|
129
|
+
index = Dict(n => i for (i, n) in enumerate(allnames))
|
|
130
|
+
arr = Array{Float64,3}(undef, nIter, length(keep), nChains)
|
|
131
|
+
for (newp, n) in enumerate(keep)
|
|
132
|
+
p = index[n]
|
|
133
|
+
for c in 1:nChains, i in 1:nIter
|
|
134
|
+
v = flat[i + (p - 1) * nIter + (c - 1) * nIter * nParams]
|
|
135
|
+
arr[i, newp, c] = v === nothing ? NaN : Float64(v)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
syms = Symbol.(keep)
|
|
139
|
+
vmap = Dict{VarName,Symbol}(eval(:(Turing.@varname($(Meta.parse(n))))) => Symbol(n) for n in keep)
|
|
140
|
+
return MCMCChains.Chains(arr, syms, Dict(:parameters => syms); info = (; varname_to_symbol = vmap))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Build the model and sample in one call so both run in the latest world age:
|
|
144
|
+
# the model methods are defined by `include` at runtime, so a plain call from
|
|
145
|
+
# this (older) world would fail with "method too new". invokelatest fixes that.
|
|
146
|
+
# Returns Turing's default chain type, a FlexiChain.
|
|
147
|
+
function build_and_sample(entry, data, sampler, draws, chains, rng)
|
|
148
|
+
model = entry(data)
|
|
149
|
+
return Logging.with_logger(JSONProgressLogger()) do
|
|
150
|
+
sample(rng, model, sampler, MCMCSerial(), draws, chains; progress = true)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# FlexiChains-native wire writer. DimArray(chn) splits array-valued parameters
|
|
155
|
+
# into scalar leaves (theta -> theta[1], theta[2]) in the (iter, chain, param)
|
|
156
|
+
# orientation; Real-valued extras become the internals section, matching what
|
|
157
|
+
# the MCMCChains bridge produced.
|
|
158
|
+
function vnchain_to_wire(chn)
|
|
159
|
+
da = DimensionalData.DimArray(chn)
|
|
160
|
+
pnames = string.(collect(DimensionalData.lookup(da, :param)))
|
|
161
|
+
arr = parent(da)
|
|
162
|
+
nIter, nChains, nParams = size(arr)
|
|
163
|
+
|
|
164
|
+
extras = [e for e in FlexiChains.extras(chn) if eltype(chn[e]) <: Union{Missing,Real}]
|
|
165
|
+
enames = [string(Symbol(e)) for e in extras]
|
|
166
|
+
total = nParams + length(extras)
|
|
167
|
+
|
|
168
|
+
flat = Vector{Union{Float64,Nothing}}(undef, nIter * total * nChains)
|
|
169
|
+
cell(v) = v === missing ? nothing : Float64(v)
|
|
170
|
+
for c in 1:nChains, p in 1:nParams, i in 1:nIter
|
|
171
|
+
flat[i + (p - 1) * nIter + (c - 1) * nIter * total] = cell(arr[i, c, p])
|
|
172
|
+
end
|
|
173
|
+
for (k, e) in enumerate(extras)
|
|
174
|
+
m = chn[e]
|
|
175
|
+
p = nParams + k
|
|
176
|
+
for c in 1:nChains, i in 1:nIter
|
|
177
|
+
flat[i + (p - 1) * nIter + (c - 1) * nIter * total] = cell(m[i, c])
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
return Dict(
|
|
182
|
+
"size" => [nIter, total, nChains],
|
|
183
|
+
"value_flat" => flat,
|
|
184
|
+
"parameters" => vcat(pnames, enames),
|
|
185
|
+
"name_map" => Dict("parameters" => pnames, "internals" => enames),
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# JuliaBUGS compiles the model at runtime, so building and sampling must run in
|
|
190
|
+
# separate latest-world frames; merging them reintroduces a world-age error. The
|
|
191
|
+
# result is already an MCMCChains.Chains, so it is not re-wrapped.
|
|
192
|
+
function sample_bugs(model, sampler, warmup, draws, chains, rng)
|
|
193
|
+
return Logging.with_logger(JSONProgressLogger()) do
|
|
194
|
+
JuliaBUGS.AbstractMCMC.sample(
|
|
195
|
+
rng, model, sampler, JuliaBUGS.AbstractMCMC.MCMCSerial(), draws, chains;
|
|
196
|
+
chain_type = MCMCChains.Chains, n_adapts = warmup, discard_initial = warmup, progress = true,
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Build the exact wire object parseMCMCChainsJson consumes: value_flat is indexed
|
|
202
|
+
# i + p*nIter + c*nIter*nParams (column-major over [iteration, parameter, chain]).
|
|
203
|
+
function chains_to_wire(chn)
|
|
204
|
+
arr = Array(chn.value)
|
|
205
|
+
nIter, nParams, nChains = size(arr)
|
|
206
|
+
flat = Vector{Union{Float64,Nothing}}(undef, nIter * nParams * nChains)
|
|
207
|
+
for c in 1:nChains, p in 1:nParams, i in 1:nIter
|
|
208
|
+
v = arr[i, p, c]
|
|
209
|
+
flat[i + (p - 1) * nIter + (c - 1) * nIter * nParams] = v === missing ? nothing : Float64(v)
|
|
210
|
+
end
|
|
211
|
+
return Dict(
|
|
212
|
+
"size" => [nIter, nParams, nChains],
|
|
213
|
+
"value_flat" => flat,
|
|
214
|
+
"parameters" => string.(names(chn)),
|
|
215
|
+
"name_map" => Dict(
|
|
216
|
+
"parameters" => string.(get(chn.name_map, :parameters, Symbol[])),
|
|
217
|
+
"internals" => string.(get(chn.name_map, :internals, Symbol[])),
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
function provenance()
|
|
223
|
+
packages = Dict{String,String}()
|
|
224
|
+
for (_, info) in Pkg.dependencies()
|
|
225
|
+
if info.name in
|
|
226
|
+
("Turing", "FlexiChains", "MCMCChains", "DynamicPPL", "AbstractMCMC", "JuliaBUGS", "AdvancedHMC", "ForwardDiff", "ADTypes") &&
|
|
227
|
+
info.version !== nothing
|
|
228
|
+
packages[info.name] = string(info.version)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
manifest = joinpath(dirname(Base.active_project()), "Manifest.toml")
|
|
232
|
+
return Dict(
|
|
233
|
+
"julia_version" => string(VERSION),
|
|
234
|
+
"packages" => packages,
|
|
235
|
+
"manifest_sha256" => isfile(manifest) ? bytes2hex(sha256(read(manifest))) : "",
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Runs one request end to end. Returns {"ok": true, "provenance": ...} or
|
|
240
|
+
# {"ok": false, "error": ..., "stage": ...}; never throws.
|
|
241
|
+
function handle_request(request)
|
|
242
|
+
out = request["out"]
|
|
243
|
+
mode = get(request, "mode", "fit")
|
|
244
|
+
backend = get(get(request, "backend", Dict()), "id", "turing")
|
|
245
|
+
stage = "compile"
|
|
246
|
+
try
|
|
247
|
+
Random.seed!(Int(request["seed"]))
|
|
248
|
+
rng = StableRNG(Int(request["seed"]))
|
|
249
|
+
Base.include(Main, abspath(request["model"]["file"]))
|
|
250
|
+
entry = getfield(Main, Symbol(request["model"]["entry"]))
|
|
251
|
+
|
|
252
|
+
wire = if mode == "predict"
|
|
253
|
+
backend == "turing" || error("predict is not yet supported for backend $backend")
|
|
254
|
+
model = Base.invokelatest(entry, predict_namedtuple(request["data"]))
|
|
255
|
+
stage = "load_samples"
|
|
256
|
+
rchn = chains_from_wire(JSON.parsefile(request["samples"]), request["predict"]["targets"])
|
|
257
|
+
stage = "predict"
|
|
258
|
+
pp = Base.invokelatest(predict, rng, model, rchn)
|
|
259
|
+
isempty(names(pp)) &&
|
|
260
|
+
error("no variables predicted; check that [predict].targets name unconditioned outcomes")
|
|
261
|
+
chains_to_wire(pp)
|
|
262
|
+
else
|
|
263
|
+
data = backend == "juliabugs" ? bugs_namedtuple(request["data"]) : to_namedtuple(request["data"])
|
|
264
|
+
sampler, warmup = build_sampler(request["sampler"], backend)
|
|
265
|
+
draws = Int(request["sampler"]["draws"])
|
|
266
|
+
chains = Int(get(request["sampler"], "chains", 4))
|
|
267
|
+
stage = "sample"
|
|
268
|
+
if backend == "juliabugs"
|
|
269
|
+
model = Base.invokelatest(entry, data)
|
|
270
|
+
chn = Base.invokelatest(sample_bugs, model, sampler, warmup, draws, chains, rng)
|
|
271
|
+
chains_to_wire(chn)
|
|
272
|
+
else
|
|
273
|
+
chn = Base.invokelatest(build_and_sample, entry, data, sampler, draws, chains, rng)
|
|
274
|
+
vnchain_to_wire(chn)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
stage = "write"
|
|
279
|
+
tmp = out * ".tmp"
|
|
280
|
+
open(tmp, "w") do io
|
|
281
|
+
JSON.print(io, wire)
|
|
282
|
+
end
|
|
283
|
+
mv(tmp, out; force = true)
|
|
284
|
+
|
|
285
|
+
return Dict("ok" => true, "provenance" => provenance())
|
|
286
|
+
catch err
|
|
287
|
+
return Dict("ok" => false, "error" => sprint(showerror, err), "stage" => stage)
|
|
288
|
+
end
|
|
289
|
+
end
|