1#![allow(clippy::needless_range_loop)]
2#![allow(missing_docs)]
10#![allow(dead_code)]
11
12use std::fs;
13use std::io::Write;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum DataType {
22 Float32,
23 Float64,
24 Int32,
25 Int64,
26 UInt8,
27 UInt32,
28}
29
30impl DataType {
31 pub fn size_bytes(&self) -> usize {
33 match self {
34 DataType::Float32 => 4,
35 DataType::Float64 => 8,
36 DataType::Int32 => 4,
37 DataType::Int64 => 8,
38 DataType::UInt8 => 1,
39 DataType::UInt32 => 4,
40 }
41 }
42
43 fn tag(&self) -> u8 {
44 match self {
45 DataType::Float32 => 0,
46 DataType::Float64 => 1,
47 DataType::Int32 => 2,
48 DataType::Int64 => 3,
49 DataType::UInt8 => 4,
50 DataType::UInt32 => 5,
51 }
52 }
53
54 fn from_tag(tag: u8) -> Result<Self, String> {
55 match tag {
56 0 => Ok(DataType::Float32),
57 1 => Ok(DataType::Float64),
58 2 => Ok(DataType::Int32),
59 3 => Ok(DataType::Int64),
60 4 => Ok(DataType::UInt8),
61 5 => Ok(DataType::UInt32),
62 _ => Err(format!("Unknown DataType tag: {}", tag)),
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct DatasetShape {
74 pub dims: Vec<usize>,
75}
76
77impl DatasetShape {
78 pub fn new(dims: Vec<usize>) -> Self {
80 Self { dims }
81 }
82
83 pub fn total_elements(&self) -> usize {
85 if self.dims.is_empty() {
86 return 1; }
88 self.dims.iter().product()
89 }
90
91 pub fn is_scalar(&self) -> bool {
93 self.dims.is_empty()
94 }
95
96 pub fn rank(&self) -> usize {
98 self.dims.len()
99 }
100}
101
102#[derive(Debug, Clone, PartialEq)]
108pub struct Attribute {
109 pub name: String,
110 pub value: AttributeValue,
111}
112
113impl Attribute {
114 pub fn new(name: impl Into<String>, value: AttributeValue) -> Self {
116 Self {
117 name: name.into(),
118 value,
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq)]
125pub enum AttributeValue {
126 Int(i64),
127 Float(f64),
128 Text(String),
129 IntArray(Vec<i64>),
130 FloatArray(Vec<f64>),
131}
132
133impl AttributeValue {
134 fn tag(&self) -> u8 {
135 match self {
136 AttributeValue::Int(_) => 0,
137 AttributeValue::Float(_) => 1,
138 AttributeValue::Text(_) => 2,
139 AttributeValue::IntArray(_) => 3,
140 AttributeValue::FloatArray(_) => 4,
141 }
142 }
143}
144
145#[derive(Debug, Clone)]
151pub struct Dataset {
152 pub name: String,
153 pub dtype: DataType,
154 pub shape: DatasetShape,
155 pub data: Vec<u8>,
157 pub attributes: Vec<Attribute>,
158}
159
160impl Dataset {
161 pub fn from_f64_slice(name: &str, data: &[f64], shape: DatasetShape) -> Self {
163 let mut bytes = Vec::with_capacity(data.len() * 8);
164 for &v in data {
165 bytes.extend_from_slice(&v.to_le_bytes());
166 }
167 Self {
168 name: name.to_string(),
169 dtype: DataType::Float64,
170 shape,
171 data: bytes,
172 attributes: Vec::new(),
173 }
174 }
175
176 pub fn from_f32_slice(name: &str, data: &[f32], shape: DatasetShape) -> Self {
178 let mut bytes = Vec::with_capacity(data.len() * 4);
179 for &v in data {
180 bytes.extend_from_slice(&v.to_le_bytes());
181 }
182 Self {
183 name: name.to_string(),
184 dtype: DataType::Float32,
185 shape,
186 data: bytes,
187 attributes: Vec::new(),
188 }
189 }
190
191 pub fn from_i32_slice(name: &str, data: &[i32], shape: DatasetShape) -> Self {
193 let mut bytes = Vec::with_capacity(data.len() * 4);
194 for &v in data {
195 bytes.extend_from_slice(&v.to_le_bytes());
196 }
197 Self {
198 name: name.to_string(),
199 dtype: DataType::Int32,
200 shape,
201 data: bytes,
202 attributes: Vec::new(),
203 }
204 }
205
206 pub fn to_f64_vec(&self) -> Result<Vec<f64>, String> {
208 if self.dtype != DataType::Float64 {
209 return Err(format!("Expected Float64, got {:?}", self.dtype));
210 }
211 let n = self.shape.total_elements();
212 if self.data.len() != n * 8 {
213 return Err(format!(
214 "Data length mismatch: {} bytes for {} f64 elements",
215 self.data.len(),
216 n
217 ));
218 }
219 Ok((0..n)
220 .map(|i| {
221 f64::from_le_bytes(
222 self.data[i * 8..i * 8 + 8]
223 .try_into()
224 .expect("slice length must match"),
225 )
226 })
227 .collect())
228 }
229
230 pub fn to_f32_vec(&self) -> Result<Vec<f32>, String> {
232 if self.dtype != DataType::Float32 {
233 return Err(format!("Expected Float32, got {:?}", self.dtype));
234 }
235 let n = self.shape.total_elements();
236 if self.data.len() != n * 4 {
237 return Err(format!(
238 "Data length mismatch: {} bytes for {} f32 elements",
239 self.data.len(),
240 n
241 ));
242 }
243 Ok((0..n)
244 .map(|i| {
245 f32::from_le_bytes(
246 self.data[i * 4..i * 4 + 4]
247 .try_into()
248 .expect("slice length must match"),
249 )
250 })
251 .collect())
252 }
253
254 pub fn to_i32_vec(&self) -> Result<Vec<i32>, String> {
256 if self.dtype != DataType::Int32 {
257 return Err(format!("Expected Int32, got {:?}", self.dtype));
258 }
259 let n = self.shape.total_elements();
260 if self.data.len() != n * 4 {
261 return Err(format!(
262 "Data length mismatch: {} bytes for {} i32 elements",
263 self.data.len(),
264 n
265 ));
266 }
267 Ok((0..n)
268 .map(|i| {
269 i32::from_le_bytes(
270 self.data[i * 4..i * 4 + 4]
271 .try_into()
272 .expect("slice length must match"),
273 )
274 })
275 .collect())
276 }
277
278 pub fn add_attribute(&mut self, attr: Attribute) {
280 self.attributes.push(attr);
281 }
282
283 pub fn get_attribute(&self, name: &str) -> Option<&Attribute> {
285 self.attributes.iter().find(|a| a.name == name)
286 }
287}
288
289#[derive(Debug, Clone)]
295pub struct Group {
296 pub name: String,
297 pub datasets: Vec<Dataset>,
298 pub subgroups: Vec<Group>,
299 pub attributes: Vec<Attribute>,
300}
301
302impl Group {
303 pub fn new(name: &str) -> Self {
305 Self {
306 name: name.to_string(),
307 datasets: Vec::new(),
308 subgroups: Vec::new(),
309 attributes: Vec::new(),
310 }
311 }
312
313 pub fn add_dataset(&mut self, ds: Dataset) {
315 self.datasets.push(ds);
316 }
317
318 pub fn add_subgroup(&mut self, g: Group) {
320 self.subgroups.push(g);
321 }
322
323 pub fn get_dataset(&self, name: &str) -> Option<&Dataset> {
325 self.datasets.iter().find(|d| d.name == name)
326 }
327
328 pub fn get_subgroup(&self, name: &str) -> Option<&Group> {
330 self.subgroups.iter().find(|g| g.name == name)
331 }
332
333 pub fn add_attribute(&mut self, attr: Attribute) {
335 self.attributes.push(attr);
336 }
337
338 pub fn get_attribute(&self, name: &str) -> Option<&Attribute> {
340 self.attributes.iter().find(|a| a.name == name)
341 }
342}
343
344const MAGIC: &[u8; 8] = b"OXIPHY01";
349
350#[derive(Debug, Clone)]
357pub struct OxiFile {
358 pub version: u32,
359 pub root: Group,
360}
361
362impl OxiFile {
363 pub fn new() -> Self {
365 Self {
366 version: 1,
367 root: Group::new("/"),
368 }
369 }
370
371 pub fn write_to_bytes(&self) -> Vec<u8> {
373 let mut buf = Vec::new();
374 buf.extend_from_slice(MAGIC);
375 write_u32(&mut buf, self.version);
376 serialize_group(&mut buf, &self.root);
377 buf
378 }
379
380 pub fn read_from_bytes(data: &[u8]) -> Result<Self, String> {
382 if data.len() < 12 {
383 return Err("Data too short to be a valid OxiFile".to_string());
384 }
385 if &data[0..8] != MAGIC {
386 return Err("Invalid magic bytes: not an OxiFile".to_string());
387 }
388 let mut pos = 8usize;
389 let version = read_u32(data, &mut pos)?;
390 let root = deserialize_group(data, &mut pos)?;
391 Ok(Self { version, root })
392 }
393
394 pub fn save(&self, path: &str) -> Result<(), String> {
396 let bytes = self.write_to_bytes();
397 let mut f =
398 fs::File::create(path).map_err(|e| format!("Cannot create file '{}': {}", path, e))?;
399 f.write_all(&bytes)
400 .map_err(|e| format!("Write error: {}", e))?;
401 Ok(())
402 }
403
404 pub fn load(path: &str) -> Result<Self, String> {
406 let bytes = fs::read(path).map_err(|e| format!("Cannot read file '{}': {}", path, e))?;
407 Self::read_from_bytes(&bytes)
408 }
409}
410
411impl Default for OxiFile {
412 fn default() -> Self {
413 Self::new()
414 }
415}
416
417pub fn write_u32(buf: &mut Vec<u8>, v: u32) {
423 buf.extend_from_slice(&v.to_le_bytes());
424}
425
426pub fn write_u64(buf: &mut Vec<u8>, v: u64) {
428 buf.extend_from_slice(&v.to_le_bytes());
429}
430
431pub fn write_string(buf: &mut Vec<u8>, s: &str) {
433 let bytes = s.as_bytes();
434 write_u32(buf, bytes.len() as u32);
435 buf.extend_from_slice(bytes);
436}
437
438pub fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, String> {
440 if *pos + 4 > data.len() {
441 return Err(format!("read_u32: unexpected end of data at pos {}", *pos));
442 }
443 let v = u32::from_le_bytes(
444 data[*pos..*pos + 4]
445 .try_into()
446 .expect("slice length must match"),
447 );
448 *pos += 4;
449 Ok(v)
450}
451
452pub fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, String> {
454 if *pos + 8 > data.len() {
455 return Err(format!("read_u64: unexpected end of data at pos {}", *pos));
456 }
457 let v = u64::from_le_bytes(
458 data[*pos..*pos + 8]
459 .try_into()
460 .expect("slice length must match"),
461 );
462 *pos += 8;
463 Ok(v)
464}
465
466pub fn read_string(data: &[u8], pos: &mut usize) -> Result<String, String> {
468 let len = read_u32(data, pos)? as usize;
469 if *pos + len > data.len() {
470 return Err(format!(
471 "read_string: string body out of bounds at pos {}",
472 *pos
473 ));
474 }
475 let s = std::str::from_utf8(&data[*pos..*pos + len])
476 .map_err(|e| format!("Invalid UTF-8: {}", e))?
477 .to_string();
478 *pos += len;
479 Ok(s)
480}
481
482fn write_i64(buf: &mut Vec<u8>, v: i64) {
487 buf.extend_from_slice(&v.to_le_bytes());
488}
489
490fn read_i64(data: &[u8], pos: &mut usize) -> Result<i64, String> {
491 if *pos + 8 > data.len() {
492 return Err(format!("read_i64: unexpected end of data at pos {}", *pos));
493 }
494 let v = i64::from_le_bytes(
495 data[*pos..*pos + 8]
496 .try_into()
497 .expect("slice length must match"),
498 );
499 *pos += 8;
500 Ok(v)
501}
502
503fn write_f64(buf: &mut Vec<u8>, v: f64) {
504 buf.extend_from_slice(&v.to_le_bytes());
505}
506
507fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64, String> {
508 if *pos + 8 > data.len() {
509 return Err(format!("read_f64: unexpected end of data at pos {}", *pos));
510 }
511 let v = f64::from_le_bytes(
512 data[*pos..*pos + 8]
513 .try_into()
514 .expect("slice length must match"),
515 );
516 *pos += 8;
517 Ok(v)
518}
519
520fn serialize_attribute(buf: &mut Vec<u8>, attr: &Attribute) {
525 write_string(buf, &attr.name);
526 buf.push(attr.value.tag());
527 match &attr.value {
528 AttributeValue::Int(v) => {
529 write_i64(buf, *v);
530 }
531 AttributeValue::Float(v) => {
532 write_f64(buf, *v);
533 }
534 AttributeValue::Text(s) => {
535 write_string(buf, s);
536 }
537 AttributeValue::IntArray(arr) => {
538 write_u64(buf, arr.len() as u64);
539 for &v in arr {
540 write_i64(buf, v);
541 }
542 }
543 AttributeValue::FloatArray(arr) => {
544 write_u64(buf, arr.len() as u64);
545 for &v in arr {
546 write_f64(buf, v);
547 }
548 }
549 }
550}
551
552fn deserialize_attribute(data: &[u8], pos: &mut usize) -> Result<Attribute, String> {
553 let name = read_string(data, pos)?;
554 if *pos >= data.len() {
555 return Err("deserialize_attribute: missing type tag".to_string());
556 }
557 let tag = data[*pos];
558 *pos += 1;
559 let value = match tag {
560 0 => AttributeValue::Int(read_i64(data, pos)?),
561 1 => AttributeValue::Float(read_f64(data, pos)?),
562 2 => AttributeValue::Text(read_string(data, pos)?),
563 3 => {
564 let n = read_u64(data, pos)? as usize;
565 let mut arr = Vec::with_capacity(n);
566 for _ in 0..n {
567 arr.push(read_i64(data, pos)?);
568 }
569 AttributeValue::IntArray(arr)
570 }
571 4 => {
572 let n = read_u64(data, pos)? as usize;
573 let mut arr = Vec::with_capacity(n);
574 for _ in 0..n {
575 arr.push(read_f64(data, pos)?);
576 }
577 AttributeValue::FloatArray(arr)
578 }
579 _ => return Err(format!("Unknown AttributeValue tag: {}", tag)),
580 };
581 Ok(Attribute { name, value })
582}
583
584fn serialize_dataset(buf: &mut Vec<u8>, ds: &Dataset) {
589 write_string(buf, &ds.name);
590 buf.push(ds.dtype.tag());
591 write_u32(buf, ds.shape.dims.len() as u32);
593 for &d in &ds.shape.dims {
594 write_u64(buf, d as u64);
595 }
596 write_u64(buf, ds.data.len() as u64);
598 buf.extend_from_slice(&ds.data);
599 write_u32(buf, ds.attributes.len() as u32);
601 for attr in &ds.attributes {
602 serialize_attribute(buf, attr);
603 }
604}
605
606fn deserialize_dataset(data: &[u8], pos: &mut usize) -> Result<Dataset, String> {
607 let name = read_string(data, pos)?;
608 if *pos >= data.len() {
609 return Err("deserialize_dataset: missing dtype tag".to_string());
610 }
611 let dtype = DataType::from_tag(data[*pos])?;
612 *pos += 1;
613 let ndims = read_u32(data, pos)? as usize;
614 let mut dims = Vec::with_capacity(ndims);
615 for _ in 0..ndims {
616 dims.push(read_u64(data, pos)? as usize);
617 }
618 let shape = DatasetShape { dims };
619 let data_len = read_u64(data, pos)? as usize;
620 if *pos + data_len > data.len() {
621 return Err(format!(
622 "deserialize_dataset: data body out of bounds at pos {}",
623 *pos
624 ));
625 }
626 let raw = data[*pos..*pos + data_len].to_vec();
627 *pos += data_len;
628 let n_attrs = read_u32(data, pos)? as usize;
629 let mut attributes = Vec::with_capacity(n_attrs);
630 for _ in 0..n_attrs {
631 attributes.push(deserialize_attribute(data, pos)?);
632 }
633 Ok(Dataset {
634 name,
635 dtype,
636 shape,
637 data: raw,
638 attributes,
639 })
640}
641
642fn serialize_group(buf: &mut Vec<u8>, group: &Group) {
647 write_string(buf, &group.name);
648 write_u32(buf, group.attributes.len() as u32);
650 for attr in &group.attributes {
651 serialize_attribute(buf, attr);
652 }
653 write_u32(buf, group.datasets.len() as u32);
655 for ds in &group.datasets {
656 serialize_dataset(buf, ds);
657 }
658 write_u32(buf, group.subgroups.len() as u32);
660 for sg in &group.subgroups {
661 serialize_group(buf, sg);
662 }
663}
664
665fn deserialize_group(data: &[u8], pos: &mut usize) -> Result<Group, String> {
666 let name = read_string(data, pos)?;
667 let n_attrs = read_u32(data, pos)? as usize;
668 let mut attributes = Vec::with_capacity(n_attrs);
669 for _ in 0..n_attrs {
670 attributes.push(deserialize_attribute(data, pos)?);
671 }
672 let n_datasets = read_u32(data, pos)? as usize;
673 let mut datasets = Vec::with_capacity(n_datasets);
674 for _ in 0..n_datasets {
675 datasets.push(deserialize_dataset(data, pos)?);
676 }
677 let n_subgroups = read_u32(data, pos)? as usize;
678 let mut subgroups = Vec::with_capacity(n_subgroups);
679 for _ in 0..n_subgroups {
680 subgroups.push(deserialize_group(data, pos)?);
681 }
682 Ok(Group {
683 name,
684 datasets,
685 subgroups,
686 attributes,
687 })
688}
689
690pub struct SimulationCheckpoint;
696
697impl SimulationCheckpoint {
698 #[allow(clippy::new_ret_no_self)]
700 pub fn new() -> OxiFile {
701 OxiFile::new()
702 }
703
704 pub fn add_positions(file: &mut OxiFile, group: &str, positions: &[[f64; 3]]) {
706 let flat: Vec<f64> = positions.iter().flat_map(|p| p.iter().copied()).collect();
707 let shape = DatasetShape::new(vec![positions.len(), 3]);
708 let ds = Dataset::from_f64_slice("positions", &flat, shape);
709 Self::get_or_create_group(&mut file.root, group).add_dataset(ds);
710 }
711
712 pub fn add_velocities(file: &mut OxiFile, group: &str, velocities: &[[f64; 3]]) {
714 let flat: Vec<f64> = velocities.iter().flat_map(|v| v.iter().copied()).collect();
715 let shape = DatasetShape::new(vec![velocities.len(), 3]);
716 let ds = Dataset::from_f64_slice("velocities", &flat, shape);
717 Self::get_or_create_group(&mut file.root, group).add_dataset(ds);
718 }
719
720 pub fn add_scalar_field(file: &mut OxiFile, group: &str, name: &str, values: &[f64]) {
722 let shape = DatasetShape::new(vec![values.len()]);
723 let ds = Dataset::from_f64_slice(name, values, shape);
724 Self::get_or_create_group(&mut file.root, group).add_dataset(ds);
725 }
726
727 pub fn add_timestep_metadata(file: &mut OxiFile, step: u64, time: f64, dt: f64) {
729 file.root
730 .add_attribute(Attribute::new("step", AttributeValue::Int(step as i64)));
731 file.root
732 .add_attribute(Attribute::new("time", AttributeValue::Float(time)));
733 file.root
734 .add_attribute(Attribute::new("dt", AttributeValue::Float(dt)));
735 }
736
737 fn get_or_create_group<'a>(root: &'a mut Group, name: &str) -> &'a mut Group {
739 if let Some(idx) = root.subgroups.iter().position(|g| g.name == name) {
740 return &mut root.subgroups[idx];
741 }
742 root.subgroups.push(Group::new(name));
743 root.subgroups
744 .last_mut()
745 .expect("collection should not be empty")
746 }
747}
748
749impl Default for SimulationCheckpoint {
750 fn default() -> Self {
751 Self
752 }
753}
754
755#[derive(Debug, Clone, Copy, PartialEq, Eq)]
761pub enum Endianness {
762 Little,
764 Big,
766}
767
768impl Endianness {
769 pub fn native() -> Self {
771 if cfg!(target_endian = "little") {
772 Endianness::Little
773 } else {
774 Endianness::Big
775 }
776 }
777
778 pub fn u32_to_bytes(self, v: u32) -> [u8; 4] {
780 match self {
781 Endianness::Little => v.to_le_bytes(),
782 Endianness::Big => v.to_be_bytes(),
783 }
784 }
785
786 pub fn u32_from_bytes(self, b: [u8; 4]) -> u32 {
788 match self {
789 Endianness::Little => u32::from_le_bytes(b),
790 Endianness::Big => u32::from_be_bytes(b),
791 }
792 }
793
794 pub fn f64_to_bytes(self, v: f64) -> [u8; 8] {
796 match self {
797 Endianness::Little => v.to_le_bytes(),
798 Endianness::Big => v.to_be_bytes(),
799 }
800 }
801
802 pub fn f64_from_bytes(self, b: [u8; 8]) -> f64 {
804 match self {
805 Endianness::Little => f64::from_le_bytes(b),
806 Endianness::Big => f64::from_be_bytes(b),
807 }
808 }
809
810 pub fn u32_to_native(self, v: u32) -> u32 {
812 match self {
813 Endianness::Little => u32::from_le_bytes(v.to_ne_bytes()),
814 Endianness::Big => u32::from_be_bytes(v.to_ne_bytes()),
815 }
816 }
817}
818
819#[derive(Debug, Clone)]
831pub struct BinaryMesh {
832 pub vertices: Vec<[f64; 3]>,
834 pub triangles: Vec<[u32; 3]>,
836}
837
838impl BinaryMesh {
839 pub fn new() -> Self {
841 Self {
842 vertices: Vec::new(),
843 triangles: Vec::new(),
844 }
845 }
846
847 pub fn to_bytes(&self) -> Vec<u8> {
849 let mut buf = Vec::new();
850 write_u32(&mut buf, self.vertices.len() as u32);
851 write_u32(&mut buf, self.triangles.len() as u32);
852 for v in &self.vertices {
853 for k in 0..3 {
854 buf.extend_from_slice(&v[k].to_le_bytes());
855 }
856 }
857 for t in &self.triangles {
858 for k in 0..3 {
859 write_u32(&mut buf, t[k]);
860 }
861 }
862 buf
863 }
864
865 pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
867 let mut pos = 0usize;
868 let n_verts = read_u32(data, &mut pos)? as usize;
869 let n_tris = read_u32(data, &mut pos)? as usize;
870
871 let mut vertices = Vec::with_capacity(n_verts);
872 for _ in 0..n_verts {
873 let mut xyz = [0.0_f64; 3];
874 for k in 0..3 {
875 if pos + 8 > data.len() {
876 return Err("BinaryMesh: vertex data truncated".to_string());
877 }
878 xyz[k] = f64::from_le_bytes(
879 data[pos..pos + 8]
880 .try_into()
881 .expect("slice length must match"),
882 );
883 pos += 8;
884 }
885 vertices.push(xyz);
886 }
887
888 let mut triangles = Vec::with_capacity(n_tris);
889 for _ in 0..n_tris {
890 let i0 = read_u32(data, &mut pos)?;
891 let i1 = read_u32(data, &mut pos)?;
892 let i2 = read_u32(data, &mut pos)?;
893 triangles.push([i0, i1, i2]);
894 }
895
896 Ok(Self {
897 vertices,
898 triangles,
899 })
900 }
901
902 pub fn n_vertices(&self) -> usize {
904 self.vertices.len()
905 }
906
907 pub fn n_triangles(&self) -> usize {
909 self.triangles.len()
910 }
911}
912
913impl Default for BinaryMesh {
914 fn default() -> Self {
915 Self::new()
916 }
917}
918
919#[derive(Debug, Clone)]
928pub struct BinaryParticleData {
929 pub positions: Vec<[f64; 3]>,
931 pub scalar_fields: Vec<Vec<f64>>,
933 pub field_names: Vec<String>,
935}
936
937const PARTICLE_MAGIC: &[u8; 7] = b"OXIPART";
938
939impl BinaryParticleData {
940 pub fn new() -> Self {
942 Self {
943 positions: Vec::new(),
944 scalar_fields: Vec::new(),
945 field_names: Vec::new(),
946 }
947 }
948
949 pub fn add_field(&mut self, name: &str, values: Vec<f64>) {
951 assert_eq!(
952 values.len(),
953 self.positions.len(),
954 "Field '{}' length {} != particle count {}",
955 name,
956 values.len(),
957 self.positions.len()
958 );
959 self.field_names.push(name.to_string());
960 self.scalar_fields.push(values);
961 }
962
963 pub fn n_particles(&self) -> usize {
965 self.positions.len()
966 }
967
968 pub fn n_fields(&self) -> usize {
970 self.scalar_fields.len()
971 }
972
973 pub fn to_bytes(&self) -> Vec<u8> {
975 let n = self.positions.len();
976 let nf = self.scalar_fields.len();
977 let mut buf = Vec::new();
978 buf.extend_from_slice(PARTICLE_MAGIC);
979 write_u32(&mut buf, n as u32);
980 write_u32(&mut buf, nf as u32);
981 for name in &self.field_names {
983 write_string(&mut buf, name);
984 }
985 for p in &self.positions {
987 for k in 0..3 {
988 buf.extend_from_slice(&p[k].to_le_bytes());
989 }
990 }
991 for field in &self.scalar_fields {
993 for &v in field {
994 buf.extend_from_slice(&v.to_le_bytes());
995 }
996 }
997 buf
998 }
999
1000 pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
1002 if data.len() < 7 {
1003 return Err("BinaryParticleData: too short".to_string());
1004 }
1005 if &data[..7] != PARTICLE_MAGIC {
1006 return Err("BinaryParticleData: bad magic".to_string());
1007 }
1008 let mut pos = 7usize;
1009 let n = read_u32(data, &mut pos)? as usize;
1010 let nf = read_u32(data, &mut pos)? as usize;
1011
1012 let mut field_names = Vec::with_capacity(nf);
1013 for _ in 0..nf {
1014 field_names.push(read_string(data, &mut pos)?);
1015 }
1016
1017 let mut positions = Vec::with_capacity(n);
1018 for _ in 0..n {
1019 let mut xyz = [0.0_f64; 3];
1020 for k in 0..3 {
1021 if pos + 8 > data.len() {
1022 return Err("BinaryParticleData: positions truncated".to_string());
1023 }
1024 xyz[k] = f64::from_le_bytes(
1025 data[pos..pos + 8]
1026 .try_into()
1027 .expect("slice length must match"),
1028 );
1029 pos += 8;
1030 }
1031 positions.push(xyz);
1032 }
1033
1034 let mut scalar_fields = Vec::with_capacity(nf);
1035 for _ in 0..nf {
1036 let mut field = Vec::with_capacity(n);
1037 for _ in 0..n {
1038 if pos + 8 > data.len() {
1039 return Err("BinaryParticleData: scalar field truncated".to_string());
1040 }
1041 let v = f64::from_le_bytes(
1042 data[pos..pos + 8]
1043 .try_into()
1044 .expect("slice length must match"),
1045 );
1046 pos += 8;
1047 field.push(v);
1048 }
1049 scalar_fields.push(field);
1050 }
1051
1052 Ok(Self {
1053 positions,
1054 scalar_fields,
1055 field_names,
1056 })
1057 }
1058}
1059
1060impl Default for BinaryParticleData {
1061 fn default() -> Self {
1062 Self::new()
1063 }
1064}
1065
1066#[allow(dead_code)]
1077pub fn rle_compress_f64(values: &[f64]) -> Vec<u8> {
1078 if values.is_empty() {
1079 let mut buf = Vec::new();
1080 write_u32(&mut buf, 0);
1081 return buf;
1082 }
1083
1084 let mut runs: Vec<(f64, u32)> = Vec::new();
1085 let mut cur = values[0];
1086 let mut cnt = 1u32;
1087 for &v in &values[1..] {
1088 if v.to_bits() == cur.to_bits() {
1089 cnt += 1;
1090 } else {
1091 runs.push((cur, cnt));
1092 cur = v;
1093 cnt = 1;
1094 }
1095 }
1096 runs.push((cur, cnt));
1097
1098 let mut buf = Vec::new();
1099 write_u32(&mut buf, runs.len() as u32);
1100 for (val, count) in runs {
1101 buf.extend_from_slice(&val.to_le_bytes());
1102 write_u32(&mut buf, count);
1103 }
1104 buf
1105}
1106
1107#[allow(dead_code)]
1109pub fn rle_decompress_f64(data: &[u8]) -> Result<Vec<f64>, String> {
1110 let mut pos = 0usize;
1111 let n_runs = read_u32(data, &mut pos)? as usize;
1112 let mut result = Vec::new();
1113 for _ in 0..n_runs {
1114 if pos + 12 > data.len() {
1115 return Err("rle_decompress_f64: truncated".to_string());
1116 }
1117 let val = f64::from_le_bytes(
1118 data[pos..pos + 8]
1119 .try_into()
1120 .expect("slice length must match"),
1121 );
1122 pos += 8;
1123 let count = read_u32(data, &mut pos)? as usize;
1124 for _ in 0..count {
1125 result.push(val);
1126 }
1127 }
1128 Ok(result)
1129}
1130
1131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1137pub enum FormatVersion {
1138 V1 = 1,
1140 V2 = 2,
1142}
1143
1144impl FormatVersion {
1145 pub fn from_u32(v: u32) -> Result<Self, String> {
1147 match v {
1148 1 => Ok(FormatVersion::V1),
1149 2 => Ok(FormatVersion::V2),
1150 _ => Err(format!("Unknown format version: {v}")),
1151 }
1152 }
1153
1154 pub fn to_u32(self) -> u32 {
1156 self as u32
1157 }
1158
1159 pub fn supports_compression(self) -> bool {
1161 matches!(self, FormatVersion::V2)
1162 }
1163}
1164
1165impl OxiFile {
1170 pub fn with_version(version: FormatVersion) -> Self {
1172 Self {
1173 version: version.to_u32(),
1174 root: Group::new("/"),
1175 }
1176 }
1177
1178 pub fn format_version(&self) -> Result<FormatVersion, String> {
1180 FormatVersion::from_u32(self.version)
1181 }
1182
1183 pub fn add_binary_mesh(&mut self, group: &str, mesh: &BinaryMesh) {
1185 let bytes = mesh.to_bytes();
1186 let shape = DatasetShape::new(vec![bytes.len()]);
1187 let mut ds = Dataset {
1188 name: "mesh_binary".to_string(),
1189 dtype: DataType::UInt8,
1190 shape,
1191 data: bytes,
1192 attributes: Vec::new(),
1193 };
1194 ds.add_attribute(Attribute::new(
1195 "n_vertices",
1196 AttributeValue::Int(mesh.n_vertices() as i64),
1197 ));
1198 ds.add_attribute(Attribute::new(
1199 "n_triangles",
1200 AttributeValue::Int(mesh.n_triangles() as i64),
1201 ));
1202 SimulationCheckpoint::get_or_create_group(&mut self.root, group).add_dataset(ds);
1203 }
1204
1205 pub fn get_binary_mesh(&self, group: &str) -> Result<BinaryMesh, String> {
1207 let grp = self
1208 .root
1209 .get_subgroup(group)
1210 .ok_or_else(|| format!("Group '{}' not found", group))?;
1211 let ds = grp
1212 .get_dataset("mesh_binary")
1213 .ok_or_else(|| "Dataset 'mesh_binary' not found".to_string())?;
1214 BinaryMesh::from_bytes(&ds.data)
1215 }
1216}
1217
1218#[cfg(test)]
1223mod tests {
1224 use super::*;
1225
1226 #[test]
1227 fn test_dataset_f64_round_trip() {
1228 let original = vec![1.0_f64, 2.5, -3.125, 0.0, 1e10];
1229 let shape = DatasetShape::new(vec![original.len()]);
1230 let ds = Dataset::from_f64_slice("test", &original, shape);
1231 let recovered = ds.to_f64_vec().expect("to_f64_vec failed");
1232 assert_eq!(original, recovered);
1233 }
1234
1235 #[test]
1236 fn test_dataset_f32_round_trip() {
1237 let original = vec![1.0_f32, 2.5, -3.125, 0.0];
1238 let shape = DatasetShape::new(vec![original.len()]);
1239 let ds = Dataset::from_f32_slice("f32ds", &original, shape);
1240 let recovered = ds.to_f32_vec().expect("to_f32_vec failed");
1241 assert_eq!(original, recovered);
1242 }
1243
1244 #[test]
1245 fn test_dataset_i32_round_trip() {
1246 let original = vec![0_i32, -1, 42, i32::MAX, i32::MIN];
1247 let shape = DatasetShape::new(vec![original.len()]);
1248 let ds = Dataset::from_i32_slice("i32ds", &original, shape);
1249 let recovered = ds.to_i32_vec().expect("to_i32_vec failed");
1250 assert_eq!(original, recovered);
1251 }
1252
1253 #[test]
1254 fn test_group_add_get_dataset() {
1255 let mut g = Group::new("particles");
1256 let ds = Dataset::from_f64_slice("energy", &[1.0, 2.0, 3.0], DatasetShape::new(vec![3]));
1257 g.add_dataset(ds);
1258 let found = g.get_dataset("energy").expect("dataset not found");
1259 assert_eq!(found.name, "energy");
1260 assert!(g.get_dataset("missing").is_none());
1261 }
1262
1263 #[test]
1264 fn test_oxifile_round_trip() {
1265 let mut file = OxiFile::new();
1266 let ds = Dataset::from_f64_slice("x", &[1.0, 2.0, 3.0], DatasetShape::new(vec![3]));
1267 file.root.add_dataset(ds);
1268
1269 let bytes = file.write_to_bytes();
1270 let loaded = OxiFile::read_from_bytes(&bytes).expect("round-trip failed");
1271 assert_eq!(loaded.version, 1);
1272 let ds2 = loaded
1273 .root
1274 .get_dataset("x")
1275 .expect("dataset missing after round-trip");
1276 let vals = ds2.to_f64_vec().unwrap();
1277 assert_eq!(vals, vec![1.0, 2.0, 3.0]);
1278 }
1279
1280 #[test]
1281 fn test_read_from_bytes_invalid_magic() {
1282 let bad: Vec<u8> = b"BADMAGIC\x01\x00\x00\x00".to_vec();
1283 let result = OxiFile::read_from_bytes(&bad);
1284 assert!(result.is_err());
1285 assert!(result.unwrap_err().contains("Invalid magic bytes"));
1286 }
1287
1288 #[test]
1289 fn test_dataset_shape_total_elements() {
1290 let s = DatasetShape::new(vec![3, 4, 5]);
1291 assert_eq!(s.total_elements(), 60);
1292 assert_eq!(s.rank(), 3);
1293 assert!(!s.is_scalar());
1294
1295 let scalar = DatasetShape::new(vec![]);
1296 assert_eq!(scalar.total_elements(), 1);
1297 assert!(scalar.is_scalar());
1298 assert_eq!(scalar.rank(), 0);
1299 }
1300
1301 #[test]
1302 fn test_attribute_get_set() {
1303 let mut ds = Dataset::from_f64_slice("d", &[1.0], DatasetShape::new(vec![1]));
1304 ds.add_attribute(Attribute::new(
1305 "units",
1306 AttributeValue::Text("meters".to_string()),
1307 ));
1308 ds.add_attribute(Attribute::new("count", AttributeValue::Int(42)));
1309
1310 let attr = ds.get_attribute("units").expect("units not found");
1311 assert_eq!(attr.value, AttributeValue::Text("meters".to_string()));
1312 assert!(ds.get_attribute("nope").is_none());
1313 }
1314
1315 #[test]
1316 fn test_simulation_checkpoint_positions() {
1317 let mut file = SimulationCheckpoint::new();
1318 let positions = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
1319 SimulationCheckpoint::add_positions(&mut file, "frame0", &positions);
1320
1321 let grp = file.root.get_subgroup("frame0").expect("group missing");
1322 let ds = grp.get_dataset("positions").expect("positions missing");
1323 let vals = ds.to_f64_vec().expect("to_f64_vec failed");
1324
1325 let expected: Vec<f64> = positions.iter().flat_map(|p| p.iter().copied()).collect();
1326 assert_eq!(vals, expected);
1327 assert_eq!(ds.shape.dims, vec![3, 3]);
1328 }
1329
1330 #[test]
1331 fn test_simulation_checkpoint_round_trip() {
1332 let mut file = SimulationCheckpoint::new();
1333 let positions = [[0.1, 0.2, 0.3], [-1.0, 2.0, -3.0]];
1334 SimulationCheckpoint::add_positions(&mut file, "step1", &positions);
1335 SimulationCheckpoint::add_timestep_metadata(&mut file, 1, 0.01, 0.001);
1336
1337 let bytes = file.write_to_bytes();
1338 let loaded = OxiFile::read_from_bytes(&bytes).expect("round-trip failed");
1339
1340 let step_attr = loaded.root.get_attribute("step").expect("step missing");
1341 assert_eq!(step_attr.value, AttributeValue::Int(1));
1342
1343 let grp = loaded.root.get_subgroup("step1").expect("subgroup missing");
1344 let ds = grp.get_dataset("positions").expect("dataset missing");
1345 let vals = ds.to_f64_vec().unwrap();
1346 let expected: Vec<f64> = positions.iter().flat_map(|p| p.iter().copied()).collect();
1347 assert_eq!(vals, expected);
1348 }
1349
1350 #[test]
1355 fn test_endianness_u32_round_trip() {
1356 let v: u32 = 0xDEAD_BEEF;
1357 for end in [Endianness::Little, Endianness::Big] {
1358 let bytes = end.u32_to_bytes(v);
1359 let back = end.u32_from_bytes(bytes);
1360 assert_eq!(back, v, "Endianness {:?} u32 round-trip failed", end);
1361 }
1362 }
1363
1364 #[test]
1365 fn test_endianness_f64_round_trip() {
1366 let v = std::f64::consts::PI;
1367 for end in [Endianness::Little, Endianness::Big] {
1368 let bytes = end.f64_to_bytes(v);
1369 let back = end.f64_from_bytes(bytes);
1370 assert!(
1371 (back - v).abs() < 1e-15,
1372 "Endianness {:?} f64 round-trip failed",
1373 end
1374 );
1375 }
1376 }
1377
1378 #[test]
1379 fn test_endianness_native() {
1380 let native = Endianness::native();
1381 assert!(native == Endianness::Little || native == Endianness::Big);
1382 }
1383
1384 #[test]
1389 fn test_binary_mesh_round_trip() {
1390 let mut mesh = BinaryMesh::new();
1391 mesh.vertices = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
1392 mesh.triangles = vec![[0, 1, 2]];
1393 let bytes = mesh.to_bytes();
1394 let mesh2 = BinaryMesh::from_bytes(&bytes).expect("round-trip failed");
1395 assert_eq!(mesh2.n_vertices(), 3);
1396 assert_eq!(mesh2.n_triangles(), 1);
1397 assert!((mesh2.vertices[1][0] - 1.0).abs() < 1e-15);
1398 assert_eq!(mesh2.triangles[0], [0, 1, 2]);
1399 }
1400
1401 #[test]
1402 fn test_binary_mesh_empty() {
1403 let mesh = BinaryMesh::new();
1404 let bytes = mesh.to_bytes();
1405 let mesh2 = BinaryMesh::from_bytes(&bytes).expect("empty mesh round-trip failed");
1406 assert_eq!(mesh2.n_vertices(), 0);
1407 assert_eq!(mesh2.n_triangles(), 0);
1408 }
1409
1410 #[test]
1415 fn test_binary_particle_data_round_trip() {
1416 let mut pd = BinaryParticleData::new();
1417 pd.positions = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
1418 pd.add_field("density", vec![1000.0, 1200.0]);
1419 pd.add_field("pressure", vec![101325.0, 202650.0]);
1420
1421 let bytes = pd.to_bytes();
1422 let pd2 = BinaryParticleData::from_bytes(&bytes).expect("round-trip failed");
1423 assert_eq!(pd2.n_particles(), 2);
1424 assert_eq!(pd2.n_fields(), 2);
1425 assert_eq!(pd2.field_names[0], "density");
1426 assert!((pd2.scalar_fields[0][1] - 1200.0).abs() < 1e-12);
1427 assert!((pd2.positions[1][2] - 6.0).abs() < 1e-15);
1428 }
1429
1430 #[test]
1431 fn test_binary_particle_data_bad_magic() {
1432 let bad: Vec<u8> = b"BADMAGIC".to_vec();
1433 assert!(BinaryParticleData::from_bytes(&bad).is_err());
1434 }
1435
1436 #[test]
1441 fn test_rle_compress_decompress_round_trip() {
1442 let original = vec![1.0, 1.0, 1.0, 2.5, 2.5, 3.0, 1.0];
1443 let compressed = rle_compress_f64(&original);
1444 let decompressed = rle_decompress_f64(&compressed).expect("decompression failed");
1445 assert_eq!(original.len(), decompressed.len());
1446 for (a, b) in original.iter().zip(decompressed.iter()) {
1447 assert!((a - b).abs() < 1e-15);
1448 }
1449 }
1450
1451 #[test]
1452 fn test_rle_compress_empty() {
1453 let compressed = rle_compress_f64(&[]);
1454 let decompressed = rle_decompress_f64(&compressed).expect("empty decompression failed");
1455 assert!(decompressed.is_empty());
1456 }
1457
1458 #[test]
1459 fn test_rle_compresses_constant_field() {
1460 let original = vec![3.125; 1000];
1461 let compressed = rle_compress_f64(&original);
1462 assert!(
1464 compressed.len() < 100,
1465 "RLE should compress constant field significantly"
1466 );
1467 let decompressed = rle_decompress_f64(&compressed).unwrap();
1468 assert_eq!(decompressed.len(), 1000);
1469 }
1470
1471 #[test]
1476 fn test_format_version_round_trip() {
1477 assert_eq!(FormatVersion::from_u32(1).unwrap(), FormatVersion::V1);
1478 assert_eq!(FormatVersion::from_u32(2).unwrap(), FormatVersion::V2);
1479 assert!(FormatVersion::from_u32(99).is_err());
1480 }
1481
1482 #[test]
1483 fn test_format_version_supports_compression() {
1484 assert!(!FormatVersion::V1.supports_compression());
1485 assert!(FormatVersion::V2.supports_compression());
1486 }
1487
1488 #[test]
1493 fn test_oxifile_binary_mesh_store_retrieve() {
1494 let mut file = OxiFile::new();
1495 let mut mesh = BinaryMesh::new();
1496 mesh.vertices = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
1497 mesh.triangles = vec![[0, 1, 2]];
1498 file.add_binary_mesh("geometry", &mesh);
1499
1500 let mesh2 = file
1501 .get_binary_mesh("geometry")
1502 .expect("retrieve mesh failed");
1503 assert_eq!(mesh2.n_vertices(), 3);
1504 assert_eq!(mesh2.n_triangles(), 1);
1505 }
1506
1507 #[test]
1508 fn test_oxifile_with_version() {
1509 let file = OxiFile::with_version(FormatVersion::V2);
1510 assert_eq!(file.version, 2);
1511 assert_eq!(file.format_version().unwrap(), FormatVersion::V2);
1512 }
1513
1514 #[test]
1515 fn test_datatype_size_bytes() {
1516 assert_eq!(DataType::Float32.size_bytes(), 4);
1517 assert_eq!(DataType::Float64.size_bytes(), 8);
1518 assert_eq!(DataType::Int32.size_bytes(), 4);
1519 assert_eq!(DataType::Int64.size_bytes(), 8);
1520 assert_eq!(DataType::UInt8.size_bytes(), 1);
1521 assert_eq!(DataType::UInt32.size_bytes(), 4);
1522 }
1523
1524 #[test]
1525 fn test_dataset_wrong_type_returns_err() {
1526 let ds = Dataset::from_f64_slice("d", &[1.0, 2.0], DatasetShape::new(vec![2]));
1527 assert!(ds.to_f32_vec().is_err());
1528 assert!(ds.to_i32_vec().is_err());
1529 }
1530}