Skip to main content

thrust_rl/train/ppo/
config.rs

1//! PPO configuration and hyperparameters
2//!
3//! This module defines the configuration parameters for PPO training
4//! and provides validation and builder pattern methods.
5
6use anyhow::{Result, anyhow};
7
8/// PPO configuration parameters
9///
10/// These hyperparameters control the PPO training process.
11/// Default values are based on common settings that work well
12/// for simple environments like CartPole.
13#[derive(Debug, Clone)]
14pub struct PPOConfig {
15    /// Learning rate for policy and value function
16    pub learning_rate: f64,
17
18    /// Number of training epochs per rollout
19    pub n_epochs: usize,
20
21    /// Minibatch size for training
22    pub batch_size: usize,
23
24    /// Discount factor (gamma)
25    pub gamma: f64,
26
27    /// GAE lambda parameter
28    pub gae_lambda: f64,
29
30    /// PPO clipping parameter (epsilon)
31    pub clip_range: f64,
32
33    /// Value function clipping parameter
34    pub clip_range_vf: f64,
35
36    /// Value function loss coefficient
37    pub vf_coef: f64,
38
39    /// Entropy bonus coefficient
40    pub ent_coef: f64,
41
42    /// Maximum gradient norm for clipping
43    pub max_grad_norm: f64,
44
45    /// Target KL divergence for early stopping
46    pub target_kl: f64,
47
48    /// Seed for the inner-loop RNG used to shuffle minibatch indices.
49    ///
50    /// Plumbed through the trainer so that two trainers built with the
51    /// same `seed` produce bit-identical updates on the same rollout
52    /// (issue #109). The PPO trainer constructs an internal
53    /// `StdRng::seed_from_u64(seed)` at `new()` time and uses it for
54    /// every minibatch shuffle.
55    pub seed: u64,
56}
57
58impl Default for PPOConfig {
59    fn default() -> Self {
60        Self {
61            learning_rate: 3e-4,
62            n_epochs: 10,
63            batch_size: 64,
64            gamma: 0.99,
65            gae_lambda: 0.95,
66            clip_range: 0.2,
67            clip_range_vf: 0.2,
68            vf_coef: 0.5,
69            ent_coef: 0.01,
70            max_grad_norm: 0.5,
71            target_kl: 0.01,
72            seed: 0,
73        }
74    }
75}
76
77impl PPOConfig {
78    /// Create a new default configuration
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    /// Validate configuration parameters
84    pub fn validate(&self) -> Result<()> {
85        if self.learning_rate <= 0.0 {
86            return Err(anyhow!("learning_rate must be positive"));
87        }
88        if self.n_epochs == 0 {
89            return Err(anyhow!("n_epochs must be positive"));
90        }
91        if self.batch_size == 0 {
92            return Err(anyhow!("batch_size must be positive"));
93        }
94        if !(0.0..=1.0).contains(&self.gamma) {
95            return Err(anyhow!("gamma must be in [0, 1]"));
96        }
97        if !(0.0..=1.0).contains(&self.gae_lambda) {
98            return Err(anyhow!("gae_lambda must be in [0, 1]"));
99        }
100        if self.clip_range <= 0.0 {
101            return Err(anyhow!("clip_range must be positive"));
102        }
103        if self.clip_range_vf <= 0.0 {
104            return Err(anyhow!("clip_range_vf must be positive"));
105        }
106        if self.vf_coef < 0.0 {
107            return Err(anyhow!("vf_coef must be non-negative"));
108        }
109        if self.ent_coef < 0.0 {
110            return Err(anyhow!("ent_coef must be non-negative"));
111        }
112        if self.max_grad_norm <= 0.0 {
113            return Err(anyhow!("max_grad_norm must be positive"));
114        }
115        if self.target_kl < 0.0 {
116            return Err(anyhow!("target_kl must be non-negative"));
117        }
118        Ok(())
119    }
120
121    /// Set learning rate
122    pub fn learning_rate(mut self, lr: f64) -> Self {
123        self.learning_rate = lr;
124        self
125    }
126
127    /// Set number of training epochs
128    pub fn n_epochs(mut self, epochs: usize) -> Self {
129        self.n_epochs = epochs;
130        self
131    }
132
133    /// Set minibatch size
134    pub fn batch_size(mut self, size: usize) -> Self {
135        self.batch_size = size;
136        self
137    }
138
139    /// Set discount factor
140    pub fn gamma(mut self, gamma: f64) -> Self {
141        self.gamma = gamma;
142        self
143    }
144
145    /// Set GAE lambda
146    pub fn gae_lambda(mut self, lambda: f64) -> Self {
147        self.gae_lambda = lambda;
148        self
149    }
150
151    /// Set PPO clipping parameter
152    pub fn clip_range(mut self, clip: f64) -> Self {
153        self.clip_range = clip;
154        self
155    }
156
157    /// Set value function clipping parameter
158    pub fn clip_range_vf(mut self, clip: f64) -> Self {
159        self.clip_range_vf = clip;
160        self
161    }
162
163    /// Set value function loss coefficient
164    pub fn vf_coef(mut self, coef: f64) -> Self {
165        self.vf_coef = coef;
166        self
167    }
168
169    /// Set entropy bonus coefficient
170    pub fn ent_coef(mut self, coef: f64) -> Self {
171        self.ent_coef = coef;
172        self
173    }
174
175    /// Set maximum gradient norm
176    pub fn max_grad_norm(mut self, norm: f64) -> Self {
177        self.max_grad_norm = norm;
178        self
179    }
180
181    /// Set target KL divergence
182    pub fn target_kl(mut self, kl: f64) -> Self {
183        self.target_kl = kl;
184        self
185    }
186
187    /// Set the seed for the inner-loop RNG used for minibatch shuffling.
188    ///
189    /// See [`PPOConfig::seed`].
190    pub fn seed(mut self, seed: u64) -> Self {
191        self.seed = seed;
192        self
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_default_config() {
202        let config = PPOConfig::default();
203        assert!(config.validate().is_ok());
204        assert_eq!(config.learning_rate, 3e-4);
205        assert_eq!(config.n_epochs, 10);
206        assert_eq!(config.batch_size, 64);
207    }
208
209    #[test]
210    fn test_config_validation() {
211        // Valid config should pass
212        let config = PPOConfig::new();
213        assert!(config.validate().is_ok());
214
215        // Invalid learning rate
216        let config = PPOConfig::new().learning_rate(-1.0);
217        assert!(config.validate().is_err());
218
219        // Invalid gamma
220        let config = PPOConfig::new().gamma(1.5);
221        assert!(config.validate().is_err());
222
223        // Invalid n_epochs
224        let config = PPOConfig::new().n_epochs(0);
225        assert!(config.validate().is_err());
226
227        // Invalid batch_size
228        let config = PPOConfig::new().batch_size(0);
229        assert!(config.validate().is_err());
230
231        // Invalid clip_range
232        let config = PPOConfig::new().clip_range(-0.1);
233        assert!(config.validate().is_err());
234
235        // Invalid vf_coef (should allow 0.0)
236        let config = PPOConfig::new().vf_coef(-0.1);
237        assert!(config.validate().is_err());
238
239        // Valid vf_coef = 0.0
240        let config = PPOConfig::new().vf_coef(0.0);
241        assert!(config.validate().is_ok());
242    }
243
244    #[test]
245    fn test_config_builder() {
246        let config = PPOConfig::new()
247            .learning_rate(1e-3)
248            .n_epochs(5)
249            .batch_size(128)
250            .gamma(0.95)
251            .clip_range(0.1);
252
253        assert_eq!(config.learning_rate, 1e-3);
254        assert_eq!(config.n_epochs, 5);
255        assert_eq!(config.batch_size, 128);
256        assert_eq!(config.gamma, 0.95);
257        assert_eq!(config.clip_range, 0.1);
258
259        // Other values should remain default
260        assert_eq!(config.gae_lambda, 0.95);
261        assert_eq!(config.vf_coef, 0.5);
262    }
263}