use crate::error::ConfigError;
#[derive(Debug, Clone)]
pub struct PlasticityConfig {
pub regen_fraction: f64,
pub regen_interval: u64,
pub utility_alpha: f64,
}
impl Default for PlasticityConfig {
fn default() -> Self {
Self {
regen_fraction: 0.01,
regen_interval: 500,
utility_alpha: 0.99,
}
}
}
impl std::fmt::Display for PlasticityConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"PlasticityConfig(regen_fraction={}, regen_interval={}, utility_alpha={})",
self.regen_fraction, self.regen_interval, self.utility_alpha
)
}
}
pub struct PlasticityConfigBuilder {
config: PlasticityConfig,
}
impl PlasticityConfig {
pub fn builder() -> PlasticityConfigBuilder {
PlasticityConfigBuilder {
config: PlasticityConfig::default(),
}
}
}
impl PlasticityConfigBuilder {
pub fn regen_fraction(mut self, f: f64) -> Self {
self.config.regen_fraction = f;
self
}
pub fn regen_interval(mut self, n: u64) -> Self {
self.config.regen_interval = n;
self
}
pub fn utility_alpha(mut self, a: f64) -> Self {
self.config.utility_alpha = a;
self
}
pub fn build(self) -> Result<PlasticityConfig, ConfigError> {
let c = &self.config;
if c.regen_fraction <= 0.0 || c.regen_fraction > 1.0 {
return Err(ConfigError::out_of_range(
"regen_fraction",
"must be in (0, 1]",
c.regen_fraction,
));
}
if c.regen_interval == 0 {
return Err(ConfigError::out_of_range(
"regen_interval",
"must be >= 1",
c.regen_interval,
));
}
if c.utility_alpha < 0.0 || c.utility_alpha >= 1.0 {
return Err(ConfigError::out_of_range(
"utility_alpha",
"must be in [0, 1)",
c.utility_alpha,
));
}
Ok(self.config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_values_match_paper() {
let c = PlasticityConfig::default();
assert!(
(c.regen_fraction - 0.01).abs() < 1e-12,
"default regen_fraction should be 0.01 (Dohare 2024 Cα)"
);
assert_eq!(
c.regen_interval, 500,
"default regen_interval should be 500"
);
assert!(
(c.utility_alpha - 0.99).abs() < 1e-12,
"default utility_alpha should be 0.99"
);
}
#[test]
fn builder_round_trips() {
let c = PlasticityConfig::builder()
.regen_fraction(0.02)
.regen_interval(1000)
.utility_alpha(0.995)
.build()
.unwrap();
assert!((c.regen_fraction - 0.02).abs() < 1e-12);
assert_eq!(c.regen_interval, 1000);
assert!((c.utility_alpha - 0.995).abs() < 1e-12);
}
#[test]
fn rejects_zero_regen_fraction() {
assert!(
PlasticityConfig::builder()
.regen_fraction(0.0)
.build()
.is_err(),
"regen_fraction=0 must be rejected"
);
}
#[test]
fn rejects_negative_regen_fraction() {
assert!(
PlasticityConfig::builder()
.regen_fraction(-0.1)
.build()
.is_err(),
"regen_fraction<0 must be rejected"
);
}
#[test]
fn accepts_regen_fraction_one() {
assert!(
PlasticityConfig::builder()
.regen_fraction(1.0)
.build()
.is_ok(),
"regen_fraction=1.0 should be valid (replace all)"
);
}
#[test]
fn rejects_zero_regen_interval() {
assert!(
PlasticityConfig::builder()
.regen_interval(0)
.build()
.is_err(),
"regen_interval=0 must be rejected"
);
}
#[test]
fn rejects_utility_alpha_one() {
assert!(
PlasticityConfig::builder()
.utility_alpha(1.0)
.build()
.is_err(),
"utility_alpha=1.0 must be rejected (denominator issue)"
);
}
#[test]
fn rejects_negative_utility_alpha() {
assert!(
PlasticityConfig::builder()
.utility_alpha(-0.1)
.build()
.is_err(),
"negative utility_alpha must be rejected"
);
}
#[test]
fn accepts_utility_alpha_zero() {
assert!(
PlasticityConfig::builder()
.utility_alpha(0.0)
.build()
.is_ok(),
"utility_alpha=0.0 should be valid (no smoothing)"
);
}
#[test]
fn display_contains_fields() {
let c = PlasticityConfig::default();
let s = format!("{c}");
assert!(
s.contains("regen_fraction="),
"display should contain regen_fraction"
);
assert!(
s.contains("regen_interval="),
"display should contain regen_interval"
);
assert!(
s.contains("utility_alpha="),
"display should contain utility_alpha"
);
}
#[test]
fn clone_is_deep() {
let orig = PlasticityConfig::builder()
.regen_fraction(0.05)
.build()
.unwrap();
let cloned = orig.clone();
assert!((cloned.regen_fraction - 0.05).abs() < 1e-12);
}
}