use serde::{Deserialize, Serialize};
use crate::mood::{Emotion, MoodHistory, MoodVector};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AffectiveMetrics {
pub complexity: f32,
pub granularity: f32,
pub inertia: f32,
pub variability: f32,
}
impl AffectiveMetrics {
#[must_use]
pub fn zero() -> Self {
Self {
complexity: 0.0,
granularity: 0.0,
inertia: 0.0,
variability: 0.0,
}
}
}
const ACTIVE_THRESHOLD: f32 = 0.1;
#[must_use]
#[inline]
fn emotional_complexity(mood: &MoodVector) -> f32 {
Emotion::ALL
.iter()
.filter(|&&e| mood.get(e).abs() > ACTIVE_THRESHOLD)
.count() as f32
}
#[must_use]
#[inline]
fn emotional_granularity(mood: &MoodVector) -> f32 {
let mut total = 0.0f32;
for &e in Emotion::ALL {
total += mood.get(e).abs();
}
if total < f32::EPSILON {
return 0.0;
}
let mut entropy = 0.0f32;
for &e in Emotion::ALL {
let p = mood.get(e).abs() / total;
if p > f32::EPSILON {
entropy -= p * p.ln();
}
}
entropy
}
#[must_use]
fn mood_distance(a: &MoodVector, b: &MoodVector) -> f32 {
let mut sum = 0.0f32;
for &e in Emotion::ALL {
let diff = a.get(e) - b.get(e);
sum += diff * diff;
}
sum.sqrt()
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn compute_affective_metrics(history: &MoodHistory) -> AffectiveMetrics {
if history.is_empty() {
return AffectiveMetrics::zero();
}
let snapshots: Vec<_> = history.iter().collect();
let n = snapshots.len();
let complexity = snapshots
.iter()
.map(|s| emotional_complexity(&s.mood))
.sum::<f32>()
/ n as f32;
let granularity = snapshots
.iter()
.map(|s| emotional_granularity(&s.mood))
.sum::<f32>()
/ n as f32;
if n < 2 {
return AffectiveMetrics {
complexity,
granularity,
inertia: 0.0,
variability: 0.0,
};
}
let variability = snapshots
.windows(2)
.map(|w| mood_distance(&w[0].mood, &w[1].mood))
.sum::<f32>()
/ (n - 1) as f32;
let intensities: Vec<f32> = snapshots.iter().map(|s| s.mood.intensity()).collect();
let inertia = lag1_autocorrelation(&intensities);
AffectiveMetrics {
complexity,
granularity,
inertia,
variability,
}
}
#[must_use]
#[inline]
pub fn snapshot_complexity(mood: &MoodVector) -> f32 {
emotional_complexity(mood)
}
#[must_use]
#[inline]
pub fn snapshot_granularity(mood: &MoodVector) -> f32 {
emotional_granularity(mood)
}
fn lag1_autocorrelation(values: &[f32]) -> f32 {
let n = values.len();
if n < 2 {
return 0.0;
}
let mean = values.iter().sum::<f32>() / n as f32;
let mut numerator = 0.0f32;
let mut denominator = 0.0f32;
for i in 0..n - 1 {
numerator += (values[i] - mean) * (values[i + 1] - mean);
}
for v in values {
denominator += (v - mean) * (v - mean);
}
if denominator.abs() < f32::EPSILON {
return 0.0;
}
(numerator / denominator).clamp(-1.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mood::{MoodHistory, MoodSnapshot, MoodState};
use chrono::Utc;
fn make_snapshot(joy: f32, arousal: f32) -> MoodSnapshot {
let mut mood = MoodVector::neutral();
mood.set(Emotion::Joy, joy);
mood.set(Emotion::Arousal, arousal);
MoodSnapshot {
mood,
state: MoodState::Calm,
deviation: 0.0,
timestamp: Utc::now(),
}
}
#[test]
fn test_empty_history() {
let h = MoodHistory::new(10);
let m = compute_affective_metrics(&h);
assert!(m.complexity.abs() < f32::EPSILON);
assert!(m.granularity.abs() < f32::EPSILON);
assert!(m.inertia.abs() < f32::EPSILON);
assert!(m.variability.abs() < f32::EPSILON);
}
#[test]
fn test_single_snapshot() {
let mut h = MoodHistory::new(10);
h.record(make_snapshot(0.5, 0.3));
let m = compute_affective_metrics(&h);
assert!(m.complexity > 0.0);
assert!(m.granularity > 0.0);
assert!(m.inertia.abs() < f32::EPSILON); assert!(m.variability.abs() < f32::EPSILON); }
#[test]
fn test_stable_mood_low_variability() {
let mut h = MoodHistory::new(10);
for _ in 0..5 {
h.record(make_snapshot(0.5, 0.3));
}
let m = compute_affective_metrics(&h);
assert!(
m.variability < 0.01,
"stable mood should have low variability: {}",
m.variability
);
}
#[test]
fn test_volatile_mood_high_variability() {
let mut h = MoodHistory::new(10);
for i in 0..6 {
let sign = if i % 2 == 0 { 1.0 } else { -1.0 };
h.record(make_snapshot(0.8 * sign, 0.5 * sign));
}
let m = compute_affective_metrics(&h);
assert!(
m.variability > 0.5,
"volatile mood should have high variability: {}",
m.variability
);
}
#[test]
fn test_high_inertia_trending() {
let mut h = MoodHistory::new(20);
for i in 0..10 {
let v = i as f32 * 0.1;
h.record(make_snapshot(v, v * 0.5));
}
let m = compute_affective_metrics(&h);
assert!(
m.inertia > 0.5,
"steadily increasing mood should have high inertia: {}",
m.inertia
);
}
#[test]
fn test_complexity_neutral() {
let mood = MoodVector::neutral();
assert!(snapshot_complexity(&mood) < f32::EPSILON);
}
#[test]
fn test_complexity_multi_emotion() {
let mut mood = MoodVector::neutral();
mood.set(Emotion::Joy, 0.5);
mood.set(Emotion::Arousal, 0.3);
mood.set(Emotion::Trust, 0.2);
assert!((snapshot_complexity(&mood) - 3.0).abs() < f32::EPSILON);
}
#[test]
fn test_granularity_single_emotion() {
let mut mood = MoodVector::neutral();
mood.set(Emotion::Joy, 0.8);
let g = snapshot_granularity(&mood);
assert!(g < 0.5, "single emotion granularity should be low: {g}");
}
#[test]
fn test_granularity_multi_emotion() {
let mut mood = MoodVector::neutral();
for &e in Emotion::ALL {
mood.set(e, 0.5);
}
let g = snapshot_granularity(&mood);
assert!(
g > 1.0,
"uniform emotions should have high granularity: {g}"
);
}
#[test]
fn test_zero_metrics() {
let m = AffectiveMetrics::zero();
assert!(m.complexity.abs() < f32::EPSILON);
}
#[test]
fn test_serde() {
let m = AffectiveMetrics {
complexity: 3.0,
granularity: 1.5,
inertia: 0.7,
variability: 0.3,
};
let json = serde_json::to_string(&m).unwrap();
let m2: AffectiveMetrics = serde_json::from_str(&json).unwrap();
assert!((m2.complexity - m.complexity).abs() < f32::EPSILON);
}
#[test]
fn test_lag1_autocorrelation_constant() {
assert!(lag1_autocorrelation(&[1.0, 1.0, 1.0, 1.0]).abs() < f32::EPSILON);
}
#[test]
fn test_lag1_autocorrelation_alternating() {
let r = lag1_autocorrelation(&[1.0, -1.0, 1.0, -1.0, 1.0]);
assert!(r < -0.5, "alternating should be negatively correlated: {r}");
}
}