A worked example
The whole flow in R: set your key once, describe the model, compile, run, and read it back. Python is identical, with AI4BayesCode. in front of the package calls and m.method() on the sampler.
library(AI4BayesCode)
ai4bayescode_set_key(Sys.getenv("ANTHROPIC_API_KEY")) # your own key (the library rejects placeholders)
ai4bayescode_generate( # writes ./generated/MyModel.cpp
"y ~ N(X beta, sigma^2), beta ~ N(0, 10^2), sigma ~ HalfNormal(0, 1)",
output_path = "./generated")
ai4bayescode_source("./generated/MyModel.cpp") # compiles + loads MyModel
ai4bayescode_doc(MyModel) # what does the constructor want?
# keep_history = TRUE records every sweep, FALSE keeps only the latest draw
m <- new(MyModel, y, X, 1L, TRUE) # data, then seed, then keep_history
m$step(10000) # run the sampler (treat early draws as burn-in)
hist <- m$get_history() # named list of matrices, one per parameter
ai4bayescode_diagnose(hist) # R-hat / ESS table + trace, ACF, density
ai4bayescode_plot_dag(m) # model DAG
# optional: warm-start from chosen values, then re-tune NUTS if the geometry shifts
m$set_current(list(sigma = 1.0)) # overwrite the current draw
m$readapt_NUTS(500, FALSE, -1) # re-tune NUTS (R needs all three)
# run several chains in parallel for cross-chain R-hat / ESS
runs <- ai4bayescode_run_chains(
function(seed) new(MyModel, y, X, as.integer(seed), TRUE),
n_chains = 4, n_burn = 5000, n_keep = 5000)
ai4bayescode_rhat_summary(runs) # cross-chain R-hat and ESS
ai4bayescode_diagnose(runs$histories[[1]]) # full diagnostics on one chain
The package
These functions take you from a model description to a running sampler. Every R name carries the ai4bayescode_ prefix. The Python module mirrors them under AI4BayesCode..
ai4bayescode_set_key
Store your LLM provider key for the session. Providers are anthropic and openai. The key lives in the process environment only. It is never written to disk and never printed in full. With check left on, an Anthropic key gets a short streaming self-check right away, so a bad key is reported here as a warning instead of surfacing part-way through a generate run.
R ai4bayescode_set_key(key, provider = "anthropic", check = TRUE)
Python AI4BayesCode.set_key(key, provider="anthropic", check=True)
ai4bayescode_key_status
Report which provider keys are set this session.
R ai4bayescode_key_status()
Python AI4BayesCode.key_status()
ai4bayescode_models
List the supported LLMs (name, provider, and effort levels) you can pass to ai4bayescode_generate.
R ai4bayescode_models()
Python AI4BayesCode.models()
ai4bayescode_generate
Parse and validate the model description, then write a sampler .cpp to output_path. A validate-then-repair loop (up to max_attempts) keeps going until the code compiles and passes the checks. Pass API_key here, or set it once with ai4bayescode_set_key.
R ai4bayescode_generate(model_description = NULL, classname = NULL, LLM = NULL,
effort = NULL, output_path = NULL, backend = NULL, API_key = NULL,
interactive = interactive(), use_cli = FALSE, max_attempts = NULL,
priors = NULL, confirm_model = NULL)
Python AI4BayesCode.generate(model_description=None, *, classname=None, LLM=None,
effort=None, output_path=None, backend=None, API_key=None,
max_attempts=None, priors=None, confirm_model=None)
- model_description
- Plain-language model spec (text, or a path to a
.txt).
- classname
- C++ class name for the generated sampler. Asked interactively if unset, otherwise derived from the description.
- LLM, effort
- Which model writes the sampler, and how hard it thinks. Left unset (
NULL/None) they are asked interactively; see ai4bayescode_models().
- backend
- Target runtime:
"R", "Python", or "both" — one dual-module .cpp usable from both (the default when unset). Asked interactively if unset.
- output_path
- Where the generated
.cpp is written (default ./generated/).
- max_attempts
- Validate-then-repair budget (default 5). Asked interactively if unset.
- API_key
- Provider key for this call. Optional if you used
ai4bayescode_set_key.
ai4bayescode_prompt
Build the code-generation prompt without calling the LLM. It is pure and offline, so nothing is sent anywhere and nothing is asked in the console. Use it to inspect or hand-edit exactly what ai4bayescode_generate would send. It returns the system prompt, the user prompt, the class name, the backend, and the list of skill files that went in.
R ai4bayescode_prompt(model_description, backend = c("both", "R", "Python"),
output_path = "./generated", classname = NULL, priors = "noninformative",
max_attempts = 5L, include_skills = FALSE, skills = NULL,
confirm_model = FALSE)
Python AI4BayesCode.prompt(model_description, *, backend="both",
output_path="./generated", classname=None, priors="noninformative",
max_attempts=5, include_skills=False, skills=None, confirm_model=False)
ai4bayescode_source
Compile the generated .cpp against the bundled library and load it, with no paths to configure. code is either a path to a .cpp file or a string holding the source itself. R binds the sampler class into the calling environment, so new(MyModel, ...) works straight afterwards. Python returns the imported module, so keep it: mod = AI4BayesCode.source(...), then mod.MyModel(...). Set rebuild when you want to force a recompile.
R ai4bayescode_source(code, rebuild = FALSE, verbose = FALSE,
extra_cppflags = character(), extra_libs = character(),
env = parent.frame(), quiet = FALSE)
Python AI4BayesCode.source(code, *, rebuild=False, verbose=False, quiet=False)
ai4bayescode_doc
Print a usage card for a compiled sampler: the constructor arguments with their types and defaults, the model description, and the available methods. It answers “what does the constructor want?”.
R ai4bayescode_doc(x)
Python AI4BayesCode.doc(x)
In Python the same cards are also browsable as an object. AI4BayesCode.blocks exposes every bundled example as an attribute, so tab completion lists them, AI4BayesCode.blocks.BartNoise? prints that sampler’s card in IPython or Jupyter, and dir(AI4BayesCode.blocks) shows everything available. It is the Python counterpart of R’s ?BartNoise.
x is the loaded class, the class name as a string, or the path to the .cpp source. Python also accepts the loaded module.
ai4bayescode_example
Compile and load a bundled example by name, a one-line way to see a working sampler. R binds the class into the calling environment, so new(GaussianLocationScale, ...) works afterwards. Python returns the module, so keep the return value: mod = AI4BayesCode.example("GaussianLocationScale"), then mod.GaussianLocationScale(...). See ai4bayescode_list_examples() for the full set.
R ai4bayescode_example(name, env = parent.frame(), ...)
Python AI4BayesCode.example(name, *, rebuild=False, quiet=False)
R passes ... on to ai4bayescode_source, so rebuild and quiet work there too.
ai4bayescode_list_examples
List the built-in example samplers that ship with the package. Load any of them by name with ai4bayescode_example().
R ai4bayescode_list_examples()
Python AI4BayesCode.list_examples()
ai4bayescode_list_skills
List the bundled skill files, the written instructions a coding agent reads to run the AI4BayesCode workflow.
R ai4bayescode_list_skills()
Python AI4BayesCode.list_skills()
ai4bayescode_version
Report the installed AI4BayesCode version. A generated runner records it, so you know which version produced a sampler.
R ai4bayescode_version()
Python AI4BayesCode.version()
ai4bayescode_stream_check
Send a short prompt to your LLM with streaming turned on and print the reply as it arrives, so you can confirm the connection and key work before a full generate run. Anthropic keys only.
R ai4bayescode_stream_check(LLM = "claude-opus-4-8", API_key = NULL,
effort = NULL, progress = TRUE)
Python AI4BayesCode.stream_check(LLM="claude-opus-4-8", API_key=None,
effort=None, progress=True)
Contributed blocks
The core blocks ship with the package. Community blocks live in the hub registry and install on demand, the same way R installs a package from CRAN. Once a block is installed, its header is on the compile path and its skill is available to the generator, so you can use it like a built-in one. The Python module mirrors each name under AI4BayesCode..
ai4bayescode_available_blocks
List the contributed blocks you can install from the hub registry, like available.packages().
R ai4bayescode_available_blocks()
Python AI4BayesCode.available_blocks()
ai4bayescode_install_block
Install a contributed block into your library, like install.packages(). It downloads a reviewed bundle from the hub registry into a fixed per-user library at ~/.AI4BayesCode/blocks_download/, shared across R, Python, and C++, and adds the block, with any vendored dependencies, to the compile path, so the next ai4bayescode_source or ai4bayescode_generate can use it. The registry is curated and pre-validated, so installing is a download and version check, not a compile.
R ai4bayescode_install_block(name, force = FALSE, quiet = FALSE)
Python AI4BayesCode.install_block(name, force=False, quiet=False)
- name
- The block name, as listed by
ai4bayescode_available_blocks().
- force
- Reinstall even if it is already present. Off by default.
- quiet
- Skip the progress messages and the install summary. Off by default.
ai4bayescode_installed_blocks
List the contributed blocks the compiler can see, like installed.packages(). This covers both tiers. It lists the blocks you downloaded into your per-user library, and it also lists the blocks you are developing under ./blocks_local/ in the current project. A block that exists in both places is listed once, because the local copy shadows the download on the compile path.
R ai4bayescode_installed_blocks(tier = c("all", "local", "download"))
Python AI4BayesCode.installed_blocks(tier="all")
- tier
- Which tier to list.
"all" is the default and covers both. "local" is ./blocks_local/ in the current project. "download" is the per-user library.
ai4bayescode_remove_block
Remove an installed contributed block, like remove.packages(). It deletes the block from your per-user library, the folder ai4bayescode_blocks_path() reports. A block you are developing under ./blocks_local/ is left alone, so delete that folder yourself when you want it gone.
R ai4bayescode_remove_block(name)
Python AI4BayesCode.remove_block(name)
The generated sampler
Once ai4bayescode_source (or ai4bayescode_example) has loaded a sampler class, build it with your data and drive it through a small stateful interface. Every block, every composite, and the top-level sampler expose the same methods. In R they are m$method(). In Python, m.method(). Each signature below is the general form.
constructor
Build the sampler with your data. The arguments depend on the model, so run ai4bayescode_doc(MyModel) to see them. The last two are always the same: an integer RNG seed, then keep_history, which records every draw when it is on and otherwise keeps only the latest. R matches constructor arguments by position, so pass them in order. In Python they are keyword arguments and the seed is called rng_seed.
R m <- new(MyModel, y, X, 1L, TRUE)
Python m = mod.MyModel(y, X, rng_seed=1, keep_history=True)
step
Run n sweeps in one call, advancing the chain. Called with no argument it advances a single sweep. Every sweep is recorded when keep_history = TRUE, so there is no separate unrecorded burn-in. Treat the early draws as burn-in when you analyze.
R m$step()
m$step(n)
Python m.step(n_steps=1)
get_current
The current draw as a named list (R) or dict (Python), keyed by parameter. Does not advance the chain.
R m$get_current()
Python m.get_current()
set_current
Overwrite current values from a named list, to warm-start from a previous run or a chosen starting point. Keys are parameter names (and, for some blocks, the data they condition on).
R m$set_current(list(sigma = 1.0, beta = b0))
Python m.set_current({"sigma": 1.0, "beta": b0})
get_history
All stored draws as a named list of matrices, each (n_draws × dim). Feed it straight to ai4bayescode_diagnose().
R m$get_history()
Python m.get_history()
predict_at
Posterior-predictive draws at new inputs, using the recorded chain. Pass the new data as a named list (column-major flattened where the constructor took a matrix). If you do not supply all of the inputs, it still returns a partial prediction of the intermediate quantities it can reach. Returns a named list and does not mutate state.
R m$predict_at(list(X = X_new))
Python m.predict_at({"X": X_new})
get_dag
The model’s dependency graph as data (a named list of edges). ai4bayescode_plot_dag() reads this to draw the figure.
R m$get_dag()
Python m.get_dag()
readapt_NUTS
Re-tune the NUTS step size for n iterations without advancing the chain. With reset = FALSE (the default) it continues adapting from the current state. With reset = TRUE it reinitializes dual-averaging and adapts from scratch. Continuing is what you want for online learning, where new data arrives and you keep refining. Resetting is for when the target geometry has changed and the old tuning no longer fits. The mass matrix is carried through unchanged, it is not adapted here. max_tree_depth caps the tree-doubling depth per iteration, and target_accept overrides the block’s target acceptance rate for this readaptation. In both, -1 means leave the block’s own setting alone. Python fills those defaults in for you. R does not, so pass at least the first three arguments in order. This method exists only on models that contain a NUTS block.
R m$readapt_NUTS(n, reset, max_tree_depth)
m$readapt_NUTS(n, reset, max_tree_depth, target_accept)
Python m.readapt_NUTS(n, reset=False, max_tree_depth=-1, target_accept=-1.0)
freeze / unfreeze / get_frozen
Hold part of a model fixed while the rest keeps sampling. freeze takes one or more block names, the same names get_current reports, and holds those blocks at their current values, so they do not change on the steps that follow. unfreeze returns them to being sampled, and with no name given it releases all of them. get_frozen lists the blocks that are currently frozen. This is useful when you want to condition on one part of a model, or hold some values fixed while you study the rest. Set the values you want with set_current first, then freeze.
R m$freeze(c("sigma", "beta"))
m$freeze("sigma", TRUE)
m$unfreeze("sigma")
m$unfreeze()
m$get_frozen()
Python m.freeze(["sigma", "beta"], quiet=False)
m.unfreeze(["sigma"])
m.unfreeze()
m.get_frozen()
A name is matched first against the block names, then against a slot name when exactly one block exposes it, and you can always give the full "block.slot" path. Freezing something twice warns instead of failing, and quiet turns that warning off. In R it is positional, so pass TRUE in the second slot.
ai4bayescode_new_frozen
Do the set-then-freeze in one call. It builds the sampler, sets the blocks named in fixed to the values you give, and freezes them, so every following step samples the parameters left free. R takes fixed as a named list, Python as a dict. The names must be flat block names, the same ones set_current accepts. This exists in both languages.
R ai4bayescode_new_frozen(module_class, ..., fixed = list(), quiet_freeze = TRUE)
ai4bayescode_new_frozen(MyModel, y, X, fixed = list(sigma = 1.0))
Python AI4BayesCode.new_frozen(module_class, *args, fixed=None, quiet_freeze=True, **kwargs)
AI4BayesCode.new_frozen(mod.MyModel, y, X, fixed={"sigma": 1.0})
Diagnostics & DAG
These helpers sit between the two layers: they take what a sampler produces, its history and its structure, and turn it into convergence diagnostics and figures. They ship with the package, so a generated runner calls the same functions by name.
ai4bayescode_diagnose
From get_history(): a summary table (R-hat, bulk / tail ESS) plus a trace, autocorrelation, and density plot. The one call to sanity-check a run.
R ai4bayescode_diagnose(hist, n_burn = 0, plot = TRUE, order_components = FALSE)
Python AI4BayesCode.diagnose(hist, n_burn=0, plot=True, order_components=False)
ai4bayescode_plot_dag
Draw the model as a plate DAG straight from the sampler, reading its get_dag() under the hood. In R, leave out_path empty to draw to the active device, or pass a path to write a PNG. The Python helper always writes a PNG and returns its path, using a temporary directory when out_path is omitted. The plotting arguments follow each language’s own convention.
R ai4bayescode_plot_dag(model, out_path = NULL, main = NULL, width = 1600,
height = 1100, res = 150, plate = TRUE)
Python AI4BayesCode.plot_dag(model, *, out_path=None, title=None, figsize=(12, 8),
dpi=150, plate=True)
ai4bayescode_run_chains
Run several independent chains in parallel and collect them, for cross-chain R-hat and ESS. The first argument is a function that builds a fresh sampler from a seed.
R ai4bayescode_run_chains(function(seed) new(MyModel, y, X, as.integer(seed), TRUE),
n_chains = 4, n_burn = 5000, n_keep = 5000)
Python AI4BayesCode.run_chains(lambda s: mod.MyModel(y, X, rng_seed=s, keep_history=True),
n_chains=4, n_burn=5000, n_keep=5000)
ai4bayescode_rhat_summary
Take the chains that run_chains returns and report the split-R-hat and bulk-ESS convergence diagnostics for every parameter in one table. When a matrix parameter shows a high R-hat only because of label switching, it says so. This is the convergence companion to run_chains.
R ai4bayescode_rhat_summary(run, keys = NULL, drop_burn = 0, order_components = FALSE)
Python AI4BayesCode.rhat_summary(chains, keys=None, drop_burn=0, order_components=False)
rhat / ess_bulk / ess_tail / posterior_summary
The lower-level diagnostics behind those tables, computed one parameter at a time. rhat is the split-R-hat, where a value near 1.0 means the chains agree. ess_bulk and ess_tail are the effective sample sizes for the bulk and the tails of the distribution. posterior_summary returns the mean, median, standard deviation, MAD, a credible interval, R-hat, and ESS for one parameter. These are Python only.
Python AI4BayesCode.rhat(samples)
Python AI4BayesCode.ess_bulk(samples)
Python AI4BayesCode.ess_tail(samples, quantile_lo=0.05, quantile_hi=0.95)
Python AI4BayesCode.posterior_summary(samples, prob=0.90, *, chains=False)
ai4bayescode_perf_hint
Deprecated. It prints the per-sweep timing and suggests moving tightly-coupled parameters into a single joint NUTS block, but it asks you to compute and pass two numbers that run_chains already returns, and its fixed half-second-per-sweep threshold ignores the size of the model. It warns on every call and will be removed, and the generated runners no longer call it. Read the timing off the run object instead: sum(run$wall) in R, sum(c["wall"] for c in runs) in Python.
R ai4bayescode_perf_hint(wall_sec, n_sweeps_total, uses_joint_nuts = FALSE,
thresholds = list(slow_sweep_sec = 0.5)) # deprecated
Python AI4BayesCode.perf_hint(wall_sec, n_sweeps_total, uses_joint_nuts=False) # deprecated
include_path / blocks_path / examples_path / skills_path
Find the files AI4BayesCode ships, and the blocks you have installed. include_path is the C++ header directory that goes on the compile path, and it takes no arguments. examples_path and skills_path point at the bundled examples and skill files, and give one a name to get the path to a single item. Those three return an empty string when the target is not there. blocks_path is the odd one out: it is your own per-user library of installed contributed blocks at ~/.AI4BayesCode/blocks_download/, which lives outside the package and is shared across R, Python, and C++. These are mainly for build tooling and coding agents.
R ai4bayescode_include_path()
ai4bayescode_blocks_path(name = NULL)
ai4bayescode_examples_path(name = NULL)
ai4bayescode_skills_path(name = NULL)
Python AI4BayesCode.include_path()
AI4BayesCode.blocks_path(name=None)
AI4BayesCode.examples_path(name=None)
AI4BayesCode.skills_path(name=None)
AI4BayesCode.vendored_include_path() # Python only