Skip to main content

oxiphysics_io/
hpc_io.rs

1#![allow(clippy::needless_range_loop)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! HPC and scientific data I/O.
6//!
7//! Provides mock implementations for HDF5, parallel netCDF, ADIOS2,
8//! checkpoint management, distributed mesh I/O, performance logging,
9//! and scientific JSON with base64-encoded float arrays.
10
11#![allow(dead_code)]
12#![allow(clippy::too_many_arguments)]
13
14use std::collections::HashMap;
15
16// ---------------------------------------------------------------------------
17// Base64 encoding helper (no external crates)
18// ---------------------------------------------------------------------------
19
20const BASE64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
21
22/// Encode bytes to base64 string.
23pub fn base64_encode(data: &[u8]) -> String {
24    let mut result = String::new();
25    let mut i = 0;
26    while i + 2 < data.len() {
27        let b0 = data[i] as usize;
28        let b1 = data[i + 1] as usize;
29        let b2 = data[i + 2] as usize;
30        result.push(BASE64_CHARS[b0 >> 2] as char);
31        result.push(BASE64_CHARS[((b0 & 3) << 4) | (b1 >> 4)] as char);
32        result.push(BASE64_CHARS[((b1 & 0xf) << 2) | (b2 >> 6)] as char);
33        result.push(BASE64_CHARS[b2 & 0x3f] as char);
34        i += 3;
35    }
36    let remaining = data.len() - i;
37    if remaining == 1 {
38        let b0 = data[i] as usize;
39        result.push(BASE64_CHARS[b0 >> 2] as char);
40        result.push(BASE64_CHARS[(b0 & 3) << 4] as char);
41        result.push('=');
42        result.push('=');
43    } else if remaining == 2 {
44        let b0 = data[i] as usize;
45        let b1 = data[i + 1] as usize;
46        result.push(BASE64_CHARS[b0 >> 2] as char);
47        result.push(BASE64_CHARS[((b0 & 3) << 4) | (b1 >> 4)] as char);
48        result.push(BASE64_CHARS[(b1 & 0xf) << 2] as char);
49        result.push('=');
50    }
51    result
52}
53
54/// Encode a slice of f64 values to base64.
55pub fn f64_slice_to_base64(data: &[f64]) -> String {
56    let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_le_bytes().to_vec()).collect();
57    base64_encode(&bytes)
58}
59
60// ---------------------------------------------------------------------------
61// Hdf5Dataset
62// ---------------------------------------------------------------------------
63
64/// Supported HDF5-like data types.
65#[derive(Clone, Debug, PartialEq)]
66pub enum H5Dtype {
67    /// 32-bit float.
68    Float32,
69    /// 64-bit float.
70    Float64,
71    /// 32-bit signed integer.
72    Int32,
73    /// 64-bit signed integer.
74    Int64,
75    /// Unsigned 8-bit integer.
76    Uint8,
77}
78
79impl H5Dtype {
80    /// Size in bytes.
81    pub fn byte_size(&self) -> usize {
82        match self {
83            H5Dtype::Float32 => 4,
84            H5Dtype::Float64 => 8,
85            H5Dtype::Int32 => 4,
86            H5Dtype::Int64 => 8,
87            H5Dtype::Uint8 => 1,
88        }
89    }
90}
91
92/// Mock HDF5 dataset.
93#[derive(Clone, Debug)]
94pub struct Hdf5Dataset {
95    /// Dataset name.
96    pub name: String,
97    /// Dimensions.
98    pub dims: Vec<usize>,
99    /// Data type.
100    pub dtype: H5Dtype,
101    /// Chunk size (for chunked storage).
102    pub chunk_size: Option<Vec<usize>>,
103    /// Compression level (0 = none, 9 = max).
104    pub compression_level: u8,
105    /// Dataset attributes.
106    pub attributes: HashMap<String, String>,
107    /// Raw data storage (f64 for simplicity).
108    pub data: Vec<f64>,
109}
110
111impl Hdf5Dataset {
112    /// Create a new dataset.
113    pub fn new(name: &str, dims: Vec<usize>, dtype: H5Dtype) -> Self {
114        let n: usize = dims.iter().product();
115        Self {
116            name: name.to_string(),
117            dims,
118            dtype,
119            chunk_size: None,
120            compression_level: 0,
121            attributes: HashMap::new(),
122            data: vec![0.0; n],
123        }
124    }
125
126    /// Set chunk size.
127    pub fn set_chunk_size(&mut self, chunk: Vec<usize>) {
128        self.chunk_size = Some(chunk);
129    }
130
131    /// Set compression level.
132    pub fn set_compression(&mut self, level: u8) {
133        self.compression_level = level.min(9);
134    }
135
136    /// Set an attribute.
137    pub fn set_attr(&mut self, key: &str, value: &str) {
138        self.attributes.insert(key.to_string(), value.to_string());
139    }
140
141    /// Get an attribute.
142    pub fn get_attr(&self, key: &str) -> Option<&str> {
143        self.attributes.get(key).map(|s| s.as_str())
144    }
145
146    /// Total number of elements.
147    pub fn n_elements(&self) -> usize {
148        self.dims.iter().product()
149    }
150
151    /// Estimated memory footprint (bytes).
152    pub fn memory_bytes(&self) -> usize {
153        self.n_elements() * self.dtype.byte_size()
154    }
155
156    /// Write data slice.
157    pub fn write_slice(&mut self, offset: usize, values: &[f64]) {
158        let end = (offset + values.len()).min(self.data.len());
159        for (i, &v) in values.iter().enumerate() {
160            if offset + i < end {
161                self.data[offset + i] = v;
162            }
163        }
164    }
165
166    /// Read data slice.
167    pub fn read_slice(&self, offset: usize, length: usize) -> Vec<f64> {
168        let end = (offset + length).min(self.data.len());
169        self.data[offset..end].to_vec()
170    }
171}
172
173// ---------------------------------------------------------------------------
174// Hdf5Group
175// ---------------------------------------------------------------------------
176
177/// Mock HDF5 group.
178#[derive(Clone, Debug)]
179pub struct Hdf5Group {
180    /// Group name.
181    pub name: String,
182    /// Child group names.
183    pub children: Vec<String>,
184    /// Group attributes.
185    pub attributes: HashMap<String, String>,
186    /// Datasets in this group.
187    pub datasets: HashMap<String, Hdf5Dataset>,
188}
189
190impl Hdf5Group {
191    /// Create a new group.
192    pub fn new(name: &str) -> Self {
193        Self {
194            name: name.to_string(),
195            children: Vec::new(),
196            attributes: HashMap::new(),
197            datasets: HashMap::new(),
198        }
199    }
200
201    /// Add a child group name.
202    pub fn add_child(&mut self, child_name: &str) {
203        self.children.push(child_name.to_string());
204    }
205
206    /// Set a group attribute.
207    pub fn set_attr(&mut self, key: &str, value: &str) {
208        self.attributes.insert(key.to_string(), value.to_string());
209    }
210
211    /// Create a dataset in this group.
212    pub fn create_dataset(&mut self, name: &str, dims: Vec<usize>, dtype: H5Dtype) {
213        let ds = Hdf5Dataset::new(name, dims, dtype);
214        self.datasets.insert(name.to_string(), ds);
215    }
216
217    /// Get a dataset.
218    pub fn get_dataset(&self, name: &str) -> Option<&Hdf5Dataset> {
219        self.datasets.get(name)
220    }
221
222    /// Get mutable dataset.
223    pub fn get_dataset_mut(&mut self, name: &str) -> Option<&mut Hdf5Dataset> {
224        self.datasets.get_mut(name)
225    }
226
227    /// List dataset names.
228    pub fn dataset_names(&self) -> Vec<&str> {
229        self.datasets.keys().map(|s| s.as_str()).collect()
230    }
231}
232
233// ---------------------------------------------------------------------------
234// Hdf5File
235// ---------------------------------------------------------------------------
236
237/// Mock HDF5 file.
238#[derive(Clone, Debug)]
239pub struct Hdf5File {
240    /// File path.
241    pub path: String,
242    /// Root group.
243    pub root: Hdf5Group,
244    /// Whether file is open.
245    pub is_open: bool,
246    /// Read-only flag.
247    pub read_only: bool,
248}
249
250impl Hdf5File {
251    /// Open an existing file.
252    pub fn open(path: &str) -> Self {
253        Self {
254            path: path.to_string(),
255            root: Hdf5Group::new("/"),
256            is_open: true,
257            read_only: true,
258        }
259    }
260
261    /// Create a new file.
262    pub fn create(path: &str) -> Self {
263        Self {
264            path: path.to_string(),
265            root: Hdf5Group::new("/"),
266            is_open: true,
267            read_only: false,
268        }
269    }
270
271    /// Close the file.
272    pub fn close(&mut self) {
273        self.is_open = false;
274    }
275
276    /// Flush (no-op in mock).
277    pub fn flush(&self) {}
278
279    /// Create a group at the root.
280    pub fn create_group(&mut self, name: &str) -> &mut Hdf5Group {
281        self.root.add_child(name);
282        // Insert if not present
283        if !self.root.children.contains(&name.to_string()) {
284            self.root.children.push(name.to_string());
285        }
286        &mut self.root
287    }
288
289    /// Get root group.
290    pub fn root_group(&self) -> &Hdf5Group {
291        &self.root
292    }
293
294    /// Get mutable root group.
295    pub fn root_group_mut(&mut self) -> &mut Hdf5Group {
296        &mut self.root
297    }
298
299    /// Create a dataset directly at root.
300    pub fn create_dataset(&mut self, name: &str, dims: Vec<usize>, dtype: H5Dtype) {
301        self.root.create_dataset(name, dims, dtype);
302    }
303}
304
305// ---------------------------------------------------------------------------
306// ParallelNetcdf
307// ---------------------------------------------------------------------------
308
309/// Parallel netCDF stub for distributed array I/O.
310#[derive(Clone, Debug)]
311pub struct ParallelNetcdf {
312    /// File path.
313    pub path: String,
314    /// Global dimensions \[nx, ny, nz\].
315    pub global_dims: Vec<usize>,
316    /// Number of MPI ranks (simulated).
317    pub n_ranks: usize,
318    /// Current rank.
319    pub rank: usize,
320    /// Variables stored (name -> local data).
321    pub variables: HashMap<String, Vec<f64>>,
322    /// Variable attributes.
323    pub var_attrs: HashMap<String, HashMap<String, String>>,
324}
325
326impl ParallelNetcdf {
327    /// Create a new parallel netCDF file.
328    pub fn new(path: &str, global_dims: Vec<usize>, n_ranks: usize, rank: usize) -> Self {
329        Self {
330            path: path.to_string(),
331            global_dims,
332            n_ranks,
333            rank,
334            variables: HashMap::new(),
335            var_attrs: HashMap::new(),
336        }
337    }
338
339    /// Define a variable.
340    pub fn def_var(&mut self, name: &str, _dims: &[&str]) {
341        self.variables.insert(name.to_string(), Vec::new());
342        self.var_attrs.insert(name.to_string(), HashMap::new());
343    }
344
345    /// Write distributed array (local portion).
346    pub fn put_var(&mut self, name: &str, data: Vec<f64>) {
347        self.variables.insert(name.to_string(), data);
348    }
349
350    /// Read variable.
351    pub fn get_var(&self, name: &str) -> Option<&[f64]> {
352        self.variables.get(name).map(|v| v.as_slice())
353    }
354
355    /// Set variable attribute.
356    pub fn set_var_attr(&mut self, var: &str, key: &str, value: &str) {
357        if let Some(attrs) = self.var_attrs.get_mut(var) {
358            attrs.insert(key.to_string(), value.to_string());
359        }
360    }
361
362    /// Total global size.
363    pub fn global_size(&self) -> usize {
364        self.global_dims.iter().product()
365    }
366
367    /// Local size for this rank (simple block decomposition along first dim).
368    pub fn local_size(&self) -> usize {
369        if self.global_dims.is_empty() {
370            return 0;
371        }
372        let n_global_first = self.global_dims[0];
373        let local_first = n_global_first.div_ceil(self.n_ranks);
374        let rest: usize = self.global_dims[1..].iter().product::<usize>().max(1);
375        local_first.min(n_global_first) * rest
376    }
377}
378
379// ---------------------------------------------------------------------------
380// AdiosWriter
381// ---------------------------------------------------------------------------
382
383/// ADIOS2-style streaming I/O writer.
384#[derive(Clone, Debug)]
385pub struct AdiosWriter {
386    /// Stream name.
387    pub stream_name: String,
388    /// Open flag.
389    pub is_open: bool,
390    /// Variables staged for write (name -> data).
391    pub staged_vars: HashMap<String, Vec<f64>>,
392    /// Variable metadata (shape, type).
393    pub var_meta: HashMap<String, (Vec<usize>, String)>,
394    /// Steps written.
395    pub steps_written: usize,
396}
397
398impl AdiosWriter {
399    /// Open an ADIOS2 output stream.
400    pub fn open(stream_name: &str) -> Self {
401        Self {
402            stream_name: stream_name.to_string(),
403            is_open: true,
404            staged_vars: HashMap::new(),
405            var_meta: HashMap::new(),
406            steps_written: 0,
407        }
408    }
409
410    /// Define a variable with shape.
411    pub fn define_variable(&mut self, name: &str, shape: Vec<usize>, dtype: &str) {
412        self.var_meta
413            .insert(name.to_string(), (shape, dtype.to_string()));
414    }
415
416    /// Put (stage) a variable for writing.
417    pub fn put_variable(&mut self, name: &str, data: Vec<f64>) {
418        self.staged_vars.insert(name.to_string(), data);
419    }
420
421    /// Perform puts: flush staged variables as a step.
422    pub fn perform_puts(&mut self) {
423        // In real ADIOS2, this triggers I/O; here we just increment step
424        self.steps_written += 1;
425        self.staged_vars.clear();
426    }
427
428    /// Close the stream.
429    pub fn close(&mut self) {
430        self.is_open = false;
431    }
432
433    /// Check if variable is defined.
434    pub fn has_variable(&self, name: &str) -> bool {
435        self.var_meta.contains_key(name)
436    }
437
438    /// Get variable shape.
439    pub fn variable_shape(&self, name: &str) -> Option<&[usize]> {
440        self.var_meta.get(name).map(|(s, _)| s.as_slice())
441    }
442}
443
444// ---------------------------------------------------------------------------
445// CheckpointManager
446// ---------------------------------------------------------------------------
447
448/// A single checkpoint entry.
449#[derive(Clone, Debug)]
450pub struct CheckpointEntry {
451    /// Checkpoint index.
452    pub index: usize,
453    /// Simulation step.
454    pub step: u64,
455    /// File path.
456    pub path: String,
457    /// Timestamp (simulated, seconds from epoch).
458    pub timestamp: f64,
459}
460
461/// Versioned checkpoint manager.
462#[derive(Clone, Debug)]
463pub struct CheckpointManager {
464    /// Base directory for checkpoints.
465    pub base_dir: String,
466    /// Maximum number of checkpoints to keep.
467    pub keep_last_n: usize,
468    /// List of checkpoint entries (oldest first).
469    pub entries: Vec<CheckpointEntry>,
470    /// Next checkpoint index.
471    pub next_index: usize,
472}
473
474impl CheckpointManager {
475    /// Create a new checkpoint manager.
476    pub fn new(base_dir: &str, keep_last_n: usize) -> Self {
477        Self {
478            base_dir: base_dir.to_string(),
479            keep_last_n,
480            entries: Vec::new(),
481            next_index: 0,
482        }
483    }
484
485    /// Register a new checkpoint.
486    pub fn register(&mut self, step: u64, timestamp: f64) -> String {
487        let path = format!("{}/checkpoint_{:06}.bin", self.base_dir, self.next_index);
488        let entry = CheckpointEntry {
489            index: self.next_index,
490            step,
491            path: path.clone(),
492            timestamp,
493        };
494        self.entries.push(entry);
495        self.next_index += 1;
496
497        // Remove old entries beyond keep_last_n
498        while self.entries.len() > self.keep_last_n {
499            self.entries.remove(0);
500        }
501
502        path
503    }
504
505    /// Get the latest checkpoint.
506    pub fn latest(&self) -> Option<&CheckpointEntry> {
507        self.entries.last()
508    }
509
510    /// Restore by index (returns path if found).
511    pub fn restore_by_index(&self, index: usize) -> Option<&CheckpointEntry> {
512        self.entries.iter().find(|e| e.index == index)
513    }
514
515    /// Number of stored checkpoints.
516    pub fn n_checkpoints(&self) -> usize {
517        self.entries.len()
518    }
519}
520
521// ---------------------------------------------------------------------------
522// RestartFile
523// ---------------------------------------------------------------------------
524
525/// Binary restart file for simulation state.
526#[derive(Clone, Debug)]
527pub struct RestartFile {
528    /// File path.
529    pub path: String,
530    /// State vector.
531    pub state: Vec<f64>,
532    /// Metadata: simulation step.
533    pub step: u64,
534    /// Metadata: simulation time.
535    pub time: f64,
536    /// Metadata: arbitrary key-value pairs.
537    pub metadata: HashMap<String, f64>,
538}
539
540impl RestartFile {
541    /// Create a new restart file.
542    pub fn new(path: &str) -> Self {
543        Self {
544            path: path.to_string(),
545            state: Vec::new(),
546            step: 0,
547            time: 0.0,
548            metadata: HashMap::new(),
549        }
550    }
551
552    /// Write state to a byte buffer (mock serialization).
553    pub fn write_to_buffer(&self) -> Vec<u8> {
554        let mut buf = Vec::new();
555        // Magic number
556        buf.extend_from_slice(b"OXIRS001");
557        // Step (u64)
558        buf.extend_from_slice(&self.step.to_le_bytes());
559        // Time (f64)
560        buf.extend_from_slice(&self.time.to_le_bytes());
561        // State length (u64)
562        let n = self.state.len() as u64;
563        buf.extend_from_slice(&n.to_le_bytes());
564        // State data
565        for &v in &self.state {
566            buf.extend_from_slice(&v.to_le_bytes());
567        }
568        buf
569    }
570
571    /// Read state from byte buffer (mock deserialization).
572    pub fn read_from_buffer(path: &str, buf: &[u8]) -> Option<Self> {
573        if buf.len() < 24 || &buf[..8] != b"OXIRS001" {
574            return None;
575        }
576        let step = u64::from_le_bytes(buf[8..16].try_into().ok()?);
577        let time = f64::from_le_bytes(buf[16..24].try_into().ok()?);
578        let n = u64::from_le_bytes(buf[24..32].try_into().ok()?) as usize;
579        let mut state = Vec::with_capacity(n);
580        for i in 0..n {
581            let off = 32 + i * 8;
582            if off + 8 > buf.len() {
583                break;
584            }
585            let v = f64::from_le_bytes(buf[off..off + 8].try_into().ok()?);
586            state.push(v);
587        }
588        Some(Self {
589            path: path.to_string(),
590            state,
591            step,
592            time,
593            metadata: HashMap::new(),
594        })
595    }
596
597    /// Set a metadata value.
598    pub fn set_meta(&mut self, key: &str, value: f64) {
599        self.metadata.insert(key.to_string(), value);
600    }
601}
602
603// ---------------------------------------------------------------------------
604// DistributedMeshIO
605// ---------------------------------------------------------------------------
606
607/// I/O for a distributed mesh partition.
608#[derive(Clone, Debug)]
609pub struct DistributedMeshIO {
610    /// Rank index.
611    pub rank: usize,
612    /// Total number of ranks.
613    pub n_ranks: usize,
614    /// Local node positions.
615    pub local_nodes: Vec<[f64; 3]>,
616    /// Ghost node positions (copies from neighbor ranks).
617    pub ghost_nodes: Vec<[f64; 3]>,
618    /// Global IDs of local nodes.
619    pub local_global_ids: Vec<u64>,
620    /// Global IDs of ghost nodes.
621    pub ghost_global_ids: Vec<u64>,
622    /// Connectivity (local element node indices).
623    pub elements: Vec<Vec<usize>>,
624}
625
626impl DistributedMeshIO {
627    /// Create a new distributed mesh partition.
628    pub fn new(rank: usize, n_ranks: usize) -> Self {
629        Self {
630            rank,
631            n_ranks,
632            local_nodes: Vec::new(),
633            ghost_nodes: Vec::new(),
634            local_global_ids: Vec::new(),
635            ghost_global_ids: Vec::new(),
636            elements: Vec::new(),
637        }
638    }
639
640    /// Add a local node.
641    pub fn add_local_node(&mut self, pos: [f64; 3], global_id: u64) {
642        self.local_nodes.push(pos);
643        self.local_global_ids.push(global_id);
644    }
645
646    /// Add a ghost node.
647    pub fn add_ghost_node(&mut self, pos: [f64; 3], global_id: u64) {
648        self.ghost_nodes.push(pos);
649        self.ghost_global_ids.push(global_id);
650    }
651
652    /// Add an element (connectivity).
653    pub fn add_element(&mut self, nodes: Vec<usize>) {
654        self.elements.push(nodes);
655    }
656
657    /// Total local + ghost nodes.
658    pub fn total_nodes(&self) -> usize {
659        self.local_nodes.len() + self.ghost_nodes.len()
660    }
661
662    /// Serialize local mesh to bytes (mock).
663    pub fn serialize(&self) -> Vec<u8> {
664        let mut buf = Vec::new();
665        buf.extend_from_slice(b"OXIDMESH");
666        let n_local = self.local_nodes.len() as u64;
667        buf.extend_from_slice(&n_local.to_le_bytes());
668        for node in &self.local_nodes {
669            for &c in node {
670                buf.extend_from_slice(&c.to_le_bytes());
671            }
672        }
673        buf
674    }
675
676    /// Build a simple block-decomposed partition from a regular grid.
677    pub fn partition_regular_grid(
678        n_nodes_total: usize,
679        positions: &[[f64; 3]],
680        rank: usize,
681        n_ranks: usize,
682    ) -> Self {
683        let block = n_nodes_total.div_ceil(n_ranks);
684        let start = rank * block;
685        let end = (start + block).min(n_nodes_total);
686        let mut mesh = Self::new(rank, n_ranks);
687        for i in start..end {
688            mesh.add_local_node(positions[i], i as u64);
689        }
690        mesh
691    }
692}
693
694// ---------------------------------------------------------------------------
695// PerformanceLog
696// ---------------------------------------------------------------------------
697
698/// A single performance log entry.
699#[derive(Clone, Debug)]
700pub struct PerfEntry {
701    /// Crate name.
702    pub crate_name: String,
703    /// Function name.
704    pub function: String,
705    /// Elapsed time in milliseconds.
706    pub time_ms: f64,
707    /// Memory usage in megabytes.
708    pub memory_mb: f64,
709    /// Number of threads.
710    pub n_threads: usize,
711    /// Additional notes.
712    pub notes: String,
713}
714
715impl PerfEntry {
716    /// Create a new performance entry.
717    pub fn new(
718        crate_name: &str,
719        function: &str,
720        time_ms: f64,
721        memory_mb: f64,
722        n_threads: usize,
723    ) -> Self {
724        Self {
725            crate_name: crate_name.to_string(),
726            function: function.to_string(),
727            time_ms,
728            memory_mb,
729            n_threads,
730            notes: String::new(),
731        }
732    }
733}
734
735/// Structured performance log.
736#[derive(Clone, Debug)]
737pub struct PerformanceLog {
738    /// All recorded entries.
739    pub entries: Vec<PerfEntry>,
740    /// Run identifier.
741    pub run_id: String,
742}
743
744impl PerformanceLog {
745    /// Create a new performance log.
746    pub fn new(run_id: &str) -> Self {
747        Self {
748            entries: Vec::new(),
749            run_id: run_id.to_string(),
750        }
751    }
752
753    /// Record an entry.
754    pub fn record(&mut self, entry: PerfEntry) {
755        self.entries.push(entry);
756    }
757
758    /// Total elapsed time across all entries.
759    pub fn total_time_ms(&self) -> f64 {
760        self.entries.iter().map(|e| e.time_ms).sum()
761    }
762
763    /// Mean time per entry.
764    pub fn mean_time_ms(&self) -> f64 {
765        if self.entries.is_empty() {
766            return 0.0;
767        }
768        self.total_time_ms() / self.entries.len() as f64
769    }
770
771    /// Maximum memory usage.
772    pub fn peak_memory_mb(&self) -> f64 {
773        self.entries
774            .iter()
775            .map(|e| e.memory_mb)
776            .fold(0.0f64, f64::max)
777    }
778
779    /// Filter entries by crate name.
780    pub fn filter_by_crate(&self, crate_name: &str) -> Vec<&PerfEntry> {
781        self.entries
782            .iter()
783            .filter(|e| e.crate_name == crate_name)
784            .collect()
785    }
786
787    /// Serialize to CSV-like string.
788    pub fn to_csv(&self) -> String {
789        let mut s = "crate,function,time_ms,memory_mb,n_threads\n".to_string();
790        for e in &self.entries {
791            s.push_str(&format!(
792                "{},{},{:.3},{:.3},{}\n",
793                e.crate_name, e.function, e.time_ms, e.memory_mb, e.n_threads
794            ));
795        }
796        s
797    }
798
799    /// Serialize to JSON string.
800    pub fn to_json(&self) -> String {
801        let entries_json: Vec<String> = self
802            .entries
803            .iter()
804            .map(|e| {
805                format!(
806                    r#"{{"crate":"{}","function":"{}","time_ms":{:.3},"memory_mb":{:.3},"n_threads":{}}}"#,
807                    e.crate_name, e.function, e.time_ms, e.memory_mb, e.n_threads
808                )
809            })
810            .collect();
811        format!(
812            r#"{{"run_id":"{}","entries":[{}]}}"#,
813            self.run_id,
814            entries_json.join(",")
815        )
816    }
817}
818
819// ---------------------------------------------------------------------------
820// ScientificJson
821// ---------------------------------------------------------------------------
822
823/// Large array JSON using base64-encoded float arrays to avoid huge string arrays.
824#[derive(Clone, Debug)]
825pub struct ScientificJson {
826    /// Document name.
827    pub name: String,
828    /// Metadata fields (string).
829    pub metadata: HashMap<String, String>,
830    /// Named float arrays (stored as base64 in JSON).
831    pub arrays: HashMap<String, Vec<f64>>,
832    /// Named scalar values.
833    pub scalars: HashMap<String, f64>,
834}
835
836impl ScientificJson {
837    /// Create a new scientific JSON document.
838    pub fn new(name: &str) -> Self {
839        Self {
840            name: name.to_string(),
841            metadata: HashMap::new(),
842            arrays: HashMap::new(),
843            scalars: HashMap::new(),
844        }
845    }
846
847    /// Set metadata.
848    pub fn set_meta(&mut self, key: &str, value: &str) {
849        self.metadata.insert(key.to_string(), value.to_string());
850    }
851
852    /// Set scalar.
853    pub fn set_scalar(&mut self, key: &str, value: f64) {
854        self.scalars.insert(key.to_string(), value);
855    }
856
857    /// Add a named float array.
858    pub fn add_array(&mut self, name: &str, data: Vec<f64>) {
859        self.arrays.insert(name.to_string(), data);
860    }
861
862    /// Get a named float array.
863    pub fn get_array(&self, name: &str) -> Option<&[f64]> {
864        self.arrays.get(name).map(|v| v.as_slice())
865    }
866
867    /// Serialize to JSON with base64-encoded arrays.
868    pub fn to_json(&self) -> String {
869        let mut parts: Vec<String> = Vec::new();
870
871        // Metadata
872        let meta_parts: Vec<String> = self
873            .metadata
874            .iter()
875            .map(|(k, v)| format!(r#""{}":"{}""#, k, v))
876            .collect();
877        if !meta_parts.is_empty() {
878            parts.push(format!(r#""metadata":{{{}}}"#, meta_parts.join(",")));
879        }
880
881        // Scalars
882        let scalar_parts: Vec<String> = self
883            .scalars
884            .iter()
885            .map(|(k, v)| format!(r#""{}":{}"#, k, v))
886            .collect();
887        if !scalar_parts.is_empty() {
888            parts.push(format!(r#""scalars":{{{}}}"#, scalar_parts.join(",")));
889        }
890
891        // Arrays: base64-encoded
892        let arr_parts: Vec<String> = self
893            .arrays
894            .iter()
895            .map(|(k, v)| {
896                let b64 = f64_slice_to_base64(v);
897                format!(
898                    r#""{}":{{"dtype":"f64","shape":[{}],"base64":"{}"}}"#,
899                    k,
900                    v.len(),
901                    b64
902                )
903            })
904            .collect();
905        if !arr_parts.is_empty() {
906            parts.push(format!(r#""arrays":{{{}}}"#, arr_parts.join(",")));
907        }
908
909        format!(r#"{{"name":"{}",{}}}"#, self.name, parts.join(","))
910    }
911
912    /// Parse a simple key-value from JSON string (basic mock parser).
913    pub fn parse_scalar(json: &str, key: &str) -> Option<f64> {
914        let search = format!("\"{}\":", key);
915        let start = json.find(&search)? + search.len();
916        let rest = &json[start..];
917        let end = rest.find([',', '}']).unwrap_or(rest.len());
918        rest[..end].trim().parse().ok()
919    }
920}
921
922// ---------------------------------------------------------------------------
923// Tests
924// ---------------------------------------------------------------------------
925
926#[cfg(test)]
927mod tests {
928    use super::*;
929
930    #[test]
931    fn test_base64_encode_empty() {
932        assert_eq!(base64_encode(&[]), "");
933    }
934
935    #[test]
936    fn test_base64_encode_hello() {
937        let encoded = base64_encode(b"Hello");
938        assert_eq!(encoded, "SGVsbG8=");
939    }
940
941    #[test]
942    fn test_f64_slice_to_base64_roundtrip_length() {
943        let data = vec![1.0, 2.0, 3.0];
944        let b64 = f64_slice_to_base64(&data);
945        // 3 f64 * 8 bytes = 24 bytes -> base64 len = ceil(24/3)*4 = 32
946        assert_eq!(b64.len(), 32);
947    }
948
949    #[test]
950    fn test_h5dtype_byte_size() {
951        assert_eq!(H5Dtype::Float32.byte_size(), 4);
952        assert_eq!(H5Dtype::Float64.byte_size(), 8);
953        assert_eq!(H5Dtype::Uint8.byte_size(), 1);
954    }
955
956    #[test]
957    fn test_hdf5_dataset_new() {
958        let ds = Hdf5Dataset::new("data", vec![10, 10], H5Dtype::Float64);
959        assert_eq!(ds.n_elements(), 100);
960        assert_eq!(ds.data.len(), 100);
961    }
962
963    #[test]
964    fn test_hdf5_dataset_memory_bytes() {
965        let ds = Hdf5Dataset::new("data", vec![10], H5Dtype::Float64);
966        assert_eq!(ds.memory_bytes(), 80);
967    }
968
969    #[test]
970    fn test_hdf5_dataset_write_read_slice() {
971        let mut ds = Hdf5Dataset::new("data", vec![10], H5Dtype::Float64);
972        ds.write_slice(2, &[1.0, 2.0, 3.0]);
973        let r = ds.read_slice(2, 3);
974        assert_eq!(r, vec![1.0, 2.0, 3.0]);
975    }
976
977    #[test]
978    fn test_hdf5_dataset_set_attr() {
979        let mut ds = Hdf5Dataset::new("data", vec![5], H5Dtype::Float32);
980        ds.set_attr("units", "m/s");
981        assert_eq!(ds.get_attr("units"), Some("m/s"));
982    }
983
984    #[test]
985    fn test_hdf5_dataset_compression() {
986        let mut ds = Hdf5Dataset::new("data", vec![5], H5Dtype::Float32);
987        ds.set_compression(15);
988        assert_eq!(ds.compression_level, 9); // capped
989    }
990
991    #[test]
992    fn test_hdf5_group_new() {
993        let g = Hdf5Group::new("sim");
994        assert_eq!(g.name, "sim");
995        assert!(g.datasets.is_empty());
996    }
997
998    #[test]
999    fn test_hdf5_group_create_dataset() {
1000        let mut g = Hdf5Group::new("results");
1001        g.create_dataset("velocity", vec![100, 3], H5Dtype::Float64);
1002        assert!(g.get_dataset("velocity").is_some());
1003    }
1004
1005    #[test]
1006    fn test_hdf5_group_dataset_names() {
1007        let mut g = Hdf5Group::new("results");
1008        g.create_dataset("pressure", vec![100], H5Dtype::Float64);
1009        g.create_dataset("density", vec![100], H5Dtype::Float64);
1010        let names = g.dataset_names();
1011        assert_eq!(names.len(), 2);
1012    }
1013
1014    #[test]
1015    fn test_hdf5_file_create() {
1016        let f = Hdf5File::create("test.h5");
1017        assert!(f.is_open);
1018        assert!(!f.read_only);
1019    }
1020
1021    #[test]
1022    fn test_hdf5_file_open() {
1023        let f = Hdf5File::open("test.h5");
1024        assert!(f.is_open);
1025        assert!(f.read_only);
1026    }
1027
1028    #[test]
1029    fn test_hdf5_file_create_dataset() {
1030        let mut f = Hdf5File::create("test.h5");
1031        f.create_dataset("temperatures", vec![50], H5Dtype::Float32);
1032        assert!(f.root.get_dataset("temperatures").is_some());
1033    }
1034
1035    #[test]
1036    fn test_hdf5_file_close() {
1037        let mut f = Hdf5File::create("test.h5");
1038        f.close();
1039        assert!(!f.is_open);
1040    }
1041
1042    #[test]
1043    fn test_parallel_netcdf_new() {
1044        let nc = ParallelNetcdf::new("out.nc", vec![100, 100, 100], 4, 0);
1045        assert_eq!(nc.global_size(), 1_000_000);
1046    }
1047
1048    #[test]
1049    fn test_parallel_netcdf_put_get_var() {
1050        let mut nc = ParallelNetcdf::new("out.nc", vec![10], 1, 0);
1051        nc.def_var("temp", &["x"]);
1052        nc.put_var("temp", vec![1.0, 2.0, 3.0]);
1053        let v = nc.get_var("temp").unwrap();
1054        assert_eq!(v, &[1.0, 2.0, 3.0]);
1055    }
1056
1057    #[test]
1058    fn test_parallel_netcdf_local_size() {
1059        let nc = ParallelNetcdf::new("out.nc", vec![100, 10], 4, 0);
1060        let ls = nc.local_size();
1061        assert!(ls > 0);
1062    }
1063
1064    #[test]
1065    fn test_adios_writer_open() {
1066        let w = AdiosWriter::open("sim.bp");
1067        assert!(w.is_open);
1068        assert_eq!(w.steps_written, 0);
1069    }
1070
1071    #[test]
1072    fn test_adios_writer_define_and_put() {
1073        let mut w = AdiosWriter::open("sim.bp");
1074        w.define_variable("velocity", vec![100, 3], "f64");
1075        assert!(w.has_variable("velocity"));
1076        w.put_variable("velocity", vec![1.0; 300]);
1077        assert!(w.staged_vars.contains_key("velocity"));
1078    }
1079
1080    #[test]
1081    fn test_adios_writer_perform_puts() {
1082        let mut w = AdiosWriter::open("sim.bp");
1083        w.put_variable("p", vec![1.0]);
1084        w.perform_puts();
1085        assert_eq!(w.steps_written, 1);
1086        assert!(w.staged_vars.is_empty());
1087    }
1088
1089    #[test]
1090    fn test_adios_writer_close() {
1091        let mut w = AdiosWriter::open("sim.bp");
1092        w.close();
1093        assert!(!w.is_open);
1094    }
1095
1096    #[test]
1097    fn test_checkpoint_manager_register() {
1098        let mut cm = CheckpointManager::new("/tmp/ckpt", 3);
1099        let path = cm.register(100, 1.0);
1100        assert!(path.contains("checkpoint_000000"));
1101        assert_eq!(cm.n_checkpoints(), 1);
1102    }
1103
1104    #[test]
1105    fn test_checkpoint_manager_keep_last_n() {
1106        let mut cm = CheckpointManager::new("/tmp/ckpt", 2);
1107        cm.register(0, 0.0);
1108        cm.register(1, 1.0);
1109        cm.register(2, 2.0);
1110        assert_eq!(cm.n_checkpoints(), 2);
1111    }
1112
1113    #[test]
1114    fn test_checkpoint_manager_latest() {
1115        let mut cm = CheckpointManager::new("/tmp/ckpt", 5);
1116        cm.register(10, 1.0);
1117        cm.register(20, 2.0);
1118        assert_eq!(cm.latest().unwrap().step, 20);
1119    }
1120
1121    #[test]
1122    fn test_checkpoint_manager_restore_by_index() {
1123        let mut cm = CheckpointManager::new("/tmp/ckpt", 5);
1124        cm.register(10, 1.0);
1125        let e = cm.restore_by_index(0);
1126        assert!(e.is_some());
1127        assert_eq!(e.unwrap().step, 10);
1128    }
1129
1130    #[test]
1131    fn test_restart_file_write_read() {
1132        let mut rf = RestartFile::new("restart.bin");
1133        rf.state = vec![1.0, 2.0, 3.0];
1134        rf.step = 500;
1135        rf.time = 0.5;
1136        let buf = rf.write_to_buffer();
1137        let rf2 = RestartFile::read_from_buffer("restart.bin", &buf).unwrap();
1138        assert_eq!(rf2.step, 500);
1139        assert!((rf2.time - 0.5).abs() < 1e-10);
1140        assert_eq!(rf2.state, vec![1.0, 2.0, 3.0]);
1141    }
1142
1143    #[test]
1144    fn test_restart_file_invalid_magic() {
1145        let buf = vec![0u8; 64];
1146        let rf = RestartFile::read_from_buffer("x.bin", &buf);
1147        assert!(rf.is_none());
1148    }
1149
1150    #[test]
1151    fn test_distributed_mesh_io_new() {
1152        let m = DistributedMeshIO::new(0, 4);
1153        assert_eq!(m.rank, 0);
1154        assert_eq!(m.total_nodes(), 0);
1155    }
1156
1157    #[test]
1158    fn test_distributed_mesh_io_add_nodes() {
1159        let mut m = DistributedMeshIO::new(0, 4);
1160        m.add_local_node([0.0, 0.0, 0.0], 0);
1161        m.add_ghost_node([1.0, 0.0, 0.0], 100);
1162        assert_eq!(m.total_nodes(), 2);
1163        assert_eq!(m.local_nodes.len(), 1);
1164    }
1165
1166    #[test]
1167    fn test_distributed_mesh_io_serialize() {
1168        let mut m = DistributedMeshIO::new(0, 1);
1169        m.add_local_node([1.0, 2.0, 3.0], 0);
1170        let buf = m.serialize();
1171        assert!(buf.starts_with(b"OXIDMESH"));
1172    }
1173
1174    #[test]
1175    fn test_distributed_mesh_io_partition() {
1176        let positions: Vec<[f64; 3]> = (0..10).map(|i| [i as f64, 0.0, 0.0]).collect();
1177        let m = DistributedMeshIO::partition_regular_grid(10, &positions, 0, 2);
1178        assert!(m.local_nodes.len() >= 4);
1179    }
1180
1181    #[test]
1182    fn test_performance_log_record() {
1183        let mut log = PerformanceLog::new("run001");
1184        log.record(PerfEntry::new("lbm", "stream", 10.0, 100.0, 4));
1185        assert_eq!(log.entries.len(), 1);
1186    }
1187
1188    #[test]
1189    fn test_performance_log_total_time() {
1190        let mut log = PerformanceLog::new("run001");
1191        log.record(PerfEntry::new("lbm", "collide", 5.0, 50.0, 4));
1192        log.record(PerfEntry::new("lbm", "stream", 3.0, 50.0, 4));
1193        assert!((log.total_time_ms() - 8.0).abs() < 1e-10);
1194    }
1195
1196    #[test]
1197    fn test_performance_log_peak_memory() {
1198        let mut log = PerformanceLog::new("run001");
1199        log.record(PerfEntry::new("md", "force", 20.0, 500.0, 8));
1200        log.record(PerfEntry::new("md", "integrate", 5.0, 100.0, 8));
1201        assert!((log.peak_memory_mb() - 500.0).abs() < 1e-10);
1202    }
1203
1204    #[test]
1205    fn test_performance_log_filter_by_crate() {
1206        let mut log = PerformanceLog::new("run001");
1207        log.record(PerfEntry::new("lbm", "stream", 5.0, 50.0, 4));
1208        log.record(PerfEntry::new("md", "force", 10.0, 100.0, 8));
1209        let lbm_entries = log.filter_by_crate("lbm");
1210        assert_eq!(lbm_entries.len(), 1);
1211    }
1212
1213    #[test]
1214    fn test_performance_log_to_csv() {
1215        let mut log = PerformanceLog::new("run001");
1216        log.record(PerfEntry::new("lbm", "stream", 5.0, 50.0, 4));
1217        let csv = log.to_csv();
1218        assert!(csv.contains("lbm"));
1219        assert!(csv.contains("stream"));
1220    }
1221
1222    #[test]
1223    fn test_scientific_json_new() {
1224        let sj = ScientificJson::new("simulation");
1225        assert_eq!(sj.name, "simulation");
1226    }
1227
1228    #[test]
1229    fn test_scientific_json_add_array() {
1230        let mut sj = ScientificJson::new("sim");
1231        sj.add_array("velocity", vec![1.0, 2.0, 3.0]);
1232        assert_eq!(sj.get_array("velocity"), Some([1.0, 2.0, 3.0].as_slice()));
1233    }
1234
1235    #[test]
1236    fn test_scientific_json_to_json_contains_base64() {
1237        let mut sj = ScientificJson::new("sim");
1238        sj.add_array("pressure", vec![1.0, 2.0]);
1239        let json = sj.to_json();
1240        assert!(json.contains("base64"));
1241        assert!(json.contains("pressure"));
1242    }
1243
1244    #[test]
1245    fn test_scientific_json_scalar() {
1246        let mut sj = ScientificJson::new("sim");
1247        sj.set_scalar("dt", 0.001);
1248        let json = sj.to_json();
1249        let val = ScientificJson::parse_scalar(&json, "dt");
1250        assert!(val.is_some());
1251        assert!((val.unwrap() - 0.001).abs() < 1e-10);
1252    }
1253}