Modules and contexts

A Context owns source snapshots and an explicit registry of module names. Use one context for definitions that belong together.

Compose inline modules

Register a definition under a logical filename. A later module can load it through ordinary FlatPPL syntax, even when neither definition has a file.

from flatppl import Context, flatppl

ctx = Context()
model = flatppl(r"""
    steve = Normal(mu = 0.0, sigma = 1.0)
""", context=ctx)
ctx.register("model.flatppl", model)

query = flatppl(r"""
    m = load_module("model.flatppl")
    x = elementof(reals)
    inputs = x
    outputs = logdensityof(m.steve, x)
""", context=ctx)

logdensity = query.compile()
print(round(float(logdensity(x=1.0)), 5))
-1.41894

steve is an ordinary binding name. Neither posterior nor any other name has special meaning to the wrapper. Models and queries are both Module objects. Compile the module whose explicit signature describes the computation you want.

Inspect bindings

binding = model.steve
print(binding.name)
print(binding is model.bindings["steve"])
steve
True

A binding handle identifies a definition and exposes inferred metadata. It is not a Python value or an expression builder. Evaluate a binding by naming it in a query’s outputs, then compile the query.

Use module.bindings["compile"] when a FlatPPL name collides with a Python attribute. The mapping contains every public binding exposed by the compiler. These names are Python attributes of a module, so attribute access never returns a binding with one of them: bindings, compile, constant, context, downstream, graph, math, pack, parameters, pyhf, query, save, set, slice, source, source_name and upstream. Later releases add names to this list, so prefer module.bindings[name] in library code. dir(module) lists the bindings that attribute access reaches.

A binding reports what inference knows about it: its kind, domain, shape, size, its source text and location, and the public bindings it depends on. module.parameters collects the bindings written as elementof(set), and module.constant(name) reads a literal binding back as a Python value.

from flatppl import flatppl

module = flatppl("""
    mu = elementof(reals)
    sigma = elementof(posreals)
    y ~ Normal(mu = mu, sigma = sigma)
    prior_scale = [1.0, 2.0]
""")
print(module.y.kind, module.y.dependencies)
print(list(module.parameters))
print(module.constant("prior_scale"))
draw ('mu', 'sigma')
['mu', 'sigma']
[1. 2.]

module.graph() returns the dependency graph, and module.upstream(name) and module.downstream(name) follow it transitively. module.slice(outputs) keeps what the outputs reach, and module.math() renders every binding as TeX, MathML or Typst. The inspection example walks through them on one model.

Query from Python

module.query(outputs) writes the query module for you. Outputs are binding names or expressions over the model’s bindings, and the free parameters they depend on become one input. The query loads the model under a fresh name in the same context, so each call adds a registry entry.

from flatppl import flatppl

model = flatppl("""
    mu = elementof(reals)
    sigma = elementof(posreals)
    upper = mu + 2.0 * sigma
""")
query = model.query("upper")
print(query.source)
print(float(query.compile()({"mu": 1.0, "sigma": 0.5})))
point = elementof(cartprod(mu = reals, sigma = posreals))
_model = load_module("flatppl-query/model-0.flatppl", mu = point.mu, sigma = point.sigma)
upper = _model.upper
inputs = point
outputs = upper

2.0

layout="flat" passes the parameters as one real vector theta, and gradient=True returns the Jacobian of the outputs with their values. A draw cannot be an output, because a loaded module hides stochastic bindings. The query example shows both options.

Load files

Load a file or ZIP directly:

from flatppl import load

model = load("model.flatppl")
# Or: model = load("model.flatppl.zip")

load creates a fresh context. Pass context=ctx or use ctx.load(path) to share an existing context. Loading a directory selects its main.flatppl. Both strings and Path objects work. Imports resolve relative to the loaded file.

For inline code in a Python script, supply the script’s path:

from flatppl import flatppl

query = flatppl(r"""
    m = load_module("model.flatppl")
    x = elementof(reals)
    inputs = x
    outputs = logdensityof(m.steve, x)
""", source_path=__file__)

Save a model.flatppl containing steve = Normal(0.0, 1.0) beside this script. In a notebook, pass an explicit path to the host source location instead of __file__.

For relative registry imports, set a logical origin such as name="models/query.flatppl". Then load_module("model.flatppl") can resolve a definition registered as "models/model.flatppl".

name sets an origin. It does not register the module automatically. Registering an alias does not change the definition’s original import paths. An inline import that matches different registry and file definitions fails as ambiguous.

Portable ZIP bundles

module.save(path) writes the module’s source, imported modules, and referenced data as a standard ZIP. load(path) reopens it in a fresh context. Both accept strings and Path objects. Saving replaces an existing file. This also works for modules composed through the context registry. Bound constants and slices retain their effective definitions.

The runnable regression analysis writes a Python data dictionary to JSON, defines a model and query with explicit inputs and outputs, then saves and reloads the ZIP. It compiles the restored query, fits the MAP coefficients, and checks them against the exact Gaussian solution. The page includes downloads as a Python script and a notebook.

include_context=True also saves every registered module in this query’s own context and restores those names on load. The default, query.save(path), saves only the query and its dependencies. Both produce the standard FlatPPL ZIP with the query as main.flatppl. Context exports add an optional JSON registry map; other engines can ignore it and load the query normally.

Equality uses cached hashes of source, imports and data. File locations and ZIP compression do not affect it. Context equality compares registered names and their content, excluding anonymous modules and caches. The first comparison reads any data not yet loaded. It checks content identity, not mathematical equivalence or Python compile options.

For bytes instead of a file, use archive = query.pack(include_context=True) and fp.Context().load_bundle("analysis.flatppl.zip", archive).

Opening a bundle extracts nothing. Its entry is main.flatppl; imports and data stay inside the archive, including nested bundles. Export retains source text except rewritten paths, and preserves data bytes. Missing dependencies stop export rather than producing an incomplete archive. A bundle does not include JAX, an environment, or the values of unbound inputs.

Declare version compatibility

A module may declare the FlatPPL versions it is written for with flatppl_compat = "0.1" (§04 “FlatPPL version compatibility”). These docs and the gallery follow one rule. Inline models and queries in Python omit it, because the package’s own version bound already pins the language they use. Model files that are saved, bundled or published declare it, as the ZIP example does. A query built by Module.query or Module.logdensity repeats the declaration of the module it loads.

Change data or definitions

Pass changing data through declared inputs to reuse a compiled query. Create a new Context when source definitions change.

Use bound = model.set(n=100) to fix declared external constants before a query. Register bound under a fresh logical filename. A query loading that name sees the bound sizes and values. The original model stays reusable for other sizes. See size binding.

A context never replaces a registered name or rereads an existing file snapshot. Register modules from the same context. An already loaded module retains its dependency definitions when more names enter the registry.

Each FlatPPL load_module(...) call creates an independent module instance. To share stochastic ancestors, load once and reuse that module reference. This follows the language’s module composition rules.

Import limits

Directory and ZIP bundles must keep imports and data inside their root. HTTP imports read the existing FlatPPL cache and do not fetch missing sources.