viva_math/free_energy
Free Energy Principle (FEP) calculations.
Based on Karl Friston’s work (2010, 2019). Free Energy bounds surprise (negative log evidence) and can be decomposed as:
F = Π · (μ - o)² + D_KL(q || p) ↑ ↑ Accuracy Complexity (weighted (deviation prediction from priors) error)
In VIVA, this is used for interoception - sensing internal state and minimizing “surprise” through prediction.
References:
- Friston (2010) “The free-energy principle: a unified brain theory?”
- Parr & Friston (2019) “Generalised free energy and active inference”
- Validated by DeepSeek R1 671B (2025)
Types
Qualitative feeling based on free energy level.
pub type Feeling {
Homeostatic
Surprised
Alarmed
Overwhelmed
}
Constructors
-
HomeostaticLow free energy - predictions match reality (F < μ - σ)
-
SurprisedModerate free energy - slight mismatch (μ - σ ≤ F < μ)
-
AlarmedHigh free energy - significant mismatch (μ ≤ F < μ + σ)
-
OverwhelmedVery high free energy - system overwhelmed (F ≥ μ + σ)
Thresholds for feeling classification. Based on system-specific statistics (mean and standard deviation).
pub type FeelingThresholds {
FeelingThresholds(mean: Float, std_dev: Float)
}
Constructors
-
FeelingThresholds(mean: Float, std_dev: Float)Arguments
- mean
-
Mean free energy (baseline)
- std_dev
-
Standard deviation of free energy
Free Energy state for a system.
pub type FreeEnergyState {
FreeEnergyState(
free_energy: Float,
prediction_error: Float,
complexity: Float,
precision: Float,
feeling: Feeling,
)
}
Constructors
-
FreeEnergyState( free_energy: Float, prediction_error: Float, complexity: Float, precision: Float, feeling: Feeling, )Arguments
- free_energy
-
The free energy value (lower is better)
- prediction_error
-
Prediction error component (precision-weighted)
- complexity
-
Complexity/KL divergence component
- precision
-
Precision used for weighting
- feeling
-
Qualitative feeling based on normalized free energy
Values
pub fn active_inference_delta(
current: vector.Vec3,
target: vector.Vec3,
rate: Float,
) -> vector.Vec3
Active Inference: compute action that minimizes expected free energy.
This returns the delta to apply to current state to move toward target. Rate controls how quickly to move (0 = no movement, 1 = instant).
pub fn belief_update(
prior: Float,
observation: Float,
precision_prior: Float,
precision_likelihood: Float,
) -> Float
Bayesian belief update: combine prior with likelihood.
posterior ∝ likelihood × prior Using precision-weighted combination: new_belief = (Π_prior × prior + Π_likelihood × observation) / (Π_prior + Π_likelihood)
pub fn classify_feeling(free_energy: Float) -> Feeling
Legacy classify_feeling with fixed thresholds. Calibrated for PAD space (max distance ~3.46).
pub fn classify_feeling_normalized(
free_energy: Float,
thresholds: FeelingThresholds,
) -> Feeling
Classify feeling using normalized thresholds.
- Homeostatic: F < μ - σ (better than expected)
- Surprised: μ - σ ≤ F < μ (slightly worse)
- Alarmed: μ ≤ F < μ + σ (worse than average)
- Overwhelmed: F ≥ μ + σ (much worse)
pub fn complexity(
current: vector.Vec3,
baseline: vector.Vec3,
prior_variance: Float,
) -> Float
Compute complexity term using KL divergence.
Complexity = D_KL(q(θ) || p(θ))
Where q is posterior belief and p is prior belief (homeostatic setpoint). Weight controls the regularization strength.
pub fn complexity_weighted(
current: vector.Vec3,
baseline: vector.Vec3,
weight: Float,
) -> Float
Legacy complexity function for backwards compatibility.
pub fn compute_state(
expected: vector.Vec3,
actual: vector.Vec3,
baseline: vector.Vec3,
precision: Float,
prior_variance: Float,
thresholds: FeelingThresholds,
) -> FreeEnergyState
Compute free energy and return full state with feeling. Uses normalized thresholds for feeling classification.
pub fn compute_state_simple(
expected: vector.Vec3,
actual: vector.Vec3,
baseline: vector.Vec3,
complexity_weight: Float,
) -> FreeEnergyState
Simplified compute_state with default thresholds and legacy interface. For backwards compatibility.
pub fn default_thresholds() -> FeelingThresholds
Default thresholds calibrated for PAD space. Mean and std_dev derived from typical emotional dynamics.
pub fn estimate_precision(errors: List(Float)) -> Float
Estimate precision from recent prediction errors.
Precision = 1 / variance of errors Higher precision means more reliable predictions.
pub fn free_energy(
expected: vector.Vec3,
actual: vector.Vec3,
baseline: vector.Vec3,
precision: Float,
prior_variance: Float,
) -> Float
Compute full Free Energy: F = Π·(μ-o)² + D_KL(q||p)
Parameters
- expected: predicted/expected state (μ)
- actual: observed/actual state (o)
- baseline: prior baseline state (p) - e.g., personality/homeostatic setpoint
- precision: inverse variance of predictions (Π)
- prior_variance: variance of prior beliefs (for KL term)
pub fn gaussian_kl_divergence(
posterior_mean: vector.Vec3,
prior_mean: vector.Vec3,
variance: Float,
) -> Float
Compute KL divergence between Gaussian distributions (closed form).
For two Gaussians with same variance: D_KL(N(μ₁,σ²) || N(μ₂,σ²)) = (μ₁ - μ₂)² / (2σ²)
This measures how much the posterior (current belief) diverges from prior.
pub fn generalized_free_energy(
expected_state: vector.Vec3,
preferred_state: vector.Vec3,
uncertainty: Float,
) -> Float
Generalized Free Energy (expected free energy for planning).
G = ambiguity + risk
- ambiguity: expected surprise under model (epistemic value)
- risk: KL divergence from preferred outcomes (pragmatic value)
Used for action selection in active inference.
pub fn precision_weighted_error_vec(
expected: vector.Vec3,
actual: vector.Vec3,
precisions: vector.Vec3,
) -> Float
Precision-weighted prediction error for Vec3.
Each dimension can have different precision. Returns weighted sum of squared errors.
pub fn precision_weighted_prediction_error(
expected: vector.Vec3,
actual: vector.Vec3,
precision: Float,
) -> Float
Compute precision-weighted prediction error.
F_accuracy = Π · (expected - actual)²
Precision (Π) = 1/variance. Higher precision = more weight on prediction errors. This is critical for biological systems where uncertainty should attenuate errors.
pub fn prediction_error(
expected: vector.Vec3,
actual: vector.Vec3,
) -> Float
Compute raw prediction error between expected and actual state. Uses squared Euclidean distance (L2 loss).
pub fn surprise(
expected: Float,
observed: Float,
sigma: Float,
) -> Float
Compute surprise for a single dimension.
Surprise = -log(p(observation | model)) Using Gaussian approximation: surprise ∝ (x - μ)² / (2σ²)
pub fn update_thresholds(
current: FeelingThresholds,
observed_fe: Float,
alpha: Float,
) -> FeelingThresholds
Update thresholds based on observed free energy history. Uses exponential moving average for online learning.
pub fn variational_bound(
observation_likelihood: Float,
kl_divergence: Float,
) -> Float
Variational Free Energy bound.
F ≤ -log p(o) + D_KL(q||p)
The free energy bounds the negative log evidence (surprise).