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