use std::io::Write;
use anyhow::Result;
use burn::backend::{Autodiff, NdArray};
use thrust_rl::{
env::{Environment, games::pendulum::PendulumSwingUp},
train::sac::{SacConfig, SacTrainer},
};
type Backend = Autodiff<NdArray<f32>>;
const BACKEND_LABEL: &str = "NdArray<f32> + Autodiff (CPU)";
const OBS_DIM: usize = 3;
const ACTION_DIM: usize = 1;
const MAX_TORQUE: f32 = 2.0;
const DEFAULT_TIMESTEPS: usize = 30_000;
const BUFFER_CAPACITY: usize = 50_000;
const MIN_BUFFER_SIZE: usize = 1_000;
const LEARNING_STARTS: usize = 1_000;
const BATCH_SIZE: usize = 256;
const HIDDEN_DIM: usize = 256;
const NUM_HIDDEN_LAYERS: usize = 2;
const SEED: u64 = 0;
const LOG_INTERVAL: usize = 1_000;
fn scale_action(action: &[f32]) -> Vec<f32> {
action.iter().map(|a| a * MAX_TORQUE).collect()
}
fn main() -> Result<()> {
tracing_subscriber::fmt().with_env_filter("info").init();
let total_timesteps: usize = std::env::var("TOTAL_TIMESTEPS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_TIMESTEPS);
tracing::info!("Starting Pendulum SAC Training (Burn backend: {})", BACKEND_LABEL);
tracing::info!("Environment: PendulumSwingUp (continuous)");
tracing::info!(" obs_dim = {}", OBS_DIM);
tracing::info!(" action_dim = {}", ACTION_DIM);
tracing::info!(" max_torque = {}", MAX_TORQUE);
tracing::info!(" total_timesteps = {}", total_timesteps);
tracing::info!(
" batch_size = {} hidden_dim = {} layers = {}",
BATCH_SIZE,
HIDDEN_DIM,
NUM_HIDDEN_LAYERS
);
let training_start = std::time::Instant::now();
let device = Default::default();
let mut curve_csv = open_curve_csv()?;
let config = SacConfig::new()
.buffer_capacity(BUFFER_CAPACITY)
.min_buffer_size(MIN_BUFFER_SIZE)
.learning_starts(LEARNING_STARTS)
.batch_size(BATCH_SIZE)
.hidden_dim(HIDDEN_DIM)
.num_hidden_layers(NUM_HIDDEN_LAYERS)
.seed(SEED);
let mut trainer = SacTrainer::<Backend>::new(config, OBS_DIM, ACTION_DIM, device)?;
tracing::info!("------------------------------------------------------------");
let mut env = PendulumSwingUp::with_seed(SEED);
env.reset();
let mut obs = env.get_observation();
let mut current_return = 0.0_f32;
let mut completed_returns: Vec<f32> = Vec::new();
let mut mean_return = 0.0_f32;
let mut last_alpha = 0.0_f64;
let mut last_buffer_len = 0_usize;
let mut next_log = LOG_INTERVAL;
for step in 1..=total_timesteps {
let action = trainer.select_action(&obs);
let result = env.step(scale_action(&action));
let done = result.terminated || result.truncated;
trainer
.buffer_mut()
.push(&obs, &action, result.reward, &result.observation, done);
trainer.increment_env_step();
current_return += result.reward;
if let Some(stats) = trainer.train()? {
last_alpha = stats.alpha;
last_buffer_len = stats.buffer_len;
}
if done {
completed_returns.push(current_return);
current_return = 0.0;
trainer.increment_episodes(1);
env.reset();
obs = env.get_observation();
} else {
obs = result.observation;
}
if step >= next_log {
if !completed_returns.is_empty() {
let n = completed_returns.len();
let recent = &completed_returns[n.saturating_sub(100)..];
mean_return = recent.iter().sum::<f32>() / recent.len() as f32;
}
if let Some(w) = curve_csv.as_mut() {
writeln!(w, "{},{:.4}", trainer.total_env_steps(), mean_return)?;
}
tracing::info!(
"env_steps={:>7}/{} episodes={:>5} mean_return(last≤100)={:9.1} alpha={:6.4} buffer={:>6}",
trainer.total_env_steps(),
total_timesteps,
trainer.total_episodes(),
mean_return,
last_alpha,
last_buffer_len,
);
next_log += LOG_INTERVAL;
}
}
if let Some(mut w) = curve_csv.take() {
w.flush()?;
}
let training_duration = training_start.elapsed();
tracing::info!("------------------------------------------------------------");
tracing::info!("Training complete.");
tracing::info!(" total env steps : {}", trainer.total_env_steps());
tracing::info!(" total episodes : {}", trainer.total_episodes());
tracing::info!(" total train steps: {}", trainer.total_train_steps());
tracing::info!(" final mean return(last≤100): {:.1}", mean_return);
tracing::info!(" training time : {:.1}s", training_duration.as_secs_f64());
tracing::info!(
" steps/sec : {:.0}",
total_timesteps as f64 / training_duration.as_secs_f64()
);
Ok(())
}
fn open_curve_csv() -> Result<Option<std::io::BufWriter<std::fs::File>>> {
match std::env::var("CURVE_CSV") {
Ok(path) if !path.is_empty() => {
let file = std::fs::File::create(&path)?;
let mut w = std::io::BufWriter::new(file);
writeln!(w, "env_steps,mean_episode_reward")?;
tracing::info!("Writing learning-curve CSV to {}", path);
Ok(Some(w))
}
_ => Ok(None),
}
}