1#[allow(unused_imports)]
6use super::functions::*;
7use std::collections::{HashMap, VecDeque};
8
9#[derive(Debug, Clone, Default)]
11pub struct DbRow {
12 pub columns: HashMap<String, DbValue>,
14}
15impl DbRow {
16 pub fn new() -> Self {
18 Self::default()
19 }
20 pub fn set(&mut self, key: impl Into<String>, val: impl Into<DbValue>) {
22 self.columns.insert(key.into(), val.into());
23 }
24 pub fn get(&self, key: &str) -> Option<&DbValue> {
26 self.columns.get(key)
27 }
28}
29#[derive(Debug, Clone)]
31pub struct SimulationMetadata {
32 pub sim_id: String,
34 pub description: String,
36 pub created_at: f64,
38 pub parameters: HashMap<String, String>,
40 pub artifacts: Vec<String>,
42}
43impl SimulationMetadata {
44 pub fn new(sim_id: impl Into<String>, description: impl Into<String>, created_at: f64) -> Self {
46 Self {
47 sim_id: sim_id.into(),
48 description: description.into(),
49 created_at,
50 parameters: HashMap::new(),
51 artifacts: Vec::new(),
52 }
53 }
54 pub fn set_param(&mut self, key: impl Into<String>, val: impl Into<String>) {
56 self.parameters.insert(key.into(), val.into());
57 }
58 pub fn add_artifact(&mut self, path: impl Into<String>) {
60 self.artifacts.push(path.into());
61 }
62}
63#[derive(Debug, Default)]
65pub struct DataCatalog {
66 pub(super) entries: HashMap<String, SimulationMetadata>,
67}
68impl DataCatalog {
69 pub fn new() -> Self {
71 Self::default()
72 }
73 pub fn register(&mut self, meta: SimulationMetadata) {
75 self.entries.insert(meta.sim_id.clone(), meta);
76 }
77 pub fn lookup(&self, sim_id: &str) -> Option<&SimulationMetadata> {
79 self.entries.get(sim_id)
80 }
81 pub fn lookup_mut(&mut self, sim_id: &str) -> Option<&mut SimulationMetadata> {
83 self.entries.get_mut(sim_id)
84 }
85 pub fn remove(&mut self, sim_id: &str) -> bool {
89 self.entries.remove(sim_id).is_some()
90 }
91 pub fn search_description(&self, query: &str) -> Vec<&SimulationMetadata> {
93 let q = query.to_lowercase();
94 self.entries
95 .values()
96 .filter(|m| m.description.to_lowercase().contains(&q))
97 .collect()
98 }
99 pub fn all_ids(&self) -> Vec<&str> {
101 self.entries.keys().map(|s| s.as_str()).collect()
102 }
103 pub fn len(&self) -> usize {
105 self.entries.len()
106 }
107 pub fn is_empty(&self) -> bool {
109 self.entries.is_empty()
110 }
111}
112#[derive(Debug, Default)]
114pub struct TimeSeriesStore {
115 pub(super) samples: Vec<TsSample>,
117 pub label: String,
119}
120impl TimeSeriesStore {
121 pub fn new(label: impl Into<String>) -> Self {
123 Self {
124 samples: Vec::new(),
125 label: label.into(),
126 }
127 }
128 pub fn append(&mut self, time: f64, value: f64) {
130 self.samples.push(TsSample::new(time, value));
131 }
132 pub fn len(&self) -> usize {
134 self.samples.len()
135 }
136 pub fn is_empty(&self) -> bool {
138 self.samples.is_empty()
139 }
140 pub fn range_query(&self, t_start: f64, t_end: f64) -> Vec<&TsSample> {
142 self.samples
143 .iter()
144 .filter(|s| s.time >= t_start && s.time <= t_end)
145 .collect()
146 }
147 pub fn decimate(&self, n: usize) -> TimeSeriesStore {
151 let mut out = TimeSeriesStore::new(format!("{}_dec{n}", self.label));
152 if n == 0 {
153 return out;
154 }
155 for (i, s) in self.samples.iter().enumerate() {
156 if i % n == 0 {
157 out.append(s.time, s.value);
158 }
159 }
160 out
161 }
162 pub fn mean_in_range(&self, t_start: f64, t_end: f64) -> Option<f64> {
164 let vals: Vec<f64> = self
165 .range_query(t_start, t_end)
166 .iter()
167 .map(|s| s.value)
168 .collect();
169 if vals.is_empty() {
170 None
171 } else {
172 Some(vals.iter().sum::<f64>() / vals.len() as f64)
173 }
174 }
175 pub fn max_value(&self) -> Option<f64> {
177 self.samples.iter().map(|s| s.value).reduce(f64::max)
178 }
179 pub fn min_value(&self) -> Option<f64> {
181 self.samples.iter().map(|s| s.value).reduce(f64::min)
182 }
183 pub fn to_csv(&self) -> String {
185 let mut out = String::from("time,value\n");
186 for s in &self.samples {
187 out.push_str(&format!("{},{}\n", s.time, s.value));
188 }
189 out
190 }
191 pub fn interpolate(&self, t: f64) -> Option<f64> {
195 if self.samples.is_empty() {
196 return None;
197 }
198 let pos = self.samples.partition_point(|s| s.time <= t);
199 if pos == 0 {
200 return Some(self.samples[0].value);
201 }
202 if pos >= self.samples.len() {
203 return Some(
204 self.samples
205 .last()
206 .expect("collection should not be empty")
207 .value,
208 );
209 }
210 let lo = &self.samples[pos - 1];
211 let hi = &self.samples[pos];
212 let dt = hi.time - lo.time;
213 if dt < 1e-15 {
214 return Some(lo.value);
215 }
216 let frac = (t - lo.time) / dt;
217 Some(lo.value + frac * (hi.value - lo.value))
218 }
219}
220#[derive(Debug, Default)]
225pub struct SnapshotCatalog {
226 pub(super) snapshots: HashMap<String, SnapshotEntry>,
227 pub(super) next_id: u64,
229}
230impl SnapshotCatalog {
231 pub fn new() -> Self {
233 Self::default()
234 }
235 pub fn register(&mut self, entry: SnapshotEntry) -> &str {
237 let id = entry.id.clone();
238 self.snapshots.insert(id.clone(), entry);
239 self.snapshots[&id].id.as_str()
240 }
241 pub fn register_auto(&mut self, sim_time: f64, path: impl Into<String>) -> String {
245 let id = format!("snap_{:06}", self.next_id);
246 self.next_id += 1;
247 self.register(SnapshotEntry::new(id.clone(), sim_time, path));
248 id
249 }
250 pub fn get(&self, id: &str) -> Option<&SnapshotEntry> {
252 self.snapshots.get(id)
253 }
254 pub fn get_mut(&mut self, id: &str) -> Option<&mut SnapshotEntry> {
256 self.snapshots.get_mut(id)
257 }
258 pub fn remove(&mut self, id: &str) -> bool {
260 self.snapshots.remove(id).is_some()
261 }
262 pub fn len(&self) -> usize {
264 self.snapshots.len()
265 }
266 pub fn is_empty(&self) -> bool {
268 self.snapshots.is_empty()
269 }
270 pub fn range_query(&self, t_start: f64, t_end: f64) -> Vec<&SnapshotEntry> {
272 self.snapshots
273 .values()
274 .filter(|s| s.sim_time >= t_start && s.sim_time <= t_end)
275 .collect()
276 }
277 pub fn query_by_tag(&self, tag: &str) -> Vec<&SnapshotEntry> {
279 self.snapshots
280 .values()
281 .filter(|s| s.tags.iter().any(|t| t == tag))
282 .collect()
283 }
284 pub fn unloaded_ids(&self) -> Vec<&str> {
286 self.snapshots
287 .values()
288 .filter(|s| !s.loaded)
289 .map(|s| s.id.as_str())
290 .collect()
291 }
292 pub fn mark_range_loaded(&mut self, t_start: f64, t_end: f64) {
294 for s in self.snapshots.values_mut() {
295 if s.sim_time >= t_start && s.sim_time <= t_end {
296 s.loaded = true;
297 }
298 }
299 }
300 pub fn total_file_size(&self) -> usize {
302 self.snapshots.values().map(|s| s.file_size).sum()
303 }
304 pub fn sorted_by_time(&self) -> Vec<&SnapshotEntry> {
306 let mut v: Vec<&SnapshotEntry> = self.snapshots.values().collect();
307 v.sort_by(|a, b| {
308 a.sim_time
309 .partial_cmp(&b.sim_time)
310 .unwrap_or(std::cmp::Ordering::Equal)
311 });
312 v
313 }
314}
315#[derive(Debug, Clone)]
317pub struct CacheEntry {
318 pub key: String,
320 pub data: Vec<f64>,
322 pub sim_time: f64,
324 pub version: u64,
326}
327impl CacheEntry {
328 pub fn new(key: impl Into<String>, data: Vec<f64>, sim_time: f64, version: u64) -> Self {
330 Self {
331 key: key.into(),
332 data,
333 sim_time,
334 version,
335 }
336 }
337}
338#[derive(Debug, Clone)]
342pub struct SimulationRecord {
343 pub name: String,
345 pub timestamp: f64,
347 pub params: HashMap<String, String>,
349 pub output_path: Option<String>,
351}
352impl SimulationRecord {
353 pub fn new(name: impl Into<String>, timestamp: f64) -> Self {
355 Self {
356 name: name.into(),
357 timestamp,
358 params: HashMap::new(),
359 output_path: None,
360 }
361 }
362 pub fn set_param(&mut self, key: impl Into<String>, val: impl Into<String>) {
364 self.params.insert(key.into(), val.into());
365 }
366 pub fn get_param(&self, key: &str) -> Option<&str> {
368 self.params.get(key).map(|s| s.as_str())
369 }
370 pub fn set_output(&mut self, path: impl Into<String>) {
372 self.output_path = Some(path.into());
373 }
374}
375#[derive(Debug, Clone, Default)]
382pub struct DatabaseSerializer;
383impl DatabaseSerializer {
384 pub fn new() -> Self {
386 Self
387 }
388 pub fn serialize(&self, rec: &SimulationRecord) -> String {
390 let params_str: Vec<String> = rec
391 .params
392 .iter()
393 .map(|(k, v)| format!(r#""{k}":"{v}""#))
394 .collect();
395 let params_json = format!("{{{}}}", params_str.join(","));
396 let output_str = match &rec.output_path {
397 Some(p) => format!(r#""{p}""#),
398 None => "null".to_string(),
399 };
400 format!(
401 r#"{{"name":"{name}","timestamp":{ts},"params":{params},"output":{out}}}"#,
402 name = rec.name,
403 ts = rec.timestamp,
404 params = params_json,
405 out = output_str,
406 )
407 }
408 pub fn serialize_all(&self, recs: &[&SimulationRecord]) -> String {
410 let items: Vec<String> = recs.iter().map(|r| self.serialize(r)).collect();
411 format!("[{}]", items.join(","))
412 }
413 pub fn deserialize(&self, s: &str) -> Option<SimulationRecord> {
418 let name = self.extract_string_field(s, "name")?;
419 let ts_str = self.extract_raw_field(s, "timestamp")?;
420 let timestamp: f64 = ts_str.trim().parse().ok()?;
421 let mut rec = SimulationRecord::new(name, timestamp);
422 let out_raw = self.extract_raw_field(s, "output").unwrap_or_default();
423 let out_raw = out_raw.trim();
424 if out_raw != "null" && out_raw.starts_with('"') {
425 let cleaned = out_raw.trim_matches('"').to_string();
426 rec.set_output(cleaned);
427 }
428 Some(rec)
429 }
430 fn extract_string_field(&self, s: &str, field: &str) -> Option<String> {
431 let key = format!(r#""{field}":""#);
432 let start = s.find(key.as_str())? + key.len();
433 let rest = &s[start..];
434 let end = rest.find('"')?;
435 Some(rest[..end].to_string())
436 }
437 fn extract_raw_field(&self, s: &str, field: &str) -> Option<String> {
438 let key = format!(r#""{field}":"#);
439 let start = s.find(key.as_str())? + key.len();
440 let rest = &s[start..];
441 let end = rest.find([',', '}']).unwrap_or(rest.len());
442 Some(rest[..end].to_string())
443 }
444}
445#[derive(Debug, Clone)]
447pub struct TsSample {
448 pub time: f64,
450 pub value: f64,
452}
453impl TsSample {
454 pub fn new(time: f64, value: f64) -> Self {
456 Self { time, value }
457 }
458}
459#[derive(Debug, Clone)]
461pub struct MaterialRecord {
462 pub name: String,
464 pub density: f64,
466 pub youngs_modulus: f64,
468 pub poisson_ratio: f64,
470 pub thermal_conductivity: f64,
472 pub yield_strength: f64,
474 pub tags: Vec<String>,
476}
477impl MaterialRecord {
478 #[allow(clippy::too_many_arguments)]
480 pub fn new(
481 name: impl Into<String>,
482 density: f64,
483 youngs_modulus: f64,
484 poisson_ratio: f64,
485 thermal_conductivity: f64,
486 yield_strength: f64,
487 tags: Vec<String>,
488 ) -> Self {
489 Self {
490 name: name.into(),
491 density,
492 youngs_modulus,
493 poisson_ratio,
494 thermal_conductivity,
495 yield_strength,
496 tags,
497 }
498 }
499}
500#[derive(Debug, Clone, Default)]
502pub struct ExportFilter {
503 pub columns: Vec<String>,
505 pub min_value: Option<(String, f64)>,
507 pub max_value: Option<(String, f64)>,
509}
510impl ExportFilter {
511 pub fn new() -> Self {
513 Self::default()
514 }
515 pub fn row_passes(&self, row: &DbRow) -> bool {
517 if let Some((col, lo)) = &self.min_value {
518 let v = row
519 .get(col)
520 .and_then(|v| v.as_f64())
521 .unwrap_or(f64::NEG_INFINITY);
522 if v < *lo {
523 return false;
524 }
525 }
526 if let Some((col, hi)) = &self.max_value {
527 let v = row
528 .get(col)
529 .and_then(|v| v.as_f64())
530 .unwrap_or(f64::INFINITY);
531 if v > *hi {
532 return false;
533 }
534 }
535 true
536 }
537}
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540pub enum ExportFormat {
541 Csv,
543 Json,
545 Hdf5Text,
547}
548#[derive(Debug)]
550pub struct ExportPipeline {
551 pub format: ExportFormat,
553 pub filter: ExportFilter,
555}
556impl ExportPipeline {
557 pub fn new(format: ExportFormat) -> Self {
559 Self {
560 format,
561 filter: ExportFilter::new(),
562 }
563 }
564 pub fn with_filter(mut self, filter: ExportFilter) -> Self {
566 self.filter = filter;
567 self
568 }
569 pub fn export(&self, db: &SimulationDatabase) -> String {
571 let cols = &self.filter.columns;
572 let rows: Vec<&DbRow> = db
573 .rows
574 .iter()
575 .filter(|r| self.filter.row_passes(r))
576 .collect();
577 match self.format {
578 ExportFormat::Csv => self.to_csv(&rows, cols),
579 ExportFormat::Json => self.to_json(&rows, cols),
580 ExportFormat::Hdf5Text => self.to_hdf5_text(&rows, cols, &db.name),
581 }
582 }
583 fn effective_cols(&self, rows: &[&DbRow], cols: &[String]) -> Vec<String> {
585 if cols.is_empty() {
586 let mut seen: Vec<String> = Vec::new();
587 for row in rows {
588 for k in row.columns.keys() {
589 if !seen.contains(k) {
590 seen.push(k.clone());
591 }
592 }
593 }
594 seen.sort();
595 seen
596 } else {
597 cols.to_vec()
598 }
599 }
600 fn value_to_str(v: &DbValue) -> String {
601 match v {
602 DbValue::Int(i) => i.to_string(),
603 DbValue::Float(f) => format!("{f}"),
604 DbValue::Text(s) => s.clone(),
605 DbValue::Bool(b) => b.to_string(),
606 DbValue::Null => "".to_string(),
607 }
608 }
609 fn to_csv(&self, rows: &[&DbRow], cols: &[String]) -> String {
610 let headers = self.effective_cols(rows, cols);
611 let mut out = headers.join(",");
612 out.push('\n');
613 for row in rows {
614 let line: Vec<String> = headers
615 .iter()
616 .map(|c| row.get(c).map(Self::value_to_str).unwrap_or_default())
617 .collect();
618 out.push_str(&line.join(","));
619 out.push('\n');
620 }
621 out
622 }
623 fn to_json(&self, rows: &[&DbRow], cols: &[String]) -> String {
624 let headers = self.effective_cols(rows, cols);
625 let mut out = String::from("[\n");
626 for (ri, row) in rows.iter().enumerate() {
627 out.push_str(" {");
628 let fields: Vec<String> = headers
629 .iter()
630 .map(|c| {
631 let val = row
632 .get(c)
633 .map(|v| match v {
634 DbValue::Text(s) => format!(r#""{s}""#),
635 DbValue::Null => "null".to_string(),
636 other => Self::value_to_str(other),
637 })
638 .unwrap_or_else(|| "null".to_string());
639 format!(r#""{c}":{val}"#)
640 })
641 .collect();
642 out.push_str(&fields.join(","));
643 out.push('}');
644 if ri + 1 < rows.len() {
645 out.push(',');
646 }
647 out.push('\n');
648 }
649 out.push(']');
650 out
651 }
652 fn to_hdf5_text(&self, rows: &[&DbRow], cols: &[String], table_name: &str) -> String {
653 let headers = self.effective_cols(rows, cols);
654 let mut out = format!("# HDF5-like text dump of table '{table_name}'\n");
655 out.push_str(&format!("# columns: {}\n", headers.join(",")));
656 out.push_str(&format!("# rows: {}\n", rows.len()));
657 for row in rows {
658 let line: Vec<String> = headers
659 .iter()
660 .map(|c| row.get(c).map(Self::value_to_str).unwrap_or_default())
661 .collect();
662 out.push_str(&line.join(" "));
663 out.push('\n');
664 }
665 out
666 }
667}
668#[derive(Debug, Default)]
670pub struct MaterialDatabase {
671 pub(super) records: Vec<MaterialRecord>,
672}
673impl MaterialDatabase {
674 pub fn new() -> Self {
676 Self::default()
677 }
678 pub fn with_defaults() -> Self {
680 let mut db = Self::new();
681 db.insert(MaterialRecord::new(
682 "steel_1020",
683 7850.0,
684 210e9,
685 0.29,
686 50.0,
687 250e6,
688 vec!["metal".into(), "steel".into(), "ferrous".into()],
689 ));
690 db.insert(MaterialRecord::new(
691 "aluminium_6061",
692 2700.0,
693 69e9,
694 0.33,
695 167.0,
696 276e6,
697 vec!["metal".into(), "aluminium".into(), "light".into()],
698 ));
699 db.insert(MaterialRecord::new(
700 "copper",
701 8960.0,
702 110e9,
703 0.34,
704 385.0,
705 70e6,
706 vec!["metal".into(), "copper".into(), "conductor".into()],
707 ));
708 db.insert(MaterialRecord::new(
709 "polycarbonate",
710 1200.0,
711 2.4e9,
712 0.37,
713 0.2,
714 60e6,
715 vec!["polymer".into(), "plastic".into(), "transparent".into()],
716 ));
717 db.insert(MaterialRecord::new(
718 "concrete",
719 2300.0,
720 30e9,
721 0.20,
722 1.7,
723 3e6,
724 vec!["composite".into(), "concrete".into(), "brittle".into()],
725 ));
726 db
727 }
728 pub fn insert(&mut self, record: MaterialRecord) {
730 self.records.push(record);
731 }
732 pub fn lookup(&self, name: &str) -> Option<&MaterialRecord> {
734 self.records.iter().find(|r| r.name == name)
735 }
736 pub fn fuzzy_search(&self, query: &str) -> Vec<&MaterialRecord> {
739 let q = query.to_lowercase();
740 self.records
741 .iter()
742 .filter(|r| {
743 r.name.to_lowercase().contains(&q)
744 || r.tags.iter().any(|t| t.to_lowercase().contains(&q))
745 })
746 .collect()
747 }
748 pub fn interpolate(&self, name_a: &str, name_b: &str, t: f64) -> Option<MaterialRecord> {
755 let a = self.lookup(name_a)?;
756 let b = self.lookup(name_b)?;
757 let lerp = |va: f64, vb: f64| va + t * (vb - va);
758 Some(MaterialRecord::new(
759 format!("{name_a}_to_{name_b}_{t:.2}"),
760 lerp(a.density, b.density),
761 lerp(a.youngs_modulus, b.youngs_modulus),
762 lerp(a.poisson_ratio, b.poisson_ratio),
763 lerp(a.thermal_conductivity, b.thermal_conductivity),
764 lerp(a.yield_strength, b.yield_strength),
765 vec![],
766 ))
767 }
768 pub fn len(&self) -> usize {
770 self.records.len()
771 }
772 pub fn is_empty(&self) -> bool {
774 self.records.is_empty()
775 }
776 pub fn names(&self) -> Vec<&str> {
778 self.records.iter().map(|r| r.name.as_str()).collect()
779 }
780}
781#[derive(Debug)]
786pub struct ResultCache {
787 pub capacity: usize,
789 pub current_version: u64,
791 pub(super) entries: VecDeque<CacheEntry>,
793}
794impl ResultCache {
795 pub fn new(capacity: usize) -> Self {
797 Self {
798 capacity,
799 current_version: 0,
800 entries: VecDeque::new(),
801 }
802 }
803 pub fn put(&mut self, entry: CacheEntry) {
805 self.entries.retain(|e| e.key != entry.key);
806 self.entries.push_front(entry);
807 while self.entries.len() > self.capacity {
808 self.entries.pop_back();
809 }
810 }
811 pub fn get(&mut self, key: &str) -> Option<&CacheEntry> {
815 let pos = self.entries.iter().position(|e| e.key == key)?;
816 let entry = self.entries.remove(pos)?;
817 if entry.version < self.current_version {
818 return None;
819 }
820 self.entries.push_front(entry);
821 self.entries.front()
822 }
823 pub fn invalidate_all(&mut self) {
825 self.current_version += 1;
826 }
827 pub fn remove(&mut self, key: &str) {
829 self.entries.retain(|e| e.key != key);
830 }
831 pub fn len(&self) -> usize {
833 self.entries.len()
834 }
835 pub fn is_empty(&self) -> bool {
837 self.entries.is_empty()
838 }
839 pub fn evict_stale(&mut self) {
841 let ver = self.current_version;
842 self.entries.retain(|e| e.version >= ver);
843 }
844}
845#[derive(Debug, Default)]
849pub struct SimulationRecordDatabase {
850 pub(super) records: HashMap<String, SimulationRecord>,
851}
852impl SimulationRecordDatabase {
853 pub fn new() -> Self {
855 Self::default()
856 }
857 pub fn insert(&mut self, rec: SimulationRecord) {
859 self.records.insert(rec.name.clone(), rec);
860 }
861 pub fn get(&self, name: &str) -> Option<&SimulationRecord> {
863 self.records.get(name)
864 }
865 pub fn get_mut(&mut self, name: &str) -> Option<&mut SimulationRecord> {
867 self.records.get_mut(name)
868 }
869 pub fn delete(&mut self, name: &str) -> bool {
871 self.records.remove(name).is_some()
872 }
873 pub fn count(&self) -> usize {
875 self.records.len()
876 }
877 pub fn is_empty(&self) -> bool {
879 self.records.is_empty()
880 }
881 pub fn query(&self, q: &DatabaseQuery) -> Vec<&SimulationRecord> {
883 self.records.values().filter(|r| q.matches(r)).collect()
884 }
885 pub fn names(&self) -> Vec<&str> {
887 self.records.keys().map(|s| s.as_str()).collect()
888 }
889 pub fn clear(&mut self) {
891 self.records.clear();
892 }
893}
894#[derive(Debug, Clone)]
896pub struct SnapshotEntry {
897 pub id: String,
899 pub sim_time: f64,
901 pub path: String,
903 pub file_size: usize,
905 pub loaded: bool,
907 pub tags: Vec<String>,
909}
910impl SnapshotEntry {
911 pub fn new(id: impl Into<String>, sim_time: f64, path: impl Into<String>) -> Self {
913 Self {
914 id: id.into(),
915 sim_time,
916 path: path.into(),
917 file_size: 0,
918 loaded: false,
919 tags: Vec::new(),
920 }
921 }
922 pub fn mark_loaded(&mut self) {
924 self.loaded = true;
925 }
926 pub fn add_tag(&mut self, tag: impl Into<String>) {
928 self.tags.push(tag.into());
929 }
930}
931#[derive(Debug, Default)]
935pub struct SimulationDatabase {
936 pub rows: Vec<DbRow>,
938 pub name: String,
940}
941impl SimulationDatabase {
942 pub fn new(name: impl Into<String>) -> Self {
944 Self {
945 rows: Vec::new(),
946 name: name.into(),
947 }
948 }
949 pub fn insert(&mut self, row: DbRow) {
951 self.rows.push(row);
952 }
953 pub fn row_count(&self) -> usize {
955 self.rows.len()
956 }
957 pub fn query_eq(&self, col: &str, val: &DbValue) -> Vec<&DbRow> {
959 self.rows
960 .iter()
961 .filter(|r| r.get(col).map(|v| v == val).unwrap_or(false))
962 .collect()
963 }
964 pub fn query_range(&self, col: &str, lo: f64, hi: f64) -> Vec<&DbRow> {
966 self.rows
967 .iter()
968 .filter(|r| {
969 r.get(col)
970 .and_then(|v| v.as_f64())
971 .map(|f| f >= lo && f <= hi)
972 .unwrap_or(false)
973 })
974 .collect()
975 }
976 pub fn project(&self, cols: &[&str]) -> Vec<HashMap<String, DbValue>> {
978 self.rows
979 .iter()
980 .map(|r| {
981 cols.iter()
982 .filter_map(|&c| r.get(c).map(|v| (c.to_string(), v.clone())))
983 .collect()
984 })
985 .collect()
986 }
987 pub fn delete_eq(&mut self, col: &str, val: &DbValue) -> usize {
991 let before = self.rows.len();
992 self.rows
993 .retain(|r| r.get(col).map(|v| v != val).unwrap_or(true));
994 before - self.rows.len()
995 }
996 pub fn clear(&mut self) {
998 self.rows.clear();
999 }
1000 pub fn column_mean(&self, col: &str) -> Option<f64> {
1004 let vals: Vec<f64> = self
1005 .rows
1006 .iter()
1007 .filter_map(|r| r.get(col).and_then(|v| v.as_f64()))
1008 .collect();
1009 if vals.is_empty() {
1010 None
1011 } else {
1012 Some(vals.iter().sum::<f64>() / vals.len() as f64)
1013 }
1014 }
1015}
1016#[derive(Debug, Clone, Default)]
1018pub struct DatabaseQuery {
1019 pub time_start: Option<f64>,
1021 pub time_end: Option<f64>,
1023 pub name_prefix: Option<String>,
1025 pub param_filter: Option<(String, String)>,
1027}
1028impl DatabaseQuery {
1029 pub fn new() -> Self {
1031 Self::default()
1032 }
1033 pub fn with_time_range(mut self, t_start: f64, t_end: f64) -> Self {
1035 self.time_start = Some(t_start);
1036 self.time_end = Some(t_end);
1037 self
1038 }
1039 pub fn with_name_prefix(mut self, prefix: impl Into<String>) -> Self {
1041 self.name_prefix = Some(prefix.into());
1042 self
1043 }
1044 pub fn with_param(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
1046 self.param_filter = Some((key.into(), val.into()));
1047 self
1048 }
1049 pub fn matches(&self, rec: &SimulationRecord) -> bool {
1051 if let Some(t0) = self.time_start
1052 && rec.timestamp < t0
1053 {
1054 return false;
1055 }
1056 if let Some(t1) = self.time_end
1057 && rec.timestamp > t1
1058 {
1059 return false;
1060 }
1061 if let Some(ref prefix) = self.name_prefix
1062 && !rec.name.starts_with(prefix.as_str())
1063 {
1064 return false;
1065 }
1066 if let Some((ref k, ref v)) = self.param_filter {
1067 match rec.params.get(k.as_str()) {
1068 Some(pv) if pv == v => {}
1069 _ => return false,
1070 }
1071 }
1072 true
1073 }
1074}
1075#[derive(Debug, Clone, PartialEq)]
1077pub enum DbValue {
1078 Int(i64),
1080 Float(f64),
1082 Text(String),
1084 Bool(bool),
1086 Null,
1088}
1089impl DbValue {
1090 pub fn as_f64(&self) -> Option<f64> {
1092 match self {
1093 DbValue::Float(v) => Some(*v),
1094 DbValue::Int(v) => Some(*v as f64),
1095 _ => None,
1096 }
1097 }
1098 pub fn as_str(&self) -> Option<&str> {
1100 match self {
1101 DbValue::Text(s) => Some(s.as_str()),
1102 _ => None,
1103 }
1104 }
1105}