@mcmcjs/julia 0.1.0 → 0.3.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 +186 -0
- package/dist/index.cjs +483 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +145 -9
- package/dist/index.d.ts +145 -9
- package/dist/index.js +467 -8
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/dist/driver.jl
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# MCMC.js fit driver. Reads a run-request JSON, runs the spec's backend (Turing or
|
|
2
|
+
# JuliaBUGS), and writes the draws as MCMCChains-JSON (the @mcmcjs/core samples
|
|
3
|
+
# artifact). On success a single line of provenance JSON is printed to stdout;
|
|
4
|
+
# failures print one-line JSON to stderr ({error, stage}) and exit nonzero. This is
|
|
5
|
+
# the only place that touches the backend chain types, so a chain-type change is
|
|
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
|
+
|
|
21
|
+
function build_sampler(sampler, backend)
|
|
22
|
+
algorithm = get(sampler, "algorithm", "NUTS")
|
|
23
|
+
algorithm == "NUTS" || error("unsupported sampler algorithm: $algorithm")
|
|
24
|
+
warmup = Int(get(sampler, "warmup", 1000))
|
|
25
|
+
adapt_delta = Float64(get(sampler, "adapt_delta", 0.8))
|
|
26
|
+
nuts = backend == "juliabugs" ? AdvancedHMC.NUTS(adapt_delta) : Turing.NUTS(warmup, adapt_delta)
|
|
27
|
+
return nuts, warmup
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
to_namedtuple(data) = (; (Symbol(k) => v for (k, v) in data)...)
|
|
31
|
+
|
|
32
|
+
# JuliaBUGS requires dense arrays with a concrete numeric eltype, but JSON parses
|
|
33
|
+
# them as Vector{Any} (and nested arrays as vectors of vectors). narrow promotes a
|
|
34
|
+
# numeric array to a concrete eltype and stacks equal-sized nested arrays into a
|
|
35
|
+
# dense array; an empty array becomes Float64[].
|
|
36
|
+
function narrow(v::AbstractVector)
|
|
37
|
+
isempty(v) && return Float64[]
|
|
38
|
+
elems = [narrow(x) for x in v]
|
|
39
|
+
if all(e -> e isa AbstractArray, elems) && allequal(size.(elems))
|
|
40
|
+
return stack(elems; dims = 1)
|
|
41
|
+
end
|
|
42
|
+
return eltype(elems) <: Real && !isconcretetype(eltype(elems)) ? float.(elems) : elems
|
|
43
|
+
end
|
|
44
|
+
narrow(x) = x
|
|
45
|
+
bugs_namedtuple(data) = (; (Symbol(k) => narrow(v) for (k, v) in data)...)
|
|
46
|
+
|
|
47
|
+
# JSON null arrives as `nothing`; map it to `missing` so a blanked outcome becomes
|
|
48
|
+
# a sampling (predictive) statement in the model.
|
|
49
|
+
to_missing(x) = x === nothing ? missing : (x isa AbstractVector ? map(to_missing, x) : x)
|
|
50
|
+
predict_namedtuple(data) = (; (Symbol(k) => to_missing(v) for (k, v) in data)...)
|
|
51
|
+
|
|
52
|
+
# Rebuild a posterior Chains (latents only) from our MCMCChains-JSON wire object so
|
|
53
|
+
# it can feed Turing.predict. The varname_to_symbol map is required for VarName
|
|
54
|
+
# indexing; target outcome columns are excluded.
|
|
55
|
+
function chains_from_wire(wire, targets)
|
|
56
|
+
sz = wire["size"]
|
|
57
|
+
nIter, nParams, nChains = Int(sz[1]), Int(sz[2]), Int(sz[3])
|
|
58
|
+
flat = wire["value_flat"]
|
|
59
|
+
allnames = wire["parameters"]
|
|
60
|
+
is_target(n) = any(t -> n == t || startswith(n, t * "["), targets)
|
|
61
|
+
keep = [n for n in wire["name_map"]["parameters"] if !is_target(n)]
|
|
62
|
+
index = Dict(n => i for (i, n) in enumerate(allnames))
|
|
63
|
+
arr = Array{Float64,3}(undef, nIter, length(keep), nChains)
|
|
64
|
+
for (newp, n) in enumerate(keep)
|
|
65
|
+
p = index[n]
|
|
66
|
+
for c in 1:nChains, i in 1:nIter
|
|
67
|
+
v = flat[i + (p - 1) * nIter + (c - 1) * nIter * nParams]
|
|
68
|
+
arr[i, newp, c] = v === nothing ? NaN : Float64(v)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
syms = Symbol.(keep)
|
|
72
|
+
vmap = Dict{VarName,Symbol}(eval(:(Turing.@varname($(Meta.parse(n))))) => Symbol(n) for n in keep)
|
|
73
|
+
return MCMCChains.Chains(arr, syms, Dict(:parameters => syms); info = (; varname_to_symbol = vmap))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Build the model and sample in one call so both run in the latest world age:
|
|
77
|
+
# the model methods are defined by `include` at runtime, so a plain call from
|
|
78
|
+
# this (older) world would fail with "method too new". invokelatest fixes that.
|
|
79
|
+
function build_and_sample(entry, data, sampler, draws, chains, rng)
|
|
80
|
+
model = entry(data)
|
|
81
|
+
# Sample into Turing's default chain type (a FlexiChain), then convert to
|
|
82
|
+
# MCMCChains for serialization.
|
|
83
|
+
chn = sample(rng, model, sampler, MCMCSerial(), draws, chains; progress = false)
|
|
84
|
+
return MCMCChains.Chains(chn)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# JuliaBUGS compiles the model at runtime, so building and sampling must run in
|
|
88
|
+
# separate latest-world frames; merging them reintroduces a world-age error. The
|
|
89
|
+
# result is already an MCMCChains.Chains, so it is not re-wrapped.
|
|
90
|
+
function sample_bugs(model, sampler, warmup, draws, chains, rng)
|
|
91
|
+
return JuliaBUGS.AbstractMCMC.sample(
|
|
92
|
+
rng, model, sampler, JuliaBUGS.AbstractMCMC.MCMCSerial(), draws, chains;
|
|
93
|
+
chain_type = MCMCChains.Chains, n_adapts = warmup, discard_initial = warmup, progress = false,
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Build the exact wire object parseMCMCChainsJson consumes: value_flat is indexed
|
|
98
|
+
# i + p*nIter + c*nIter*nParams (column-major over [iteration, parameter, chain]).
|
|
99
|
+
function chains_to_wire(chn)
|
|
100
|
+
arr = Array(chn.value)
|
|
101
|
+
nIter, nParams, nChains = size(arr)
|
|
102
|
+
flat = Vector{Union{Float64,Nothing}}(undef, nIter * nParams * nChains)
|
|
103
|
+
for c in 1:nChains, p in 1:nParams, i in 1:nIter
|
|
104
|
+
v = arr[i, p, c]
|
|
105
|
+
flat[i + (p - 1) * nIter + (c - 1) * nIter * nParams] = v === missing ? nothing : Float64(v)
|
|
106
|
+
end
|
|
107
|
+
return Dict(
|
|
108
|
+
"size" => [nIter, nParams, nChains],
|
|
109
|
+
"value_flat" => flat,
|
|
110
|
+
"parameters" => string.(names(chn)),
|
|
111
|
+
"name_map" => Dict(
|
|
112
|
+
"parameters" => string.(get(chn.name_map, :parameters, Symbol[])),
|
|
113
|
+
"internals" => string.(get(chn.name_map, :internals, Symbol[])),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
function provenance()
|
|
119
|
+
packages = Dict{String,String}()
|
|
120
|
+
for (_, info) in Pkg.dependencies()
|
|
121
|
+
if info.name in
|
|
122
|
+
("Turing", "MCMCChains", "DynamicPPL", "AbstractMCMC", "JuliaBUGS", "AdvancedHMC", "ForwardDiff", "ADTypes") &&
|
|
123
|
+
info.version !== nothing
|
|
124
|
+
packages[info.name] = string(info.version)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
manifest = joinpath(dirname(Base.active_project()), "Manifest.toml")
|
|
128
|
+
return Dict(
|
|
129
|
+
"julia_version" => string(VERSION),
|
|
130
|
+
"packages" => packages,
|
|
131
|
+
"manifest_sha256" => isfile(manifest) ? bytes2hex(sha256(read(manifest))) : "",
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
function main()
|
|
136
|
+
request = JSON.parsefile(ARGS[1])
|
|
137
|
+
out = request["out"]
|
|
138
|
+
mode = get(request, "mode", "fit")
|
|
139
|
+
backend = get(get(request, "backend", Dict()), "id", "turing")
|
|
140
|
+
stage = "compile"
|
|
141
|
+
try
|
|
142
|
+
Random.seed!(Int(request["seed"]))
|
|
143
|
+
rng = StableRNG(Int(request["seed"]))
|
|
144
|
+
include(abspath(request["model"]["file"]))
|
|
145
|
+
entry = getfield(Main, Symbol(request["model"]["entry"]))
|
|
146
|
+
|
|
147
|
+
wire = if mode == "predict"
|
|
148
|
+
backend == "turing" || error("predict is not yet supported for backend $backend")
|
|
149
|
+
model = Base.invokelatest(entry, predict_namedtuple(request["data"]))
|
|
150
|
+
stage = "load_samples"
|
|
151
|
+
rchn = chains_from_wire(JSON.parsefile(request["samples"]), request["predict"]["targets"])
|
|
152
|
+
stage = "predict"
|
|
153
|
+
pp = Base.invokelatest(predict, rng, model, rchn)
|
|
154
|
+
isempty(names(pp)) &&
|
|
155
|
+
error("no variables predicted; check that [predict].targets name unconditioned outcomes")
|
|
156
|
+
chains_to_wire(pp)
|
|
157
|
+
else
|
|
158
|
+
data = backend == "juliabugs" ? bugs_namedtuple(request["data"]) : to_namedtuple(request["data"])
|
|
159
|
+
sampler, warmup = build_sampler(request["sampler"], backend)
|
|
160
|
+
draws = Int(request["sampler"]["draws"])
|
|
161
|
+
chains = Int(get(request["sampler"], "chains", 4))
|
|
162
|
+
stage = "sample"
|
|
163
|
+
chn = if backend == "juliabugs"
|
|
164
|
+
model = Base.invokelatest(entry, data)
|
|
165
|
+
Base.invokelatest(sample_bugs, model, sampler, warmup, draws, chains, rng)
|
|
166
|
+
else
|
|
167
|
+
Base.invokelatest(build_and_sample, entry, data, sampler, draws, chains, rng)
|
|
168
|
+
end
|
|
169
|
+
chains_to_wire(chn)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
stage = "write"
|
|
173
|
+
tmp = out * ".tmp"
|
|
174
|
+
open(tmp, "w") do io
|
|
175
|
+
JSON.print(io, wire)
|
|
176
|
+
end
|
|
177
|
+
mv(tmp, out; force = true)
|
|
178
|
+
|
|
179
|
+
println(JSON.json(provenance()))
|
|
180
|
+
catch err
|
|
181
|
+
println(stderr, JSON.json(Dict("error" => sprint(showerror, err), "stage" => stage)))
|
|
182
|
+
exit(1)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
main()
|
package/dist/index.cjs
CHANGED
|
@@ -20,22 +20,41 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
addVersion: () => addVersion,
|
|
24
|
+
assertVersionsInstalled: () => assertVersionsInstalled,
|
|
23
25
|
detectJulia: () => detectJulia,
|
|
24
26
|
detectJuliaup: () => detectJuliaup,
|
|
25
|
-
|
|
27
|
+
ensureProject: () => ensureProject,
|
|
28
|
+
gcVersions: () => gcVersions,
|
|
29
|
+
juliaEngine: () => juliaEngine,
|
|
30
|
+
juliaupInstallCommand: () => juliaupInstallCommand,
|
|
31
|
+
listVersions: () => listVersions,
|
|
32
|
+
managedProjectDir: () => managedProjectDir,
|
|
33
|
+
managedProjectReady: () => managedProjectReady,
|
|
34
|
+
planSetup: () => planSetup,
|
|
35
|
+
predictData: () => predictData,
|
|
36
|
+
removeVersion: () => removeVersion,
|
|
37
|
+
resolveVersion: () => resolveVersion,
|
|
38
|
+
runDoctor: () => runDoctor,
|
|
39
|
+
runFit: () => runFit,
|
|
40
|
+
runMatrix: () => runMatrix,
|
|
41
|
+
runPredict: () => runPredict,
|
|
42
|
+
runSetup: () => runSetup,
|
|
43
|
+
setDefaultVersion: () => setDefaultVersion,
|
|
44
|
+
updateVersion: () => updateVersion
|
|
26
45
|
});
|
|
27
46
|
module.exports = __toCommonJS(index_exports);
|
|
28
47
|
|
|
48
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.15_typescript@6.0.3/node_modules/tsup/assets/cjs_shims.js
|
|
49
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
50
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
51
|
+
|
|
29
52
|
// src/environment.ts
|
|
30
|
-
var import_node_child_process = require("child_process");
|
|
31
53
|
var import_node_os = require("os");
|
|
32
54
|
var import_node_path = require("path");
|
|
33
|
-
var
|
|
34
|
-
var
|
|
35
|
-
var defaultRunner =
|
|
36
|
-
const { stdout } = await execFileAsync(command, args, { timeout: 1e4 });
|
|
37
|
-
return stdout;
|
|
38
|
-
};
|
|
55
|
+
var import_engine = require("@mcmcjs/engine");
|
|
56
|
+
var import_engine2 = require("@mcmcjs/engine");
|
|
57
|
+
var defaultRunner = (0, import_engine.createRunner)();
|
|
39
58
|
function candidates(binary) {
|
|
40
59
|
return [(0, import_node_path.join)((0, import_node_os.homedir)(), ".juliaup", "bin", binary), binary];
|
|
41
60
|
}
|
|
@@ -62,10 +81,465 @@ async function runDoctor(runner) {
|
|
|
62
81
|
const [juliaup, julia] = await Promise.all([detectJuliaup(runner), detectJulia(runner)]);
|
|
63
82
|
return { juliaup, julia, ready: julia.found };
|
|
64
83
|
}
|
|
84
|
+
|
|
85
|
+
// src/engine.ts
|
|
86
|
+
var JULIAUP_URL = "https://github.com/JuliaLang/juliaup";
|
|
87
|
+
var juliaEngine = {
|
|
88
|
+
id: "julia",
|
|
89
|
+
displayName: "Julia",
|
|
90
|
+
capabilities: { setup: true, versions: true, fit: true, predict: true },
|
|
91
|
+
async doctor(ctx) {
|
|
92
|
+
const report = await runDoctor(ctx.run);
|
|
93
|
+
return {
|
|
94
|
+
engineId: "julia",
|
|
95
|
+
ready: report.ready,
|
|
96
|
+
tools: [
|
|
97
|
+
{ name: "juliaup", ...report.juliaup },
|
|
98
|
+
{ name: "julia", ...report.julia }
|
|
99
|
+
],
|
|
100
|
+
hint: report.ready ? void 0 : `Julia not found. Install it with juliaup (${JULIAUP_URL}), or run \`mcmc setup\`.`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/fit.ts
|
|
106
|
+
var import_node_fs = require("fs");
|
|
107
|
+
var import_node_os2 = require("os");
|
|
108
|
+
var import_node_path3 = require("path");
|
|
109
|
+
var import_core = require("@mcmcjs/core");
|
|
110
|
+
|
|
111
|
+
// src/runner-common.ts
|
|
112
|
+
var import_node_crypto = require("crypto");
|
|
113
|
+
var import_node_path2 = require("path");
|
|
114
|
+
var import_node_url = require("url");
|
|
115
|
+
function driverPath() {
|
|
116
|
+
return (0, import_node_path2.join)((0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl)), "driver.jl");
|
|
117
|
+
}
|
|
118
|
+
function sha256(text) {
|
|
119
|
+
return (0, import_node_crypto.createHash)("sha256").update(text).digest("hex");
|
|
120
|
+
}
|
|
121
|
+
function lastJsonLine(text) {
|
|
122
|
+
for (const line of text.trimEnd().split("\n").reverse()) {
|
|
123
|
+
const trimmed = line.trim();
|
|
124
|
+
if (!trimmed.startsWith("{")) continue;
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(trimmed);
|
|
127
|
+
} catch {
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
var STAGES = /* @__PURE__ */ new Set([
|
|
134
|
+
"compile",
|
|
135
|
+
"sample",
|
|
136
|
+
"load_samples",
|
|
137
|
+
"predict",
|
|
138
|
+
"write",
|
|
139
|
+
"spawn"
|
|
140
|
+
]);
|
|
141
|
+
function toStage(value) {
|
|
142
|
+
return typeof value === "string" && STAGES.has(value) ? value : void 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/fit.ts
|
|
146
|
+
async function runFit(spec, resolved, io) {
|
|
147
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
148
|
+
const start = performance.now();
|
|
149
|
+
const runtimeRequested = spec.backend.version;
|
|
150
|
+
const tmp = io.tmpDir ?? (0, import_node_fs.mkdtempSync)((0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "mcmcjs-fit-"));
|
|
151
|
+
const requestPath = (0, import_node_path3.join)(tmp, "request.json");
|
|
152
|
+
(0, import_node_fs.writeFileSync)(
|
|
153
|
+
requestPath,
|
|
154
|
+
JSON.stringify({
|
|
155
|
+
schema_version: spec.schema_version,
|
|
156
|
+
backend: { id: spec.backend.id },
|
|
157
|
+
model: { file: spec.modelPath, entry: spec.model.entry },
|
|
158
|
+
data: spec.data,
|
|
159
|
+
sampler: spec.sampler,
|
|
160
|
+
seed: spec.seed,
|
|
161
|
+
out: io.outPath
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
const args = [
|
|
165
|
+
...resolved.args,
|
|
166
|
+
"--startup-file=no",
|
|
167
|
+
`--project=${io.projectDir}`,
|
|
168
|
+
driverPath(),
|
|
169
|
+
requestPath
|
|
170
|
+
];
|
|
171
|
+
const { stdout, stderr, code } = await io.spawn(resolved.command, args);
|
|
172
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
173
|
+
if (code !== 0 || !(0, import_node_fs.existsSync)(io.outPath)) {
|
|
174
|
+
const failure = lastJsonLine(stderr);
|
|
175
|
+
const stage = toStage(failure?.stage);
|
|
176
|
+
const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
|
|
177
|
+
return { status: "error", runtimeRequested, elapsedMs, stage, error };
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
(0, import_core.parseSamples)((0, import_node_fs.readFileSync)(io.outPath, "utf8"));
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return {
|
|
183
|
+
status: "error",
|
|
184
|
+
runtimeRequested,
|
|
185
|
+
elapsedMs,
|
|
186
|
+
stage: "write",
|
|
187
|
+
error: `samples file did not parse: ${error.message}`
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const provenance = lastJsonLine(stdout);
|
|
191
|
+
const julia = provenance?.julia_version;
|
|
192
|
+
const record = {
|
|
193
|
+
schema_version: import_core.RUN_RECORD_SCHEMA_VERSION,
|
|
194
|
+
spec_hash: spec.specHash,
|
|
195
|
+
seed: spec.seed,
|
|
196
|
+
backend: { id: spec.backend.id, runtime: spec.backend.runtime },
|
|
197
|
+
runtime: { requested: runtimeRequested, actual: julia, path: resolved.command },
|
|
198
|
+
manifest_sha256: provenance?.manifest_sha256,
|
|
199
|
+
packages: provenance?.packages,
|
|
200
|
+
model_sha256: (0, import_node_fs.existsSync)(spec.modelPath) ? sha256((0, import_node_fs.readFileSync)(spec.modelPath, "utf8")) : void 0,
|
|
201
|
+
data_sha256: sha256((0, import_core.canonicalJson)(spec.data)),
|
|
202
|
+
samples_file: io.outPath,
|
|
203
|
+
started_at: startedAt,
|
|
204
|
+
elapsed_ms: elapsedMs
|
|
205
|
+
};
|
|
206
|
+
(0, import_node_fs.writeFileSync)(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
|
|
207
|
+
`);
|
|
208
|
+
return {
|
|
209
|
+
status: "ok",
|
|
210
|
+
samplesFile: io.outPath,
|
|
211
|
+
runtimeRequested,
|
|
212
|
+
runtimeActual: julia,
|
|
213
|
+
elapsedMs
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/predict.ts
|
|
218
|
+
var import_node_fs2 = require("fs");
|
|
219
|
+
var import_node_os3 = require("os");
|
|
220
|
+
var import_node_path4 = require("path");
|
|
221
|
+
var import_core2 = require("@mcmcjs/core");
|
|
222
|
+
function predictData(spec) {
|
|
223
|
+
const predict = spec.predict;
|
|
224
|
+
if (!predict) throw new Error("spec has no [predict] block");
|
|
225
|
+
const data = { ...spec.data, ...predict.data ?? {} };
|
|
226
|
+
const blank = (value) => Array.isArray(value) ? value.map(blank) : null;
|
|
227
|
+
for (const target of predict.targets) {
|
|
228
|
+
data[target] = blank(data[target]);
|
|
229
|
+
}
|
|
230
|
+
return data;
|
|
231
|
+
}
|
|
232
|
+
async function runPredict(spec, resolved, io) {
|
|
233
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
234
|
+
const start = performance.now();
|
|
235
|
+
const runtimeRequested = spec.backend.version;
|
|
236
|
+
const data = predictData(spec);
|
|
237
|
+
const tmp = io.tmpDir ?? (0, import_node_fs2.mkdtempSync)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "mcmcjs-predict-"));
|
|
238
|
+
const requestPath = (0, import_node_path4.join)(tmp, "request.json");
|
|
239
|
+
(0, import_node_fs2.writeFileSync)(
|
|
240
|
+
requestPath,
|
|
241
|
+
JSON.stringify({
|
|
242
|
+
mode: "predict",
|
|
243
|
+
schema_version: spec.schema_version,
|
|
244
|
+
backend: { id: spec.backend.id },
|
|
245
|
+
model: { file: spec.modelPath, entry: spec.model.entry },
|
|
246
|
+
data,
|
|
247
|
+
predict: { targets: spec.predict?.targets ?? [] },
|
|
248
|
+
samples: io.samplesPath,
|
|
249
|
+
seed: spec.seed,
|
|
250
|
+
out: io.outPath
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
const args = [
|
|
254
|
+
...resolved.args,
|
|
255
|
+
"--startup-file=no",
|
|
256
|
+
`--project=${io.projectDir}`,
|
|
257
|
+
driverPath(),
|
|
258
|
+
requestPath
|
|
259
|
+
];
|
|
260
|
+
const { stdout, stderr, code } = await io.spawn(resolved.command, args);
|
|
261
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
262
|
+
if (code !== 0 || !(0, import_node_fs2.existsSync)(io.outPath)) {
|
|
263
|
+
const failure = lastJsonLine(stderr);
|
|
264
|
+
const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
|
|
265
|
+
return { status: "error", runtimeRequested, elapsedMs, stage: toStage(failure?.stage), error };
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
(0, import_core2.parseSamples)((0, import_node_fs2.readFileSync)(io.outPath, "utf8"));
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return {
|
|
271
|
+
status: "error",
|
|
272
|
+
runtimeRequested,
|
|
273
|
+
elapsedMs,
|
|
274
|
+
stage: "write",
|
|
275
|
+
error: `samples file did not parse: ${error.message}`
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const provenance = lastJsonLine(stdout);
|
|
279
|
+
const julia = provenance?.julia_version;
|
|
280
|
+
const record = {
|
|
281
|
+
schema_version: import_core2.RUN_RECORD_SCHEMA_VERSION,
|
|
282
|
+
spec_hash: spec.specHash,
|
|
283
|
+
seed: spec.seed,
|
|
284
|
+
backend: { id: spec.backend.id, runtime: spec.backend.runtime },
|
|
285
|
+
runtime: { requested: runtimeRequested, actual: julia, path: resolved.command },
|
|
286
|
+
manifest_sha256: provenance?.manifest_sha256,
|
|
287
|
+
packages: provenance?.packages,
|
|
288
|
+
model_sha256: (0, import_node_fs2.existsSync)(spec.modelPath) ? sha256((0, import_node_fs2.readFileSync)(spec.modelPath, "utf8")) : void 0,
|
|
289
|
+
data_sha256: sha256((0, import_core2.canonicalJson)(data)),
|
|
290
|
+
posterior_samples: io.samplesPath,
|
|
291
|
+
posterior_samples_sha256: (0, import_node_fs2.existsSync)(io.samplesPath) ? sha256((0, import_node_fs2.readFileSync)(io.samplesPath, "utf8")) : void 0,
|
|
292
|
+
samples_file: io.outPath,
|
|
293
|
+
started_at: startedAt,
|
|
294
|
+
elapsed_ms: elapsedMs
|
|
295
|
+
};
|
|
296
|
+
(0, import_node_fs2.writeFileSync)(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
|
|
297
|
+
`);
|
|
298
|
+
return {
|
|
299
|
+
status: "ok",
|
|
300
|
+
samplesFile: io.outPath,
|
|
301
|
+
runtimeRequested,
|
|
302
|
+
runtimeActual: julia,
|
|
303
|
+
elapsedMs
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/project.ts
|
|
308
|
+
var import_node_fs3 = require("fs");
|
|
309
|
+
var import_node_os4 = require("os");
|
|
310
|
+
var import_node_path5 = require("path");
|
|
311
|
+
var import_engine3 = require("@mcmcjs/engine");
|
|
312
|
+
var PACKAGES = [
|
|
313
|
+
"Turing",
|
|
314
|
+
"MCMCChains",
|
|
315
|
+
"JuliaBUGS",
|
|
316
|
+
"AdvancedHMC",
|
|
317
|
+
"ForwardDiff",
|
|
318
|
+
"JSON",
|
|
319
|
+
"StableRNGs"
|
|
320
|
+
];
|
|
321
|
+
function managedProjectDir() {
|
|
322
|
+
const base = process.env.XDG_DATA_HOME || (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".local", "share");
|
|
323
|
+
return (0, import_node_path5.join)(base, "mcmcjs", "julia", "env");
|
|
324
|
+
}
|
|
325
|
+
function sentinelPath(dir) {
|
|
326
|
+
return (0, import_node_path5.join)(dir, ".mcmcjs-packages.json");
|
|
327
|
+
}
|
|
328
|
+
function provisioned(dir) {
|
|
329
|
+
try {
|
|
330
|
+
return (0, import_node_fs3.readFileSync)(sentinelPath(dir), "utf8").trim() === JSON.stringify([...PACKAGES].sort());
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function managedProjectReady(dir = managedProjectDir()) {
|
|
336
|
+
return (0, import_node_fs3.existsSync)((0, import_node_path5.join)(dir, "Project.toml")) && provisioned(dir);
|
|
337
|
+
}
|
|
338
|
+
async function ensureProject(juliaBin, run = (0, import_engine3.createRunner)(30 * 6e4), dir = managedProjectDir()) {
|
|
339
|
+
if (managedProjectReady(dir)) return dir;
|
|
340
|
+
(0, import_node_fs3.mkdirSync)(dir, { recursive: true });
|
|
341
|
+
const code = `using Pkg; Pkg.add(${JSON.stringify(PACKAGES)}); Pkg.precompile()`;
|
|
342
|
+
await run(juliaBin, ["--startup-file=no", `--project=${dir}`, "-e", code]);
|
|
343
|
+
(0, import_node_fs3.writeFileSync)(sentinelPath(dir), JSON.stringify([...PACKAGES].sort()));
|
|
344
|
+
return dir;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/run-matrix.ts
|
|
348
|
+
var import_node_path6 = require("path");
|
|
349
|
+
async function runMatrix(spec, versions, io) {
|
|
350
|
+
const entries = [];
|
|
351
|
+
for (const version of versions) {
|
|
352
|
+
let entry;
|
|
353
|
+
try {
|
|
354
|
+
const resolved = await io.resolve(version);
|
|
355
|
+
const versioned = { ...spec, backend: { ...spec.backend, version } };
|
|
356
|
+
const result = await runFit(versioned, resolved, {
|
|
357
|
+
spawn: io.spawn,
|
|
358
|
+
projectDir: io.projectDir,
|
|
359
|
+
outPath: (0, import_node_path6.join)(io.outDir, `${version}.samples.json`)
|
|
360
|
+
});
|
|
361
|
+
entry = {
|
|
362
|
+
version,
|
|
363
|
+
status: result.status,
|
|
364
|
+
samplesFile: result.samplesFile,
|
|
365
|
+
runtimeActual: result.runtimeActual,
|
|
366
|
+
elapsedMs: result.elapsedMs,
|
|
367
|
+
stage: result.stage,
|
|
368
|
+
error: result.error
|
|
369
|
+
};
|
|
370
|
+
} catch (error) {
|
|
371
|
+
entry = { version, status: "error", elapsedMs: 0, error: error.message };
|
|
372
|
+
}
|
|
373
|
+
entries.push(entry);
|
|
374
|
+
if (entry.status === "error" && !io.keepGoing) break;
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
entries,
|
|
378
|
+
ok: entries.length === versions.length && entries.every((e) => e.status === "ok")
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/setup.ts
|
|
383
|
+
var JULIAUP_INSTALL_URL = "https://install.julialang.org";
|
|
384
|
+
function juliaupInstallCommand(platform) {
|
|
385
|
+
if (platform === "win32") {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
return { command: "sh", args: ["-c", `curl -fsSL ${JULIAUP_INSTALL_URL} | sh -s -- --yes`] };
|
|
389
|
+
}
|
|
390
|
+
function planSetup(report, platform) {
|
|
391
|
+
if (report.julia.found) return [];
|
|
392
|
+
if (report.juliaup.found) {
|
|
393
|
+
return [
|
|
394
|
+
{
|
|
395
|
+
tool: "julia",
|
|
396
|
+
label: "install the latest stable Julia via juliaup",
|
|
397
|
+
command: { command: report.juliaup.path ?? "juliaup", args: ["add", "release"] }
|
|
398
|
+
}
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
return [
|
|
402
|
+
{
|
|
403
|
+
tool: "juliaup",
|
|
404
|
+
label: "install juliaup (this also installs Julia)",
|
|
405
|
+
command: juliaupInstallCommand(platform)
|
|
406
|
+
}
|
|
407
|
+
];
|
|
408
|
+
}
|
|
409
|
+
async function runSetup(options = {}) {
|
|
410
|
+
const {
|
|
411
|
+
runner,
|
|
412
|
+
installer = (0, import_engine2.createRunner)(15 * 6e4),
|
|
413
|
+
platform = process.platform,
|
|
414
|
+
dryRun = false
|
|
415
|
+
} = options;
|
|
416
|
+
const before = await runDoctor(runner);
|
|
417
|
+
if (before.ready) return { ...before, steps: [] };
|
|
418
|
+
const plan = planSetup(before, platform);
|
|
419
|
+
if (dryRun) {
|
|
420
|
+
return {
|
|
421
|
+
...before,
|
|
422
|
+
steps: plan.map((step) => ({ ...step, status: step.command ? "skipped" : "unsupported" }))
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const steps = [];
|
|
426
|
+
for (const step of plan) {
|
|
427
|
+
if (!step.command) {
|
|
428
|
+
steps.push({ ...step, status: "unsupported" });
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
await installer(step.command.command, step.command.args);
|
|
433
|
+
steps.push({ ...step, status: "ran" });
|
|
434
|
+
} catch (error) {
|
|
435
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
436
|
+
steps.push({ ...step, status: "failed", detail });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const after = await runDoctor(runner);
|
|
440
|
+
return { juliaup: after.juliaup, julia: after.julia, ready: after.ready, steps };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/versions.ts
|
|
444
|
+
function parseChannel(value) {
|
|
445
|
+
const c = value;
|
|
446
|
+
if (!c || typeof c.Name !== "string" || typeof c.File !== "string") {
|
|
447
|
+
throw new Error("unexpected `juliaup api getconfig1` channel shape");
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
Name: c.Name,
|
|
451
|
+
File: c.File,
|
|
452
|
+
Args: Array.isArray(c.Args) ? c.Args : void 0,
|
|
453
|
+
Version: typeof c.Version === "string" ? c.Version : void 0,
|
|
454
|
+
Arch: typeof c.Arch === "string" ? c.Arch : void 0
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function parseGetconfig1(raw) {
|
|
458
|
+
let data;
|
|
459
|
+
try {
|
|
460
|
+
data = JSON.parse(raw);
|
|
461
|
+
} catch {
|
|
462
|
+
throw new Error("could not parse `juliaup api getconfig1` output as JSON");
|
|
463
|
+
}
|
|
464
|
+
const obj = data;
|
|
465
|
+
if (!obj || typeof obj !== "object" || !Array.isArray(obj.OtherChannels)) {
|
|
466
|
+
throw new Error("unexpected `juliaup api getconfig1` output");
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
DefaultChannel: obj.DefaultChannel ? parseChannel(obj.DefaultChannel) : null,
|
|
470
|
+
OtherChannels: obj.OtherChannels.map(parseChannel)
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function toVersion(channel, isDefault) {
|
|
474
|
+
return { id: channel.Name, version: channel.Version, path: channel.File || void 0, isDefault };
|
|
475
|
+
}
|
|
476
|
+
async function listVersions(bin, run) {
|
|
477
|
+
const config = parseGetconfig1(await run(bin, ["api", "getconfig1"]));
|
|
478
|
+
const versions = [];
|
|
479
|
+
if (config.DefaultChannel) versions.push(toVersion(config.DefaultChannel, true));
|
|
480
|
+
for (const channel of config.OtherChannels) versions.push(toVersion(channel, false));
|
|
481
|
+
return versions;
|
|
482
|
+
}
|
|
483
|
+
async function addVersion(bin, channel, opts, run) {
|
|
484
|
+
await run(bin, ["add", channel]);
|
|
485
|
+
if (opts.default) await run(bin, ["default", channel]);
|
|
486
|
+
}
|
|
487
|
+
async function removeVersion(bin, channel, run) {
|
|
488
|
+
await run(bin, ["remove", channel]);
|
|
489
|
+
}
|
|
490
|
+
async function setDefaultVersion(bin, channel, run) {
|
|
491
|
+
await run(bin, ["default", channel]);
|
|
492
|
+
}
|
|
493
|
+
async function updateVersion(bin, channel, run) {
|
|
494
|
+
await run(bin, channel ? ["update", channel] : ["update"]);
|
|
495
|
+
}
|
|
496
|
+
async function gcVersions(bin, run) {
|
|
497
|
+
await run(bin, ["gc"]);
|
|
498
|
+
}
|
|
499
|
+
async function resolveVersion(bin, channel, run) {
|
|
500
|
+
const config = parseGetconfig1(await run(bin, ["api", "getconfig1"]));
|
|
501
|
+
const all = config.DefaultChannel ? [config.DefaultChannel, ...config.OtherChannels] : config.OtherChannels;
|
|
502
|
+
const match = all.find((c) => c.Name === channel);
|
|
503
|
+
if (!match?.File) {
|
|
504
|
+
throw new Error(
|
|
505
|
+
`Julia version "${channel}" is not installed. Add it with: mcmc julia version add ${channel}`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
return { command: match.File, args: match.Args ?? [] };
|
|
509
|
+
}
|
|
510
|
+
function assertVersionsInstalled(channels, installed) {
|
|
511
|
+
const have = new Set(installed.map((v) => v.id));
|
|
512
|
+
const missing = channels.filter((c) => !have.has(c));
|
|
513
|
+
if (missing.length > 0) {
|
|
514
|
+
const fixes = missing.map((m) => `mcmc julia version add ${m}`).join("; ");
|
|
515
|
+
throw new Error(
|
|
516
|
+
`Julia version(s) not installed: ${missing.join(", ")}. Install with: ${fixes}`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
65
520
|
// Annotate the CommonJS export names for ESM import in node:
|
|
66
521
|
0 && (module.exports = {
|
|
522
|
+
addVersion,
|
|
523
|
+
assertVersionsInstalled,
|
|
67
524
|
detectJulia,
|
|
68
525
|
detectJuliaup,
|
|
69
|
-
|
|
526
|
+
ensureProject,
|
|
527
|
+
gcVersions,
|
|
528
|
+
juliaEngine,
|
|
529
|
+
juliaupInstallCommand,
|
|
530
|
+
listVersions,
|
|
531
|
+
managedProjectDir,
|
|
532
|
+
managedProjectReady,
|
|
533
|
+
planSetup,
|
|
534
|
+
predictData,
|
|
535
|
+
removeVersion,
|
|
536
|
+
resolveVersion,
|
|
537
|
+
runDoctor,
|
|
538
|
+
runFit,
|
|
539
|
+
runMatrix,
|
|
540
|
+
runPredict,
|
|
541
|
+
runSetup,
|
|
542
|
+
setDefaultVersion,
|
|
543
|
+
updateVersion
|
|
70
544
|
});
|
|
71
545
|
//# sourceMappingURL=index.cjs.map
|