dreamwell-intelligence 1.0.0

QuantumGPT (The Loom) — Quantum Information Pretrained Transformer. Density matrix attention with intrinsic thermodynamic loss, φ-scaled causal dephasing, and parameter shift gradient.
Documentation
// Training metrics — collection, reporting, and CSV export.
//
// Clean Compute: all metrics are scalar. No heap allocation per-epoch.

use crate::train::EpochMetrics;

/// Accumulated training metrics.
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);
    }

    /// Average throughput: tokens processed per second.
    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)
    }

    /// Print summary table.
    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());
    }

    /// Export to CSV.
    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(); // should not panic
    }

    #[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);
    }
}