use crate::mood::{Emotion, MoodVector};
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn mood_from_threat_response(response: jantu::ThreatResponse) -> MoodVector {
match response {
jantu::ThreatResponse::Fight => MoodVector {
joy: -0.3,
arousal: 0.9,
dominance: 0.6,
trust: -0.4,
interest: -0.2,
frustration: 0.5,
},
jantu::ThreatResponse::Flight => MoodVector {
joy: -0.5,
arousal: 0.8,
dominance: -0.6,
trust: -0.5,
interest: -0.3,
frustration: 0.3,
},
jantu::ThreatResponse::Freeze => MoodVector {
joy: -0.4,
arousal: 0.3,
dominance: -0.8,
trust: -0.3,
interest: -0.5,
frustration: 0.2,
},
jantu::ThreatResponse::Fawn => MoodVector {
joy: -0.2,
arousal: 0.4,
dominance: -0.7,
trust: 0.2,
interest: 0.1,
frustration: 0.1,
},
_ => MoodVector {
joy: -0.3,
arousal: 0.5,
dominance: -0.3,
trust: -0.2,
interest: -0.2,
frustration: 0.3,
},
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn load_from_stress(stress: &jantu::stress::StressState) -> f32 {
let acute_contribution = stress.acute * (1.0 - stress.resilience) * 0.3;
let chronic_contribution = stress.chronic * 0.7;
(acute_contribution + chronic_contribution).clamp(0.0, 1.0)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn mood_shift_from_instinct(instinct: &jantu::Instinct) -> (Emotion, f32) {
let intensity = instinct.priority.clamp(0.0, 1.0);
match instinct.instinct_type {
jantu::InstinctType::Fear => (Emotion::Arousal, intensity),
jantu::InstinctType::Aggression => (Emotion::Frustration, intensity),
jantu::InstinctType::Hunger | jantu::InstinctType::Thirst => {
(Emotion::Frustration, intensity * 0.5)
}
jantu::InstinctType::Curiosity => (Emotion::Interest, intensity),
jantu::InstinctType::Social => (Emotion::Trust, intensity),
jantu::InstinctType::Nurturing => (Emotion::Joy, intensity * 0.6),
jantu::InstinctType::Reproduction => (Emotion::Arousal, intensity * 0.4),
jantu::InstinctType::Rest => (Emotion::Arousal, -intensity * 0.5),
_ => (Emotion::Interest, intensity * 0.2),
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn dominance_from_rank(position: jantu::HierarchyPosition) -> f32 {
position.value() * 2.0 - 1.0
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn instinct_layer_score(instincts: &[jantu::Instinct]) -> f32 {
jantu::instinct::dominant_instinct(instincts)
.map(|i| i.priority.clamp(0.0, 1.0))
.unwrap_or(0.0)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn mood_from_contagion(state: jantu::contagion::EmotionalState, magnitude: f32) -> MoodVector {
let mag = magnitude.clamp(0.0, 1.0);
match state {
jantu::contagion::EmotionalState::Fear => MoodVector {
joy: -0.3 * mag,
arousal: 0.7 * mag,
dominance: -0.4 * mag,
trust: -0.3 * mag,
interest: -0.2 * mag,
frustration: 0.2 * mag,
},
jantu::contagion::EmotionalState::Aggression => MoodVector {
joy: -0.2 * mag,
arousal: 0.6 * mag,
dominance: 0.3 * mag,
trust: -0.4 * mag,
interest: -0.1 * mag,
frustration: 0.6 * mag,
},
jantu::contagion::EmotionalState::Calm => MoodVector {
joy: 0.2 * mag,
arousal: -0.4 * mag,
dominance: 0.1 * mag,
trust: 0.3 * mag,
interest: 0.1 * mag,
frustration: -0.3 * mag,
},
jantu::contagion::EmotionalState::Excitement => MoodVector {
joy: 0.4 * mag,
arousal: 0.6 * mag,
dominance: 0.1 * mag,
trust: 0.2 * mag,
interest: 0.5 * mag,
frustration: -0.1 * mag,
},
_ => MoodVector {
joy: 0.0,
arousal: 0.1 * mag,
dominance: 0.0,
trust: 0.0,
interest: 0.0,
frustration: 0.0,
},
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
#[inline]
pub fn trust_from_cohesion(cohesion: f32) -> f32 {
cohesion.clamp(0.0, 1.0) - 0.5
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
#[inline]
pub fn mood_from_territorial(response_intensity: f32) -> MoodVector {
let r = response_intensity.clamp(0.0, 1.0);
MoodVector {
joy: -0.1 * r,
arousal: 0.5 * r,
dominance: 0.6 * r,
trust: -0.2 * r,
interest: 0.2 * r,
frustration: 0.4 * r,
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
#[inline]
pub fn reactivity_from_habituation(response: &jantu::habituation::StimulusResponse) -> f32 {
response.response_multiplier().clamp(0.0, 2.0)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
#[inline]
pub fn actr_seed_from_memory(trace: &jantu::memory::MemoryTrace) -> (f32, f32) {
(
trace.valence.clamp(-1.0, 1.0),
trace.strength.clamp(0.0, 1.0),
)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
#[inline]
pub fn stress_from_landscape(perceived_risk: f32) -> f32 {
let r = perceived_risk.clamp(0.0, 1.0);
(r * r).clamp(0.0, 1.0)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn energy_drain_from_drives(instincts: &[jantu::Instinct]) -> f32 {
if instincts.is_empty() {
return 0.0;
}
let mut total = 0.0_f32;
let mut count = 0_u32;
for inst in instincts {
let p = inst.priority.clamp(0.0, 1.0);
if matches!(inst.instinct_type, jantu::InstinctType::Rest) {
total -= p * 0.3;
} else {
total += p;
}
count += 1;
}
(total / count as f32).clamp(0.0, 1.0)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
#[inline]
pub fn alertness_from_activity(activity_level: f32) -> f32 {
let a = activity_level.clamp(0.0, 1.0);
a * a * (3.0 - 2.0 * a)
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TraitSeeds {
pub warmth: f32,
pub empathy: f32,
pub patience: f32,
pub confidence: f32,
pub curiosity: f32,
pub risk_tolerance: f32,
pub directness: f32,
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn trait_seeds_from_genome(genome: &jantu::genetics::BehavioralGenome) -> TraitSeeds {
let agg = genome.aggression.genotype.clamp(0.0, 1.0) * 2.0 - 1.0;
let bold = genome.boldness.genotype.clamp(0.0, 1.0) * 2.0 - 1.0;
let soc = genome.sociability.genotype.clamp(0.0, 1.0) * 2.0 - 1.0;
let expl = genome.exploration.genotype.clamp(0.0, 1.0) * 2.0 - 1.0;
TraitSeeds {
warmth: (soc * 0.8).clamp(-1.0, 1.0),
empathy: (soc * 0.6 + (1.0 - agg.abs()) * 0.4).clamp(-1.0, 1.0),
patience: (-agg * 0.7).clamp(-1.0, 1.0),
confidence: (bold * 0.8).clamp(-1.0, 1.0),
curiosity: (expl * 0.9).clamp(-1.0, 1.0),
risk_tolerance: (bold * 0.6 + agg * 0.3).clamp(-1.0, 1.0),
directness: (agg * 0.5 + bold * 0.3).clamp(-1.0, 1.0),
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn mood_from_signal(signal: &jantu::signals::Signal) -> MoodVector {
let eff = (signal.intensity * (0.5 + signal.honesty * 0.5)).clamp(0.0, 1.0);
match signal.function {
jantu::signals::SignalFunction::Alarm => MoodVector {
joy: -0.3 * eff,
arousal: 0.8 * eff,
dominance: -0.2 * eff,
trust: -0.2 * eff,
interest: 0.3 * eff,
frustration: 0.1 * eff,
},
jantu::signals::SignalFunction::MatingCall => MoodVector {
joy: 0.3 * eff,
arousal: 0.4 * eff,
dominance: 0.0,
trust: 0.2 * eff,
interest: 0.5 * eff,
frustration: -0.1 * eff,
},
jantu::signals::SignalFunction::TerritorialDisplay => MoodVector {
joy: -0.1 * eff,
arousal: 0.5 * eff,
dominance: 0.4 * eff,
trust: -0.3 * eff,
interest: 0.2 * eff,
frustration: 0.3 * eff,
},
jantu::signals::SignalFunction::Submission => MoodVector {
joy: 0.1 * eff,
arousal: -0.2 * eff,
dominance: 0.5 * eff,
trust: 0.3 * eff,
interest: 0.0,
frustration: -0.2 * eff,
},
jantu::signals::SignalFunction::Threat => MoodVector {
joy: -0.4 * eff,
arousal: 0.7 * eff,
dominance: -0.3 * eff,
trust: -0.5 * eff,
interest: 0.1 * eff,
frustration: 0.4 * eff,
},
jantu::signals::SignalFunction::Begging => MoodVector {
joy: -0.1 * eff,
arousal: 0.2 * eff,
dominance: 0.2 * eff,
trust: 0.1 * eff,
interest: 0.1 * eff,
frustration: 0.1 * eff,
},
jantu::signals::SignalFunction::Contact => MoodVector {
joy: 0.2 * eff,
arousal: 0.1 * eff,
dominance: 0.0,
trust: 0.3 * eff,
interest: 0.2 * eff,
frustration: -0.1 * eff,
},
jantu::signals::SignalFunction::FoodCall => MoodVector {
joy: 0.3 * eff,
arousal: 0.2 * eff,
dominance: 0.0,
trust: 0.2 * eff,
interest: 0.4 * eff,
frustration: -0.2 * eff,
},
_ => MoodVector {
joy: 0.0,
arousal: 0.1 * eff,
dominance: 0.0,
trust: 0.0,
interest: 0.1 * eff,
frustration: 0.0,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fight_response_high_arousal() {
let mood = mood_from_threat_response(jantu::ThreatResponse::Fight);
assert!(mood.arousal > 0.5);
assert!(mood.dominance > 0.0);
}
#[test]
fn flight_response_low_dominance() {
let mood = mood_from_threat_response(jantu::ThreatResponse::Flight);
assert!(mood.dominance < 0.0);
assert!(mood.arousal > 0.5);
}
#[test]
fn freeze_response_lowest_dominance() {
let mood = mood_from_threat_response(jantu::ThreatResponse::Freeze);
assert!(mood.dominance < -0.5);
}
#[test]
fn fawn_response_slight_trust() {
let mood = mood_from_threat_response(jantu::ThreatResponse::Fawn);
assert!(mood.trust > 0.0);
}
#[test]
fn unstressed_low_load() {
let s = jantu::stress::StressState::new();
assert!(load_from_stress(&s) < 0.01);
}
#[test]
fn chronic_stress_high_load() {
let mut s = jantu::stress::StressState::new();
for _ in 0..20 {
s.apply_stressor(0.7);
}
assert!(load_from_stress(&s) > 0.1);
}
#[test]
fn fear_instinct_maps_to_arousal() {
let mut fear = jantu::Instinct::new(jantu::InstinctType::Fear);
fear.drive = jantu::DriveLevel::new(0.8);
fear.update_priority();
let (emotion, mag) = mood_shift_from_instinct(&fear);
assert_eq!(emotion, Emotion::Arousal);
assert!(mag > 0.5);
}
#[test]
fn curiosity_instinct_maps_to_interest() {
let mut curiosity = jantu::Instinct::new(jantu::InstinctType::Curiosity);
curiosity.drive = jantu::DriveLevel::new(0.9);
curiosity.update_priority();
let (emotion, _) = mood_shift_from_instinct(&curiosity);
assert_eq!(emotion, Emotion::Interest);
}
#[test]
fn alpha_positive_dominance() {
assert!(dominance_from_rank(jantu::HierarchyPosition::new(0.9)) > 0.0);
}
#[test]
fn omega_negative_dominance() {
assert!(dominance_from_rank(jantu::HierarchyPosition::new(0.1)) < 0.0);
}
#[test]
fn instinct_layer_score_high_fear() {
let mut fear = jantu::Instinct::new(jantu::InstinctType::Fear);
fear.drive = jantu::DriveLevel::new(0.9);
fear.update_priority();
let score = instinct_layer_score(&[fear]);
assert!(score > 0.7);
}
#[test]
fn instinct_layer_score_empty() {
assert_eq!(instinct_layer_score(&[]), 0.0);
}
#[test]
fn serde_roundtrip_mood_from_threat() {
let mood = mood_from_threat_response(jantu::ThreatResponse::Fight);
let json = serde_json::to_string(&mood).unwrap();
let mood2: MoodVector = serde_json::from_str(&json).unwrap();
assert!((mood.arousal - mood2.arousal).abs() < f32::EPSILON);
}
#[test]
fn fear_contagion_raises_arousal() {
let mood = mood_from_contagion(jantu::contagion::EmotionalState::Fear, 0.8);
assert!(mood.arousal > 0.3);
assert!(mood.trust < 0.0);
}
#[test]
fn calm_contagion_positive_trust() {
let mood = mood_from_contagion(jantu::contagion::EmotionalState::Calm, 0.7);
assert!(mood.trust > 0.0);
assert!(mood.arousal < 0.0);
}
#[test]
fn excitement_contagion_joy_and_interest() {
let mood = mood_from_contagion(jantu::contagion::EmotionalState::Excitement, 0.9);
assert!(mood.joy > 0.0);
assert!(mood.interest > 0.0);
}
#[test]
fn aggression_contagion_high_frustration() {
let mood = mood_from_contagion(jantu::contagion::EmotionalState::Aggression, 0.8);
assert!(mood.frustration > 0.3);
}
#[test]
fn zero_magnitude_contagion_is_neutral() {
let mood = mood_from_contagion(jantu::contagion::EmotionalState::Fear, 0.0);
assert!(mood.arousal.abs() < f32::EPSILON);
assert!(mood.joy.abs() < f32::EPSILON);
}
#[test]
fn high_cohesion_positive_trust() {
assert!(trust_from_cohesion(0.9) > 0.0);
}
#[test]
fn low_cohesion_negative_trust() {
assert!(trust_from_cohesion(0.1) < 0.0);
}
#[test]
fn mid_cohesion_neutral_trust() {
assert!(trust_from_cohesion(0.5).abs() < f32::EPSILON);
}
#[test]
fn strong_territorial_high_dominance() {
let mood = mood_from_territorial(0.9);
assert!(mood.dominance > 0.4);
assert!(mood.frustration > 0.3);
}
#[test]
fn weak_territorial_low_effect() {
let mood = mood_from_territorial(0.1);
assert!(mood.dominance < 0.1);
}
#[test]
fn fresh_stimulus_neutral_reactivity() {
let fresh = jantu::habituation::StimulusResponse::new();
let r = reactivity_from_habituation(&fresh);
assert!((r - 1.0).abs() < 0.01);
}
#[test]
fn habituated_stimulus_reduced_reactivity() {
let params = jantu::habituation::HabituationParams::default();
let mut resp = jantu::habituation::StimulusResponse::new();
for _ in 0..30 {
resp.expose(0.3, ¶ms);
}
assert!(reactivity_from_habituation(&resp) < 1.0);
}
#[test]
fn threat_memory_negative_valence() {
let threat = jantu::memory::MemoryTrace::new(jantu::memory::MemoryType::Threat, 0.9, -0.8);
let (valence, strength) = actr_seed_from_memory(&threat);
assert!(valence < 0.0);
assert!(strength > 0.5);
}
#[test]
fn food_memory_positive_valence() {
let food = jantu::memory::MemoryTrace::new(jantu::memory::MemoryType::FoodSource, 0.7, 0.6);
let (valence, _) = actr_seed_from_memory(&food);
assert!(valence > 0.0);
}
#[test]
fn high_risk_high_stress() {
assert!(stress_from_landscape(0.9) > 0.5);
}
#[test]
fn low_risk_low_stress() {
assert!(stress_from_landscape(0.1) < 0.1);
}
#[test]
fn stress_from_landscape_quadratic() {
let low = stress_from_landscape(0.3);
let high = stress_from_landscape(0.6);
assert!(high > low * 2.0);
}
#[test]
fn fear_drive_high_exertion() {
let mut fear = jantu::Instinct::new(jantu::InstinctType::Fear);
fear.drive = jantu::DriveLevel::new(0.9);
fear.update_priority();
let exertion = energy_drain_from_drives(&[fear]);
assert!(exertion > 0.3);
}
#[test]
fn rest_drive_low_exertion() {
let mut rest = jantu::Instinct::new(jantu::InstinctType::Rest);
rest.drive = jantu::DriveLevel::new(0.9);
rest.update_priority();
let exertion = energy_drain_from_drives(&[rest]);
assert!(exertion < 0.1);
}
#[test]
fn empty_drives_zero_exertion() {
assert_eq!(energy_drain_from_drives(&[]), 0.0);
}
#[test]
fn diurnal_midday_more_alert_than_midnight() {
let clock =
jantu::circadian::CircadianClock::new(jantu::circadian::ActivityPattern::Diurnal);
let midday = alertness_from_activity(clock.activity_level(12.0));
let midnight = alertness_from_activity(clock.activity_level(0.0));
assert!(midday > midnight);
}
#[test]
fn alertness_smoothstep_bounds() {
assert!((alertness_from_activity(0.0)).abs() < f32::EPSILON);
assert!((alertness_from_activity(1.0) - 1.0).abs() < f32::EPSILON);
}
fn genome(
agg: f32,
bold: f32,
soc: f32,
act: f32,
expl: f32,
) -> jantu::genetics::BehavioralGenome {
use jantu::genetics::HeritableTrait;
jantu::genetics::BehavioralGenome {
aggression: HeritableTrait::new(agg, 0.4),
boldness: HeritableTrait::new(bold, 0.35),
sociability: HeritableTrait::new(soc, 0.3),
activity: HeritableTrait::new(act, 0.45),
exploration: HeritableTrait::new(expl, 0.3),
}
}
#[test]
fn social_genome_warm_personality() {
let g = genome(0.2, 0.5, 0.9, 0.5, 0.5);
let seeds = trait_seeds_from_genome(&g);
assert!(seeds.warmth > 0.0);
assert!(seeds.empathy > 0.0);
}
#[test]
fn aggressive_genome_low_patience() {
let g = genome(0.9, 0.5, 0.3, 0.5, 0.5);
let seeds = trait_seeds_from_genome(&g);
assert!(seeds.patience < 0.0);
assert!(seeds.directness > 0.0);
}
#[test]
fn bold_explorer_curious_confident() {
let g = genome(0.3, 0.9, 0.5, 0.7, 0.9);
let seeds = trait_seeds_from_genome(&g);
assert!(seeds.confidence > 0.0);
assert!(seeds.curiosity > 0.0);
assert!(seeds.risk_tolerance > 0.0);
}
#[test]
fn trait_seeds_bounded() {
let g = genome(1.0, 1.0, 1.0, 1.0, 1.0);
let seeds = trait_seeds_from_genome(&g);
assert!(seeds.warmth >= -1.0 && seeds.warmth <= 1.0);
assert!(seeds.empathy >= -1.0 && seeds.empathy <= 1.0);
assert!(seeds.patience >= -1.0 && seeds.patience <= 1.0);
assert!(seeds.confidence >= -1.0 && seeds.confidence <= 1.0);
assert!(seeds.curiosity >= -1.0 && seeds.curiosity <= 1.0);
assert!(seeds.risk_tolerance >= -1.0 && seeds.risk_tolerance <= 1.0);
assert!(seeds.directness >= -1.0 && seeds.directness <= 1.0);
}
#[test]
fn alarm_signal_raises_arousal() {
let alarm = jantu::signals::Signal::new(
jantu::signals::SignalModality::Acoustic,
jantu::signals::SignalFunction::Alarm,
0.9,
);
let mood = mood_from_signal(&alarm);
assert!(mood.arousal > 0.3);
}
#[test]
fn submission_signal_raises_receiver_dominance() {
let sub = jantu::signals::Signal::new(
jantu::signals::SignalModality::Visual,
jantu::signals::SignalFunction::Submission,
0.8,
);
let mood = mood_from_signal(&sub);
assert!(mood.dominance > 0.0);
assert!(mood.trust > 0.0);
}
#[test]
fn threat_signal_negative_trust() {
let threat = jantu::signals::Signal::new(
jantu::signals::SignalModality::Acoustic,
jantu::signals::SignalFunction::Threat,
0.9,
);
let mood = mood_from_signal(&threat);
assert!(mood.trust < 0.0);
assert!(mood.frustration > 0.0);
}
#[test]
fn contact_signal_positive_trust() {
let contact = jantu::signals::Signal::new(
jantu::signals::SignalModality::Tactile,
jantu::signals::SignalFunction::Contact,
0.7,
);
let mood = mood_from_signal(&contact);
assert!(mood.trust > 0.0);
assert!(mood.joy > 0.0);
}
#[test]
fn food_call_positive_mood() {
let food = jantu::signals::Signal::new(
jantu::signals::SignalModality::Acoustic,
jantu::signals::SignalFunction::FoodCall,
0.8,
);
let mood = mood_from_signal(&food);
assert!(mood.joy > 0.0);
assert!(mood.interest > 0.0);
}
#[test]
fn dishonest_signal_reduced_effect() {
let mut honest = jantu::signals::Signal::new(
jantu::signals::SignalModality::Acoustic,
jantu::signals::SignalFunction::Alarm,
0.9,
);
let honest_mood = mood_from_signal(&honest);
honest.honesty = 0.1;
let dishonest_mood = mood_from_signal(&honest);
assert!(honest_mood.arousal > dishonest_mood.arousal);
}
#[test]
fn serde_roundtrip_trait_seeds() {
let genome = jantu::genetics::BehavioralGenome::default_genome();
let seeds = trait_seeds_from_genome(&genome);
let json = serde_json::to_string(&seeds).unwrap();
let seeds2: TraitSeeds = serde_json::from_str(&json).unwrap();
assert!((seeds.warmth - seeds2.warmth).abs() < f32::EPSILON);
}
}