use crate::train::EpochMetrics;
pub struct TrainingReport {
pub epochs: Vec<EpochMetrics>,
pub total_tokens: usize,
}
impl TrainingReport {
pub fn new() -> Self {
Self {
epochs: Vec::new(),
total_tokens: 0,
}
}
pub fn record(&mut self, m: EpochMetrics) {
self.epochs.push(m);
}
pub fn throughput(&self) -> f32 {
if self.epochs.is_empty() {
return 0.0;
}
let total_ms: f32 = self.epochs.iter().map(|e| e.elapsed_ms).sum();
if total_ms < 1e-6 {
return 0.0;
}
(self.total_tokens as f32 * self.epochs.len() as f32) / (total_ms / 1000.0)
}
pub fn print_summary(&self) {
if self.epochs.is_empty() {
return;
}
let first = &self.epochs[0];
let last = self.epochs.last().unwrap();
println!(
"Training: {} epochs, {} params",
self.epochs.len(),
first.params_trained
);
println!(
"Loss: {:.4} → {:.4} (Δ={:+.4})",
first.loss,
last.loss,
last.loss - first.loss
);
println!("F: {:.4} → {:.4}", first.free_energy, last.free_energy);
println!("|∇|: {:.6} → {:.6}", first.grad_norm, last.grad_norm);
println!("LR: {:.5} → {:.5}", first.learning_rate, last.learning_rate);
println!("Time: {:.0}ms/epoch avg", self.avg_epoch_ms());
println!("Throughput: {:.0} tokens/s", self.throughput());
}
pub fn export_csv(&self, path: &std::path::Path) -> std::io::Result<()> {
use std::io::Write;
let mut f = std::fs::File::create(path)?;
writeln!(f, "epoch,loss,free_energy,grad_norm,learning_rate,elapsed_ms,params")?;
for e in &self.epochs {
writeln!(
f,
"{},{:.6},{:.6},{:.8},{:.6},{:.1},{}",
e.epoch, e.loss, e.free_energy, e.grad_norm, e.learning_rate, e.elapsed_ms, e.params_trained
)?;
}
Ok(())
}
fn avg_epoch_ms(&self) -> f32 {
if self.epochs.is_empty() {
return 0.0;
}
self.epochs.iter().map(|e| e.elapsed_ms).sum::<f32>() / self.epochs.len() as f32
}
}
impl Default for TrainingReport {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn report_empty_is_safe() {
let r = TrainingReport::new();
assert_eq!(r.throughput(), 0.0);
r.print_summary(); }
#[test]
fn csv_export() {
let mut r = TrainingReport::new();
r.record(EpochMetrics {
epoch: 0,
loss: 3.5,
free_energy: -0.5,
grad_norm: 0.01,
elapsed_ms: 100.0,
learning_rate: 0.03,
params_trained: 544,
});
let dir = std::env::temp_dir();
let path = dir.join("qgpt_test_metrics.csv");
r.export_csv(&path).expect("CSV export failed");
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("epoch,loss"), "CSV should have header");
assert!(content.contains("0,3.5"), "CSV should have data");
let _ = std::fs::remove_file(&path);
}
}