Ferric
A Probabilistic Programming Language in Rust with a declarative syntax.
Installation
Add this to your Cargo.toml:
[]
= "0.2"
How it works
Ferric's make_model! macro declares a Bayesian model and the relationships between random
variables. Inside the macro you:
- Define random variables and their distributions using
let name : Type ~ Distribution;. - Define model constants using
const name : Type;. - Mark variables with
observeto condition the model on observed data. - Mark variables with
queryto include variables in posterior samples.
After expansion the macro produces a module containing a Model struct. Construct the model
by supplying values for the observed fields, then draw from the posterior using one of the two
sampling strategies below.
Sampling strategies
Rejection sampling — sample_iter
Valid for discrete observations only. Each call to next() draws from the prior and
discards the sample if the discrete observations don't match. Every returned Sample is an
exact draw from the posterior.
use make_model;
make_model!
Weighted sampling — weighted_sample_iter
Valid for any model, including those with continuous observations. Each call to next()
draws from the prior and returns a WeightedSample { log_weight, sample } pair where
log_weight is the sum of the log-likelihoods of all observations given the draw.
Use ferric::weighted_mean and ferric::weighted_std to compute posterior summaries from the
weighted samples.
use make_model;
make_model!
The WeightedSample type nests query variables under .sample.* and exposes the metadata
separately at .log_weight, so there is no naming conflict even if a query variable is
named log_weight. Use ferric::effective_sample_size(&log_weights) to compute ESS
from collected weighted samples.
User-proposal importance sampling — importance_sampler
For continuous models where drawing latents from the prior is inefficient, Ferric also generates a small proposer API. Each model module includes:
ObservedData: a generated struct containing the model constants and observations.Proposal: a generated struct whose fields correspond to latent stochastic variables.Proposer<R>: a trait whosenewmethod receivesObservedData, and whoseproposemethod returns oneProposal.
The proposal's log_prob must be the joint log probability of every value supplied in the
proposal. Fields left as None are drawn from the model prior. Ferric computes the sample
weight as log p_model(proposed values) - log q(proposed values) + log p(observations | world).
For diagnostics, importance_sampler_debug::<P>(n) traces the first n worlds, printing
the proposal, model-prior terms for proposed values, observed likelihood terms, sampled
stochastic values, and final log weight before continuing as a normal iterator.
As with weighted_sample_iter, collect the returned log_weight values and pass them to
ferric::effective_sample_size to monitor weight degeneracy.
The rats example also exposes this through FERRIC_DEBUG_IMPORTANCE; for example,
FERRIC_DEBUG_IMPORTANCE=1 cargo run -p ferric --example rats traces the first
importance sample in each rats experiment.
use ;
use make_model;
make_model!
Indexed random variables
Ferric can declare arrays of random variables by adding one or more index ranges after the variable name:
use make_model;
make_model!
Each index range has the form index_name of upper_bound. The upper bound
must be a constant or an earlier variable in the model. The index takes values
from 0 through upper_bound - 1; if the upper bound is zero, the indexed
variable has no values. The generated query type is a nested Vec, so alive
is Vec<Vec<bool>> and age is Vec<u64>.
Constants are supplied when the generated Model is constructed:
let model = Model ;
Observed indexed stochastic variables use nested vectors with Option<T> at
the leaves. Some(value) is an observed value and None masks a missing
entry. Directly observed indexed variables must have constant-valued bounds,
or variable bounds that are themselves observed.
An index upper bound may also be stochastic:
make_model!
let model = Model ;
When a stochastic variable is used as an index bound, Ferric requires an
explicit max ... annotation on that variable. The max value must be a
literal or a previously declared constant of the same type as the variable.
The annotation bounds the random variable's domain. For example,
n ~ Poisson::new(3.0) max 100 means n may take values 0..=100, and
likelihoods are normalized over that bounded domain using
Distribution::log_cum_prob.
Distribution and deterministic expressions inside indexed variables can refer
to the explicitly named indices. Deterministic variables can depend on indexed
variables, including arrays whose size depends on a stochastic bound. This
supports observed aggregates such as counts or summaries when the raw array
cardinality is latent. See examples/urn_unknown_marbles.rs for a complete
unknown-urn-size example.
Gelfand Rats Model
The rats example in examples/rats.rs writes the classic Gelfand hierarchical
growth model with the full observed 30-rat weight table. It is included as a
language example for indexed continuous observations and hierarchical
dependencies. The current inference methods are not intended to solve this
model well; later samplers will make this kind of posterior practical.
Radar Simulation Sketch
The radar example in examples/radar.rs shows a richer simulation-only model.
The number of aircraft is Poisson-bounded. Each aircraft has a 3D latent path:
the first timestep is drawn from a broad Gaussian, and later timesteps move
with a narrow Gaussian transition around the previous location. Real blips are
small-Gaussian measurements around their aircraft. False blips have their own
bounded count at each timestep and broad-Gaussian locations. A deterministic
query collects the set of all real and false blip locations for every timestep.
This example is meant for forward simulation; later inference methods will make
conditioning on the generated blip sets practical.
make_model!
Available distributions
| Distribution | Domain | Parameters |
|---|---|---|
Bernoulli |
bool |
p ∈ [0, 1] |
Dirac<T> |
T |
deterministic value |
Empirical<T> |
T |
finite weighted support |
Categorical |
usize |
non-negative category weights |
Binomial |
u64 |
n ≥ 1, p ∈ (0, 1) |
BetaBinomial |
u64 |
n ≥ 1, alpha > 0, beta > 0 |
DiscreteUniform |
i64 |
a ≤ b |
Geometric |
u64 |
p ∈ (0, 1] |
Hypergeometric |
u64 |
population, successes, draws |
NegativeBinomial |
u64 |
r > 0, p ∈ (0, 1] |
Poisson |
u64 |
rate > 0 |
Uniform |
f64 |
low < high |
Exponential |
f64 |
rate > 0 |
Normal |
f64 |
mean, std_dev > 0 |
LogNormal |
f64 |
mu, sigma > 0 |
Beta |
f64 |
alpha > 0, beta > 0 |
Cauchy |
f64 |
median, scale > 0 |
Chi |
f64 |
k > 0 |
ChiSquared |
f64 |
k > 0 |
Erlang |
f64 |
integer k ≥ 1, lambda > 0 |
FisherF |
f64 |
d1 > 0, d2 > 0 |
Frechet |
f64 |
location, scale > 0, shape > 0 |
Gamma |
f64 |
shape > 0, scale > 0 |
Gumbel |
f64 |
mu, beta > 0 |
HalfNormal |
f64 |
sigma > 0 |
InverseGamma |
f64 |
alpha > 0, beta > 0 |
InverseGaussian |
f64 |
mu > 0, lambda > 0 |
Laplace |
f64 |
mu, b > 0 |
Logistic |
f64 |
mu, s > 0 |
Pareto |
f64 |
x_m > 0, alpha > 0 |
Rayleigh |
f64 |
sigma > 0 |
StudentT |
f64 |
df > 0 |
Triangular |
f64 |
a ≤ c ≤ b, a < b |
Weibull |
f64 |
lambda > 0, k > 0 |
Dirichlet |
Vec<f64> |
concentration vector, each alpha_i > 0 |
Multinomial |
Vec<u64> |
n ≥ 1, probability vector |
MultivariateNormal |
nalgebra::DVector<f64> |
mean vector, SPD covariance matrix |
MultivariateStudentT |
nalgebra::DVector<f64> |
mean vector, SPD scale matrix, df > 0 |
MatrixNormal |
nalgebra::DMatrix<f64> |
mean matrix, SPD row and column covariance matrices |
Wishart |
nalgebra::DMatrix<f64> |
df > p - 1, SPD scale matrix |
Documentation
Ferric's API documentation is published automatically by docs.rs when a new crate version is published to crates.io. There is no separate official Rust documentation host to push to manually; the canonical Rust crate docs live on docs.rs.
For project docs beyond API reference, keep them in this repository unless they grow into
a distinct website. A separate Ferric-AI/docs repo would add coordination overhead right
now, while docs.rs plus this README gives users the normal Rust discovery path.
License
Licensed under either of
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
See CONTRIBUTING.md for development setup, coverage tools, macro expansion, and publishing instructions.