thrust_rl/train/a2c/
config.rs1use anyhow::{Result, anyhow};
14
15#[derive(Debug, Clone)]
23pub struct A2cConfig {
24 pub learning_rate: f64,
26
27 pub gamma: f64,
29
30 pub gae_lambda: f64,
33
34 pub value_coef: f64,
36
37 pub entropy_coef: f64,
39
40 pub n_steps: usize,
43
44 pub num_envs: usize,
46
47 pub max_grad_norm: f64,
49
50 pub normalize_advantages: bool,
53
54 pub seed: u64,
59}
60
61impl Default for A2cConfig {
62 fn default() -> Self {
63 Self {
64 learning_rate: 7e-4,
65 gamma: 0.99,
66 gae_lambda: 1.0,
67 value_coef: 0.5,
68 entropy_coef: 0.01,
69 n_steps: 5,
70 num_envs: 16,
71 max_grad_norm: 0.5,
72 normalize_advantages: true,
73 seed: 0,
74 }
75 }
76}
77
78impl A2cConfig {
79 pub fn new() -> Self {
81 Self::default()
82 }
83
84 pub fn validate(&self) -> Result<()> {
88 if self.learning_rate <= 0.0 {
89 return Err(anyhow!("learning_rate must be positive, got {}", self.learning_rate));
90 }
91 if !(0.0..=1.0).contains(&self.gamma) {
92 return Err(anyhow!("gamma must be in [0, 1], got {}", self.gamma));
93 }
94 if !(0.0..=1.0).contains(&self.gae_lambda) {
95 return Err(anyhow!("gae_lambda must be in [0, 1], got {}", self.gae_lambda));
96 }
97 if self.value_coef < 0.0 {
98 return Err(anyhow!("value_coef must be non-negative, got {}", self.value_coef));
99 }
100 if self.entropy_coef < 0.0 {
101 return Err(anyhow!("entropy_coef must be non-negative, got {}", self.entropy_coef));
102 }
103 if self.n_steps == 0 {
104 return Err(anyhow!("n_steps must be positive"));
105 }
106 if self.num_envs == 0 {
107 return Err(anyhow!("num_envs must be positive"));
108 }
109 if self.max_grad_norm <= 0.0 {
110 return Err(anyhow!("max_grad_norm must be positive, got {}", self.max_grad_norm));
111 }
112 Ok(())
113 }
114
115 pub fn learning_rate(mut self, lr: f64) -> Self {
119 self.learning_rate = lr;
120 self
121 }
122
123 pub fn gamma(mut self, gamma: f64) -> Self {
125 self.gamma = gamma;
126 self
127 }
128
129 pub fn gae_lambda(mut self, lambda: f64) -> Self {
131 self.gae_lambda = lambda;
132 self
133 }
134
135 pub fn value_coef(mut self, coef: f64) -> Self {
137 self.value_coef = coef;
138 self
139 }
140
141 pub fn entropy_coef(mut self, coef: f64) -> Self {
143 self.entropy_coef = coef;
144 self
145 }
146
147 pub fn n_steps(mut self, steps: usize) -> Self {
149 self.n_steps = steps;
150 self
151 }
152
153 pub fn num_envs(mut self, envs: usize) -> Self {
155 self.num_envs = envs;
156 self
157 }
158
159 pub fn max_grad_norm(mut self, norm: f64) -> Self {
161 self.max_grad_norm = norm;
162 self
163 }
164
165 pub fn normalize_advantages(mut self, enabled: bool) -> Self {
167 self.normalize_advantages = enabled;
168 self
169 }
170
171 pub fn seed(mut self, seed: u64) -> Self {
173 self.seed = seed;
174 self
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_default_config() {
184 let config = A2cConfig::default();
185 assert!(config.validate().is_ok());
186 assert_eq!(config.learning_rate, 7e-4);
187 assert_eq!(config.gamma, 0.99);
188 assert_eq!(config.gae_lambda, 1.0);
189 assert_eq!(config.value_coef, 0.5);
190 assert_eq!(config.entropy_coef, 0.01);
191 assert_eq!(config.n_steps, 5);
192 assert_eq!(config.num_envs, 16);
193 assert_eq!(config.max_grad_norm, 0.5);
194 assert!(config.normalize_advantages);
195 assert_eq!(config.seed, 0);
196 }
197
198 #[test]
199 fn test_config_validation() {
200 let config = A2cConfig::new();
202 assert!(config.validate().is_ok());
203
204 assert!(A2cConfig::new().learning_rate(0.0).validate().is_err());
206 assert!(A2cConfig::new().learning_rate(-1.0).validate().is_err());
207
208 assert!(A2cConfig::new().gamma(-0.1).validate().is_err());
210 assert!(A2cConfig::new().gamma(1.5).validate().is_err());
211 assert!(A2cConfig::new().gamma(0.0).validate().is_ok());
212 assert!(A2cConfig::new().gamma(1.0).validate().is_ok());
213
214 assert!(A2cConfig::new().gae_lambda(-0.1).validate().is_err());
216 assert!(A2cConfig::new().gae_lambda(1.5).validate().is_err());
217 assert!(A2cConfig::new().gae_lambda(0.0).validate().is_ok());
218 assert!(A2cConfig::new().gae_lambda(1.0).validate().is_ok());
219
220 assert!(A2cConfig::new().value_coef(-0.1).validate().is_err());
222 assert!(A2cConfig::new().value_coef(0.0).validate().is_ok());
223
224 assert!(A2cConfig::new().entropy_coef(-0.1).validate().is_err());
226 assert!(A2cConfig::new().entropy_coef(0.0).validate().is_ok());
227
228 assert!(A2cConfig::new().n_steps(0).validate().is_err());
230
231 assert!(A2cConfig::new().num_envs(0).validate().is_err());
233
234 assert!(A2cConfig::new().max_grad_norm(0.0).validate().is_err());
236 assert!(A2cConfig::new().max_grad_norm(-1.0).validate().is_err());
237 }
238
239 #[test]
240 fn test_config_builder() {
241 let config = A2cConfig::new()
242 .learning_rate(1e-3)
243 .gamma(0.95)
244 .gae_lambda(0.9)
245 .value_coef(0.25)
246 .entropy_coef(0.05)
247 .n_steps(20)
248 .num_envs(8)
249 .max_grad_norm(1.0)
250 .normalize_advantages(false)
251 .seed(42);
252
253 assert!(config.validate().is_ok());
254 assert_eq!(config.learning_rate, 1e-3);
255 assert_eq!(config.gamma, 0.95);
256 assert_eq!(config.gae_lambda, 0.9);
257 assert_eq!(config.value_coef, 0.25);
258 assert_eq!(config.entropy_coef, 0.05);
259 assert_eq!(config.n_steps, 20);
260 assert_eq!(config.num_envs, 8);
261 assert_eq!(config.max_grad_norm, 1.0);
262 assert!(!config.normalize_advantages);
263 assert_eq!(config.seed, 42);
264 }
265}