Skip to main content

oxiphysics_io/netcdf/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#[allow(unused_imports)]
6use super::functions::*;
7#[allow(unused_imports)]
8use super::functions_2::*;
9use std::io::Write;
10
11/// Dimension descriptor.
12#[derive(Debug, Clone)]
13#[allow(dead_code)]
14pub struct NetCdfDimension {
15    /// Dimension name.
16    pub name: String,
17    /// Dimension size. `None` indicates an unlimited dimension.
18    pub size: Option<usize>,
19}
20impl NetCdfDimension {
21    /// Check if this is an unlimited dimension.
22    #[allow(dead_code)]
23    pub fn is_unlimited(&self) -> bool {
24        self.size.is_none()
25    }
26    /// Get the effective size (0 for unlimited with no data).
27    #[allow(dead_code)]
28    pub fn effective_size(&self) -> usize {
29        self.size.unwrap_or(0)
30    }
31}
32/// A single variable inside a [`NetCdfFile`].
33#[derive(Debug, Clone)]
34#[allow(dead_code)]
35pub struct NetCdfVariable {
36    /// Variable name.
37    pub name: String,
38    /// Ordered dimension names this variable depends on.
39    pub dims: Vec<String>,
40    /// Flat data values (row-major).
41    pub data: Vec<f64>,
42    /// Per-variable attributes.
43    pub attributes: Vec<VariableAttribute>,
44}
45impl NetCdfVariable {
46    /// Create a new variable with the given name, dimensions, and data.
47    #[allow(dead_code)]
48    pub fn new(name: &str, dims: Vec<String>, data: Vec<f64>) -> Self {
49        NetCdfVariable {
50            name: name.to_string(),
51            dims,
52            data,
53            attributes: Vec::new(),
54        }
55    }
56    /// Add an attribute to this variable.
57    #[allow(dead_code)]
58    pub fn add_attribute(&mut self, key: &str, value: &str) {
59        self.attributes.push(VariableAttribute {
60            key: key.to_string(),
61            value: value.to_string(),
62        });
63    }
64    /// Get the value of an attribute by key.
65    #[allow(dead_code)]
66    pub fn get_attribute(&self, key: &str) -> Option<&str> {
67        self.attributes
68            .iter()
69            .find(|a| a.key == key)
70            .map(|a| a.value.as_str())
71    }
72    /// Number of data elements.
73    #[allow(dead_code)]
74    pub fn len(&self) -> usize {
75        self.data.len()
76    }
77    /// Check if variable has no data.
78    #[allow(dead_code)]
79    pub fn is_empty(&self) -> bool {
80        self.data.is_empty()
81    }
82}
83/// Extended statistics for a [`NetcdfVariable`].
84#[derive(Debug, Clone)]
85#[allow(dead_code)]
86pub struct NetcdfVariableStats {
87    /// Variable name.
88    pub name: String,
89    /// Minimum value.
90    pub min: f64,
91    /// Maximum value.
92    pub max: f64,
93    /// Arithmetic mean.
94    pub mean: f64,
95    /// Standard deviation (population).
96    pub std_dev: f64,
97    /// Number of elements.
98    pub count: usize,
99}
100#[allow(dead_code)]
101impl NetcdfVariableStats {
102    /// Range = max − min.
103    pub fn range(&self) -> f64 {
104        self.max - self.min
105    }
106    /// Coefficient of variation (std/mean), or 0 if mean == 0.
107    pub fn cv(&self) -> f64 {
108        if self.mean.abs() < f64::EPSILON {
109            0.0
110        } else {
111            self.std_dev / self.mean
112        }
113    }
114}
115/// A NetCDF-4 (HDF5-based) in-memory dataset with group support.
116#[derive(Debug, Clone)]
117#[allow(dead_code)]
118pub struct Nc4File {
119    /// Root group (equivalent to "/" in HDF5).
120    pub root: Nc4Group,
121    /// Global (file-level) attributes.
122    pub global_attributes: Vec<(String, String)>,
123    /// All dimensions (name → size, None = unlimited).
124    pub dimensions: Vec<(String, Option<usize>)>,
125    /// Current size of the unlimited dimension (number of records written).
126    pub unlimited_size: usize,
127}
128impl Nc4File {
129    /// Create a new empty NetCDF-4 file.
130    #[allow(dead_code)]
131    pub fn new() -> Self {
132        Self {
133            root: Nc4Group::new("/"),
134            global_attributes: Vec::new(),
135            dimensions: Vec::new(),
136            unlimited_size: 0,
137        }
138    }
139    /// Add a fixed dimension.
140    #[allow(dead_code)]
141    pub fn add_dimension(&mut self, name: &str, size: usize) {
142        self.dimensions.push((name.to_string(), Some(size)));
143    }
144    /// Add an unlimited dimension.
145    #[allow(dead_code)]
146    pub fn add_unlimited_dimension(&mut self, name: &str) {
147        self.dimensions.push((name.to_string(), None));
148    }
149    /// Extend the unlimited dimension by one record.
150    #[allow(dead_code)]
151    pub fn extend_unlimited(&mut self) {
152        self.unlimited_size += 1;
153        for entry in self.dimensions.iter_mut() {
154            if entry.1.is_none() || entry.1 == Some(self.unlimited_size - 1) {
155                entry.1 = Some(self.unlimited_size);
156                break;
157            }
158        }
159    }
160    /// Add a global attribute.
161    #[allow(dead_code)]
162    pub fn add_global_attribute(&mut self, key: &str, value: &str) {
163        self.global_attributes
164            .push((key.to_string(), value.to_string()));
165    }
166    /// Get a global attribute.
167    #[allow(dead_code)]
168    pub fn get_global_attribute(&self, key: &str) -> Option<&str> {
169        self.global_attributes
170            .iter()
171            .find(|(k, _)| k == key)
172            .map(|(_, v)| v.as_str())
173    }
174    /// Get a dimension size.
175    #[allow(dead_code)]
176    pub fn get_dimension_size(&self, name: &str) -> Option<usize> {
177        self.dimensions
178            .iter()
179            .find(|(n, _)| n == name)
180            .and_then(|(_, s)| *s)
181    }
182    /// Whether a dimension is unlimited.
183    #[allow(dead_code)]
184    pub fn is_unlimited(&self, name: &str) -> bool {
185        self.dimensions.iter().any(|(n, s)| {
186            n == name && s.is_none()
187                || (n == name && *s == Some(self.unlimited_size) && self.unlimited_size > 0)
188        })
189    }
190    /// Add a variable to the root group.
191    #[allow(dead_code)]
192    pub fn add_variable(&mut self, var: Nc4Variable) {
193        self.root.add_variable(var);
194    }
195    /// Get a variable from the root group.
196    #[allow(dead_code)]
197    pub fn get_variable(&self, name: &str) -> Option<&Nc4Variable> {
198        self.root.get_variable(name)
199    }
200    /// Add a named sub-group to the root.
201    #[allow(dead_code)]
202    pub fn add_group(&mut self, group: Nc4Group) {
203        self.root.add_subgroup(group);
204    }
205    /// Get a sub-group by name from the root.
206    #[allow(dead_code)]
207    pub fn get_group(&self, name: &str) -> Option<&Nc4Group> {
208        self.root.subgroups.iter().find(|g| g.name == name)
209    }
210    /// All variable count (all groups).
211    #[allow(dead_code)]
212    pub fn total_variable_count(&self) -> usize {
213        self.root.total_variable_count()
214    }
215    /// Serialize to a simple CDL-based binary envelope.
216    ///
217    /// Layout: `OXNC4` magic (5 bytes) + payload length u32 LE + CDL-like text payload.
218    ///
219    /// The CDL payload stores global attributes, dimensions, unlimited size,
220    /// and root variable names as a simple key=value text block.
221    #[allow(dead_code)]
222    pub fn to_bytes(&self) -> Vec<u8> {
223        let mut lines = Vec::new();
224        lines.push(format!("unlimited_size:{}", self.unlimited_size));
225        for (k, v) in &self.global_attributes {
226            lines.push(format!("attr:{}={}", k, v));
227        }
228        for (name, size) in &self.dimensions {
229            match size {
230                Some(s) => lines.push(format!("dim:{}={}", name, s)),
231                None => lines.push(format!("dim:{}=UNLIMITED", name)),
232            }
233        }
234        for var in &self.root.variables {
235            lines.push(format!(
236                "var:{}:{}:{}",
237                var.name,
238                var.dims.join(","),
239                var.data_type.type_name()
240            ));
241        }
242        let payload = lines.join("\n");
243        let payload_bytes = payload.as_bytes();
244        let mut buf = Vec::with_capacity(9 + payload_bytes.len());
245        buf.extend_from_slice(b"OXNC4");
246        buf.extend_from_slice(&(payload_bytes.len() as u32).to_le_bytes());
247        buf.extend_from_slice(payload_bytes);
248        buf
249    }
250}
251impl Default for Nc4File {
252    fn default() -> Self {
253        Self::new()
254    }
255}
256/// NetCDF4 variable type tags (mirrors HDF5 / NetCDF-4 data types).
257#[derive(Debug, Clone, PartialEq, Eq)]
258#[allow(dead_code)]
259pub enum Nc4DataType {
260    /// 64-bit IEEE floating point.
261    Float64,
262    /// 32-bit IEEE floating point.
263    Float32,
264    /// 32-bit signed integer.
265    Int32,
266    /// 8-bit unsigned integer.
267    UInt8,
268    /// Variable-length string (NC_STRING).
269    String,
270}
271impl Nc4DataType {
272    /// Return the typical byte width (variable-length types return 0).
273    #[allow(dead_code)]
274    pub fn byte_width(&self) -> usize {
275        match self {
276            Nc4DataType::Float64 => 8,
277            Nc4DataType::Float32 => 4,
278            Nc4DataType::Int32 => 4,
279            Nc4DataType::UInt8 => 1,
280            Nc4DataType::String => 0,
281        }
282    }
283    /// NetCDF-4 type name string.
284    #[allow(dead_code)]
285    pub fn type_name(&self) -> &'static str {
286        match self {
287            Nc4DataType::Float64 => "double",
288            Nc4DataType::Float32 => "float",
289            Nc4DataType::Int32 => "int",
290            Nc4DataType::UInt8 => "ubyte",
291            Nc4DataType::String => "string",
292        }
293    }
294}
295/// Fluent builder for writing NetCDF-convention trajectories.
296#[derive(Debug, Clone, Default)]
297#[allow(dead_code)]
298pub struct NetcdfTrajectoryBuilder {
299    /// Title / description of the trajectory.
300    pub title: String,
301    /// Application name that produced this trajectory.
302    pub application: String,
303    /// Frames in insertion order.
304    pub frames: Vec<TrajectoryFrame>,
305    /// Whether positions are stored in Ã… (true) or nm (false).
306    pub use_angstroms: bool,
307}
308impl NetcdfTrajectoryBuilder {
309    /// Create an empty builder.
310    #[allow(dead_code)]
311    pub fn new() -> Self {
312        NetcdfTrajectoryBuilder {
313            title: String::new(),
314            application: "OxiPhysics".to_string(),
315            frames: Vec::new(),
316            use_angstroms: true,
317        }
318    }
319    /// Set the trajectory title.
320    #[allow(dead_code)]
321    pub fn with_title(mut self, title: &str) -> Self {
322        self.title = title.to_string();
323        self
324    }
325    /// Set the application name.
326    #[allow(dead_code)]
327    pub fn with_application(mut self, app: &str) -> Self {
328        self.application = app.to_string();
329        self
330    }
331    /// Use Ã… as the position unit.
332    #[allow(dead_code)]
333    pub fn in_angstroms(mut self) -> Self {
334        self.use_angstroms = true;
335        self
336    }
337    /// Use nm as the position unit.
338    #[allow(dead_code)]
339    pub fn in_nanometres(mut self) -> Self {
340        self.use_angstroms = false;
341        self
342    }
343    /// Append a frame to the trajectory.
344    #[allow(dead_code)]
345    pub fn add_frame(&mut self, frame: TrajectoryFrame) {
346        self.frames.push(frame);
347    }
348    /// Number of frames stored.
349    #[allow(dead_code)]
350    pub fn frame_count(&self) -> usize {
351        self.frames.len()
352    }
353    /// Number of atoms (taken from the first frame; 0 if empty).
354    #[allow(dead_code)]
355    pub fn n_atoms(&self) -> usize {
356        self.frames.first().map(|f| f.n_atoms()).unwrap_or(0)
357    }
358    /// Compute the RMSD trajectory (each frame vs. frame 0).
359    #[allow(dead_code)]
360    pub fn rmsd_series(&self) -> Vec<f64> {
361        if self.frames.is_empty() {
362            return vec![];
363        }
364        let ref_frame = &self.frames[0];
365        self.frames.iter().map(|f| f.rmsd_from(ref_frame)).collect()
366    }
367    /// Write a minimal CDL representation to a writer.
368    #[allow(dead_code)]
369    pub fn write_cdl<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
370        let n_atoms = self.n_atoms();
371        let n_frames = self.frame_count();
372        writeln!(writer, "netcdf trajectory {{")?;
373        writeln!(writer, "dimensions:")?;
374        writeln!(writer, "\tatom = {n_atoms} ;")?;
375        writeln!(writer, "\tframe = UNLIMITED ; // currently {n_frames}")?;
376        writeln!(writer, "\tspatial = 3 ;")?;
377        writeln!(writer, "\tlabel = 5 ;")?;
378        writeln!(writer, "variables:")?;
379        let unit = if self.use_angstroms {
380            "angstrom"
381        } else {
382            "nanometer"
383        };
384        writeln!(writer, "\tdouble coordinates(frame, atom, spatial) ;")?;
385        writeln!(writer, "\t\tcoordinates:units = \"{unit}\" ;")?;
386        writeln!(writer, "\tdouble time(frame) ;")?;
387        writeln!(writer, "\t\ttime:units = \"picosecond\" ;")?;
388        writeln!(writer, "\tdouble cell_lengths(frame, spatial) ;")?;
389        writeln!(writer, "\t\tcell_lengths:units = \"{unit}\" ;")?;
390        writeln!(writer, "// global attributes:")?;
391        writeln!(writer, "\t:title = \"{}\" ;", self.title)?;
392        writeln!(writer, "\t:application = \"{}\" ;", self.application)?;
393        writeln!(writer, "\t:Conventions = \"AMBER\" ;")?;
394        writeln!(writer, "\t:ConventionVersion = \"1.0\" ;")?;
395        writeln!(writer, "}}")?;
396        Ok(())
397    }
398    /// Extract the time series (ps) of all frames.
399    #[allow(dead_code)]
400    pub fn time_series(&self) -> Vec<f64> {
401        self.frames.iter().map(|f| f.time_ps).collect()
402    }
403    /// Compute per-frame centre-of-mass trajectories.
404    #[allow(dead_code)]
405    pub fn com_trajectory(&self) -> Vec<[f64; 3]> {
406        self.frames.iter().map(|f| f.centre_of_mass()).collect()
407    }
408    /// Get frame at a specific index (panics if out of range).
409    #[allow(dead_code)]
410    pub fn frame(&self, idx: usize) -> &TrajectoryFrame {
411        &self.frames[idx]
412    }
413}
414/// An in-memory representation of a NetCDF-like dataset.
415#[derive(Debug, Clone)]
416#[allow(dead_code)]
417pub struct NetCdfFile {
418    /// Named dimensions and their sizes.
419    pub dimensions: Vec<(String, usize)>,
420    /// Variables stored in the file.
421    pub variables: Vec<NetCdfVariable>,
422    /// Global attributes as key-value string pairs.
423    pub attributes: Vec<(String, String)>,
424    /// Unlimited dimension name, if any.
425    pub unlimited_dim: Option<String>,
426}
427impl NetCdfFile {
428    /// Create an empty NetCDF file.
429    #[allow(dead_code)]
430    pub fn new() -> Self {
431        NetCdfFile {
432            dimensions: Vec::new(),
433            variables: Vec::new(),
434            attributes: Vec::new(),
435            unlimited_dim: None,
436        }
437    }
438    /// Add a dimension.
439    #[allow(dead_code)]
440    pub fn add_dimension(&mut self, name: &str, size: usize) {
441        self.dimensions.push((name.to_string(), size));
442    }
443    /// Add an unlimited dimension.
444    #[allow(dead_code)]
445    pub fn add_unlimited_dimension(&mut self, name: &str, current_size: usize) {
446        self.dimensions.push((name.to_string(), current_size));
447        self.unlimited_dim = Some(name.to_string());
448    }
449    /// Add a variable.
450    #[allow(dead_code)]
451    pub fn add_variable(&mut self, var: NetCdfVariable) {
452        self.variables.push(var);
453    }
454    /// Add a global attribute.
455    #[allow(dead_code)]
456    pub fn add_attribute(&mut self, key: &str, value: &str) {
457        self.attributes.push((key.to_string(), value.to_string()));
458    }
459    /// Get a variable by name.
460    #[allow(dead_code)]
461    pub fn get_variable(&self, name: &str) -> Option<&NetCdfVariable> {
462        self.variables.iter().find(|v| v.name == name)
463    }
464    /// Get a mutable variable by name.
465    #[allow(dead_code)]
466    pub fn get_variable_mut(&mut self, name: &str) -> Option<&mut NetCdfVariable> {
467        self.variables.iter_mut().find(|v| v.name == name)
468    }
469    /// Get a global attribute value by key.
470    #[allow(dead_code)]
471    pub fn get_attribute(&self, key: &str) -> Option<&str> {
472        self.attributes
473            .iter()
474            .find(|(k, _)| k == key)
475            .map(|(_, v)| v.as_str())
476    }
477    /// Get the size of a dimension by name.
478    #[allow(dead_code)]
479    pub fn get_dimension_size(&self, name: &str) -> Option<usize> {
480        self.dimensions
481            .iter()
482            .find(|(n, _)| n == name)
483            .map(|(_, s)| *s)
484    }
485    /// Check if a dimension is unlimited.
486    #[allow(dead_code)]
487    pub fn is_unlimited_dimension(&self, name: &str) -> bool {
488        self.unlimited_dim.as_deref() == Some(name)
489    }
490    /// List all variable names.
491    #[allow(dead_code)]
492    pub fn variable_names(&self) -> Vec<&str> {
493        self.variables.iter().map(|v| v.name.as_str()).collect()
494    }
495    /// List all dimension names.
496    #[allow(dead_code)]
497    pub fn dimension_names(&self) -> Vec<&str> {
498        self.dimensions.iter().map(|(n, _)| n.as_str()).collect()
499    }
500}
501impl Default for NetCdfFile {
502    fn default() -> Self {
503        Self::new()
504    }
505}
506/// In-memory NetCDF-like file using HashMap for O(1) dimension lookups.
507#[derive(Debug, Clone)]
508#[allow(dead_code)]
509pub struct NetcdfFile {
510    /// Dimension name → size.
511    pub dimensions: std::collections::HashMap<String, usize>,
512    /// Variables.
513    pub variables: Vec<NetcdfVariable>,
514    /// Global attributes as (key, value) pairs.
515    pub global_attrs: Vec<(String, String)>,
516}
517#[allow(dead_code)]
518impl NetcdfFile {
519    /// Create a new empty `NetcdfFile`.
520    pub fn new() -> Self {
521        Self {
522            dimensions: std::collections::HashMap::new(),
523            variables: Vec::new(),
524            global_attrs: Vec::new(),
525        }
526    }
527    /// Add a named dimension with size `size`.
528    pub fn add_dimension(&mut self, name: &str, size: usize) {
529        self.dimensions.insert(name.to_string(), size);
530    }
531    /// Add a variable.
532    pub fn add_variable(&mut self, var: NetcdfVariable) {
533        self.variables.push(var);
534    }
535    /// Add a global attribute.
536    pub fn add_global_attr(&mut self, key: &str, value: &str) {
537        self.global_attrs.push((key.to_string(), value.to_string()));
538    }
539    /// Serialize to a CDL text string.
540    pub fn write_cdl(&self) -> String {
541        let mut out = String::new();
542        out.push_str("netcdf data {\n");
543        out.push_str("dimensions:\n");
544        let mut dims: Vec<(&String, &usize)> = self.dimensions.iter().collect();
545        dims.sort_by_key(|(k, _)| k.as_str());
546        for (name, size) in &dims {
547            out.push_str(&format!("\t{} = {} ;\n", name, size));
548        }
549        out.push_str("variables:\n");
550        for var in &self.variables {
551            let dims_str = var.dimensions.join(", ");
552            out.push_str(&format!("\tdouble {}({}) ;\n", var.name, dims_str));
553            if !var.units.is_empty() {
554                out.push_str(&format!("\t\t{}:units = \"{}\" ;\n", var.name, var.units));
555            }
556            if !var.long_name.is_empty() {
557                out.push_str(&format!(
558                    "\t\t{}:long_name = \"{}\" ;\n",
559                    var.name, var.long_name
560                ));
561            }
562        }
563        if !self.global_attrs.is_empty() {
564            out.push_str("// global attributes:\n");
565            for (key, value) in &self.global_attrs {
566                out.push_str(&format!("\t\t:{} = \"{}\" ;\n", key, value));
567            }
568        }
569        out.push_str("data:\n");
570        for var in &self.variables {
571            let vals: Vec<String> = var.data.iter().map(|v| format!("{}", v)).collect();
572            out.push_str(&format!("\t{} = {} ;\n", var.name, vals.join(", ")));
573        }
574        out.push_str("}\n");
575        out
576    }
577    /// Write a simplified MD trajectory in NetCDF format (text CDL) to `path`.
578    ///
579    /// `times` is a slice of time values; `positions` is a slice of frames where
580    /// each frame is a `Vec<[f64;3]>` of atom positions.
581    pub fn trajectory_write(
582        path: &str,
583        times: &[f64],
584        positions: &[Vec<[f64; 3]>],
585    ) -> std::io::Result<()> {
586        use std::io::Write;
587        let n_frames = times.len();
588        let n_atoms = positions.first().map(|f| f.len()).unwrap_or(0);
589        let mut f = std::fs::File::create(path)?;
590        writeln!(f, "netcdf trajectory {{")?;
591        writeln!(f, "dimensions:")?;
592        writeln!(f, "\tframe = {} ;", n_frames)?;
593        writeln!(f, "\tatom = {} ;", n_atoms)?;
594        writeln!(f, "\tspatial = 3 ;")?;
595        writeln!(f, "variables:")?;
596        writeln!(f, "\tdouble time(frame) ;")?;
597        writeln!(f, "\t\ttime:units = \"ps\" ;")?;
598        writeln!(f, "\tdouble coordinates(frame, atom, spatial) ;")?;
599        writeln!(f, "\t\tcoordinates:units = \"angstrom\" ;")?;
600        writeln!(f, "data:")?;
601        let time_vals: Vec<String> = times.iter().map(|t| format!("{}", t)).collect();
602        writeln!(f, "\ttime = {} ;", time_vals.join(", "))?;
603        write!(f, "\tcoordinates = ")?;
604        let mut first = true;
605        for frame in positions {
606            for pos in frame {
607                for &coord in pos {
608                    if !first {
609                        write!(f, ", ")?;
610                    }
611                    write!(f, "{}", coord)?;
612                    first = false;
613                }
614            }
615        }
616        writeln!(f, " ;")?;
617        writeln!(f, "}}")?;
618        Ok(())
619    }
620}
621/// A builder-style writer for the simplified NetCDF-like text (CDL) format.
622///
623/// Supports dimension management, coordinate variables, typed variables,
624/// global attributes, and trajectory serialisation.
625#[derive(Debug, Clone)]
626#[allow(dead_code)]
627pub struct NetcdfWriter {
628    pub(super) name: String,
629    pub(super) dimensions: Vec<(String, usize)>,
630    pub(super) unlimited_dim: Option<String>,
631    pub(super) variables: Vec<NetcdfWriterVariable>,
632    pub(super) global_attrs: Vec<(String, String)>,
633}
634#[allow(dead_code)]
635impl NetcdfWriter {
636    /// Create a new writer with a dataset name.
637    pub fn new(name: &str) -> Self {
638        Self {
639            name: name.to_string(),
640            dimensions: Vec::new(),
641            unlimited_dim: None,
642            variables: Vec::new(),
643            global_attrs: Vec::new(),
644        }
645    }
646    /// Add a fixed-size dimension.
647    pub fn add_dimension(&mut self, name: &str, size: usize) -> &mut Self {
648        self.dimensions.push((name.to_string(), size));
649        self
650    }
651    /// Add an unlimited (record) dimension.
652    pub fn add_unlimited_dimension(&mut self, name: &str, current_size: usize) -> &mut Self {
653        self.dimensions.push((name.to_string(), current_size));
654        self.unlimited_dim = Some(name.to_string());
655        self
656    }
657    /// Add a global attribute.
658    pub fn add_global_attribute(&mut self, key: &str, value: &str) -> &mut Self {
659        self.global_attrs.push((key.to_string(), value.to_string()));
660        self
661    }
662    /// Add a variable with given dimension names and flat data.
663    pub fn add_variable(&mut self, name: &str, dims: &[&str], data: Vec<f64>) -> &mut Self {
664        self.variables.push(NetcdfWriterVariable {
665            name: name.to_string(),
666            dims: dims.iter().map(|s| s.to_string()).collect(),
667            data,
668            attrs: Vec::new(),
669        });
670        self
671    }
672    /// Add an attribute to the most recently added variable.
673    pub fn add_variable_attribute(&mut self, key: &str, value: &str) -> &mut Self {
674        if let Some(v) = self.variables.last_mut() {
675            v.attrs.push((key.to_string(), value.to_string()));
676        }
677        self
678    }
679    /// Add a coordinate variable (1D variable with the same name as its dimension).
680    pub fn add_coordinate(&mut self, dim_name: &str, data: Vec<f64>, units: &str) -> &mut Self {
681        self.add_variable(dim_name, &[dim_name], data);
682        self.add_variable_attribute("units", units);
683        self.add_variable_attribute("axis", dim_name);
684        self
685    }
686    /// Number of dimensions.
687    pub fn n_dimensions(&self) -> usize {
688        self.dimensions.len()
689    }
690    /// Number of variables.
691    pub fn n_variables(&self) -> usize {
692        self.variables.len()
693    }
694    /// Serialise to a CDL text string.
695    pub fn to_cdl(&self) -> String {
696        let mut out = String::new();
697        out.push_str(&format!("netcdf {} {{\n", self.name));
698        out.push_str("dimensions:\n");
699        for (name, size) in &self.dimensions {
700            if self.unlimited_dim.as_deref() == Some(name.as_str()) {
701                out.push_str(&format!(
702                    "\t{} = UNLIMITED ; // ({} currently)\n",
703                    name, size
704                ));
705            } else {
706                out.push_str(&format!("\t{} = {} ;\n", name, size));
707            }
708        }
709        out.push_str("variables:\n");
710        for var in &self.variables {
711            let dims = var.dims.join(", ");
712            out.push_str(&format!("\tdouble {}({}) ;\n", var.name, dims));
713            for (k, v) in &var.attrs {
714                out.push_str(&format!("\t\t{}:{} = \"{}\" ;\n", var.name, k, v));
715            }
716        }
717        if !self.global_attrs.is_empty() {
718            out.push_str("// global attributes:\n");
719            for (k, v) in &self.global_attrs {
720                out.push_str(&format!("\t:{} = \"{}\" ;\n", k, v));
721            }
722        }
723        out.push_str("data:\n");
724        for var in &self.variables {
725            let vals: Vec<String> = var.data.iter().map(|v| format!("{}", v)).collect();
726            out.push_str(&format!("\t{} = {} ;\n", var.name, vals.join(", ")));
727        }
728        out.push_str("}\n");
729        out
730    }
731    /// Consume the writer and return the CDL string.
732    pub fn finish(self) -> String {
733        self.to_cdl()
734    }
735    /// Write the CDL to a file.
736    pub fn write_to_file(&self, path: &str) -> std::io::Result<()> {
737        let mut f = std::fs::File::create(path)?;
738        f.write_all(self.to_cdl().as_bytes())
739    }
740}
741/// A single variable in the new simplified NetcdfFile.
742#[derive(Debug, Clone)]
743#[allow(dead_code)]
744pub struct NetcdfVariable {
745    /// Variable name.
746    pub name: String,
747    /// Dimension names for this variable.
748    pub dimensions: Vec<String>,
749    /// Flat data values.
750    pub data: Vec<f64>,
751    /// Unit string (e.g., `"m/s"`).
752    pub units: String,
753    /// Long descriptive name.
754    pub long_name: String,
755}
756/// One snapshot of positions + optional velocities for an MD trajectory.
757#[derive(Debug, Clone)]
758#[allow(dead_code)]
759pub struct TrajectoryFrame {
760    /// Simulation time in ps.
761    pub time_ps: f64,
762    /// Positions: `n_atoms × 3`, row-major (Å or nm depending on convention).
763    pub positions: Vec<[f64; 3]>,
764    /// Optional velocities: `n_atoms × 3` in nm/ps.
765    pub velocities: Option<Vec<[f64; 3]>>,
766    /// Optional box vectors (orthorhombic: `[Lx, Ly, Lz]`).
767    pub box_lengths: Option<[f64; 3]>,
768}
769impl TrajectoryFrame {
770    /// Number of atoms in this frame.
771    #[allow(dead_code)]
772    pub fn n_atoms(&self) -> usize {
773        self.positions.len()
774    }
775    /// Compute the centre-of-mass position (equal masses assumed).
776    #[allow(dead_code)]
777    pub fn centre_of_mass(&self) -> [f64; 3] {
778        if self.positions.is_empty() {
779            return [0.0; 3];
780        }
781        let mut s = [0.0_f64; 3];
782        for p in &self.positions {
783            s[0] += p[0];
784            s[1] += p[1];
785            s[2] += p[2];
786        }
787        let inv = 1.0 / self.positions.len() as f64;
788        [s[0] * inv, s[1] * inv, s[2] * inv]
789    }
790    /// Compute the root-mean-square displacement from a reference frame.
791    #[allow(dead_code)]
792    pub fn rmsd_from(&self, reference: &TrajectoryFrame) -> f64 {
793        let n = self.positions.len().min(reference.positions.len());
794        if n == 0 {
795            return 0.0;
796        }
797        let sum: f64 = (0..n)
798            .map(|i| {
799                let dx = self.positions[i][0] - reference.positions[i][0];
800                let dy = self.positions[i][1] - reference.positions[i][1];
801                let dz = self.positions[i][2] - reference.positions[i][2];
802                dx * dx + dy * dy + dz * dz
803            })
804            .sum();
805        (sum / n as f64).sqrt()
806    }
807    /// Compute kinetic energy assuming all masses equal to `mass_amu`.
808    /// Returns energy in kJ/mol (1 amu × nm²/ps² = 1 kJ/mol).
809    #[allow(dead_code)]
810    pub fn kinetic_energy(&self, mass_amu: f64) -> f64 {
811        let vels = match &self.velocities {
812            Some(v) => v,
813            None => return 0.0,
814        };
815        let sum: f64 = vels
816            .iter()
817            .map(|v| v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
818            .sum();
819        0.5 * mass_amu * sum
820    }
821    /// Translate all positions by `delta`.
822    #[allow(dead_code)]
823    pub fn translate(&mut self, delta: [f64; 3]) {
824        for p in &mut self.positions {
825            p[0] += delta[0];
826            p[1] += delta[1];
827            p[2] += delta[2];
828        }
829    }
830}
831/// A simple CDL text reader.
832///
833/// Reads dimension, variable, and data sections from CDL output produced by
834/// [`NetcdfWriter`] or [`NetcdfFile::write_cdl`].
835#[derive(Debug, Clone)]
836#[allow(dead_code)]
837pub struct NetcdfReader {
838    /// Parsed dimensions (name → size).
839    pub dimensions: std::collections::HashMap<String, usize>,
840    /// Parsed variables.
841    pub variables: Vec<NetcdfVariable>,
842    /// Global attributes.
843    pub global_attrs: Vec<(String, String)>,
844    /// Unlimited dimension name.
845    pub unlimited_dim: Option<String>,
846}
847#[allow(dead_code)]
848impl NetcdfReader {
849    /// Parse a CDL string.
850    pub fn from_cdl(cdl: &str) -> Result<Self, String> {
851        let file = parse_ncf_text(cdl)?;
852        let variables: Vec<NetcdfVariable> = file
853            .variables
854            .into_iter()
855            .map(|v| {
856                let units = v.get_attribute("units").unwrap_or("").to_string();
857                let long_name = v.get_attribute("long_name").unwrap_or("").to_string();
858                NetcdfVariable {
859                    name: v.name,
860                    dimensions: v.dims,
861                    data: v.data,
862                    units,
863                    long_name,
864                }
865            })
866            .collect();
867        Ok(Self {
868            dimensions: file.dimensions.into_iter().collect(),
869            variables,
870            global_attrs: file.attributes,
871            unlimited_dim: file.unlimited_dim,
872        })
873    }
874    /// Get dimension size by name.
875    pub fn get_dimension(&self, name: &str) -> Option<usize> {
876        self.dimensions.get(name).copied()
877    }
878    /// Get variable by name.
879    pub fn get_variable(&self, name: &str) -> Option<&NetcdfVariable> {
880        self.variables.iter().find(|v| v.name == name)
881    }
882    /// Get data slice for a variable.
883    pub fn get_data(&self, var_name: &str) -> Option<&[f64]> {
884        self.get_variable(var_name).map(|v| v.data.as_slice())
885    }
886    /// List all variable names.
887    pub fn variable_names(&self) -> Vec<&str> {
888        self.variables.iter().map(|v| v.name.as_str()).collect()
889    }
890    /// List all dimension names.
891    pub fn dimension_names(&self) -> Vec<&str> {
892        let mut names: Vec<&str> = self.dimensions.keys().map(String::as_str).collect();
893        names.sort();
894        names
895    }
896}
897/// A variable attribute (key-value string pair).
898#[derive(Debug, Clone)]
899#[allow(dead_code)]
900pub struct VariableAttribute {
901    /// Attribute key.
902    pub key: String,
903    /// Attribute value.
904    pub value: String,
905}
906/// A named group within a NetCDF-4 file.
907///
908/// Groups allow hierarchical organization of variables, akin to HDF5 groups.
909#[derive(Debug, Clone)]
910#[allow(dead_code)]
911pub struct Nc4Group {
912    /// Group name.
913    pub name: String,
914    /// Variables in this group.
915    pub variables: Vec<Nc4Variable>,
916    /// Sub-groups.
917    pub subgroups: Vec<Nc4Group>,
918    /// Group-level attributes (inherited by sub-groups).
919    pub attributes: Vec<(String, String)>,
920}
921impl Nc4Group {
922    /// Create a new empty group.
923    #[allow(dead_code)]
924    pub fn new(name: &str) -> Self {
925        Self {
926            name: name.to_string(),
927            variables: Vec::new(),
928            subgroups: Vec::new(),
929            attributes: Vec::new(),
930        }
931    }
932    /// Add a variable to this group.
933    #[allow(dead_code)]
934    pub fn add_variable(&mut self, var: Nc4Variable) {
935        self.variables.push(var);
936    }
937    /// Add a sub-group.
938    #[allow(dead_code)]
939    pub fn add_subgroup(&mut self, group: Nc4Group) {
940        self.subgroups.push(group);
941    }
942    /// Add an attribute.
943    #[allow(dead_code)]
944    pub fn add_attribute(&mut self, key: &str, value: &str) {
945        self.attributes.push((key.to_string(), value.to_string()));
946    }
947    /// Get a variable by name.
948    #[allow(dead_code)]
949    pub fn get_variable(&self, name: &str) -> Option<&Nc4Variable> {
950        self.variables.iter().find(|v| v.name == name)
951    }
952    /// Get attribute value.
953    #[allow(dead_code)]
954    pub fn get_attribute(&self, key: &str) -> Option<&str> {
955        self.attributes
956            .iter()
957            .find(|(k, _)| k == key)
958            .map(|(_, v)| v.as_str())
959    }
960    /// All variables in this group and all sub-groups (depth-first).
961    #[allow(dead_code)]
962    pub fn all_variables(&self) -> Vec<&Nc4Variable> {
963        let mut out: Vec<&Nc4Variable> = self.variables.iter().collect();
964        for sg in &self.subgroups {
965            out.extend(sg.all_variables());
966        }
967        out
968    }
969    /// Total variable count (recursive).
970    #[allow(dead_code)]
971    pub fn total_variable_count(&self) -> usize {
972        self.variables.len()
973            + self
974                .subgroups
975                .iter()
976                .map(|g| g.total_variable_count())
977                .sum::<usize>()
978    }
979}
980/// A dimension with name, size, and whether it is an unlimited record dimension.
981#[derive(Debug, Clone, PartialEq)]
982#[allow(dead_code)]
983pub struct NetcdfDimSpec {
984    /// Dimension name.
985    pub name: String,
986    /// Current size (may grow if unlimited).
987    pub size: usize,
988    /// Whether this is the unlimited (record) dimension.
989    pub unlimited: bool,
990}
991impl NetcdfDimSpec {
992    /// Create a fixed-size dimension.
993    #[allow(dead_code)]
994    pub fn fixed(name: &str, size: usize) -> Self {
995        NetcdfDimSpec {
996            name: name.to_string(),
997            size,
998            unlimited: false,
999        }
1000    }
1001    /// Create an unlimited record dimension with current size.
1002    #[allow(dead_code)]
1003    pub fn unlimited(name: &str, current_size: usize) -> Self {
1004        NetcdfDimSpec {
1005            name: name.to_string(),
1006            size: current_size,
1007            unlimited: true,
1008        }
1009    }
1010    /// Produce the CDL declaration string.
1011    #[allow(dead_code)]
1012    pub fn to_cdl(&self) -> String {
1013        if self.unlimited {
1014            format!("\t{} = UNLIMITED ; // currently {}", self.name, self.size)
1015        } else {
1016            format!("\t{} = {} ;", self.name, self.size)
1017        }
1018    }
1019}
1020/// A NetCDF-4 variable with typed storage.
1021#[derive(Debug, Clone)]
1022#[allow(dead_code)]
1023pub struct Nc4Variable {
1024    /// Variable name.
1025    pub name: String,
1026    /// Ordered dimension names.
1027    pub dims: Vec<String>,
1028    /// Floating-point data (used when data_type is Float64 or Float32).
1029    pub float_data: Vec<f64>,
1030    /// String data (used when data_type is String).
1031    pub string_data: Vec<String>,
1032    /// Integer data (used when data_type is Int32 or UInt8).
1033    pub int_data: Vec<i64>,
1034    /// Data type.
1035    pub data_type: Nc4DataType,
1036    /// Per-variable attributes.
1037    pub attributes: Vec<VariableAttribute>,
1038}
1039impl Nc4Variable {
1040    /// Create a Float64 variable.
1041    #[allow(dead_code)]
1042    pub fn float64(name: &str, dims: Vec<String>, data: Vec<f64>) -> Self {
1043        Self {
1044            name: name.to_string(),
1045            dims,
1046            float_data: data,
1047            string_data: Vec::new(),
1048            int_data: Vec::new(),
1049            data_type: Nc4DataType::Float64,
1050            attributes: Vec::new(),
1051        }
1052    }
1053    /// Create a String variable.
1054    #[allow(dead_code)]
1055    pub fn string_var(name: &str, dims: Vec<String>, data: Vec<String>) -> Self {
1056        Self {
1057            name: name.to_string(),
1058            dims,
1059            float_data: Vec::new(),
1060            string_data: data,
1061            int_data: Vec::new(),
1062            data_type: Nc4DataType::String,
1063            attributes: Vec::new(),
1064        }
1065    }
1066    /// Create an Int32 variable.
1067    #[allow(dead_code)]
1068    pub fn int32(name: &str, dims: Vec<String>, data: Vec<i64>) -> Self {
1069        Self {
1070            name: name.to_string(),
1071            dims,
1072            float_data: Vec::new(),
1073            string_data: Vec::new(),
1074            int_data: data,
1075            data_type: Nc4DataType::Int32,
1076            attributes: Vec::new(),
1077        }
1078    }
1079    /// Add an attribute.
1080    #[allow(dead_code)]
1081    pub fn add_attribute(&mut self, key: &str, value: &str) {
1082        self.attributes.push(VariableAttribute {
1083            key: key.to_string(),
1084            value: value.to_string(),
1085        });
1086    }
1087    /// Get attribute value by key.
1088    #[allow(dead_code)]
1089    pub fn get_attribute(&self, key: &str) -> Option<&str> {
1090        self.attributes
1091            .iter()
1092            .find(|a| a.key == key)
1093            .map(|a| a.value.as_str())
1094    }
1095    /// Number of elements (type-appropriate).
1096    #[allow(dead_code)]
1097    pub fn len(&self) -> usize {
1098        match self.data_type {
1099            Nc4DataType::String => self.string_data.len(),
1100            Nc4DataType::Int32 | Nc4DataType::UInt8 => self.int_data.len(),
1101            _ => self.float_data.len(),
1102        }
1103    }
1104    /// Whether the variable has no data.
1105    #[allow(dead_code)]
1106    pub fn is_empty(&self) -> bool {
1107        self.len() == 0
1108    }
1109}
1110/// Internal variable representation for [`NetcdfWriter`].
1111#[derive(Debug, Clone)]
1112#[allow(dead_code)]
1113pub(super) struct NetcdfWriterVariable {
1114    pub(super) name: String,
1115    pub(super) dims: Vec<String>,
1116    pub(super) data: Vec<f64>,
1117    pub(super) attrs: Vec<(String, String)>,
1118}