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