use crate::math::clamp;
use crate::types::belief::AffectValence;
pub const BODY_BUDGET_HEALTHY_THRESHOLD: f64 = 0.70;
#[derive(Debug, Clone)]
pub struct ThresholdProfile {
pub pe_weight: f64,
pub arousal_weight: f64,
pub confidence_weight: f64,
pub volatility_weight: f64,
}
#[derive(Debug, Clone)]
pub struct ThresholdContext {
pub sensory_pe: f64,
pub arousal: f64,
pub gate_confidence: f64,
pub pe_volatility: f64,
}
#[derive(Debug, Clone)]
pub struct ThresholdRegistration {
pub id: String,
pub base: f64,
pub profile: ThresholdProfile,
pub min: f64,
pub max: f64,
}
#[derive(Debug, Clone)]
pub struct DimensionPE {
pub pe: f64,
pub precision: f64,
}
pub fn threshold_body_budget_conservation() -> ThresholdRegistration {
ThresholdRegistration {
id: "body-budget-conservation".into(),
base: BODY_BUDGET_HEALTHY_THRESHOLD,
profile: ThresholdProfile {
pe_weight: 0.10, arousal_weight: 0.05, confidence_weight: -0.10, volatility_weight: 0.0,
},
min: 0.40,
max: 0.85,
}
}
pub fn threshold_arousal_intervention() -> ThresholdRegistration {
ThresholdRegistration {
id: "arousal-intervention".into(),
base: 0.40,
profile: ThresholdProfile {
pe_weight: -0.05, arousal_weight: 0.0, confidence_weight: 0.10, volatility_weight: -0.10, },
min: 0.25,
max: 0.70,
}
}
pub fn threshold_resource_pressure() -> ThresholdRegistration {
ThresholdRegistration {
id: "resource-pressure".into(),
base: 0.70,
profile: ThresholdProfile {
pe_weight: -0.05, arousal_weight: -0.05, confidence_weight: 0.10, volatility_weight: -0.05, },
min: 0.50,
max: 0.85,
}
}
pub fn threshold_delta_volatility() -> ThresholdRegistration {
ThresholdRegistration {
id: "delta-volatility".into(),
base: 0.60,
profile: ThresholdProfile {
pe_weight: -0.10, arousal_weight: -0.05, confidence_weight: 0.10, volatility_weight: 0.0, },
min: 0.35,
max: 0.80,
}
}
pub fn threshold_delta_arousal_emergency() -> ThresholdRegistration {
ThresholdRegistration {
id: "delta-arousal-emergency".into(),
base: 0.55,
profile: ThresholdProfile {
pe_weight: -0.05, arousal_weight: 0.0, confidence_weight: 0.05, volatility_weight: -0.10, },
min: 0.40,
max: 0.80,
}
}
pub fn get_adaptive_threshold(threshold: &ThresholdRegistration, ctx: &ThresholdContext) -> f64 {
let pe = safe_num(ctx.sensory_pe, 0.5);
let arousal = safe_num(ctx.arousal, 0.0);
let conf = safe_num(ctx.gate_confidence, 0.5);
let vol = safe_num(ctx.pe_volatility, 0.3);
let modulation = (pe - 0.5) * threshold.profile.pe_weight
+ arousal * threshold.profile.arousal_weight
+ (conf - 0.5) * threshold.profile.confidence_weight
+ (vol - 0.3) * threshold.profile.volatility_weight;
clamp(
threshold.base * (1.0 + modulation),
threshold.min,
threshold.max,
)
}
pub fn build_threshold_context(
sensory_pe: f64,
arousal: f64,
gate_confidence: f64,
pe_volatility: f64,
valence: Option<AffectValence>,
body_budget: Option<f64>,
) -> ThresholdContext {
let mut adj_arousal = arousal;
let mut adj_pe = sensory_pe;
let mut adj_conf = gate_confidence;
if let Some(v) = valence {
match v {
AffectValence::Negative => adj_arousal *= 1.15, AffectValence::Positive => adj_arousal *= 0.9, AffectValence::Neutral => {}
}
}
if let Some(budget) = body_budget {
if budget < BODY_BUDGET_HEALTHY_THRESHOLD {
let deficit = BODY_BUDGET_HEALTHY_THRESHOLD - budget;
adj_pe += deficit * 0.15; adj_conf -= deficit * 0.1; }
}
ThresholdContext {
sensory_pe: clamp(adj_pe, 0.0, 1.0),
arousal: clamp(adj_arousal, 0.0, 1.0),
gate_confidence: clamp(adj_conf, 0.0, 1.0),
pe_volatility: clamp(pe_volatility, 0.0, 1.0),
}
}
pub fn compute_unified_pe(dimensions: &[DimensionPE]) -> f64 {
if dimensions.is_empty() {
return 0.5; }
let total_precision: f64 = dimensions.iter().map(|d| d.precision.max(0.0)).sum();
if total_precision == 0.0 {
return dimensions.iter().map(|d| d.pe).sum::<f64>() / dimensions.len() as f64;
}
let weighted: f64 = dimensions
.iter()
.map(|d| d.precision.max(0.0) * d.pe)
.sum();
weighted / total_precision
}
fn safe_num(value: f64, fallback: f64) -> f64 {
if value.is_nan() || value.is_infinite() {
fallback
} else {
value
}
}
#[cfg(test)]
mod tests {
use super::*;
fn neutral_ctx() -> ThresholdContext {
ThresholdContext {
sensory_pe: 0.5,
arousal: 0.0,
gate_confidence: 0.5,
pe_volatility: 0.3,
}
}
#[test]
fn neutral_context_returns_base() {
let t = threshold_resource_pressure();
let effective = get_adaptive_threshold(&t, &neutral_ctx());
assert!((effective - t.base).abs() < 0.05);
}
#[test]
fn high_arousal_lowers_threshold() {
let t = threshold_resource_pressure();
let base = get_adaptive_threshold(&t, &neutral_ctx());
let aroused = get_adaptive_threshold(
&t,
&ThresholdContext {
arousal: 0.9,
..neutral_ctx()
},
);
assert!(aroused < base); }
#[test]
fn high_confidence_raises_threshold() {
let t = threshold_resource_pressure();
let base = get_adaptive_threshold(&t, &neutral_ctx());
let confident = get_adaptive_threshold(
&t,
&ThresholdContext {
gate_confidence: 0.9,
..neutral_ctx()
},
);
assert!(confident > base); }
#[test]
fn threshold_clamped_to_bounds() {
let t = threshold_resource_pressure();
let extreme = get_adaptive_threshold(
&t,
&ThresholdContext {
sensory_pe: 1.0,
arousal: 1.0,
gate_confidence: 0.0,
pe_volatility: 1.0,
},
);
assert!(extreme >= t.min);
assert!(extreme <= t.max);
}
#[test]
fn unified_pe_precision_weighted() {
let dims = vec![
DimensionPE { pe: 0.8, precision: 0.9 }, DimensionPE { pe: 0.2, precision: 0.1 }, ];
let unified = compute_unified_pe(&dims);
assert!(unified > 0.6);
}
#[test]
fn unified_pe_equal_precision() {
let dims = vec![
DimensionPE { pe: 0.8, precision: 1.0 },
DimensionPE { pe: 0.2, precision: 1.0 },
];
let unified = compute_unified_pe(&dims);
assert!((unified - 0.5).abs() < f64::EPSILON);
}
#[test]
fn unified_pe_empty() {
assert!((compute_unified_pe(&[]) - 0.5).abs() < f64::EPSILON);
}
#[test]
fn unified_pe_zero_precision_fallback() {
let dims = vec![
DimensionPE { pe: 0.3, precision: 0.0 },
DimensionPE { pe: 0.7, precision: 0.0 },
];
let unified = compute_unified_pe(&dims);
assert!((unified - 0.5).abs() < f64::EPSILON); }
#[test]
fn negative_valence_amplifies_arousal() {
let ctx = build_threshold_context(0.5, 0.5, 0.5, 0.3, Some(AffectValence::Negative), None);
assert!(ctx.arousal > 0.5); }
#[test]
fn positive_valence_dampens_arousal() {
let ctx = build_threshold_context(0.5, 0.5, 0.5, 0.3, Some(AffectValence::Positive), None);
assert!(ctx.arousal < 0.5); }
#[test]
fn low_body_budget_increases_pe() {
let ctx = build_threshold_context(0.5, 0.3, 0.5, 0.3, None, Some(0.3));
assert!(ctx.sensory_pe > 0.5); assert!(ctx.gate_confidence < 0.5); }
#[test]
fn safe_num_handles_nan() {
assert_eq!(safe_num(f64::NAN, 0.5), 0.5);
assert_eq!(safe_num(0.7, 0.5), 0.7);
}
}