1use crate::error::{OptimError, Result};
7use crate::unified_api::OptimizerConfig;
8use chrono::{DateTime, Utc};
9use scirs2_core::ndarray::Array2;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Experiment {
16 pub id: String,
18 pub name: String,
20 pub hypothesis: String,
22 pub description: String,
24 pub status: ExperimentStatus,
26 pub config: ExperimentConfig,
28 pub optimizer_configs: HashMap<String, OptimizerConfig<f64>>,
30 pub dataset_info: DatasetInfo,
32 pub metrics: Vec<String>,
34 pub results: Vec<ExperimentResult>,
36 pub reproducibility: ReproducibilityInfo,
38 pub timeline: ExperimentTimeline,
40 pub notes: Vec<ExperimentNote>,
42 pub metadata: ExperimentMetadata,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
48pub enum ExperimentStatus {
49 Planning,
51 Ready,
53 Running,
55 Completed,
57 Failed,
59 Paused,
61 Cancelled,
63 Analyzing,
65 Published,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ExperimentConfig {
72 pub random_seed: u64,
74 pub num_runs: usize,
76 pub max_epochs: usize,
78 pub early_stopping: Option<EarlyStoppingConfig>,
80 pub hardware_config: HardwareConfig,
82 pub environment: HashMap<String, String>,
84 pub validation_split: f64,
86 pub test_split: f64,
88 pub cv_folds: Option<usize>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct EarlyStoppingConfig {
95 pub monitor_metric: String,
97 pub patience: usize,
99 pub min_improvement: f64,
101 pub mode: OptimizationMode,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107pub enum OptimizationMode {
108 Minimize,
110 Maximize,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, Default)]
116pub struct HardwareConfig {
117 pub cpu_info: CpuInfo,
119 pub gpu_info: Option<GpuInfo>,
121 pub memory_config: MemoryConfig,
123 pub parallel_config: ParallelConfig,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct CpuInfo {
130 pub model: String,
132 pub cores: usize,
134 pub threads: usize,
136 pub frequency_mhz: u32,
138 pub cache_sizes: Vec<String>,
140 pub simd_capabilities: Vec<String>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct GpuInfo {
147 pub model: String,
149 pub memory_mb: usize,
151 pub compute_capability: String,
153 pub cuda_version: Option<String>,
155 pub driver_version: String,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct MemoryConfig {
162 pub total_memory_mb: usize,
164 pub available_memory_mb: usize,
166 pub allocation_strategy: MemoryAllocationStrategy,
168 pub pool_size_mb: Option<usize>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
174pub enum MemoryAllocationStrategy {
175 Standard,
177 Pooled,
179 MemoryMapped,
181 Compressed,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct ParallelConfig {
188 pub num_threads: usize,
190 pub thread_affinity: Option<Vec<usize>>,
192 pub work_stealing: bool,
194 pub chunk_size: Option<usize>,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct DatasetInfo {
201 pub name: String,
203 pub description: String,
205 pub source: String,
207 pub version: String,
209 pub num_samples: usize,
211 pub num_features: usize,
213 pub num_classes: Option<usize>,
215 pub data_type: DataType,
217 pub statistics: DatasetStatistics,
219 pub preprocessing: Vec<PreprocessingStep>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
225pub enum DataType {
226 Tabular,
228 Image,
230 Text,
232 Audio,
234 Video,
236 TimeSeries,
238 Graph,
240 MultiModal,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, Default)]
246pub struct DatasetStatistics {
247 pub feature_means: Vec<f64>,
249 pub feature_stds: Vec<f64>,
251 pub feature_ranges: Vec<(f64, f64)>,
253 pub class_distribution: Option<HashMap<String, usize>>,
255 pub missing_values: Vec<usize>,
257 pub correlation_matrix: Option<Array2<f64>>,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct PreprocessingStep {
264 pub name: String,
266 pub description: String,
268 pub parameters: HashMap<String, serde_json::Value>,
270 pub order: usize,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct ExperimentResult {
277 pub run_id: String,
279 pub optimizer_name: String,
281 pub start_time: DateTime<Utc>,
283 pub end_time: Option<DateTime<Utc>>,
285 pub status: RunStatus,
287 pub final_metrics: HashMap<String, f64>,
289 pub training_history: TrainingHistory,
291 pub resource_usage: ResourceUsage,
293 pub error_info: Option<String>,
295 pub metadata: HashMap<String, serde_json::Value>,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
301pub enum RunStatus {
302 Success,
304 Failed,
306 Terminated,
308 Timeout,
310 Cancelled,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct TrainingHistory {
317 pub epochs: Vec<usize>,
319 pub train_metrics: HashMap<String, Vec<f64>>,
321 pub val_metrics: HashMap<String, Vec<f64>>,
323 pub learning_rates: Vec<f64>,
325 pub gradient_norms: Vec<f64>,
327 pub parameter_norms: Vec<f64>,
329 pub step_times: Vec<f64>,
331}
332
333#[derive(Debug, Clone, Default, Serialize, Deserialize)]
335pub struct ResourceUsage {
336 pub peak_cpu_usage: f64,
338 pub avg_cpu_usage: f64,
340 pub peak_memory_mb: usize,
342 pub avg_memory_mb: usize,
344 pub peak_gpu_memory_mb: Option<usize>,
346 pub total_time_seconds: f64,
348 pub energy_consumption_joules: Option<f64>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize, Default)]
354pub struct ReproducibilityInfo {
355 pub environment_hash: String,
357 pub git_commit: Option<String>,
359 pub code_checksum: String,
361 pub dependency_versions: HashMap<String, String>,
363 pub system_info: SystemInfo,
365 pub checklist: ReproducibilityChecklist,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct SystemInfo {
372 pub os: String,
374 pub os_version: String,
376 pub architecture: String,
378 pub hostname: String,
380 pub username: String,
382 pub timezone: String,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, Default)]
388pub struct ReproducibilityChecklist {
389 pub random_seed_set: bool,
391 pub dependencies_pinned: bool,
393 pub data_version_controlled: bool,
395 pub code_version_controlled: bool,
397 pub environment_documented: bool,
399 pub hardware_documented: bool,
401 pub results_archived: bool,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct ExperimentTimeline {
408 pub created_at: DateTime<Utc>,
410 pub started_at: Option<DateTime<Utc>>,
412 pub completed_at: Option<DateTime<Utc>>,
414 pub estimated_duration: Option<chrono::Duration>,
416 pub actual_duration: Option<chrono::Duration>,
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct ExperimentNote {
423 pub timestamp: DateTime<Utc>,
425 pub author: String,
427 pub content: String,
429 pub note_type: NoteType,
431 pub run_id: Option<String>,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
437pub enum NoteType {
438 Observation,
440 Issue,
442 Solution,
444 Hypothesis,
446 Conclusion,
448 Question,
450 Reminder,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize, Default)]
456pub struct ExperimentMetadata {
457 pub tags: Vec<String>,
459 pub research_question: String,
461 pub expected_outcomes: Vec<String>,
463 pub success_criteria: Vec<String>,
465 pub related_experiments: Vec<String>,
467 pub references: Vec<String>,
469}
470
471pub struct ExperimentRunner {
473 experiment: Experiment,
475 resource_monitor: ResourceMonitor,
477 progress_callback: Option<Box<dyn Fn(f64) + Send + Sync>>,
479}
480
481impl std::fmt::Debug for ExperimentRunner {
482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 f.debug_struct("ExperimentRunner")
484 .field("experiment", &self.experiment)
485 .field("resource_monitor", &self.resource_monitor)
486 .field("progress_callback", &self.progress_callback.is_some())
487 .finish()
488 }
489}
490
491#[derive(Debug)]
493pub struct ResourceMonitor {
494 cpu_usage: Vec<f64>,
496 memory_usage: Vec<usize>,
498 gpu_memory_usage: Vec<Option<usize>>,
500 interval_seconds: u64,
502}
503
504impl Experiment {
505 pub fn new(name: &str) -> Self {
507 let now = Utc::now();
508 Self {
509 id: uuid::Uuid::new_v4().to_string(),
510 name: name.to_string(),
511 hypothesis: String::new(),
512 description: String::new(),
513 status: ExperimentStatus::Planning,
514 config: ExperimentConfig::default(),
515 optimizer_configs: HashMap::new(),
516 dataset_info: DatasetInfo::default(),
517 metrics: Vec::new(),
518 results: Vec::new(),
519 reproducibility: ReproducibilityInfo::default(),
520 timeline: ExperimentTimeline {
521 created_at: now,
522 started_at: None,
523 completed_at: None,
524 estimated_duration: None,
525 actual_duration: None,
526 },
527 notes: Vec::new(),
528 metadata: ExperimentMetadata::default(),
529 }
530 }
531
532 pub fn hypothesis(mut self, hypothesis: &str) -> Self {
534 self.hypothesis = hypothesis.to_string();
535 self
536 }
537
538 pub fn description(mut self, description: &str) -> Self {
540 self.description = description.to_string();
541 self
542 }
543
544 pub fn add_optimizer_config(mut self, name: &str, config: OptimizerConfig<f64>) -> Self {
546 self.optimizer_configs.insert(name.to_string(), config);
547 self
548 }
549
550 pub fn dataset(mut self, datasetinfo: DatasetInfo) -> Self {
552 self.dataset_info = datasetinfo;
553 self
554 }
555
556 pub fn metrics(mut self, metrics: Vec<String>) -> Self {
558 self.metrics = metrics;
559 self
560 }
561
562 pub fn add_note(&mut self, author: &str, content: &str, notetype: NoteType) {
564 let note = ExperimentNote {
565 timestamp: Utc::now(),
566 author: author.to_string(),
567 content: content.to_string(),
568 note_type: notetype,
569 run_id: None,
570 };
571 self.notes.push(note);
572 }
573
574 pub fn start(&mut self) -> Result<()> {
576 if self.status != ExperimentStatus::Ready && self.status != ExperimentStatus::Planning {
577 return Err(OptimError::InvalidConfig(format!(
578 "Cannot start experiment in status {:?}",
579 self.status
580 )));
581 }
582
583 self.status = ExperimentStatus::Running;
584 self.timeline.started_at = Some(Utc::now());
585
586 Ok(())
587 }
588
589 pub fn complete(&mut self) -> Result<()> {
591 if self.status != ExperimentStatus::Running {
592 return Err(OptimError::InvalidConfig(format!(
593 "Cannot complete experiment in status {:?}",
594 self.status
595 )));
596 }
597
598 self.status = ExperimentStatus::Completed;
599 self.timeline.completed_at = Some(Utc::now());
600
601 if let (Some(start), Some(end)) = (self.timeline.started_at, self.timeline.completed_at) {
602 self.timeline.actual_duration = Some(end - start);
603 }
604
605 Ok(())
606 }
607
608 pub fn generate_report(&self) -> String {
610 let mut report = String::new();
611
612 report.push_str(&format!("# Experiment Report: {}\n\n", self.name));
613 report.push_str(&format!("**ID**: {}\n", self.id));
614 report.push_str(&format!("**Status**: {:?}\n", self.status));
615 report.push_str(&format!("**Hypothesis**: {}\n\n", self.hypothesis));
616
617 if !self.description.is_empty() {
618 report.push_str(&format!("## Description\n\n{}\n\n", self.description));
619 }
620
621 report.push_str("## Configuration\n\n");
622 report.push_str(&format!("- **Random Seed**: {}\n", self.config.random_seed));
623 report.push_str(&format!("- **Number of Runs**: {}\n", self.config.num_runs));
624 report.push_str(&format!("- **Max Epochs**: {}\n", self.config.max_epochs));
625
626 report.push_str("\n## Optimizers\n\n");
627 for name in self.optimizer_configs.keys() {
628 report.push_str(&format!("- {}\n", name));
629 }
630
631 report.push_str("\n## Dataset\n\n");
632 report.push_str(&format!("- **Name**: {}\n", self.dataset_info.name));
633 report.push_str(&format!(
634 "- **Samples**: {}\n",
635 self.dataset_info.num_samples
636 ));
637 report.push_str(&format!(
638 "- **Features**: {}\n",
639 self.dataset_info.num_features
640 ));
641
642 report.push_str("\n## Results\n\n");
643 report.push_str(&format!("**Total Runs**: {}\n\n", self.results.len()));
644
645 let mut optimizer_results: HashMap<String, Vec<&ExperimentResult>> = HashMap::new();
647 for result in &self.results {
648 optimizer_results
649 .entry(result.optimizer_name.clone())
650 .or_default()
651 .push(result);
652 }
653
654 for (optimizer, results) in optimizer_results {
655 report.push_str(&format!("### {}\n\n", optimizer));
656
657 if !results.is_empty() {
658 let successful_runs: Vec<&ExperimentResult> = results
660 .iter()
661 .filter(|r| r.status == RunStatus::Success)
662 .copied()
663 .collect();
664
665 report.push_str(&format!(
666 "- **Successful Runs**: {}/{}\n",
667 successful_runs.len(),
668 results.len()
669 ));
670
671 if !successful_runs.is_empty() {
672 for metric in &self.metrics {
673 if let Some(values) = self.get_metric_values(&successful_runs, metric) {
674 let mean = values.iter().sum::<f64>() / values.len() as f64;
675 let std = (values.iter().map(|v| (v - mean).powi(2)).sum::<f64>()
676 / values.len() as f64)
677 .sqrt();
678 report
679 .push_str(&format!("- **{}**: {:.4} ± {:.4}\n", metric, mean, std));
680 }
681 }
682 }
683 }
684 report.push('\n');
685 }
686
687 if !self.notes.is_empty() {
688 report.push_str("## Notes\n\n");
689 for note in &self.notes {
690 report.push_str(&format!(
691 "**{}** ({}): {}\n\n",
692 note.author,
693 note.timestamp.format("%Y-%m-%d %H:%M"),
694 note.content
695 ));
696 }
697 }
698
699 report
700 }
701
702 fn get_metric_values(&self, results: &[&ExperimentResult], metric: &str) -> Option<Vec<f64>> {
703 let mut values = Vec::new();
704 for result in results {
705 if let Some(&value) = result.final_metrics.get(metric) {
706 values.push(value);
707 }
708 }
709 if values.is_empty() {
710 None
711 } else {
712 Some(values)
713 }
714 }
715}
716
717impl Default for ExperimentConfig {
718 fn default() -> Self {
719 Self {
720 random_seed: 42,
721 num_runs: 1,
722 max_epochs: 100,
723 early_stopping: None,
724 hardware_config: HardwareConfig::default(),
725 environment: HashMap::new(),
726 validation_split: 0.2,
727 test_split: 0.1,
728 cv_folds: None,
729 }
730 }
731}
732
733impl Default for CpuInfo {
734 fn default() -> Self {
735 Self {
736 model: "Unknown".to_string(),
737 cores: std::thread::available_parallelism()
738 .map(|p| p.get())
739 .unwrap_or(1),
740 threads: std::thread::available_parallelism()
741 .map(|p| p.get())
742 .unwrap_or(1),
743 frequency_mhz: 0,
744 cache_sizes: Vec::new(),
745 simd_capabilities: Vec::new(),
746 }
747 }
748}
749
750impl Default for MemoryConfig {
751 fn default() -> Self {
752 Self {
753 total_memory_mb: 8192, available_memory_mb: 6144, allocation_strategy: MemoryAllocationStrategy::Standard,
756 pool_size_mb: None,
757 }
758 }
759}
760
761impl Default for ParallelConfig {
762 fn default() -> Self {
763 Self {
764 num_threads: std::thread::available_parallelism()
765 .map(|p| p.get())
766 .unwrap_or(1),
767 thread_affinity: None,
768 work_stealing: true,
769 chunk_size: None,
770 }
771 }
772}
773
774impl Default for DatasetInfo {
775 fn default() -> Self {
776 Self {
777 name: "Unknown".to_string(),
778 description: String::new(),
779 source: String::new(),
780 version: "1.0".to_string(),
781 num_samples: 0,
782 num_features: 0,
783 num_classes: None,
784 data_type: DataType::Tabular,
785 statistics: DatasetStatistics::default(),
786 preprocessing: Vec::new(),
787 }
788 }
789}
790
791impl Default for SystemInfo {
792 fn default() -> Self {
793 Self {
794 os: std::env::consts::OS.to_string(),
795 os_version: String::new(),
796 architecture: std::env::consts::ARCH.to_string(),
797 hostname: String::new(),
798 username: std::env::var("USER").unwrap_or_else(|_| "unknown".to_string()),
799 timezone: String::new(),
800 }
801 }
802}
803
804impl ExperimentRunner {
805 pub fn new(experiment: Experiment, monitor_interval_seconds: u64) -> Self {
808 Self {
809 experiment,
810 resource_monitor: ResourceMonitor::new(monitor_interval_seconds),
811 progress_callback: None,
812 }
813 }
814
815 pub fn set_progress_callback<F>(&mut self, callback: F)
818 where
819 F: Fn(f64) + Send + Sync + 'static,
820 {
821 self.progress_callback = Some(Box::new(callback));
822 }
823
824 pub fn experiment(&self) -> &Experiment {
826 &self.experiment
827 }
828
829 pub fn experiment_mut(&mut self) -> &mut Experiment {
831 &mut self.experiment
832 }
833
834 pub fn resource_monitor(&self) -> &ResourceMonitor {
836 &self.resource_monitor
837 }
838
839 pub fn start(&mut self) -> Result<()> {
842 self.experiment.start()?;
843 self.resource_monitor.start_monitoring();
844 Ok(())
845 }
846
847 pub fn record_result(&mut self, mut result: ExperimentResult) {
851 if result.end_time.is_none() {
852 result.end_time = Some(Utc::now());
853 }
854 self.experiment.results.push(result);
855
856 if let Some(ref callback) = self.progress_callback {
857 let target = self.experiment.config.num_runs.max(1);
858 let completed = self.experiment.results.len().min(target);
859 callback(completed as f64 / target as f64);
860 }
861 }
862
863 pub fn finish(&mut self) -> Result<ResourceUsage> {
866 let usage = self.resource_monitor.stop_monitoring();
867 self.experiment.complete()?;
868 Ok(usage)
869 }
870}
871
872impl ResourceMonitor {
873 pub fn new(_intervalseconds: u64) -> Self {
875 Self {
876 cpu_usage: Vec::new(),
877 memory_usage: Vec::new(),
878 gpu_memory_usage: Vec::new(),
879 interval_seconds: _intervalseconds,
880 }
881 }
882
883 pub fn start_monitoring(&mut self) {
894 self.cpu_usage.clear();
895 self.memory_usage.clear();
896 self.gpu_memory_usage.clear();
897 }
898
899 pub fn record_sample(
901 &mut self,
902 cpu_percent: f64,
903 memory_mb: usize,
904 gpu_memory_mb: Option<usize>,
905 ) {
906 self.cpu_usage.push(cpu_percent);
907 self.memory_usage.push(memory_mb);
908 self.gpu_memory_usage.push(gpu_memory_mb);
909 }
910
911 pub fn sample_count(&self) -> usize {
913 self.cpu_usage.len()
914 }
915
916 pub fn interval_seconds(&self) -> u64 {
918 self.interval_seconds
919 }
920
921 pub fn stop_monitoring(&self) -> ResourceUsage {
923 let peak_cpu = self.cpu_usage.iter().fold(0.0f64, |a, &b| a.max(b));
924 let avg_cpu = if self.cpu_usage.is_empty() {
925 0.0
926 } else {
927 self.cpu_usage.iter().sum::<f64>() / self.cpu_usage.len() as f64
928 };
929
930 let peak_memory = self.memory_usage.iter().fold(0usize, |a, &b| a.max(b));
931 let avg_memory = if self.memory_usage.is_empty() {
932 0
933 } else {
934 self.memory_usage.iter().sum::<usize>() / self.memory_usage.len()
935 };
936
937 ResourceUsage {
938 peak_cpu_usage: peak_cpu,
939 avg_cpu_usage: avg_cpu,
940 peak_memory_mb: peak_memory,
941 avg_memory_mb: avg_memory,
942 peak_gpu_memory_mb: self.gpu_memory_usage.iter().flatten().copied().max(),
947 total_time_seconds: self.cpu_usage.len().saturating_sub(1) as f64
948 * self.interval_seconds as f64,
949 energy_consumption_joules: None,
951 }
952 }
953}
954
955#[cfg(test)]
956mod tests {
957 use super::*;
958
959 #[test]
960 fn test_experiment_creation() {
961 let experiment = Experiment::new("Test Experiment")
962 .hypothesis("Test hypothesis")
963 .description("Test description")
964 .metrics(vec!["accuracy".to_string(), "loss".to_string()]);
965
966 assert_eq!(experiment.name, "Test Experiment");
967 assert_eq!(experiment.hypothesis, "Test hypothesis");
968 assert_eq!(experiment.description, "Test description");
969 assert_eq!(experiment.metrics.len(), 2);
970 assert_eq!(experiment.status, ExperimentStatus::Planning);
971 }
972
973 #[test]
974 fn test_experiment_lifecycle() {
975 let mut experiment = Experiment::new("Lifecycle Test");
976
977 experiment.status = ExperimentStatus::Ready;
979 assert!(experiment.start().is_ok());
980 assert_eq!(experiment.status, ExperimentStatus::Running);
981 assert!(experiment.timeline.started_at.is_some());
982
983 assert!(experiment.complete().is_ok());
985 assert_eq!(experiment.status, ExperimentStatus::Completed);
986 assert!(experiment.timeline.completed_at.is_some());
987 assert!(experiment.timeline.actual_duration.is_some());
988 }
989
990 #[test]
991 fn test_experiment_notes() {
992 let mut experiment = Experiment::new("Notes Test");
993
994 experiment.add_note("Researcher", "Initial observation", NoteType::Observation);
995 experiment.add_note("Researcher", "Found an issue", NoteType::Issue);
996
997 assert_eq!(experiment.notes.len(), 2);
998 assert_eq!(experiment.notes[0].note_type, NoteType::Observation);
999 assert_eq!(experiment.notes[1].note_type, NoteType::Issue);
1000 }
1001
1002 #[test]
1006 fn test_experiment_runner_is_constructible_and_functional() {
1007 let mut experiment = Experiment::new("Runner Test");
1008 experiment.config.num_runs = 2;
1009 experiment.status = ExperimentStatus::Ready;
1010
1011 let mut runner = ExperimentRunner::new(experiment, 1);
1012 assert!(runner.start().is_ok());
1013 assert_eq!(runner.experiment().status, ExperimentStatus::Running);
1014
1015 let progress = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1016 let progress_clone = progress.clone();
1017 runner.set_progress_callback(move |fraction| {
1018 progress_clone.lock().expect("lock").push(fraction);
1019 });
1020
1021 let make_result = |run_id: &str| ExperimentResult {
1022 run_id: run_id.to_string(),
1023 optimizer_name: "adam".to_string(),
1024 start_time: Utc::now(),
1025 end_time: None,
1026 status: RunStatus::Success,
1027 final_metrics: HashMap::new(),
1028 training_history: TrainingHistory {
1029 epochs: vec![],
1030 train_metrics: HashMap::new(),
1031 val_metrics: HashMap::new(),
1032 learning_rates: vec![],
1033 gradient_norms: vec![],
1034 parameter_norms: vec![],
1035 step_times: vec![],
1036 },
1037 resource_usage: ResourceUsage::default(),
1038 error_info: None,
1039 metadata: HashMap::new(),
1040 };
1041
1042 runner.record_result(make_result("run-1"));
1043 runner.record_result(make_result("run-2"));
1044
1045 assert_eq!(runner.experiment().results.len(), 2);
1046 assert!(runner.experiment().results[0].end_time.is_some());
1047 let recorded_progress = progress.lock().expect("lock").clone();
1048 assert_eq!(recorded_progress, vec![0.5, 1.0]);
1049
1050 let usage = runner.finish().expect("finish should succeed");
1051 assert_eq!(usage.peak_cpu_usage, 0.0);
1052 assert_eq!(runner.experiment().status, ExperimentStatus::Completed);
1053 }
1054}