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