stochastic-rs

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

FamilyCountExamples
Diffusion36OU, GBM (log + standard + correlated multi-asset), CIR, CEV, CKLS, Aït-Sahalia, Pearson, Jacobi, regime-switching, Fouque, Wishart (matrix-valued CIR)
Core building blocks20Brownian 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
Jump17Merton, Kou, CGMY, NIG, VG, bilateral gamma, KoBoL, RDTS, CTS, Bates, Lévy diffusion
Volatility16Heston (+ log, 2D, multifactor, stochastic-local), SABR (+ multifactor), Bergomi, rough Bergomi, rough Heston, double-Heston, BNS, HKDE, Bates SVJ (+ fractional), SVCGMY
Interest rate16Vasicek (+ 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 / GARCH9AR(p), MA(q), ARIMA, SARIMA, ARCH, GARCH, EGARCH, GJR-GARCH, AGARCH
Noise6Fractional Gaussian noise, Gaussian noise, white noise, correlated FGN / GN, k-dimensional correlated increments from a correlation matrix
Rough (Riemann–Liouville lift)4RL Black-Scholes, RL fBM, RL Heston, RL fOU
Stochastic correlation4Van Emmerich / Jacobi-type, Teng-modified OU, general transformed-OU, Heston with stochastic correlation
Sheet1Fractional Brownian sheet (Fbs)
Volterra3See 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 needStart withThen
A price path with constant volatilityGbm (exact log-normal steps)GbmLog for the log-price, Cev for a local-vol elasticity
Mean reversionOu (Gaussian), Cir (positive, square-root), Vasicek / HullWhite for ratesCirPlusPlus to fit a term structure, Cheyette for a quasi-Gaussian HJM
Stochastic volatilityHeston (Euler / QE schemes), Sabr, BergomiDoubleHeston, MultifactorHeston, HestonStochCorr, HestonSlv
Rough volatilityRoughBergomi (hybrid scheme), RoughHeston, RlHeston (Markov lift)the volterra module for a general kernel
Long memory / fractional noiseFgn → Fbm, Fou, Fcir, Fgbmthese are the processes with FFT device backends
JumpsMerton, Kou, Bates1996, CompoundPoisson, HawkesLevyDiffusion with any Distribution for the jump size
Several correlated assetsMcgns (driver) → MultiGbm, Wishart (covariance)Cfgns for correlated fractional noise
A rates curve, not a rateHjm, Lmm, Bgm, AdgG2pp-style two-factor: HullWhite2F, Cir2F
Time-series styleAr, Arima, Garch / Egarch / GjrGarch, Sarimathe stats crate fits them

Examples

Geometric Brownian Motion

The textbook log-normal diffusion dSt=μSt dt+σSt dWtdS_t = \mu S_t\,dt + \sigma S_t\,dW_t.

tests/doctest_processes_gbm.rs
// 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 dSt=μSt dt+Vt St dWt1dS_t = \mu S_t\,dt + \sqrt{V_t}\,S_t\,dW^1_t, dVt=κ(θ−Vt) dt+σVt dWt2dV_t = \kappa(\theta - V_t)\,dt + \sigma\sqrt{V_t}\,dW^2_t with d⟨W1,W2⟩t=ρ dtd\langle W^1, W^2\rangle_t = \rho\,dt.

tests/doctest_processes_heston.rs
// 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 arrays

Discretisation 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():

tests/doctest_processes_heston_qe.rs
// 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 L(t,S)L(t, S) and a mixing fraction η\eta: dSt=μSt dt+L(t,St)Vt St dWt1dS_t = \mu S_t\,dt + L(t, S_t)\sqrt{V_t}\,S_t\,dW^1_t, dVt=κ(θ−Vt) dt+ησVt dWt2dV_t = \kappa(\theta - V_t)\,dt + \eta\sigma\sqrt{V_t}\,dW^2_t. 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 η=1\eta = 1 and L≡1L \equiv 1 the path is HestonLog's, bit for bit. The quant page calibrates LL to a vanilla surface.

tests/doctest_processes_heston_slv.rs
// 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 arrays

Fractional Brownian motion

Roughness controlled by the Hurst parameter H∈(0,1)H \in (0, 1). H=0.5H = 0.5 recovers standard Brownian motion; H<0.5H < 0.5 is rough, H>0.5H > 0.5 is persistent.

tests/doctest_processes_fgn.rs
// 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.

tests/doctest_processes_qmc.rs
// 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.

tests/doctest_processes_multi_gbm.rs
// 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))    # ≈ rho

Wishart 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.

tests/doctest_processes_wishart.rs
// 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.

tests/doctest_processes_cheyette.rs
// 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 state

Common 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 n

sample_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 branch

Backends: 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:

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):

BackendFeatureDevicePrecision
CudacudaNVIDIA, cudarc + NVRTCf32 or f64, after T
MetalmetalApple GPUs, hand-written MSLf32

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.

tests/doctest_stochastic_euler_engine.rs
// 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.

TypeWhat it is
VolterraSdeThe general equation X_t = X_0 + ∫ K(t-s) b(s,X_s) ds + ∫ K(t-s) σ(s,X_s) dW_s
VolterraSquareRootThe Volterra Heston variance leg, nonnegative by construction
GaussianPolynomialVolatilityVolatility 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.

On this page