use anyhow::{Result, anyhow};
#[derive(Debug, Clone)]
pub struct SacConfig {
pub actor_lr: f64,
pub critic_lr: f64,
pub alpha_lr: f64,
pub gamma: f64,
pub tau: f64,
pub batch_size: usize,
pub buffer_capacity: usize,
pub min_buffer_size: usize,
pub learning_starts: usize,
pub gradient_steps_per_env_step: usize,
pub hidden_dim: usize,
pub num_hidden_layers: usize,
pub auto_alpha: bool,
pub init_alpha: f32,
pub target_entropy: Option<f32>,
pub max_grad_norm: Option<f64>,
pub seed: u64,
}
impl Default for SacConfig {
fn default() -> Self {
Self {
actor_lr: 3e-4,
critic_lr: 3e-4,
alpha_lr: 3e-4,
gamma: 0.99,
tau: 0.005,
batch_size: 256,
buffer_capacity: 1_000_000,
min_buffer_size: 1_000,
learning_starts: 1_000,
gradient_steps_per_env_step: 1,
hidden_dim: 256,
num_hidden_layers: 2,
auto_alpha: true,
init_alpha: 0.2,
target_entropy: None,
max_grad_norm: None,
seed: 0,
}
}
}
impl SacConfig {
pub fn new() -> Self {
Self::default()
}
pub fn validate(&self) -> Result<()> {
if self.actor_lr <= 0.0 {
return Err(anyhow!("actor_lr must be positive, got {}", self.actor_lr));
}
if self.critic_lr <= 0.0 {
return Err(anyhow!("critic_lr must be positive, got {}", self.critic_lr));
}
if self.alpha_lr <= 0.0 {
return Err(anyhow!("alpha_lr must be positive, got {}", self.alpha_lr));
}
if !(self.tau > 0.0 && self.tau <= 1.0) {
return Err(anyhow!("tau must be in (0, 1], got {}", self.tau));
}
if !(0.0..=1.0).contains(&self.gamma) {
return Err(anyhow!("gamma must be in [0, 1], got {}", self.gamma));
}
if self.batch_size == 0 {
return Err(anyhow!("batch_size must be positive"));
}
if self.buffer_capacity < self.batch_size {
return Err(anyhow!(
"buffer_capacity ({}) must be at least batch_size ({})",
self.buffer_capacity,
self.batch_size
));
}
if self.min_buffer_size > self.buffer_capacity {
return Err(anyhow!(
"min_buffer_size ({}) must be <= buffer_capacity ({})",
self.min_buffer_size,
self.buffer_capacity
));
}
if self.init_alpha <= 0.0 {
return Err(anyhow!("init_alpha must be positive, got {}", self.init_alpha));
}
if self.gradient_steps_per_env_step == 0 {
return Err(anyhow!("gradient_steps_per_env_step must be positive"));
}
if self.hidden_dim == 0 {
return Err(anyhow!("hidden_dim must be positive"));
}
if self.num_hidden_layers == 0 {
return Err(anyhow!("num_hidden_layers must be positive"));
}
if let Some(clip) = self.max_grad_norm
&& clip <= 0.0
{
return Err(anyhow!("max_grad_norm must be positive when set, got {}", clip));
}
if let Some(te) = self.target_entropy
&& !te.is_finite()
{
return Err(anyhow!("target_entropy must be finite when set, got {}", te));
}
Ok(())
}
pub fn resolved_target_entropy(&self, action_dim: usize) -> f32 {
self.target_entropy.unwrap_or(-(action_dim as f32))
}
pub fn actor_lr(mut self, lr: f64) -> Self {
self.actor_lr = lr;
self
}
pub fn critic_lr(mut self, lr: f64) -> Self {
self.critic_lr = lr;
self
}
pub fn alpha_lr(mut self, lr: f64) -> Self {
self.alpha_lr = lr;
self
}
pub fn gamma(mut self, gamma: f64) -> Self {
self.gamma = gamma;
self
}
pub fn tau(mut self, tau: f64) -> Self {
self.tau = tau;
self
}
pub fn batch_size(mut self, size: usize) -> Self {
self.batch_size = size;
self
}
pub fn buffer_capacity(mut self, capacity: usize) -> Self {
self.buffer_capacity = capacity;
self
}
pub fn min_buffer_size(mut self, size: usize) -> Self {
self.min_buffer_size = size;
self
}
pub fn learning_starts(mut self, steps: usize) -> Self {
self.learning_starts = steps;
self
}
pub fn gradient_steps_per_env_step(mut self, steps: usize) -> Self {
self.gradient_steps_per_env_step = steps;
self
}
pub fn hidden_dim(mut self, dim: usize) -> Self {
self.hidden_dim = dim;
self
}
pub fn num_hidden_layers(mut self, layers: usize) -> Self {
self.num_hidden_layers = layers;
self
}
pub fn auto_alpha(mut self, enabled: bool) -> Self {
self.auto_alpha = enabled;
self
}
pub fn init_alpha(mut self, alpha: f32) -> Self {
self.init_alpha = alpha;
self
}
pub fn target_entropy(mut self, entropy: f32) -> Self {
self.target_entropy = Some(entropy);
self
}
pub fn max_grad_norm(mut self, norm: f64) -> Self {
self.max_grad_norm = Some(norm);
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config_validates() {
let cfg = SacConfig::default();
assert!(cfg.validate().is_ok());
assert_eq!(cfg.actor_lr, 3e-4);
assert_eq!(cfg.critic_lr, 3e-4);
assert_eq!(cfg.alpha_lr, 3e-4);
assert_eq!(cfg.gamma, 0.99);
assert_eq!(cfg.tau, 0.005);
assert_eq!(cfg.batch_size, 256);
assert_eq!(cfg.buffer_capacity, 1_000_000);
assert_eq!(cfg.min_buffer_size, 1_000);
assert_eq!(cfg.learning_starts, 1_000);
assert_eq!(cfg.gradient_steps_per_env_step, 1);
assert_eq!(cfg.hidden_dim, 256);
assert_eq!(cfg.num_hidden_layers, 2);
assert!(cfg.auto_alpha);
assert_eq!(cfg.init_alpha, 0.2);
assert!(cfg.target_entropy.is_none());
assert!(cfg.max_grad_norm.is_none());
assert_eq!(cfg.seed, 0);
}
#[test]
fn test_builder() {
let cfg = SacConfig::new()
.actor_lr(1e-3)
.critic_lr(2e-3)
.alpha_lr(5e-4)
.gamma(0.95)
.tau(0.01)
.batch_size(128)
.buffer_capacity(50_000)
.min_buffer_size(500)
.learning_starts(2_000)
.gradient_steps_per_env_step(2)
.hidden_dim(64)
.num_hidden_layers(3)
.auto_alpha(false)
.init_alpha(0.1)
.target_entropy(-2.0)
.max_grad_norm(5.0)
.seed(42);
assert!(cfg.validate().is_ok());
assert_eq!(cfg.actor_lr, 1e-3);
assert_eq!(cfg.critic_lr, 2e-3);
assert_eq!(cfg.alpha_lr, 5e-4);
assert_eq!(cfg.gamma, 0.95);
assert_eq!(cfg.tau, 0.01);
assert_eq!(cfg.batch_size, 128);
assert_eq!(cfg.buffer_capacity, 50_000);
assert_eq!(cfg.min_buffer_size, 500);
assert_eq!(cfg.learning_starts, 2_000);
assert_eq!(cfg.gradient_steps_per_env_step, 2);
assert_eq!(cfg.hidden_dim, 64);
assert_eq!(cfg.num_hidden_layers, 3);
assert!(!cfg.auto_alpha);
assert_eq!(cfg.init_alpha, 0.1);
assert_eq!(cfg.target_entropy, Some(-2.0));
assert_eq!(cfg.max_grad_norm, Some(5.0));
assert_eq!(cfg.seed, 42);
}
#[test]
fn test_resolved_target_entropy() {
let cfg = SacConfig::default();
assert_eq!(cfg.resolved_target_entropy(1), -1.0);
assert_eq!(cfg.resolved_target_entropy(3), -3.0);
let cfg = cfg.target_entropy(-7.5);
assert_eq!(cfg.resolved_target_entropy(3), -7.5);
}
#[test]
fn test_validate_rejects_non_positive_lrs() {
assert!(SacConfig::new().actor_lr(0.0).validate().is_err());
assert!(SacConfig::new().actor_lr(-1.0).validate().is_err());
assert!(SacConfig::new().critic_lr(0.0).validate().is_err());
assert!(SacConfig::new().alpha_lr(-1e-4).validate().is_err());
}
#[test]
fn test_validate_rejects_tau_out_of_range() {
assert!(SacConfig::new().tau(0.0).validate().is_err());
assert!(SacConfig::new().tau(-0.1).validate().is_err());
assert!(SacConfig::new().tau(1.5).validate().is_err());
assert!(SacConfig::new().tau(1.0).validate().is_ok());
assert!(SacConfig::new().tau(0.005).validate().is_ok());
}
#[test]
fn test_validate_rejects_gamma_out_of_range() {
assert!(SacConfig::new().gamma(-0.1).validate().is_err());
assert!(SacConfig::new().gamma(1.5).validate().is_err());
assert!(SacConfig::new().gamma(0.0).validate().is_ok());
assert!(SacConfig::new().gamma(1.0).validate().is_ok());
}
#[test]
fn test_validate_rejects_zero_batch() {
assert!(SacConfig::new().batch_size(0).validate().is_err());
}
#[test]
fn test_validate_rejects_capacity_below_batch() {
let cfg = SacConfig::new().buffer_capacity(64).batch_size(256);
assert!(cfg.validate().is_err());
}
#[test]
fn test_validate_rejects_min_buffer_above_capacity() {
let cfg = SacConfig::new().buffer_capacity(1_000).min_buffer_size(5_000);
assert!(cfg.validate().is_err());
}
#[test]
fn test_validate_rejects_non_positive_init_alpha() {
assert!(SacConfig::new().init_alpha(0.0).validate().is_err());
assert!(SacConfig::new().init_alpha(-0.2).validate().is_err());
assert!(SacConfig::new().init_alpha(0.2).validate().is_ok());
}
#[test]
fn test_validate_rejects_zero_gradient_steps() {
assert!(SacConfig::new().gradient_steps_per_env_step(0).validate().is_err());
}
#[test]
fn test_validate_rejects_zero_hidden_and_layers() {
assert!(SacConfig::new().hidden_dim(0).validate().is_err());
assert!(SacConfig::new().num_hidden_layers(0).validate().is_err());
}
#[test]
fn test_validate_rejects_non_positive_grad_clip() {
assert!(SacConfig::new().max_grad_norm(0.0).validate().is_err());
assert!(SacConfig::new().max_grad_norm(-1.0).validate().is_err());
assert!(SacConfig::new().max_grad_norm(10.0).validate().is_ok());
}
}