use crate::environment::Environment;
use crate::error::{CriticError, CriticField, NonFiniteKind, require_finite};
use crate::modulators::ModulatorVector;
pub struct SimpleCritic;
impl SimpleCritic {
#[must_use]
pub fn assess(env: &impl Environment) -> ModulatorVector {
let objective = env.objective();
let stress = env.stress();
let volatility = env.volatility();
let surprise = env.surprise();
simple_modulators(objective, volatility, surprise, stress)
}
pub fn try_assess(env: &impl Environment) -> Result<ModulatorVector, CriticError> {
let objective = require_finite(env.objective(), CriticField::Objective)?;
let volatility = require_finite(env.volatility(), CriticField::Volatility)?;
let surprise = require_finite(env.surprise(), CriticField::Surprise)?;
let stress = require_finite(env.stress(), CriticField::Stress)?;
Ok(simple_modulators(objective, volatility, surprise, stress))
}
}
fn simple_modulators(
objective: f32,
volatility: f32,
surprise: f32,
stress: f32,
) -> ModulatorVector {
let dopamine = if objective > 0.0 {
objective.clamp(0.0, 1.0)
} else {
0.0
};
ModulatorVector {
dopamine,
serotonin: volatility.clamp(0.0, 1.0),
acetylcholine: surprise.clamp(0.0, 1.0),
norepinephrine: stress.clamp(0.0, 1.0),
}
}
pub struct TDCritic {
prev_objective: f32,
ema_reward: f32,
alpha: f32, }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidAlpha;
impl std::fmt::Display for InvalidAlpha {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("TDCritic alpha must be in (0, 1] (finite, greater than 0, at most 1)")
}
}
impl std::error::Error for InvalidAlpha {}
impl TDCritic {
pub fn new(alpha: f32) -> Result<Self, InvalidAlpha> {
if !is_valid_alpha(alpha) {
return Err(InvalidAlpha);
}
Ok(Self {
prev_objective: 0.0,
ema_reward: 0.0,
alpha,
})
}
pub fn assess(&mut self, env: &impl Environment) -> ModulatorVector {
let objective = env.objective();
let td_error = objective - self.prev_objective;
self.prev_objective = objective;
self.ema_reward = (1.0 - self.alpha) * self.ema_reward + self.alpha * td_error;
let stress = env.stress();
let volatility = env.volatility();
td_modulators(td_error, self.ema_reward, volatility, stress)
}
pub fn try_assess(&mut self, env: &impl Environment) -> Result<ModulatorVector, CriticError> {
let objective = require_finite(env.objective(), CriticField::Objective)?;
let volatility = require_finite(env.volatility(), CriticField::Volatility)?;
let stress = require_finite(env.stress(), CriticField::Stress)?;
require_finite(self.prev_objective, CriticField::TdError)?;
require_finite(self.ema_reward, CriticField::EmaReward)?;
let td_error = shaped_td_error(objective, self.prev_objective)?;
let ema_reward = shaped_ema(self.ema_reward, self.alpha, td_error)?;
let mods = td_modulators(td_error, ema_reward, volatility, stress);
debug_assert!(
mods.dopamine.is_finite()
&& mods.serotonin.is_finite()
&& mods.acetylcholine.is_finite()
&& mods.norepinephrine.is_finite()
);
self.prev_objective = objective;
self.ema_reward = ema_reward;
Ok(mods)
}
}
fn td_modulators(td_error: f32, ema_reward: f32, volatility: f32, stress: f32) -> ModulatorVector {
ModulatorVector {
dopamine: ema_reward.tanh().clamp(-1.0, 1.0),
serotonin: volatility.clamp(0.0, 1.0),
acetylcholine: td_error.abs().tanh().clamp(0.0, 1.0),
norepinephrine: stress.clamp(0.0, 1.0),
}
}
fn shaped_td_error(objective: f32, prev_objective: f32) -> Result<f32, CriticError> {
saturate_or_reject(objective - prev_objective, CriticField::TdError)
}
fn shaped_ema(prev_ema: f32, alpha: f32, td_error: f32) -> Result<f32, CriticError> {
saturate_or_reject(
(1.0 - alpha) * prev_ema + alpha * td_error,
CriticField::EmaReward,
)
}
fn saturate_or_reject(value: f32, field: CriticField) -> Result<f32, CriticError> {
if value.is_nan() {
return Err(CriticError::NonFinite {
field,
kind: NonFiniteKind::Nan,
});
}
if value.is_infinite() {
return Ok(value.signum() * f32::MAX);
}
Ok(value)
}
fn is_valid_alpha(alpha: f32) -> bool {
alpha > 0.0 && alpha <= 1.0
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct ConstEnv(f32);
impl Environment for ConstEnv {
fn objective(&self) -> f32 {
self.0
}
}
struct VolatileEnv {
steps: Vec<f32>,
index: usize,
}
impl VolatileEnv {
fn new(values: Vec<f32>) -> Self {
Self {
steps: values,
index: 0,
}
}
}
impl Environment for VolatileEnv {
fn objective(&self) -> f32 {
self.steps[self.index]
}
}
#[test]
fn test_simple_critic_positive_objective() {
let env = ConstEnv(0.8);
let mods = SimpleCritic::assess(&env);
assert_eq!(mods.dopamine, 0.8);
assert_eq!(mods.norepinephrine, 0.0);
}
#[test]
fn test_simple_critic_negative_objective() {
let env = ConstEnv(-0.5);
let mods = SimpleCritic::assess(&env);
assert_eq!(mods.dopamine, 0.0);
}
#[test]
fn test_simple_critic_clamping() {
let env = ConstEnv(5.0);
let mods = SimpleCritic::assess(&env);
assert_eq!(mods.dopamine, 1.0);
}
#[test]
fn test_simple_critic_stress() {
struct StressedEnv;
impl Environment for StressedEnv {
fn objective(&self) -> f32 {
0.5
}
fn stress(&self) -> f32 {
0.9
}
}
let mods = SimpleCritic::assess(&StressedEnv);
assert_eq!(mods.norepinephrine, 0.9);
}
#[test]
fn test_simple_critic_volatility_serotonin() {
struct SimpleVolatileEnv;
impl Environment for SimpleVolatileEnv {
fn objective(&self) -> f32 {
0.5
}
fn volatility(&self) -> f32 {
0.6
}
}
let mods = SimpleCritic::assess(&SimpleVolatileEnv);
assert_eq!(mods.serotonin, 0.6);
}
#[test]
fn test_simple_critic_surprise_acetylcholine() {
struct SurprisingEnv {
surprise: f32,
}
impl Environment for SurprisingEnv {
fn objective(&self) -> f32 {
0.5
}
fn surprise(&self) -> f32 {
self.surprise
}
}
let mods = SimpleCritic::assess(&SurprisingEnv { surprise: 0.7 });
assert_eq!(mods.acetylcholine, 0.7);
}
#[test]
fn test_simple_critic_surprise_acetylcholine_clamping() {
struct SurprisingEnv(f32);
impl Environment for SurprisingEnv {
fn objective(&self) -> f32 {
0.0
}
fn surprise(&self) -> f32 {
self.0
}
}
assert_eq!(
SimpleCritic::assess(&SurprisingEnv(-0.2)).acetylcholine,
0.0
);
assert_eq!(SimpleCritic::assess(&SurprisingEnv(1.5)).acetylcholine, 1.0);
}
#[test]
fn test_td_critic_no_change() {
let env = ConstEnv(0.5);
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
let mods = td.assess(&env);
assert!((mods.dopamine - 0.05f32.tanh()).abs() < 1e-6);
}
#[test]
fn test_td_critic_improvement() {
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
let mut env = VolatileEnv::new(vec![0.0, 1.0]);
let first = td.assess(&env);
env.index = 1;
let second = td.assess(&env);
assert!(second.dopamine > first.dopamine);
}
#[test]
fn test_td_critic_degradation() {
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
let mut env = VolatileEnv::new(vec![1.0, 0.0]);
let first = td.assess(&env);
env.index = 1;
let second = td.assess(&env);
assert!(second.dopamine < first.dopamine);
}
#[test]
fn test_td_critic_surprise() {
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
let env = ConstEnv(0.5);
let mods = td.assess(&env);
assert!((mods.acetylcholine - 0.5f32.tanh()).abs() < 1e-6);
}
#[test]
fn test_td_critic_volatility_serotonin() {
struct TdVolatileEnv {
v: f32,
}
impl Environment for TdVolatileEnv {
fn objective(&self) -> f32 {
0.0
}
fn volatility(&self) -> f32 {
self.v
}
}
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
let env = TdVolatileEnv { v: 0.6 };
let mods = td.assess(&env);
assert_eq!(mods.serotonin, 0.6);
}
#[test]
fn td_critic_rejects_non_positive_or_above_one_alpha() {
assert!(matches!(TDCritic::new(0.0), Err(InvalidAlpha)));
assert!(matches!(TDCritic::new(-0.1), Err(InvalidAlpha)));
assert!(matches!(TDCritic::new(-0.0), Err(InvalidAlpha)));
assert!(matches!(TDCritic::new(1.0001), Err(InvalidAlpha)));
assert!(matches!(TDCritic::new(2.0), Err(InvalidAlpha)));
}
#[test]
fn td_critic_rejects_nonfinite_alpha() {
assert!(matches!(TDCritic::new(f32::NAN), Err(InvalidAlpha)));
assert!(matches!(TDCritic::new(f32::INFINITY), Err(InvalidAlpha)));
assert!(matches!(
TDCritic::new(f32::NEG_INFINITY),
Err(InvalidAlpha)
));
}
#[test]
fn invalid_alpha_display_and_error() {
assert!(matches!(TDCritic::new(0.0), Err(InvalidAlpha)));
assert_eq!(
InvalidAlpha.to_string(),
"TDCritic alpha must be in (0, 1] (finite, greater than 0, at most 1)"
);
let as_error: &dyn std::error::Error = &InvalidAlpha;
assert!(as_error.source().is_none());
}
#[test]
fn td_critic_accepts_unit_interval_alpha() {
assert!(TDCritic::new(1.0).is_ok());
assert!(TDCritic::new(f32::EPSILON).is_ok());
assert!(TDCritic::new(0.5).is_ok());
}
struct AuxEnv {
objective: f32,
surprise: f32,
volatility: f32,
stress: f32,
}
impl Environment for AuxEnv {
fn objective(&self) -> f32 {
self.objective
}
fn surprise(&self) -> f32 {
self.surprise
}
fn volatility(&self) -> f32 {
self.volatility
}
fn stress(&self) -> f32 {
self.stress
}
}
#[test]
fn simple_critic_nonfinite_and_negative_objectives() {
assert_eq!(SimpleCritic::assess(&ConstEnv(f32::NAN)).dopamine, 0.0);
assert_eq!(
SimpleCritic::assess(&ConstEnv(f32::NEG_INFINITY)).dopamine,
0.0
);
assert_eq!(SimpleCritic::assess(&ConstEnv(-1.5)).dopamine, 0.0);
assert_eq!(SimpleCritic::assess(&ConstEnv(0.0)).dopamine, 0.0);
assert_eq!(SimpleCritic::assess(&ConstEnv(f32::INFINITY)).dopamine, 1.0);
}
#[test]
fn simple_critic_nonfinite_aux_signals_follow_clamp() {
let nan = SimpleCritic::assess(&AuxEnv {
objective: 0.2,
surprise: f32::NAN,
volatility: f32::NAN,
stress: f32::NAN,
});
assert!(nan.acetylcholine.is_nan());
assert!(nan.serotonin.is_nan());
assert!(nan.norepinephrine.is_nan());
let inf = SimpleCritic::assess(&AuxEnv {
objective: 0.2,
surprise: f32::INFINITY,
volatility: f32::NEG_INFINITY,
stress: f32::INFINITY,
});
assert_eq!(inf.acetylcholine, 1.0);
assert_eq!(inf.serotonin, 0.0);
assert_eq!(inf.norepinephrine, 1.0);
}
#[test]
fn td_critic_nan_objective_propagates_through_td() {
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
let mods = td.assess(&ConstEnv(f32::NAN));
assert!(mods.dopamine.is_nan());
assert!(mods.acetylcholine.is_nan());
}
#[test]
fn td_critic_infinite_objective_saturates_tanh() {
let mut td = TDCritic::new(1.0).expect("alpha in (0, 1]");
let pos = td.assess(&ConstEnv(f32::INFINITY));
assert_eq!(pos.dopamine, 1.0);
assert_eq!(pos.acetylcholine, 1.0);
let mut td = TDCritic::new(1.0).expect("alpha in (0, 1]");
let neg = td.assess(&ConstEnv(f32::NEG_INFINITY));
assert_eq!(neg.dopamine, -1.0);
assert_eq!(neg.acetylcholine, 1.0);
}
#[test]
fn td_critic_infinite_history_then_finite_yields_nan_dopamine() {
let mut td = TDCritic::new(1.0).expect("alpha in (0, 1]");
let _ = td.assess(&ConstEnv(f32::INFINITY));
let after = td.assess(&ConstEnv(0.0));
assert!(after.dopamine.is_nan());
assert_eq!(after.acetylcholine, 1.0);
}
#[test]
fn td_critic_nan_history_then_finite_stays_nan() {
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
let _ = td.assess(&ConstEnv(f32::NAN));
let after = td.assess(&ConstEnv(0.5));
assert!(after.dopamine.is_nan());
assert!(after.acetylcholine.is_nan());
}
#[test]
fn td_critic_same_signed_infinities_yield_nan_ach() {
let mut td = TDCritic::new(1.0).expect("alpha in (0, 1]");
let _ = td.assess(&ConstEnv(f32::INFINITY));
let after = td.assess(&ConstEnv(f32::INFINITY));
assert!(after.dopamine.is_nan());
assert!(after.acetylcholine.is_nan());
}
#[test]
fn simple_critic_clamps_all_modulator_channels() {
let high = SimpleCritic::assess(&AuxEnv {
objective: 8.0,
surprise: 3.0,
volatility: 4.0,
stress: 5.0,
});
assert_eq!(high.dopamine, 1.0);
assert_eq!(high.acetylcholine, 1.0);
assert_eq!(high.serotonin, 1.0);
assert_eq!(high.norepinephrine, 1.0);
let low = SimpleCritic::assess(&AuxEnv {
objective: -2.0,
surprise: -0.5,
volatility: -1.0,
stress: -3.0,
});
assert_eq!(low.dopamine, 0.0);
assert_eq!(low.acetylcholine, 0.0);
assert_eq!(low.serotonin, 0.0);
assert_eq!(low.norepinephrine, 0.0);
}
#[test]
fn simple_critic_zero_objective_is_no_dopamine() {
let mods = SimpleCritic::assess(&AuxEnv {
objective: 0.0,
surprise: 0.25,
volatility: 0.4,
stress: 0.1,
});
assert_eq!(mods.dopamine, 0.0);
assert_eq!(mods.acetylcholine, 0.25);
assert_eq!(mods.serotonin, 0.4);
assert_eq!(mods.norepinephrine, 0.1);
}
#[test]
fn td_critic_dopamine_positive_on_objective_improvement() {
let mut td = TDCritic::new(0.5).expect("alpha in (0, 1]");
let baseline = td.assess(&ConstEnv(0.2));
let improved = td.assess(&ConstEnv(0.9));
assert!(
improved.dopamine > baseline.dopamine,
"improvement should raise dopamine ({baseline:?} -> {improved:?})"
);
assert!(improved.dopamine > 0.0);
}
#[test]
fn td_critic_acetylcholine_is_abs_td_tanh() {
let mut td = TDCritic::new(0.3).expect("alpha in (0, 1]");
let first = td.assess(&ConstEnv(0.4));
assert!((first.acetylcholine - 0.4f32.abs().tanh()).abs() < 1e-6);
let drop = td.assess(&ConstEnv(-0.6));
assert!((drop.acetylcholine - 1.0f32.tanh()).abs() < 1e-6);
let mut td_up = TDCritic::new(0.3).expect("alpha in (0, 1]");
let _ = td_up.assess(&ConstEnv(0.4));
let rise = td_up.assess(&ConstEnv(1.4)); assert!((rise.acetylcholine - drop.acetylcholine).abs() < 1e-6);
}
#[test]
fn td_critic_negative_and_zero_objectives() {
let mut td = TDCritic::new(1.0).expect("alpha in (0, 1]");
let zero = td.assess(&ConstEnv(0.0));
assert!((zero.dopamine - 0.0f32.tanh()).abs() < 1e-6);
assert!((zero.acetylcholine - 0.0f32.tanh()).abs() < 1e-6);
let worse = td.assess(&ConstEnv(-0.8));
assert!(worse.dopamine < 0.0);
assert!((worse.dopamine - (-0.8f32).tanh()).abs() < 1e-6);
assert!((worse.acetylcholine - 0.8f32.tanh()).abs() < 1e-6);
let recover = td.assess(&ConstEnv(0.0));
assert!(recover.dopamine > 0.0);
assert!((recover.dopamine - 0.8f32.tanh()).abs() < 1e-6);
}
#[test]
fn td_critic_alpha_one_tracks_latest_delta_only() {
let mut td = TDCritic::new(1.0).expect("alpha in (0, 1]");
let _ = td.assess(&ConstEnv(2.0));
let second = td.assess(&ConstEnv(2.5));
assert!((second.dopamine - 0.5f32.tanh()).abs() < 1e-6);
}
fn finite_env() -> AuxEnv {
AuxEnv {
objective: 0.4,
surprise: 0.1,
volatility: 0.2,
stress: 0.3,
}
}
fn assert_non_finite(err: CriticError, field: CriticField, kind: NonFiniteKind) {
assert_eq!(err.field(), field);
assert_eq!(err.kind(), kind);
}
fn assert_simple_bounds(mods: ModulatorVector) {
assert!(mods.dopamine.is_finite() && (0.0..=1.0).contains(&mods.dopamine));
assert!(mods.serotonin.is_finite() && (0.0..=1.0).contains(&mods.serotonin));
assert!(mods.acetylcholine.is_finite() && (0.0..=1.0).contains(&mods.acetylcholine));
assert!(mods.norepinephrine.is_finite() && (0.0..=1.0).contains(&mods.norepinephrine));
}
fn assert_td_bounds(mods: ModulatorVector) {
assert!(mods.dopamine.is_finite() && (-1.0..=1.0).contains(&mods.dopamine));
assert!(mods.serotonin.is_finite() && (0.0..=1.0).contains(&mods.serotonin));
assert!(mods.acetylcholine.is_finite() && (0.0..=1.0).contains(&mods.acetylcholine));
assert!(mods.norepinephrine.is_finite() && (0.0..=1.0).contains(&mods.norepinephrine));
}
fn non_finite_cases() -> [(f32, NonFiniteKind); 3] {
[
(f32::NAN, NonFiniteKind::Nan),
(f32::INFINITY, NonFiniteKind::PositiveInfinity),
(f32::NEG_INFINITY, NonFiniteKind::NegativeInfinity),
]
}
#[test]
fn try_assess_rejects_each_simple_channel_independently() {
for (value, kind) in non_finite_cases() {
let mut env = finite_env();
env.objective = value;
assert_non_finite(
SimpleCritic::try_assess(&env).unwrap_err(),
CriticField::Objective,
kind,
);
env = finite_env();
env.volatility = value;
assert_non_finite(
SimpleCritic::try_assess(&env).unwrap_err(),
CriticField::Volatility,
kind,
);
env = finite_env();
env.surprise = value;
assert_non_finite(
SimpleCritic::try_assess(&env).unwrap_err(),
CriticField::Surprise,
kind,
);
env = finite_env();
env.stress = value;
assert_non_finite(
SimpleCritic::try_assess(&env).unwrap_err(),
CriticField::Stress,
kind,
);
}
}
#[test]
fn try_assess_rejects_each_td_shared_channel_independently() {
for (value, kind) in non_finite_cases() {
let mut env = finite_env();
env.objective = value;
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
assert_non_finite(
td.try_assess(&env).unwrap_err(),
CriticField::Objective,
kind,
);
env = finite_env();
env.volatility = value;
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
assert_non_finite(
td.try_assess(&env).unwrap_err(),
CriticField::Volatility,
kind,
);
env = finite_env();
env.stress = value;
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
assert_non_finite(td.try_assess(&env).unwrap_err(), CriticField::Stress, kind);
}
}
#[test]
fn try_assess_shared_channel_errors_match_between_critics() {
for (value, kind) in non_finite_cases() {
let cases = [
(
AuxEnv {
objective: value,
surprise: 0.1,
volatility: 0.2,
stress: 0.3,
},
CriticField::Objective,
),
(
AuxEnv {
objective: 0.4,
surprise: 0.1,
volatility: value,
stress: 0.3,
},
CriticField::Volatility,
),
(
AuxEnv {
objective: 0.4,
surprise: 0.1,
volatility: 0.2,
stress: value,
},
CriticField::Stress,
),
];
for (env, field) in cases {
let simple = SimpleCritic::try_assess(&env).unwrap_err();
let td = TDCritic::new(0.25)
.expect("alpha in (0, 1]")
.try_assess(&env)
.unwrap_err();
assert_eq!(simple, td);
assert_non_finite(simple, field, kind);
}
}
}
#[test]
fn try_assess_td_ignores_nonfinite_surprise() {
let env = AuxEnv {
objective: 0.4,
surprise: f32::NAN,
volatility: 0.2,
stress: 0.3,
};
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
let mods = td.try_assess(&env).expect("surprise is not a TD input");
assert_td_bounds(mods);
assert!(SimpleCritic::try_assess(&env).is_err());
}
#[test]
fn try_assess_reports_first_nonfinite_channel_in_documented_order() {
let all_bad = AuxEnv {
objective: f32::NAN,
surprise: f32::INFINITY,
volatility: f32::NEG_INFINITY,
stress: f32::NAN,
};
assert_eq!(
SimpleCritic::try_assess(&all_bad).unwrap_err().field(),
CriticField::Objective
);
let aux_bad = AuxEnv {
objective: 0.1,
surprise: f32::NAN,
volatility: f32::INFINITY,
stress: f32::NAN,
};
assert_eq!(
SimpleCritic::try_assess(&aux_bad).unwrap_err().field(),
CriticField::Volatility
);
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
assert_eq!(
td.try_assess(&aux_bad).unwrap_err().field(),
CriticField::Volatility
);
}
#[test]
fn try_assess_invalid_sample_does_not_poison_td_or_simple_output() {
let valid = finite_env();
let retry = AuxEnv {
objective: 0.85,
surprise: 0.15,
volatility: 0.25,
stress: 0.05,
};
let invalid_envs = [
AuxEnv {
objective: f32::NAN,
surprise: 0.1,
volatility: 0.2,
stress: 0.3,
},
AuxEnv {
objective: 0.4,
surprise: 0.1,
volatility: f32::INFINITY,
stress: 0.3,
},
AuxEnv {
objective: 0.4,
surprise: 0.1,
volatility: 0.2,
stress: f32::NEG_INFINITY,
},
];
for invalid in invalid_envs {
let mut observed = TDCritic::new(0.2).expect("alpha in (0, 1]");
let mut control = TDCritic::new(0.2).expect("alpha in (0, 1]");
assert_eq!(
observed.try_assess(&valid).unwrap(),
control.try_assess(&valid).unwrap()
);
let simple_err = SimpleCritic::try_assess(&invalid).unwrap_err();
let td_err = observed.try_assess(&invalid).unwrap_err();
assert_eq!(simple_err, td_err);
assert_eq!(
observed.try_assess(&retry).unwrap(),
control.try_assess(&retry).unwrap()
);
assert_simple_bounds(SimpleCritic::try_assess(&retry).unwrap());
}
}
#[test]
fn try_assess_td_surprise_nan_does_not_diverge_from_control() {
let valid = finite_env();
let surprise_nan = AuxEnv {
objective: 0.4,
surprise: f32::NAN,
volatility: 0.2,
stress: 0.3,
};
let retry = AuxEnv {
objective: 0.9,
surprise: 0.0,
volatility: 0.2,
stress: 0.3,
};
let mut observed = TDCritic::new(0.2).expect("alpha in (0, 1]");
let mut control = TDCritic::new(0.2).expect("alpha in (0, 1]");
assert_eq!(
observed.try_assess(&valid).unwrap(),
control.try_assess(&valid).unwrap()
);
assert_eq!(
observed.try_assess(&surprise_nan).unwrap(),
control.try_assess(&surprise_nan).unwrap()
);
assert_eq!(
observed.try_assess(&retry).unwrap(),
control.try_assess(&retry).unwrap()
);
}
#[test]
fn try_assess_extreme_finite_values_are_clamped() {
let high = AuxEnv {
objective: f32::MAX,
surprise: f32::MAX,
volatility: 1.0e30,
stress: 8.0,
};
let simple_high = SimpleCritic::try_assess(&high).unwrap();
assert_eq!(simple_high.dopamine, 1.0);
assert_eq!(simple_high.acetylcholine, 1.0);
assert_eq!(simple_high.serotonin, 1.0);
assert_eq!(simple_high.norepinephrine, 1.0);
let low = AuxEnv {
objective: f32::MIN,
surprise: -1.0e20,
volatility: f32::MIN,
stress: -0.25,
};
let simple_low = SimpleCritic::try_assess(&low).unwrap();
assert_eq!(simple_low.dopamine, 0.0);
assert_eq!(simple_low.acetylcholine, 0.0);
assert_eq!(simple_low.serotonin, 0.0);
assert_eq!(simple_low.norepinephrine, 0.0);
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
let first = td.try_assess(&high).unwrap();
assert_td_bounds(first);
assert_eq!(first.serotonin, 1.0);
assert_eq!(first.norepinephrine, 1.0);
assert_eq!(first.acetylcholine, 1.0);
assert_eq!(first.dopamine, 1.0);
let opposite = AuxEnv {
objective: f32::MIN,
surprise: 0.0,
volatility: 0.0,
stress: 0.0,
};
let second = td.try_assess(&opposite).unwrap();
assert_td_bounds(second);
assert_eq!(second.dopamine, -1.0);
assert_eq!(second.acetylcholine, 1.0);
let modest = td.try_assess(&finite_env()).unwrap();
assert_td_bounds(modest);
}
#[test]
fn try_assess_matches_assess_on_finite_inputs() {
let samples = [
finite_env(),
AuxEnv {
objective: -2.0,
surprise: 1.5,
volatility: -0.4,
stress: 0.0,
},
AuxEnv {
objective: 0.0,
surprise: 0.25,
volatility: 0.4,
stress: 0.1,
},
];
for env in samples {
assert_eq!(
SimpleCritic::assess(&env),
SimpleCritic::try_assess(&env).unwrap()
);
}
let sequence = [0.0, 0.4, -0.2, 1.2, 0.8];
let mut via_assess = TDCritic::new(0.3).expect("alpha in (0, 1]");
let mut via_try = TDCritic::new(0.3).expect("alpha in (0, 1]");
for objective in sequence {
let env = ConstEnv(objective);
assert_eq!(via_assess.assess(&env), via_try.try_assess(&env).unwrap());
}
}
#[test]
fn try_assess_nan_intermediate_from_poisoned_assess_state() {
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
let _ = td.assess(&ConstEnv(f32::NAN));
let err = td.try_assess(&ConstEnv(0.5)).unwrap_err();
assert_eq!(err.field(), CriticField::TdError);
assert_eq!(err.kind(), NonFiniteKind::Nan);
let still = td.try_assess(&ConstEnv(0.25)).unwrap_err();
assert_eq!(still.field(), CriticField::TdError);
}
#[test]
fn try_assess_rejects_infinite_state_from_poisoned_assess() {
let mut td = TDCritic::new(1.0).expect("alpha in (0, 1]");
let _ = td.assess(&ConstEnv(f32::INFINITY));
let err = td.try_assess(&ConstEnv(0.5)).unwrap_err();
assert_eq!(err.field(), CriticField::TdError);
assert_eq!(err.kind(), NonFiniteKind::PositiveInfinity);
let still = td.try_assess(&ConstEnv(0.25)).unwrap_err();
assert_eq!(still.field(), CriticField::TdError);
assert_eq!(still.kind(), NonFiniteKind::PositiveInfinity);
}
#[test]
fn assess_reads_environment_channels_in_compatibility_order() {
struct OrderEnv {
calls: RefCell<Vec<&'static str>>,
}
impl Environment for OrderEnv {
fn objective(&self) -> f32 {
self.calls.borrow_mut().push("objective");
0.5
}
fn volatility(&self) -> f32 {
self.calls.borrow_mut().push("volatility");
0.0
}
fn surprise(&self) -> f32 {
self.calls.borrow_mut().push("surprise");
0.0
}
fn stress(&self) -> f32 {
self.calls.borrow_mut().push("stress");
0.0
}
}
let simple = OrderEnv {
calls: RefCell::new(Vec::new()),
};
let _ = SimpleCritic::assess(&simple);
assert_eq!(
*simple.calls.borrow(),
["objective", "stress", "volatility", "surprise"]
);
let td_env = OrderEnv {
calls: RefCell::new(Vec::new()),
};
let mut td = TDCritic::new(0.1).expect("alpha in (0, 1]");
let _ = td.assess(&td_env);
assert_eq!(
*td_env.calls.borrow(),
["objective", "stress", "volatility"]
);
}
#[test]
fn property_try_assess_outputs_are_finite_and_in_range() {
let interesting = [
0.0,
-0.0,
1.0,
-1.0,
f32::MIN_POSITIVE,
-f32::MIN_POSITIVE,
f32::MAX,
f32::MIN,
1.0e-20,
-1.0e10,
42.0,
];
for &objective in &interesting {
for &surprise in &interesting {
for &volatility in &interesting {
for &stress in &interesting {
let env = AuxEnv {
objective,
surprise,
volatility,
stress,
};
let simple = SimpleCritic::try_assess(&env).unwrap();
assert_simple_bounds(simple);
}
}
}
}
let mut td = TDCritic::new(0.15).expect("alpha in (0, 1]");
let mut bits: u32 = 0xA5A5_5A5A;
for _ in 0..400 {
bits ^= bits << 13;
bits ^= bits >> 17;
bits ^= bits << 5;
let objective = f32::from_bits(bits);
if !objective.is_finite() {
continue;
}
bits ^= bits << 13;
bits ^= bits >> 17;
bits ^= bits << 5;
let volatility = f32::from_bits(bits);
if !volatility.is_finite() {
continue;
}
bits ^= bits << 13;
bits ^= bits >> 17;
bits ^= bits << 5;
let stress = f32::from_bits(bits);
if !stress.is_finite() {
continue;
}
let env = AuxEnv {
objective,
surprise: 0.0,
volatility,
stress,
};
let mods = td.try_assess(&env).unwrap();
assert_td_bounds(mods);
let simple = SimpleCritic::try_assess(&env).unwrap();
assert_simple_bounds(simple);
}
}
#[test]
fn try_assess_simple_surprise_invalid_does_not_affect_retry() {
let valid = finite_env();
let invalid = AuxEnv {
objective: 0.4,
surprise: f32::NAN,
volatility: 0.2,
stress: 0.3,
};
assert_eq!(
SimpleCritic::try_assess(&invalid).unwrap_err().field(),
CriticField::Surprise
);
assert_eq!(
SimpleCritic::try_assess(&valid).unwrap(),
SimpleCritic::assess(&valid)
);
}
}