use crate::cognition::adaptive_thresholds::{
build_threshold_context, get_adaptive_threshold, threshold_body_budget_conservation,
};
use crate::math::clamp;
use crate::types::gate::GateType;
use crate::types::intervention::CognitiveSignals;
use crate::types::world::{GainMode, WorldModel};
const PRESSURE_CONSERVATION_WEIGHT: f64 = 0.3;
const GATE_URGENT_SALIENCE: f64 = 0.3;
const GATE_NOVEL_SALIENCE: f64 = 0.15;
const PE_SALIENCE_WEIGHT: f64 = 0.2;
const VOLATILITY_CONFIDENCE_REDUCTION: f64 = 0.5;
pub fn compute_signals(model: &WorldModel, gain_mode: GainMode) -> CognitiveSignals {
let threshold_ctx = build_threshold_context(
model.sensory_pe,
model.belief.affect.arousal,
model.gate.confidence,
model.pe_volatility,
Some(model.belief.affect.valence),
Some(model.body_budget),
);
let budget_threshold =
get_adaptive_threshold(&threshold_body_budget_conservation(), &threshold_ctx);
let budget_factor = if model.body_budget < budget_threshold {
(budget_threshold - model.body_budget) / budget_threshold.max(0.01)
} else {
0.0
};
let sustained_penalty = (1.0 - model.belief.affect.sustained).max(0.0) * 0.2;
let conservation = clamp(
budget_factor + model.resource_pressure * PRESSURE_CONSERVATION_WEIGHT + sustained_penalty,
0.0,
1.0,
);
let gate_salience = match model.gate.gate {
GateType::Urgent => GATE_URGENT_SALIENCE,
GateType::Novel => GATE_NOVEL_SALIENCE,
GateType::Routine => 0.0,
};
let salience = clamp(
model.belief.affect.arousal + gate_salience + model.sensory_pe * PE_SALIENCE_WEIGHT,
0.0,
1.0,
);
let confidence = clamp(
model.gate.confidence * (1.0 - model.pe_volatility * VOLATILITY_CONFIDENCE_REDUCTION),
0.0,
1.0,
);
CognitiveSignals {
conservation,
salience,
confidence,
strategy: model.recommended_strategy,
gain_mode,
valence: model.belief.affect.valence,
recent_quality: model.last_response_prediction,
rpe: model.response_rpe,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::belief::AffectValence;
fn default_model() -> WorldModel {
WorldModel::new("test".into())
}
#[test]
fn default_produces_neutral_signals() {
let signals = compute_signals(&default_model(), GainMode::Neutral);
assert!(signals.conservation < 0.2, "Default should have low conservation, got {}", signals.conservation);
assert!(signals.salience < 0.2, "Default should have low salience, got {}", signals.salience);
assert_eq!(signals.gain_mode, GainMode::Neutral);
assert_eq!(signals.valence, AffectValence::Neutral);
assert!(signals.strategy.is_none());
}
#[test]
fn depleted_budget_high_conservation() {
let mut model = default_model();
model.body_budget = 0.1; let signals = compute_signals(&model, GainMode::Neutral);
assert!(
signals.conservation > 0.5,
"Depleted budget should produce high conservation, got {}",
signals.conservation
);
}
#[test]
fn high_pressure_increases_conservation() {
let mut model = default_model();
model.resource_pressure = 0.9;
let signals = compute_signals(&model, GainMode::Neutral);
assert!(
signals.conservation > 0.2,
"High pressure should increase conservation, got {}",
signals.conservation
);
}
#[test]
fn urgent_gate_high_salience() {
let mut model = default_model();
model.gate.gate = GateType::Urgent;
model.belief.affect.arousal = 0.7;
let signals = compute_signals(&model, GainMode::Phasic);
assert!(
signals.salience > 0.8,
"Urgent gate + high arousal should produce high salience, got {}",
signals.salience
);
}
#[test]
fn routine_low_salience() {
let mut model = default_model();
model.gate.gate = GateType::Routine;
model.belief.affect.arousal = 0.1;
let signals = compute_signals(&model, GainMode::Neutral);
assert!(
signals.salience < 0.2,
"Routine gate + low arousal should produce low salience, got {}",
signals.salience
);
}
#[test]
fn high_volatility_reduces_confidence() {
let mut model = default_model();
model.gate.confidence = 0.8;
model.pe_volatility = 0.9;
let signals = compute_signals(&model, GainMode::Neutral);
assert!(
signals.confidence < model.gate.confidence,
"Volatility should reduce confidence below gate_confidence, got {}",
signals.confidence
);
assert!(signals.confidence > 0.0);
}
#[test]
fn recent_quality_and_rpe_passed_through() {
let mut model = default_model();
model.last_response_prediction = 0.35;
model.response_rpe = -0.2;
let signals = compute_signals(&model, GainMode::Neutral);
assert_eq!(signals.recent_quality, 0.35);
assert_eq!(signals.rpe, -0.2);
}
#[test]
fn conservation_clamped_to_unit() {
let mut model = default_model();
model.body_budget = 0.0;
model.resource_pressure = 1.0;
let signals = compute_signals(&model, GainMode::Neutral);
assert!(
signals.conservation <= 1.0,
"Conservation must be clamped to [0, 1]"
);
}
#[test]
fn signals_serde_round_trip() {
use crate::types::world::ResponseStrategy;
let signals = CognitiveSignals {
conservation: 0.42,
salience: 0.78,
confidence: 0.65,
strategy: Some(ResponseStrategy::StepByStep),
gain_mode: GainMode::Phasic,
valence: AffectValence::Negative,
recent_quality: 0.55,
rpe: -0.12,
};
let json = serde_json::to_string(&signals)
.expect("CognitiveSignals should serialize to JSON");
let restored: CognitiveSignals = serde_json::from_str(&json)
.expect("CognitiveSignals should deserialize from JSON");
assert!((restored.conservation - signals.conservation).abs() < 1e-10);
assert!((restored.salience - signals.salience).abs() < 1e-10);
assert!((restored.confidence - signals.confidence).abs() < 1e-10);
assert_eq!(restored.strategy, signals.strategy);
assert_eq!(restored.gain_mode, signals.gain_mode);
assert_eq!(restored.valence, signals.valence);
assert!((restored.recent_quality - signals.recent_quality).abs() < 1e-10);
assert!((restored.rpe - signals.rpe).abs() < 1e-10);
}
#[test]
fn signals_with_no_strategy_serializes() {
let signals = CognitiveSignals {
conservation: 0.0,
salience: 0.0,
confidence: 0.5,
strategy: None,
gain_mode: GainMode::Neutral,
valence: AffectValence::Neutral,
recent_quality: 0.5,
rpe: 0.0,
};
let json = serde_json::to_string(&signals).unwrap();
let restored: CognitiveSignals = serde_json::from_str(&json).unwrap();
assert!(restored.strategy.is_none());
}
}