[](https://github.com/Ferric-AI/ferric/actions/workflows/ci.yml)
[](https://crates.io/crates/ferric)
[](https://coveralls.io/github/Ferric-AI/ferric)
# Ferric
A Probabilistic Programming Language in Rust with a declarative syntax.
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
ferric = "0.1"
```
## 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;`.
- Mark variables with `observe` to condition the model on observed data.
- Mark variables with `query` to 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.
```rust
use ferric::make_model;
make_model! {
mod grass;
use ferric::distributions::Bernoulli;
let rain : bool ~ Bernoulli::new(0.2);
let sprinkler : bool ~ if rain { Bernoulli::new(0.01) } else { Bernoulli::new(0.4) };
let grass_wet : bool ~ Bernoulli::new(
if sprinkler && rain { 0.99 }
else if sprinkler { 0.90 }
else if rain { 0.80 }
else { 0.00 }
);
observe grass_wet;
query rain;
query sprinkler;
}
fn main() {
let model = grass::Model { grass_wet: true };
let num_samples = 100_000;
let mut num_rain = 0;
let mut num_sprinkler = 0;
for sample in model.sample_iter().take(num_samples) {
if sample.rain { num_rain += 1; }
if sample.sprinkler { num_sprinkler += 1; }
}
println!(
"P(rain | wet) ≈ {:.3} P(sprinkler | wet) ≈ {:.3}",
num_rain as f64 / num_samples as f64,
num_sprinkler as f64 / num_samples as f64,
);
}
```
### 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.
```rust
use ferric::make_model;
make_model! {
mod signal_estimation;
use ferric::distributions::Normal;
// prior: true signal unknown
let true_signal : f64 ~ Normal::new(0.0, 2.0);
// likelihood: noisy sensor reading
let sensor_reading : f64 ~ Normal::new(true_signal, 1.0);
observe sensor_reading;
query true_signal;
}
fn main() {
let model = signal_estimation::Model { sensor_reading: 2.5 };
let num_samples = 100_000;
let mut signal_vals = Vec::with_capacity(num_samples);
let mut log_weights = Vec::with_capacity(num_samples);
for ws in model.weighted_sample_iter().take(num_samples) {
signal_vals.push(ws.sample.true_signal);
log_weights.push(ws.log_weight);
}
let post_mean = ferric::weighted_mean(&signal_vals, &log_weights);
let post_std = ferric::weighted_std(&signal_vals, &log_weights);
println!("posterior: true_signal = {:.3} ± {:.3}", post_mean, post_std);
// Analytical answer: mean ≈ 2.0, std ≈ 0.894
}
```
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`.
## Available distributions
| `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](https://docs.rs/ferric)
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
- [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
- [MIT license](http://opensource.org/licenses/MIT)
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](CONTRIBUTING.md) for development setup, coverage tools, macro
expansion, and publishing instructions.