zernel/experiments/
compare.rs

1// Copyright (C) 2026 Dyber, Inc. — Proprietary
2
3use super::store::Experiment;
4use std::collections::BTreeSet;
5
6/// Compare two experiments and produce a human-readable diff.
7pub fn compare(a: &Experiment, b: &Experiment) -> String {
8    let mut out = String::new();
9
10    out.push_str(&format!("Comparing: {} vs {}\n", a.id, b.id));
11    out.push_str(&format!("          {} vs {}\n\n", a.name, b.name));
12
13    // Hyperparameter diff
14    let all_keys: BTreeSet<_> = a
15        .hyperparams
16        .keys()
17        .chain(b.hyperparams.keys())
18        .cloned()
19        .collect();
20
21    if !all_keys.is_empty() {
22        out.push_str("Hyperparameters:\n");
23        for key in &all_keys {
24            let va = a
25                .hyperparams
26                .get(key)
27                .map(|v| v.to_string())
28                .unwrap_or_default();
29            let vb = b
30                .hyperparams
31                .get(key)
32                .map(|v| v.to_string())
33                .unwrap_or_default();
34            if va != vb {
35                out.push_str(&format!("  {key}: {va} -> {vb}\n"));
36            }
37        }
38        out.push('\n');
39    }
40
41    // Metrics diff
42    let all_metrics: BTreeSet<_> = a.metrics.keys().chain(b.metrics.keys()).cloned().collect();
43
44    if !all_metrics.is_empty() {
45        out.push_str("Metrics:\n");
46        for key in &all_metrics {
47            let va = a.metrics.get(key).copied();
48            let vb = b.metrics.get(key).copied();
49            match (va, vb) {
50                (Some(a_val), Some(b_val)) => {
51                    let pct = if a_val != 0.0 {
52                        ((b_val - a_val) / a_val) * 100.0
53                    } else {
54                        0.0
55                    };
56                    let arrow = if pct > 0.0 { "+" } else { "" };
57                    out.push_str(&format!(
58                        "  {key}: {a_val:.4} -> {b_val:.4} ({arrow}{pct:.1}%)\n"
59                    ));
60                }
61                (Some(v), None) => out.push_str(&format!("  {key}: {v:.4} -> (missing)\n")),
62                (None, Some(v)) => out.push_str(&format!("  {key}: (missing) -> {v:.4}\n")),
63                (None, None) => {}
64            }
65        }
66    }
67
68    out
69}