1#![allow(dead_code)]
21
22use std::collections::HashMap;
23
24#[derive(Debug, Clone)]
30pub struct SimulationRecord {
31 pub id: String,
33 pub timestamp: u64,
35 pub parameters: HashMap<String, f64>,
37 pub metadata: HashMap<String, String>,
39}
40
41impl SimulationRecord {
42 pub fn new(id: impl Into<String>, timestamp: u64) -> Self {
44 Self {
45 id: id.into(),
46 timestamp,
47 parameters: HashMap::new(),
48 metadata: HashMap::new(),
49 }
50 }
51
52 pub fn set_param(&mut self, key: impl Into<String>, value: f64) {
54 self.parameters.insert(key.into(), value);
55 }
56
57 pub fn set_meta(&mut self, key: impl Into<String>, value: impl Into<String>) {
59 self.metadata.insert(key.into(), value.into());
60 }
61}
62
63#[derive(Debug, Default)]
69pub struct SimulationDatabase {
70 pub records: Vec<SimulationRecord>,
72 pub file_path: String,
74}
75
76impl SimulationDatabase {
77 pub fn new(file_path: impl Into<String>) -> Self {
79 Self {
80 records: Vec::new(),
81 file_path: file_path.into(),
82 }
83 }
84
85 pub fn add_record(&mut self, record: SimulationRecord) {
87 self.records.push(record);
88 }
89
90 pub fn find_by_id(&self, id: &str) -> Option<&SimulationRecord> {
92 self.records.iter().find(|r| r.id == id)
93 }
94
95 pub fn query_range(&self, param: &str, lo: f64, hi: f64) -> Vec<&SimulationRecord> {
97 self.records
98 .iter()
99 .filter(|r| r.parameters.get(param).is_some_and(|&v| v >= lo && v <= hi))
100 .collect()
101 }
102
103 pub fn query_time_range(&self, t_lo: u64, t_hi: u64) -> Vec<&SimulationRecord> {
105 self.records
106 .iter()
107 .filter(|r| r.timestamp >= t_lo && r.timestamp <= t_hi)
108 .collect()
109 }
110
111 pub fn delete_by_id(&mut self, id: &str) -> usize {
114 let before = self.records.len();
115 self.records.retain(|r| r.id != id);
116 before - self.records.len()
117 }
118
119 pub fn save_to_csv(&self) -> String {
123 let mut out = String::from("id,timestamp,parameters,metadata\n");
124 for r in &self.records {
125 let params: Vec<String> = r
126 .parameters
127 .iter()
128 .map(|(k, v)| format!("{k}={v}"))
129 .collect();
130 let meta: Vec<String> = r.metadata.iter().map(|(k, v)| format!("{k}={v}")).collect();
131 out.push_str(&format!(
132 "{},{},{},{}\n",
133 r.id,
134 r.timestamp,
135 params.join(";"),
136 meta.join(";")
137 ));
138 }
139 out
140 }
141
142 pub fn load_from_csv(&mut self, s: &str) {
146 self.records.clear();
147 for line in s.lines().skip(1) {
148 let parts: Vec<&str> = line.splitn(4, ',').collect();
149 if parts.len() < 2 {
150 continue;
151 }
152 let id = parts[0].to_string();
153 let timestamp: u64 = parts[1].parse().unwrap_or(0);
154 let mut record = SimulationRecord::new(id, timestamp);
155 if parts.len() > 2 && !parts[2].is_empty() {
156 for pair in parts[2].split(';') {
157 let kv: Vec<&str> = pair.splitn(2, '=').collect();
158 if kv.len() == 2
159 && let Ok(v) = kv[1].parse::<f64>()
160 {
161 record.parameters.insert(kv[0].to_string(), v);
162 }
163 }
164 }
165 if parts.len() > 3 && !parts[3].is_empty() {
166 for pair in parts[3].split(';') {
167 let kv: Vec<&str> = pair.splitn(2, '=').collect();
168 if kv.len() == 2 {
169 record.metadata.insert(kv[0].to_string(), kv[1].to_string());
170 }
171 }
172 }
173 self.records.push(record);
174 }
175 }
176
177 pub fn statistics(&self, param: &str) -> (f64, f64, f64) {
180 let values: Vec<f64> = self
181 .records
182 .iter()
183 .filter_map(|r| r.parameters.get(param).copied())
184 .collect();
185 if values.is_empty() {
186 return (0.0, 0.0, 0.0);
187 }
188 let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
189 let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
190 let mean = values.iter().sum::<f64>() / values.len() as f64;
191 (min, max, mean)
192 }
193
194 pub fn export_json(&self) -> String {
196 let mut out = String::from("[\n");
197 for (i, r) in self.records.iter().enumerate() {
198 out.push_str(" {\n");
199 out.push_str(&format!(" \"id\": \"{}\",\n", r.id));
200 out.push_str(&format!(" \"timestamp\": {},\n", r.timestamp));
201 out.push_str(" \"parameters\": {");
202 let params: Vec<String> = r
203 .parameters
204 .iter()
205 .map(|(k, v)| format!("\"{k}\": {v}"))
206 .collect();
207 out.push_str(¶ms.join(", "));
208 out.push_str("},\n");
209 out.push_str(" \"metadata\": {");
210 let meta: Vec<String> = r
211 .metadata
212 .iter()
213 .map(|(k, v)| format!("\"{k}\": \"{v}\""))
214 .collect();
215 out.push_str(&meta.join(", "));
216 out.push_str("}\n");
217 if i + 1 < self.records.len() {
218 out.push_str(" },\n");
219 } else {
220 out.push_str(" }\n");
221 }
222 }
223 out.push(']');
224 out
225 }
226
227 pub fn import_json_ids(&mut self, json: &str) {
232 for chunk in json.split('{') {
234 let id = extract_json_str(chunk, "\"id\"");
235 let ts_str = extract_json_number(chunk, "\"timestamp\"");
236 if let Some(id) = id {
237 let ts: u64 = ts_str.unwrap_or_default().parse().unwrap_or(0);
238 self.records.push(SimulationRecord::new(id, ts));
239 }
240 }
241 }
242
243 pub fn len(&self) -> usize {
245 self.records.len()
246 }
247
248 pub fn is_empty(&self) -> bool {
250 self.records.is_empty()
251 }
252}
253
254fn extract_json_str(chunk: &str, key: &str) -> Option<String> {
256 let pos = chunk.find(key)?;
257 let rest = &chunk[pos + key.len()..];
258 let colon = rest.find(':')? + 1;
259 let rest2 = rest[colon..].trim_start();
260 if !rest2.starts_with('"') {
261 return None;
262 }
263 let inner = &rest2[1..];
264 let end = inner.find('"')?;
265 Some(inner[..end].to_string())
266}
267
268fn extract_json_number(chunk: &str, key: &str) -> Option<String> {
270 let pos = chunk.find(key)?;
271 let rest = &chunk[pos + key.len()..];
272 let colon = rest.find(':')? + 1;
273 let rest2 = rest[colon..].trim_start();
274 let end = rest2
275 .find(|c: char| !c.is_ascii_digit())
276 .unwrap_or(rest2.len());
277 if end == 0 {
278 return None;
279 }
280 Some(rest2[..end].to_string())
281}
282
283pub fn rle_encode(data: &[f64]) -> Vec<(f64, usize)> {
291 if data.is_empty() {
292 return vec![];
293 }
294 let mut result = Vec::new();
295 let mut current = data[0];
296 let mut count = 1usize;
297 for &v in &data[1..] {
298 if (v - current).abs() < f64::EPSILON {
299 count += 1;
300 } else {
301 result.push((current, count));
302 current = v;
303 count = 1;
304 }
305 }
306 result.push((current, count));
307 result
308}
309
310pub fn rle_decode(encoded: &[(f64, usize)]) -> Vec<f64> {
312 let mut result = Vec::new();
313 for &(v, n) in encoded {
314 for _ in 0..n {
315 result.push(v);
316 }
317 }
318 result
319}
320
321pub fn rle_compression_ratio(original_len: usize, encoded: &[(f64, usize)]) -> f64 {
325 if encoded.is_empty() || original_len == 0 {
326 return 1.0;
327 }
328 original_len as f64 / encoded.len() as f64
329}
330
331#[derive(Debug, Clone)]
338pub struct Snapshot {
339 pub time: f64,
341 pub fields: HashMap<String, Vec<f64>>,
343}
344
345impl Snapshot {
346 pub fn new(time: f64) -> Self {
348 Self {
349 time,
350 fields: HashMap::new(),
351 }
352 }
353
354 pub fn set_field(&mut self, name: impl Into<String>, data: Vec<f64>) {
356 self.fields.insert(name.into(), data);
357 }
358
359 pub fn node_count(&self) -> usize {
361 self.fields.values().next().map_or(0, |v| v.len())
362 }
363}
364
365#[derive(Debug, Clone)]
368pub struct DiffEntry {
369 pub time: f64,
371 pub field: String,
373 pub indices: Vec<usize>,
375 pub new_values: Vec<f64>,
377}
378
379impl DiffEntry {
380 pub fn new(time: f64, field: impl Into<String>) -> Self {
382 Self {
383 time,
384 field: field.into(),
385 indices: Vec::new(),
386 new_values: Vec::new(),
387 }
388 }
389}
390
391#[derive(Debug, Default)]
394pub struct SnapshotTable {
395 pub snapshots: Vec<Snapshot>,
397 pub diffs: Vec<DiffEntry>,
399 pub compressed: HashMap<String, Vec<(f64, usize)>>,
401}
402
403impl SnapshotTable {
404 pub fn new() -> Self {
406 Self::default()
407 }
408
409 pub fn insert(&mut self, snap: Snapshot) {
411 let pos = self.snapshots.partition_point(|s| s.time < snap.time);
412 self.snapshots.insert(pos, snap);
413 }
414
415 pub fn query_time_range(&self, t_lo: f64, t_hi: f64) -> Vec<&Snapshot> {
417 self.snapshots
418 .iter()
419 .filter(|s| s.time >= t_lo && s.time <= t_hi)
420 .collect()
421 }
422
423 pub fn nearest(&self, t: f64) -> Option<&Snapshot> {
425 self.snapshots.iter().min_by(|a, b| {
426 (a.time - t)
427 .abs()
428 .partial_cmp(&(b.time - t).abs())
429 .unwrap_or(std::cmp::Ordering::Equal)
430 })
431 }
432
433 pub fn compress_field(&mut self, snap_idx: usize, field: &str) {
438 if let Some(snap) = self.snapshots.get(snap_idx)
439 && let Some(data) = snap.fields.get(field)
440 {
441 let encoded = rle_encode(data);
442 let key = format!("{field}@{snap_idx}");
443 self.compressed.insert(key, encoded);
444 }
445 }
446
447 pub fn decompress_field(&self, snap_idx: usize, field: &str) -> Option<Vec<f64>> {
451 let key = format!("{field}@{snap_idx}");
452 self.compressed.get(&key).map(|enc| rle_decode(enc))
453 }
454
455 pub fn compute_diff(&mut self, field: &str, snap_idx: usize) -> usize {
460 if snap_idx == 0 || snap_idx >= self.snapshots.len() {
461 return 0;
462 }
463 let prev = self.snapshots[snap_idx - 1]
464 .fields
465 .get(field)
466 .cloned()
467 .unwrap_or_default();
468 let curr = self.snapshots[snap_idx]
469 .fields
470 .get(field)
471 .cloned()
472 .unwrap_or_default();
473 let time = self.snapshots[snap_idx].time;
474 let mut entry = DiffEntry::new(time, field);
475 for (i, (&p, &c)) in prev.iter().zip(curr.iter()).enumerate() {
476 if (c - p).abs() > f64::EPSILON {
477 entry.indices.push(i);
478 entry.new_values.push(c);
479 }
480 }
481 let changed = entry.indices.len();
482 self.diffs.push(entry);
483 changed
484 }
485
486 pub fn apply_diff(base: &mut [f64], diff: &DiffEntry) {
490 for (&idx, &val) in diff.indices.iter().zip(diff.new_values.iter()) {
491 if idx < base.len() {
492 base[idx] = val;
493 }
494 }
495 }
496
497 pub fn len(&self) -> usize {
499 self.snapshots.len()
500 }
501
502 pub fn is_empty(&self) -> bool {
504 self.snapshots.is_empty()
505 }
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
514pub enum EventLevel {
515 Debug,
517 Info,
519 Warning,
521 Error,
523 Critical,
525}
526
527impl EventLevel {
528 pub fn as_str(self) -> &'static str {
530 match self {
531 EventLevel::Debug => "DEBUG",
532 EventLevel::Info => "INFO",
533 EventLevel::Warning => "WARNING",
534 EventLevel::Error => "ERROR",
535 EventLevel::Critical => "CRITICAL",
536 }
537 }
538}
539
540#[derive(Debug, Clone)]
542pub struct LogEvent {
543 pub sim_time: f64,
545 pub wall_time: u64,
547 pub level: EventLevel,
549 pub category: String,
551 pub message: String,
553}
554
555impl LogEvent {
556 pub fn new(
558 sim_time: f64,
559 wall_time: u64,
560 level: EventLevel,
561 category: impl Into<String>,
562 message: impl Into<String>,
563 ) -> Self {
564 Self {
565 sim_time,
566 wall_time,
567 level,
568 category: category.into(),
569 message: message.into(),
570 }
571 }
572}
573
574#[derive(Debug, Default)]
576pub struct EventLog {
577 pub events: Vec<LogEvent>,
579}
580
581impl EventLog {
582 pub fn new() -> Self {
584 Self::default()
585 }
586
587 pub fn log(&mut self, event: LogEvent) {
589 self.events.push(event);
590 }
591
592 pub fn info(&mut self, sim_time: f64, category: impl Into<String>, message: impl Into<String>) {
594 self.log(LogEvent::new(
595 sim_time,
596 0,
597 EventLevel::Info,
598 category,
599 message,
600 ));
601 }
602
603 pub fn warn(&mut self, sim_time: f64, category: impl Into<String>, message: impl Into<String>) {
605 self.log(LogEvent::new(
606 sim_time,
607 0,
608 EventLevel::Warning,
609 category,
610 message,
611 ));
612 }
613
614 pub fn error(
616 &mut self,
617 sim_time: f64,
618 category: impl Into<String>,
619 message: impl Into<String>,
620 ) {
621 self.log(LogEvent::new(
622 sim_time,
623 0,
624 EventLevel::Error,
625 category,
626 message,
627 ));
628 }
629
630 pub fn filter_level(&self, min_level: EventLevel) -> Vec<&LogEvent> {
632 self.events
633 .iter()
634 .filter(|e| e.level >= min_level)
635 .collect()
636 }
637
638 pub fn filter_category<'a>(&'a self, cat: &str) -> Vec<&'a LogEvent> {
640 self.events.iter().filter(|e| e.category == cat).collect()
641 }
642
643 pub fn filter_sim_time(&self, t_lo: f64, t_hi: f64) -> Vec<&LogEvent> {
645 self.events
646 .iter()
647 .filter(|e| e.sim_time >= t_lo && e.sim_time <= t_hi)
648 .collect()
649 }
650
651 pub fn to_csv(&self) -> String {
653 let mut out = String::from("sim_time,wall_time,level,category,message\n");
654 for e in &self.events {
655 out.push_str(&format!(
656 "{},{},{},{},{}\n",
657 e.sim_time,
658 e.wall_time,
659 e.level.as_str(),
660 e.category,
661 e.message
662 ));
663 }
664 out
665 }
666
667 pub fn len(&self) -> usize {
669 self.events.len()
670 }
671
672 pub fn is_empty(&self) -> bool {
674 self.events.is_empty()
675 }
676}
677
678#[derive(Debug, Clone)]
684pub struct AggStats {
685 pub count: usize,
687 pub min: f64,
689 pub max: f64,
691 pub mean: f64,
693 pub std: f64,
695 pub sum: f64,
697}
698
699impl AggStats {
700 pub fn from_slice(data: &[f64]) -> Option<Self> {
702 if data.is_empty() {
703 return None;
704 }
705 let n = data.len();
706 let sum = data.iter().sum::<f64>();
707 let mean = sum / n as f64;
708 let min = data.iter().cloned().fold(f64::INFINITY, f64::min);
709 let max = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
710 let variance = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
711 let std = variance.sqrt();
712 Some(Self {
713 count: n,
714 min,
715 max,
716 mean,
717 std,
718 sum,
719 })
720 }
721}
722
723#[derive(Debug, Default)]
725pub struct ResultAggregator {
726 pub samples: Vec<f64>,
728}
729
730impl ResultAggregator {
731 pub fn new() -> Self {
733 Self::default()
734 }
735
736 pub fn add_snapshot_mean(&mut self, snapshot: &Snapshot, field: &str) {
738 if let Some(data) = snapshot.fields.get(field)
739 && !data.is_empty()
740 {
741 let mean = data.iter().sum::<f64>() / data.len() as f64;
742 self.samples.push(mean);
743 }
744 }
745
746 pub fn add_snapshot_max_abs(&mut self, snapshot: &Snapshot, field: &str) {
748 if let Some(data) = snapshot.fields.get(field)
749 && let Some(max_abs) = data.iter().cloned().map(f64::abs).reduce(f64::max)
750 {
751 self.samples.push(max_abs);
752 }
753 }
754
755 pub fn push(&mut self, v: f64) {
757 self.samples.push(v);
758 }
759
760 pub fn compute(&self) -> Option<AggStats> {
762 AggStats::from_slice(&self.samples)
763 }
764
765 pub fn reset(&mut self) {
767 self.samples.clear();
768 }
769}
770
771#[derive(Debug, Default, Clone)]
778pub struct MetadataStore {
779 pub entries: HashMap<String, String>,
781}
782
783impl MetadataStore {
784 pub fn new() -> Self {
786 Self::default()
787 }
788
789 pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
791 self.entries.insert(key.into(), value.into());
792 }
793
794 pub fn get(&self, key: &str) -> Option<&str> {
796 self.entries.get(key).map(String::as_str)
797 }
798
799 pub fn to_properties(&self) -> String {
801 let mut lines: Vec<String> = self
802 .entries
803 .iter()
804 .map(|(k, v)| format!("{k}={v}"))
805 .collect();
806 lines.sort();
807 lines.join("\n")
808 }
809
810 pub fn from_properties(s: &str) -> Self {
812 let mut store = Self::new();
813 for line in s.lines() {
814 if let Some(pos) = line.find('=') {
815 let k = &line[..pos];
816 let v = &line[pos + 1..];
817 store.set(k, v);
818 }
819 }
820 store
821 }
822
823 pub fn merge(&mut self, other: &MetadataStore) {
825 for (k, v) in &other.entries {
826 self.entries.insert(k.clone(), v.clone());
827 }
828 }
829
830 pub fn len(&self) -> usize {
832 self.entries.len()
833 }
834
835 pub fn is_empty(&self) -> bool {
837 self.entries.is_empty()
838 }
839}
840
841#[derive(Debug, Clone, PartialEq, Eq)]
847pub enum ProvenanceType {
848 Computation,
850 Dataset,
852 Agent,
854}
855
856#[derive(Debug, Clone)]
862pub struct ProvenanceNode {
863 pub id: String,
865 pub type_: ProvenanceType,
867 pub inputs: Vec<String>,
869 pub outputs: Vec<String>,
871}
872
873impl ProvenanceNode {
874 pub fn new(id: impl Into<String>, type_: ProvenanceType) -> Self {
876 Self {
877 id: id.into(),
878 type_,
879 inputs: Vec::new(),
880 outputs: Vec::new(),
881 }
882 }
883}
884
885#[derive(Debug, Default)]
891pub struct ProvenanceTracker {
892 pub graph: Vec<ProvenanceNode>,
894}
895
896impl ProvenanceTracker {
897 pub fn new() -> Self {
899 Self::default()
900 }
901
902 pub fn add_computation(
904 &mut self,
905 id: impl Into<String>,
906 inputs: Vec<String>,
907 outputs: Vec<String>,
908 ) {
909 let mut node = ProvenanceNode::new(id, ProvenanceType::Computation);
910 node.inputs = inputs;
911 node.outputs = outputs;
912 self.graph.push(node);
913 }
914
915 pub fn add_data_source(&mut self, id: impl Into<String>, outputs: Vec<String>) {
917 let mut node = ProvenanceNode::new(id, ProvenanceType::Dataset);
918 node.outputs = outputs;
919 self.graph.push(node);
920 }
921
922 pub fn trace_lineage(&self, target_id: &str) -> Vec<String> {
926 let mut visited: Vec<String> = Vec::new();
927 let mut queue: Vec<String> = vec![target_id.to_string()];
928 while let Some(current) = queue.pop() {
929 if visited.contains(¤t) {
930 continue;
931 }
932 visited.push(current.clone());
933 for node in &self.graph {
934 if node.outputs.contains(¤t) && !visited.contains(&node.id) {
935 queue.push(node.id.clone());
936 }
937 }
938 if let Some(node) = self.graph.iter().find(|n| n.id == current) {
939 for inp in &node.inputs {
940 if !visited.contains(inp) {
941 queue.push(inp.clone());
942 }
943 }
944 }
945 }
946 visited
947 }
948
949 pub fn to_prov_json(&self) -> String {
951 let mut out = String::from("{\n \"nodes\": [\n");
952 for (i, node) in self.graph.iter().enumerate() {
953 let type_str = match node.type_ {
954 ProvenanceType::Computation => "Activity",
955 ProvenanceType::Dataset => "Entity",
956 ProvenanceType::Agent => "Agent",
957 };
958 out.push_str(&format!(
959 " {{\"id\": \"{}\", \"type\": \"{}\", \"inputs\": [{}], \"outputs\": [{}]}}",
960 node.id,
961 type_str,
962 node.inputs
963 .iter()
964 .map(|s| format!("\"{s}\""))
965 .collect::<Vec<_>>()
966 .join(", "),
967 node.outputs
968 .iter()
969 .map(|s| format!("\"{s}\""))
970 .collect::<Vec<_>>()
971 .join(", "),
972 ));
973 if i + 1 < self.graph.len() {
974 out.push_str(",\n");
975 } else {
976 out.push('\n');
977 }
978 }
979 out.push_str(" ]\n}");
980 out
981 }
982}
983
984#[derive(Debug)]
993pub struct CheckpointManager {
994 pub base_dir: String,
996 pub max_checkpoints: usize,
998 store: HashMap<usize, Vec<f64>>,
999}
1000
1001impl CheckpointManager {
1002 pub fn new(base_dir: impl Into<String>, max_checkpoints: usize) -> Self {
1004 Self {
1005 base_dir: base_dir.into(),
1006 max_checkpoints: max_checkpoints.max(1),
1007 store: HashMap::new(),
1008 }
1009 }
1010
1011 pub fn save_checkpoint(&mut self, step: usize, data: &[f64]) -> String {
1013 self.store.insert(step, data.to_vec());
1014 self.cleanup_old();
1015 format!("{}/checkpoint_{step:06}.bin", self.base_dir)
1016 }
1017
1018 pub fn list_checkpoints(&self) -> Vec<usize> {
1020 let mut steps: Vec<usize> = self.store.keys().copied().collect();
1021 steps.sort_unstable();
1022 steps
1023 }
1024
1025 pub fn load_checkpoint(&self, step: usize) -> Option<Vec<f64>> {
1027 self.store.get(&step).cloned()
1028 }
1029
1030 pub fn cleanup_old(&mut self) {
1032 let mut steps = self.list_checkpoints();
1033 while steps.len() > self.max_checkpoints {
1034 let oldest = steps.remove(0);
1035 self.store.remove(&oldest);
1036 }
1037 }
1038}
1039
1040#[derive(Debug, Default)]
1049pub struct ParameterSweep {
1050 pub params: Vec<(String, Vec<f64>)>,
1052}
1053
1054impl ParameterSweep {
1055 pub fn new() -> Self {
1057 Self::default()
1058 }
1059
1060 pub fn add_param(&mut self, name: impl Into<String>, values: Vec<f64>) {
1062 self.params.push((name.into(), values));
1063 }
1064
1065 pub fn count(&self) -> usize {
1067 if self.params.is_empty() {
1068 return 0;
1069 }
1070 self.params.iter().map(|(_, v)| v.len()).product()
1071 }
1072
1073 pub fn cartesian_product(&self) -> Vec<HashMap<String, f64>> {
1075 if self.params.is_empty() {
1076 return vec![];
1077 }
1078 let mut result: Vec<HashMap<String, f64>> = vec![HashMap::new()];
1079 for (name, values) in &self.params {
1080 let mut next: Vec<HashMap<String, f64>> = Vec::new();
1081 for existing in &result {
1082 for &v in values {
1083 let mut map = existing.clone();
1084 map.insert(name.clone(), v);
1085 next.push(map);
1086 }
1087 }
1088 result = next;
1089 }
1090 result
1091 }
1092
1093 pub fn latin_hypercube_sample(&self, n: usize) -> Vec<HashMap<String, f64>> {
1095 if n == 0 || self.params.is_empty() {
1096 return vec![];
1097 }
1098 let mut samples: Vec<HashMap<String, f64>> = (0..n).map(|_| HashMap::new()).collect();
1099 for (dim, (name, values)) in self.params.iter().enumerate() {
1100 let k = values.len();
1101 if k == 0 {
1102 continue;
1103 }
1104 let mut perm: Vec<usize> = (0..n).collect();
1105 let seed: usize = dim
1106 .wrapping_mul(6_364_136_223_846_793_005)
1107 .wrapping_add(1_442_695_040_888_963_407);
1108 for i in (1..n).rev() {
1109 let j = seed.wrapping_mul(i).wrapping_add(dim) % (i + 1);
1110 perm.swap(i, j);
1111 }
1112 for (i, map) in samples.iter_mut().enumerate() {
1113 let idx = ((perm[i] * k) / n).min(k - 1);
1114 let t = (perm[i] as f64 + 0.5) / n as f64;
1115 let lo = values[idx];
1116 let hi = if idx + 1 < k { values[idx + 1] } else { lo };
1117 let local_t = (t * n as f64 - perm[i] as f64).clamp(0.0, 1.0);
1118 let v = lo + local_t * (hi - lo);
1119 map.insert(name.clone(), v);
1120 }
1121 }
1122 samples
1123 }
1124}
1125
1126#[derive(Debug, Default)]
1133pub struct QueryBuilder<'a> {
1134 db: Option<&'a SimulationDatabase>,
1135 param_filters: Vec<(String, f64, f64)>,
1136 time_range: Option<(u64, u64)>,
1137 meta_filter: Option<(String, String)>,
1138}
1139
1140impl<'a> QueryBuilder<'a> {
1141 pub fn from(db: &'a SimulationDatabase) -> Self {
1143 Self {
1144 db: Some(db),
1145 ..Self::default()
1146 }
1147 }
1148
1149 pub fn param_range(mut self, name: impl Into<String>, lo: f64, hi: f64) -> Self {
1151 self.param_filters.push((name.into(), lo, hi));
1152 self
1153 }
1154
1155 pub fn time_range(mut self, t_lo: u64, t_hi: u64) -> Self {
1157 self.time_range = Some((t_lo, t_hi));
1158 self
1159 }
1160
1161 pub fn meta_eq(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1163 self.meta_filter = Some((key.into(), value.into()));
1164 self
1165 }
1166
1167 pub fn execute(&self) -> Vec<&SimulationRecord> {
1169 let db = match self.db {
1170 Some(d) => d,
1171 None => return vec![],
1172 };
1173 db.records
1174 .iter()
1175 .filter(|r| {
1176 for (name, lo, hi) in &self.param_filters {
1178 match r.parameters.get(name.as_str()) {
1179 Some(&v) if v >= *lo && v <= *hi => {}
1180 _ => return false,
1181 }
1182 }
1183 if let Some((t_lo, t_hi)) = self.time_range
1185 && (r.timestamp < t_lo || r.timestamp > t_hi)
1186 {
1187 return false;
1188 }
1189 if let Some((k, v)) = &self.meta_filter {
1191 match r.metadata.get(k.as_str()) {
1192 Some(val) if val == v => {}
1193 _ => return false,
1194 }
1195 }
1196 true
1197 })
1198 .collect()
1199 }
1200}
1201
1202#[cfg(test)]
1207mod tests {
1208 use super::*;
1209
1210 #[test]
1213 fn test_record_new() {
1214 let r = SimulationRecord::new("run-001", 1_700_000_000);
1215 assert_eq!(r.id, "run-001");
1216 assert_eq!(r.timestamp, 1_700_000_000);
1217 assert!(r.parameters.is_empty());
1218 assert!(r.metadata.is_empty());
1219 }
1220
1221 #[test]
1222 fn test_record_set_param() {
1223 let mut r = SimulationRecord::new("r1", 0);
1224 r.set_param("dt", 0.01);
1225 assert_eq!(r.parameters["dt"], 0.01);
1226 }
1227
1228 #[test]
1229 fn test_record_set_meta() {
1230 let mut r = SimulationRecord::new("r2", 0);
1231 r.set_meta("solver", "rk4");
1232 assert_eq!(r.metadata["solver"], "rk4");
1233 }
1234
1235 #[test]
1238 fn test_db_new_empty() {
1239 let db = SimulationDatabase::new("/tmp/test.csv");
1240 assert!(db.records.is_empty());
1241 assert_eq!(db.file_path, "/tmp/test.csv");
1242 }
1243
1244 #[test]
1245 fn test_db_add_and_find() {
1246 let mut db = SimulationDatabase::new("/tmp/test.csv");
1247 let r = SimulationRecord::new("abc", 42);
1248 db.add_record(r);
1249 assert!(db.find_by_id("abc").is_some());
1250 assert!(db.find_by_id("xyz").is_none());
1251 }
1252
1253 #[test]
1254 fn test_db_delete_by_id() {
1255 let mut db = SimulationDatabase::new("/tmp/test.csv");
1256 db.add_record(SimulationRecord::new("to_delete", 0));
1257 db.add_record(SimulationRecord::new("keep", 0));
1258 let removed = db.delete_by_id("to_delete");
1259 assert_eq!(removed, 1);
1260 assert!(db.find_by_id("to_delete").is_none());
1261 assert!(db.find_by_id("keep").is_some());
1262 }
1263
1264 #[test]
1265 fn test_db_query_range_basic() {
1266 let mut db = SimulationDatabase::new("/tmp/test.csv");
1267 let mut r1 = SimulationRecord::new("a", 0);
1268 r1.set_param("dt", 0.01);
1269 let mut r2 = SimulationRecord::new("b", 0);
1270 r2.set_param("dt", 0.1);
1271 let mut r3 = SimulationRecord::new("c", 0);
1272 r3.set_param("dt", 1.0);
1273 db.add_record(r1);
1274 db.add_record(r2);
1275 db.add_record(r3);
1276 let found = db.query_range("dt", 0.005, 0.2);
1277 assert_eq!(found.len(), 2);
1278 }
1279
1280 #[test]
1281 fn test_db_query_time_range() {
1282 let mut db = SimulationDatabase::new("/tmp/t.csv");
1283 for i in 0_u64..5 {
1284 db.add_record(SimulationRecord::new(format!("r{i}"), 1000 + i * 100));
1285 }
1286 let found = db.query_time_range(1100, 1300);
1287 assert_eq!(found.len(), 3); }
1289
1290 #[test]
1291 fn test_db_save_and_load_roundtrip() {
1292 let mut db = SimulationDatabase::new("/tmp/rt.csv");
1293 let mut r = SimulationRecord::new("run42", 999);
1294 r.set_param("Re", 1000.0);
1295 r.set_meta("solver", "rk4");
1296 db.add_record(r);
1297 let csv = db.save_to_csv();
1298 let mut db2 = SimulationDatabase::new("/tmp/rt.csv");
1299 db2.load_from_csv(&csv);
1300 assert_eq!(db2.records.len(), 1);
1301 assert_eq!(db2.records[0].id, "run42");
1302 assert_eq!(db2.records[0].timestamp, 999);
1303 assert!((db2.records[0].parameters["Re"] - 1000.0).abs() < 1e-9);
1304 }
1305
1306 #[test]
1307 fn test_db_statistics_basic() {
1308 let mut db = SimulationDatabase::new("/tmp/s.csv");
1309 for (i, v) in [1.0_f64, 2.0, 3.0, 4.0, 5.0].iter().enumerate() {
1310 let mut r = SimulationRecord::new(format!("r{i}"), 0);
1311 r.set_param("x", *v);
1312 db.add_record(r);
1313 }
1314 let (min, max, mean) = db.statistics("x");
1315 assert!((min - 1.0).abs() < 1e-9);
1316 assert!((max - 5.0).abs() < 1e-9);
1317 assert!((mean - 3.0).abs() < 1e-9);
1318 }
1319
1320 #[test]
1321 fn test_db_export_json_contains_id() {
1322 let mut db = SimulationDatabase::new("/tmp/t.csv");
1323 db.add_record(SimulationRecord::new("sim-1", 0));
1324 let json = db.export_json();
1325 assert!(json.contains("\"sim-1\""));
1326 }
1327
1328 #[test]
1331 fn test_rle_encode_all_same() {
1332 let data = vec![3.125; 100];
1333 let enc = rle_encode(&data);
1334 assert_eq!(enc.len(), 1);
1335 assert_eq!(enc[0].1, 100);
1336 }
1337
1338 #[test]
1339 fn test_rle_roundtrip() {
1340 let data = vec![1.0, 1.0, 2.0, 3.0, 3.0, 3.0, 4.0];
1341 let enc = rle_encode(&data);
1342 let dec = rle_decode(&enc);
1343 assert_eq!(dec, data);
1344 }
1345
1346 #[test]
1347 fn test_rle_empty() {
1348 let enc = rle_encode(&[]);
1349 assert!(enc.is_empty());
1350 let dec = rle_decode(&[]);
1351 assert!(dec.is_empty());
1352 }
1353
1354 #[test]
1355 fn test_rle_compression_ratio() {
1356 let data = vec![0.0_f64; 50];
1357 let enc = rle_encode(&data);
1358 let ratio = rle_compression_ratio(data.len(), &enc);
1359 assert!(ratio > 1.0);
1360 }
1361
1362 #[test]
1363 fn test_rle_no_compression_all_different() {
1364 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
1365 let enc = rle_encode(&data);
1366 let ratio = rle_compression_ratio(data.len(), &enc);
1367 assert!((ratio - 1.0).abs() < 1e-9);
1369 }
1370
1371 #[test]
1374 fn test_snapshot_insert_sorted() {
1375 let mut table = SnapshotTable::new();
1376 table.insert(Snapshot::new(3.0));
1377 table.insert(Snapshot::new(1.0));
1378 table.insert(Snapshot::new(2.0));
1379 let times: Vec<f64> = table.snapshots.iter().map(|s| s.time).collect();
1380 assert_eq!(times, vec![1.0, 2.0, 3.0]);
1381 }
1382
1383 #[test]
1384 fn test_snapshot_query_time_range() {
1385 let mut table = SnapshotTable::new();
1386 for t in [0.0, 1.0, 2.0, 3.0, 4.0] {
1387 table.insert(Snapshot::new(t));
1388 }
1389 let found = table.query_time_range(1.0, 3.0);
1390 assert_eq!(found.len(), 3);
1391 }
1392
1393 #[test]
1394 fn test_snapshot_nearest() {
1395 let mut table = SnapshotTable::new();
1396 for t in [0.0, 1.0, 2.0] {
1397 table.insert(Snapshot::new(t));
1398 }
1399 let near = table.nearest(1.4).unwrap();
1400 assert!((near.time - 1.0).abs() < 1e-9);
1401 }
1402
1403 #[test]
1404 fn test_snapshot_compress_decompress() {
1405 let mut table = SnapshotTable::new();
1406 let mut snap = Snapshot::new(0.0);
1407 snap.set_field("pressure", vec![1.0, 1.0, 1.0, 2.0]);
1408 table.insert(snap);
1409 table.compress_field(0, "pressure");
1410 let dec = table.decompress_field(0, "pressure").unwrap();
1411 assert_eq!(dec, vec![1.0, 1.0, 1.0, 2.0]);
1412 }
1413
1414 #[test]
1415 fn test_snapshot_diff_changes_counted() {
1416 let mut table = SnapshotTable::new();
1417 let mut s0 = Snapshot::new(0.0);
1418 s0.set_field("u", vec![1.0, 2.0, 3.0]);
1419 let mut s1 = Snapshot::new(1.0);
1420 s1.set_field("u", vec![1.0, 9.0, 3.0]); table.insert(s0);
1422 table.insert(s1);
1423 let changed = table.compute_diff("u", 1);
1424 assert_eq!(changed, 1);
1425 assert_eq!(table.diffs[0].indices, vec![1]);
1426 assert!((table.diffs[0].new_values[0] - 9.0).abs() < 1e-9);
1427 }
1428
1429 #[test]
1430 fn test_snapshot_apply_diff() {
1431 let diff = DiffEntry {
1432 time: 1.0,
1433 field: "u".to_string(),
1434 indices: vec![0, 2],
1435 new_values: vec![10.0, 30.0],
1436 };
1437 let mut base = vec![1.0, 2.0, 3.0];
1438 SnapshotTable::apply_diff(&mut base, &diff);
1439 assert!((base[0] - 10.0).abs() < 1e-9);
1440 assert!((base[1] - 2.0).abs() < 1e-9);
1441 assert!((base[2] - 30.0).abs() < 1e-9);
1442 }
1443
1444 #[test]
1447 fn test_event_log_basic() {
1448 let mut log = EventLog::new();
1449 log.info(1.0, "solver", "step started");
1450 assert_eq!(log.len(), 1);
1451 assert!(!log.is_empty());
1452 }
1453
1454 #[test]
1455 fn test_event_log_filter_level() {
1456 let mut log = EventLog::new();
1457 log.info(0.0, "a", "msg");
1458 log.warn(1.0, "b", "warn");
1459 log.error(2.0, "c", "err");
1460 let warns_and_above = log.filter_level(EventLevel::Warning);
1461 assert_eq!(warns_and_above.len(), 2);
1462 }
1463
1464 #[test]
1465 fn test_event_log_filter_category() {
1466 let mut log = EventLog::new();
1467 log.info(0.0, "io", "read file");
1468 log.info(0.5, "solver", "step 1");
1469 log.info(1.0, "io", "write file");
1470 let io_events = log.filter_category("io");
1471 assert_eq!(io_events.len(), 2);
1472 }
1473
1474 #[test]
1475 fn test_event_log_filter_sim_time() {
1476 let mut log = EventLog::new();
1477 for t in [0.0, 1.0, 2.0, 3.0, 4.0_f64] {
1478 log.info(t, "x", "msg");
1479 }
1480 let found = log.filter_sim_time(1.0, 3.0);
1481 assert_eq!(found.len(), 3);
1482 }
1483
1484 #[test]
1485 fn test_event_log_to_csv() {
1486 let mut log = EventLog::new();
1487 log.info(1.0, "solver", "done");
1488 let csv = log.to_csv();
1489 assert!(csv.contains("INFO"));
1490 assert!(csv.contains("solver"));
1491 assert!(csv.contains("done"));
1492 }
1493
1494 #[test]
1495 fn test_event_level_ordering() {
1496 assert!(EventLevel::Critical > EventLevel::Error);
1497 assert!(EventLevel::Error > EventLevel::Warning);
1498 assert!(EventLevel::Warning > EventLevel::Info);
1499 assert!(EventLevel::Info > EventLevel::Debug);
1500 }
1501
1502 #[test]
1505 fn test_agg_stats_basic() {
1506 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1507 let stats = AggStats::from_slice(&data).unwrap();
1508 assert!((stats.min - 1.0).abs() < 1e-9);
1509 assert!((stats.max - 5.0).abs() < 1e-9);
1510 assert!((stats.mean - 3.0).abs() < 1e-9);
1511 assert!((stats.std - 2_f64.sqrt()).abs() < 1e-9);
1513 }
1514
1515 #[test]
1516 fn test_agg_stats_empty() {
1517 assert!(AggStats::from_slice(&[]).is_none());
1518 }
1519
1520 #[test]
1521 fn test_result_aggregator_push_compute() {
1522 let mut agg = ResultAggregator::new();
1523 agg.push(10.0);
1524 agg.push(20.0);
1525 agg.push(30.0);
1526 let stats = agg.compute().unwrap();
1527 assert!((stats.mean - 20.0).abs() < 1e-9);
1528 }
1529
1530 #[test]
1531 fn test_result_aggregator_add_snapshot_mean() {
1532 let mut agg = ResultAggregator::new();
1533 let mut snap = Snapshot::new(0.0);
1534 snap.set_field("v", vec![2.0, 4.0, 6.0]);
1535 agg.add_snapshot_mean(&snap, "v");
1536 let stats = agg.compute().unwrap();
1537 assert!((stats.mean - 4.0).abs() < 1e-9);
1538 }
1539
1540 #[test]
1541 fn test_result_aggregator_reset() {
1542 let mut agg = ResultAggregator::new();
1543 agg.push(1.0);
1544 agg.reset();
1545 assert!(agg.compute().is_none());
1546 }
1547
1548 #[test]
1551 fn test_metadata_store_set_get() {
1552 let mut store = MetadataStore::new();
1553 store.set("git_hash", "abc123");
1554 assert_eq!(store.get("git_hash"), Some("abc123"));
1555 assert_eq!(store.get("missing"), None);
1556 }
1557
1558 #[test]
1559 fn test_metadata_store_roundtrip() {
1560 let mut store = MetadataStore::new();
1561 store.set("a", "1");
1562 store.set("b", "hello world");
1563 let props = store.to_properties();
1564 let loaded = MetadataStore::from_properties(&props);
1565 assert_eq!(loaded.get("a"), Some("1"));
1566 assert_eq!(loaded.get("b"), Some("hello world"));
1567 }
1568
1569 #[test]
1570 fn test_metadata_store_merge() {
1571 let mut a = MetadataStore::new();
1572 a.set("x", "1");
1573 let mut b = MetadataStore::new();
1574 b.set("y", "2");
1575 b.set("x", "overwritten");
1576 a.merge(&b);
1577 assert_eq!(a.get("x"), Some("overwritten"));
1578 assert_eq!(a.get("y"), Some("2"));
1579 }
1580
1581 #[test]
1584 fn test_prov_add_computation() {
1585 let mut tracker = ProvenanceTracker::new();
1586 tracker.add_computation("comp1", vec!["data_in".into()], vec!["data_out".into()]);
1587 assert_eq!(tracker.graph.len(), 1);
1588 assert_eq!(tracker.graph[0].type_, ProvenanceType::Computation);
1589 }
1590
1591 #[test]
1592 fn test_prov_trace_lineage_chain() {
1593 let mut tracker = ProvenanceTracker::new();
1594 tracker.add_data_source("raw", vec!["raw".into()]);
1595 tracker.add_computation("step1", vec!["raw".into()], vec!["processed".into()]);
1596 tracker.add_computation("step2", vec!["processed".into()], vec!["result".into()]);
1597 let lineage = tracker.trace_lineage("result");
1598 assert!(lineage.contains(&"result".to_string()));
1599 assert!(lineage.contains(&"step2".to_string()));
1600 }
1601
1602 #[test]
1605 fn test_checkpoint_save_and_load() {
1606 let mut mgr = CheckpointManager::new("/tmp/checkpoints", 5);
1607 mgr.save_checkpoint(0, &[1.0, 2.0, 3.0]);
1608 let data = mgr.load_checkpoint(0).unwrap();
1609 assert_eq!(data, vec![1.0, 2.0, 3.0]);
1610 }
1611
1612 #[test]
1613 fn test_checkpoint_cleanup_old() {
1614 let mut mgr = CheckpointManager::new("/tmp/ckpt", 3);
1615 for step in 0..6_usize {
1616 mgr.save_checkpoint(step, &[step as f64]);
1617 }
1618 assert!(mgr.list_checkpoints().len() <= 3);
1619 }
1620
1621 #[test]
1624 fn test_sweep_cartesian_product() {
1625 let mut sweep = ParameterSweep::new();
1626 sweep.add_param("dt", vec![0.01, 0.1]);
1627 sweep.add_param("Re", vec![100.0, 500.0, 1000.0]);
1628 assert_eq!(sweep.count(), 6);
1629 let product = sweep.cartesian_product();
1630 assert_eq!(product.len(), 6);
1631 }
1632
1633 #[test]
1634 fn test_sweep_latin_hypercube() {
1635 let mut sweep = ParameterSweep::new();
1636 sweep.add_param("x", vec![0.0, 1.0, 2.0, 3.0]);
1637 sweep.add_param("y", vec![10.0, 20.0, 30.0]);
1638 let samples = sweep.latin_hypercube_sample(5);
1639 assert_eq!(samples.len(), 5);
1640 }
1641
1642 #[test]
1645 fn test_query_builder_param_range() {
1646 let mut db = SimulationDatabase::new("/tmp/q.csv");
1647 for i in 0..5_usize {
1648 let mut r = SimulationRecord::new(format!("r{i}"), 0);
1649 r.set_param("v", i as f64);
1650 db.add_record(r);
1651 }
1652 let binding = QueryBuilder::from(&db).param_range("v", 1.0, 3.0);
1653 let results = binding.execute();
1654 assert_eq!(results.len(), 3);
1655 }
1656
1657 #[test]
1658 fn test_query_builder_time_range() {
1659 let mut db = SimulationDatabase::new("/tmp/q.csv");
1660 for i in 0_u64..5 {
1661 db.add_record(SimulationRecord::new(format!("t{i}"), 1000 + i * 100));
1662 }
1663 let binding = QueryBuilder::from(&db).time_range(1100, 1300);
1664 let results = binding.execute();
1665 assert_eq!(results.len(), 3);
1666 }
1667
1668 #[test]
1669 fn test_query_builder_meta_eq() {
1670 let mut db = SimulationDatabase::new("/tmp/q.csv");
1671 let mut r1 = SimulationRecord::new("a", 0);
1672 r1.set_meta("solver", "rk4");
1673 let mut r2 = SimulationRecord::new("b", 0);
1674 r2.set_meta("solver", "euler");
1675 db.add_record(r1);
1676 db.add_record(r2);
1677 let binding = QueryBuilder::from(&db).meta_eq("solver", "rk4");
1678 let results = binding.execute();
1679 assert_eq!(results.len(), 1);
1680 assert_eq!(results[0].id, "a");
1681 }
1682
1683 #[test]
1684 fn test_query_builder_combined_filters() {
1685 let mut db = SimulationDatabase::new("/tmp/q.csv");
1686 for i in 0_u64..6 {
1687 let mut r = SimulationRecord::new(format!("r{i}"), 1000 + i * 100);
1688 r.set_param("Re", (i as f64) * 100.0);
1689 r.set_meta("solver", if i % 2 == 0 { "rk4" } else { "euler" });
1690 db.add_record(r);
1691 }
1692 let binding = QueryBuilder::from(&db)
1693 .param_range("Re", 100.0, 400.0)
1694 .meta_eq("solver", "euler");
1695 let results = binding.execute();
1696 assert_eq!(results.len(), 2);
1699 }
1700}