use crate::memory::{MemoryLayers, MemoryTag};
use crate::state::{IndividualState, Mood};
use crate::types::Duration;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PrimingDeltas {
pub valence_delta: f32,
pub arousal_delta: f32,
}
impl PrimingDeltas {
#[must_use]
pub fn zero() -> Self {
PrimingDeltas {
valence_delta: 0.0,
arousal_delta: 0.0,
}
}
#[must_use]
pub fn new(valence_delta: f32, arousal_delta: f32) -> Self {
PrimingDeltas {
valence_delta,
arousal_delta,
}
}
}
impl Default for PrimingDeltas {
fn default() -> Self {
PrimingDeltas::zero()
}
}
const PRIMING_SCALE_PER_DAY: f32 = 0.01;
const MAX_PRIMING_EFFECT: f32 = 0.2;
const AROUSAL_PRIMING_SALIENCE_THRESHOLD: f32 = 0.5;
const AROUSAL_PRIMING_FACTOR: f32 = 0.02;
#[must_use]
pub fn compute_priming_deltas(memories: &MemoryLayers, mood: &Mood) -> PrimingDeltas {
if memories.is_empty() {
return PrimingDeltas::zero();
}
let valence = mood.valence_effective();
let mut valence_delta = 0.0;
let mut arousal_delta = 0.0;
let memory_count = memories.all_memories().count();
for memory in memories.all_memories() {
let salience = memory.salience();
let (tag_polarity, has_polarity_tags) = compute_tag_polarity(memory.tags());
let snapshot_valence = memory.emotional_snapshot().valence();
let memory_valence = if has_polarity_tags {
tag_polarity * 0.7 + snapshot_valence * 0.3
} else {
snapshot_valence
};
let congruence = compute_mood_congruence(valence, tag_polarity, has_polarity_tags);
valence_delta += memory_valence * salience * congruence * 0.1;
if salience >= AROUSAL_PRIMING_SALIENCE_THRESHOLD {
arousal_delta += salience * AROUSAL_PRIMING_FACTOR;
}
}
valence_delta /= memory_count as f32;
valence_delta = valence_delta.clamp(-MAX_PRIMING_EFFECT, MAX_PRIMING_EFFECT);
arousal_delta = arousal_delta.clamp(0.0, MAX_PRIMING_EFFECT);
PrimingDeltas::new(valence_delta, arousal_delta)
}
fn compute_tag_polarity(tags: &[MemoryTag]) -> (f32, bool) {
let mut negative_count = 0;
let mut positive_count = 0;
for tag in tags {
if tag.is_negative() {
negative_count += 1;
}
if tag.is_positive() {
positive_count += 1;
}
}
let total_polarity_tags = negative_count + positive_count;
if total_polarity_tags == 0 {
return (0.0, false);
}
let polarity = (positive_count as f32 - negative_count as f32) / total_polarity_tags as f32;
(polarity, true)
}
fn compute_mood_congruence(mood_valence: f32, tag_polarity: f32, has_polarity_tags: bool) -> f32 {
if !has_polarity_tags {
return 1.0;
}
if mood_valence.abs() < 0.1 {
return 1.0;
}
let mood_negative = mood_valence < 0.0;
let tag_negative = tag_polarity < 0.0;
if mood_negative == tag_negative && tag_polarity.abs() > 0.1 {
1.0 + mood_valence.abs() * 0.5 } else if tag_polarity.abs() < 0.1 {
1.0
} else {
1.0 - mood_valence.abs() * 0.3 }
}
#[must_use]
pub fn apply_memory_consolidation(
mut state: IndividualState,
memories: &MemoryLayers,
duration: Duration,
) -> IndividualState {
if duration.is_zero() {
return state;
}
if memories.is_empty() {
return state;
}
let priming = compute_priming_deltas(memories, state.mood());
let days = duration.as_days() as f32;
let scale = (days * PRIMING_SCALE_PER_DAY).min(1.0);
let scaled_valence = priming.valence_delta * scale;
let scaled_arousal = priming.arousal_delta * scale;
state.mood_mut().add_valence_delta(scaled_valence);
state.mood_mut().add_arousal_delta(scaled_arousal);
state
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::{EmotionalSnapshot, MemoryEntry, MemoryLayer, MemoryTag};
fn create_memory_with_emotion(valence: f32, arousal: f32, salience: f32) -> MemoryEntry {
MemoryEntry::new(Duration::days(1), "Test memory")
.with_emotional_snapshot(EmotionalSnapshot::new(valence, arousal, 0.0))
.with_salience(salience)
}
fn create_negative_memory(salience: f32) -> MemoryEntry {
create_memory_with_emotion(-0.7, 0.3, salience).add_tag(MemoryTag::Loss)
}
fn create_positive_memory(salience: f32) -> MemoryEntry {
create_memory_with_emotion(0.7, 0.3, salience).add_tag(MemoryTag::Achievement)
}
#[test]
fn negative_mood_increases_negative_memory_priming() {
let mut memories = MemoryLayers::new();
memories.add(MemoryLayer::ShortTerm, create_negative_memory(0.7));
memories.add(MemoryLayer::ShortTerm, create_positive_memory(0.7));
let negative_mood = Mood::new().with_valence_base(-0.5);
let negative_priming = compute_priming_deltas(&memories, &negative_mood);
let neutral_mood = Mood::new();
let neutral_priming = compute_priming_deltas(&memories, &neutral_mood);
assert!(negative_priming.valence_delta < neutral_priming.valence_delta);
}
#[test]
fn positive_mood_increases_positive_memory_priming() {
let mut memories = MemoryLayers::new();
memories.add(MemoryLayer::ShortTerm, create_negative_memory(0.7));
memories.add(MemoryLayer::ShortTerm, create_positive_memory(0.7));
let positive_mood = Mood::new().with_valence_base(0.5);
let positive_priming = compute_priming_deltas(&memories, &positive_mood);
let neutral_mood = Mood::new();
let neutral_priming = compute_priming_deltas(&memories, &neutral_mood);
assert!(positive_priming.valence_delta > neutral_priming.valence_delta);
}
#[test]
fn priming_affects_valence_delta() {
let mut memories = MemoryLayers::new();
for _ in 0..5 {
memories.add(MemoryLayer::ShortTerm, create_negative_memory(0.8));
}
let state = IndividualState::new();
let original_valence = state.mood().valence_delta();
let modified = apply_memory_consolidation(state, &memories, Duration::days(30));
assert!((modified.mood().valence_delta() - original_valence).abs() > f32::EPSILON);
}
#[test]
fn priming_affects_arousal_delta() {
let mut memories = MemoryLayers::new();
for _ in 0..5 {
memories.add(
MemoryLayer::ShortTerm,
create_memory_with_emotion(0.0, 0.0, 0.8), );
}
let state = IndividualState::new();
let original_arousal = state.mood().arousal_delta();
let modified = apply_memory_consolidation(state, &memories, Duration::days(30));
assert!(modified.mood().arousal_delta() > original_arousal);
}
#[test]
fn same_inputs_produce_same_output() {
let mut memories = MemoryLayers::new();
memories.add(MemoryLayer::ShortTerm, create_negative_memory(0.7));
memories.add(MemoryLayer::ShortTerm, create_positive_memory(0.6));
let state = IndividualState::new();
let duration = Duration::days(30);
let result1 = apply_memory_consolidation(state.clone(), &memories, duration);
let result2 = apply_memory_consolidation(state, &memories, duration);
assert!(
(result1.mood().valence_delta() - result2.mood().valence_delta()).abs() < f32::EPSILON
);
assert!(
(result1.mood().arousal_delta() - result2.mood().arousal_delta()).abs() < f32::EPSILON
);
}
#[test]
fn zero_duration_produces_no_priming() {
let mut memories = MemoryLayers::new();
memories.add(MemoryLayer::ShortTerm, create_negative_memory(0.8));
let state = IndividualState::new();
let original_valence = state.mood().valence_delta();
let result = apply_memory_consolidation(state, &memories, Duration::zero());
assert!((result.mood().valence_delta() - original_valence).abs() < f32::EPSILON);
}
#[test]
fn no_memories_produces_no_effect() {
let memories = MemoryLayers::new(); let state = IndividualState::new();
let original_valence = state.mood().valence_delta();
let original_arousal = state.mood().arousal_delta();
let result = apply_memory_consolidation(state, &memories, Duration::days(30));
assert!((result.mood().valence_delta() - original_valence).abs() < f32::EPSILON);
assert!((result.mood().arousal_delta() - original_arousal).abs() < f32::EPSILON);
}
#[test]
fn priming_deltas_zero_creates_zero_values() {
let deltas = PrimingDeltas::zero();
assert!(deltas.valence_delta.abs() < f32::EPSILON);
assert!(deltas.arousal_delta.abs() < f32::EPSILON);
}
#[test]
fn priming_deltas_new_stores_values() {
let deltas = PrimingDeltas::new(0.3, 0.2);
assert!((deltas.valence_delta - 0.3).abs() < f32::EPSILON);
assert!((deltas.arousal_delta - 0.2).abs() < f32::EPSILON);
}
#[test]
fn priming_deltas_default_is_zero() {
let deltas = PrimingDeltas::default();
assert!(deltas.valence_delta.abs() < f32::EPSILON);
assert!(deltas.arousal_delta.abs() < f32::EPSILON);
}
#[test]
fn priming_deltas_clone_and_equality() {
let deltas1 = PrimingDeltas::new(0.5, 0.3);
let deltas2 = deltas1;
assert_eq!(deltas1, deltas2);
}
#[test]
fn priming_deltas_debug_format() {
let deltas = PrimingDeltas::new(0.1, 0.2);
let debug = format!("{:?}", deltas);
assert!(debug.contains("PrimingDeltas"));
}
#[test]
fn compute_priming_deltas_empty_memories_returns_zero() {
let memories = MemoryLayers::new();
let mood = Mood::new();
let deltas = compute_priming_deltas(&memories, &mood);
assert!(deltas.valence_delta.abs() < f32::EPSILON);
assert!(deltas.arousal_delta.abs() < f32::EPSILON);
}
#[test]
fn arousal_priming_only_from_high_salience() {
let mut low_salience_memories = MemoryLayers::new();
low_salience_memories.add(
MemoryLayer::ShortTerm,
create_memory_with_emotion(0.0, 0.0, 0.2), );
let mut high_salience_memories = MemoryLayers::new();
high_salience_memories.add(
MemoryLayer::ShortTerm,
create_memory_with_emotion(0.0, 0.0, 0.8), );
let mood = Mood::new();
let low_deltas = compute_priming_deltas(&low_salience_memories, &mood);
let high_deltas = compute_priming_deltas(&high_salience_memories, &mood);
assert!(low_deltas.arousal_delta.abs() < f32::EPSILON);
assert!(high_deltas.arousal_delta > 0.0);
}
#[test]
fn consolidation_scales_with_duration() {
let mut memories = MemoryLayers::new();
memories.add(MemoryLayer::ShortTerm, create_negative_memory(0.8));
let state = IndividualState::new();
let short_result = apply_memory_consolidation(state.clone(), &memories, Duration::days(1));
let long_result = apply_memory_consolidation(state, &memories, Duration::days(100));
let short_effect = short_result.mood().valence_delta().abs();
let long_effect = long_result.mood().valence_delta().abs();
assert!(long_effect >= short_effect);
}
#[test]
fn priming_is_clamped_to_bounds() {
let mut memories = MemoryLayers::new();
for _ in 0..100 {
memories.add(
MemoryLayer::ShortTerm,
create_memory_with_emotion(-1.0, 0.0, 1.0),
);
}
let negative_mood = Mood::new().with_valence_base(-1.0);
let deltas = compute_priming_deltas(&memories, &negative_mood);
assert!(deltas.valence_delta >= -MAX_PRIMING_EFFECT);
assert!(deltas.valence_delta <= MAX_PRIMING_EFFECT);
}
#[test]
fn compute_tag_polarity_empty_tags_returns_zero_and_false() {
let tags: Vec<MemoryTag> = vec![];
let (polarity, has_tags) = compute_tag_polarity(&tags);
assert!(polarity.abs() < f32::EPSILON);
assert!(!has_tags);
}
#[test]
fn compute_tag_polarity_negative_tags_returns_negative() {
let tags = vec![MemoryTag::Loss, MemoryTag::Violence];
let (polarity, has_tags) = compute_tag_polarity(&tags);
assert!(polarity < 0.0);
assert!(has_tags);
assert!((polarity - (-1.0)).abs() < f32::EPSILON);
}
#[test]
fn compute_tag_polarity_positive_tags_returns_positive() {
let tags = vec![MemoryTag::Achievement, MemoryTag::Support];
let (polarity, has_tags) = compute_tag_polarity(&tags);
assert!(polarity > 0.0);
assert!(has_tags);
assert!((polarity - 1.0).abs() < f32::EPSILON);
}
#[test]
fn compute_tag_polarity_mixed_tags_returns_net() {
let tags = vec![MemoryTag::Loss, MemoryTag::Achievement];
let (polarity, has_tags) = compute_tag_polarity(&tags);
assert!(polarity.abs() < f32::EPSILON);
assert!(has_tags);
}
#[test]
fn compute_tag_polarity_neutral_tags_returns_zero_and_false() {
let tags = vec![MemoryTag::Mission, MemoryTag::Personal];
let (polarity, has_tags) = compute_tag_polarity(&tags);
assert!(polarity.abs() < f32::EPSILON);
assert!(!has_tags);
}
#[test]
fn compute_tag_polarity_mixed_with_neutral_ignores_neutral() {
let tags = vec![MemoryTag::Mission, MemoryTag::Loss, MemoryTag::Personal];
let (polarity, has_tags) = compute_tag_polarity(&tags);
assert!(polarity < 0.0);
assert!(has_tags);
assert!((polarity - (-1.0)).abs() < f32::EPSILON);
}
#[test]
fn compute_mood_congruence_no_polarity_tags_returns_one() {
let congruence = compute_mood_congruence(-0.5, 0.0, false);
assert!((congruence - 1.0).abs() < f32::EPSILON);
}
#[test]
fn compute_mood_congruence_neutral_mood_returns_one() {
let congruence = compute_mood_congruence(0.05, -0.5, true);
assert!((congruence - 1.0).abs() < f32::EPSILON);
}
#[test]
fn compute_mood_congruence_congruent_negative_boosts() {
let congruence = compute_mood_congruence(-0.5, -0.5, true);
assert!(congruence > 1.0);
assert!((congruence - 1.25).abs() < f32::EPSILON);
}
#[test]
fn compute_mood_congruence_congruent_positive_boosts() {
let congruence = compute_mood_congruence(0.6, 0.8, true);
assert!(congruence > 1.0);
assert!((congruence - 1.3).abs() < f32::EPSILON);
}
#[test]
fn compute_mood_congruence_incongruent_reduces() {
let congruence = compute_mood_congruence(-0.5, 0.5, true);
assert!(congruence < 1.0);
assert!((congruence - 0.85).abs() < f32::EPSILON);
}
#[test]
fn compute_mood_congruence_mixed_tags_neutral_polarity() {
let congruence = compute_mood_congruence(-0.5, 0.05, true);
assert!((congruence - 1.0).abs() < f32::EPSILON);
}
#[test]
fn compute_mood_congruence_same_sign_but_weak_polarity_negative() {
let congruence = compute_mood_congruence(-0.5, -0.08, true);
assert!((congruence - 1.0).abs() < f32::EPSILON);
}
#[test]
fn compute_mood_congruence_same_sign_but_weak_polarity_positive() {
let congruence = compute_mood_congruence(0.5, 0.08, true);
assert!((congruence - 1.0).abs() < f32::EPSILON);
}
#[test]
fn compute_mood_congruence_opposite_signs() {
let congruence = compute_mood_congruence(0.5, -0.5, true);
assert!(congruence < 1.0);
assert!((congruence - 0.85).abs() < f32::EPSILON);
}
#[test]
fn tag_polarity_drives_priming_over_snapshot() {
let mut memories = MemoryLayers::new();
memories.add(
MemoryLayer::ShortTerm,
MemoryEntry::new(Duration::days(1), "Negative tagged, positive emotion")
.with_emotional_snapshot(EmotionalSnapshot::new(0.8, 0.0, 0.0)) .add_tag(MemoryTag::Loss) .with_salience(0.7),
);
let negative_mood = Mood::new().with_valence_base(-0.5);
let priming = compute_priming_deltas(&memories, &negative_mood);
assert!(priming.valence_delta < 0.0);
}
#[test]
fn memories_without_tags_use_snapshot_only() {
let mut memories = MemoryLayers::new();
memories.add(
MemoryLayer::ShortTerm,
MemoryEntry::new(Duration::days(1), "No polarity tags")
.with_emotional_snapshot(EmotionalSnapshot::new(-0.5, 0.0, 0.0))
.add_tag(MemoryTag::Mission) .with_salience(0.7),
);
let mood = Mood::new();
let priming = compute_priming_deltas(&memories, &mood);
assert!(priming.valence_delta < 0.0);
}
#[test]
fn positive_tagged_memories_with_positive_mood_amplify() {
let mut memories = MemoryLayers::new();
for _ in 0..3 {
memories.add(
MemoryLayer::ShortTerm,
MemoryEntry::new(Duration::days(1), "Achievement")
.with_emotional_snapshot(EmotionalSnapshot::new(0.5, 0.0, 0.0))
.add_tag(MemoryTag::Achievement)
.with_salience(0.7),
);
}
let positive_mood = Mood::new().with_valence_base(0.6);
let neutral_mood = Mood::new();
let positive_priming = compute_priming_deltas(&memories, &positive_mood);
let neutral_priming = compute_priming_deltas(&memories, &neutral_mood);
assert!(positive_priming.valence_delta > neutral_priming.valence_delta);
}
}