use anyhow::{Result, anyhow};
#[derive(Debug, Clone)]
pub struct A2cConfig {
pub learning_rate: f64,
pub gamma: f64,
pub gae_lambda: f64,
pub value_coef: f64,
pub entropy_coef: f64,
pub n_steps: usize,
pub num_envs: usize,
pub max_grad_norm: f64,
pub normalize_advantages: bool,
pub use_vtrace: bool,
pub vtrace_rho_bar: f32,
pub vtrace_c_bar: f32,
pub seed: u64,
}
impl Default for A2cConfig {
fn default() -> Self {
Self {
learning_rate: 7e-4,
gamma: 0.99,
gae_lambda: 1.0,
value_coef: 0.5,
entropy_coef: 0.01,
n_steps: 5,
num_envs: 16,
max_grad_norm: 0.5,
normalize_advantages: true,
use_vtrace: false,
vtrace_rho_bar: 1.0,
vtrace_c_bar: 1.0,
seed: 0,
}
}
}
impl A2cConfig {
pub fn new() -> Self {
Self::default()
}
pub fn validate(&self) -> Result<()> {
if self.learning_rate <= 0.0 {
return Err(anyhow!("learning_rate must be positive, got {}", self.learning_rate));
}
if !(0.0..=1.0).contains(&self.gamma) {
return Err(anyhow!("gamma must be in [0, 1], got {}", self.gamma));
}
if !(0.0..=1.0).contains(&self.gae_lambda) {
return Err(anyhow!("gae_lambda must be in [0, 1], got {}", self.gae_lambda));
}
if self.value_coef < 0.0 {
return Err(anyhow!("value_coef must be non-negative, got {}", self.value_coef));
}
if self.entropy_coef < 0.0 {
return Err(anyhow!("entropy_coef must be non-negative, got {}", self.entropy_coef));
}
if self.n_steps == 0 {
return Err(anyhow!("n_steps must be positive"));
}
if self.num_envs == 0 {
return Err(anyhow!("num_envs must be positive"));
}
if self.max_grad_norm <= 0.0 {
return Err(anyhow!("max_grad_norm must be positive, got {}", self.max_grad_norm));
}
if self.vtrace_rho_bar <= 0.0 {
return Err(anyhow!("vtrace_rho_bar must be positive, got {}", self.vtrace_rho_bar));
}
if self.vtrace_c_bar <= 0.0 {
return Err(anyhow!("vtrace_c_bar must be positive, got {}", self.vtrace_c_bar));
}
Ok(())
}
pub fn learning_rate(mut self, lr: f64) -> Self {
self.learning_rate = lr;
self
}
pub fn gamma(mut self, gamma: f64) -> Self {
self.gamma = gamma;
self
}
pub fn gae_lambda(mut self, lambda: f64) -> Self {
self.gae_lambda = lambda;
self
}
pub fn value_coef(mut self, coef: f64) -> Self {
self.value_coef = coef;
self
}
pub fn entropy_coef(mut self, coef: f64) -> Self {
self.entropy_coef = coef;
self
}
pub fn n_steps(mut self, steps: usize) -> Self {
self.n_steps = steps;
self
}
pub fn num_envs(mut self, envs: usize) -> Self {
self.num_envs = envs;
self
}
pub fn max_grad_norm(mut self, norm: f64) -> Self {
self.max_grad_norm = norm;
self
}
pub fn normalize_advantages(mut self, enabled: bool) -> Self {
self.normalize_advantages = enabled;
self
}
pub fn use_vtrace(mut self, enabled: bool) -> Self {
self.use_vtrace = enabled;
self
}
pub fn vtrace_rho_bar(mut self, rho_bar: f32) -> Self {
self.vtrace_rho_bar = rho_bar;
self
}
pub fn vtrace_c_bar(mut self, c_bar: f32) -> Self {
self.vtrace_c_bar = c_bar;
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = A2cConfig::default();
assert!(config.validate().is_ok());
assert_eq!(config.learning_rate, 7e-4);
assert_eq!(config.gamma, 0.99);
assert_eq!(config.gae_lambda, 1.0);
assert_eq!(config.value_coef, 0.5);
assert_eq!(config.entropy_coef, 0.01);
assert_eq!(config.n_steps, 5);
assert_eq!(config.num_envs, 16);
assert_eq!(config.max_grad_norm, 0.5);
assert!(config.normalize_advantages);
assert!(!config.use_vtrace);
assert_eq!(config.vtrace_rho_bar, 1.0);
assert_eq!(config.vtrace_c_bar, 1.0);
assert_eq!(config.seed, 0);
}
#[test]
fn test_config_validation() {
let config = A2cConfig::new();
assert!(config.validate().is_ok());
assert!(A2cConfig::new().learning_rate(0.0).validate().is_err());
assert!(A2cConfig::new().learning_rate(-1.0).validate().is_err());
assert!(A2cConfig::new().gamma(-0.1).validate().is_err());
assert!(A2cConfig::new().gamma(1.5).validate().is_err());
assert!(A2cConfig::new().gamma(0.0).validate().is_ok());
assert!(A2cConfig::new().gamma(1.0).validate().is_ok());
assert!(A2cConfig::new().gae_lambda(-0.1).validate().is_err());
assert!(A2cConfig::new().gae_lambda(1.5).validate().is_err());
assert!(A2cConfig::new().gae_lambda(0.0).validate().is_ok());
assert!(A2cConfig::new().gae_lambda(1.0).validate().is_ok());
assert!(A2cConfig::new().value_coef(-0.1).validate().is_err());
assert!(A2cConfig::new().value_coef(0.0).validate().is_ok());
assert!(A2cConfig::new().entropy_coef(-0.1).validate().is_err());
assert!(A2cConfig::new().entropy_coef(0.0).validate().is_ok());
assert!(A2cConfig::new().n_steps(0).validate().is_err());
assert!(A2cConfig::new().num_envs(0).validate().is_err());
assert!(A2cConfig::new().max_grad_norm(0.0).validate().is_err());
assert!(A2cConfig::new().max_grad_norm(-1.0).validate().is_err());
assert!(A2cConfig::new().vtrace_rho_bar(0.0).validate().is_err());
assert!(A2cConfig::new().vtrace_rho_bar(-0.5).validate().is_err());
assert!(A2cConfig::new().vtrace_rho_bar(1.0).validate().is_ok());
assert!(A2cConfig::new().vtrace_c_bar(0.0).validate().is_err());
assert!(A2cConfig::new().vtrace_c_bar(-0.5).validate().is_err());
assert!(A2cConfig::new().vtrace_c_bar(1.0).validate().is_ok());
assert!(
A2cConfig::new()
.use_vtrace(true)
.vtrace_rho_bar(0.8)
.vtrace_c_bar(1.2)
.validate()
.is_ok()
);
}
#[test]
fn test_config_builder() {
let config = A2cConfig::new()
.learning_rate(1e-3)
.gamma(0.95)
.gae_lambda(0.9)
.value_coef(0.25)
.entropy_coef(0.05)
.n_steps(20)
.num_envs(8)
.max_grad_norm(1.0)
.normalize_advantages(false)
.use_vtrace(true)
.vtrace_rho_bar(0.9)
.vtrace_c_bar(1.1)
.seed(42);
assert!(config.validate().is_ok());
assert_eq!(config.learning_rate, 1e-3);
assert_eq!(config.gamma, 0.95);
assert_eq!(config.gae_lambda, 0.9);
assert_eq!(config.value_coef, 0.25);
assert_eq!(config.entropy_coef, 0.05);
assert_eq!(config.n_steps, 20);
assert_eq!(config.num_envs, 8);
assert_eq!(config.max_grad_norm, 1.0);
assert!(!config.normalize_advantages);
assert!(config.use_vtrace);
assert_eq!(config.vtrace_rho_bar, 0.9);
assert_eq!(config.vtrace_c_bar, 1.1);
assert_eq!(config.seed, 42);
}
}