use anyhow::{Result, anyhow};
#[derive(Debug, Clone)]
pub struct PPOConfig {
pub learning_rate: f64,
pub n_epochs: usize,
pub batch_size: usize,
pub gamma: f64,
pub gae_lambda: f64,
pub clip_range: f64,
pub clip_range_vf: f64,
pub vf_coef: f64,
pub ent_coef: f64,
pub max_grad_norm: f64,
pub target_kl: f64,
pub seed: u64,
}
impl Default for PPOConfig {
fn default() -> Self {
Self {
learning_rate: 3e-4,
n_epochs: 10,
batch_size: 64,
gamma: 0.99,
gae_lambda: 0.95,
clip_range: 0.2,
clip_range_vf: 0.2,
vf_coef: 0.5,
ent_coef: 0.01,
max_grad_norm: 0.5,
target_kl: 0.01,
seed: 0,
}
}
}
impl PPOConfig {
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"));
}
if self.n_epochs == 0 {
return Err(anyhow!("n_epochs must be positive"));
}
if self.batch_size == 0 {
return Err(anyhow!("batch_size must be positive"));
}
if !(0.0..=1.0).contains(&self.gamma) {
return Err(anyhow!("gamma must be in [0, 1]"));
}
if !(0.0..=1.0).contains(&self.gae_lambda) {
return Err(anyhow!("gae_lambda must be in [0, 1]"));
}
if self.clip_range <= 0.0 {
return Err(anyhow!("clip_range must be positive"));
}
if self.clip_range_vf <= 0.0 {
return Err(anyhow!("clip_range_vf must be positive"));
}
if self.vf_coef < 0.0 {
return Err(anyhow!("vf_coef must be non-negative"));
}
if self.ent_coef < 0.0 {
return Err(anyhow!("ent_coef must be non-negative"));
}
if self.max_grad_norm <= 0.0 {
return Err(anyhow!("max_grad_norm must be positive"));
}
if self.target_kl < 0.0 {
return Err(anyhow!("target_kl must be non-negative"));
}
Ok(())
}
pub fn learning_rate(mut self, lr: f64) -> Self {
self.learning_rate = lr;
self
}
pub fn n_epochs(mut self, epochs: usize) -> Self {
self.n_epochs = epochs;
self
}
pub fn batch_size(mut self, size: usize) -> Self {
self.batch_size = size;
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 clip_range(mut self, clip: f64) -> Self {
self.clip_range = clip;
self
}
pub fn clip_range_vf(mut self, clip: f64) -> Self {
self.clip_range_vf = clip;
self
}
pub fn vf_coef(mut self, coef: f64) -> Self {
self.vf_coef = coef;
self
}
pub fn ent_coef(mut self, coef: f64) -> Self {
self.ent_coef = coef;
self
}
pub fn max_grad_norm(mut self, norm: f64) -> Self {
self.max_grad_norm = norm;
self
}
pub fn target_kl(mut self, kl: f64) -> Self {
self.target_kl = kl;
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 = PPOConfig::default();
assert!(config.validate().is_ok());
assert_eq!(config.learning_rate, 3e-4);
assert_eq!(config.n_epochs, 10);
assert_eq!(config.batch_size, 64);
}
#[test]
fn test_config_validation() {
let config = PPOConfig::new();
assert!(config.validate().is_ok());
let config = PPOConfig::new().learning_rate(-1.0);
assert!(config.validate().is_err());
let config = PPOConfig::new().gamma(1.5);
assert!(config.validate().is_err());
let config = PPOConfig::new().n_epochs(0);
assert!(config.validate().is_err());
let config = PPOConfig::new().batch_size(0);
assert!(config.validate().is_err());
let config = PPOConfig::new().clip_range(-0.1);
assert!(config.validate().is_err());
let config = PPOConfig::new().vf_coef(-0.1);
assert!(config.validate().is_err());
let config = PPOConfig::new().vf_coef(0.0);
assert!(config.validate().is_ok());
}
#[test]
fn test_config_builder() {
let config = PPOConfig::new()
.learning_rate(1e-3)
.n_epochs(5)
.batch_size(128)
.gamma(0.95)
.clip_range(0.1);
assert_eq!(config.learning_rate, 1e-3);
assert_eq!(config.n_epochs, 5);
assert_eq!(config.batch_size, 128);
assert_eq!(config.gamma, 0.95);
assert_eq!(config.clip_range, 0.1);
assert_eq!(config.gae_lambda, 0.95);
assert_eq!(config.vf_coef, 0.5);
}
}