viva_math/ou

Ornstein-Uhlenbeck mood dynamics.

Mean-reverting stochastic process for affective dynamics — the canonical model for emotion regulation toward a baseline. Underlies VIVA’s homeostatic emotional decay.

SDE: dX_t = θ(μ - X_t) dt + σ dW_t

Parameters:

Analytical properties

Given X_0 = x_0:

Integration

step uses the exact transition kernel (Doob 1942) — no discretization error regardless of dt. Closed form:

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

where Z ~ N(0, 1). For Euler-Maruyama on the same SDE, use ode.euler_maruyama directly with a custom drift/diffusion.

References

Types

Scalar Ornstein-Uhlenbeck parameters.

pub type OUParams1D {
  OUParams1D(theta: Float, mu: Float, sigma: Float)
}

Constructors

  • OUParams1D(theta: Float, mu: Float, sigma: Float)

Componentwise Ornstein-Uhlenbeck parameters over PAD.

Each axis (pleasure, arousal, dominance) gets its own θ, μ, σ — no cross-correlation between axes (diagonal covariance). Sufficient for affective dynamics where each dimension regulates independently.

pub type OUParamsVec3 {
  OUParamsVec3(
    theta: vector.Vec3,
    mu: vector.Vec3,
    sigma: vector.Vec3,
  )
}

Constructors

Values

pub fn autocovariance(params: OUParams1D, lag: Float) -> Float

Autocovariance at lag τ (in time units).

Cov(X_s, X_{s+τ}) = σ²/(2θ) · e^(−θ·|τ|) (stationary regime).

pub fn half_life(params: OUParams1D) -> Float

Half-life of mean reversion: ln(2) / θ.

Time at which E[X_t] has covered half the gap toward μ.

pub fn is_valid(params: OUParams1D) -> Bool

Check whether 1D parameters are physically meaningful.

Requires theta > 0 (otherwise no mean-reversion) and sigma >= 0.

pub fn is_valid_vec3(params: OUParamsVec3) -> Bool

Vec3 validity — every component of theta strictly positive and every component of sigma non-negative.

pub fn mean_at(params: OUParams1D, x0: Float, t: Float) -> Float

Closed-form E[X_t | X_0 = x0].

μ + (x0 − μ) · e^(−θ·t)

pub fn mean_at_vec3(
  params: OUParamsVec3,
  x0: vector.Vec3,
  t: Float,
) -> vector.Vec3

Closed-form E[X_t | X_0 = x0] componentwise.

pub fn simulate(
  params: OUParams1D,
  x0: Float,
  dt: Float,
  n: Int,
  seed: random.Seed,
) -> #(List(Float), random.Seed)

Simulate n steps starting from x0 with constant time-step dt.

Returns the trajectory excluding the initial point (length n) and the final seed for chaining.

Pre-computes the transition kernel (decay, std) once — the loop only does a multiply-add and a normal draw per step.

pub fn simulate_vec3(
  params: OUParamsVec3,
  x0: vector.Vec3,
  dt: Float,
  n: Int,
  seed: random.Seed,
) -> #(List(vector.Vec3), random.Seed)

Simulate Vec3 trajectory. Returns n Vec3 points excluding initial.

Pre-computes the three componentwise transition kernels once — the loop only does multiply-adds and three normal draws per step.

pub fn stationary_std(params: OUParams1D) -> Float

Stationary standard deviation.

pub fn stationary_variance(params: OUParams1D) -> Float

Stationary variance σ² / (2θ) — the variance of X_∞ ~ N(μ, σ²/(2θ)).

pub fn stationary_variance_vec3(
  params: OUParamsVec3,
) -> vector.Vec3

Stationary variance per axis.

pub fn step(
  params: OUParams1D,
  x: Float,
  dt: Float,
  seed: random.Seed,
) -> #(Float, random.Seed)

One step of the exact OU transition kernel.

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

Z ~ N(0, 1). No discretization error: works correctly even for large dt.

Caller-validated inputs: callers must ensure theta > 0, sigma >= 0, and dt >= 0 (use is_valid for the params). With dt < 0 the variance term goes negative and std_term silently collapses to 0.0, producing a deterministic backward step that consumes a normal draw without using it — not a physically meaningful transition.

pub fn step_vec3(
  params: OUParamsVec3,
  x: vector.Vec3,
  dt: Float,
  seed: random.Seed,
) -> #(vector.Vec3, random.Seed)

One Vec3 OU step. Each axis (P, A, D) updated independently via the exact 1D kernel. Three normals drawn from the seed in sequence.

pub fn variance_at(
  params: OUParams1D,
  x0: Float,
  t: Float,
) -> Float

Closed-form Var[X_t | X_0 = x0].

σ² / (2θ) · (1 − e^(−2θ·t))

Note: the conditional variance is independent of x0 — OU’s noise is additive Brownian, so all x0-dependence is absorbed into the mean. The parameter is kept in the signature only to mirror mean_at and variance_at_vec3 for API symmetry; pass any value.

Routed through scalar.expm1 so the Brownian limit σ²·t (as θ·t → 0) is recovered without catastrophic cancellation.

pub fn variance_at_vec3(
  params: OUParamsVec3,
  x0: vector.Vec3,
  t: Float,
) -> vector.Vec3

Closed-form Var[X_t | X_0 = x0] componentwise.

Search Document