1#![allow(dead_code)]
23#![allow(clippy::too_many_arguments)]
24
25use std::collections::HashMap;
26
27#[derive(Debug, Clone, Default)]
36pub struct ParticleDataset {
37 pub ids: Vec<u64>,
39 pub positions: Vec<[f64; 3]>,
41 pub velocities: Vec<[f64; 3]>,
43 pub masses: Vec<f64>,
45 pub radii: Vec<f64>,
47 pub properties: HashMap<String, Vec<f64>>,
49 pub time: f64,
51 pub step: u64,
53}
54
55impl ParticleDataset {
56 pub fn new() -> Self {
58 Self::default()
59 }
60
61 pub fn with_time(time: f64, step: u64) -> Self {
63 Self {
64 time,
65 step,
66 ..Default::default()
67 }
68 }
69
70 pub fn len(&self) -> usize {
72 self.ids.len()
73 }
74
75 pub fn is_empty(&self) -> bool {
77 self.ids.is_empty()
78 }
79
80 pub fn add_particle(
82 &mut self,
83 id: u64,
84 position: [f64; 3],
85 velocity: [f64; 3],
86 mass: f64,
87 radius: f64,
88 ) -> usize {
89 let idx = self.ids.len();
90 self.ids.push(id);
91 self.positions.push(position);
92 self.velocities.push(velocity);
93 self.masses.push(mass);
94 self.radii.push(radius);
95 idx
96 }
97
98 pub fn add_property(&mut self, name: impl Into<String>, values: Vec<f64>) {
103 self.properties.insert(name.into(), values);
104 }
105
106 pub fn position(&self, i: usize) -> [f64; 3] {
108 self.positions[i]
109 }
110
111 pub fn velocity(&self, i: usize) -> [f64; 3] {
113 self.velocities[i]
114 }
115
116 pub fn bounding_box(&self) -> Option<([f64; 3], [f64; 3])> {
120 if self.positions.is_empty() {
121 return None;
122 }
123 let mut mn = [f64::INFINITY; 3];
124 let mut mx = [f64::NEG_INFINITY; 3];
125 for p in &self.positions {
126 for k in 0..3 {
127 if p[k] < mn[k] {
128 mn[k] = p[k];
129 }
130 if p[k] > mx[k] {
131 mx[k] = p[k];
132 }
133 }
134 }
135 Some((mn, mx))
136 }
137
138 pub fn centroid(&self) -> [f64; 3] {
140 if self.positions.is_empty() {
141 return [0.0; 3];
142 }
143 let n = self.positions.len() as f64;
144 let mut sum = [0.0; 3];
145 for p in &self.positions {
146 sum[0] += p[0];
147 sum[1] += p[1];
148 sum[2] += p[2];
149 }
150 [sum[0] / n, sum[1] / n, sum[2] / n]
151 }
152
153 pub fn kinetic_energy(&self) -> f64 {
155 self.masses
156 .iter()
157 .zip(self.velocities.iter())
158 .map(|(m, v)| {
159 let v2 = v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
160 0.5 * m * v2
161 })
162 .sum()
163 }
164
165 pub fn max_speed(&self) -> f64 {
167 self.velocities
168 .iter()
169 .map(|v| (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt())
170 .fold(0.0_f64, f64::max)
171 }
172
173 pub fn merge(&mut self, other: &ParticleDataset) {
175 self.ids.extend_from_slice(&other.ids);
176 self.positions.extend_from_slice(&other.positions);
177 self.velocities.extend_from_slice(&other.velocities);
178 self.masses.extend_from_slice(&other.masses);
179 self.radii.extend_from_slice(&other.radii);
180 }
181
182 pub fn filter<F>(&self, pred: F) -> ParticleDataset
184 where
185 F: Fn(usize) -> bool,
186 {
187 let mut out = ParticleDataset::with_time(self.time, self.step);
188 for i in 0..self.ids.len() {
189 if pred(i) {
190 out.add_particle(
191 self.ids[i],
192 self.positions[i],
193 self.velocities[i],
194 self.masses[i],
195 self.radii[i],
196 );
197 }
198 }
199 out
200 }
201}
202
203#[derive(Debug, Clone)]
209pub struct H5PartStep {
210 pub step: u64,
212 pub time: f64,
214 pub dataset: ParticleDataset,
216}
217
218#[derive(Debug, Clone, Default)]
224pub struct H5partReader {
225 pub steps: Vec<H5PartStep>,
227 pub attributes: HashMap<String, String>,
229}
230
231impl H5partReader {
232 pub fn new() -> Self {
234 Self::default()
235 }
236
237 pub fn from_bytes(data: &[u8]) -> crate::Result<Self> {
242 if data.len() < 8 || &data[..6] != b"H5PART" {
244 return Err(crate::Error::Parse("not a valid H5Part file".to_string()));
245 }
246 Ok(Self::default())
247 }
248
249 pub fn num_steps(&self) -> usize {
251 self.steps.len()
252 }
253
254 pub fn step(&self, idx: usize) -> Option<&H5PartStep> {
256 self.steps.get(idx)
257 }
258
259 pub fn times(&self) -> Vec<f64> {
261 self.steps.iter().map(|s| s.time).collect()
262 }
263
264 pub fn dataset_names(&self) -> Vec<String> {
266 if let Some(step) = self.steps.first() {
267 let mut names = vec![
268 "x".to_string(),
269 "y".to_string(),
270 "z".to_string(),
271 "vx".to_string(),
272 "vy".to_string(),
273 "vz".to_string(),
274 "mass".to_string(),
275 "radius".to_string(),
276 "id".to_string(),
277 ];
278 names.extend(step.dataset.properties.keys().cloned());
279 names
280 } else {
281 vec![]
282 }
283 }
284
285 pub fn push_step(&mut self, step: H5PartStep) {
287 self.steps.push(step);
288 }
289
290 pub fn iter_steps(&self) -> impl Iterator<Item = &H5PartStep> {
292 self.steps.iter()
293 }
294
295 pub fn step_near(&self, target_time: f64) -> Option<&H5PartStep> {
297 self.steps.iter().min_by(|a, b| {
298 (a.time - target_time)
299 .abs()
300 .partial_cmp(&(b.time - target_time).abs())
301 .unwrap_or(std::cmp::Ordering::Equal)
302 })
303 }
304}
305
306#[derive(Debug, Default)]
314pub struct H5partWriter {
315 steps: Vec<H5PartStep>,
316 attributes: HashMap<String, String>,
317 scalar_fields: Vec<String>,
318 vector_fields: Vec<String>,
319}
320
321impl H5partWriter {
322 pub fn new() -> Self {
324 Self::default()
325 }
326
327 pub fn set_attribute(&mut self, key: impl Into<String>, value: impl Into<String>) {
329 self.attributes.insert(key.into(), value.into());
330 }
331
332 pub fn register_scalar_field(&mut self, name: impl Into<String>) {
334 self.scalar_fields.push(name.into());
335 }
336
337 pub fn register_vector_field(&mut self, name: impl Into<String>) {
339 self.vector_fields.push(name.into());
340 }
341
342 pub fn write_step(&mut self, dataset: ParticleDataset) {
344 let step_idx = self.steps.len() as u64;
345 let time = dataset.time;
346 self.steps.push(H5PartStep {
347 step: step_idx,
348 time,
349 dataset,
350 });
351 }
352
353 pub fn num_steps(&self) -> usize {
355 self.steps.len()
356 }
357
358 pub fn to_bytes(&self) -> Vec<u8> {
360 let mut out = b"H5PART\0\0".to_vec();
361 out.extend_from_slice(&(self.steps.len() as u64).to_le_bytes());
363 for step in &self.steps {
364 out.extend_from_slice(&step.step.to_le_bytes());
365 out.extend_from_slice(&step.time.to_le_bytes());
366 out.extend_from_slice(&(step.dataset.len() as u64).to_le_bytes());
367 for i in 0..step.dataset.len() {
368 out.extend_from_slice(&step.dataset.positions[i][0].to_le_bytes());
369 out.extend_from_slice(&step.dataset.positions[i][1].to_le_bytes());
370 out.extend_from_slice(&step.dataset.positions[i][2].to_le_bytes());
371 out.extend_from_slice(&step.dataset.velocities[i][0].to_le_bytes());
372 out.extend_from_slice(&step.dataset.velocities[i][1].to_le_bytes());
373 out.extend_from_slice(&step.dataset.velocities[i][2].to_le_bytes());
374 out.extend_from_slice(&step.dataset.masses[i].to_le_bytes());
375 out.extend_from_slice(&step.dataset.radii[i].to_le_bytes());
376 }
377 }
378 out
379 }
380
381 pub fn write_to_file(&self, path: &str) -> crate::Result<()> {
383 let bytes = self.to_bytes();
384 std::fs::write(path, bytes).map_err(crate::Error::from)?;
385 Ok(())
386 }
387
388 pub fn into_reader(self) -> H5partReader {
390 H5partReader {
391 steps: self.steps,
392 attributes: self.attributes,
393 }
394 }
395}
396
397#[derive(Debug, Default)]
406pub struct ParticleVtkWriter {
407 pub binary: bool,
409 pub scalar_attributes: Vec<String>,
411 pub include_velocity: bool,
413 pub include_ids: bool,
415}
416
417impl ParticleVtkWriter {
418 pub fn new() -> Self {
420 Self {
421 binary: false,
422 scalar_attributes: vec![],
423 include_velocity: true,
424 include_ids: true,
425 }
426 }
427
428 pub fn with_binary(mut self) -> Self {
430 self.binary = true;
431 self
432 }
433
434 pub fn with_scalar(mut self, name: impl Into<String>) -> Self {
436 self.scalar_attributes.push(name.into());
437 self
438 }
439
440 pub fn write_to_string(&self, ds: &ParticleDataset) -> crate::Result<String> {
442 let mut s = String::new();
443 s.push_str("<?xml version=\"1.0\"?>\n");
444 s.push_str("<VTKFile type=\"PolyData\" version=\"0.1\" byte_order=\"LittleEndian\">\n");
445 s.push_str(" <PolyData>\n");
446 s.push_str(&format!(
447 " <Piece NumberOfPoints=\"{}\" NumberOfVerts=\"{}\">\n",
448 ds.len(),
449 ds.len()
450 ));
451
452 s.push_str(" <Points>\n");
454 s.push_str(
455 " <DataArray type=\"Float64\" NumberOfComponents=\"3\" format=\"ascii\">\n",
456 );
457 for p in &ds.positions {
458 s.push_str(&format!(" {} {} {}\n", p[0], p[1], p[2]));
459 }
460 s.push_str(" </DataArray>\n");
461 s.push_str(" </Points>\n");
462
463 s.push_str(" <Verts>\n");
465 s.push_str(" <DataArray type=\"Int64\" Name=\"connectivity\" format=\"ascii\">\n");
466 s.push_str(" ");
467 for i in 0..ds.len() {
468 s.push_str(&format!(" {i}"));
469 }
470 s.push('\n');
471 s.push_str(" </DataArray>\n");
472 s.push_str(" <DataArray type=\"Int64\" Name=\"offsets\" format=\"ascii\">\n");
473 s.push_str(" ");
474 for i in 1..=ds.len() {
475 s.push_str(&format!(" {i}"));
476 }
477 s.push('\n');
478 s.push_str(" </DataArray>\n");
479 s.push_str(" </Verts>\n");
480
481 s.push_str(" <PointData>\n");
483
484 s.push_str(" <DataArray type=\"Float64\" Name=\"radius\" format=\"ascii\">\n");
486 s.push_str(" ");
487 for r in &ds.radii {
488 s.push_str(&format!(" {r}"));
489 }
490 s.push('\n');
491 s.push_str(" </DataArray>\n");
492
493 s.push_str(" <DataArray type=\"Float64\" Name=\"mass\" format=\"ascii\">\n");
495 s.push_str(" ");
496 for m in &ds.masses {
497 s.push_str(&format!(" {m}"));
498 }
499 s.push('\n');
500 s.push_str(" </DataArray>\n");
501
502 if self.include_ids {
503 s.push_str(" <DataArray type=\"Int64\" Name=\"id\" format=\"ascii\">\n");
504 s.push_str(" ");
505 for id in &ds.ids {
506 s.push_str(&format!(" {id}"));
507 }
508 s.push('\n');
509 s.push_str(" </DataArray>\n");
510 }
511
512 if self.include_velocity {
513 s.push_str(
514 " <DataArray type=\"Float64\" Name=\"velocity\" NumberOfComponents=\"3\" format=\"ascii\">\n",
515 );
516 for v in &ds.velocities {
517 s.push_str(&format!(" {} {} {}\n", v[0], v[1], v[2]));
518 }
519 s.push_str(" </DataArray>\n");
520 }
521
522 for attr in &self.scalar_attributes {
524 if let Some(vals) = ds.properties.get(attr) {
525 s.push_str(&format!(
526 " <DataArray type=\"Float64\" Name=\"{attr}\" format=\"ascii\">\n"
527 ));
528 s.push_str(" ");
529 for v in vals {
530 s.push_str(&format!(" {v}"));
531 }
532 s.push('\n');
533 s.push_str(" </DataArray>\n");
534 }
535 }
536
537 s.push_str(" </PointData>\n");
538 s.push_str(" </Piece>\n");
539 s.push_str(" </PolyData>\n");
540 s.push_str("</VTKFile>\n");
541 Ok(s)
542 }
543
544 pub fn write_to_file(&self, ds: &ParticleDataset, path: &str) -> crate::Result<()> {
546 let content = self.write_to_string(ds)?;
547 std::fs::write(path, content).map_err(crate::Error::from)?;
548 Ok(())
549 }
550}
551
552#[derive(Debug, Default)]
571pub struct ParticleJsonWriter {
572 pub include_bbox: bool,
574 pub include_velocity: bool,
576 pub include_properties: bool,
578 pub precision: usize,
580}
581
582impl ParticleJsonWriter {
583 pub fn new() -> Self {
585 Self {
586 include_bbox: true,
587 include_velocity: true,
588 include_properties: true,
589 precision: 6,
590 }
591 }
592
593 pub fn write_to_string(ds: &ParticleDataset, include_bbox: bool) -> crate::Result<String> {
595 let writer = ParticleJsonWriter {
596 include_bbox,
597 include_velocity: true,
598 include_properties: true,
599 precision: 6,
600 };
601 writer.format(ds)
602 }
603
604 pub fn format(&self, ds: &ParticleDataset) -> crate::Result<String> {
606 let prec = self.precision;
607 let mut s = String::new();
608 s.push_str("{\n");
609 s.push_str(&format!(" \"time\": {:.prec$},\n", ds.time));
610 s.push_str(&format!(" \"step\": {},\n", ds.step));
611
612 if self.include_bbox {
613 match ds.bounding_box() {
614 Some((mn, mx)) => {
615 s.push_str(&format!(
616 " \"bbox\": {{ \"min\": [{:.prec$},{:.prec$},{:.prec$}], \"max\": [{:.prec$},{:.prec$},{:.prec$}] }},\n",
617 mn[0], mn[1], mn[2], mx[0], mx[1], mx[2]
618 ));
619 }
620 None => {
621 s.push_str(" \"bbox\": null,\n");
622 }
623 }
624 }
625
626 s.push_str(" \"particles\": [\n");
627 for i in 0..ds.len() {
628 let p = ds.positions[i];
629 let v = ds.velocities[i];
630 s.push_str(" {");
631 s.push_str(&format!("\"id\": {}", ds.ids[i]));
632 s.push_str(&format!(
633 ", \"pos\": [{:.prec$},{:.prec$},{:.prec$}]",
634 p[0], p[1], p[2]
635 ));
636 if self.include_velocity {
637 s.push_str(&format!(
638 ", \"vel\": [{:.prec$},{:.prec$},{:.prec$}]",
639 v[0], v[1], v[2]
640 ));
641 }
642 s.push_str(&format!(
643 ", \"mass\": {:.prec$}, \"radius\": {:.prec$}",
644 ds.masses[i], ds.radii[i]
645 ));
646
647 if self.include_properties && !ds.properties.is_empty() {
648 s.push_str(", \"props\": {");
649 let mut first = true;
650 let mut keys: Vec<&String> = ds.properties.keys().collect();
651 keys.sort();
652 for key in keys {
653 if let Some(vals) = ds.properties.get(key)
654 && let Some(val) = vals.get(i)
655 {
656 if !first {
657 s.push_str(", ");
658 }
659 s.push_str(&format!("\"{key}\": {val:.prec$}"));
660 first = false;
661 }
662 }
663 s.push('}');
664 }
665
666 s.push('}');
667 if i + 1 < ds.len() {
668 s.push(',');
669 }
670 s.push('\n');
671 }
672 s.push_str(" ]\n");
673 s.push_str("}\n");
674 Ok(s)
675 }
676
677 pub fn write_to_file(&self, ds: &ParticleDataset, path: &str) -> crate::Result<()> {
679 let content = self.format(ds)?;
680 std::fs::write(path, content).map_err(crate::Error::from)?;
681 Ok(())
682 }
683}
684
685#[derive(Debug, Clone)]
691pub struct ParticleDelta {
692 pub id: u64,
694 pub delta_pos: [f64; 3],
696 pub delta_vel: [f64; 3],
698 pub is_new: bool,
700 pub is_removed: bool,
702}
703
704#[derive(Debug, Default)]
709pub struct ParticleDiffWriter {
710 previous: Option<ParticleDataset>,
712 pub pos_threshold: f64,
714 pub vel_threshold: f64,
716 pub deltas: Vec<(u64, Vec<ParticleDelta>)>,
718}
719
720impl ParticleDiffWriter {
721 pub fn new() -> Self {
723 Self {
724 previous: None,
725 pos_threshold: 1e-9,
726 vel_threshold: 1e-9,
727 deltas: vec![],
728 }
729 }
730
731 pub fn with_thresholds(pos_threshold: f64, vel_threshold: f64) -> Self {
733 Self {
734 previous: None,
735 pos_threshold,
736 vel_threshold,
737 deltas: vec![],
738 }
739 }
740
741 pub fn write_frame(&mut self, current: &ParticleDataset) -> usize {
745 let step = current.step;
746 let deltas = match &self.previous {
747 None => {
748 current
750 .ids
751 .iter()
752 .enumerate()
753 .map(|(i, &id)| ParticleDelta {
754 id,
755 delta_pos: current.positions[i],
756 delta_vel: current.velocities[i],
757 is_new: true,
758 is_removed: false,
759 })
760 .collect::<Vec<_>>()
761 }
762 Some(prev) => {
763 let prev_map: HashMap<u64, usize> = prev
764 .ids
765 .iter()
766 .enumerate()
767 .map(|(i, &id)| (id, i))
768 .collect();
769 let curr_map: HashMap<u64, usize> = current
770 .ids
771 .iter()
772 .enumerate()
773 .map(|(i, &id)| (id, i))
774 .collect();
775
776 let mut out = vec![];
777
778 for (i, &id) in current.ids.iter().enumerate() {
780 if let Some(&pi) = prev_map.get(&id) {
781 let dp = [
782 current.positions[i][0] - prev.positions[pi][0],
783 current.positions[i][1] - prev.positions[pi][1],
784 current.positions[i][2] - prev.positions[pi][2],
785 ];
786 let dv = [
787 current.velocities[i][0] - prev.velocities[pi][0],
788 current.velocities[i][1] - prev.velocities[pi][1],
789 current.velocities[i][2] - prev.velocities[pi][2],
790 ];
791 let pos_changed = dp.iter().any(|x| x.abs() > self.pos_threshold);
792 let vel_changed = dv.iter().any(|x| x.abs() > self.vel_threshold);
793 if pos_changed || vel_changed {
794 out.push(ParticleDelta {
795 id,
796 delta_pos: dp,
797 delta_vel: dv,
798 is_new: false,
799 is_removed: false,
800 });
801 }
802 } else {
803 out.push(ParticleDelta {
804 id,
805 delta_pos: current.positions[i],
806 delta_vel: current.velocities[i],
807 is_new: true,
808 is_removed: false,
809 });
810 }
811 }
812
813 for &id in prev.ids.iter() {
815 if !curr_map.contains_key(&id) {
816 out.push(ParticleDelta {
817 id,
818 delta_pos: [0.0; 3],
819 delta_vel: [0.0; 3],
820 is_new: false,
821 is_removed: true,
822 });
823 }
824 }
825
826 out
827 }
828 };
829
830 let n = deltas.len();
831 self.deltas.push((step, deltas));
832 self.previous = Some(current.clone());
833 n
834 }
835
836 pub fn total_deltas(&self) -> usize {
838 self.deltas.iter().map(|(_, d)| d.len()).sum()
839 }
840
841 pub fn to_bytes(&self) -> Vec<u8> {
843 let mut out = b"PDIFF\0\0\0".to_vec();
844 out.extend_from_slice(&(self.deltas.len() as u64).to_le_bytes());
845 for (step, deltas) in &self.deltas {
846 out.extend_from_slice(&step.to_le_bytes());
847 out.extend_from_slice(&(deltas.len() as u64).to_le_bytes());
848 for d in deltas {
849 out.extend_from_slice(&d.id.to_le_bytes());
850 let flags: u8 = (d.is_new as u8) | ((d.is_removed as u8) << 1);
851 out.push(flags);
852 for x in &d.delta_pos {
853 out.extend_from_slice(&x.to_le_bytes());
854 }
855 for x in &d.delta_vel {
856 out.extend_from_slice(&x.to_le_bytes());
857 }
858 }
859 }
860 out
861 }
862
863 pub fn reconstruct(&self, through_step: u64) -> ParticleDataset {
865 let mut state: HashMap<u64, (usize, [f64; 3], [f64; 3])> = HashMap::new();
866 let mut pos_map: HashMap<u64, [f64; 3]> = HashMap::new();
868 let mut vel_map: HashMap<u64, [f64; 3]> = HashMap::new();
869
870 for (step, deltas) in &self.deltas {
871 if *step > through_step {
872 break;
873 }
874 for d in deltas {
875 if d.is_removed {
876 pos_map.remove(&d.id);
877 vel_map.remove(&d.id);
878 state.remove(&d.id);
879 } else if d.is_new {
880 pos_map.insert(d.id, d.delta_pos);
881 vel_map.insert(d.id, d.delta_vel);
882 } else {
883 let p = pos_map.entry(d.id).or_insert([0.0; 3]);
884 let v = vel_map.entry(d.id).or_insert([0.0; 3]);
885 for k in 0..3 {
886 p[k] += d.delta_pos[k];
887 v[k] += d.delta_vel[k];
888 }
889 }
890 }
891 }
892
893 let mut ds = ParticleDataset::with_time(0.0, through_step);
894 let mut ids: Vec<u64> = pos_map.keys().cloned().collect();
895 ids.sort();
896 for id in ids {
897 let pos = pos_map[&id];
898 let vel = vel_map[&id];
899 ds.add_particle(id, pos, vel, 1.0, 0.1);
900 }
901 let _ = state; ds
903 }
904}
905
906#[derive(Debug, Default)]
912pub struct ParticleTrajectory {
913 pub frames: Vec<ParticleDataset>,
915 pub title: String,
917}
918
919impl ParticleTrajectory {
920 pub fn new(title: impl Into<String>) -> Self {
922 Self {
923 frames: vec![],
924 title: title.into(),
925 }
926 }
927
928 pub fn push(&mut self, ds: ParticleDataset) {
930 self.frames.push(ds);
931 }
932
933 pub fn num_frames(&self) -> usize {
935 self.frames.len()
936 }
937
938 pub fn time_span(&self) -> Option<(f64, f64)> {
940 if self.frames.is_empty() {
941 return None;
942 }
943 let t0 = self
944 .frames
945 .first()
946 .expect("collection should not be empty")
947 .time;
948 let t1 = self
949 .frames
950 .last()
951 .expect("collection should not be empty")
952 .time;
953 Some((t0, t1))
954 }
955
956 pub fn frame_at(&self, t: f64) -> Option<&ParticleDataset> {
958 self.frames.iter().min_by(|a, b| {
959 (a.time - t)
960 .abs()
961 .partial_cmp(&(b.time - t).abs())
962 .unwrap_or(std::cmp::Ordering::Equal)
963 })
964 }
965
966 pub fn kinetic_energy_series(&self) -> Vec<(f64, f64)> {
968 self.frames
969 .iter()
970 .map(|f| (f.time, f.kinetic_energy()))
971 .collect()
972 }
973}
974
975#[derive(Debug, Clone, Default)]
981pub struct ParticleStats {
982 pub count: usize,
984 pub mean_pos: [f64; 3],
986 pub mean_speed: f64,
988 pub max_speed: f64,
990 pub total_mass: f64,
992 pub kinetic_energy: f64,
994 pub bbox_min: [f64; 3],
996 pub bbox_max: [f64; 3],
998}
999
1000impl ParticleStats {
1001 pub fn compute(ds: &ParticleDataset) -> Self {
1003 let count = ds.len();
1004 if count == 0 {
1005 return Self::default();
1006 }
1007 let mean_pos = ds.centroid();
1008 let speeds: Vec<f64> = ds
1009 .velocities
1010 .iter()
1011 .map(|v| (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt())
1012 .collect();
1013 let mean_speed = speeds.iter().sum::<f64>() / count as f64;
1014 let max_speed = speeds.iter().cloned().fold(0.0_f64, f64::max);
1015 let total_mass = ds.masses.iter().sum();
1016 let kinetic_energy = ds.kinetic_energy();
1017 let (bbox_min, bbox_max) = ds.bounding_box().unwrap_or(([0.0; 3], [0.0; 3]));
1018 Self {
1019 count,
1020 mean_pos,
1021 mean_speed,
1022 max_speed,
1023 total_mass,
1024 kinetic_energy,
1025 bbox_min,
1026 bbox_max,
1027 }
1028 }
1029}
1030
1031pub fn make_test_dataset(n: usize, time: f64) -> ParticleDataset {
1037 let mut ds = ParticleDataset::with_time(time, 0);
1038 let side = (n as f64).cbrt().ceil() as usize;
1039 let mut idx = 0u64;
1040 'outer: for ix in 0..side {
1041 for iy in 0..side {
1042 for iz in 0..side {
1043 if idx as usize >= n {
1044 break 'outer;
1045 }
1046 ds.add_particle(
1047 idx,
1048 [ix as f64, iy as f64, iz as f64],
1049 [0.1 * ix as f64, 0.0, 0.0],
1050 1.0,
1051 0.4,
1052 );
1053 idx += 1;
1054 }
1055 }
1056 }
1057 ds
1058}
1059
1060pub fn interpolate_frames(a: &ParticleDataset, b: &ParticleDataset, t: f64) -> ParticleDataset {
1065 let a_map: HashMap<u64, usize> = a.ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
1066 let b_map: HashMap<u64, usize> = b.ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
1067 let time = a.time + t * (b.time - a.time);
1068 let step = a.step;
1069 let mut out = ParticleDataset::with_time(time, step);
1070 for (&id, &ai) in &a_map {
1071 if let Some(&bi) = b_map.get(&id) {
1072 let pa = a.positions[ai];
1073 let pb = b.positions[bi];
1074 let va = a.velocities[ai];
1075 let vb = b.velocities[bi];
1076 let pos = [
1077 pa[0] + t * (pb[0] - pa[0]),
1078 pa[1] + t * (pb[1] - pa[1]),
1079 pa[2] + t * (pb[2] - pa[2]),
1080 ];
1081 let vel = [
1082 va[0] + t * (vb[0] - va[0]),
1083 va[1] + t * (vb[1] - va[1]),
1084 va[2] + t * (vb[2] - va[2]),
1085 ];
1086 let mass = a.masses[ai];
1087 let radius = a.radii[ai];
1088 out.add_particle(id, pos, vel, mass, radius);
1089 }
1090 }
1091 out
1092}
1093
1094#[cfg(test)]
1099mod tests {
1100 use super::*;
1101
1102 fn make_ds(n: usize) -> ParticleDataset {
1103 make_test_dataset(n, 0.0)
1104 }
1105
1106 #[test]
1109 fn test_dataset_empty() {
1110 let ds = ParticleDataset::new();
1111 assert!(ds.is_empty());
1112 assert_eq!(ds.len(), 0);
1113 }
1114
1115 #[test]
1116 fn test_dataset_add_particle() {
1117 let mut ds = ParticleDataset::new();
1118 let idx = ds.add_particle(0, [1.0, 2.0, 3.0], [0.1, 0.2, 0.3], 1.5, 0.25);
1119 assert_eq!(idx, 0);
1120 assert_eq!(ds.len(), 1);
1121 assert_eq!(ds.position(0), [1.0, 2.0, 3.0]);
1122 assert_eq!(ds.velocity(0), [0.1, 0.2, 0.3]);
1123 }
1124
1125 #[test]
1126 fn test_dataset_bounding_box_single() {
1127 let mut ds = ParticleDataset::new();
1128 ds.add_particle(0, [3.0, -1.0, 5.0], [0.0; 3], 1.0, 0.1);
1129 let (mn, mx) = ds.bounding_box().unwrap();
1130 assert_eq!(mn, [3.0, -1.0, 5.0]);
1131 assert_eq!(mx, [3.0, -1.0, 5.0]);
1132 }
1133
1134 #[test]
1135 fn test_dataset_bounding_box_empty() {
1136 let ds = ParticleDataset::new();
1137 assert!(ds.bounding_box().is_none());
1138 }
1139
1140 #[test]
1141 fn test_dataset_bounding_box_multiple() {
1142 let mut ds = ParticleDataset::new();
1143 ds.add_particle(0, [0.0, 0.0, 0.0], [0.0; 3], 1.0, 0.1);
1144 ds.add_particle(1, [1.0, 2.0, 3.0], [0.0; 3], 1.0, 0.1);
1145 ds.add_particle(2, [-1.0, 5.0, -2.0], [0.0; 3], 1.0, 0.1);
1146 let (mn, mx) = ds.bounding_box().unwrap();
1147 assert_eq!(mn, [-1.0, 0.0, -2.0]);
1148 assert_eq!(mx, [1.0, 5.0, 3.0]);
1149 }
1150
1151 #[test]
1152 fn test_dataset_centroid() {
1153 let mut ds = ParticleDataset::new();
1154 ds.add_particle(0, [0.0, 0.0, 0.0], [0.0; 3], 1.0, 0.1);
1155 ds.add_particle(1, [2.0, 0.0, 0.0], [0.0; 3], 1.0, 0.1);
1156 let c = ds.centroid();
1157 assert!((c[0] - 1.0).abs() < 1e-10);
1158 }
1159
1160 #[test]
1161 fn test_dataset_kinetic_energy() {
1162 let mut ds = ParticleDataset::new();
1163 ds.add_particle(0, [0.0; 3], [1.0, 0.0, 0.0], 2.0, 0.1);
1165 assert!((ds.kinetic_energy() - 1.0).abs() < 1e-10);
1166 }
1167
1168 #[test]
1169 fn test_dataset_max_speed() {
1170 let mut ds = ParticleDataset::new();
1171 ds.add_particle(0, [0.0; 3], [1.0, 0.0, 0.0], 1.0, 0.1);
1172 ds.add_particle(1, [0.0; 3], [0.0, 3.0, 4.0], 1.0, 0.1);
1173 assert!((ds.max_speed() - 5.0).abs() < 1e-10);
1174 }
1175
1176 #[test]
1177 fn test_dataset_merge() {
1178 let mut a = ParticleDataset::new();
1179 a.add_particle(0, [0.0; 3], [0.0; 3], 1.0, 0.1);
1180 let mut b = ParticleDataset::new();
1181 b.add_particle(1, [1.0; 3], [0.0; 3], 1.0, 0.1);
1182 b.add_particle(2, [2.0; 3], [0.0; 3], 1.0, 0.1);
1183 a.merge(&b);
1184 assert_eq!(a.len(), 3);
1185 }
1186
1187 #[test]
1188 fn test_dataset_filter() {
1189 let ds = make_ds(8);
1190 let filtered = ds.filter(|i| i % 2 == 0);
1191 assert_eq!(filtered.len(), 4);
1192 }
1193
1194 #[test]
1195 fn test_dataset_with_time() {
1196 let ds = ParticleDataset::with_time(3.125, 42);
1197 assert!((ds.time - 3.125).abs() < 1e-10);
1198 assert_eq!(ds.step, 42);
1199 }
1200
1201 #[test]
1202 fn test_dataset_add_property() {
1203 let mut ds = make_ds(3);
1204 ds.add_property("temperature", vec![300.0, 310.0, 320.0]);
1205 assert!(ds.properties.contains_key("temperature"));
1206 assert_eq!(ds.properties["temperature"].len(), 3);
1207 }
1208
1209 #[test]
1212 fn test_h5part_reader_empty() {
1213 let r = H5partReader::new();
1214 assert_eq!(r.num_steps(), 0);
1215 assert!(r.step(0).is_none());
1216 }
1217
1218 #[test]
1219 fn test_h5part_reader_invalid_bytes() {
1220 let result = H5partReader::from_bytes(b"NOTVALID");
1221 assert!(result.is_err());
1222 }
1223
1224 #[test]
1225 fn test_h5part_reader_valid_magic() {
1226 let mut data = b"H5PART\0\0".to_vec();
1227 data.extend_from_slice(&0u64.to_le_bytes());
1228 let r = H5partReader::from_bytes(&data).unwrap();
1229 assert_eq!(r.num_steps(), 0);
1230 }
1231
1232 #[test]
1233 fn test_h5part_reader_push_and_get() {
1234 let mut r = H5partReader::new();
1235 let ds = make_ds(5);
1236 r.push_step(H5PartStep {
1237 step: 0,
1238 time: 0.0,
1239 dataset: ds,
1240 });
1241 assert_eq!(r.num_steps(), 1);
1242 assert!(r.step(0).is_some());
1243 }
1244
1245 #[test]
1246 fn test_h5part_reader_times() {
1247 let mut r = H5partReader::new();
1248 for i in 0..3 {
1249 let ds = ParticleDataset::with_time(i as f64 * 0.1, i);
1250 r.push_step(H5PartStep {
1251 step: i,
1252 time: i as f64 * 0.1,
1253 dataset: ds,
1254 });
1255 }
1256 let times = r.times();
1257 assert_eq!(times.len(), 3);
1258 assert!((times[1] - 0.1).abs() < 1e-10);
1259 }
1260
1261 #[test]
1262 fn test_h5part_reader_step_near() {
1263 let mut r = H5partReader::new();
1264 for i in 0..5u64 {
1265 let ds = ParticleDataset::with_time(i as f64, i);
1266 r.push_step(H5PartStep {
1267 step: i,
1268 time: i as f64,
1269 dataset: ds,
1270 });
1271 }
1272 let near = r.step_near(2.4).unwrap();
1273 assert_eq!(near.step, 2);
1274 }
1275
1276 #[test]
1277 fn test_h5part_reader_dataset_names() {
1278 let mut r = H5partReader::new();
1279 let ds = make_ds(2);
1280 r.push_step(H5PartStep {
1281 step: 0,
1282 time: 0.0,
1283 dataset: ds,
1284 });
1285 let names = r.dataset_names();
1286 assert!(names.contains(&"x".to_string()));
1287 assert!(names.contains(&"mass".to_string()));
1288 }
1289
1290 #[test]
1291 fn test_h5part_reader_iter_steps() {
1292 let mut r = H5partReader::new();
1293 for i in 0..4u64 {
1294 let ds = ParticleDataset::with_time(i as f64, i);
1295 r.push_step(H5PartStep {
1296 step: i,
1297 time: i as f64,
1298 dataset: ds,
1299 });
1300 }
1301 assert_eq!(r.iter_steps().count(), 4);
1302 }
1303
1304 #[test]
1307 fn test_h5part_writer_empty() {
1308 let w = H5partWriter::new();
1309 assert_eq!(w.num_steps(), 0);
1310 }
1311
1312 #[test]
1313 fn test_h5part_writer_write_step() {
1314 let mut w = H5partWriter::new();
1315 let ds = make_ds(10);
1316 w.write_step(ds);
1317 assert_eq!(w.num_steps(), 1);
1318 }
1319
1320 #[test]
1321 fn test_h5part_writer_to_bytes_magic() {
1322 let mut w = H5partWriter::new();
1323 let ds = make_ds(3);
1324 w.write_step(ds);
1325 let bytes = w.to_bytes();
1326 assert_eq!(&bytes[..6], b"H5PART");
1327 }
1328
1329 #[test]
1330 fn test_h5part_writer_round_trip() {
1331 let mut w = H5partWriter::new();
1332 let ds = make_ds(5);
1333 w.write_step(ds.clone());
1334 let r = w.into_reader();
1335 assert_eq!(r.num_steps(), 1);
1336 assert_eq!(r.step(0).unwrap().dataset.len(), ds.len());
1337 }
1338
1339 #[test]
1340 fn test_h5part_writer_multiple_steps() {
1341 let mut w = H5partWriter::new();
1342 for i in 0..5u64 {
1343 let ds = ParticleDataset::with_time(i as f64 * 0.5, i);
1344 w.write_step(ds);
1345 }
1346 assert_eq!(w.num_steps(), 5);
1347 }
1348
1349 #[test]
1350 fn test_h5part_writer_attributes() {
1351 let mut w = H5partWriter::new();
1352 w.set_attribute("author", "OxiPhysics");
1353 let bytes = w.to_bytes();
1354 assert!(!bytes.is_empty());
1355 }
1356
1357 #[test]
1360 fn test_vtk_writer_empty_dataset() {
1361 let w = ParticleVtkWriter::new();
1362 let ds = ParticleDataset::new();
1363 let s = w.write_to_string(&ds).unwrap();
1364 assert!(s.contains("VTKFile"));
1365 assert!(s.contains("NumberOfPoints=\"0\""));
1366 }
1367
1368 #[test]
1369 fn test_vtk_writer_basic() {
1370 let w = ParticleVtkWriter::new();
1371 let ds = make_ds(3);
1372 let s = w.write_to_string(&ds).unwrap();
1373 assert!(s.contains("NumberOfPoints=\"3\""));
1374 let _ = ds.len();
1375 }
1376
1377 #[test]
1378 fn test_vtk_writer_contains_radius() {
1379 let w = ParticleVtkWriter::new();
1380 let ds = make_ds(2);
1381 let s = w.write_to_string(&ds).unwrap();
1382 assert!(s.contains("Name=\"radius\""));
1383 }
1384
1385 #[test]
1386 fn test_vtk_writer_contains_velocity() {
1387 let w = ParticleVtkWriter::new();
1388 let ds = make_ds(2);
1389 let s = w.write_to_string(&ds).unwrap();
1390 assert!(s.contains("Name=\"velocity\""));
1391 }
1392
1393 #[test]
1394 fn test_vtk_writer_scalar_attribute() {
1395 let mut w = ParticleVtkWriter::new();
1396 w = w.with_scalar("temperature");
1397 let mut ds = make_ds(3);
1398 ds.add_property("temperature", vec![300.0, 310.0, 320.0]);
1399 let s = w.write_to_string(&ds).unwrap();
1400 assert!(s.contains("Name=\"temperature\""));
1401 }
1402
1403 #[test]
1404 fn test_vtk_writer_no_velocity() {
1405 let mut w = ParticleVtkWriter::new();
1406 w.include_velocity = false;
1407 let ds = make_ds(2);
1408 let s = w.write_to_string(&ds).unwrap();
1409 assert!(!s.contains("Name=\"velocity\""));
1410 }
1411
1412 #[test]
1413 fn test_vtk_writer_no_ids() {
1414 let mut w = ParticleVtkWriter::new();
1415 w.include_ids = false;
1416 let ds = make_ds(2);
1417 let s = w.write_to_string(&ds).unwrap();
1418 assert!(!s.contains("Name=\"id\""));
1419 }
1420
1421 #[test]
1424 fn test_json_writer_basic() {
1425 let ds = make_ds(2);
1426 let s = ParticleJsonWriter::write_to_string(&ds, true).unwrap();
1427 assert!(s.contains("\"time\""));
1428 assert!(s.contains("\"particles\""));
1429 }
1430
1431 #[test]
1432 fn test_json_writer_bbox() {
1433 let ds = make_ds(4);
1434 let s = ParticleJsonWriter::write_to_string(&ds, true).unwrap();
1435 assert!(s.contains("\"bbox\""));
1436 assert!(s.contains("\"min\""));
1437 assert!(s.contains("\"max\""));
1438 }
1439
1440 #[test]
1441 fn test_json_writer_no_bbox() {
1442 let ds = make_ds(2);
1443 let s = ParticleJsonWriter::write_to_string(&ds, false).unwrap();
1444 assert!(!s.contains("\"bbox\""));
1445 }
1446
1447 #[test]
1448 fn test_json_writer_empty_dataset() {
1449 let ds = ParticleDataset::new();
1450 let s = ParticleJsonWriter::write_to_string(&ds, true).unwrap();
1451 assert!(s.contains("\"particles\": []") || s.contains("\"particles\":"));
1452 }
1453
1454 #[test]
1455 fn test_json_writer_particle_count() {
1456 let ds = make_ds(5);
1457 let s = ParticleJsonWriter::write_to_string(&ds, false).unwrap();
1458 let count = s.matches("\"id\"").count();
1460 assert_eq!(count, 5);
1461 }
1462
1463 #[test]
1464 fn test_json_writer_with_properties() {
1465 let mut ds = make_ds(3);
1466 ds.add_property("temperature", vec![300.0, 310.0, 320.0]);
1467 let w = ParticleJsonWriter::new();
1468 let s = w.format(&ds).unwrap();
1469 assert!(s.contains("\"temperature\""));
1470 }
1471
1472 #[test]
1473 fn test_json_writer_custom_precision() {
1474 let w = ParticleJsonWriter {
1475 include_bbox: false,
1476 include_velocity: false,
1477 include_properties: false,
1478 precision: 2,
1479 };
1480 let mut ds = ParticleDataset::new();
1481 ds.add_particle(0, [1.23456789, 0.0, 0.0], [0.0; 3], 1.0, 0.1);
1482 let s = w.format(&ds).unwrap();
1483 assert!(s.contains("1.23"));
1484 assert!(!s.contains("1.234567"));
1485 }
1486
1487 #[test]
1490 fn test_diff_writer_first_frame_all_new() {
1491 let mut w = ParticleDiffWriter::new();
1492 let ds = make_ds(4);
1493 let n = w.write_frame(&ds);
1494 assert_eq!(n, 4);
1495 assert!(w.deltas[0].1.iter().all(|d| d.is_new));
1496 }
1497
1498 #[test]
1499 fn test_diff_writer_unchanged_frame() {
1500 let mut w = ParticleDiffWriter::new();
1501 let ds = make_ds(3);
1502 w.write_frame(&ds);
1503 let n = w.write_frame(&ds);
1504 assert_eq!(n, 0, "no changes → 0 deltas");
1505 }
1506
1507 #[test]
1508 fn test_diff_writer_moved_particle() {
1509 let mut w = ParticleDiffWriter::new();
1510 let ds1 = make_ds(2);
1511 let mut ds2 = ds1.clone();
1512 ds2.positions[0][0] += 1.0;
1513 w.write_frame(&ds1);
1514 let n = w.write_frame(&ds2);
1515 assert_eq!(n, 1);
1516 let delta = &w.deltas[1].1[0];
1517 assert!((delta.delta_pos[0] - 1.0).abs() < 1e-10);
1518 }
1519
1520 #[test]
1521 fn test_diff_writer_removed_particle() {
1522 let mut w = ParticleDiffWriter::new();
1523 let ds1 = make_ds(3);
1524 let mut ds2 = ds1.clone();
1525 let id1 = ds2.ids[1];
1527 ds2.ids.remove(1);
1528 ds2.positions.remove(1);
1529 ds2.velocities.remove(1);
1530 ds2.masses.remove(1);
1531 ds2.radii.remove(1);
1532 w.write_frame(&ds1);
1533 let n = w.write_frame(&ds2);
1534 assert_eq!(n, 1);
1535 let removed = w.deltas[1].1.iter().find(|d| d.id == id1).unwrap();
1536 assert!(removed.is_removed);
1537 }
1538
1539 #[test]
1540 fn test_diff_writer_added_particle() {
1541 let mut w = ParticleDiffWriter::new();
1542 let ds1 = make_ds(2);
1543 let mut ds2 = ds1.clone();
1544 ds2.add_particle(99, [10.0; 3], [0.0; 3], 1.0, 0.1);
1545 w.write_frame(&ds1);
1546 let n = w.write_frame(&ds2);
1547 assert_eq!(n, 1);
1548 assert!(w.deltas[1].1[0].is_new);
1549 }
1550
1551 #[test]
1552 fn test_diff_writer_to_bytes() {
1553 let mut w = ParticleDiffWriter::new();
1554 let ds = make_ds(3);
1555 w.write_frame(&ds);
1556 let bytes = w.to_bytes();
1557 assert_eq!(&bytes[..5], b"PDIFF");
1558 }
1559
1560 #[test]
1561 fn test_diff_writer_total_deltas() {
1562 let mut w = ParticleDiffWriter::new();
1563 let ds = make_ds(5);
1564 w.write_frame(&ds);
1565 assert_eq!(w.total_deltas(), 5); }
1567
1568 #[test]
1569 fn test_diff_writer_reconstruct() {
1570 let mut w = ParticleDiffWriter::new();
1571 let ds = make_ds(4);
1572 w.write_frame(&ds);
1573 let rec = w.reconstruct(0);
1574 assert_eq!(rec.len(), 4);
1575 }
1576
1577 #[test]
1578 fn test_diff_writer_thresholds() {
1579 let mut w = ParticleDiffWriter::with_thresholds(1.0, 1.0);
1580 let ds1 = make_ds(2);
1581 let mut ds2 = ds1.clone();
1582 ds2.positions[0][0] += 0.5; w.write_frame(&ds1);
1584 let n = w.write_frame(&ds2);
1585 assert_eq!(n, 0, "sub-threshold changes should be ignored");
1586 }
1587
1588 #[test]
1591 fn test_trajectory_empty() {
1592 let t = ParticleTrajectory::new("test");
1593 assert_eq!(t.num_frames(), 0);
1594 assert!(t.time_span().is_none());
1595 }
1596
1597 #[test]
1598 fn test_trajectory_push() {
1599 let mut traj = ParticleTrajectory::new("test");
1600 for i in 0..3u64 {
1601 let ds = ParticleDataset::with_time(i as f64, i);
1602 traj.push(ds);
1603 }
1604 assert_eq!(traj.num_frames(), 3);
1605 }
1606
1607 #[test]
1608 fn test_trajectory_time_span() {
1609 let mut traj = ParticleTrajectory::new("test");
1610 traj.push(ParticleDataset::with_time(0.0, 0));
1611 traj.push(ParticleDataset::with_time(1.0, 1));
1612 traj.push(ParticleDataset::with_time(2.0, 2));
1613 let (t0, t1) = traj.time_span().unwrap();
1614 assert!((t0 - 0.0).abs() < 1e-10);
1615 assert!((t1 - 2.0).abs() < 1e-10);
1616 }
1617
1618 #[test]
1619 fn test_trajectory_frame_at() {
1620 let mut traj = ParticleTrajectory::new("test");
1621 for i in 0..5u64 {
1622 traj.push(ParticleDataset::with_time(i as f64, i));
1623 }
1624 let f = traj.frame_at(2.3).unwrap();
1625 assert_eq!(f.step, 2);
1626 }
1627
1628 #[test]
1629 fn test_trajectory_kinetic_energy_series() {
1630 let mut traj = ParticleTrajectory::new("KE");
1631 let mut ds = ParticleDataset::with_time(0.0, 0);
1632 ds.add_particle(0, [0.0; 3], [1.0, 0.0, 0.0], 2.0, 0.1);
1633 traj.push(ds);
1634 let series = traj.kinetic_energy_series();
1635 assert_eq!(series.len(), 1);
1636 assert!((series[0].1 - 1.0).abs() < 1e-10);
1637 }
1638
1639 #[test]
1642 fn test_stats_empty() {
1643 let s = ParticleStats::compute(&ParticleDataset::new());
1644 assert_eq!(s.count, 0);
1645 }
1646
1647 #[test]
1648 fn test_stats_count() {
1649 let ds = make_ds(7);
1650 let s = ParticleStats::compute(&ds);
1651 assert_eq!(s.count, 7);
1652 }
1653
1654 #[test]
1655 fn test_stats_total_mass() {
1656 let ds = make_ds(5);
1657 let s = ParticleStats::compute(&ds);
1658 assert!((s.total_mass - 5.0).abs() < 1e-10); }
1660
1661 #[test]
1662 fn test_stats_max_speed() {
1663 let mut ds = ParticleDataset::new();
1664 ds.add_particle(0, [0.0; 3], [3.0, 4.0, 0.0], 1.0, 0.1);
1665 let s = ParticleStats::compute(&ds);
1666 assert!((s.max_speed - 5.0).abs() < 1e-10);
1667 }
1668
1669 #[test]
1672 fn test_interpolate_midpoint() {
1673 let mut a = ParticleDataset::with_time(0.0, 0);
1674 a.add_particle(0, [0.0, 0.0, 0.0], [0.0; 3], 1.0, 0.1);
1675 let mut b = ParticleDataset::with_time(1.0, 1);
1676 b.add_particle(0, [2.0, 0.0, 0.0], [0.0; 3], 1.0, 0.1);
1677 let mid = interpolate_frames(&a, &b, 0.5);
1678 assert!((mid.positions[0][0] - 1.0).abs() < 1e-10);
1679 }
1680
1681 #[test]
1682 fn test_interpolate_t0_is_a() {
1683 let mut a = ParticleDataset::with_time(0.0, 0);
1684 a.add_particle(0, [1.0, 2.0, 3.0], [0.0; 3], 1.0, 0.1);
1685 let mut b = ParticleDataset::with_time(1.0, 1);
1686 b.add_particle(0, [4.0, 5.0, 6.0], [0.0; 3], 1.0, 0.1);
1687 let result = interpolate_frames(&a, &b, 0.0);
1688 assert_eq!(result.positions[0], [1.0, 2.0, 3.0]);
1689 }
1690
1691 #[test]
1692 fn test_interpolate_t1_is_b() {
1693 let mut a = ParticleDataset::with_time(0.0, 0);
1694 a.add_particle(0, [0.0; 3], [0.0; 3], 1.0, 0.1);
1695 let mut b = ParticleDataset::with_time(1.0, 1);
1696 b.add_particle(0, [1.0, 1.0, 1.0], [0.0; 3], 1.0, 0.1);
1697 let result = interpolate_frames(&a, &b, 1.0);
1698 assert!((result.positions[0][0] - 1.0).abs() < 1e-10);
1699 }
1700
1701 #[test]
1702 fn test_interpolate_only_common_particles() {
1703 let mut a = ParticleDataset::with_time(0.0, 0);
1704 a.add_particle(0, [0.0; 3], [0.0; 3], 1.0, 0.1);
1705 a.add_particle(1, [1.0; 3], [0.0; 3], 1.0, 0.1);
1706 let mut b = ParticleDataset::with_time(1.0, 1);
1707 b.add_particle(0, [2.0; 3], [0.0; 3], 1.0, 0.1);
1708 let result = interpolate_frames(&a, &b, 0.5);
1710 assert_eq!(result.len(), 1);
1711 }
1712
1713 #[test]
1716 fn test_make_test_dataset_count() {
1717 let ds = make_test_dataset(8, 1.5);
1718 assert_eq!(ds.len(), 8);
1719 assert!((ds.time - 1.5).abs() < 1e-10);
1720 }
1721
1722 #[test]
1723 fn test_make_test_dataset_zero() {
1724 let ds = make_test_dataset(0, 0.0);
1725 assert!(ds.is_empty());
1726 }
1727
1728 #[test]
1729 fn test_make_test_dataset_one() {
1730 let ds = make_test_dataset(1, 0.0);
1731 assert_eq!(ds.len(), 1);
1732 }
1733}