1#![allow(dead_code)]
11
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, PartialEq)]
20pub struct SimLogEntry {
21 pub timestep: u64,
23 pub sim_time: f64,
25 pub wall_time: f64,
27 pub energy: f64,
29 pub n_bodies: usize,
31 pub custom: HashMap<String, f64>,
33}
34
35impl SimLogEntry {
36 pub fn new(timestep: u64, sim_time: f64, wall_time: f64, energy: f64, n_bodies: usize) -> Self {
38 Self {
39 timestep,
40 sim_time,
41 wall_time,
42 energy,
43 n_bodies,
44 custom: HashMap::new(),
45 }
46 }
47
48 pub fn insert(&mut self, key: impl Into<String>, value: f64) {
50 self.custom.insert(key.into(), value);
51 }
52
53 pub fn get(&self, key: &str) -> Option<f64> {
55 self.custom.get(key).copied()
56 }
57
58 pub fn to_csv_row(&self, extra_keys: &[String]) -> String {
61 let mut parts = vec![
62 self.timestep.to_string(),
63 format!("{:.9}", self.sim_time),
64 format!("{:.9}", self.wall_time),
65 format!("{:.9}", self.energy),
66 self.n_bodies.to_string(),
67 ];
68 for key in extra_keys {
69 if let Some(v) = self.custom.get(key) {
70 parts.push(format!("{:.9}", v));
71 } else {
72 parts.push(String::new());
73 }
74 }
75 parts.join(",")
76 }
77}
78
79#[derive(Debug, Default, Clone)]
85pub struct SimLogger {
86 pub entries: Vec<SimLogEntry>,
88 pub flush_interval: usize,
90 buffer: Vec<String>,
92}
93
94impl SimLogger {
95 pub fn new() -> Self {
97 Self::default()
98 }
99
100 pub fn with_flush_interval(flush_interval: usize) -> Self {
102 Self {
103 flush_interval,
104 ..Default::default()
105 }
106 }
107
108 pub fn append(&mut self, entry: SimLogEntry) {
110 self.entries.push(entry);
111 }
112
113 pub fn len(&self) -> usize {
115 self.entries.len()
116 }
117
118 pub fn is_empty(&self) -> bool {
120 self.entries.is_empty()
121 }
122
123 pub fn all_custom_keys(&self) -> Vec<String> {
125 let mut keys: Vec<String> = self
126 .entries
127 .iter()
128 .flat_map(|e| e.custom.keys().cloned())
129 .collect::<std::collections::HashSet<_>>()
130 .into_iter()
131 .collect();
132 keys.sort();
133 keys
134 }
135
136 pub fn to_csv(&self) -> String {
141 let keys = self.all_custom_keys();
142 let mut lines = Vec::with_capacity(self.entries.len() + 1);
143 let mut header = ["timestep", "sim_time", "wall_time", "energy", "n_bodies"]
144 .iter()
145 .map(|s| s.to_string())
146 .collect::<Vec<_>>();
147 header.extend(keys.iter().cloned());
148 lines.push(header.join(","));
149 for entry in &self.entries {
150 lines.push(entry.to_csv_row(&keys));
151 }
152 lines.join("\n")
153 }
154
155 pub fn to_json(&self) -> String {
157 let items: Vec<String> = self.entries.iter().map(entry_to_json).collect();
158 format!("[{}]", items.join(","))
159 }
160
161 pub fn flush(&mut self) -> Vec<String> {
163 std::mem::take(&mut self.buffer)
164 }
165}
166
167fn entry_to_json(e: &SimLogEntry) -> String {
169 let mut parts = vec![
170 format!("\"timestep\":{}", e.timestep),
171 format!("\"sim_time\":{:.9}", e.sim_time),
172 format!("\"wall_time\":{:.9}", e.wall_time),
173 format!("\"energy\":{:.9}", e.energy),
174 format!("\"n_bodies\":{}", e.n_bodies),
175 ];
176 let mut custom_pairs: Vec<(&String, &f64)> = e.custom.iter().collect();
177 custom_pairs.sort_by_key(|(k, _)| k.as_str());
178 for (k, v) in custom_pairs {
179 let esc = k.replace('"', "\\\"");
180 parts.push(format!("\"{}\":{:.9}", esc, v));
181 }
182 format!("{{{}}}", parts.join(","))
183}
184
185#[derive(Debug, Clone, Default)]
191pub struct EnergyMonitor {
192 pub history: Vec<(f64, f64)>,
194 pub anomaly_threshold: f64,
197 pub anomaly_steps: Vec<usize>,
199}
200
201impl EnergyMonitor {
202 pub fn new(anomaly_threshold: f64) -> Self {
204 Self {
205 anomaly_threshold,
206 ..Default::default()
207 }
208 }
209
210 pub fn record(&mut self, kinetic: f64, potential: f64) {
212 let total = kinetic + potential;
213 if let Some(&(ke_prev, pe_prev)) = self.history.last() {
214 let prev_total = ke_prev + pe_prev;
215 if prev_total.abs() > f64::EPSILON {
216 let rel_change = (total - prev_total).abs() / prev_total.abs();
217 if rel_change > self.anomaly_threshold {
218 self.anomaly_steps.push(self.history.len());
219 }
220 }
221 }
222 self.history.push((kinetic, potential));
223 }
224
225 pub fn latest_total(&self) -> Option<f64> {
227 self.history.last().map(|(ke, pe)| ke + pe)
228 }
229
230 pub fn mean_total(&self) -> f64 {
232 if self.history.is_empty() {
233 return 0.0;
234 }
235 let sum: f64 = self.history.iter().map(|(ke, pe)| ke + pe).sum();
236 sum / self.history.len() as f64
237 }
238
239 pub fn has_anomaly(&self) -> bool {
241 !self.anomaly_steps.is_empty()
242 }
243}
244
245#[derive(Debug, Clone, Default, PartialEq)]
251pub struct StepTiming {
252 pub broadphase: f64,
254 pub narrowphase: f64,
256 pub solve: f64,
258 pub integrate: f64,
260}
261
262impl StepTiming {
263 pub fn total(&self) -> f64 {
265 self.broadphase + self.narrowphase + self.solve + self.integrate
266 }
267}
268
269#[derive(Debug, Clone, Default)]
271pub struct PerformanceLog {
272 pub timings: Vec<StepTiming>,
274}
275
276impl PerformanceLog {
277 pub fn new() -> Self {
279 Self::default()
280 }
281
282 pub fn push(&mut self, timing: StepTiming) {
284 self.timings.push(timing);
285 }
286
287 pub fn mean_total(&self) -> f64 {
289 if self.timings.is_empty() {
290 return 0.0;
291 }
292 let sum: f64 = self
293 .timings
294 .iter()
295 .map(|t| t.total())
296 .collect::<Vec<_>>()
297 .iter()
298 .sum();
299 sum / self.timings.len() as f64
300 }
301
302 pub fn max_total(&self) -> f64 {
304 self.timings
305 .iter()
306 .map(|t| t.total())
307 .fold(f64::NEG_INFINITY, f64::max)
308 }
309
310 pub fn min_total(&self) -> f64 {
312 self.timings
313 .iter()
314 .map(|t| t.total())
315 .fold(f64::INFINITY, f64::min)
316 }
317}
318
319#[derive(Debug, Clone, Default)]
325pub struct LogFilter {
326 pub sim_time_min: Option<f64>,
328 pub sim_time_max: Option<f64>,
330 pub energy_min: Option<f64>,
332 pub energy_max: Option<f64>,
334 pub n_bodies_min: Option<usize>,
336}
337
338impl LogFilter {
339 pub fn new() -> Self {
341 Self::default()
342 }
343
344 pub fn sim_time_range(mut self, min: f64, max: f64) -> Self {
346 self.sim_time_min = Some(min);
347 self.sim_time_max = Some(max);
348 self
349 }
350
351 pub fn energy_range(mut self, min: f64, max: f64) -> Self {
353 self.energy_min = Some(min);
354 self.energy_max = Some(max);
355 self
356 }
357
358 pub fn min_bodies(mut self, n: usize) -> Self {
360 self.n_bodies_min = Some(n);
361 self
362 }
363
364 pub fn apply<'a>(&self, log: &'a [SimLogEntry]) -> Vec<&'a SimLogEntry> {
366 log.iter()
367 .filter(|e| {
368 if let Some(min) = self.sim_time_min
369 && e.sim_time < min
370 {
371 return false;
372 }
373 if let Some(max) = self.sim_time_max
374 && e.sim_time > max
375 {
376 return false;
377 }
378 if let Some(min) = self.energy_min
379 && e.energy < min
380 {
381 return false;
382 }
383 if let Some(max) = self.energy_max
384 && e.energy > max
385 {
386 return false;
387 }
388 if let Some(n) = self.n_bodies_min
389 && e.n_bodies < n
390 {
391 return false;
392 }
393 true
394 })
395 .collect()
396 }
397}
398
399#[derive(Debug, Clone, PartialEq)]
405pub struct SnapshotVersion {
406 pub major: u32,
408 pub minor: u32,
410}
411
412impl SnapshotVersion {
413 pub const CURRENT: SnapshotVersion = SnapshotVersion { major: 1, minor: 0 };
415}
416
417impl std::fmt::Display for SnapshotVersion {
418 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
419 write!(f, "{}.{}", self.major, self.minor)
420 }
421}
422
423#[derive(Debug, Clone)]
425pub struct SimSnapshot {
426 pub version: SnapshotVersion,
428 pub sim_time: f64,
430 pub positions: Vec<[f64; 3]>,
432 pub velocities: Vec<[f64; 3]>,
434 pub masses: Vec<f64>,
436 pub metadata: HashMap<String, String>,
438}
439
440impl SimSnapshot {
441 pub fn new(
443 sim_time: f64,
444 positions: Vec<[f64; 3]>,
445 velocities: Vec<[f64; 3]>,
446 masses: Vec<f64>,
447 ) -> Self {
448 Self {
449 version: SnapshotVersion::CURRENT,
450 sim_time,
451 positions,
452 velocities,
453 masses,
454 metadata: HashMap::new(),
455 }
456 }
457
458 pub fn n_bodies(&self) -> usize {
460 self.positions.len()
461 }
462
463 pub fn to_json(&self) -> String {
465 let pos_json = array3_to_json_arr(&self.positions);
466 let vel_json = array3_to_json_arr(&self.velocities);
467 let mass_json = format!(
468 "[{}]",
469 self.masses
470 .iter()
471 .map(|m| format!("{:.9}", m))
472 .collect::<Vec<_>>()
473 .join(",")
474 );
475 let mut meta_parts: Vec<String> = self
476 .metadata
477 .iter()
478 .map(|(k, v)| {
479 let ek = k.replace('"', "\\\"");
480 let ev = v.replace('"', "\\\"");
481 format!("\"{}\":\"{}\"", ek, ev)
482 })
483 .collect();
484 meta_parts.sort();
485 let meta_json = format!("{{{}}}", meta_parts.join(","));
486 format!(
487 "{{\"version\":\"{}\",\"sim_time\":{:.9},\"n_bodies\":{},\"positions\":{},\"velocities\":{},\"masses\":{},\"metadata\":{}}}",
488 self.version,
489 self.sim_time,
490 self.n_bodies(),
491 pos_json,
492 vel_json,
493 mass_json,
494 meta_json,
495 )
496 }
497}
498
499fn array3_to_json_arr(data: &[[f64; 3]]) -> String {
500 let inner: Vec<String> = data
501 .iter()
502 .map(|v| format!("[{:.9},{:.9},{:.9}]", v[0], v[1], v[2]))
503 .collect();
504 format!("[{}]", inner.join(","))
505}
506
507#[derive(Debug, Clone)]
514pub struct TelemetryStream {
515 ring: Vec<Option<SimLogEntry>>,
517 head: usize,
519 capacity: usize,
521 total_count: u64,
523 all_entries: Vec<SimLogEntry>,
525}
526
527impl TelemetryStream {
528 pub fn new(capacity: usize) -> Self {
530 assert!(capacity > 0, "capacity must be > 0");
531 Self {
532 ring: vec![None; capacity],
533 head: 0,
534 capacity,
535 total_count: 0,
536 all_entries: Vec::new(),
537 }
538 }
539
540 pub fn push(&mut self, entry: SimLogEntry) {
542 self.all_entries.push(entry.clone());
543 self.ring[self.head] = Some(entry);
544 self.head = (self.head + 1) % self.capacity;
545 self.total_count += 1;
546 }
547
548 pub fn total_count(&self) -> u64 {
550 self.total_count
551 }
552
553 pub fn ring_iter(&self) -> impl Iterator<Item = &SimLogEntry> {
555 self.ring.iter().filter_map(|e| e.as_ref())
556 }
557
558 pub fn ring_len(&self) -> usize {
560 self.ring.iter().filter(|e| e.is_some()).count()
561 }
562
563 pub fn all(&self) -> &[SimLogEntry] {
565 &self.all_entries
566 }
567}
568
569#[derive(Debug, Clone)]
575pub struct SimMetrics {
576 values: Vec<f64>,
578}
579
580impl SimMetrics {
581 pub fn from_energy(entries: &[SimLogEntry]) -> Self {
583 Self {
584 values: entries.iter().map(|e| e.energy).collect(),
585 }
586 }
587
588 pub fn from_custom(entries: &[SimLogEntry], key: &str) -> Self {
591 Self {
592 values: entries.iter().filter_map(|e| e.get(key)).collect(),
593 }
594 }
595
596 pub fn from_values(values: Vec<f64>) -> Self {
598 Self { values }
599 }
600
601 pub fn count(&self) -> usize {
603 self.values.len()
604 }
605
606 pub fn mean(&self) -> f64 {
608 if self.values.is_empty() {
609 return 0.0;
610 }
611 self.values.iter().copied().sum::<f64>() / self.values.len() as f64
612 }
613
614 pub fn std_dev(&self) -> f64 {
616 if self.values.len() < 2 {
617 return 0.0;
618 }
619 let m = self.mean();
620 let var =
621 self.values.iter().map(|v| (v - m).powi(2)).sum::<f64>() / self.values.len() as f64;
622 var.sqrt()
623 }
624
625 pub fn min(&self) -> f64 {
627 self.values.iter().copied().fold(f64::INFINITY, f64::min)
628 }
629
630 pub fn max(&self) -> f64 {
632 self.values
633 .iter()
634 .copied()
635 .fold(f64::NEG_INFINITY, f64::max)
636 }
637
638 pub fn median(&self) -> f64 {
640 if self.values.is_empty() {
641 return 0.0;
642 }
643 let mut sorted = self.values.clone();
644 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
645 let n = sorted.len();
646 if n.is_multiple_of(2) {
647 (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
648 } else {
649 sorted[n / 2]
650 }
651 }
652}
653
654#[derive(Debug, Clone)]
660pub struct ReplayLog {
661 entries: Vec<SimLogEntry>,
663}
664
665impl ReplayLog {
666 pub fn new(mut entries: Vec<SimLogEntry>) -> Self {
669 entries.sort_by(|a, b| {
670 a.sim_time
671 .partial_cmp(&b.sim_time)
672 .unwrap_or(std::cmp::Ordering::Equal)
673 });
674 Self { entries }
675 }
676
677 pub fn len(&self) -> usize {
679 self.entries.len()
680 }
681
682 pub fn is_empty(&self) -> bool {
684 self.entries.is_empty()
685 }
686
687 pub fn get(&self, i: usize) -> Option<&SimLogEntry> {
689 self.entries.get(i)
690 }
691
692 pub fn energy_series(&self) -> Vec<(f64, f64)> {
694 self.entries
695 .iter()
696 .map(|e| (e.sim_time, e.energy))
697 .collect()
698 }
699
700 pub fn interpolate_energy(&self, t: f64) -> Option<f64> {
704 if self.entries.is_empty() {
705 return None;
706 }
707 let first = self
709 .entries
710 .first()
711 .expect("collection should not be empty");
712 let last = self.entries.last().expect("collection should not be empty");
713 if t <= first.sim_time {
714 return Some(first.energy);
715 }
716 if t >= last.sim_time {
717 return Some(last.energy);
718 }
719 let idx = self.entries.partition_point(|e| e.sim_time <= t);
721 let e0 = &self.entries[idx - 1];
722 let e1 = &self.entries[idx];
723 let dt = e1.sim_time - e0.sim_time;
724 if dt.abs() < f64::EPSILON {
725 return Some(e0.energy);
726 }
727 let frac = (t - e0.sim_time) / dt;
728 Some(e0.energy + frac * (e1.energy - e0.energy))
729 }
730
731 pub fn iter(&self) -> impl Iterator<Item = &SimLogEntry> {
733 self.entries.iter()
734 }
735}
736
737#[derive(Debug, Clone, PartialEq)]
743pub struct RleRun {
744 pub value: f64,
746 pub count: usize,
748}
749
750#[derive(Debug, Clone)]
753pub struct DeltaTimestamps {
754 pub first: f64,
756 pub deltas: Vec<f64>,
758}
759
760impl DeltaTimestamps {
761 pub fn encode(timestamps: &[f64]) -> Self {
763 if timestamps.is_empty() {
764 return Self {
765 first: 0.0,
766 deltas: Vec::new(),
767 };
768 }
769 let first = timestamps[0];
770 let deltas = timestamps.windows(2).map(|w| w[1] - w[0]).collect();
771 Self { first, deltas }
772 }
773
774 pub fn decode(&self) -> Vec<f64> {
776 let mut out = Vec::with_capacity(self.deltas.len() + 1);
777 out.push(self.first);
778 let mut cur = self.first;
779 for &d in &self.deltas {
780 cur += d;
781 out.push(cur);
782 }
783 out
784 }
785
786 pub fn len(&self) -> usize {
788 self.deltas.len() + 1
789 }
790
791 pub fn is_empty(&self) -> bool {
793 self.deltas.is_empty() && self.first == 0.0
794 }
795}
796
797pub fn rle_encode(values: &[f64]) -> Vec<RleRun> {
801 let mut runs = Vec::new();
802 let mut iter = values.iter().copied();
803 let Some(first) = iter.next() else {
804 return runs;
805 };
806 let mut cur = first;
807 let mut count = 1usize;
808 for v in iter {
809 if v.to_bits() == cur.to_bits() {
810 count += 1;
811 } else {
812 runs.push(RleRun { value: cur, count });
813 cur = v;
814 count = 1;
815 }
816 }
817 runs.push(RleRun { value: cur, count });
818 runs
819}
820
821pub fn rle_decode(runs: &[RleRun]) -> Vec<f64> {
823 runs.iter()
824 .flat_map(|r| std::iter::repeat_n(r.value, r.count))
825 .collect()
826}
827
828#[derive(Debug, Clone)]
830pub struct LogCompression {
831 pub original_len: usize,
833 pub rle_runs: usize,
835 pub ratio: f64,
837}
838
839impl LogCompression {
840 pub fn analyse(values: &[f64]) -> Self {
842 let runs = rle_encode(values);
843 let original_len = values.len();
844 let rle_runs = runs.len();
845 let ratio = if rle_runs == 0 {
846 1.0
847 } else {
848 original_len as f64 / rle_runs as f64
849 };
850 Self {
851 original_len,
852 rle_runs,
853 ratio,
854 }
855 }
856}
857
858#[cfg(test)]
863mod tests {
864 use super::*;
865
866 fn make_entry(step: u64, t: f64, energy: f64) -> SimLogEntry {
867 SimLogEntry::new(step, t, t * 0.001, energy, 100)
868 }
869
870 #[test]
873 fn test_entry_new_fields() {
874 let e = make_entry(5, 0.5, 42.0);
875 assert_eq!(e.timestep, 5);
876 assert!((e.sim_time - 0.5).abs() < 1e-12);
877 assert!((e.energy - 42.0).abs() < 1e-12);
878 assert_eq!(e.n_bodies, 100);
879 assert!(e.custom.is_empty());
880 }
881
882 #[test]
883 fn test_entry_insert_and_get() {
884 let mut e = make_entry(0, 0.0, 0.0);
885 e.insert("temp", 300.0);
886 assert!((e.get("temp").unwrap() - 300.0).abs() < 1e-12);
887 assert!(e.get("missing").is_none());
888 }
889
890 #[test]
891 fn test_entry_to_csv_row_no_custom() {
892 let e = SimLogEntry::new(1, 0.1, 0.0001, 50.0, 10);
893 let row = e.to_csv_row(&[]);
894 assert!(row.starts_with("1,"));
895 assert!(row.contains("50.0") || row.contains("50."));
896 }
897
898 #[test]
899 fn test_entry_to_csv_row_with_custom() {
900 let mut e = make_entry(2, 1.0, 10.0);
901 e.insert("foo", 3.125);
902 let keys = vec!["foo".to_string()];
903 let row = e.to_csv_row(&keys);
904 assert!(row.contains("3.125"));
905 }
906
907 #[test]
908 fn test_entry_to_csv_row_missing_custom_key() {
909 let e = make_entry(0, 0.0, 0.0);
910 let keys = vec!["absent".to_string()];
911 let row = e.to_csv_row(&keys);
912 assert!(row.ends_with(','));
914 }
915
916 #[test]
919 fn test_logger_append_and_len() {
920 let mut logger = SimLogger::new();
921 assert!(logger.is_empty());
922 logger.append(make_entry(0, 0.0, 100.0));
923 assert_eq!(logger.len(), 1);
924 assert!(!logger.is_empty());
925 }
926
927 #[test]
928 fn test_logger_to_csv_header() {
929 let mut logger = SimLogger::new();
930 logger.append(make_entry(0, 0.0, 1.0));
931 let csv = logger.to_csv();
932 let first_line = csv.lines().next().unwrap();
933 assert!(first_line.contains("timestep"));
934 assert!(first_line.contains("energy"));
935 }
936
937 #[test]
938 fn test_logger_to_csv_row_count() {
939 let mut logger = SimLogger::new();
940 for i in 0..5 {
941 logger.append(make_entry(i, i as f64 * 0.1, 10.0));
942 }
943 let csv = logger.to_csv();
944 let lines: Vec<&str> = csv.lines().collect();
945 assert_eq!(lines.len(), 6); }
947
948 #[test]
949 fn test_logger_to_json_is_array() {
950 let mut logger = SimLogger::new();
951 logger.append(make_entry(0, 0.0, 5.0));
952 let json = logger.to_json();
953 assert!(json.starts_with('['));
954 assert!(json.ends_with(']'));
955 }
956
957 #[test]
958 fn test_logger_all_custom_keys_sorted() {
959 let mut logger = SimLogger::new();
960 let mut e = make_entry(0, 0.0, 0.0);
961 e.insert("z_key", 1.0);
962 e.insert("a_key", 2.0);
963 logger.append(e);
964 let keys = logger.all_custom_keys();
965 assert_eq!(keys[0], "a_key");
966 assert_eq!(keys[1], "z_key");
967 }
968
969 #[test]
970 fn test_logger_flush_buffer() {
971 let mut logger = SimLogger::new();
972 let flushed = logger.flush();
973 assert!(flushed.is_empty());
974 }
975
976 #[test]
979 fn test_energy_monitor_no_anomaly_steady() {
980 let mut mon = EnergyMonitor::new(0.1);
981 for _ in 0..10 {
982 mon.record(50.0, 50.0); }
984 assert!(!mon.has_anomaly());
985 }
986
987 #[test]
988 fn test_energy_monitor_detects_jump() {
989 let mut mon = EnergyMonitor::new(0.1); mon.record(50.0, 50.0); mon.record(70.0, 80.0); assert!(mon.has_anomaly());
993 }
994
995 #[test]
996 fn test_energy_monitor_latest_total() {
997 let mut mon = EnergyMonitor::new(1.0);
998 mon.record(30.0, 20.0);
999 assert!((mon.latest_total().unwrap() - 50.0).abs() < 1e-12);
1000 }
1001
1002 #[test]
1003 fn test_energy_monitor_mean_total() {
1004 let mut mon = EnergyMonitor::new(1.0);
1005 mon.record(10.0, 10.0); mon.record(20.0, 20.0); assert!((mon.mean_total() - 30.0).abs() < 1e-12);
1008 }
1009
1010 #[test]
1011 fn test_energy_monitor_empty() {
1012 let mon = EnergyMonitor::new(0.1);
1013 assert!(mon.latest_total().is_none());
1014 assert!((mon.mean_total()).abs() < 1e-12);
1015 assert!(!mon.has_anomaly());
1016 }
1017
1018 #[test]
1021 fn test_perf_log_step_timing_total() {
1022 let t = StepTiming {
1023 broadphase: 0.001,
1024 narrowphase: 0.002,
1025 solve: 0.003,
1026 integrate: 0.001,
1027 };
1028 assert!((t.total() - 0.007).abs() < 1e-12);
1029 }
1030
1031 #[test]
1032 fn test_perf_log_mean_total() {
1033 let mut log = PerformanceLog::new();
1034 log.push(StepTiming {
1035 broadphase: 0.01,
1036 narrowphase: 0.0,
1037 solve: 0.0,
1038 integrate: 0.0,
1039 });
1040 log.push(StepTiming {
1041 broadphase: 0.03,
1042 narrowphase: 0.0,
1043 solve: 0.0,
1044 integrate: 0.0,
1045 });
1046 assert!((log.mean_total() - 0.02).abs() < 1e-12);
1047 }
1048
1049 #[test]
1050 fn test_perf_log_max_min() {
1051 let mut log = PerformanceLog::new();
1052 log.push(StepTiming {
1053 broadphase: 0.005,
1054 narrowphase: 0.0,
1055 solve: 0.0,
1056 integrate: 0.0,
1057 });
1058 log.push(StepTiming {
1059 broadphase: 0.020,
1060 narrowphase: 0.0,
1061 solve: 0.0,
1062 integrate: 0.0,
1063 });
1064 assert!((log.max_total() - 0.020).abs() < 1e-12);
1065 assert!((log.min_total() - 0.005).abs() < 1e-12);
1066 }
1067
1068 #[test]
1069 fn test_perf_log_empty_mean() {
1070 let log = PerformanceLog::new();
1071 assert!((log.mean_total()).abs() < 1e-12);
1072 }
1073
1074 #[test]
1077 fn test_filter_sim_time_range() {
1078 let entries = vec![
1079 make_entry(0, 0.0, 10.0),
1080 make_entry(1, 1.0, 10.0),
1081 make_entry(2, 2.0, 10.0),
1082 ];
1083 let filter = LogFilter::new().sim_time_range(0.5, 1.5);
1084 let filtered = filter.apply(&entries);
1085 assert_eq!(filtered.len(), 1);
1086 assert!((filtered[0].sim_time - 1.0).abs() < 1e-12);
1087 }
1088
1089 #[test]
1090 fn test_filter_energy_range() {
1091 let entries = vec![
1092 make_entry(0, 0.0, 5.0),
1093 make_entry(1, 1.0, 50.0),
1094 make_entry(2, 2.0, 500.0),
1095 ];
1096 let filter = LogFilter::new().energy_range(10.0, 100.0);
1097 let filtered = filter.apply(&entries);
1098 assert_eq!(filtered.len(), 1);
1099 assert!((filtered[0].energy - 50.0).abs() < 1e-12);
1100 }
1101
1102 #[test]
1103 fn test_filter_min_bodies() {
1104 let mut entries = vec![make_entry(0, 0.0, 10.0)];
1105 entries[0].n_bodies = 5;
1106 let mut e2 = make_entry(1, 1.0, 10.0);
1107 e2.n_bodies = 200;
1108 entries.push(e2);
1109 let filter = LogFilter::new().min_bodies(100);
1110 let filtered = filter.apply(&entries);
1111 assert_eq!(filtered.len(), 1);
1112 assert_eq!(filtered[0].n_bodies, 200);
1113 }
1114
1115 #[test]
1116 fn test_filter_empty_accepts_all() {
1117 let entries: Vec<SimLogEntry> = (0..5).map(|i| make_entry(i, i as f64, 1.0)).collect();
1118 let filter = LogFilter::new();
1119 assert_eq!(filter.apply(&entries).len(), 5);
1120 }
1121
1122 #[test]
1125 fn test_snapshot_n_bodies() {
1126 let snap = SimSnapshot::new(
1127 1.0,
1128 vec![[0.0; 3], [1.0; 3]],
1129 vec![[0.0; 3], [0.0; 3]],
1130 vec![1.0, 2.0],
1131 );
1132 assert_eq!(snap.n_bodies(), 2);
1133 }
1134
1135 #[test]
1136 fn test_snapshot_to_json_contains_version() {
1137 let snap = SimSnapshot::new(0.5, vec![], vec![], vec![]);
1138 let json = snap.to_json();
1139 assert!(json.contains("\"version\""));
1140 assert!(json.contains("1.0"));
1141 }
1142
1143 #[test]
1144 fn test_snapshot_to_json_contains_n_bodies() {
1145 let snap = SimSnapshot::new(0.0, vec![[1.0, 2.0, 3.0]], vec![[0.1, 0.2, 0.3]], vec![5.0]);
1146 let json = snap.to_json();
1147 assert!(json.contains("\"n_bodies\":1"));
1148 }
1149
1150 #[test]
1151 fn test_snapshot_version_display() {
1152 assert_eq!(SnapshotVersion::CURRENT.to_string(), "1.0");
1153 }
1154
1155 #[test]
1158 fn test_telemetry_stream_basic_push() {
1159 let mut stream = TelemetryStream::new(3);
1160 stream.push(make_entry(0, 0.0, 1.0));
1161 assert_eq!(stream.total_count(), 1);
1162 assert_eq!(stream.ring_len(), 1);
1163 }
1164
1165 #[test]
1166 fn test_telemetry_stream_ring_wraps() {
1167 let mut stream = TelemetryStream::new(3);
1168 for i in 0..5u64 {
1169 stream.push(make_entry(i, i as f64, 1.0));
1170 }
1171 assert_eq!(stream.total_count(), 5);
1172 assert_eq!(stream.ring_len(), 3); }
1174
1175 #[test]
1176 fn test_telemetry_stream_all_entries() {
1177 let mut stream = TelemetryStream::new(2);
1178 for i in 0..10u64 {
1179 stream.push(make_entry(i, i as f64, 0.0));
1180 }
1181 assert_eq!(stream.all().len(), 10);
1182 }
1183
1184 #[test]
1185 fn test_telemetry_stream_ring_iter() {
1186 let mut stream = TelemetryStream::new(5);
1187 stream.push(make_entry(0, 0.0, 42.0));
1188 let in_ring: Vec<_> = stream.ring_iter().collect();
1189 assert_eq!(in_ring.len(), 1);
1190 assert!((in_ring[0].energy - 42.0).abs() < 1e-12);
1191 }
1192
1193 #[test]
1196 fn test_metrics_mean() {
1197 let m = SimMetrics::from_values(vec![1.0, 2.0, 3.0]);
1198 assert!((m.mean() - 2.0).abs() < 1e-12);
1199 }
1200
1201 #[test]
1202 fn test_metrics_std_dev() {
1203 let m = SimMetrics::from_values(vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
1204 assert!((m.std_dev() - 2.0).abs() < 1e-9);
1206 }
1207
1208 #[test]
1209 fn test_metrics_min_max() {
1210 let m = SimMetrics::from_values(vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0]);
1211 assert!((m.min() - 1.0).abs() < 1e-12);
1212 assert!((m.max() - 9.0).abs() < 1e-12);
1213 }
1214
1215 #[test]
1216 fn test_metrics_median_odd() {
1217 let m = SimMetrics::from_values(vec![5.0, 1.0, 3.0]);
1218 assert!((m.median() - 3.0).abs() < 1e-12);
1219 }
1220
1221 #[test]
1222 fn test_metrics_median_even() {
1223 let m = SimMetrics::from_values(vec![1.0, 2.0, 3.0, 4.0]);
1224 assert!((m.median() - 2.5).abs() < 1e-12);
1225 }
1226
1227 #[test]
1228 fn test_metrics_from_energy() {
1229 let entries: Vec<SimLogEntry> = vec![make_entry(0, 0.0, 10.0), make_entry(1, 1.0, 20.0)];
1230 let m = SimMetrics::from_energy(&entries);
1231 assert_eq!(m.count(), 2);
1232 assert!((m.mean() - 15.0).abs() < 1e-12);
1233 }
1234
1235 #[test]
1236 fn test_metrics_from_custom_key() {
1237 let mut e1 = make_entry(0, 0.0, 0.0);
1238 e1.insert("temp", 100.0);
1239 let mut e2 = make_entry(1, 1.0, 0.0);
1240 e2.insert("temp", 200.0);
1241 let entries = vec![e1, e2];
1242 let m = SimMetrics::from_custom(&entries, "temp");
1243 assert_eq!(m.count(), 2);
1244 assert!((m.mean() - 150.0).abs() < 1e-12);
1245 }
1246
1247 #[test]
1248 fn test_metrics_empty() {
1249 let m = SimMetrics::from_values(vec![]);
1250 assert_eq!(m.count(), 0);
1251 assert!((m.mean()).abs() < 1e-12);
1252 assert!((m.std_dev()).abs() < 1e-12);
1253 assert!((m.median()).abs() < 1e-12);
1254 }
1255
1256 #[test]
1259 fn test_replay_sorted_on_construction() {
1260 let entries = vec![
1261 make_entry(2, 2.0, 20.0),
1262 make_entry(0, 0.0, 0.0),
1263 make_entry(1, 1.0, 10.0),
1264 ];
1265 let replay = ReplayLog::new(entries);
1266 assert!((replay.get(0).unwrap().sim_time - 0.0).abs() < 1e-12);
1267 assert!((replay.get(2).unwrap().sim_time - 2.0).abs() < 1e-12);
1268 }
1269
1270 #[test]
1271 fn test_replay_energy_series_len() {
1272 let entries: Vec<_> = (0..5)
1273 .map(|i| make_entry(i, i as f64, i as f64 * 10.0))
1274 .collect();
1275 let replay = ReplayLog::new(entries);
1276 assert_eq!(replay.energy_series().len(), 5);
1277 }
1278
1279 #[test]
1280 fn test_replay_interpolate_energy_midpoint() {
1281 let entries = vec![make_entry(0, 0.0, 0.0), make_entry(1, 2.0, 20.0)];
1282 let replay = ReplayLog::new(entries);
1283 let e = replay.interpolate_energy(1.0).unwrap();
1284 assert!((e - 10.0).abs() < 1e-9);
1285 }
1286
1287 #[test]
1288 fn test_replay_interpolate_clamp_before_start() {
1289 let entries = vec![make_entry(0, 1.0, 5.0), make_entry(1, 2.0, 10.0)];
1290 let replay = ReplayLog::new(entries);
1291 assert!((replay.interpolate_energy(0.0).unwrap() - 5.0).abs() < 1e-12);
1292 }
1293
1294 #[test]
1295 fn test_replay_interpolate_clamp_after_end() {
1296 let entries = vec![make_entry(0, 0.0, 5.0), make_entry(1, 1.0, 10.0)];
1297 let replay = ReplayLog::new(entries);
1298 assert!((replay.interpolate_energy(99.0).unwrap() - 10.0).abs() < 1e-12);
1299 }
1300
1301 #[test]
1302 fn test_replay_empty() {
1303 let replay = ReplayLog::new(vec![]);
1304 assert!(replay.is_empty());
1305 assert!(replay.interpolate_energy(0.5).is_none());
1306 }
1307
1308 #[test]
1311 fn test_rle_encode_constant() {
1312 let data = vec![3.125; 5];
1313 let runs = rle_encode(&data);
1314 assert_eq!(runs.len(), 1);
1315 assert_eq!(runs[0].count, 5);
1316 assert!((runs[0].value - 3.125).abs() < 1e-12);
1317 }
1318
1319 #[test]
1320 fn test_rle_decode_roundtrip() {
1321 let data = vec![1.0, 1.0, 2.0, 3.0, 3.0, 3.0];
1322 let runs = rle_encode(&data);
1323 let decoded = rle_decode(&runs);
1324 assert_eq!(decoded, data);
1325 }
1326
1327 #[test]
1328 fn test_rle_encode_all_unique() {
1329 let data = vec![1.0, 2.0, 3.0, 4.0];
1330 let runs = rle_encode(&data);
1331 assert_eq!(runs.len(), 4);
1332 }
1333
1334 #[test]
1335 fn test_rle_encode_empty() {
1336 let runs = rle_encode(&[]);
1337 assert!(runs.is_empty());
1338 }
1339
1340 #[test]
1341 fn test_log_compression_analyse_ratio() {
1342 let data = vec![42.0f64; 100];
1343 let comp = LogCompression::analyse(&data);
1344 assert_eq!(comp.original_len, 100);
1345 assert_eq!(comp.rle_runs, 1);
1346 assert!((comp.ratio - 100.0).abs() < 1e-9);
1347 }
1348
1349 #[test]
1352 fn test_delta_timestamps_encode_decode_roundtrip() {
1353 let ts = vec![0.0, 0.1, 0.2, 0.4, 0.7, 1.1];
1354 let enc = DeltaTimestamps::encode(&ts);
1355 let dec = enc.decode();
1356 for (a, b) in ts.iter().zip(dec.iter()) {
1357 assert!((a - b).abs() < 1e-10, "a={a} b={b}");
1358 }
1359 }
1360
1361 #[test]
1362 fn test_delta_timestamps_len() {
1363 let ts = vec![0.0, 1.0, 2.0, 3.0];
1364 let enc = DeltaTimestamps::encode(&ts);
1365 assert_eq!(enc.len(), 4);
1366 }
1367
1368 #[test]
1369 fn test_delta_timestamps_empty() {
1370 let enc = DeltaTimestamps::encode(&[]);
1371 assert_eq!(enc.len(), 1); }
1373}