Ornstein-Uhlenbeck Mood Dynamics

The Ornstein-Uhlenbeck (OU) process is the canonical model for mean-reverting stochastic processes — the kind of dynamic you want when modelling emotion regulation: noise pushes the system around, but a homeostatic pull brings it back toward a baseline.

dX_t = θ(μ − X_t) dt + σ dW_t
ParameterMeaning
θ (theta)Mean-reversion speed. Larger ⇒ faster return to μ.
μ (mu)Long-run mean (the attractor).
σ (sigma)Diffusion / volatility.

Scalar API

import viva_math/ou
import viva_math/random

let params = ou.OUParams1D(theta: 1.0, mu: 0.5, sigma: 0.3)
let seed = random.from_int(42)

// Single step — exact transition kernel (Doob 1942), no Euler discretisation.
let #(x1, seed1) = ou.step(params, 0.0, 0.1, seed)

// Full trajectory.
let #(traj, _) = ou.simulate(params, 0.0, 0.01, 1000, seed)

Closed-form moments

ou.mean_at(params, x0: 0.0, t: 5.0)
// μ + (x0 − μ)·e^(−θt)

ou.variance_at(params, _x0: 0.0, t: 5.0)
// σ²/(2θ) · (1 − e^(−2θt))

ou.stationary_variance(params)
// σ²/(2θ)

ou.stationary_std(params)
// σ/√(2θ)

ou.autocovariance(params, lag: 1.5)
// σ²/(2θ) · e^(−θ|τ|)

ou.half_life(params)
// ln(2)/θ

Why not Euler-Maruyama?

ou.step uses Doob’s exact transition kernel:

X_{t+Δ} = μ + (X_t − μ)·e^(−θΔ) + σ·√((1 − e^(−2θΔ))/(2θ)) · Z

with Z ~ N(0, 1). This gives zero discretisation error regardless of dt, unlike Euler-Maruyama which is O(√dt) weak. For Euler/Milstein on arbitrary SDEs, use viva_math/ode.

The variance factor routes through scalar.expm1 to avoid catastrophic cancellation when θ·dt → 0 — the Brownian limit σ²·dt is recovered without precision loss.

Vec3 — componentwise PAD dynamics

For affective dynamics across all three PAD axes:

import viva_math/ou
import viva_math/vector.{Vec3}

let params =
  ou.OUParamsVec3(
    theta: Vec3(0.5, 1.0, 0.7),    // arousal regulates fastest
    mu:    Vec3(0.2, 0.0, 0.1),    // mild positive baseline
    sigma: Vec3(0.1, 0.2, 0.1),    // arousal more volatile
  )

let seed = random.from_int(7)
let #(trajectory, _) =
  ou.simulate_vec3(params, vector.zero(), dt: 0.05, n: 200, seed)

Each axis evolves independently (diagonal covariance) — sufficient for most affective modelling. For cross-axis correlation, you’d need a multivariate extension (roadmap).

Numerical guarantees

PropertyTestTolerance
mean_at(t = 0) = x_0identities_test.gleam1e-15
variance_at(t = 0) = 0ou_test.gleam1e-12
step(dt = 0) = x_0identities_test.gleam1e-15
variance_at(t → ∞) = stationary_varianceidentities_test.gleam1e-12
Brownian limit σ²·t as θ·t → 0qcheck_test.gleamrel 1% at θ·t = 1e-9
autocov(0) = stationary_varianceqcheck_test.gleam1e-12

References

Search Document