1use super::types::{DataType, Dataset, ShdfGroup, XdmfParams};
6
7pub(super) const MAGIC: &[u8; 4] = b"SHDF";
8pub(super) const VERSION: u32 = 1;
9#[allow(dead_code)]
13pub fn encode_string(s: &str) -> Vec<u8> {
14 let bytes = s.as_bytes();
15 let mut out = Vec::with_capacity(4 + bytes.len());
16 out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
17 out.extend_from_slice(bytes);
18 out
19}
20#[allow(dead_code)]
24pub fn decode_string(data: &[u8], offset: &mut usize) -> Result<String, String> {
25 let len = read_u32(data, offset)? as usize;
26 if *offset + len > data.len() {
27 return Err(format!(
28 "string data out of bounds: need {} bytes at offset {}",
29 len, offset
30 ));
31 }
32 let s = std::str::from_utf8(&data[*offset..*offset + len])
33 .map_err(|e| format!("invalid UTF-8 in string: {e}"))?
34 .to_string();
35 *offset += len;
36 Ok(s)
37}
38pub(super) fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8, String> {
39 if *pos >= data.len() {
40 return Err(format!("unexpected EOF reading u8 at offset {pos}"));
41 }
42 let v = data[*pos];
43 *pos += 1;
44 Ok(v)
45}
46pub(super) fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, String> {
47 require_bytes(data, *pos, 4)?;
48 let v = u32::from_le_bytes(
49 data[*pos..*pos + 4]
50 .try_into()
51 .expect("slice length must match"),
52 );
53 *pos += 4;
54 Ok(v)
55}
56pub(super) fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, String> {
57 require_bytes(data, *pos, 8)?;
58 let v = u64::from_le_bytes(
59 data[*pos..*pos + 8]
60 .try_into()
61 .expect("slice length must match"),
62 );
63 *pos += 8;
64 Ok(v)
65}
66pub(super) fn read_f32(data: &[u8], pos: &mut usize) -> Result<f32, String> {
67 require_bytes(data, *pos, 4)?;
68 let v = f32::from_le_bytes(
69 data[*pos..*pos + 4]
70 .try_into()
71 .expect("slice length must match"),
72 );
73 *pos += 4;
74 Ok(v)
75}
76pub(super) fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64, String> {
77 require_bytes(data, *pos, 8)?;
78 let v = f64::from_le_bytes(
79 data[*pos..*pos + 8]
80 .try_into()
81 .expect("slice length must match"),
82 );
83 *pos += 8;
84 Ok(v)
85}
86pub(super) fn read_i32(data: &[u8], pos: &mut usize) -> Result<i32, String> {
87 require_bytes(data, *pos, 4)?;
88 let v = i32::from_le_bytes(
89 data[*pos..*pos + 4]
90 .try_into()
91 .expect("slice length must match"),
92 );
93 *pos += 4;
94 Ok(v)
95}
96pub(super) fn read_i64(data: &[u8], pos: &mut usize) -> Result<i64, String> {
97 require_bytes(data, *pos, 8)?;
98 let v = i64::from_le_bytes(
99 data[*pos..*pos + 8]
100 .try_into()
101 .expect("slice length must match"),
102 );
103 *pos += 8;
104 Ok(v)
105}
106pub(super) fn require_bytes(data: &[u8], pos: usize, n: usize) -> Result<(), String> {
107 if pos + n > data.len() {
108 Err(format!(
109 "unexpected EOF: need {n} bytes at offset {pos}, have {}",
110 data.len()
111 ))
112 } else {
113 Ok(())
114 }
115}
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::hdf5_simple::AttributeHelper;
120
121 use crate::hdf5_simple::ChunkingConfig;
122
123 use crate::hdf5_simple::CompressionAlgorithm;
124
125 use crate::hdf5_simple::CompressionSettings;
126
127 use crate::hdf5_simple::ShdfFile;
128 use crate::hdf5_simple::ShdfSchema;
129
130 use crate::hdf5_simple::types::*;
131 #[test]
132 fn test_roundtrip_f64() {
133 let mut file = ShdfFile::new();
134 let original_data: Vec<f64> = vec![1.0, 2.5, -3.15625, 0.0, 1e100];
135 file.add_dataset_f64("temperatures", vec![5], original_data.clone());
136 let bytes = file.to_bytes();
137 let recovered = ShdfFile::from_bytes(&bytes).expect("from_bytes failed");
138 let got = recovered
139 .get_f64("temperatures")
140 .expect("dataset not found");
141 assert_eq!(got.len(), original_data.len());
142 for (a, b) in original_data.iter().zip(got.iter()) {
143 assert_eq!(a.to_bits(), b.to_bits(), "f64 value mismatch: {a} vs {b}");
144 }
145 }
146 #[test]
147 fn test_roundtrip_i32() {
148 let mut file = ShdfFile::new();
149 let original_data: Vec<i32> = vec![0, 1, -1, i32::MAX, i32::MIN];
150 file.add_dataset_i32("indices", vec![5], original_data.clone());
151 let bytes = file.to_bytes();
152 let recovered = ShdfFile::from_bytes(&bytes).expect("from_bytes failed");
153 let got = recovered.get_i32("indices").expect("dataset not found");
154 assert_eq!(got, original_data.as_slice());
155 }
156 #[test]
157 fn test_multiple_datasets() {
158 let mut file = ShdfFile::new();
159 file.add_dataset_f64("velocities", vec![3, 3], vec![0.1; 9]);
160 file.add_dataset_i32("labels", vec![3], vec![10, 20, 30]);
161 file.add_dataset_f64("pressure", vec![1], vec![101325.0]);
162 let bytes = file.to_bytes();
163 let recovered = ShdfFile::from_bytes(&bytes).expect("from_bytes failed");
164 let vel = recovered.get_f64("velocities").expect("velocities missing");
165 assert_eq!(vel.len(), 9);
166 let lbl = recovered.get_i32("labels").expect("labels missing");
167 assert_eq!(lbl, &[10, 20, 30]);
168 let pres = recovered.get_f64("pressure").expect("pressure missing");
169 assert!((pres[0] - 101325.0).abs() < 1e-6);
170 assert_eq!(recovered.datasets[0].shape, vec![3, 3]);
171 assert_eq!(recovered.datasets[1].shape, vec![3]);
172 assert_eq!(recovered.datasets[2].shape, vec![1]);
173 }
174 #[test]
175 fn test_empty_file_roundtrip() {
176 let file = ShdfFile::new();
177 let bytes = file.to_bytes();
178 let recovered = ShdfFile::from_bytes(&bytes).expect("from_bytes failed");
179 assert!(recovered.datasets.is_empty());
180 assert!(recovered.global_attributes.is_empty());
181 }
182 #[test]
183 fn test_attribute_roundtrip() {
184 let mut file = ShdfFile::new();
185 file.add_global_attr("author", "Team KitaSan");
186 file.add_global_attr("date", "2026-03-14");
187 file.add_dataset_f64("energy", vec![2], vec![1.0, 2.0]);
188 file.datasets[0]
189 .attributes
190 .push(("units".to_string(), "Joules".to_string()));
191 let bytes = file.to_bytes();
192 let recovered = ShdfFile::from_bytes(&bytes).expect("from_bytes failed");
193 assert_eq!(recovered.global_attributes.len(), 2);
194 assert_eq!(
195 recovered.global_attributes[0],
196 ("author".to_string(), "Team KitaSan".to_string())
197 );
198 assert_eq!(
199 recovered.global_attributes[1],
200 ("date".to_string(), "2026-03-14".to_string())
201 );
202 assert_eq!(recovered.datasets[0].attributes.len(), 1);
203 assert_eq!(recovered.datasets[0].attributes[0].0, "units");
204 assert_eq!(recovered.datasets[0].attributes[0].1, "Joules");
205 }
206 #[test]
207 fn test_write_to_text() {
208 let mut file = ShdfFile::new();
209 file.add_global_attr("title", "Demo");
210 file.add_dataset_f64("pos", vec![2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
211 let text = file.write_to_text();
212 assert!(text.contains("SHDF"));
213 assert!(text.contains("title = Demo"));
214 assert!(text.contains("[pos]"));
215 assert!(text.contains("2Γ3"));
216 assert!(text.contains("Float64"));
217 }
218 #[test]
219 fn test_encode_decode_string() {
220 let original = "hello, δΈη!";
221 let encoded = encode_string(original);
222 let mut offset = 0usize;
223 let decoded = decode_string(&encoded, &mut offset).expect("decode failed");
224 assert_eq!(decoded, original);
225 assert_eq!(offset, encoded.len());
226 }
227 #[test]
228 fn test_group_empty() {
229 let group = ShdfGroup::new("root");
230 assert_eq!(group.name, "root");
231 assert!(group.datasets.is_empty());
232 assert!(group.children.is_empty());
233 assert_eq!(group.total_datasets(), 0);
234 }
235 #[test]
236 fn test_group_add_dataset() {
237 let mut group = ShdfGroup::new("root");
238 group.add_dataset_f64("temperatures", vec![10], vec![0.0; 10]);
239 assert_eq!(group.datasets.len(), 1);
240 assert_eq!(group.total_datasets(), 1);
241 assert!(group.get_dataset("temperatures").is_some());
242 }
243 #[test]
244 fn test_group_nested() {
245 let mut root = ShdfGroup::new("root");
246 let mut child = ShdfGroup::new("particles");
247 child.add_dataset_f64("positions", vec![100, 3], vec![0.0; 300]);
248 child.add_dataset_i32("types", vec![100], vec![0; 100]);
249 root.add_child(child);
250 assert_eq!(root.total_datasets(), 2);
251 let particles = root.get_child("particles").unwrap();
252 assert_eq!(particles.datasets.len(), 2);
253 }
254 #[test]
255 fn test_group_attributes() {
256 let mut group = ShdfGroup::new("simulation");
257 group.add_attribute("timestep", "0.001");
258 group.add_attribute("units", "SI");
259 assert_eq!(group.attributes.len(), 2);
260 }
261 #[test]
262 fn test_group_summary() {
263 let mut root = ShdfGroup::new("root");
264 root.add_dataset_f64("energy", vec![1], vec![42.0]);
265 let summary = root.summary(0);
266 assert!(summary.contains("root"));
267 assert!(summary.contains("energy"));
268 }
269 #[test]
270 fn test_group_get_missing_dataset() {
271 let group = ShdfGroup::new("root");
272 assert!(group.get_dataset("nonexistent").is_none());
273 }
274 #[test]
275 fn test_group_get_missing_child() {
276 let group = ShdfGroup::new("root");
277 assert!(group.get_child("nonexistent").is_none());
278 }
279 #[test]
280 fn test_chunking_n_chunks_1d() {
281 let config = ChunkingConfig::new(vec![10]);
282 let n = config.n_chunks(&[100]);
283 assert_eq!(n, 10);
284 }
285 #[test]
286 fn test_chunking_n_chunks_2d() {
287 let config = ChunkingConfig::new(vec![10, 5]);
288 let n = config.n_chunks(&[20, 15]);
289 assert_eq!(n, 2 * 3);
290 }
291 #[test]
292 fn test_chunking_n_chunks_remainder() {
293 let config = ChunkingConfig::new(vec![10]);
294 let n = config.n_chunks(&[15]);
295 assert_eq!(n, 2);
296 }
297 #[test]
298 fn test_chunking_chunk_index() {
299 let config = ChunkingConfig::new(vec![10]);
300 assert_eq!(config.chunk_index(&[5], &[100]), 0);
301 assert_eq!(config.chunk_index(&[15], &[100]), 1);
302 }
303 #[test]
304 fn test_chunking_default() {
305 let config = ChunkingConfig::default_for_shape(&[1000, 3]);
306 assert_eq!(config.chunk_dims, vec![64, 3]);
307 }
308 #[test]
309 fn test_chunking_mismatched_dims() {
310 let config = ChunkingConfig::new(vec![10, 10]);
311 assert_eq!(config.n_chunks(&[100]), 0);
312 }
313 #[test]
314 fn test_delta_encode_decode_f64() {
315 let data = vec![1.0, 3.0, 6.0, 10.0, 15.0];
316 let encoded = CompressionSettings::delta_encode_f64(&data);
317 let decoded = CompressionSettings::delta_decode_f64(&encoded);
318 for (a, b) in data.iter().zip(decoded.iter()) {
319 assert!((a - b).abs() < 1e-14, "Mismatch: {a} vs {b}");
320 }
321 }
322 #[test]
323 fn test_delta_encode_decode_i32() {
324 let data = vec![10, 20, 30, 25, 35];
325 let encoded = CompressionSettings::delta_encode_i32(&data);
326 let decoded = CompressionSettings::delta_decode_i32(&encoded);
327 assert_eq!(data, decoded);
328 }
329 #[test]
330 fn test_delta_encode_empty() {
331 assert!(CompressionSettings::delta_encode_f64(&[]).is_empty());
332 assert!(CompressionSettings::delta_decode_f64(&[]).is_empty());
333 assert!(CompressionSettings::delta_encode_i32(&[]).is_empty());
334 assert!(CompressionSettings::delta_decode_i32(&[]).is_empty());
335 }
336 #[test]
337 fn test_delta_encode_single() {
338 let data = vec![42.0];
339 let encoded = CompressionSettings::delta_encode_f64(&data);
340 let decoded = CompressionSettings::delta_decode_f64(&encoded);
341 assert_eq!(decoded, data);
342 }
343 #[test]
344 fn test_compression_none() {
345 let settings = CompressionSettings::none();
346 assert_eq!(settings.algorithm, CompressionAlgorithm::None);
347 assert_eq!(settings.level, 0);
348 }
349 #[test]
350 fn test_compression_delta() {
351 let settings = CompressionSettings::delta();
352 assert_eq!(settings.algorithm, CompressionAlgorithm::Delta);
353 }
354 #[test]
355 fn test_attribute_value_string_roundtrip() {
356 let val = AttributeValue::String("hello".to_string());
357 let s = AttributeHelper::to_string(&val);
358 let recovered = AttributeHelper::from_string(&s);
359 assert_eq!(recovered, val);
360 }
361 #[test]
362 fn test_attribute_value_float_roundtrip() {
363 let val = AttributeValue::Float64(3.125);
364 let s = AttributeHelper::to_string(&val);
365 let recovered = AttributeHelper::from_string(&s);
366 if let AttributeValue::Float64(f) = recovered {
367 assert!((f - 3.125).abs() < 1e-10);
368 } else {
369 panic!("Expected Float64");
370 }
371 }
372 #[test]
373 fn test_attribute_value_int_roundtrip() {
374 let val = AttributeValue::Int32(42);
375 let s = AttributeHelper::to_string(&val);
376 let recovered = AttributeHelper::from_string(&s);
377 assert_eq!(recovered, val);
378 }
379 #[test]
380 fn test_attribute_value_bool_roundtrip() {
381 let val = AttributeValue::Bool(true);
382 let s = AttributeHelper::to_string(&val);
383 let recovered = AttributeHelper::from_string(&s);
384 assert_eq!(recovered, val);
385 let val2 = AttributeValue::Bool(false);
386 let s2 = AttributeHelper::to_string(&val2);
387 let recovered2 = AttributeHelper::from_string(&s2);
388 assert_eq!(recovered2, val2);
389 }
390 #[test]
391 fn test_attribute_value_unknown_prefix() {
392 let recovered = AttributeHelper::from_string("unknown_value");
393 assert_eq!(
394 recovered,
395 AttributeValue::String("unknown_value".to_string())
396 );
397 }
398 #[test]
399 fn test_schema_valid_file() {
400 let mut schema = ShdfSchema::new();
401 schema.expect_dataset("positions", DataType::Float64);
402 schema.require_attribute("author");
403 let mut file = ShdfFile::new();
404 file.add_dataset_f64("positions", vec![10, 3], vec![0.0; 30]);
405 file.add_global_attr("author", "Test");
406 let errors = schema.validate(&file);
407 assert!(errors.is_empty(), "Expected no errors, got: {:?}", errors);
408 }
409 #[test]
410 fn test_schema_missing_dataset() {
411 let mut schema = ShdfSchema::new();
412 schema.expect_dataset("positions", DataType::Float64);
413 let file = ShdfFile::new();
414 let errors = schema.validate(&file);
415 assert_eq!(errors.len(), 1);
416 assert!(errors[0].contains("Missing dataset: positions"));
417 }
418 #[test]
419 fn test_schema_wrong_dtype() {
420 let mut schema = ShdfSchema::new();
421 schema.expect_dataset("indices", DataType::Float64);
422 let mut file = ShdfFile::new();
423 file.add_dataset_i32("indices", vec![10], vec![0; 10]);
424 let errors = schema.validate(&file);
425 assert_eq!(errors.len(), 1);
426 assert!(errors[0].contains("expected"));
427 }
428 #[test]
429 fn test_schema_missing_attribute() {
430 let mut schema = ShdfSchema::new();
431 schema.require_attribute("version");
432 let file = ShdfFile::new();
433 let errors = schema.validate(&file);
434 assert_eq!(errors.len(), 1);
435 assert!(errors[0].contains("Missing global attribute: version"));
436 }
437 #[test]
438 fn test_schema_empty_valid() {
439 let schema = ShdfSchema::new();
440 let file = ShdfFile::new();
441 let errors = schema.validate(&file);
442 assert!(errors.is_empty());
443 }
444 #[test]
445 fn test_datatype_equality() {
446 assert_eq!(DataType::Float64, DataType::Float64);
447 assert_ne!(DataType::Float64, DataType::Float32);
448 assert_ne!(DataType::Int32, DataType::Int64);
449 }
450 #[test]
451 fn test_from_bytes_bad_magic() {
452 let data = b"BAAD\x01\x00\x00\x00";
453 let result = ShdfFile::from_bytes(data);
454 assert!(result.is_err());
455 }
456 #[test]
457 fn test_from_bytes_too_short() {
458 let result = ShdfFile::from_bytes(&[0u8; 3]);
459 assert!(result.is_err());
460 }
461 #[test]
462 fn test_from_bytes_bad_version() {
463 let mut data = Vec::new();
464 data.extend_from_slice(b"SHDF");
465 data.extend_from_slice(&99u32.to_le_bytes());
466 let result = ShdfFile::from_bytes(&data);
467 assert!(result.is_err());
468 }
469}
470#[allow(dead_code)]
472pub fn write_xdmf(path: &str, params: &XdmfParams) -> std::io::Result<()> {
473 use std::io::Write;
474 let file = std::fs::File::create(path)?;
475 let mut w = std::io::BufWriter::new(file);
476 writeln!(w, r#"<?xml version="1.0" ?>"#)?;
477 writeln!(w, r#"<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>"#)?;
478 writeln!(w, r#"<Xdmf Version="2.0">"#)?;
479 writeln!(w, r#" <Domain>"#)?;
480 writeln!(w, r#" <Grid Name="Mesh" GridType="Uniform">"#)?;
481 writeln!(
482 w,
483 r#" <Topology TopologyType="{}" NumberOfElements="{}">"#,
484 params.topology.as_str(),
485 params.n_elements
486 )?;
487 writeln!(
488 w,
489 r#" <DataItem Dimensions="{} {}" NumberType="Int" Format="HDF">"#,
490 params.n_elements, params.nodes_per_element
491 )?;
492 writeln!(
493 w,
494 r#" {}:{}"#,
495 params.hdf5_path, params.connectivity_dataset
496 )?;
497 writeln!(w, r#" </DataItem>"#)?;
498 writeln!(w, r#" </Topology>"#)?;
499 writeln!(w, r#" <Geometry GeometryType="XYZ">"#)?;
500 writeln!(
501 w,
502 r#" <DataItem Dimensions="{} 3" NumberType="Float" Precision="8" Format="HDF">"#,
503 params.n_nodes
504 )?;
505 writeln!(
506 w,
507 r#" {}:{}"#,
508 params.hdf5_path, params.coords_dataset
509 )?;
510 writeln!(w, r#" </DataItem>"#)?;
511 writeln!(w, r#" </Geometry>"#)?;
512 for (attr_name, ds_path) in ¶ms.attributes {
513 writeln!(
514 w,
515 r#" <Attribute Name="{attr_name}" AttributeType="Scalar" Center="Node">"#
516 )?;
517 writeln!(
518 w,
519 r#" <DataItem Dimensions="{}" NumberType="Float" Precision="8" Format="HDF">"#,
520 params.n_nodes
521 )?;
522 writeln!(w, r#" {}:{}"#, params.hdf5_path, ds_path)?;
523 writeln!(w, r#" </DataItem>"#)?;
524 writeln!(w, r#" </Attribute>"#)?;
525 }
526 writeln!(w, r#" </Grid>"#)?;
527 writeln!(w, r#" </Domain>"#)?;
528 writeln!(w, r#"</Xdmf>"#)?;
529 w.flush()?;
530 Ok(())
531}
532#[cfg(test)]
533mod tests_hdf5_extended {
534 use super::*;
535
536 use crate::hdf5_simple::ChunkedDataset;
537
538 use crate::hdf5_simple::CompoundDataset;
539 use crate::hdf5_simple::CompoundField;
540
541 use crate::hdf5_simple::CompressionLevel;
542
543 use crate::hdf5_simple::DeflateMetadata;
544
545 use crate::hdf5_simple::XdmfTopologyType;
546 use crate::hdf5_simple::types::*;
547 #[test]
548 fn test_chunked_dataset_n_elements() {
549 let ds = ChunkedDataset::new("data", vec![100, 50], vec![10, 10]);
550 assert_eq!(ds.n_elements(), 5000);
551 }
552 #[test]
553 fn test_chunked_dataset_n_chunks_1d() {
554 let ds = ChunkedDataset::new("v", vec![100], vec![32]);
555 let nchunks = ds.n_chunks_per_dim();
556 assert_eq!(nchunks[0], 4);
557 }
558 #[test]
559 fn test_chunked_dataset_total_chunks() {
560 let ds = ChunkedDataset::new("v", vec![100, 50], vec![10, 10]);
561 assert_eq!(ds.total_chunks(), 50);
562 }
563 #[test]
564 fn test_chunked_dataset_write_read_chunk() {
565 let mut ds = ChunkedDataset::new("v", vec![10], vec![5]);
566 ds.write_chunk_1d(0, &[1.0, 2.0, 3.0, 4.0, 5.0]);
567 ds.write_chunk_1d(1, &[6.0, 7.0, 8.0, 9.0, 10.0]);
568 assert!((ds.data[0] - 1.0).abs() < 1e-12);
569 assert!((ds.data[9] - 10.0).abs() < 1e-12);
570 }
571 #[test]
572 fn test_chunked_dataset_to_bytes_nonempty() {
573 let mut ds = ChunkedDataset::new("pressure", vec![4], vec![2]);
574 ds.data = vec![1.0, 2.0, 3.0, 4.0];
575 let bytes = ds.to_bytes();
576 assert!(!bytes.is_empty());
577 }
578 #[test]
579 fn test_chunked_dataset_attrs() {
580 let mut ds = ChunkedDataset::new("temp", vec![5], vec![5]);
581 ds.add_attr("units", "K");
582 ds.add_attr("long_name", "Temperature");
583 assert_eq!(ds.attrs.len(), 2);
584 }
585 #[test]
586 fn test_shdf_group_new() {
587 let g = ShdfGroup::new("root");
588 assert_eq!(g.name, "root");
589 assert!(g.datasets.is_empty());
590 assert!(g.children.is_empty());
591 }
592 #[test]
593 fn test_shdf_group_add_dataset() {
594 let mut g = ShdfGroup::new("data");
595 g.add_dataset_f64("velocity", vec![3], vec![1.0, 2.0, 3.0]);
596 assert_eq!(g.datasets.len(), 1);
597 assert!(g.get_dataset("velocity").is_some());
598 }
599 #[test]
600 fn test_shdf_group_hierarchy() {
601 let mut root = ShdfGroup::new("root");
602 let child1 = ShdfGroup::new("geometry");
603 let child2 = ShdfGroup::new("fields");
604 root.add_child(child1);
605 root.add_child(child2);
606 assert_eq!(root.children.len(), 2);
607 assert!(root.get_child("geometry").is_some());
608 assert!(root.get_child("missing").is_none());
609 }
610 #[test]
611 fn test_shdf_group_total_datasets_recursive() {
612 let mut root = ShdfGroup::new("root");
613 let mut child = ShdfGroup::new("child");
614 child.add_dataset_f64("x", vec![1], vec![0.0]);
615 root.add_child(child);
616 assert_eq!(root.total_datasets(), 1);
617 }
618 #[test]
619 fn test_shdf_group_name() {
620 let g = ShdfGroup::new("geometry");
621 assert_eq!(g.name, "geometry");
622 }
623 #[test]
624 fn test_write_xdmf_creates_file() {
625 let params = XdmfParams {
626 hdf5_path: "sim.shdf".to_string(),
627 coords_dataset: "/coordinates".to_string(),
628 connectivity_dataset: "/connectivity".to_string(),
629 n_nodes: 100,
630 n_elements: 50,
631 nodes_per_element: 4,
632 topology: XdmfTopologyType::Tetrahedron,
633 attributes: vec![("pressure".to_string(), "/fields/pressure".to_string())],
634 };
635 let path = "/tmp/test_oxiphysics_xdmf.xdmf";
636 write_xdmf(path, ¶ms).unwrap();
637 let content = std::fs::read_to_string(path).unwrap();
638 assert!(content.contains("Xdmf"));
639 assert!(content.contains("Tetrahedron"));
640 assert!(content.contains("pressure"));
641 assert!(content.contains("sim.shdf"));
642 std::fs::remove_file(path).ok();
643 }
644 #[test]
645 fn test_xdmf_topology_type_str() {
646 assert_eq!(XdmfTopologyType::Triangle.as_str(), "Triangle");
647 assert_eq!(XdmfTopologyType::Hexahedron.as_str(), "Hexahedron");
648 }
649 #[test]
650 fn test_compression_level_values() {
651 assert_eq!(CompressionLevel::None.level(), 0);
652 assert_eq!(CompressionLevel::Fast.level(), 1);
653 assert_eq!(CompressionLevel::Balanced.level(), 5);
654 assert_eq!(CompressionLevel::Maximum.level(), 9);
655 }
656 #[test]
657 fn test_compression_level_is_compressed() {
658 assert!(!CompressionLevel::None.is_compressed());
659 assert!(CompressionLevel::Fast.is_compressed());
660 assert!(CompressionLevel::Maximum.is_compressed());
661 }
662 #[test]
663 fn test_deflate_metadata_uncompressed() {
664 let meta = DeflateMetadata::uncompressed(1000);
665 assert_eq!(meta.compression_ratio(), 1.0);
666 assert_eq!(meta.space_savings(), 0.0);
667 }
668 #[test]
669 fn test_deflate_metadata_compression_ratio() {
670 let meta = DeflateMetadata {
671 level: CompressionLevel::Balanced,
672 shuffle: true,
673 chunk_shape: vec![100],
674 compressed_size: 400,
675 uncompressed_size: 1000,
676 };
677 assert!(
678 (meta.compression_ratio() - 2.5).abs() < 1e-10,
679 "ratio={}",
680 meta.compression_ratio()
681 );
682 assert!(
683 (meta.space_savings() - 0.6).abs() < 1e-10,
684 "savings={}",
685 meta.space_savings()
686 );
687 }
688 #[test]
689 fn test_deflate_metadata_zero_compressed() {
690 let meta = DeflateMetadata {
691 level: CompressionLevel::Balanced,
692 shuffle: false,
693 chunk_shape: vec![],
694 compressed_size: 0,
695 uncompressed_size: 1000,
696 };
697 assert_eq!(meta.compression_ratio(), 1.0);
698 }
699 #[test]
700 fn test_compound_dataset_new() {
701 let ds = CompoundDataset::new("particles", 10);
702 assert_eq!(ds.n_records, 10);
703 assert_eq!(ds.n_fields(), 0);
704 }
705 #[test]
706 fn test_compound_dataset_add_field() {
707 let mut ds = CompoundDataset::new("atoms", 3);
708 ds.add_field(CompoundField::new(
709 "x",
710 DataType::Float64,
711 vec![1.0, 2.0, 3.0],
712 ));
713 ds.add_field(CompoundField::new(
714 "y",
715 DataType::Float64,
716 vec![4.0, 5.0, 6.0],
717 ));
718 assert_eq!(ds.n_fields(), 2);
719 let xs = ds.get_field("x").unwrap();
720 assert_eq!(xs.len(), 3);
721 assert!((xs[0] - 1.0).abs() < 1e-12);
722 }
723 #[test]
724 fn test_compound_dataset_get_missing_field() {
725 let ds = CompoundDataset::new("particles", 5);
726 assert!(ds.get_field("missing").is_none());
727 }
728 #[test]
729 fn test_compound_dataset_to_csv_bytes() {
730 let mut ds = CompoundDataset::new("data", 2);
731 ds.add_field(CompoundField::new("a", DataType::Float64, vec![1.0, 2.0]));
732 ds.add_field(CompoundField::new("b", DataType::Float32, vec![3.0, 4.0]));
733 let csv = ds.to_csv_bytes();
734 let s = String::from_utf8(csv).unwrap();
735 assert!(s.contains("a,b"));
736 assert!(s.contains("1.000000"));
737 }
738 #[test]
739 fn test_compound_dataset_attrs() {
740 let mut ds = CompoundDataset::new("traj", 0);
741 ds.add_attr("source", "MD simulation");
742 ds.add_attr("units", "nm");
743 assert_eq!(ds.attrs.len(), 2);
744 }
745 #[test]
746 #[should_panic]
747 fn test_compound_dataset_wrong_field_length() {
748 let mut ds = CompoundDataset::new("atoms", 5);
749 ds.add_field(CompoundField::new(
750 "x",
751 DataType::Float64,
752 vec![1.0, 2.0, 3.0],
753 ));
754 }
755}
756pub fn cdl_dump(root: &ShdfGroup) -> String {
759 let mut out = String::new();
760 cdl_dump_group(root, 0, &mut out);
761 out
762}
763pub(super) fn cdl_dump_group(group: &ShdfGroup, depth: usize, out: &mut String) {
764 let indent = " ".repeat(depth);
765 out.push_str(&format!("{}group: {} {{\n", indent, group.name));
766 for ds in &group.datasets {
767 cdl_dump_dataset(ds, depth + 1, out);
768 }
769 for child in &group.children {
770 cdl_dump_group(child, depth + 1, out);
771 }
772 out.push_str(&format!("{}}} // group: {}\n", indent, group.name));
773}
774pub(super) fn cdl_dump_dataset(ds: &Dataset, depth: usize, out: &mut String) {
775 let indent = " ".repeat(depth);
776 let type_str = match ds.dtype {
777 DataType::Float64 => "double",
778 DataType::Float32 => "float",
779 DataType::Int32 => "int",
780 DataType::Int64 => "int64",
781 };
782 let shape_str: Vec<String> = ds.shape.iter().map(|d| d.to_string()).collect();
783 out.push_str(&format!(
784 "{}{} {}({}) ;\n",
785 indent,
786 type_str,
787 ds.name,
788 shape_str.join(", ")
789 ));
790 for (k, v) in &ds.attributes {
791 out.push_str(&format!("{} {}:{} = \"{}\" ;\n", indent, ds.name, k, v));
792 }
793 let preview_len = ds.data_f64.len().min(8);
794 if preview_len > 0 {
795 let vals: Vec<String> = ds.data_f64[..preview_len]
796 .iter()
797 .map(|v| format!("{:.6}", v))
798 .collect();
799 let ellipsis = if ds.data_f64.len() > 8 { ", ..." } else { "" };
800 out.push_str(&format!(
801 "{} // data = {}{} ;\n",
802 indent,
803 vals.join(", "),
804 ellipsis
805 ));
806 }
807}
808#[cfg(test)]
809mod tests_hdf5_additions {
810 use super::*;
811
812 use crate::hdf5_simple::DatasetStats;
813
814 use crate::hdf5_simple::GroupNavigator;
815
816 use crate::hdf5_simple::TimeSeriesAppender;
817 use crate::hdf5_simple::VirtualLink;
818
819 use crate::hdf5_simple::types::*;
820 #[test]
821 fn test_navigator_get_dataset_simple() {
822 let mut root = ShdfGroup::new("root");
823 root.add_dataset_f64("pos", vec![3], vec![1.0, 2.0, 3.0]);
824 let nav = GroupNavigator::new(root);
825 assert!(nav.get_dataset("/root/pos").is_some());
826 assert!(nav.get_dataset("/root/missing").is_none());
827 }
828 #[test]
829 fn test_navigator_nested_path() {
830 let mut root = ShdfGroup::new("root");
831 let mut sim = ShdfGroup::new("simulation");
832 let mut atoms = ShdfGroup::new("atoms");
833 atoms.add_dataset_f64("positions", vec![10, 3], vec![0.0; 30]);
834 sim.add_child(atoms);
835 root.add_child(sim);
836 let nav = GroupNavigator::new(root);
837 assert!(
838 nav.get_dataset("/root/simulation/atoms/positions")
839 .is_some()
840 );
841 assert_eq!(nav.total_datasets(), 1);
842 }
843 #[test]
844 fn test_navigator_all_paths() {
845 let mut root = ShdfGroup::new("root");
846 root.add_dataset_f64("a", vec![1], vec![1.0]);
847 root.add_dataset_f64("b", vec![1], vec![2.0]);
848 let nav = GroupNavigator::new(root);
849 let paths = nav.all_paths();
850 assert_eq!(paths.len(), 2);
851 assert!(paths.iter().any(|p| p.ends_with("/a")));
852 assert!(paths.iter().any(|p| p.ends_with("/b")));
853 }
854 #[test]
855 fn test_navigator_empty_group() {
856 let root = ShdfGroup::new("empty");
857 let nav = GroupNavigator::new(root);
858 assert_eq!(nav.all_paths().len(), 0);
859 assert_eq!(nav.total_datasets(), 0);
860 }
861 #[test]
862 fn test_navigator_leading_slash_optional() {
863 let mut root = ShdfGroup::new("root");
864 root.add_dataset_f64("v", vec![2], vec![1.0, 2.0]);
865 let nav = GroupNavigator::new(root);
866 assert!(nav.get_dataset("/root/v").is_some());
867 }
868 #[test]
869 fn test_timeseries_appender_basic() {
870 let mut app = TimeSeriesAppender::new("temperature", 3);
871 app.append(&[300.0, 301.0, 302.0]);
872 app.append(&[303.0, 304.0, 305.0]);
873 assert_eq!(app.n_frames, 2);
874 assert_eq!(app.total_samples(), 6);
875 }
876 #[test]
877 fn test_timeseries_get_frame() {
878 let mut app = TimeSeriesAppender::new("vel", 3);
879 app.append(&[1.0, 2.0, 3.0]);
880 app.append(&[4.0, 5.0, 6.0]);
881 let frame = app.get_frame(1).unwrap();
882 assert_eq!(frame, &[4.0, 5.0, 6.0]);
883 }
884 #[test]
885 fn test_timeseries_get_frame_out_of_bounds() {
886 let app = TimeSeriesAppender::new("x", 2);
887 assert!(app.get_frame(0).is_none());
888 }
889 #[test]
890 fn test_timeseries_to_dataset_shape() {
891 let mut app = TimeSeriesAppender::new("pressure", 1);
892 app.append(&[1.0]);
893 app.append(&[2.0]);
894 app.append(&[3.0]);
895 let ds = app.to_dataset();
896 assert_eq!(ds.shape, vec![3, 1]);
897 assert!(ds.attributes.iter().any(|(k, _)| k == "n_frames"));
898 }
899 #[test]
900 #[should_panic]
901 fn test_timeseries_wrong_frame_width() {
902 let mut app = TimeSeriesAppender::new("x", 3);
903 app.append(&[1.0, 2.0]);
904 }
905 #[test]
906 fn test_cdl_dump_contains_group_name() {
907 let mut root = ShdfGroup::new("simulation");
908 root.add_dataset_f64("energy", vec![5], vec![1.0; 5]);
909 let cdl = cdl_dump(&root);
910 assert!(cdl.contains("simulation"), "CDL should contain group name");
911 assert!(cdl.contains("energy"), "CDL should contain dataset name");
912 assert!(cdl.contains("double"), "CDL should contain type");
913 }
914 #[test]
915 fn test_cdl_dump_nested() {
916 let mut root = ShdfGroup::new("root");
917 let mut child = ShdfGroup::new("atoms");
918 child.add_dataset_f64("x", vec![4], vec![1.0, 2.0, 3.0, 4.0]);
919 root.add_child(child);
920 let cdl = cdl_dump(&root);
921 assert!(cdl.contains("atoms"));
922 assert!(cdl.contains("x"));
923 }
924 #[test]
925 fn test_cdl_dump_with_attrs() {
926 let mut root = ShdfGroup::new("meta");
927 let mut ds = Dataset {
928 name: "temp".to_string(),
929 dtype: DataType::Float32,
930 shape: vec![2],
931 data_f64: vec![300.0, 310.0],
932 data_i32: Vec::new(),
933 attributes: vec![("units".to_string(), "K".to_string())],
934 };
935 ds.attributes
936 .push(("source".to_string(), "sensor".to_string()));
937 root.datasets.push(ds);
938 let cdl = cdl_dump(&root);
939 assert!(cdl.contains("units"));
940 assert!(cdl.contains("source"));
941 }
942 #[test]
943 fn test_cdl_dump_int32_type_label() {
944 let mut root = ShdfGroup::new("ids");
945 root.datasets.push(Dataset {
946 name: "atom_id".to_string(),
947 dtype: DataType::Int32,
948 shape: vec![3],
949 data_f64: vec![1.0, 2.0, 3.0],
950 data_i32: Vec::new(),
951 attributes: vec![],
952 });
953 let cdl = cdl_dump(&root);
954 assert!(cdl.contains("int"), "CDL should label Int32 as 'int'");
955 }
956 #[test]
957 fn test_virtual_link_no_slice() {
958 let link = VirtualLink::new("/virtual/pos", "sim.shdf", "/atoms/positions");
959 let cdl = link.to_cdl();
960 assert!(cdl.contains("/virtual/pos"));
961 assert!(cdl.contains("sim.shdf"));
962 assert!(cdl.contains("(:)"), "no-slice should show '(:)'");
963 }
964 #[test]
965 fn test_virtual_link_with_slice() {
966 let link = VirtualLink::new("/v/vel", "traj.shdf", "/vel")
967 .with_slice(0, 100, 1)
968 .with_slice(0, 3, 1);
969 let cdl = link.to_cdl();
970 assert!(
971 cdl.contains("0:100:1"),
972 "should contain first dimension slice"
973 );
974 assert!(
975 cdl.contains("0:3:1"),
976 "should contain second dimension slice"
977 );
978 }
979 #[test]
980 fn test_virtual_link_clone() {
981 let link = VirtualLink::new("/a", "f.shdf", "/b");
982 let cloned = link.clone();
983 assert_eq!(cloned.virtual_path, link.virtual_path);
984 }
985 #[test]
986 fn test_dataset_stats_basic() {
987 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
988 let stats = DatasetStats::from_slice(&data).unwrap();
989 assert!((stats.min - 1.0).abs() < 1e-12);
990 assert!((stats.max - 5.0).abs() < 1e-12);
991 assert!((stats.mean - 3.0).abs() < 1e-12);
992 assert!((stats.range() - 4.0).abs() < 1e-12);
993 assert_eq!(stats.count, 5);
994 }
995 #[test]
996 fn test_dataset_stats_single_element() {
997 let stats = DatasetStats::from_slice(&[42.0]).unwrap();
998 assert!((stats.min - 42.0).abs() < 1e-12);
999 assert!((stats.max - 42.0).abs() < 1e-12);
1000 assert!((stats.mean - 42.0).abs() < 1e-12);
1001 assert!((stats.variance).abs() < 1e-12);
1002 assert!((stats.std_dev()).abs() < 1e-12);
1003 }
1004 #[test]
1005 fn test_dataset_stats_empty() {
1006 assert!(DatasetStats::from_slice(&[]).is_none());
1007 }
1008 #[test]
1009 fn test_dataset_stats_std_dev_constant() {
1010 let data = vec![5.0; 10];
1011 let stats = DatasetStats::from_slice(&data).unwrap();
1012 assert!(
1013 stats.std_dev().abs() < 1e-12,
1014 "constant data has zero std dev"
1015 );
1016 }
1017 #[test]
1018 fn test_dataset_stats_variance_known() {
1019 let data = vec![0.0, 2.0, 4.0];
1020 let stats = DatasetStats::from_slice(&data).unwrap();
1021 let expected_var = 8.0 / 3.0;
1022 assert!(
1023 (stats.variance - expected_var).abs() < 1e-10,
1024 "variance={}",
1025 stats.variance
1026 );
1027 }
1028}