1use anyhow::{Result, anyhow};
8
9#[derive(Debug, Clone)]
16pub struct DQNConfig {
17 pub learning_rate: f64,
19
20 pub batch_size: usize,
22
23 pub buffer_capacity: usize,
26
27 pub min_buffer_size: usize,
32
33 pub target_update_interval: usize,
36
37 pub gamma: f64,
41
42 pub epsilon_start: f64,
44
45 pub epsilon_end: f64,
47
48 pub epsilon_decay_steps: usize,
52
53 pub max_grad_norm: f64,
55
56 pub soft_update_tau: Option<f64>,
76
77 pub prioritized_replay: bool,
90
91 pub per_alpha: f64,
96
97 pub per_beta_start: f64,
100
101 pub per_beta_end: f64,
105
106 pub per_beta_steps: usize,
111
112 pub per_epsilon: f64,
116}
117
118impl Default for DQNConfig {
119 fn default() -> Self {
120 Self {
121 learning_rate: 1e-3,
122 batch_size: 64,
123 buffer_capacity: 50_000,
124 min_buffer_size: 1_000,
125 target_update_interval: 500,
126 gamma: 0.99,
127 epsilon_start: 1.0,
128 epsilon_end: 0.05,
129 epsilon_decay_steps: 10_000,
130 max_grad_norm: 10.0,
131 soft_update_tau: None,
132 prioritized_replay: false,
133 per_alpha: 0.6,
134 per_beta_start: 0.4,
135 per_beta_end: 1.0,
136 per_beta_steps: 0,
137 per_epsilon: 1e-6,
138 }
139 }
140}
141
142impl DQNConfig {
143 pub fn new() -> Self {
145 Self::default()
146 }
147
148 pub fn validate(&self) -> Result<()> {
152 if self.learning_rate <= 0.0 {
153 return Err(anyhow!("learning_rate must be positive"));
154 }
155 if self.batch_size == 0 {
156 return Err(anyhow!("batch_size must be positive"));
157 }
158 if self.buffer_capacity == 0 {
159 return Err(anyhow!("buffer_capacity must be positive"));
160 }
161 if self.buffer_capacity < self.batch_size {
162 return Err(anyhow!(
163 "buffer_capacity ({}) must be at least batch_size ({})",
164 self.buffer_capacity,
165 self.batch_size
166 ));
167 }
168 if self.min_buffer_size > self.buffer_capacity {
169 return Err(anyhow!(
170 "min_buffer_size ({}) must be <= buffer_capacity ({})",
171 self.min_buffer_size,
172 self.buffer_capacity
173 ));
174 }
175 if self.target_update_interval == 0 {
176 return Err(anyhow!("target_update_interval must be positive"));
177 }
178 if !(0.0..=1.0).contains(&self.gamma) {
179 return Err(anyhow!("gamma must be in [0, 1]"));
180 }
181 if !(0.0..=1.0).contains(&self.epsilon_start) {
182 return Err(anyhow!("epsilon_start must be in [0, 1]"));
183 }
184 if !(0.0..=1.0).contains(&self.epsilon_end) {
185 return Err(anyhow!("epsilon_end must be in [0, 1]"));
186 }
187 if self.epsilon_end > self.epsilon_start {
188 return Err(anyhow!(
189 "epsilon_end ({}) must be <= epsilon_start ({})",
190 self.epsilon_end,
191 self.epsilon_start
192 ));
193 }
194 if self.epsilon_decay_steps == 0 {
195 return Err(anyhow!("epsilon_decay_steps must be positive"));
196 }
197 if self.max_grad_norm <= 0.0 {
198 return Err(anyhow!("max_grad_norm must be positive"));
199 }
200 if let Some(tau) = self.soft_update_tau
201 && !(tau > 0.0 && tau <= 1.0)
202 {
203 return Err(anyhow!("soft_update_tau must be in (0, 1], got {}", tau));
204 }
205 if !(0.0..=1.0).contains(&self.per_alpha) {
210 return Err(anyhow!("per_alpha must be in [0, 1], got {}", self.per_alpha));
211 }
212 if !(0.0..=1.0).contains(&self.per_beta_start) {
213 return Err(anyhow!("per_beta_start must be in [0, 1], got {}", self.per_beta_start));
214 }
215 if !(0.0..=1.0).contains(&self.per_beta_end) {
216 return Err(anyhow!("per_beta_end must be in [0, 1], got {}", self.per_beta_end));
217 }
218 if self.per_beta_start > self.per_beta_end {
219 return Err(anyhow!(
220 "per_beta_start ({}) must be <= per_beta_end ({})",
221 self.per_beta_start,
222 self.per_beta_end
223 ));
224 }
225 if self.per_epsilon < 0.0 || !self.per_epsilon.is_finite() {
226 return Err(anyhow!(
227 "per_epsilon must be finite and non-negative, got {}",
228 self.per_epsilon
229 ));
230 }
231 Ok(())
232 }
233
234 pub fn beta_at(&self, env_steps: usize) -> f64 {
244 let steps = if self.per_beta_steps == 0 {
245 self.epsilon_decay_steps.max(1)
246 } else {
247 self.per_beta_steps
248 };
249 let fraction = ((env_steps as f64) / (steps as f64)).clamp(0.0, 1.0);
250 self.per_beta_start + (self.per_beta_end - self.per_beta_start) * fraction
251 }
252
253 pub fn epsilon_at(&self, env_steps: usize) -> f64 {
260 if self.epsilon_decay_steps == 0 {
261 return self.epsilon_end;
262 }
263 let fraction = (env_steps as f64) / (self.epsilon_decay_steps as f64);
264 let eps = self.epsilon_start - (self.epsilon_start - self.epsilon_end) * fraction;
265 eps.max(self.epsilon_end)
266 }
267
268 pub fn learning_rate(mut self, lr: f64) -> Self {
272 self.learning_rate = lr;
273 self
274 }
275
276 pub fn batch_size(mut self, size: usize) -> Self {
278 self.batch_size = size;
279 self
280 }
281
282 pub fn buffer_capacity(mut self, capacity: usize) -> Self {
284 self.buffer_capacity = capacity;
285 self
286 }
287
288 pub fn min_buffer_size(mut self, size: usize) -> Self {
290 self.min_buffer_size = size;
291 self
292 }
293
294 pub fn target_update_interval(mut self, steps: usize) -> Self {
296 self.target_update_interval = steps;
297 self
298 }
299
300 pub fn gamma(mut self, gamma: f64) -> Self {
302 self.gamma = gamma;
303 self
304 }
305
306 pub fn epsilon_start(mut self, eps: f64) -> Self {
308 self.epsilon_start = eps;
309 self
310 }
311
312 pub fn epsilon_end(mut self, eps: f64) -> Self {
314 self.epsilon_end = eps;
315 self
316 }
317
318 pub fn epsilon_decay_steps(mut self, steps: usize) -> Self {
320 self.epsilon_decay_steps = steps;
321 self
322 }
323
324 pub fn max_grad_norm(mut self, norm: f64) -> Self {
326 self.max_grad_norm = norm;
327 self
328 }
329
330 pub fn soft_update_tau(mut self, tau: f64) -> Self {
339 self.soft_update_tau = Some(tau);
340 self
341 }
342
343 pub fn prioritized_replay(mut self, enabled: bool) -> Self {
345 self.prioritized_replay = enabled;
346 self
347 }
348
349 pub fn per_alpha(mut self, alpha: f64) -> Self {
351 self.per_alpha = alpha;
352 self
353 }
354
355 pub fn per_beta_start(mut self, beta: f64) -> Self {
357 self.per_beta_start = beta;
358 self
359 }
360
361 pub fn per_beta_end(mut self, beta: f64) -> Self {
363 self.per_beta_end = beta;
364 self
365 }
366
367 pub fn per_beta_steps(mut self, steps: usize) -> Self {
370 self.per_beta_steps = steps;
371 self
372 }
373
374 pub fn per_epsilon(mut self, eps: f64) -> Self {
376 self.per_epsilon = eps;
377 self
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_default_config_validates() {
387 let cfg = DQNConfig::default();
388 assert!(cfg.validate().is_ok());
389 assert_eq!(cfg.learning_rate, 1e-3);
390 assert_eq!(cfg.batch_size, 64);
391 assert_eq!(cfg.buffer_capacity, 50_000);
392 assert_eq!(cfg.min_buffer_size, 1_000);
393 assert_eq!(cfg.target_update_interval, 500);
394 assert_eq!(cfg.gamma, 0.99);
395 assert_eq!(cfg.epsilon_start, 1.0);
396 assert_eq!(cfg.epsilon_end, 0.05);
397 assert_eq!(cfg.epsilon_decay_steps, 10_000);
398 assert_eq!(cfg.max_grad_norm, 10.0);
399 }
400
401 #[test]
402 fn test_builder() {
403 let cfg = DQNConfig::new()
404 .learning_rate(5e-4)
405 .batch_size(128)
406 .buffer_capacity(20_000)
407 .min_buffer_size(500)
408 .target_update_interval(250)
409 .gamma(0.95)
410 .epsilon_start(0.5)
411 .epsilon_end(0.01)
412 .epsilon_decay_steps(5_000)
413 .max_grad_norm(5.0);
414 assert!(cfg.validate().is_ok());
415 assert_eq!(cfg.learning_rate, 5e-4);
416 assert_eq!(cfg.batch_size, 128);
417 assert_eq!(cfg.buffer_capacity, 20_000);
418 assert_eq!(cfg.min_buffer_size, 500);
419 assert_eq!(cfg.target_update_interval, 250);
420 assert_eq!(cfg.gamma, 0.95);
421 assert_eq!(cfg.epsilon_start, 0.5);
422 assert_eq!(cfg.epsilon_end, 0.01);
423 assert_eq!(cfg.epsilon_decay_steps, 5_000);
424 assert_eq!(cfg.max_grad_norm, 5.0);
425 }
426
427 #[test]
428 fn test_validate_rejects_negative_lr() {
429 let cfg = DQNConfig::new().learning_rate(-1.0);
430 assert!(cfg.validate().is_err());
431 }
432
433 #[test]
434 fn test_validate_rejects_zero_batch() {
435 let cfg = DQNConfig::new().batch_size(0);
436 assert!(cfg.validate().is_err());
437 }
438
439 #[test]
440 fn test_validate_rejects_gamma_out_of_range() {
441 assert!(DQNConfig::new().gamma(-0.1).validate().is_err());
442 assert!(DQNConfig::new().gamma(1.5).validate().is_err());
443 assert!(DQNConfig::new().gamma(0.0).validate().is_ok());
444 assert!(DQNConfig::new().gamma(1.0).validate().is_ok());
445 }
446
447 #[test]
448 fn test_validate_rejects_epsilon_end_above_start() {
449 let cfg = DQNConfig::new().epsilon_start(0.1).epsilon_end(0.5);
450 assert!(cfg.validate().is_err());
451 }
452
453 #[test]
454 fn test_validate_rejects_zero_target_update() {
455 let cfg = DQNConfig::new().target_update_interval(0);
456 assert!(cfg.validate().is_err());
457 }
458
459 #[test]
460 fn test_validate_rejects_zero_epsilon_decay() {
461 let cfg = DQNConfig::new().epsilon_decay_steps(0);
462 assert!(cfg.validate().is_err());
463 }
464
465 #[test]
466 fn test_validate_rejects_capacity_below_batch() {
467 let cfg = DQNConfig::new().buffer_capacity(32).batch_size(64);
468 assert!(cfg.validate().is_err());
469 }
470
471 #[test]
472 fn test_validate_rejects_min_buffer_above_capacity() {
473 let cfg = DQNConfig::new().buffer_capacity(100).min_buffer_size(1000);
474 assert!(cfg.validate().is_err());
475 }
476
477 #[test]
478 fn test_validate_rejects_zero_max_grad_norm() {
479 let cfg = DQNConfig::new().max_grad_norm(0.0);
480 assert!(cfg.validate().is_err());
481 }
482
483 #[test]
484 fn test_default_soft_update_tau_is_none() {
485 let cfg = DQNConfig::default();
486 assert!(cfg.soft_update_tau.is_none());
487 }
488
489 #[test]
490 fn test_soft_update_tau_builder() {
491 let cfg = DQNConfig::new().soft_update_tau(0.005);
492 assert_eq!(cfg.soft_update_tau, Some(0.005));
493 assert!(cfg.validate().is_ok());
494 }
495
496 #[test]
497 fn test_validate_rejects_soft_update_tau_out_of_range() {
498 assert!(DQNConfig::new().soft_update_tau(0.0).validate().is_err());
499 assert!(DQNConfig::new().soft_update_tau(-0.1).validate().is_err());
500 assert!(DQNConfig::new().soft_update_tau(1.5).validate().is_err());
501 assert!(DQNConfig::new().soft_update_tau(1.0).validate().is_ok());
502 assert!(DQNConfig::new().soft_update_tau(0.005).validate().is_ok());
503 }
504
505 #[test]
506 fn test_default_per_fields() {
507 let cfg = DQNConfig::default();
508 assert!(!cfg.prioritized_replay);
509 assert!((cfg.per_alpha - 0.6).abs() < 1e-9);
510 assert!((cfg.per_beta_start - 0.4).abs() < 1e-9);
511 assert!((cfg.per_beta_end - 1.0).abs() < 1e-9);
512 assert_eq!(cfg.per_beta_steps, 0);
513 assert!((cfg.per_epsilon - 1e-6).abs() < 1e-12);
514 }
515
516 #[test]
517 fn test_per_builder_setters() {
518 let cfg = DQNConfig::new()
519 .prioritized_replay(true)
520 .per_alpha(0.7)
521 .per_beta_start(0.3)
522 .per_beta_end(1.0)
523 .per_beta_steps(20_000)
524 .per_epsilon(1e-5);
525 assert!(cfg.prioritized_replay);
526 assert!((cfg.per_alpha - 0.7).abs() < 1e-9);
527 assert!((cfg.per_beta_start - 0.3).abs() < 1e-9);
528 assert!((cfg.per_beta_end - 1.0).abs() < 1e-9);
529 assert_eq!(cfg.per_beta_steps, 20_000);
530 assert!((cfg.per_epsilon - 1e-5).abs() < 1e-12);
531 assert!(cfg.validate().is_ok());
532 }
533
534 #[test]
535 fn test_validate_rejects_per_alpha_out_of_range() {
536 assert!(DQNConfig::new().per_alpha(-0.1).validate().is_err());
537 assert!(DQNConfig::new().per_alpha(1.5).validate().is_err());
538 assert!(DQNConfig::new().per_alpha(0.0).validate().is_ok());
539 assert!(DQNConfig::new().per_alpha(1.0).validate().is_ok());
540 }
541
542 #[test]
543 fn test_validate_rejects_per_beta_out_of_range() {
544 assert!(DQNConfig::new().per_beta_start(-0.1).validate().is_err());
545 assert!(DQNConfig::new().per_beta_end(1.5).validate().is_err());
546 }
547
548 #[test]
549 fn test_validate_rejects_per_beta_start_above_end() {
550 let cfg = DQNConfig::new().per_beta_start(0.9).per_beta_end(0.5);
551 assert!(cfg.validate().is_err());
552 }
553
554 #[test]
555 fn test_validate_rejects_negative_per_epsilon() {
556 let cfg = DQNConfig::new().per_epsilon(-1e-6);
557 assert!(cfg.validate().is_err());
558 }
559
560 #[test]
561 fn test_beta_schedule_linear() {
562 let cfg = DQNConfig::new().per_beta_start(0.4).per_beta_end(1.0).per_beta_steps(1000);
563 assert!((cfg.beta_at(0) - 0.4).abs() < 1e-9);
564 assert!((cfg.beta_at(500) - 0.7).abs() < 1e-9);
565 assert!((cfg.beta_at(1000) - 1.0).abs() < 1e-9);
566 assert!((cfg.beta_at(10_000) - 1.0).abs() < 1e-9);
568 }
569
570 #[test]
571 fn test_beta_schedule_falls_back_to_epsilon_decay() {
572 let cfg = DQNConfig::new()
574 .per_beta_start(0.4)
575 .per_beta_end(1.0)
576 .per_beta_steps(0)
577 .epsilon_decay_steps(2_000);
578 assert!((cfg.beta_at(1_000) - 0.7).abs() < 1e-9);
579 assert!((cfg.beta_at(2_000) - 1.0).abs() < 1e-9);
580 }
581
582 #[test]
583 fn test_epsilon_schedule_linear() {
584 let cfg = DQNConfig::new().epsilon_start(1.0).epsilon_end(0.1).epsilon_decay_steps(1000);
585
586 assert!((cfg.epsilon_at(0) - 1.0).abs() < 1e-9);
587 assert!((cfg.epsilon_at(500) - 0.55).abs() < 1e-6);
589 assert!((cfg.epsilon_at(1000) - 0.1).abs() < 1e-9);
591 assert!((cfg.epsilon_at(10_000) - 0.1).abs() < 1e-9);
593 }
594}