1use std::collections::VecDeque;
18use std::fmt::Write as FmtWrite;
19use std::fs::File;
20use std::io::{self, BufWriter, Write as IoWrite};
21use std::path::Path;
22
23use serde::{Deserialize, Serialize};
24
25use crate::engine::{SimState, SimTime};
26use crate::error::{SimError, SimResult};
27
28#[cfg(feature = "tui")]
30pub mod tui;
31
32#[cfg(feature = "tui")]
33pub use tui::SimularTui;
34
35#[cfg(feature = "web")]
37pub mod web;
38
39#[cfg(feature = "web")]
40pub use web::{WebPayload, WebVisualization};
41
42#[derive(Debug, Clone, Default, Serialize, Deserialize)]
48pub struct SimMetrics {
49 pub time: f64,
51 pub step: u64,
53 pub steps_per_second: f64,
55 pub total_energy: Option<f64>,
57 pub kinetic_energy: Option<f64>,
59 pub potential_energy: Option<f64>,
61 pub energy_drift: Option<f64>,
63 pub body_count: usize,
65 pub jidoka_warnings: u32,
67 pub jidoka_errors: u32,
69 pub memory_bytes: usize,
71 pub custom: std::collections::HashMap<String, f64>,
73}
74
75impl SimMetrics {
76 #[must_use]
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn update_from_state(&mut self, state: &SimState, time: SimTime) {
84 self.time = time.as_secs_f64();
85 self.body_count = state.num_bodies();
86 self.kinetic_energy = Some(state.kinetic_energy());
87 self.potential_energy = Some(state.potential_energy());
88 self.total_energy = Some(state.total_energy());
89 }
90
91 pub fn set_energy_drift(&mut self, initial_energy: f64) {
93 if let Some(current) = self.total_energy {
94 if initial_energy.abs() > f64::EPSILON {
95 self.energy_drift = Some((current - initial_energy).abs() / initial_energy.abs());
96 }
97 }
98 }
99
100 pub fn add_custom(&mut self, name: impl Into<String>, value: f64) {
102 self.custom.insert(name.into(), value);
103 }
104
105 #[must_use]
107 pub fn get_custom(&self, name: &str) -> Option<f64> {
108 self.custom.get(name).copied()
109 }
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct DataPoint {
115 pub time: f64,
117 pub value: f64,
119}
120
121#[derive(Debug, Clone)]
123pub struct TimeSeries {
124 data: VecDeque<DataPoint>,
126 capacity: usize,
128 name: String,
130}
131
132impl TimeSeries {
133 #[must_use]
135 pub fn new(name: impl Into<String>, capacity: usize) -> Self {
136 Self {
137 data: VecDeque::with_capacity(capacity),
138 capacity,
139 name: name.into(),
140 }
141 }
142
143 pub fn push(&mut self, time: f64, value: f64) {
145 if self.data.len() >= self.capacity {
146 self.data.pop_front();
147 }
148 self.data.push_back(DataPoint { time, value });
149 }
150
151 #[must_use]
153 pub fn data(&self) -> &VecDeque<DataPoint> {
154 &self.data
155 }
156
157 #[must_use]
159 pub fn name(&self) -> &str {
160 &self.name
161 }
162
163 #[must_use]
165 pub fn last_value(&self) -> Option<f64> {
166 self.data.back().map(|p| p.value)
167 }
168
169 #[must_use]
171 pub fn min(&self) -> Option<f64> {
172 self.data
173 .iter()
174 .map(|p| p.value)
175 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
176 }
177
178 #[must_use]
180 pub fn max(&self) -> Option<f64> {
181 self.data
182 .iter()
183 .map(|p| p.value)
184 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
185 }
186
187 #[must_use]
189 pub fn time_range(&self) -> Option<(f64, f64)> {
190 if self.data.is_empty() {
191 return None;
192 }
193 let first = self.data.front().map_or(0.0, |p| p.time);
194 let last = self.data.back().map_or(0.0, |p| p.time);
195 Some((first, last))
196 }
197
198 pub fn clear(&mut self) {
200 self.data.clear();
201 }
202
203 #[must_use]
205 pub fn is_empty(&self) -> bool {
206 self.data.is_empty()
207 }
208
209 #[must_use]
211 pub fn len(&self) -> usize {
212 self.data.len()
213 }
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct TrajectoryFrame {
223 pub time: f64,
225 pub index: u64,
227 pub positions: Vec<f64>,
229 pub velocities: Vec<f64>,
231 pub metrics: SimMetrics,
233}
234
235impl TrajectoryFrame {
236 #[must_use]
238 pub fn from_state(state: &SimState, time: SimTime, index: u64) -> Self {
239 let positions: Vec<f64> = state
240 .positions()
241 .iter()
242 .flat_map(|p| [p.x, p.y, p.z])
243 .collect();
244
245 let velocities: Vec<f64> = state
246 .velocities()
247 .iter()
248 .flat_map(|v| [v.x, v.y, v.z])
249 .collect();
250
251 let mut metrics = SimMetrics::new();
252 metrics.update_from_state(state, time);
253
254 Self {
255 time: time.as_secs_f64(),
256 index,
257 positions,
258 velocities,
259 metrics,
260 }
261 }
262}
263
264#[derive(Debug, Clone, Default, Serialize, Deserialize)]
266pub struct Trajectory {
267 pub frames: Vec<TrajectoryFrame>,
269 pub metadata: TrajectoryMetadata,
271}
272
273#[derive(Debug, Clone, Default, Serialize, Deserialize)]
275pub struct TrajectoryMetadata {
276 pub name: String,
278 pub body_count: usize,
280 pub start_time: f64,
282 pub end_time: f64,
284 pub timestep: f64,
286 pub seed: u64,
288}
289
290impl Trajectory {
291 #[must_use]
293 pub fn new() -> Self {
294 Self::default()
295 }
296
297 #[must_use]
299 pub fn with_metadata(metadata: TrajectoryMetadata) -> Self {
300 Self {
301 frames: Vec::new(),
302 metadata,
303 }
304 }
305
306 pub fn add_frame(&mut self, frame: TrajectoryFrame) {
308 if let Some(last) = self.frames.last() {
309 self.metadata.end_time = frame.time.max(last.time);
310 } else {
311 self.metadata.start_time = frame.time;
312 self.metadata.end_time = frame.time;
313 }
314 self.frames.push(frame);
315 }
316
317 #[must_use]
319 pub fn len(&self) -> usize {
320 self.frames.len()
321 }
322
323 #[must_use]
325 pub fn is_empty(&self) -> bool {
326 self.frames.is_empty()
327 }
328
329 #[must_use]
331 pub fn frame(&self, index: usize) -> Option<&TrajectoryFrame> {
332 self.frames.get(index)
333 }
334
335 #[must_use]
337 pub fn frame_at_time(&self, time: f64) -> Option<&TrajectoryFrame> {
338 self.frames.iter().min_by(|a, b| {
339 let diff_a = (a.time - time).abs();
340 let diff_b = (b.time - time).abs();
341 diff_a
342 .partial_cmp(&diff_b)
343 .unwrap_or(std::cmp::Ordering::Equal)
344 })
345 }
346
347 #[must_use]
349 pub fn duration(&self) -> f64 {
350 self.metadata.end_time - self.metadata.start_time
351 }
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
360pub enum VideoFormat {
361 Mp4,
363 Gif,
365 WebM,
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
371pub enum ParquetCompression {
372 None,
374 Snappy,
376 Zstd,
378 Lz4,
380}
381
382#[derive(Debug, Clone, Default, Serialize, Deserialize)]
384pub enum ExportFormat {
385 #[default]
387 JsonLines,
388 Parquet {
390 compression: ParquetCompression,
392 },
393 Video {
395 format: VideoFormat,
397 fps: u32,
399 },
400 Csv,
402 Binary,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct ExportConfig {
409 pub format: ExportFormat,
411 pub include_velocities: bool,
413 pub include_metrics: bool,
415 pub decimation: usize,
417}
418
419impl Default for ExportConfig {
420 fn default() -> Self {
421 Self {
422 format: ExportFormat::JsonLines,
423 include_velocities: true,
424 include_metrics: true,
425 decimation: 1,
426 }
427 }
428}
429
430#[derive(Debug, Clone)]
432pub struct Exporter {
433 config: ExportConfig,
435}
436
437impl Default for Exporter {
438 fn default() -> Self {
439 Self::new()
440 }
441}
442
443impl Exporter {
444 #[must_use]
446 pub fn new() -> Self {
447 Self {
448 config: ExportConfig::default(),
449 }
450 }
451
452 #[must_use]
454 pub fn with_config(config: ExportConfig) -> Self {
455 Self { config }
456 }
457
458 pub fn to_json_lines(&self, trajectory: &Trajectory, path: &Path) -> SimResult<()> {
464 let file =
465 File::create(path).map_err(|e| SimError::io(format!("Failed to create file: {e}")))?;
466 let mut writer = BufWriter::new(file);
467
468 for (i, frame) in trajectory.frames.iter().enumerate() {
469 if i % self.config.decimation.max(1) != 0 {
470 continue;
471 }
472
473 let json = serde_json::to_string(frame)
474 .map_err(|e| SimError::serialization(format!("JSON serialization failed: {e}")))?;
475 writeln!(writer, "{json}").map_err(|e| SimError::io(format!("Write failed: {e}")))?;
476 }
477
478 writer
479 .flush()
480 .map_err(|e| SimError::io(format!("Flush failed: {e}")))?;
481
482 Ok(())
483 }
484
485 pub fn to_csv(&self, trajectory: &Trajectory, path: &Path) -> SimResult<()> {
491 let file =
492 File::create(path).map_err(|e| SimError::io(format!("Failed to create file: {e}")))?;
493 let mut writer = BufWriter::new(file);
494
495 let mut header = String::from("time,index");
497 if !trajectory.frames.is_empty() {
498 let n_bodies = trajectory.frames[0].positions.len() / 3;
499 for i in 0..n_bodies {
500 let _ = write!(header, ",x{i},y{i},z{i}");
501 if self.config.include_velocities {
502 let _ = write!(header, ",vx{i},vy{i},vz{i}");
503 }
504 }
505 }
506 if self.config.include_metrics {
507 header.push_str(",total_energy,kinetic_energy,potential_energy");
508 }
509 writeln!(writer, "{header}")
510 .map_err(|e| SimError::io(format!("Write header failed: {e}")))?;
511
512 for (i, frame) in trajectory.frames.iter().enumerate() {
514 if i % self.config.decimation.max(1) != 0 {
515 continue;
516 }
517
518 let mut line = format!("{},{}", frame.time, frame.index);
519
520 for (j, pos) in frame.positions.chunks(3).enumerate() {
521 if pos.len() == 3 {
522 let _ = write!(line, ",{},{},{}", pos[0], pos[1], pos[2]);
523 }
524 if self.config.include_velocities {
525 if let Some(vel) = frame.velocities.chunks(3).nth(j) {
526 if vel.len() == 3 {
527 let _ = write!(line, ",{},{},{}", vel[0], vel[1], vel[2]);
528 }
529 }
530 }
531 }
532
533 if self.config.include_metrics {
534 let te = frame.metrics.total_energy.unwrap_or(0.0);
535 let ke = frame.metrics.kinetic_energy.unwrap_or(0.0);
536 let pe = frame.metrics.potential_energy.unwrap_or(0.0);
537 let _ = write!(line, ",{te},{ke},{pe}");
538 }
539
540 writeln!(writer, "{line}")
541 .map_err(|e| SimError::io(format!("Write data failed: {e}")))?;
542 }
543
544 writer
545 .flush()
546 .map_err(|e| SimError::io(format!("Flush failed: {e}")))?;
547
548 Ok(())
549 }
550
551 pub fn to_binary(&self, trajectory: &Trajectory, path: &Path) -> SimResult<()> {
557 let file =
558 File::create(path).map_err(|e| SimError::io(format!("Failed to create file: {e}")))?;
559 let writer = BufWriter::new(file);
560
561 bincode::serialize_into(writer, trajectory)
562 .map_err(|e| SimError::serialization(format!("Binary serialization failed: {e}")))?;
563
564 Ok(())
565 }
566
567 pub fn from_binary(path: &Path) -> SimResult<Trajectory> {
573 let file =
574 File::open(path).map_err(|e| SimError::io(format!("Failed to open file: {e}")))?;
575 let reader = io::BufReader::new(file);
576
577 bincode::deserialize_from(reader)
578 .map_err(|e| SimError::serialization(format!("Binary deserialization failed: {e}")))
579 }
580
581 pub fn export(&self, trajectory: &Trajectory, path: &Path) -> SimResult<()> {
587 match &self.config.format {
588 ExportFormat::JsonLines => self.to_json_lines(trajectory, path),
589 ExportFormat::Csv => self.to_csv(trajectory, path),
590 ExportFormat::Binary => self.to_binary(trajectory, path),
591 ExportFormat::Parquet { .. } => Err(SimError::config(
592 "Parquet export requires alimentar integration".to_string(),
593 )),
594 ExportFormat::Video { .. } => Err(SimError::config(
595 "Video export requires ffmpeg integration".to_string(),
596 )),
597 }
598 }
599}
600
601pub struct StreamingExporter {
603 writer: BufWriter<File>,
605 frame_count: u64,
607 decimation_count: usize,
609 decimation: usize,
611}
612
613impl StreamingExporter {
614 pub fn new(path: &Path, decimation: usize) -> SimResult<Self> {
620 let file =
621 File::create(path).map_err(|e| SimError::io(format!("Failed to create file: {e}")))?;
622 Ok(Self {
623 writer: BufWriter::new(file),
624 frame_count: 0,
625 decimation_count: 0,
626 decimation: decimation.max(1),
627 })
628 }
629
630 pub fn write_frame(&mut self, frame: &TrajectoryFrame) -> SimResult<()> {
636 self.decimation_count += 1;
637 if self.decimation_count < self.decimation {
638 return Ok(());
639 }
640 self.decimation_count = 0;
641
642 let json = serde_json::to_string(frame)
643 .map_err(|e| SimError::serialization(format!("JSON serialization failed: {e}")))?;
644 writeln!(self.writer, "{json}").map_err(|e| SimError::io(format!("Write failed: {e}")))?;
645
646 self.frame_count += 1;
647 Ok(())
648 }
649
650 pub fn finish(mut self) -> SimResult<u64> {
656 self.writer
657 .flush()
658 .map_err(|e| SimError::io(format!("Flush failed: {e}")))?;
659 Ok(self.frame_count)
660 }
661
662 #[must_use]
664 pub fn frame_count(&self) -> u64 {
665 self.frame_count
666 }
667}
668
669#[cfg(test)]
674#[allow(clippy::unwrap_used, clippy::expect_used)]
675mod tests {
676 use super::*;
677 use std::io::Read as IoRead;
678 use tempfile::tempdir;
679
680 #[test]
681 fn test_sim_metrics_new() {
682 let metrics = SimMetrics::new();
683 assert_eq!(metrics.step, 0);
684 assert_eq!(metrics.body_count, 0);
685 }
686
687 #[test]
688 fn test_sim_metrics_custom() {
689 let mut metrics = SimMetrics::new();
690 metrics.add_custom("test_metric", 42.0);
691 assert!((metrics.get_custom("test_metric").unwrap() - 42.0).abs() < f64::EPSILON);
692 assert!(metrics.get_custom("nonexistent").is_none());
693 }
694
695 #[test]
696 fn test_sim_metrics_default() {
697 let metrics: SimMetrics = Default::default();
698 assert_eq!(metrics.step, 0);
699 assert!(metrics.total_energy.is_none());
700 }
701
702 #[test]
703 fn test_sim_metrics_set_energy_drift() {
704 let mut metrics = SimMetrics::new();
705 metrics.total_energy = Some(10.5);
706 metrics.set_energy_drift(10.0);
707 assert!(metrics.energy_drift.is_some());
708 let drift = metrics.energy_drift.unwrap();
709 assert!((drift - 0.05).abs() < 1e-10);
710 }
711
712 #[test]
713 fn test_sim_metrics_set_energy_drift_zero_initial() {
714 let mut metrics = SimMetrics::new();
715 metrics.total_energy = Some(10.5);
716 metrics.set_energy_drift(0.0); assert!(metrics.energy_drift.is_none());
718 }
719
720 #[test]
721 fn test_sim_metrics_set_energy_drift_no_total() {
722 let mut metrics = SimMetrics::new();
723 metrics.set_energy_drift(10.0);
725 assert!(metrics.energy_drift.is_none());
726 }
727
728 #[test]
729 fn test_sim_metrics_clone() {
730 let mut metrics = SimMetrics::new();
731 metrics.step = 42;
732 metrics.add_custom("key", 1.0);
733 let cloned = metrics.clone();
734 assert_eq!(cloned.step, 42);
735 assert!((cloned.get_custom("key").unwrap() - 1.0).abs() < f64::EPSILON);
736 }
737
738 #[test]
739 fn test_time_series_new() {
740 let series = TimeSeries::new("test", 100);
741 assert!(series.is_empty());
742 assert_eq!(series.name(), "test");
743 }
744
745 #[test]
746 fn test_time_series_push() {
747 let mut series = TimeSeries::new("test", 100);
748 series.push(0.0, 1.0);
749 series.push(1.0, 2.0);
750 series.push(2.0, 3.0);
751
752 assert_eq!(series.len(), 3);
753 assert!((series.last_value().unwrap() - 3.0).abs() < f64::EPSILON);
754 }
755
756 #[test]
757 fn test_time_series_capacity() {
758 let mut series = TimeSeries::new("test", 3);
759 series.push(0.0, 1.0);
760 series.push(1.0, 2.0);
761 series.push(2.0, 3.0);
762 series.push(3.0, 4.0); assert_eq!(series.len(), 3);
765 assert!((series.data().front().unwrap().value - 2.0).abs() < f64::EPSILON);
766 }
767
768 #[test]
769 fn test_time_series_min_max() {
770 let mut series = TimeSeries::new("test", 100);
771 series.push(0.0, 5.0);
772 series.push(1.0, 2.0);
773 series.push(2.0, 8.0);
774
775 assert!((series.min().unwrap() - 2.0).abs() < f64::EPSILON);
776 assert!((series.max().unwrap() - 8.0).abs() < f64::EPSILON);
777 }
778
779 #[test]
780 fn test_time_series_range() {
781 let mut series = TimeSeries::new("test", 100);
782 series.push(1.0, 0.0);
783 series.push(5.0, 0.0);
784
785 let (start, end) = series.time_range().unwrap();
786 assert!((start - 1.0).abs() < f64::EPSILON);
787 assert!((end - 5.0).abs() < f64::EPSILON);
788 }
789
790 #[test]
791 fn test_time_series_empty_stats() {
792 let series = TimeSeries::new("test", 100);
793 assert!(series.min().is_none());
794 assert!(series.max().is_none());
795 assert!(series.last_value().is_none());
796 assert!(series.time_range().is_none());
797 }
798
799 #[test]
800 fn test_time_series_clear() {
801 let mut series = TimeSeries::new("test", 100);
802 series.push(0.0, 1.0);
803 series.push(1.0, 2.0);
804 assert!(!series.is_empty());
805
806 series.clear();
807 assert!(series.is_empty());
808 assert_eq!(series.len(), 0);
809 }
810
811 #[test]
812 fn test_time_series_clone() {
813 let mut series = TimeSeries::new("test", 100);
814 series.push(0.0, 1.0);
815 let cloned = series.clone();
816 assert_eq!(cloned.len(), 1);
817 assert_eq!(cloned.name(), "test");
818 }
819
820 #[test]
821 fn test_data_point_clone() {
822 let dp = DataPoint {
823 time: 1.0,
824 value: 2.0,
825 };
826 let cloned = dp.clone();
827 assert!((cloned.time - 1.0).abs() < f64::EPSILON);
828 assert!((cloned.value - 2.0).abs() < f64::EPSILON);
829 }
830
831 #[test]
832 fn test_trajectory_new() {
833 let traj = Trajectory::new();
834 assert!(traj.is_empty());
835 assert_eq!(traj.len(), 0);
836 }
837
838 #[test]
839 fn test_trajectory_with_metadata() {
840 let metadata = TrajectoryMetadata {
841 name: "test".to_string(),
842 body_count: 5,
843 start_time: 0.0,
844 end_time: 10.0,
845 timestep: 0.01,
846 seed: 42,
847 };
848 let traj = Trajectory::with_metadata(metadata);
849 assert!(traj.is_empty());
850 assert_eq!(traj.metadata.name, "test");
851 assert_eq!(traj.metadata.body_count, 5);
852 }
853
854 #[test]
855 fn test_trajectory_add_frame() {
856 let mut traj = Trajectory::new();
857 let frame = TrajectoryFrame {
858 time: 0.0,
859 index: 0,
860 positions: vec![1.0, 2.0, 3.0],
861 velocities: vec![0.1, 0.2, 0.3],
862 metrics: SimMetrics::new(),
863 };
864 traj.add_frame(frame);
865
866 assert_eq!(traj.len(), 1);
867 assert!((traj.metadata.start_time - 0.0).abs() < f64::EPSILON);
868 }
869
870 #[test]
871 fn test_trajectory_add_multiple_frames() {
872 let mut traj = Trajectory::new();
873 for i in 0..5 {
874 traj.add_frame(TrajectoryFrame {
875 time: i as f64,
876 index: i,
877 positions: vec![],
878 velocities: vec![],
879 metrics: SimMetrics::new(),
880 });
881 }
882 assert_eq!(traj.len(), 5);
883 assert!((traj.metadata.start_time - 0.0).abs() < f64::EPSILON);
884 assert!((traj.metadata.end_time - 4.0).abs() < f64::EPSILON);
885 }
886
887 #[test]
888 fn test_trajectory_duration() {
889 let mut traj = Trajectory::new();
890 traj.add_frame(TrajectoryFrame {
891 time: 0.0,
892 index: 0,
893 positions: vec![],
894 velocities: vec![],
895 metrics: SimMetrics::new(),
896 });
897 traj.add_frame(TrajectoryFrame {
898 time: 10.0,
899 index: 1,
900 positions: vec![],
901 velocities: vec![],
902 metrics: SimMetrics::new(),
903 });
904 assert!((traj.duration() - 10.0).abs() < f64::EPSILON);
905 }
906
907 #[test]
908 fn test_trajectory_frame() {
909 let mut traj = Trajectory::new();
910 traj.add_frame(TrajectoryFrame {
911 time: 1.0,
912 index: 0,
913 positions: vec![1.0, 2.0, 3.0],
914 velocities: vec![],
915 metrics: SimMetrics::new(),
916 });
917
918 let frame = traj.frame(0).unwrap();
919 assert_eq!(frame.index, 0);
920 assert!(traj.frame(1).is_none());
921 }
922
923 #[test]
924 fn test_trajectory_frame_at_time() {
925 let mut traj = Trajectory::new();
926 for i in 0..10 {
927 traj.add_frame(TrajectoryFrame {
928 time: i as f64,
929 index: i,
930 positions: vec![],
931 velocities: vec![],
932 metrics: SimMetrics::new(),
933 });
934 }
935
936 let frame = traj.frame_at_time(5.5).unwrap();
937 assert!(frame.index == 5 || frame.index == 6);
939 }
940
941 #[test]
942 fn test_trajectory_frame_at_time_empty() {
943 let traj = Trajectory::new();
944 assert!(traj.frame_at_time(5.0).is_none());
945 }
946
947 #[test]
948 fn test_trajectory_clone() {
949 let mut traj = Trajectory::new();
950 traj.add_frame(TrajectoryFrame {
951 time: 0.0,
952 index: 0,
953 positions: vec![1.0],
954 velocities: vec![],
955 metrics: SimMetrics::new(),
956 });
957 let cloned = traj.clone();
958 assert_eq!(cloned.len(), 1);
959 }
960
961 #[test]
962 fn test_trajectory_frame_clone() {
963 let frame = TrajectoryFrame {
964 time: 1.0,
965 index: 42,
966 positions: vec![1.0, 2.0],
967 velocities: vec![3.0, 4.0],
968 metrics: SimMetrics::new(),
969 };
970 let cloned = frame.clone();
971 assert_eq!(cloned.index, 42);
972 assert_eq!(cloned.positions.len(), 2);
973 }
974
975 #[test]
976 fn test_exporter_new() {
977 let exporter = Exporter::new();
978 assert!(matches!(exporter.config.format, ExportFormat::JsonLines));
979 }
980
981 #[test]
982 fn test_exporter_default() {
983 let exporter: Exporter = Default::default();
984 assert!(matches!(exporter.config.format, ExportFormat::JsonLines));
985 }
986
987 #[test]
988 fn test_exporter_with_config() {
989 let config = ExportConfig {
990 format: ExportFormat::Csv,
991 include_velocities: false,
992 include_metrics: false,
993 decimation: 2,
994 };
995 let exporter = Exporter::with_config(config);
996 assert!(matches!(exporter.config.format, ExportFormat::Csv));
997 assert!(!exporter.config.include_velocities);
998 }
999
1000 #[test]
1001 fn test_export_format_default() {
1002 let format = ExportFormat::default();
1003 assert!(matches!(format, ExportFormat::JsonLines));
1004 }
1005
1006 #[test]
1007 fn test_export_config_default() {
1008 let config = ExportConfig::default();
1009 assert!(config.include_velocities);
1010 assert!(config.include_metrics);
1011 assert_eq!(config.decimation, 1);
1012 }
1013
1014 #[test]
1015 fn test_export_to_json_lines() {
1016 let dir = tempdir().unwrap();
1017 let path = dir.path().join("test.jsonl");
1018
1019 let mut traj = Trajectory::new();
1020 traj.add_frame(TrajectoryFrame {
1021 time: 0.0,
1022 index: 0,
1023 positions: vec![1.0, 2.0, 3.0],
1024 velocities: vec![0.1, 0.2, 0.3],
1025 metrics: SimMetrics::new(),
1026 });
1027
1028 let exporter = Exporter::new();
1029 exporter.to_json_lines(&traj, &path).unwrap();
1030
1031 let mut content = String::new();
1032 File::open(&path)
1033 .unwrap()
1034 .read_to_string(&mut content)
1035 .unwrap();
1036 assert!(content.contains("\"time\":0.0"));
1037 assert!(content.contains("\"index\":0"));
1038 }
1039
1040 #[test]
1041 fn test_export_to_json_lines_decimation() {
1042 let dir = tempdir().unwrap();
1043 let path = dir.path().join("test.jsonl");
1044
1045 let mut traj = Trajectory::new();
1046 for i in 0..10 {
1047 traj.add_frame(TrajectoryFrame {
1048 time: i as f64,
1049 index: i,
1050 positions: vec![],
1051 velocities: vec![],
1052 metrics: SimMetrics::new(),
1053 });
1054 }
1055
1056 let config = ExportConfig {
1057 decimation: 2,
1058 ..Default::default()
1059 };
1060 let exporter = Exporter::with_config(config);
1061 exporter.to_json_lines(&traj, &path).unwrap();
1062
1063 let mut content = String::new();
1064 File::open(&path)
1065 .unwrap()
1066 .read_to_string(&mut content)
1067 .unwrap();
1068 let lines: Vec<_> = content.lines().collect();
1070 assert_eq!(lines.len(), 5);
1071 }
1072
1073 #[test]
1074 fn test_export_to_csv() {
1075 let dir = tempdir().unwrap();
1076 let path = dir.path().join("test.csv");
1077
1078 let mut traj = Trajectory::new();
1079 traj.add_frame(TrajectoryFrame {
1080 time: 0.0,
1081 index: 0,
1082 positions: vec![1.0, 2.0, 3.0],
1083 velocities: vec![0.1, 0.2, 0.3],
1084 metrics: SimMetrics::new(),
1085 });
1086
1087 let exporter = Exporter::new();
1088 exporter.to_csv(&traj, &path).unwrap();
1089
1090 let mut content = String::new();
1091 File::open(&path)
1092 .unwrap()
1093 .read_to_string(&mut content)
1094 .unwrap();
1095 assert!(content.contains("time,index"));
1096 assert!(content.contains("0,0"));
1097 }
1098
1099 #[test]
1100 fn test_export_to_csv_no_velocities() {
1101 let dir = tempdir().unwrap();
1102 let path = dir.path().join("test.csv");
1103
1104 let mut traj = Trajectory::new();
1105 traj.add_frame(TrajectoryFrame {
1106 time: 0.0,
1107 index: 0,
1108 positions: vec![1.0, 2.0, 3.0],
1109 velocities: vec![0.1, 0.2, 0.3],
1110 metrics: SimMetrics::new(),
1111 });
1112
1113 let config = ExportConfig {
1114 include_velocities: false,
1115 ..Default::default()
1116 };
1117 let exporter = Exporter::with_config(config);
1118 exporter.to_csv(&traj, &path).unwrap();
1119
1120 let mut content = String::new();
1121 File::open(&path)
1122 .unwrap()
1123 .read_to_string(&mut content)
1124 .unwrap();
1125 assert!(!content.lines().next().unwrap().contains("vx"));
1127 }
1128
1129 #[test]
1130 fn test_export_to_csv_no_metrics() {
1131 let dir = tempdir().unwrap();
1132 let path = dir.path().join("test.csv");
1133
1134 let mut traj = Trajectory::new();
1135 traj.add_frame(TrajectoryFrame {
1136 time: 0.0,
1137 index: 0,
1138 positions: vec![1.0, 2.0, 3.0],
1139 velocities: vec![],
1140 metrics: SimMetrics::new(),
1141 });
1142
1143 let config = ExportConfig {
1144 include_metrics: false,
1145 ..Default::default()
1146 };
1147 let exporter = Exporter::with_config(config);
1148 exporter.to_csv(&traj, &path).unwrap();
1149
1150 let mut content = String::new();
1151 File::open(&path)
1152 .unwrap()
1153 .read_to_string(&mut content)
1154 .unwrap();
1155 assert!(!content.contains("total_energy"));
1156 }
1157
1158 #[test]
1159 fn test_export_to_csv_decimation() {
1160 let dir = tempdir().unwrap();
1161 let path = dir.path().join("test.csv");
1162
1163 let mut traj = Trajectory::new();
1164 for i in 0..10 {
1165 traj.add_frame(TrajectoryFrame {
1166 time: i as f64,
1167 index: i,
1168 positions: vec![],
1169 velocities: vec![],
1170 metrics: SimMetrics::new(),
1171 });
1172 }
1173
1174 let config = ExportConfig {
1175 decimation: 3,
1176 ..Default::default()
1177 };
1178 let exporter = Exporter::with_config(config);
1179 exporter.to_csv(&traj, &path).unwrap();
1180
1181 let mut content = String::new();
1182 File::open(&path)
1183 .unwrap()
1184 .read_to_string(&mut content)
1185 .unwrap();
1186 let lines: Vec<_> = content.lines().collect();
1188 assert_eq!(lines.len(), 5); }
1190
1191 #[test]
1192 fn test_export_to_binary() {
1193 let dir = tempdir().unwrap();
1194 let path = dir.path().join("test.bin");
1195
1196 let mut traj = Trajectory::new();
1197 traj.add_frame(TrajectoryFrame {
1198 time: 0.0,
1199 index: 0,
1200 positions: vec![1.0, 2.0, 3.0],
1201 velocities: vec![0.1, 0.2, 0.3],
1202 metrics: SimMetrics::new(),
1203 });
1204
1205 let exporter = Exporter::new();
1206 exporter.to_binary(&traj, &path).unwrap();
1207
1208 let loaded = Exporter::from_binary(&path).unwrap();
1210 assert_eq!(loaded.len(), 1);
1211 assert!((loaded.frames[0].time - 0.0).abs() < f64::EPSILON);
1212 }
1213
1214 #[test]
1215 fn test_export_generic() {
1216 let dir = tempdir().unwrap();
1217 let path = dir.path().join("test.jsonl");
1218
1219 let mut traj = Trajectory::new();
1220 traj.add_frame(TrajectoryFrame {
1221 time: 0.0,
1222 index: 0,
1223 positions: vec![],
1224 velocities: vec![],
1225 metrics: SimMetrics::new(),
1226 });
1227
1228 let exporter = Exporter::new();
1229 exporter.export(&traj, &path).unwrap();
1230
1231 assert!(path.exists());
1232 }
1233
1234 #[test]
1235 fn test_export_parquet_unsupported() {
1236 let dir = tempdir().unwrap();
1237 let path = dir.path().join("test.parquet");
1238
1239 let traj = Trajectory::new();
1240 let config = ExportConfig {
1241 format: ExportFormat::Parquet {
1242 compression: ParquetCompression::Snappy,
1243 },
1244 ..Default::default()
1245 };
1246 let exporter = Exporter::with_config(config);
1247 let result = exporter.export(&traj, &path);
1248 assert!(result.is_err());
1249 }
1250
1251 #[test]
1252 fn test_export_video_unsupported() {
1253 let dir = tempdir().unwrap();
1254 let path = dir.path().join("test.mp4");
1255
1256 let traj = Trajectory::new();
1257 let config = ExportConfig {
1258 format: ExportFormat::Video {
1259 format: VideoFormat::Mp4,
1260 fps: 30,
1261 },
1262 ..Default::default()
1263 };
1264 let exporter = Exporter::with_config(config);
1265 let result = exporter.export(&traj, &path);
1266 assert!(result.is_err());
1267 }
1268
1269 #[test]
1270 fn test_streaming_exporter() {
1271 let dir = tempdir().unwrap();
1272 let path = dir.path().join("stream.jsonl");
1273
1274 let mut stream = StreamingExporter::new(&path, 1).unwrap();
1275 assert_eq!(stream.frame_count(), 0);
1276
1277 let frame = TrajectoryFrame {
1278 time: 0.0,
1279 index: 0,
1280 positions: vec![1.0, 2.0, 3.0],
1281 velocities: vec![],
1282 metrics: SimMetrics::new(),
1283 };
1284 stream.write_frame(&frame).unwrap();
1285 assert_eq!(stream.frame_count(), 1);
1286
1287 let count = stream.finish().unwrap();
1288 assert_eq!(count, 1);
1289 }
1290
1291 #[test]
1292 fn test_streaming_exporter_decimation() {
1293 let dir = tempdir().unwrap();
1294 let path = dir.path().join("stream.jsonl");
1295
1296 let mut stream = StreamingExporter::new(&path, 2).unwrap();
1297
1298 for i in 0..10 {
1299 let frame = TrajectoryFrame {
1300 time: i as f64,
1301 index: i,
1302 positions: vec![],
1303 velocities: vec![],
1304 metrics: SimMetrics::new(),
1305 };
1306 stream.write_frame(&frame).unwrap();
1307 }
1308
1309 let count = stream.finish().unwrap();
1310 assert_eq!(count, 5); }
1312
1313 #[test]
1314 fn test_video_format_eq() {
1315 assert_eq!(VideoFormat::Mp4, VideoFormat::Mp4);
1316 assert_ne!(VideoFormat::Mp4, VideoFormat::Gif);
1317 assert_ne!(VideoFormat::Gif, VideoFormat::WebM);
1318 }
1319
1320 #[test]
1321 fn test_parquet_compression_eq() {
1322 assert_eq!(ParquetCompression::None, ParquetCompression::None);
1323 assert_ne!(ParquetCompression::None, ParquetCompression::Snappy);
1324 assert_ne!(ParquetCompression::Zstd, ParquetCompression::Lz4);
1325 }
1326
1327 #[test]
1328 fn test_export_format_parquet_variants() {
1329 let _ = ExportFormat::Parquet {
1330 compression: ParquetCompression::None,
1331 };
1332 let _ = ExportFormat::Parquet {
1333 compression: ParquetCompression::Zstd,
1334 };
1335 let _ = ExportFormat::Parquet {
1336 compression: ParquetCompression::Lz4,
1337 };
1338 }
1339
1340 #[test]
1341 fn test_export_format_video_variants() {
1342 let _ = ExportFormat::Video {
1343 format: VideoFormat::Gif,
1344 fps: 24,
1345 };
1346 let _ = ExportFormat::Video {
1347 format: VideoFormat::WebM,
1348 fps: 60,
1349 };
1350 }
1351
1352 #[test]
1353 fn test_trajectory_metadata_default() {
1354 let meta: TrajectoryMetadata = Default::default();
1355 assert!(meta.name.is_empty());
1356 assert_eq!(meta.body_count, 0);
1357 }
1358
1359 #[test]
1362 fn test_sim_metrics_update_from_state() {
1363 use crate::engine::state::Vec3;
1364 let mut state = SimState::new();
1365 state.add_body(1.0, Vec3::new(1.0, 2.0, 3.0), Vec3::new(0.5, 0.0, 0.0));
1366 state.set_potential_energy(-10.0);
1367
1368 let mut metrics = SimMetrics::new();
1369 metrics.update_from_state(&state, crate::engine::SimTime::from_secs(1.5));
1370
1371 assert_eq!(metrics.body_count, 1);
1372 assert!((metrics.time - 1.5).abs() < f64::EPSILON);
1373 assert!(metrics.kinetic_energy.is_some());
1374 assert!(metrics.potential_energy.is_some());
1375 assert!(metrics.total_energy.is_some());
1376 }
1377
1378 #[test]
1379 fn test_sim_metrics_debug() {
1380 let metrics = SimMetrics::new();
1381 let debug = format!("{:?}", metrics);
1382 assert!(debug.contains("SimMetrics"));
1383 }
1384
1385 #[test]
1386 fn test_trajectory_frame_from_state() {
1387 use crate::engine::state::Vec3;
1388 let mut state = SimState::new();
1389 state.add_body(1.0, Vec3::new(1.0, 2.0, 3.0), Vec3::new(0.5, 0.0, 0.0));
1390
1391 let frame = TrajectoryFrame::from_state(&state, crate::engine::SimTime::from_secs(1.0), 42);
1392
1393 assert!((frame.time - 1.0).abs() < f64::EPSILON);
1394 assert_eq!(frame.index, 42);
1395 assert_eq!(frame.positions.len(), 3); assert_eq!(frame.velocities.len(), 3);
1397 }
1398
1399 #[test]
1400 fn test_trajectory_frame_debug() {
1401 let frame = TrajectoryFrame {
1402 time: 0.0,
1403 index: 0,
1404 positions: vec![],
1405 velocities: vec![],
1406 metrics: SimMetrics::new(),
1407 };
1408 let debug = format!("{:?}", frame);
1409 assert!(debug.contains("TrajectoryFrame"));
1410 }
1411
1412 #[test]
1413 fn test_trajectory_debug() {
1414 let traj = Trajectory::new();
1415 let debug = format!("{:?}", traj);
1416 assert!(debug.contains("Trajectory"));
1417 }
1418
1419 #[test]
1420 fn test_time_series_clear_with_capacity_check() {
1421 let mut series = TimeSeries::new("test", 100);
1422 series.push(0.0, 1.0);
1423 series.push(1.0, 2.0);
1424 assert!(!series.is_empty());
1425
1426 series.clear();
1427 assert!(series.is_empty());
1428 assert_eq!(series.len(), 0);
1429 assert_eq!(series.name(), "test");
1431 }
1432
1433 #[test]
1434 fn test_time_series_data_access() {
1435 let mut series = TimeSeries::new("test", 100);
1436 series.push(0.5, 1.0);
1437 series.push(1.5, 2.0);
1438 series.push(2.5, 3.0);
1439
1440 let data = series.data();
1441 assert_eq!(data.len(), 3);
1442 assert!((data.front().unwrap().time - 0.5).abs() < f64::EPSILON);
1443 assert!((data.back().unwrap().time - 2.5).abs() < f64::EPSILON);
1444 }
1445
1446 #[test]
1447 fn test_data_point_debug_clone() {
1448 let dp = DataPoint {
1449 time: 1.0,
1450 value: 2.0,
1451 };
1452 let cloned = dp.clone();
1453 assert!((cloned.time - 1.0).abs() < f64::EPSILON);
1454 assert!((cloned.value - 2.0).abs() < f64::EPSILON);
1455
1456 let debug = format!("{:?}", dp);
1457 assert!(debug.contains("DataPoint"));
1458 }
1459
1460 #[test]
1461 fn test_video_format_debug_clone() {
1462 let vf = VideoFormat::Mp4;
1463 let cloned = vf.clone();
1464 assert_eq!(cloned, VideoFormat::Mp4);
1465
1466 let debug = format!("{:?}", vf);
1467 assert!(debug.contains("Mp4"));
1468 }
1469
1470 #[test]
1471 fn test_parquet_compression_debug_clone() {
1472 let pc = ParquetCompression::Zstd;
1473 let cloned = pc.clone();
1474 assert_eq!(cloned, ParquetCompression::Zstd);
1475
1476 let debug = format!("{:?}", pc);
1477 assert!(debug.contains("Zstd"));
1478 }
1479
1480 #[test]
1481 fn test_export_format_debug_clone() {
1482 let ef = ExportFormat::Csv;
1483 let cloned = ef.clone();
1484 assert!(matches!(cloned, ExportFormat::Csv));
1485
1486 let debug = format!("{:?}", ef);
1487 assert!(debug.contains("Csv"));
1488 }
1489
1490 #[test]
1491 fn test_export_config_debug_clone() {
1492 let config = ExportConfig::default();
1493 let cloned = config.clone();
1494 assert_eq!(cloned.decimation, config.decimation);
1495
1496 let debug = format!("{:?}", config);
1497 assert!(debug.contains("ExportConfig"));
1498 }
1499
1500 #[test]
1501 fn test_exporter_debug() {
1502 let exporter = Exporter::new();
1503 let debug = format!("{:?}", exporter);
1504 assert!(debug.contains("Exporter"));
1505 }
1506
1507 #[test]
1508 fn test_trajectory_metadata_debug_clone() {
1509 let meta = TrajectoryMetadata {
1510 name: "test".to_string(),
1511 body_count: 5,
1512 start_time: 0.0,
1513 end_time: 10.0,
1514 timestep: 0.01,
1515 seed: 42,
1516 };
1517 let cloned = meta.clone();
1518 assert_eq!(cloned.name, "test");
1519
1520 let debug = format!("{:?}", meta);
1521 assert!(debug.contains("TrajectoryMetadata"));
1522 }
1523
1524 #[test]
1525 fn test_trajectory_clone_with_positions() {
1526 let mut traj = Trajectory::new();
1527 traj.add_frame(TrajectoryFrame {
1528 time: 0.0,
1529 index: 0,
1530 positions: vec![1.0, 2.0, 3.0],
1531 velocities: vec![],
1532 metrics: SimMetrics::new(),
1533 });
1534
1535 let cloned = traj.clone();
1536 assert_eq!(cloned.len(), 1);
1537 assert!(cloned.frame(0).is_some());
1539 }
1540
1541 #[test]
1542 fn test_time_series_debug_impl() {
1543 let series = TimeSeries::new("test", 10);
1544 let debug = format!("{:?}", series);
1545 assert!(debug.contains("TimeSeries"));
1546 }
1547
1548 #[test]
1549 fn test_time_series_clone_impl() {
1550 let mut series = TimeSeries::new("test", 10);
1551 series.push(1.0, 100.0);
1552 let cloned = series.clone();
1553 assert_eq!(cloned.len(), 1);
1554 assert_eq!(cloned.name(), "test");
1555 }
1556
1557 #[test]
1558 fn test_export_binary_format() {
1559 let dir = tempdir().unwrap();
1560 let path = dir.path().join("test.bin");
1561
1562 let mut traj = Trajectory::new();
1563 traj.add_frame(TrajectoryFrame {
1564 time: 0.0,
1565 index: 0,
1566 positions: vec![1.0],
1567 velocities: vec![0.1],
1568 metrics: SimMetrics::new(),
1569 });
1570
1571 let config = ExportConfig {
1572 format: ExportFormat::Binary,
1573 ..Default::default()
1574 };
1575 let exporter = Exporter::with_config(config);
1576 exporter.export(&traj, &path).unwrap();
1577
1578 assert!(path.exists());
1579 }
1580}