Packages

Core mathematical functions for VIVA - sentient digital life. PAD emotions, Cusp catastrophe, Free Energy Principle, attractor dynamics.

Current section

Files

Jump to
viva_math src viva_math@ou.erl
Raw

src/viva_math@ou.erl

-module(viva_math@ou).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/viva_math/ou.gleam").
-export([is_valid/1, is_valid_vec3/1, step/4, simulate/5, mean_at/3, variance_at/3, stationary_variance/1, stationary_std/1, autocovariance/2, half_life/1, step_vec3/4, simulate_vec3/5, mean_at_vec3/3, variance_at_vec3/3, stationary_variance_vec3/1]).
-export_type([o_u_params1_d/0, o_u_params_vec3/0, kernel/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Ornstein-Uhlenbeck mood dynamics.\n"
"\n"
" Mean-reverting stochastic process for affective dynamics — the canonical\n"
" model for emotion regulation toward a baseline. Underlies VIVA's\n"
" homeostatic emotional decay.\n"
"\n"
" **SDE**: `dX_t = θ(μ - X_t) dt + σ dW_t`\n"
"\n"
" Parameters:\n"
" - `theta` (θ) — mean-reversion speed (> 0). Larger = faster return to μ.\n"
" - `mu` (μ) — long-run mean (the attractor).\n"
" - `sigma` (σ) — diffusion (volatility, ≥ 0).\n"
"\n"
" ## Analytical properties\n"
"\n"
" Given `X_0 = x_0`:\n"
" - `E[X_t] = μ + (x_0 − μ) · e^(−θt)`\n"
" - `Var[X_t] = σ² / (2θ) · (1 − e^(−2θt))`\n"
" - Stationary: `X_∞ ~ N(μ, σ²/(2θ))`\n"
" - Autocovariance at lag `τ`: `σ²/(2θ) · e^(−θ|τ|)`\n"
" - Half-life of expectation: `ln(2) / θ`\n"
"\n"
" ## Integration\n"
"\n"
" `step` uses the **exact transition kernel** (Doob 1942) — no\n"
" discretization error regardless of `dt`. Closed form:\n"
"\n"
" ```\n"
" X_{t+Δ} = μ + (X_t − μ)·e^(−θΔ) + σ·sqrt((1 − e^(−2θΔ))/(2θ)) · Z\n"
" ```\n"
"\n"
" where `Z ~ N(0, 1)`. For Euler-Maruyama on the same SDE, use\n"
" `ode.euler_maruyama` directly with a custom drift/diffusion.\n"
"\n"
" ## References\n"
"\n"
" - Uhlenbeck & Ornstein (1930) — *On the theory of Brownian motion*\n"
" - Oravecz, Tuerlinckx & Vandekerckhove (2009) — *Ornstein-Uhlenbeck\n"
" Process in Affective Dynamics*\n"
" - Doob (1942) — *The Brownian Movement and Stochastic Equations*\n"
).
-type o_u_params1_d() :: {o_u_params1_d, float(), float(), float()}.
-type o_u_params_vec3() :: {o_u_params_vec3,
viva_math@vector:vec3(),
viva_math@vector:vec3(),
viva_math@vector:vec3()}.
-type kernel() :: {kernel, float(), float()}.
-file("src/viva_math/ou.gleam", 73).
?DOC(
" Check whether 1D parameters are physically meaningful.\n"
"\n"
" Requires `theta > 0` (otherwise no mean-reversion) and `sigma >= 0`.\n"
).
-spec is_valid(o_u_params1_d()) -> boolean().
is_valid(Params) ->
(erlang:element(2, Params) > +0.0) andalso (erlang:element(4, Params) >= +0.0).
-file("src/viva_math/ou.gleam", 79).
?DOC(
" Vec3 validity — every component of `theta` strictly positive and every\n"
" component of `sigma` non-negative.\n"
).
-spec is_valid_vec3(o_u_params_vec3()) -> boolean().
is_valid_vec3(Params) ->
(((((erlang:element(2, erlang:element(2, Params)) > +0.0) andalso (erlang:element(
3,
erlang:element(2, Params)
)
> +0.0))
andalso (erlang:element(4, erlang:element(2, Params)) > +0.0))
andalso (erlang:element(2, erlang:element(4, Params)) >= +0.0))
andalso (erlang:element(3, erlang:element(4, Params)) >= +0.0))
andalso (erlang:element(4, erlang:element(4, Params)) >= +0.0).
-file("src/viva_math/ou.gleam", 103).
?DOC(
" One step of the exact OU transition kernel.\n"
"\n"
" `X_{t+dt} = μ + (X_t − μ)·e^(−θ·dt) + σ·sqrt((1 − e^(−2θ·dt))/(2θ)) · Z`\n"
"\n"
" `Z ~ N(0, 1)`. No discretization error: works correctly even for large `dt`.\n"
"\n"
" **Caller-validated inputs**: callers must ensure `theta > 0`, `sigma >= 0`,\n"
" and `dt >= 0` (use `is_valid` for the params). With `dt < 0` the variance\n"
" term goes negative and `std_term` silently collapses to `0.0`, producing\n"
" a deterministic backward step that consumes a normal draw without using\n"
" it — not a physically meaningful transition.\n"
).
-spec step(o_u_params1_d(), float(), float(), viva_math@random:seed()) -> {float(),
viva_math@random:seed()}.
step(Params, X, Dt, Seed) ->
{o_u_params1_d, Theta, Mu, Sigma} = Params,
Decay = math:exp(+0.0 - (Theta * Dt)),
Drift_term = Mu + ((X - Mu) * Decay),
Var_term = case (2.0 * Theta) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> (Sigma * Sigma) * (+0.0 - viva_math@scalar:expm1(
+0.0 - ((2.0 * Theta) * Dt)
))
/ Gleam@denominator
end,
Std_term = case gleam@float:square_root(Var_term) of
{ok, S} ->
S;
{error, _} ->
+0.0
end,
{Z, New_seed} = viva_math@random:standard_normal(Seed),
{Drift_term + (Std_term * Z), New_seed}.
-file("src/viva_math/ou.gleam", 156).
-spec simulate_loop(
float(),
float(),
float(),
float(),
integer(),
viva_math@random:seed(),
list(float())
) -> {list(float()), viva_math@random:seed()}.
simulate_loop(Decay, Mu, Std, X, N, Seed, Acc) ->
case N =< 0 of
true ->
{Acc, Seed};
false ->
{Z, S_next} = viva_math@random:standard_normal(Seed),
X_next = (Mu + ((X - Mu) * Decay)) + (Std * Z),
simulate_loop(Decay, Mu, Std, X_next, N - 1, S_next, [X_next | Acc])
end.
-file("src/viva_math/ou.gleam", 134).
?DOC(
" Simulate `n` steps starting from `x0` with constant time-step `dt`.\n"
"\n"
" Returns the trajectory **excluding** the initial point (length `n`) and the\n"
" final seed for chaining.\n"
"\n"
" Pre-computes the transition kernel (`decay`, `std`) once — the loop only\n"
" does a multiply-add and a normal draw per step.\n"
).
-spec simulate(
o_u_params1_d(),
float(),
float(),
integer(),
viva_math@random:seed()
) -> {list(float()), viva_math@random:seed()}.
simulate(Params, X0, Dt, N, Seed) ->
{o_u_params1_d, Theta, Mu, Sigma} = Params,
Decay = math:exp(+0.0 - (Theta * Dt)),
Var_term = case (2.0 * Theta) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> (Sigma * Sigma) * (+0.0 - viva_math@scalar:expm1(
+0.0 - ((2.0 * Theta) * Dt)
))
/ Gleam@denominator
end,
Std_term = case gleam@float:square_root(Var_term) of
{ok, S} ->
S;
{error, _} ->
+0.0
end,
{Traj, S@1} = simulate_loop(Decay, Mu, Std_term, X0, N, Seed, []),
{lists:reverse(Traj), S@1}.
-file("src/viva_math/ou.gleam", 182).
?DOC(
" Closed-form `E[X_t | X_0 = x0]`.\n"
"\n"
" `μ + (x0 − μ) · e^(−θ·t)`\n"
).
-spec mean_at(o_u_params1_d(), float(), float()) -> float().
mean_at(Params, X0, T) ->
{o_u_params1_d, Theta, Mu, _} = Params,
Decay = math:exp(+0.0 - (Theta * T)),
Mu + ((X0 - Mu) * Decay).
-file("src/viva_math/ou.gleam", 199).
?DOC(
" Closed-form `Var[X_t | X_0 = x0]`.\n"
"\n"
" `σ² / (2θ) · (1 − e^(−2θ·t))`\n"
"\n"
" **Note**: the conditional variance is **independent of `x0`** — OU's noise\n"
" is additive Brownian, so all `x0`-dependence is absorbed into the mean.\n"
" The parameter is kept in the signature only to mirror `mean_at` and\n"
" `variance_at_vec3` for API symmetry; pass any value.\n"
"\n"
" Routed through `scalar.expm1` so the Brownian limit `σ²·t` (as `θ·t → 0`)\n"
" is recovered without catastrophic cancellation.\n"
).
-spec variance_at(o_u_params1_d(), float(), float()) -> float().
variance_at(Params, _, T) ->
{o_u_params1_d, Theta, _, Sigma} = Params,
case (2.0 * Theta) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> (Sigma * Sigma) * (+0.0 - viva_math@scalar:expm1(
+0.0 - ((2.0 * Theta) * T)
))
/ Gleam@denominator
end.
-file("src/viva_math/ou.gleam", 209).
?DOC(" Stationary variance `σ² / (2θ)` — the variance of `X_∞ ~ N(μ, σ²/(2θ))`.\n").
-spec stationary_variance(o_u_params1_d()) -> float().
stationary_variance(Params) ->
{o_u_params1_d, Theta, _, Sigma} = Params,
case (2.0 * Theta) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> Sigma * Sigma / Gleam@denominator
end.
-file("src/viva_math/ou.gleam", 215).
?DOC(" Stationary standard deviation.\n").
-spec stationary_std(o_u_params1_d()) -> float().
stationary_std(Params) ->
case gleam@float:square_root(stationary_variance(Params)) of
{ok, S} ->
S;
{error, _} ->
+0.0
end.
-file("src/viva_math/ou.gleam", 225).
?DOC(
" Autocovariance at lag `τ` (in time units).\n"
"\n"
" `Cov(X_s, X_{s+τ}) = σ²/(2θ) · e^(−θ·|τ|)` (stationary regime).\n"
).
-spec autocovariance(o_u_params1_d(), float()) -> float().
autocovariance(Params, Lag) ->
Abs_lag = gleam@float:absolute_value(Lag),
stationary_variance(Params) * math:exp(
+0.0 - (erlang:element(2, Params) * Abs_lag)
).
-file("src/viva_math/ou.gleam", 233).
?DOC(
" Half-life of mean reversion: `ln(2) / θ`.\n"
"\n"
" Time at which `E[X_t]` has covered half the gap toward `μ`.\n"
).
-spec half_life(o_u_params1_d()) -> float().
half_life(Params) ->
case viva_math@scalar:logarithm(2.0) of
{ok, L} ->
case erlang:element(2, Params) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> L / Gleam@denominator
end;
{error, _} ->
+0.0
end.
-file("src/viva_math/ou.gleam", 246).
?DOC(
" One Vec3 OU step. Each axis (P, A, D) updated independently via the exact\n"
" 1D kernel. Three normals drawn from the seed in sequence.\n"
).
-spec step_vec3(
o_u_params_vec3(),
viva_math@vector:vec3(),
float(),
viva_math@random:seed()
) -> {viva_math@vector:vec3(), viva_math@random:seed()}.
step_vec3(Params, X, Dt, Seed) ->
{o_u_params_vec3, Th, Mu, Sg} = Params,
{Px, S1} = step(
{o_u_params1_d,
erlang:element(2, Th),
erlang:element(2, Mu),
erlang:element(2, Sg)},
erlang:element(2, X),
Dt,
Seed
),
{Py, S2} = step(
{o_u_params1_d,
erlang:element(3, Th),
erlang:element(3, Mu),
erlang:element(3, Sg)},
erlang:element(3, X),
Dt,
S1
),
{Pz, S3} = step(
{o_u_params1_d,
erlang:element(4, Th),
erlang:element(4, Mu),
erlang:element(4, Sg)},
erlang:element(4, X),
Dt,
S2
),
{{vec3, Px, Py, Pz}, S3}.
-file("src/viva_math/ou.gleam", 297).
-spec simulate_vec3_loop(
kernel(),
kernel(),
kernel(),
viva_math@vector:vec3(),
viva_math@vector:vec3(),
integer(),
viva_math@random:seed(),
list(viva_math@vector:vec3())
) -> {list(viva_math@vector:vec3()), viva_math@random:seed()}.
simulate_vec3_loop(Kx, Ky, Kz, Mu, X, N, Seed, Acc) ->
case N =< 0 of
true ->
{Acc, Seed};
false ->
{Zx, S1} = viva_math@random:standard_normal(Seed),
{Zy, S2} = viva_math@random:standard_normal(S1),
{Zz, S3} = viva_math@random:standard_normal(S2),
Nx = (erlang:element(2, Mu) + ((erlang:element(2, X) - erlang:element(
2,
Mu
))
* erlang:element(2, Kx)))
+ (erlang:element(3, Kx) * Zx),
Ny = (erlang:element(3, Mu) + ((erlang:element(3, X) - erlang:element(
3,
Mu
))
* erlang:element(2, Ky)))
+ (erlang:element(3, Ky) * Zy),
Nz = (erlang:element(4, Mu) + ((erlang:element(4, X) - erlang:element(
4,
Mu
))
* erlang:element(2, Kz)))
+ (erlang:element(3, Kz) * Zz),
X_next = {vec3, Nx, Ny, Nz},
simulate_vec3_loop(
Kx,
Ky,
Kz,
Mu,
X_next,
N - 1,
S3,
[X_next | Acc]
)
end.
-file("src/viva_math/ou.gleam", 283).
-spec build_kernel(float(), float(), float()) -> kernel().
build_kernel(Theta, Sigma, Dt) ->
Decay = math:exp(+0.0 - (Theta * Dt)),
Var_term = case (2.0 * Theta) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> (Sigma * Sigma) * (+0.0 - viva_math@scalar:expm1(
+0.0 - ((2.0 * Theta) * Dt)
))
/ Gleam@denominator
end,
Std = case gleam@float:square_root(Var_term) of
{ok, S} ->
S;
{error, _} ->
+0.0
end,
{kernel, Decay, Std}.
-file("src/viva_math/ou.gleam", 263).
?DOC(
" Simulate Vec3 trajectory. Returns `n` Vec3 points excluding initial.\n"
"\n"
" Pre-computes the three componentwise transition kernels once — the loop\n"
" only does multiply-adds and three normal draws per step.\n"
).
-spec simulate_vec3(
o_u_params_vec3(),
viva_math@vector:vec3(),
float(),
integer(),
viva_math@random:seed()
) -> {list(viva_math@vector:vec3()), viva_math@random:seed()}.
simulate_vec3(Params, X0, Dt, N, Seed) ->
{o_u_params_vec3, Th, Mu, Sg} = Params,
Kx = build_kernel(erlang:element(2, Th), erlang:element(2, Sg), Dt),
Ky = build_kernel(erlang:element(3, Th), erlang:element(3, Sg), Dt),
Kz = build_kernel(erlang:element(4, Th), erlang:element(4, Sg), Dt),
{Traj, S} = simulate_vec3_loop(Kx, Ky, Kz, Mu, X0, N, Seed, []),
{lists:reverse(Traj), S}.
-file("src/viva_math/ou.gleam", 323).
?DOC(" Closed-form `E[X_t | X_0 = x0]` componentwise.\n").
-spec mean_at_vec3(o_u_params_vec3(), viva_math@vector:vec3(), float()) -> viva_math@vector:vec3().
mean_at_vec3(Params, X0, T) ->
{vec3,
mean_at(
{o_u_params1_d,
erlang:element(2, erlang:element(2, Params)),
erlang:element(2, erlang:element(3, Params)),
erlang:element(2, erlang:element(4, Params))},
erlang:element(2, X0),
T
),
mean_at(
{o_u_params1_d,
erlang:element(3, erlang:element(2, Params)),
erlang:element(3, erlang:element(3, Params)),
erlang:element(3, erlang:element(4, Params))},
erlang:element(3, X0),
T
),
mean_at(
{o_u_params1_d,
erlang:element(4, erlang:element(2, Params)),
erlang:element(4, erlang:element(3, Params)),
erlang:element(4, erlang:element(4, Params))},
erlang:element(4, X0),
T
)}.
-file("src/viva_math/ou.gleam", 332).
?DOC(" Closed-form `Var[X_t | X_0 = x0]` componentwise.\n").
-spec variance_at_vec3(o_u_params_vec3(), viva_math@vector:vec3(), float()) -> viva_math@vector:vec3().
variance_at_vec3(Params, X0, T) ->
{vec3,
variance_at(
{o_u_params1_d,
erlang:element(2, erlang:element(2, Params)),
erlang:element(2, erlang:element(3, Params)),
erlang:element(2, erlang:element(4, Params))},
erlang:element(2, X0),
T
),
variance_at(
{o_u_params1_d,
erlang:element(3, erlang:element(2, Params)),
erlang:element(3, erlang:element(3, Params)),
erlang:element(3, erlang:element(4, Params))},
erlang:element(3, X0),
T
),
variance_at(
{o_u_params1_d,
erlang:element(4, erlang:element(2, Params)),
erlang:element(4, erlang:element(3, Params)),
erlang:element(4, erlang:element(4, Params))},
erlang:element(4, X0),
T
)}.
-file("src/viva_math/ou.gleam", 353).
?DOC(" Stationary variance per axis.\n").
-spec stationary_variance_vec3(o_u_params_vec3()) -> viva_math@vector:vec3().
stationary_variance_vec3(Params) ->
{vec3,
stationary_variance(
{o_u_params1_d,
erlang:element(2, erlang:element(2, Params)),
erlang:element(2, erlang:element(3, Params)),
erlang:element(2, erlang:element(4, Params))}
),
stationary_variance(
{o_u_params1_d,
erlang:element(3, erlang:element(2, Params)),
erlang:element(3, erlang:element(3, Params)),
erlang:element(3, erlang:element(4, Params))}
),
stationary_variance(
{o_u_params1_d,
erlang:element(4, erlang:element(2, Params)),
erlang:element(4, erlang:element(3, Params)),
erlang:element(4, erlang:element(4, Params))}
)}.