# simsam
Sim(ple)sam(ple) — a Rust library for sampling from custom discrete and continuous distributions.
Define distributions by PDF or CDF (closures, histograms, location-scale transforms, truncation, or [simsym](https://docs.rs/simsym) symbolic expressions), then draw samples via inverse transform sampling — similar to [SciPy `rv_continuous`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.rv_continuous.html).
## Features
### Sampling
- `sample()` / `sample_with_rng()` — inverse transform (default: bisection + Newton)
- [`BuildOptions::with_hermite(n)`](https://docs.rs/simsam) — fast PPF table (SciPy [`stats.sampling`](https://docs.scipy.org/doc/scipy/tutorial/stats/sampling.html)-style numerical inversion)
- [`BuildOptions::with_tdr()`](https://docs.rs/simsam) — transformed density rejection (TDR); automatic numerical dPDF when only PDF is given
- `from_pdf_dpdf_fn` — explicit dPDF for TDR (log-concave hat construction)
- `rand::distr::Distribution` integration
- Multivariate sampling: rejection sampling, Metropolis–Hastings (MH), Gibbs, HMC
- Multivariate CDF approximation via Monte Carlo
- Gaussian copula for correlated samples from arbitrary 1D marginals
#### Choosing a 1D sampler
| Default / need accurate `ppf` | `BuildOptions::default()` (inverse transform) |
| Many samples, smooth unimodal CDF | `BuildOptions::default().with_hermite(128)` |
| Have PDF (+ optional dPDF), rejection-friendly density | `BuildOptions::default().with_tdr()` |
Note: `ppf`, `mean`, and other statistics always use numerical inverse CDF even when TDR is selected for `sample()`.
### Distribution API (SciPy-like)
| `pdf`, `logpdf` | Density (when PDF is available) |
| `cdf`, `logcdf` | Cumulative distribution |
| `sf`, `logsf`, `isf` | Survival functions |
| `ppf` | Percent point function (inverse CDF) |
| `mean`, `var`, `std`, `median` | Summary statistics |
| `entropy`, `expect`, `interval` | Entropy, E[f(X)], confidence interval |
### Constructors
| `from_pdf_fn` / `from_cdf_fn` | Subclass `rv_continuous` |
| `from_pdf_dpdf_fn` | TDR with explicit dPDF |
| `from_histogram` | `rv_histogram` |
| `from_pdf_loc_scale` | `loc` / `scale` parameters |
| `Truncated` | Truncate to sub-interval |
| `SymbolicContinuous` | Custom `_pdf` + symbolic CAS (via **simsym**) |
| `DiscreteSampler::from_pmf` | `rv_discrete(values=...)` |
### Cargo features
- **Default**: no symbolic dependency
- **`symbolic`**: enables `SymbolicContinuous` and `SymbolicPdfNd`
## Examples
### PDF + statistics
```rust
use simsam::{from_pdf_fn, Interval};
let support = Interval::new(0.0, 1.0).unwrap();
let m = dist.mean().unwrap(); // 0.75
let v = dist.var().unwrap();
let (lo, hi) = dist.interval(0.9).unwrap();
```
### Fast Hermite sampling
```rust
use simsam::{from_pdf_fn_with_options, BuildOptions, Interval};
let opts = BuildOptions::default().with_hermite(128);
let _ = dist.sample().unwrap();
}
```
### TDR sampling
```rust
use simsam::{from_pdf_fn_with_options, BuildOptions, Interval, TdrBuildConfig, TdrTransform};
let support = Interval::new(-1.0, 1.0).unwrap();
let dist = from_pdf_fn_with_options(
|x| 1.0 - x * x,
support,
BuildOptions::default().with_tdr_config(TdrBuildConfig {
transform: TdrTransform::InvSqrt,
..TdrBuildConfig::default()
}),
)
.unwrap();
```
```bash
cargo run --example tdr_quadratic
cargo run --example sampling_methods
```
### Histogram
```rust
use simsam::{from_histogram, BuildOptions};
let edges = vec![0.0, 1.0, 2.0];
let counts = vec![1.0, 3.0];
let dist = from_histogram(edges, counts, false, BuildOptions::default()).unwrap();
```
### Location-scale
```rust
use simsam::{from_pdf_loc_scale, PdfFn, BuildOptions, Interval};
let base = Interval::new(0.0, 1.0).unwrap();
```bash
cargo run --example symbolic --features symbolic
```
### Multivariate: rejection sampling (2D uniform)
```bash
cargo run --example multivar_rejection_uniform2d
```
### Multivariate: Metropolis–Hastings (2D uniform)
```bash
cargo run --example multivar_mh_uniform2d
```
### Multivariate: HMC (2D truncated Gaussian; numeric gradient)
```bash
cargo run --example multivar_hmc_gaussian2d
```
### Multivariate: Gibbs (independent uniforms via conditionals)
```bash
cargo run --example multivar_gibbs_independent_uniforms
```
### Multivariate: CDF approximation (Monte Carlo)
```bash
cargo run --example multivar_cdf_mc_uniform2d
```
### Copula: Gaussian copula with custom marginals
```bash
cargo run --example multivar_copula_gaussian
```
### Conditional factorization sampler
```bash
cargo run --example multivar_factorization
```
### Multivariate symbolic HMC (symbolic gradient)
```bash
cargo run --example multivar_symbolic_hmc --features symbolic
```
## Python
Python bindings live in [`python/`](python/) as a separate maturin/PyO3 crate. The root Rust crate has **no** PyO3 dependency, so `cargo publish` from the repo root is unchanged.
### Setup
```bash
cd python
uv sync
uv run maturin develop --release
uv run pytest
```
### Usage
```python
import simsam
dist = simsam.from_pdf(lambda x: 2.0 * x, simsam.Interval(0.0, 1.0))
print(dist.mean(), dist.sample_n(10_000))
opts = simsam.BuildOptions.hermite(128)
dist = simsam.from_pdf_with_options(lambda x: 3.0 * x * x, simsam.Interval(0, 1), opts)
mh = simsam.metropolis_hastings(
lambda x, y: 1.0,
simsam.HyperRect([0.0, 0.0], [1.0, 1.0]),
)
print(mh.sample_n(1000))
```
Optional [simsym](https://docs.rs/simsym) symbolic PDFs (requires building with the `symbolic` Cargo feature):
```bash
uv run maturin develop --release --features symbolic,pyo3/extension-module
```
```python
dist = simsam.symbolic_continuous("3 * x^2", "x", simsam.Interval(0.0, 1.0))
```
See [`python/README.md`](python/README.md) for the full Python API.
## Limitations
- **Finite support** required; use a wide interval + [`Truncated`](https://docs.rs/simsam) for partial ranges.
- **Unimodal** CDF assumed for inverse transform on continuous distributions.
- No built-in catalog of named distributions (use `rand_distr` / `statrs`), multivariate laws, KDE, or `fit(data)`.
## License
BSD-3-Clause — see [LICENSE](LICENSE).