Stochastic processes
132 stochastic processes — diffusion, jump, volatility, interest-rate, fractional / rough, and noise. Catalog organised by mathematical family.
Stochastic processes
The stochastic-rs-stochastic crate ships 132 processes organised
into eleven families (below). Every process implements
ProcessExt<T>: sample() returns one path,
sample_map(m, f) folds over m paths in parallel, and sample_par(m)
returns Vec<Output> (one ndarray::Array1<T> per path for single-factor
models).
Derivation: grep -rn "ProcessExt<T> for\|ProcessExt<T," stochastic-rs-stochastic/src --include='*.rs' | grep -v /traits/ | wc -l
— the same command and per-directory breakdown that
stochastic-rs-stochastic/tests/reproducibility_all_processes.rs uses to
derive its own exhaustive guard list, so the two never disagree. That file
excludes src/traits/process.rs deliberately: it contributes only blanket
impls of marker traits keyed off a P: ProcessExt<...> bound
(OneDimensional, MultiDimensional, …), never a concrete process type —
counting those inflates 132 to 135, which is how earlier audits of this
page disagreed with each other.
Families
| Family | Count | Examples |
|---|---|---|
| Diffusion | 36 | OU, GBM (log + standard + correlated multi-asset), CIR, CEV, CKLS, Aït-Sahalia, Pearson, Jacobi, regime-switching, Fouque, Wishart (matrix-valued CIR) |
| Core building blocks | 20 | Brownian motion (+ bridge, correlated, correlated-fractional), fBM, Poisson (+ compound, custom jump sizes), Hawkes (+ multivariate), subordinators (α-stable, gamma, inverse-Gaussian, tempered-stable, Poisson, CTRW), linear fractional stable motion, naive (direct-convolution) Volterra |
| Jump | 17 | Merton, Kou, CGMY, NIG, VG, bilateral gamma, KoBoL, RDTS, CTS, Bates, Lévy diffusion |
| Volatility | 16 | Heston (+ log, 2D, multifactor, stochastic-local), SABR (+ multifactor), Bergomi, rough Bergomi, rough Heston, double-Heston, BNS, HKDE, Bates SVJ (+ fractional), SVCGMY |
| Interest rate | 16 | Vasicek (+ fractional), CIR++, CIR 2-factor, ADG (affine/quadratic-Gaussian multi-factor), Hull-White (1F + 2F), Black-Karasinski, Ho-Lee, HJM, LMM, BGM, Wu-Zhang, Duffie-Kan (+ jump), Cheyette (quasi-Gaussian, local volatility) |
| Autoregressive / GARCH | 9 | AR(p), MA(q), ARIMA, SARIMA, ARCH, GARCH, EGARCH, GJR-GARCH, AGARCH |
| Noise | 6 | Fractional Gaussian noise, Gaussian noise, white noise, correlated FGN / GN, k-dimensional correlated increments from a correlation matrix |
| Rough (Riemann–Liouville lift) | 4 | RL Black-Scholes, RL fBM, RL Heston, RL fOU |
| Stochastic correlation | 4 | Van Emmerich / Jacobi-type, Teng-modified OU, general transformed-OU, Heston with stochastic correlation |
| Sheet | 1 | Fractional Brownian sheet (Fbs) |
| Volterra | 3 | See the Volterra section below |
Every row lists representative examples, not an exhaustive per-type list —
the exhaustive list (one line per type) is the guard file linked above, or
cargo doc for the API reference.
Choosing a family
| You need | Start with | Then |
|---|---|---|
| A price path with constant volatility | Gbm (exact log-normal steps) | GbmLog for the log-price, Cev for a local-vol elasticity |
| Mean reversion | Ou (Gaussian), Cir (positive, square-root), Vasicek / HullWhite for rates | CirPlusPlus to fit a term structure, Cheyette for a quasi-Gaussian HJM |
| Stochastic volatility | Heston (Euler / QE schemes), Sabr, Bergomi | DoubleHeston, MultifactorHeston, HestonStochCorr, HestonSlv |
| Rough volatility | RoughBergomi (hybrid scheme), RoughHeston, RlHeston (Markov lift) | the volterra module for a general kernel |
| Long memory / fractional noise | Fgn → Fbm, Fou, Fcir, Fgbm | these are the processes with FFT device backends |
| Jumps | Merton, Kou, Bates1996, CompoundPoisson, Hawkes | LevyDiffusion with any Distribution for the jump size |
| Several correlated assets | Mcgns (driver) → MultiGbm, Wishart (covariance) | Cfgns for correlated fractional noise |
| A rates curve, not a rate | Hjm, Lmm, Bgm, Adg | G2pp-style two-factor: HullWhite2F, Cir2F |
| Time-series style | Ar, Arima, Garch / Egarch / GjrGarch, Sarima | the stats crate fits them |
Examples
Geometric Brownian Motion
The textbook log-normal diffusion .
// docs: processes#geometric-brownian-motion
//! Backs the GBM example on the processes catalog page.
use stochastic_rs::prelude::*;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stochastic::diffusion::gbm::Gbm;
#[test]
fn gbm_sample_and_sample_par() {
let p = Gbm::<f64, _>::new(
0.05,
0.2,
1_000,
Some(100.0),
Some(1.0),
Deterministic::new(42),
);
let path = p.sample();
assert_eq!(path.len(), 1_000);
let paths = p.sample_par(10_000);
assert_eq!(paths.len(), 10_000);
assert_eq!(paths[0].len(), 1_000);
}import stochastic_rs as srs
p = srs.PyGbm(mu=0.05, sigma=0.2, n=1000, x0=100.0, t=1.0)
path = p.sample() # numpy.ndarray, shape (1000,)
paths = p.sample_par(10_000) # shape (10_000, 1000)Heston stochastic volatility
Two-factor model , with .
// docs: processes#heston-stochastic-volatility
//! Backs the Heston example on the processes catalog page.
use stochastic_rs::simd_rng::Unseeded;
use stochastic_rs::stochastic::volatility::HestonPow;
use stochastic_rs::stochastic::volatility::heston::Heston;
use stochastic_rs::traits::ProcessExt;
#[test]
fn heston_two_factor_sample() {
let p = Heston::<f64, _>::new(
Some(100.0),
Some(0.04),
2.0,
0.04,
0.3,
-0.7,
0.03,
1_000,
Some(1.0),
HestonPow::Sqrt,
Some(true),
Unseeded,
);
let [s_path, v_path] = p.sample();
assert_eq!(s_path.len(), 1_000);
assert_eq!(v_path.len(), 1_000);
assert!(v_path.iter().all(|&v| v >= 0.0));
}import stochastic_rs as srs
p = srs.PyHeston(kappa=2.0, theta=0.04, sigma=0.3, rho=-0.7, mu=0.03,
n=1000, s0=100.0, v0=0.04, t=1.0)
s, v = p.sample() # both numpy arraysDiscretisation schemes
sample() integrates the variance with the Euler full-truncation scheme by
default. For large κ, high |ρ|, or long maturities Euler carries a noticeable
discretisation bias; switch to the Andersen (2008) Quadratic-Exponential (QE)
scheme at compile time with .qe():
// docs: processes#discretisation-schemes
//! Backs the Andersen QE discretisation example on the processes catalog page.
use stochastic_rs::simd_rng::Unseeded;
use stochastic_rs::stochastic::volatility::HestonPow;
use stochastic_rs::stochastic::volatility::heston::Heston;
use stochastic_rs::traits::ProcessExt;
#[test]
fn heston_qe_scheme_sample() {
let qe = Heston::<f64, _>::new(
Some(100.0),
Some(0.04),
2.0,
0.04,
0.3,
-0.7,
0.03,
1_000,
Some(1.0),
HestonPow::Sqrt,
Some(true),
Unseeded,
)
.qe();
let [s_path, v_path] = qe.sample();
assert_eq!(s_path.len(), 1_000);
assert_eq!(v_path.len(), 1_000);
}The scheme is a zero-sized type parameter (Heston<T, S, Sch>), so the choice is
monomorphised with no per-step branch. QE is defined for the square-root (CIR)
variance only, so keep HestonPow::Sqrt. Reference: Andersen, L. (2008),
Efficient simulation of the Heston stochastic volatility model, Journal of
Computational Finance 11(3). (.qe() is Rust-only; the Python Heston uses the
Euler scheme.)
Heston stochastic-local volatility
The Heston model under a leverage function and a mixing fraction
: ,
. The leverage
is an Fn2D of time and spot: an Expr reaches a device; a closure, a
Python callable or a tabulated Grid2D — what a calibrated LeverageSurface
converts into — keeps the process on the host. With and
the path is HestonLog's, bit for bit. The
quant page
calibrates to a vanilla surface.
// docs: processes#heston-stochastic-local-volatility
//! Backs the Heston SLV example on the processes catalog page.
use ndarray::Array1;
use ndarray::Array2;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stochastic::volatility::heston_slv::HestonSlv;
use stochastic_rs::traits::Expr;
use stochastic_rs::traits::Grid2D;
use stochastic_rs::traits::ProcessExt;
#[test]
fn heston_slv_two_factor_sample_under_an_expression_leverage() {
// L(t, S) = 1.2 − 0.003 S, a leverage that falls with the spot, written as
// an expression so the process can also run on a device; eta = 0.6 scales
// the vol-of-vol.
let leverage = Expr::lit(1.2) - Expr::x() * 0.003;
let p = HestonSlv::<f64, _>::new(
Some(100.0),
Some(0.04),
2.0,
0.04,
0.3,
-0.7,
0.03,
0.6,
leverage,
1_000,
Some(1.0),
Deterministic::new(7),
);
assert!(p.device_ready());
let [s_path, v_path] = p.sample();
assert_eq!(s_path.len(), 1_000);
assert!(s_path.iter().all(|&s| s > 0.0));
assert!(v_path.iter().all(|&v| v >= 0.0));
// A tabulated leverage — what a calibrated `LeverageSurface` converts
// into — drives the same sampler on the host.
let grid = Grid2D::new(
Array1::from_vec(vec![0.0, 1.0]),
Array1::from_vec(vec![50.0, 150.0]),
Array2::from_elem((2, 2), 1.0),
);
let tabulated = p.with_leverage(grid);
assert!(!tabulated.device_ready());
let [s_grid, _] = tabulated.sample();
assert_eq!(s_grid.len(), 1_000);
}import stochastic_rs as srs
# leverage: a callable L(t, s), a (spots, times, values) triple of arrays, or
# the LeverageSurface a HestonSlvCalibrator returns
p = srs.PyHestonSlv(kappa=2.0, theta=0.04, sigma=0.3, rho=-0.7, mu=0.03, eta=0.6,
leverage=lambda t, s: 1.2 - 0.003 * s, n=1000,
s0=100.0, v0=0.04, t=1.0)
s, v = p.sample() # both numpy arraysFractional Brownian motion
Roughness controlled by the Hurst parameter . recovers standard Brownian motion; is rough, is persistent.
// docs: processes#fractional-brownian-motion
//! Backs the fGN example on the processes catalog page.
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stochastic::noise::fgn::Fgn;
use stochastic_rs::traits::ProcessExt;
#[test]
fn fgn_sample() {
let fgn = Fgn::<f64, _>::new(
/* hurst */ 0.3,
/* n */ 4096,
/* t */ Some(1.0),
Deterministic::new(42),
);
let increments = fgn.sample();
assert_eq!(increments.len(), 4096);
}import stochastic_rs as srs
import numpy as np
fgn = srs.PyFgn(hurst=0.3, n=4096, t=1.0, seed=42)
increments = fgn.sample()
fbm = np.cumsum(increments) # fBM = cumsum(fGN)Quasi-Monte Carlo paths (Sobol + Brownian bridge)
SobolSeq now carries Joe and Kuo's full new-joe-kuo-6.21201 direction table (any dimension up to 21201) and an Owen-type scramble (SobolSeq::scrambled, random linear scrambling plus digital shift) for randomised-QMC error bars. BrownianBridgeQmc turns one Sobol point per path into Brownian levels by bridge bisection — terminal value first, then midpoints — so the sequence's best-equidistributed coordinates carry the path's coarsest features; increments() hands the same paths to an Euler loop.
// docs: processes#quasi-monte-carlo-paths-sobol--brownian-bridge
//! Backs the quasi-Monte Carlo example on the processes page.
use ndarray::Array2;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stochastic::mc::brownian_bridge_qmc::BrownianBridgeQmc;
use stochastic_rs::stochastic::mc::sobol::SobolSeq;
#[test]
fn qmc_brownian_paths_price_a_martingale() {
// 2^12 scrambled Sobol paths of a one-year Brownian motion on 64 steps.
let qmc = BrownianBridgeQmc::scrambled(64, 1.0, &Deterministic::new(7));
let w: Array2<f64> = qmc.paths(4_096);
assert_eq!(w.dim(), (4_096, 64));
// exp(σW_T − σ²T/2) is a martingale, so its QMC average sits at 1 to a
// few parts in a thousand — tighter than plain MC with the same budget.
let sigma = 0.2;
let mean = w
.column(63)
.iter()
.map(|x| (sigma * x - 0.5 * sigma * sigma).exp())
.sum::<f64>()
/ 4_096.0;
assert!((mean - 1.0).abs() < 5e-3, "martingale mean {mean}");
// The sequence itself reaches far beyond the old 21-dimension ceiling.
let deep: Array2<f64> = SobolSeq::new(5_000).sample(16);
assert!(deep.iter().all(|u| (0.0..1.0).contains(u)));
}import numpy as np
import stochastic_rs as srs
qmc = srs.PyBrownianBridgeQmc(steps=64, horizon=1.0, seed=7) # seed → scrambled
w = qmc.paths(4096) # (4096, 64) Brownian levels
print(np.cov(w[:, -1], w[:, 31])[0, 1]) # ≈ min(t, s) = 0.5
sobol = srs.PySobolSeq(1000) # 1000 dimensions, unscrambled
print(sobol.sample(8)[:, 999])Correlated multi-asset GBM
MultiGbm simulates k geometric Brownian motions on one grid from a correlation matrix: the increments come from Mcgns, the k-dimensional correlated driver (independent normals through the Cholesky factor of ρ), and each asset is stepped by the exact log-Euler scheme, so the grid law is the multivariate lognormal. The output is one (k, n) matrix per path.
// docs: processes#correlated-multi-asset-gbm
//! Backs the correlated multi-asset GBM example on the processes page.
use ndarray::array;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stochastic::diffusion::multi_gbm::MultiGbm;
use stochastic_rs::traits::ProcessExt;
#[test]
fn multi_gbm_terminal_correlation_follows_rho() {
// Two assets over one year on 252 steps with correlation −0.4.
let model = MultiGbm::<f64, _>::new(
array![0.05, 0.02],
array![0.2, 0.3],
array![[1.0, -0.4], [-0.4, 1.0]],
253,
array![100.0, 50.0],
Some(1.0),
Deterministic::new(7),
);
let paths = model.sample_par(8_000);
assert_eq!(paths[0].dim(), (2, 253));
// Correlation of the terminal log-returns tracks ρ.
let logs: Vec<(f64, f64)> = paths
.iter()
.map(|p| ((p[(0, 252)] / 100.0).ln(), (p[(1, 252)] / 50.0).ln()))
.collect();
let n = logs.len() as f64;
let (m0, m1) = (
logs.iter().map(|l| l.0).sum::<f64>() / n,
logs.iter().map(|l| l.1).sum::<f64>() / n,
);
let v0 = logs.iter().map(|l| (l.0 - m0).powi(2)).sum::<f64>() / n;
let v1 = logs.iter().map(|l| (l.1 - m1).powi(2)).sum::<f64>() / n;
let cov = logs.iter().map(|l| (l.0 - m0) * (l.1 - m1)).sum::<f64>() / n;
let corr = cov / (v0 * v1).sqrt();
assert!((corr + 0.4).abs() < 0.05, "terminal correlation {corr}");
}import numpy as np
import stochastic_rs as srs
rho = np.array([[1.0, 0.6, -0.2], [0.6, 1.0, 0.1], [-0.2, 0.1, 1.0]])
model = srs.PyMultiGbm(mu=[0.05, 0.03, 0.01], sigma=[0.2, 0.3, 0.15], rho=rho, n=253, x0=[100.0, 50.0, 10.0], t=1.0, seed=7)
paths = model.sample_par(10_000) # list of (3, 253) arrays
terminal = np.array([p[:, -1] for p in paths])
print(np.corrcoef(np.log(terminal).T)) # ≈ rhoWishart process (stochastic covariance)
Wishart simulates the matrix-valued diffusion dX = (α aᵀa + bX + Xbᵀ) dt + √X dW a + aᵀ dWᵀ √X on the positive semidefinite cone: the matrix CIR behind the Gouriéroux–Sufana and Da Fonseca–Grasselli–Tebaldi stochastic-covariance models. Every grid step is sampled exactly with the Ahdida–Alfonsi (2013) splitting (one noncentral χ² and r Gaussians per coordinate after an extended Cholesky of the remaining block), so any degree α ≥ d − 1 is admissible and the paths never leave the cone. The closed-form mean and Laplace transform ship alongside for validation. Each path is one (n, d, d) array.
// docs: processes#wishart-process-stochastic-covariance
//! Backs the Wishart process example on the processes page.
use ndarray::Array2;
use ndarray::array;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stochastic::diffusion::wishart::Wishart;
use stochastic_rs::traits::ProcessExt;
#[test]
fn wishart_terminal_law_matches_the_closed_forms() {
// 2 × 2 Wishart with a full drift, four exact steps over one year.
let process = Wishart::<f64, _>::new(
2.5,
array![[-0.5, 0.1], [0.05, -0.3]],
array![[0.3, 0.1], [0.0, 0.2]],
array![[1.0, 0.2], [0.2, 0.5]],
5,
Some(1.0),
Deterministic::new(7),
);
let paths = process.sample_par(8_000);
assert_eq!(paths[0].dim(), (5, 2, 2));
// Terminal mean against E[X_1] = m x₀ mᵀ + α q (affine moment formula).
let mut mean = Array2::<f64>::zeros((2, 2));
for p in &paths {
mean += &p.index_axis(ndarray::Axis(0), 4);
}
mean /= paths.len() as f64;
let want = process.mean(1.0);
assert!(
(&mean - &want).iter().all(|e| e.abs() < 0.05),
"mean {mean:?} vs {want:?}"
);
// Laplace transform E[exp(Tr(v X_1))] for a negative definite v, eq. (10).
let v = array![[-0.4, -0.12], [-0.12, -0.4]];
let mc = paths
.iter()
.map(|p| {
let x = p.index_axis(ndarray::Axis(0), 4);
(v.dot(&x)).diag().sum().exp()
})
.sum::<f64>()
/ paths.len() as f64;
let exact = process.laplace_transform(&v, 1.0);
assert!((mc - exact).abs() < 0.03, "laplace {mc} vs {exact}");
}import numpy as np
import stochastic_rs as srs
b = np.array([[-0.5, 0.1], [0.05, -0.3]])
a = np.array([[0.3, 0.1], [0.0, 0.2]])
x0 = np.array([[1.0, 0.2], [0.2, 0.5]])
process = srs.PyWishart(alpha=2.5, b=b, a=a, x0=x0, n=5, t=1.0, seed=7)
paths = process.sample_par(10_000) # list of (5, 2, 2) arrays
terminal = np.mean([p[-1] for p in paths], axis=0)
print(np.abs(terminal - process.mean(1.0)).max()) # ≈ 0: the scheme is exact
v = -0.4 * np.array([[1.0, 0.3], [0.3, 1.0]])
print(process.laplace_transform(v, 1.0)) # E[exp(Tr(v X_1))], eq. (10)Cheyette quasi-Gaussian short rate
Cheyette is the one-factor quasi-Gaussian HJM model: the state (x, y) follows dx = (y − κx) dt + σ(t, x) dW, dy = (σ² − 2κy) dt, and the whole forward curve is a function of that pair — f_t(T) = f_0(T) + e^{−κ(T−t)} (x_t + G y_t) and P_t(T) = P_0(T)/P_0(t) · exp(−G x_t − ½ G² y_t) with G = (1 − e^{−κ(T−t)})/κ. A constant σ is Hull–White; a displaced or CEV σ(t, x) adds the rate skew (Andersen–Piterbarg Vol. II, Ch. 13). The path output is the pair [x, y]; short_rate, forward_rate and zero_bond rebuild rates and bonds from it.
// docs: processes#cheyette-quasi-gaussian-short-rate
//! Backs the Cheyette example on the processes page.
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::stochastic::interest::cheyette::Cheyette;
use stochastic_rs::traits::ProcessExt;
fn flat_forward(_t: f64) -> f64 {
0.03
}
fn displaced_vol(_t: f64, x: f64) -> f64 {
0.01 + 0.3 * x
}
#[test]
fn cheyette_state_and_bond_reconstruction() {
// Flat 3 % initial curve, κ = 0.5, displaced local volatility; one year on 100 steps.
let model = Cheyette::<f64, _>::new(
flat_forward as fn(f64) -> f64,
0.5,
displaced_vol as fn(f64, f64) -> f64,
101,
Some(1.0),
Deterministic::new(7),
);
let [x, y] = model.sample();
assert_eq!(x.len(), 101);
assert_eq!((x[0], y[0]), (0.0, 0.0));
// y accumulates the variance of x, so it stays positive along the path.
assert!(y.iter().skip(1).all(|v| *v > 0.0));
// Rates and bonds rebuild from the terminal state.
let r_1 = model.short_rate(1.0, x[100]);
let p_1_3 = model.zero_bond(1.0, 3.0, x[100], y[100]);
assert!((r_1 - 0.03).abs() < 0.05 && p_1_3 > 0.8 && p_1_3 < 1.0);
// At the origin the reconstruction is the initial curve.
assert!((model.zero_bond(0.0, 2.0, 0.0, 0.0) - (-0.06_f64).exp()).abs() < 1e-12);
}import numpy as np
import stochastic_rs as srs
model = srs.PyCheyette(f0=lambda t: 0.03, kappa=0.5, sigma=lambda t, x: 0.01 + 0.3 * x, n=101, t=1.0, seed=7)
x, y = model.sample() # one state path (x_t, y_t), both length 101
xs, ys = model.sample_par(2000) # (2000, 101) each
print(model.short_rate(1.0, xs[:, -1].mean())) # ≈ 0.03: x is centred
print(model.zero_bond(1.0, 3.0, 0.0, 0.0)) # P_0(3) / P_0(1) at the zero stateCommon patterns
Construction
Every process follows the same new(args…, seed) pattern — the model
parameters first, then n, x0, t, and the seed source last
(mirrors the distribution constructors — see the seeding concept
page). The seed argument is a value implementing
SeedExt. Gbm stands in for any process
below; substitute the parameters of the one you want:
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::simd_rng::Unseeded;
use stochastic_rs::stochastic::diffusion::gbm::Gbm;
// auto-seeded
let p = Gbm::<f64, _>::new(0.05, 0.2, 1_000, Some(100.0), Some(1.0), Unseeded);
// reproducible
let p = Gbm::<f64, _>::new(
0.05,
0.2,
1_000,
Some(100.0),
Some(1.0),
Deterministic::new(42),
);Use Deterministic in tests so they replay bit-exactly; pass a shared
Deterministic to several processes when chaining correlated factors —
each call to seed.rng() / seed.rng_ext() atomically advances the
internal counter, so the streams diverge despite the shared root seed.
SeedExt::reseed(u64) swaps a Deterministic source in place to sweep
seeds without rebuilding the process.
Sampling
Continuing with the same p from above. sample / sample_par are
ProcessExt methods, so the trait has to be in scope:
use stochastic_rs::traits::ProcessExt;
let path = p.sample(); // Array1<f64>, length n
let paths = p.sample_par(64); // Vec<Array1<f64>>, 64 paths of length nsample_par(m) uses Rayon. Each path gets a deterministic seed
derived from the master seed plus the path index, so the result is
thread-count-independent.
Acceleration
CPU SIMD (f64x4 / f32x8) is on by default. The fractional / fGN family
(Fgn, Fbm, Fou, Fcir, Fgbm, FJacobi, Cfou, Cfgns, JumpFou,
JumpFOUCustom, and the generic Sde) additionally picks a sampling
backend at compile time with .on::<B>().
Cuda only exists when the cuda feature is compiled, which
needs an NVIDIA CUDA 12.x toolkit and GPU — not available in the CI
runners that build this site, so this block is illustrative rather than
compiled:
use stochastic_rs::simd_rng::Unseeded;
use stochastic_rs::stochastic::device::Cuda;
use stochastic_rs::stochastic::noise::fgn::Fgn;
use stochastic_rs::traits::ProcessExt;
let fgn = Fgn::<f32, _>::new(0.7, 65_536, None, Unseeded);
let path = fgn.on::<Cuda>().sample(); // FFT on the GPU, zero runtime branchBackends: Cpu (default), Cuda (cuda), Metal (metal),
Accelerate (accelerate) — each handle exists only when its feature is
compiled, so an
unavailable backend is a compile error, not a runtime fallback. See the
Backends concept page and the
Feature flags matrix.
Adding a new process
If you contribute, the four relevant SKILLs are:
add-diffusion-process— GBM / OU / CIR / Heston-styleadd-jump-process— Merton-jump / Kou / Bates / compound-Poissonadd-fractional-process— fBM / rough Bergomi / fractional CIRadd-gpu-sampler— CUDA / Metal port
Each contains the file-by-file recipe.
Euler engine
What the engine runs on today, per backend and precision, is tabulated on GPU support.
The sampling backend is a type parameter of every process
(Name<T, S, B = Cpu>, switched with .on::<B>()); the Euler engine is the
capability behind the device markers for Gbm, Ou and Cir:
gbm.on::<Metal>().sample_par(m) samples through ProcessExt as usual.
On Cpu (and Accelerate) that is the process's own sampler, so nothing is
re-implemented on the host: GBM keeps its exact log-normal scheme, OU and CIR
their SIMD steppers. Every other process accepts the host markers only until it
gains a kernel (see the backends concept page). The GPU back-ends
run one device thread per path with the whole Euler–Maruyama recursion in the
kernel and counter-hashed Box–Muller normals (sample_par is one launch for
all m paths):
| Backend | Feature | Device | Precision |
|---|---|---|---|
Cuda | cuda | NVIDIA, cudarc + NVRTC | f32 or f64, after T |
Metal | metal | Apple GPUs, hand-written MSL | f32 |
The device seed is drawn from the process's own seed source (the same
Deterministic seed value gives the same paths, consecutive calls advance the
stream, Unseeded draws fresh entropy).
The device kernels share one integer hash for their uniforms, so the device
back-ends agree with each other seed for seed up to libm rounding; the host
path is the process's own stream, so CPU and device paths agree in
distribution rather than bit for bit. A process joins the engine by declaring one
of its families inside the crate; 129 of the 131 do, and every process answers
device_ready() for the configuration it holds — a false samples on the
host, bit-identically to the Cpu build.
// docs: processes#euler-engine
//! Backs the Euler-engine example on the processes page.
use stochastic_rs::stochastic::device::Cpu;
use stochastic_rs::stochastic::diffusion::gbm::Gbm;
use stochastic_rs::traits::ProcessExt;
use stochastic_rs_core::simd_rng::Deterministic;
#[test]
fn euler_engine_prices_a_forward() {
let gbm = Gbm::new(
0.05,
0.2,
253,
Some(100.0),
Some(1.0),
Deterministic::new(7),
);
// The backend is a type parameter of the process: `Cpu` (the default) is the
// process's own SIMD sampler; `.on::<Metal>()` or `.on::<Cuda>()` (with the
// matching feature) run the Euler kernel on the device through the very
// same `sample_par` call.
let paths = gbm.clone().on::<Cpu>().sample_par(20_000); // Vec<Array1<f64>>, 20_000 × 253
assert_eq!(paths.len(), 20_000);
let terminal_mean = paths.iter().map(|p| p[252]).sum::<f64>() / 20_000.0;
assert!((terminal_mean / (100.0 * 0.05_f64.exp()) - 1.0).abs() < 0.01);
// `on::<Cpu>()` changes nothing: it is exactly what the process returns.
assert_eq!(paths[0].to_vec(), gbm.sample_par(20_000)[0].to_vec());
}import numpy as np
import stochastic_rs as srs
# PyGbm(mu, sigma, n, x0=None, t=None, seed=None, dtype=None, device=None)
paths = srs.PyGbm(0.05, 0.2, 253, x0=100.0, t=1.0, seed=7).sample_par(20_000)
print(paths.shape, paths[:, -1].mean()) # (20000, 253), ≈ 100·e^{0.05}
ou = srs.PyOu(2.0, 1.0, 0.5, 501, x0=1.0, t=2.0).sample_par(10_000)
# device= names one backend: "cuda", "metal" or "accelerate", each needing
# the matching cargo feature of the build;
# the single-precision devices want dtype="f32" and return float32 arrays
gpu = srs.PyGbm(0.05, 0.2, 253, x0=100.0, t=1.0, seed=7, dtype="f32", device="metal").sample_par(20_000)Volterra (stochastic Volterra equations)
Solved by Markovian lift at O(n N') rather than the naive O(n^2)
convolution. See stochastic_rs::stochastic::volterra.
| Type | What it is |
|---|---|
VolterraSde | The general equation X_t = X_0 + ∫ K(t-s) b(s,X_s) ds + ∫ K(t-s) σ(s,X_s) dW_s |
VolterraSquareRoot | The Volterra Heston variance leg, nonnegative by construction |
GaussianPolynomialVolatility | Volatility as a polynomial of a Gaussian Volterra process, incl. the quintic parameterisation |
Kernels implementing VolterraKernel: RlKernel (Riemann–Liouville),
ExponentialKernel (exact at one mode), GammaKernel, SumOfExponentials.
Comparison with QuantLib and RustQuant
An honest feature-coverage comparison of stochastic-rs, QuantLib and RustQuant, including where each library is the better choice.
Distributions
26 of 36 SIMD distribution structs with full Python + closed-form parity. Bulk samplers; ziggurat, rejection, inversion.