Skip to main content

trueno_db/experiment/
store.rs

1//! Experiment Store - in-memory storage for experiment tracking data
2//!
3//! This module provides the storage layer for experiment tracking,
4//! optimized for time-series metric queries.
5
6use std::collections::HashMap;
7
8use super::{ExperimentRecord, MetricRecord, RunRecord};
9
10/// In-memory store for experiment tracking data.
11///
12/// ## Design
13///
14/// The store uses hash maps for O(1) lookups by ID, and stores metrics
15/// in a vector that can be filtered and sorted for time-series queries.
16///
17/// ## Time-Series Optimization
18///
19/// The `get_metrics_for_run` function returns metrics ordered by step,
20/// enabling efficient time-series visualization and analysis.
21#[derive(Debug, Default)]
22pub struct ExperimentStore {
23    experiments: HashMap<String, ExperimentRecord>,
24    runs: HashMap<String, RunRecord>,
25    metrics: Vec<MetricRecord>,
26}
27
28impl ExperimentStore {
29    /// Create a new empty experiment store.
30    #[must_use]
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Check if the store is empty (no experiments, runs, or metrics).
36    #[must_use]
37    pub fn is_empty(&self) -> bool {
38        self.experiments.is_empty() && self.runs.is_empty() && self.metrics.is_empty()
39    }
40
41    /// Get the number of experiments in the store.
42    #[must_use]
43    pub fn experiment_count(&self) -> usize {
44        self.experiments.len()
45    }
46
47    /// Get the number of runs in the store.
48    #[must_use]
49    pub fn run_count(&self) -> usize {
50        self.runs.len()
51    }
52
53    /// Get the number of metrics in the store.
54    #[must_use]
55    pub fn metric_count(&self) -> usize {
56        self.metrics.len()
57    }
58
59    /// Add an experiment to the store.
60    pub fn add_experiment(&mut self, experiment: ExperimentRecord) {
61        self.experiments.insert(experiment.experiment_id().to_string(), experiment);
62    }
63
64    /// Get an experiment by ID.
65    #[must_use]
66    pub fn get_experiment(&self, experiment_id: &str) -> Option<&ExperimentRecord> {
67        self.experiments.get(experiment_id)
68    }
69
70    /// Add a run to the store.
71    pub fn add_run(&mut self, run: RunRecord) {
72        self.runs.insert(run.run_id().to_string(), run);
73    }
74
75    /// Get a run by ID.
76    #[must_use]
77    pub fn get_run(&self, run_id: &str) -> Option<&RunRecord> {
78        self.runs.get(run_id)
79    }
80
81    /// Get all runs for an experiment.
82    #[must_use]
83    pub fn get_runs_for_experiment(&self, experiment_id: &str) -> Vec<&RunRecord> {
84        self.runs.values().filter(|run| run.experiment_id() == experiment_id).collect()
85    }
86
87    /// Add a metric to the store.
88    pub fn add_metric(&mut self, metric: MetricRecord) {
89        self.metrics.push(metric);
90    }
91
92    /// Get metrics for a specific run and key, ordered by step.
93    ///
94    /// This is the primary query function for time-series metric data.
95    ///
96    /// ## Arguments
97    ///
98    /// * `run_id` - The ID of the run to query
99    /// * `key` - The metric key/name to filter by
100    ///
101    /// ## Returns
102    ///
103    /// A vector of metrics matching the `run_id` and key, sorted by step
104    /// in ascending order.
105    ///
106    /// ## Example
107    ///
108    /// ```rust
109    /// use trueno_db::experiment::{ExperimentStore, MetricRecord};
110    ///
111    /// let mut store = ExperimentStore::new();
112    ///
113    /// // Log some training metrics
114    /// for step in 0..100 {
115    ///     let loss = 1.0 / (step as f64 + 1.0);
116    ///     store.add_metric(MetricRecord::new("run-001", "loss", step, loss));
117    /// }
118    ///
119    /// // Query the loss curve
120    /// let loss_metrics = store.get_metrics_for_run("run-001", "loss");
121    /// assert_eq!(loss_metrics.len(), 100);
122    /// ```
123    #[must_use]
124    pub fn get_metrics_for_run(&self, run_id: &str, key: &str) -> Vec<MetricRecord> {
125        let mut metrics: Vec<MetricRecord> = self
126            .metrics
127            .iter()
128            .filter(|m| m.run_id() == run_id && m.key() == key)
129            .cloned()
130            .collect();
131
132        // Sort by step for time-series ordering
133        metrics.sort_by_key(MetricRecord::step);
134
135        metrics
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_store_default() {
145        let store = ExperimentStore::new();
146        assert!(store.is_empty());
147        assert_eq!(store.experiment_count(), 0);
148        assert_eq!(store.run_count(), 0);
149        assert_eq!(store.metric_count(), 0);
150    }
151
152    #[test]
153    fn test_store_add_and_get() {
154        let mut store = ExperimentStore::new();
155
156        let experiment = ExperimentRecord::new("exp-1", "Test");
157        store.add_experiment(experiment);
158
159        let run = RunRecord::new("run-1", "exp-1");
160        store.add_run(run);
161
162        let metric = MetricRecord::new("run-1", "loss", 0, 0.5);
163        store.add_metric(metric);
164
165        assert!(!store.is_empty());
166        assert!(store.get_experiment("exp-1").is_some());
167        assert!(store.get_run("run-1").is_some());
168    }
169
170    #[test]
171    fn test_get_metrics_for_run_ordering() {
172        let mut store = ExperimentStore::new();
173
174        // Add out of order
175        store.add_metric(MetricRecord::new("run-1", "loss", 2, 0.2));
176        store.add_metric(MetricRecord::new("run-1", "loss", 0, 0.0));
177        store.add_metric(MetricRecord::new("run-1", "loss", 1, 0.1));
178
179        let metrics = store.get_metrics_for_run("run-1", "loss");
180
181        assert_eq!(metrics.len(), 3);
182        assert_eq!(metrics[0].step(), 0);
183        assert_eq!(metrics[1].step(), 1);
184        assert_eq!(metrics[2].step(), 2);
185    }
186}