1#![allow(clippy::type_complexity)]
6use crate::netcdf::types::*;
7
8#[allow(dead_code)]
15pub fn create_amber_trajectory(
16 n_atoms: usize,
17 n_frames: usize,
18 coordinates: Vec<f64>,
19 time: Vec<f64>,
20 cell_lengths: Option<Vec<f64>>,
21 cell_angles: Option<Vec<f64>>,
22) -> NetCdfFile {
23 let mut file = NetCdfFile::new();
24 file.add_unlimited_dimension("frame", n_frames);
25 file.add_dimension("atom", n_atoms);
26 file.add_dimension("spatial", 3);
27 file.add_attribute("Conventions", "AMBER");
28 file.add_attribute("ConventionVersion", "1.0");
29 file.add_attribute("program", "OxiPhysics");
30 let mut coords_var = NetCdfVariable::new(
31 "coordinates",
32 vec!["frame".into(), "atom".into(), "spatial".into()],
33 coordinates,
34 );
35 coords_var.add_attribute("units", "angstrom");
36 file.add_variable(coords_var);
37 let mut time_var = NetCdfVariable::new("time", vec!["frame".into()], time);
38 time_var.add_attribute("units", "picosecond");
39 file.add_variable(time_var);
40 if let Some(lengths) = cell_lengths {
41 let mut var = NetCdfVariable::new(
42 "cell_lengths",
43 vec!["frame".into(), "spatial".into()],
44 lengths,
45 );
46 var.add_attribute("units", "angstrom");
47 file.add_variable(var);
48 }
49 if let Some(angles) = cell_angles {
50 let mut var = NetCdfVariable::new(
51 "cell_angles",
52 vec!["frame".into(), "spatial".into()],
53 angles,
54 );
55 var.add_attribute("units", "degree");
56 file.add_variable(var);
57 }
58 file
59}
60#[allow(dead_code)]
62pub fn extract_frame_coordinates(file: &NetCdfFile, frame_idx: usize) -> Option<Vec<[f64; 3]>> {
63 let coords_var = file.get_variable("coordinates")?;
64 let n_atoms = file.get_dimension_size("atom")?;
65 let start = frame_idx * n_atoms * 3;
66 let end = start + n_atoms * 3;
67 if end > coords_var.data.len() {
68 return None;
69 }
70 let mut positions = Vec::with_capacity(n_atoms);
71 for i in 0..n_atoms {
72 let base = start + i * 3;
73 positions.push([
74 coords_var.data[base],
75 coords_var.data[base + 1],
76 coords_var.data[base + 2],
77 ]);
78 }
79 Some(positions)
80}
81#[allow(dead_code)]
83pub fn create_coordinate_variable(name: &str, values: Vec<f64>, units: &str) -> NetCdfVariable {
84 let mut var = NetCdfVariable::new(name, vec![name.to_string()], values);
85 var.add_attribute("units", units);
86 var
87}
88#[allow(dead_code)]
90pub fn write_ncf_text(file: &NetCdfFile) -> String {
91 let mut out = String::new();
92 out.push_str("netcdf data {\n");
93 out.push_str("dimensions:\n");
94 for (name, size) in &file.dimensions {
95 if file.unlimited_dim.as_deref() == Some(name.as_str()) {
96 out.push_str(&format!("\t{name} = UNLIMITED ; // ({size} currently)\n"));
97 } else {
98 out.push_str(&format!("\t{name} = {size} ;\n"));
99 }
100 }
101 out.push_str("variables:\n");
102 for var in &file.variables {
103 let dims = var.dims.join(", ");
104 out.push_str(&format!("\tdouble {}({}) ;\n", var.name, dims));
105 for attr in &var.attributes {
106 out.push_str(&format!(
107 "\t\t{}:{} = \"{}\" ;\n",
108 var.name, attr.key, attr.value
109 ));
110 }
111 }
112 if !file.attributes.is_empty() {
113 out.push_str("// global attributes:\n");
114 for (key, value) in &file.attributes {
115 out.push_str(&format!("\t\t:data:{key} = \"{value}\" ;\n"));
116 }
117 }
118 out.push_str("data:\n");
119 for var in &file.variables {
120 let values: Vec<String> = var.data.iter().map(|v| format!("{v}")).collect();
121 out.push_str(&format!("\t{} = {} ;\n", var.name, values.join(", ")));
122 }
123 out.push_str("}\n");
124 out
125}
126#[allow(dead_code)]
130pub fn parse_ncf_text(cdl: &str) -> Result<NetCdfFile, String> {
131 let mut dimensions: Vec<(String, usize)> = Vec::new();
132 let mut variable_headers: Vec<(String, Vec<String>)> = Vec::new();
133 let mut variable_attrs: std::collections::HashMap<String, Vec<VariableAttribute>> =
134 std::collections::HashMap::new();
135 let mut attributes: Vec<(String, String)> = Vec::new();
136 let mut data_map: std::collections::HashMap<String, Vec<f64>> =
137 std::collections::HashMap::new();
138 let mut unlimited_dim: Option<String> = None;
139 #[derive(PartialEq)]
140 pub(super) enum Section {
141 None,
142 Dimensions,
143 Variables,
144 Attributes,
145 Data,
146 }
147 let mut section = Section::None;
148 for raw in cdl.lines() {
149 let line = raw.trim();
150 if line.is_empty() || line == "{" || line == "}" || line.starts_with("netcdf ") {
151 continue;
152 }
153 if line == "dimensions:" {
154 section = Section::Dimensions;
155 continue;
156 }
157 if line == "variables:" {
158 section = Section::Variables;
159 continue;
160 }
161 if line.starts_with("// global attributes") {
162 section = Section::Attributes;
163 continue;
164 }
165 if line == "data:" {
166 section = Section::Data;
167 continue;
168 }
169 let line_no_comment = if let Some(pos) = line.find("//") {
170 line[..pos].trim()
171 } else {
172 line
173 };
174 let line_clean = line_no_comment.trim_end_matches(';').trim();
175 match section {
176 Section::Dimensions => {
177 let parts: Vec<&str> = line_clean.splitn(2, '=').collect();
178 if parts.len() != 2 {
179 return Err(format!("Bad dimension line: {raw}"));
180 }
181 let dim_name = parts[0].trim().to_string();
182 let size_str = parts[1].trim();
183 if size_str == "UNLIMITED" {
184 dimensions.push((dim_name.clone(), 0));
185 unlimited_dim = Some(dim_name);
186 } else {
187 let dim_size = size_str
188 .parse::<usize>()
189 .map_err(|_| format!("Bad dimension size in: {raw}"))?;
190 dimensions.push((dim_name, dim_size));
191 }
192 }
193 Section::Variables => {
194 if line_clean.contains(':') && !line_clean.starts_with("double ") {
195 let parts: Vec<&str> = line_clean.splitn(2, ':').collect();
196 if parts.len() == 2 {
197 let var_name = parts[0].trim().to_string();
198 let rest: Vec<&str> = parts[1].splitn(2, '=').collect();
199 if rest.len() == 2 {
200 let key = rest[0].trim().to_string();
201 let val = rest[1].trim().trim_matches('"').to_string();
202 variable_attrs
203 .entry(var_name)
204 .or_default()
205 .push(VariableAttribute { key, value: val });
206 }
207 }
208 continue;
209 }
210 if !line_clean.starts_with("double ") {
211 continue;
212 }
213 let rest = line_clean["double ".len()..].trim();
214 if let Some(paren) = rest.find('(') {
215 let var_name = rest[..paren].trim().to_string();
216 let dims_str = rest[paren + 1..].trim_end_matches(')').trim();
217 let dims: Vec<String> = if dims_str.is_empty() {
218 Vec::new()
219 } else {
220 dims_str.split(',').map(|s| s.trim().to_string()).collect()
221 };
222 variable_headers.push((var_name, dims));
223 } else {
224 return Err(format!("Bad variable line: {raw}"));
225 }
226 }
227 Section::Attributes => {
228 if !line_clean.starts_with(":data:") {
229 continue;
230 }
231 let rest = &line_clean[":data:".len()..];
232 let parts: Vec<&str> = rest.splitn(2, '=').collect();
233 if parts.len() != 2 {
234 return Err(format!("Bad attribute line: {raw}"));
235 }
236 let key = parts[0].trim().to_string();
237 let val = parts[1].trim().trim_matches('"').to_string();
238 attributes.push((key, val));
239 }
240 Section::Data => {
241 let parts: Vec<&str> = line_clean.splitn(2, '=').collect();
242 if parts.len() != 2 {
243 return Err(format!("Bad data line: {raw}"));
244 }
245 let var_name = parts[0].trim().to_string();
246 let values: Result<Vec<f64>, _> = parts[1]
247 .split(',')
248 .map(|s| s.trim().parse::<f64>())
249 .collect();
250 let values =
251 values.map_err(|_| format!("Bad numeric data in variable '{var_name}'"))?;
252 data_map.insert(var_name, values);
253 }
254 Section::None => {}
255 }
256 }
257 let variables: Vec<NetCdfVariable> = variable_headers
258 .into_iter()
259 .map(|(name, dims)| {
260 let data = data_map.remove(&name).unwrap_or_default();
261 let attrs = variable_attrs.remove(&name).unwrap_or_default();
262 NetCdfVariable {
263 name,
264 dims,
265 data,
266 attributes: attrs,
267 }
268 })
269 .collect();
270 Ok(NetCdfFile {
271 dimensions,
272 variables,
273 attributes,
274 unlimited_dim,
275 })
276}
277#[allow(dead_code)]
279pub fn reshape_2d(data: &[f64], n_rows: usize, n_cols: usize) -> Vec<Vec<f64>> {
280 let mut result = Vec::with_capacity(n_rows);
281 for r in 0..n_rows {
282 let start = r * n_cols;
283 let end = (start + n_cols).min(data.len());
284 if start < data.len() {
285 result.push(data[start..end].to_vec());
286 }
287 }
288 result
289}
290#[allow(dead_code)]
292pub fn variable_stats(var: &NetCdfVariable) -> Option<(f64, f64, f64)> {
293 if var.data.is_empty() {
294 return None;
295 }
296 let mut min = f64::INFINITY;
297 let mut max = f64::NEG_INFINITY;
298 let mut sum = 0.0;
299 for &v in &var.data {
300 if v < min {
301 min = v;
302 }
303 if v > max {
304 max = v;
305 }
306 sum += v;
307 }
308 let mean = sum / var.data.len() as f64;
309 Some((min, max, mean))
310}
311#[allow(dead_code)]
313pub fn create_string_variable(name: &str, dims: Vec<String>, values: Vec<String>) -> Nc4Variable {
314 let mut var = Nc4Variable::string_var(name, dims, values);
315 var.add_attribute("dtype", "string");
316 var
317}
318#[allow(dead_code)]
321pub fn inherit_global_attributes(source: &NetCdfFile, target: &mut NetCdfFile) {
322 for (key, value) in &source.attributes {
323 for var in target.variables.iter_mut() {
324 if var.get_attribute(key).is_none() {
325 var.add_attribute(key, value);
326 }
327 }
328 }
329}
330#[allow(dead_code)]
333pub fn merge_netcdf_files(base: &NetCdfFile, overlay: &NetCdfFile) -> NetCdfFile {
334 let mut result = base.clone();
335 for var in &overlay.variables {
336 if let Some(existing) = result.variables.iter_mut().find(|v| v.name == var.name) {
337 *existing = var.clone();
338 } else {
339 result.variables.push(var.clone());
340 }
341 }
342 for (key, value) in &overlay.attributes {
343 if let Some(existing) = result.attributes.iter_mut().find(|(k, _)| k == key) {
344 existing.1 = value.clone();
345 } else {
346 result.attributes.push((key.clone(), value.clone()));
347 }
348 }
349 result
350}
351#[allow(dead_code)]
355pub fn parse_cdl_dimensions(cdl: &str) -> Vec<(String, usize)> {
356 let mut result = Vec::new();
357 let mut in_dimensions = false;
358 for line in cdl.lines() {
359 let trimmed = line.trim();
360 if trimmed == "dimensions:" {
361 in_dimensions = true;
362 continue;
363 }
364 if in_dimensions {
365 if trimmed == "variables:"
366 || trimmed == "data:"
367 || trimmed.starts_with("//")
368 || trimmed == "}"
369 {
370 break;
371 }
372 let clean = trimmed.trim_end_matches(';').trim();
373 let parts: Vec<&str> = clean.splitn(2, '=').collect();
374 if parts.len() == 2 {
375 let name = parts[0].trim().to_string();
376 let size_str = parts[1].trim();
377 if let Ok(size) = size_str.parse::<usize>() {
378 result.push((name, size));
379 }
380 }
381 }
382 }
383 result
384}
385#[allow(dead_code)]
393pub fn trajectory_stats(times: &[f64], positions: &[Vec<[f64; 3]>]) -> (f64, f64, usize) {
394 let n_frames = times.len();
395 if n_frames == 0 {
396 return (0.0, 0.0, 0);
397 }
398 let total_time = if n_frames >= 2 {
399 times[n_frames - 1] - times[0]
400 } else {
401 0.0
402 };
403 let avg_displacement = if n_frames >= 2 && !positions.is_empty() {
404 let first = &positions[0];
405 let last = &positions[positions.len() - 1];
406 let n = first.len().min(last.len());
407 if n == 0 {
408 0.0
409 } else {
410 let sum: f64 = (0..n)
411 .map(|i| {
412 let dx = last[i][0] - first[i][0];
413 let dy = last[i][1] - first[i][1];
414 let dz = last[i][2] - first[i][2];
415 (dx * dx + dy * dy + dz * dz).sqrt()
416 })
417 .sum();
418 sum / n as f64
419 }
420 } else {
421 0.0
422 };
423 (total_time, avg_displacement, n_frames)
424}
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 fn make_file() -> NetCdfFile {
430 NetCdfFile {
431 dimensions: vec![("time".to_string(), 3), ("space".to_string(), 2)],
432 variables: vec![
433 NetCdfVariable {
434 name: "temperature".to_string(),
435 dims: vec!["time".to_string(), "space".to_string()],
436 data: vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
437 attributes: Vec::new(),
438 },
439 NetCdfVariable {
440 name: "pressure".to_string(),
441 dims: vec!["time".to_string()],
442 data: vec![101.3, 100.8, 99.5],
443 attributes: Vec::new(),
444 },
445 ],
446 attributes: vec![
447 ("title".to_string(), "Test dataset".to_string()),
448 ("units".to_string(), "SI".to_string()),
449 ],
450 unlimited_dim: None,
451 }
452 }
453 #[test]
454 fn test_write_ncf_text_structure() {
455 let file = make_file();
456 let cdl = write_ncf_text(&file);
457 assert!(cdl.contains("dimensions:"));
458 assert!(cdl.contains("time = 3"));
459 assert!(cdl.contains("space = 2"));
460 assert!(cdl.contains("variables:"));
461 assert!(cdl.contains("double temperature(time, space)"));
462 assert!(cdl.contains("double pressure(time)"));
463 assert!(cdl.contains("data:"));
464 assert!(cdl.contains("temperature ="));
465 assert!(cdl.contains("pressure ="));
466 assert!(cdl.contains(r#":data:title = "Test dataset""#));
467 }
468 #[test]
469 fn test_ncf_roundtrip() {
470 let original = make_file();
471 let cdl = write_ncf_text(&original);
472 let parsed = parse_ncf_text(&cdl).expect("parse failed");
473 assert_eq!(parsed.dimensions.len(), original.dimensions.len());
474 for (orig, got) in original.dimensions.iter().zip(parsed.dimensions.iter()) {
475 assert_eq!(orig.0, got.0, "dimension name mismatch");
476 assert_eq!(orig.1, got.1, "dimension size mismatch");
477 }
478 assert_eq!(parsed.variables.len(), original.variables.len());
479 for (orig, got) in original.variables.iter().zip(parsed.variables.iter()) {
480 assert_eq!(orig.name, got.name, "variable name mismatch");
481 assert_eq!(orig.dims, got.dims, "variable dims mismatch");
482 assert_eq!(orig.data.len(), got.data.len(), "data length mismatch");
483 for (a, b) in orig.data.iter().zip(got.data.iter()) {
484 assert!((a - b).abs() < 1e-9, "data value mismatch: {a} vs {b}");
485 }
486 }
487 assert_eq!(parsed.attributes.len(), original.attributes.len());
488 for (orig, got) in original.attributes.iter().zip(parsed.attributes.iter()) {
489 assert_eq!(orig.0, got.0, "attribute key mismatch");
490 assert_eq!(orig.1, got.1, "attribute value mismatch");
491 }
492 }
493 #[test]
494 fn test_ncf_empty_dataset() {
495 let file = NetCdfFile {
496 dimensions: vec![],
497 variables: vec![],
498 attributes: vec![],
499 unlimited_dim: None,
500 };
501 let cdl = write_ncf_text(&file);
502 let parsed = parse_ncf_text(&cdl).expect("parse failed");
503 assert!(parsed.dimensions.is_empty());
504 assert!(parsed.variables.is_empty());
505 assert!(parsed.attributes.is_empty());
506 }
507 #[test]
508 fn test_netcdf_file_new() {
509 let file = NetCdfFile::new();
510 assert!(file.dimensions.is_empty());
511 assert!(file.variables.is_empty());
512 assert!(file.attributes.is_empty());
513 assert!(file.unlimited_dim.is_none());
514 }
515 #[test]
516 fn test_add_dimension() {
517 let mut file = NetCdfFile::new();
518 file.add_dimension("time", 10);
519 assert_eq!(file.get_dimension_size("time"), Some(10));
520 assert_eq!(file.get_dimension_size("missing"), None);
521 }
522 #[test]
523 fn test_add_unlimited_dimension() {
524 let mut file = NetCdfFile::new();
525 file.add_unlimited_dimension("frame", 5);
526 assert!(file.is_unlimited_dimension("frame"));
527 assert!(!file.is_unlimited_dimension("atom"));
528 assert_eq!(file.get_dimension_size("frame"), Some(5));
529 }
530 #[test]
531 fn test_variable_new_and_attributes() {
532 let mut var = NetCdfVariable::new("temp", vec!["time".into()], vec![1.0, 2.0, 3.0]);
533 assert_eq!(var.len(), 3);
534 assert!(!var.is_empty());
535 var.add_attribute("units", "K");
536 assert_eq!(var.get_attribute("units"), Some("K"));
537 assert_eq!(var.get_attribute("missing"), None);
538 }
539 #[test]
540 fn test_variable_empty() {
541 let var = NetCdfVariable::new("empty", vec![], vec![]);
542 assert!(var.is_empty());
543 assert_eq!(var.len(), 0);
544 }
545 #[test]
546 fn test_get_variable() {
547 let mut file = NetCdfFile::new();
548 file.add_variable(NetCdfVariable::new("x", vec![], vec![1.0]));
549 file.add_variable(NetCdfVariable::new("y", vec![], vec![2.0]));
550 assert!(file.get_variable("x").is_some());
551 assert!(file.get_variable("z").is_none());
552 assert_eq!(file.get_variable("y").unwrap().data[0], 2.0);
553 }
554 #[test]
555 fn test_get_variable_mut() {
556 let mut file = NetCdfFile::new();
557 file.add_variable(NetCdfVariable::new("x", vec![], vec![1.0]));
558 file.get_variable_mut("x").unwrap().data.push(2.0);
559 assert_eq!(file.get_variable("x").unwrap().data.len(), 2);
560 }
561 #[test]
562 fn test_global_attribute() {
563 let mut file = NetCdfFile::new();
564 file.add_attribute("title", "test");
565 assert_eq!(file.get_attribute("title"), Some("test"));
566 assert_eq!(file.get_attribute("missing"), None);
567 }
568 #[test]
569 fn test_variable_names() {
570 let mut file = NetCdfFile::new();
571 file.add_variable(NetCdfVariable::new("a", vec![], vec![]));
572 file.add_variable(NetCdfVariable::new("b", vec![], vec![]));
573 let names = file.variable_names();
574 assert_eq!(names, vec!["a", "b"]);
575 }
576 #[test]
577 fn test_dimension_names() {
578 let mut file = NetCdfFile::new();
579 file.add_dimension("time", 10);
580 file.add_dimension("space", 3);
581 let names = file.dimension_names();
582 assert_eq!(names, vec!["time", "space"]);
583 }
584 #[test]
585 fn test_dimension_descriptor() {
586 let unlimited = NetCdfDimension {
587 name: "frame".into(),
588 size: None,
589 };
590 assert!(unlimited.is_unlimited());
591 assert_eq!(unlimited.effective_size(), 0);
592 let fixed = NetCdfDimension {
593 name: "atom".into(),
594 size: Some(100),
595 };
596 assert!(!fixed.is_unlimited());
597 assert_eq!(fixed.effective_size(), 100);
598 }
599 #[test]
600 fn test_amber_trajectory_creation() {
601 let n_atoms = 3;
602 let n_frames = 2;
603 let coords: Vec<f64> = (0..18).map(|i| i as f64).collect();
604 let time = vec![0.0, 1.0];
605 let cell_lengths = Some(vec![10.0, 10.0, 10.0, 10.0, 10.0, 10.0]);
606 let cell_angles = Some(vec![90.0, 90.0, 90.0, 90.0, 90.0, 90.0]);
607 let file =
608 create_amber_trajectory(n_atoms, n_frames, coords, time, cell_lengths, cell_angles);
609 assert_eq!(file.get_attribute("Conventions"), Some("AMBER"));
610 assert_eq!(file.get_dimension_size("atom"), Some(3));
611 assert_eq!(file.get_dimension_size("spatial"), Some(3));
612 assert!(file.is_unlimited_dimension("frame"));
613 assert!(file.get_variable("coordinates").is_some());
614 assert!(file.get_variable("time").is_some());
615 assert!(file.get_variable("cell_lengths").is_some());
616 assert!(file.get_variable("cell_angles").is_some());
617 }
618 #[test]
619 fn test_amber_trajectory_no_cell() {
620 let file = create_amber_trajectory(2, 1, vec![0.0; 6], vec![0.0], None, None);
621 assert!(file.get_variable("cell_lengths").is_none());
622 assert!(file.get_variable("cell_angles").is_none());
623 }
624 #[test]
625 fn test_extract_frame_coordinates() {
626 let coords = vec![
627 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
628 ];
629 let file = create_amber_trajectory(2, 2, coords, vec![0.0, 1.0], None, None);
630 let frame0 = extract_frame_coordinates(&file, 0).unwrap();
631 assert_eq!(frame0.len(), 2);
632 assert!((frame0[0][0] - 1.0).abs() < 1e-10);
633 assert!((frame0[1][2] - 6.0).abs() < 1e-10);
634 let frame1 = extract_frame_coordinates(&file, 1).unwrap();
635 assert!((frame1[0][0] - 7.0).abs() < 1e-10);
636 assert!(extract_frame_coordinates(&file, 5).is_none());
637 }
638 #[test]
639 fn test_coordinate_variable() {
640 let var = create_coordinate_variable("time", vec![0.0, 0.5, 1.0], "seconds");
641 assert_eq!(var.name, "time");
642 assert_eq!(var.dims, vec!["time"]);
643 assert_eq!(var.get_attribute("units"), Some("seconds"));
644 assert_eq!(var.data.len(), 3);
645 }
646 #[test]
647 fn test_reshape_2d() {
648 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
649 let matrix = reshape_2d(&data, 2, 3);
650 assert_eq!(matrix.len(), 2);
651 assert_eq!(matrix[0], vec![1.0, 2.0, 3.0]);
652 assert_eq!(matrix[1], vec![4.0, 5.0, 6.0]);
653 }
654 #[test]
655 fn test_reshape_2d_empty() {
656 let matrix = reshape_2d(&[], 0, 3);
657 assert!(matrix.is_empty());
658 }
659 #[test]
660 fn test_variable_stats() {
661 let var = NetCdfVariable::new("t", vec![], vec![1.0, 5.0, 3.0]);
662 let (min, max, mean) = variable_stats(&var).unwrap();
663 assert!((min - 1.0).abs() < 1e-10);
664 assert!((max - 5.0).abs() < 1e-10);
665 assert!((mean - 3.0).abs() < 1e-10);
666 }
667 #[test]
668 fn test_variable_stats_empty() {
669 let var = NetCdfVariable::new("empty", vec![], vec![]);
670 assert!(variable_stats(&var).is_none());
671 }
672 #[test]
673 fn test_unlimited_dim_roundtrip() {
674 let mut file = NetCdfFile::new();
675 file.add_unlimited_dimension("frame", 5);
676 file.add_dimension("atom", 3);
677 file.add_variable(NetCdfVariable::new(
678 "x",
679 vec!["frame".into(), "atom".into()],
680 vec![1.0; 15],
681 ));
682 let cdl = write_ncf_text(&file);
683 assert!(cdl.contains("UNLIMITED"));
684 let parsed = parse_ncf_text(&cdl).unwrap();
685 assert!(parsed.unlimited_dim.is_some());
686 assert_eq!(parsed.unlimited_dim.as_deref(), Some("frame"));
687 }
688 #[test]
689 fn test_variable_attributes_roundtrip() {
690 let mut file = NetCdfFile::new();
691 file.add_dimension("x", 3);
692 let mut var = NetCdfVariable::new("temp", vec!["x".into()], vec![1.0, 2.0, 3.0]);
693 var.add_attribute("units", "kelvin");
694 var.add_attribute("long_name", "temperature");
695 file.add_variable(var);
696 let cdl = write_ncf_text(&file);
697 let parsed = parse_ncf_text(&cdl).unwrap();
698 let parsed_var = parsed.get_variable("temp").unwrap();
699 assert_eq!(parsed_var.get_attribute("units"), Some("kelvin"));
700 assert_eq!(parsed_var.get_attribute("long_name"), Some("temperature"));
701 }
702 #[test]
703 fn test_amber_trajectory_roundtrip_cdl() {
704 let file = create_amber_trajectory(
705 2,
706 1,
707 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
708 vec![0.0],
709 None,
710 None,
711 );
712 let cdl = write_ncf_text(&file);
713 let parsed = parse_ncf_text(&cdl).unwrap();
714 assert_eq!(parsed.get_attribute("Conventions"), Some("AMBER"));
715 assert!(parsed.get_variable("coordinates").is_some());
716 }
717 #[test]
718 fn test_full_amber_with_cell() {
719 let file = create_amber_trajectory(
720 1,
721 2,
722 vec![0.0; 6],
723 vec![0.0, 0.5],
724 Some(vec![10.0, 10.0, 10.0, 10.0, 10.0, 10.0]),
725 Some(vec![90.0, 90.0, 90.0, 90.0, 90.0, 90.0]),
726 );
727 let coords_var = file.get_variable("coordinates").unwrap();
728 assert_eq!(coords_var.get_attribute("units"), Some("angstrom"));
729 let time_var = file.get_variable("time").unwrap();
730 assert_eq!(time_var.get_attribute("units"), Some("picosecond"));
731 }
732 #[test]
733 fn test_nc4_data_type_byte_width() {
734 assert_eq!(Nc4DataType::Float64.byte_width(), 8);
735 assert_eq!(Nc4DataType::Float32.byte_width(), 4);
736 assert_eq!(Nc4DataType::Int32.byte_width(), 4);
737 assert_eq!(Nc4DataType::UInt8.byte_width(), 1);
738 assert_eq!(Nc4DataType::String.byte_width(), 0);
739 }
740 #[test]
741 fn test_nc4_data_type_name() {
742 assert_eq!(Nc4DataType::Float64.type_name(), "double");
743 assert_eq!(Nc4DataType::String.type_name(), "string");
744 assert_eq!(Nc4DataType::Int32.type_name(), "int");
745 }
746 #[test]
747 fn test_nc4_variable_float64() {
748 let var = Nc4Variable::float64("temp", vec!["time".into()], vec![1.0, 2.0, 3.0]);
749 assert_eq!(var.data_type, Nc4DataType::Float64);
750 assert_eq!(var.len(), 3);
751 assert!(!var.is_empty());
752 }
753 #[test]
754 fn test_nc4_variable_string() {
755 let var = Nc4Variable::string_var(
756 "names",
757 vec!["n".into()],
758 vec!["ALA".to_string(), "GLY".to_string()],
759 );
760 assert_eq!(var.data_type, Nc4DataType::String);
761 assert_eq!(var.string_data.len(), 2);
762 assert_eq!(var.string_data[0], "ALA");
763 assert_eq!(var.len(), 2);
764 }
765 #[test]
766 fn test_nc4_variable_int32() {
767 let var = Nc4Variable::int32("step", vec!["frame".into()], vec![0, 100, 200]);
768 assert_eq!(var.data_type, Nc4DataType::Int32);
769 assert_eq!(var.int_data.len(), 3);
770 assert_eq!(var.int_data[1], 100);
771 }
772 #[test]
773 fn test_nc4_variable_attributes() {
774 let mut var = Nc4Variable::float64("temp", vec![], vec![]);
775 var.add_attribute("units", "K");
776 var.add_attribute("long_name", "temperature");
777 assert_eq!(var.get_attribute("units"), Some("K"));
778 assert_eq!(var.get_attribute("missing"), None);
779 }
780 #[test]
781 fn test_nc4_group_add_variable() {
782 let mut group = Nc4Group::new("trajectory");
783 group.add_variable(Nc4Variable::float64("x", vec![], vec![1.0, 2.0]));
784 group.add_variable(Nc4Variable::float64("y", vec![], vec![3.0, 4.0]));
785 assert_eq!(group.variables.len(), 2);
786 let xv = group.get_variable("x").unwrap();
787 assert!((xv.float_data[0] - 1.0).abs() < 1e-10);
788 }
789 #[test]
790 fn test_nc4_group_add_subgroup() {
791 let mut root = Nc4Group::new("/");
792 let mut child = Nc4Group::new("data");
793 child.add_variable(Nc4Variable::float64("t", vec![], vec![0.0]));
794 root.add_subgroup(child);
795 assert_eq!(root.subgroups.len(), 1);
796 assert_eq!(root.subgroups[0].name, "data");
797 }
798 #[test]
799 fn test_nc4_group_total_variable_count() {
800 let mut root = Nc4Group::new("/");
801 root.add_variable(Nc4Variable::float64("a", vec![], vec![]));
802 let mut child = Nc4Group::new("child");
803 child.add_variable(Nc4Variable::float64("b", vec![], vec![]));
804 child.add_variable(Nc4Variable::float64("c", vec![], vec![]));
805 root.add_subgroup(child);
806 assert_eq!(root.total_variable_count(), 3);
807 }
808 #[test]
809 fn test_nc4_group_all_variables() {
810 let mut root = Nc4Group::new("/");
811 root.add_variable(Nc4Variable::float64("a", vec![], vec![]));
812 let mut child = Nc4Group::new("child");
813 child.add_variable(Nc4Variable::float64("b", vec![], vec![]));
814 root.add_subgroup(child);
815 let all = root.all_variables();
816 assert_eq!(all.len(), 2);
817 }
818 #[test]
819 fn test_nc4_group_attributes() {
820 let mut group = Nc4Group::new("meta");
821 group.add_attribute("author", "test");
822 assert_eq!(group.get_attribute("author"), Some("test"));
823 assert_eq!(group.get_attribute("missing"), None);
824 }
825 #[test]
826 fn test_nc4_file_add_dimension() {
827 let mut f = Nc4File::new();
828 f.add_dimension("atom", 100);
829 f.add_unlimited_dimension("frame");
830 assert_eq!(f.get_dimension_size("atom"), Some(100));
831 assert_eq!(f.get_dimension_size("frame"), None);
832 }
833 #[test]
834 fn test_nc4_file_extend_unlimited() {
835 let mut f = Nc4File::new();
836 f.add_unlimited_dimension("frame");
837 f.extend_unlimited();
838 f.extend_unlimited();
839 assert_eq!(f.unlimited_size, 2);
840 }
841 #[test]
842 fn test_nc4_file_global_attributes() {
843 let mut f = Nc4File::new();
844 f.add_global_attribute("Conventions", "CF-1.8");
845 f.add_global_attribute("title", "Test");
846 assert_eq!(f.get_global_attribute("Conventions"), Some("CF-1.8"));
847 assert_eq!(f.get_global_attribute("missing"), None);
848 }
849 #[test]
850 fn test_nc4_file_add_get_variable() {
851 let mut f = Nc4File::new();
852 f.add_variable(Nc4Variable::float64(
853 "pressure",
854 vec!["time".into()],
855 vec![101.3, 100.5],
856 ));
857 let v = f.get_variable("pressure").unwrap();
858 assert_eq!(v.float_data.len(), 2);
859 assert!(f.get_variable("missing").is_none());
860 }
861 #[test]
862 fn test_nc4_file_add_group() {
863 let mut f = Nc4File::new();
864 let mut g = Nc4Group::new("simdata");
865 g.add_variable(Nc4Variable::int32("step", vec![], vec![0, 1, 2]));
866 f.add_group(g);
867 let retrieved = f.get_group("simdata").unwrap();
868 assert_eq!(retrieved.variables.len(), 1);
869 assert!(f.get_group("missing").is_none());
870 }
871 #[test]
872 fn test_nc4_file_total_variable_count() {
873 let mut f = Nc4File::new();
874 f.add_variable(Nc4Variable::float64("a", vec![], vec![]));
875 let mut g = Nc4Group::new("sub");
876 g.add_variable(Nc4Variable::float64("b", vec![], vec![]));
877 f.add_group(g);
878 assert_eq!(f.total_variable_count(), 2);
879 }
880 #[test]
881 fn test_nc4_file_to_bytes_non_empty() {
882 let mut f = Nc4File::new();
883 f.add_global_attribute("test", "value");
884 f.add_variable(Nc4Variable::float64("x", vec![], vec![1.0, 2.0, 3.0]));
885 let bytes = f.to_bytes();
886 assert!(!bytes.is_empty());
887 assert_eq!(&bytes[0..5], b"OXNC4");
888 }
889 #[test]
890 fn test_create_string_variable() {
891 let var = create_string_variable(
892 "residue_name",
893 vec!["atom".into()],
894 vec!["ALA".to_string(), "GLY".to_string(), "PRO".to_string()],
895 );
896 assert_eq!(var.name, "residue_name");
897 assert_eq!(var.string_data.len(), 3);
898 assert_eq!(var.string_data[1], "GLY");
899 assert_eq!(var.get_attribute("dtype"), Some("string"));
900 }
901 #[test]
902 fn test_inherit_global_attributes() {
903 let mut source = NetCdfFile::new();
904 source.add_attribute("Conventions", "AMBER");
905 source.add_attribute("program", "OxiPhysics");
906 let mut target = NetCdfFile::new();
907 let mut var = NetCdfVariable::new("x", vec![], vec![1.0]);
908 var.add_attribute("Conventions", "CF");
909 target.add_variable(var);
910 inherit_global_attributes(&source, &mut target);
911 let xv = target.get_variable("x").unwrap();
912 assert_eq!(xv.get_attribute("Conventions"), Some("CF"));
913 assert_eq!(xv.get_attribute("program"), Some("OxiPhysics"));
914 }
915 #[test]
916 fn test_merge_netcdf_files() {
917 let mut base = NetCdfFile::new();
918 base.add_dimension("time", 3);
919 base.add_variable(NetCdfVariable::new(
920 "a",
921 vec!["time".into()],
922 vec![1.0, 2.0, 3.0],
923 ));
924 base.add_variable(NetCdfVariable::new(
925 "b",
926 vec!["time".into()],
927 vec![4.0, 5.0, 6.0],
928 ));
929 base.add_attribute("source", "base");
930 let mut overlay = NetCdfFile::new();
931 overlay.add_variable(NetCdfVariable::new(
932 "a",
933 vec!["time".into()],
934 vec![10.0, 20.0, 30.0],
935 ));
936 overlay.add_variable(NetCdfVariable::new("c", vec![], vec![99.0]));
937 overlay.add_attribute("source", "overlay");
938 let merged = merge_netcdf_files(&base, &overlay);
939 assert_eq!(merged.variable_names().len(), 3);
940 let av = merged.get_variable("a").unwrap();
941 assert!((av.data[0] - 10.0).abs() < 1e-10);
942 let bv = merged.get_variable("b").unwrap();
943 assert!((bv.data[0] - 4.0).abs() < 1e-10);
944 assert_eq!(merged.get_attribute("source"), Some("overlay"));
945 }
946 #[test]
947 fn test_netcdf_file_new_empty() {
948 let f = NetcdfFile::new();
949 assert!(f.dimensions.is_empty());
950 assert!(f.variables.is_empty());
951 assert!(f.global_attrs.is_empty());
952 }
953 #[test]
954 fn test_netcdf_file_add_dimension() {
955 let mut f = NetcdfFile::new();
956 f.add_dimension("time", 10);
957 f.add_dimension("space", 3);
958 assert_eq!(f.dimensions.get("time"), Some(&10));
959 assert_eq!(f.dimensions.get("space"), Some(&3));
960 }
961 #[test]
962 fn test_netcdf_file_add_variable_increases_count() {
963 let mut f = NetcdfFile::new();
964 assert_eq!(f.variables.len(), 0);
965 f.add_variable(NetcdfVariable {
966 name: "temp".to_string(),
967 dimensions: vec!["time".to_string()],
968 data: vec![1.0, 2.0, 3.0],
969 units: "K".to_string(),
970 long_name: "temperature".to_string(),
971 });
972 assert_eq!(f.variables.len(), 1);
973 f.add_variable(NetcdfVariable {
974 name: "pressure".to_string(),
975 dimensions: vec!["time".to_string()],
976 data: vec![100.0],
977 units: "Pa".to_string(),
978 long_name: "pressure".to_string(),
979 });
980 assert_eq!(f.variables.len(), 2);
981 }
982 #[test]
983 fn test_netcdf_file_add_global_attr() {
984 let mut f = NetcdfFile::new();
985 f.add_global_attr("Conventions", "CF-1.8");
986 f.add_global_attr("title", "Test");
987 assert_eq!(f.global_attrs.len(), 2);
988 assert!(
989 f.global_attrs
990 .iter()
991 .any(|(k, v)| k == "Conventions" && v == "CF-1.8")
992 );
993 }
994 #[test]
995 fn test_netcdf_file_write_cdl_contains_dimension_name() {
996 let mut f = NetcdfFile::new();
997 f.add_dimension("latitude", 180);
998 f.add_dimension("longitude", 360);
999 let cdl = f.write_cdl();
1000 assert!(cdl.contains("latitude"), "CDL missing dimension 'latitude'");
1001 assert!(cdl.contains("180"), "CDL missing dimension size 180");
1002 assert!(cdl.contains("longitude"));
1003 assert!(cdl.contains("360"));
1004 }
1005 #[test]
1006 fn test_netcdf_file_write_cdl_contains_variable() {
1007 let mut f = NetcdfFile::new();
1008 f.add_dimension("t", 5);
1009 f.add_variable(NetcdfVariable {
1010 name: "velocity".to_string(),
1011 dimensions: vec!["t".to_string()],
1012 data: vec![1.0, 2.0, 3.0, 4.0, 5.0],
1013 units: "m/s".to_string(),
1014 long_name: "wind velocity".to_string(),
1015 });
1016 let cdl = f.write_cdl();
1017 assert!(cdl.contains("velocity"));
1018 assert!(cdl.contains("m/s"));
1019 assert!(cdl.contains("wind velocity"));
1020 }
1021 #[test]
1022 fn test_parse_cdl_dimensions() {
1023 let cdl = "netcdf test {\ndimensions:\n\ttime = 10 ;\n\tatom = 100 ;\nvariables:\n}\n";
1024 let dims = parse_cdl_dimensions(cdl);
1025 assert_eq!(dims.len(), 2, "expected 2 dimensions, got {:?}", dims);
1026 assert!(dims.iter().any(|(n, s)| n == "time" && *s == 10));
1027 assert!(dims.iter().any(|(n, s)| n == "atom" && *s == 100));
1028 }
1029 #[test]
1030 fn test_trajectory_stats_n_frames() {
1031 let times = vec![0.0, 0.5, 1.0, 1.5, 2.0];
1032 let positions: Vec<Vec<[f64; 3]>> = times.iter().map(|_| vec![[0.0, 0.0, 0.0]]).collect();
1033 let (_total_time, _avg_disp, n_frames) = trajectory_stats(×, &positions);
1034 assert_eq!(n_frames, 5);
1035 }
1036 #[test]
1037 fn test_trajectory_stats_total_time() {
1038 let times = vec![0.0, 1.0, 2.0];
1039 let positions: Vec<Vec<[f64; 3]>> = times.iter().map(|_| vec![[0.0, 0.0, 0.0]]).collect();
1040 let (total_time, _, _) = trajectory_stats(×, &positions);
1041 assert!((total_time - 2.0).abs() < 1e-10);
1042 }
1043 #[test]
1044 fn test_trajectory_stats_avg_displacement() {
1045 let times = vec![0.0, 1.0];
1046 let positions = vec![
1047 vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
1048 vec![[3.0, 4.0, 0.0], [1.0, 0.0, 0.0]],
1049 ];
1050 let (_total_time, avg_disp, _) = trajectory_stats(×, &positions);
1051 assert!((avg_disp - 2.5).abs() < 1e-10, "avg_disp={}", avg_disp);
1052 }
1053 #[test]
1054 fn test_trajectory_write_creates_file() {
1055 let times = vec![0.0, 0.5, 1.0];
1056 let positions = vec![
1057 vec![[1.0, 2.0, 3.0]],
1058 vec![[1.1, 2.1, 3.1]],
1059 vec![[1.2, 2.2, 3.2]],
1060 ];
1061 let path = "/tmp/test_trajectory.cdl";
1062 NetcdfFile::trajectory_write(path, ×, &positions).unwrap();
1063 let content = std::fs::read_to_string(path).unwrap();
1064 assert!(content.contains("frame"), "missing 'frame' dimension");
1065 assert!(content.contains("atom"));
1066 assert!(content.contains("coordinates"));
1067 assert!(content.contains("time"));
1068 }
1069}
1070#[allow(dead_code)]
1075pub fn write_particle_trajectory_cdl(
1076 times: &[f64],
1077 positions: &[Vec<[f64; 3]>],
1078 units_time: &str,
1079 units_pos: &str,
1080) -> String {
1081 let n_frames = times.len();
1082 let n_atoms = positions.first().map(|f| f.len()).unwrap_or(0);
1083 let mut w = NetcdfWriter::new("trajectory");
1084 w.add_unlimited_dimension("time", n_frames);
1085 w.add_dimension("atom", n_atoms);
1086 w.add_dimension("spatial", 3);
1087 w.add_global_attribute("Conventions", "OxiPhysics");
1088 w.add_global_attribute("n_frames", &format!("{}", n_frames));
1089 w.add_global_attribute("n_atoms", &format!("{}", n_atoms));
1090 w.add_coordinate("time", times.to_vec(), units_time);
1091 let x_data: Vec<f64> = positions
1092 .iter()
1093 .flat_map(|f| f.iter().map(|p| p[0]))
1094 .collect();
1095 let y_data: Vec<f64> = positions
1096 .iter()
1097 .flat_map(|f| f.iter().map(|p| p[1]))
1098 .collect();
1099 let z_data: Vec<f64> = positions
1100 .iter()
1101 .flat_map(|f| f.iter().map(|p| p[2]))
1102 .collect();
1103 w.add_variable("x", &["time", "atom"], x_data);
1104 w.add_variable_attribute("units", units_pos);
1105 w.add_variable_attribute("long_name", "x coordinate");
1106 w.add_variable("y", &["time", "atom"], y_data);
1107 w.add_variable_attribute("units", units_pos);
1108 w.add_variable_attribute("long_name", "y coordinate");
1109 w.add_variable("z", &["time", "atom"], z_data);
1110 w.add_variable_attribute("units", units_pos);
1111 w.add_variable_attribute("long_name", "z coordinate");
1112 w.finish()
1113}
1114#[allow(dead_code)]
1118pub fn read_particle_trajectory_cdl(cdl: &str) -> Result<(Vec<f64>, Vec<Vec<[f64; 3]>>), String> {
1119 let reader = NetcdfReader::from_cdl(cdl)?;
1120 let times = reader
1121 .get_data("time")
1122 .ok_or("missing 'time' variable")?
1123 .to_vec();
1124 let n_frames = times.len();
1125 let n_atoms = reader.get_dimension("atom").unwrap_or(0);
1126 let x_data = reader.get_data("x").ok_or("missing 'x' variable")?.to_vec();
1127 let y_data = reader.get_data("y").ok_or("missing 'y' variable")?.to_vec();
1128 let z_data = reader.get_data("z").ok_or("missing 'z' variable")?.to_vec();
1129 let mut positions = Vec::with_capacity(n_frames);
1130 for frame in 0..n_frames {
1131 let mut frame_pos = Vec::with_capacity(n_atoms);
1132 for atom in 0..n_atoms {
1133 let idx = frame * n_atoms + atom;
1134 let x = x_data.get(idx).copied().unwrap_or(0.0);
1135 let y = y_data.get(idx).copied().unwrap_or(0.0);
1136 let z = z_data.get(idx).copied().unwrap_or(0.0);
1137 frame_pos.push([x, y, z]);
1138 }
1139 positions.push(frame_pos);
1140 }
1141 Ok((times, positions))
1142}
1143#[allow(dead_code)]
1145pub fn linspace_time(start: f64, step: f64, n: usize) -> Vec<f64> {
1146 (0..n).map(|i| start + i as f64 * step).collect()
1147}
1148#[allow(dead_code)]
1150pub fn uniform_grid_coords(
1151 nx: usize,
1152 ny: usize,
1153 nz: usize,
1154 origin: [f64; 3],
1155 spacing: [f64; 3],
1156) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
1157 let x: Vec<f64> = (0..nx).map(|i| origin[0] + i as f64 * spacing[0]).collect();
1158 let y: Vec<f64> = (0..ny).map(|j| origin[1] + j as f64 * spacing[1]).collect();
1159 let z: Vec<f64> = (0..nz).map(|k| origin[2] + k as f64 * spacing[2]).collect();
1160 (x, y, z)
1161}
1162#[allow(dead_code)]
1164pub fn cf_global_attributes(title: &str, institution: &str, source: &str) -> Vec<(String, String)> {
1165 vec![
1166 ("Conventions".to_string(), "CF-1.8".to_string()),
1167 ("title".to_string(), title.to_string()),
1168 ("institution".to_string(), institution.to_string()),
1169 ("source".to_string(), source.to_string()),
1170 ("history".to_string(), "Created by OxiPhysics".to_string()),
1171 ]
1172}
1173#[allow(dead_code)]
1175pub fn apply_cf_conventions(file: &mut NetCdfFile, title: &str) {
1176 file.add_attribute("Conventions", "CF-1.8");
1177 file.add_attribute("title", title);
1178 file.add_attribute("institution", "OxiPhysics");
1179}
1180#[allow(dead_code)]
1182pub fn extended_variable_stats(var: &NetCdfVariable) -> Option<NetcdfVariableStats> {
1183 if var.data.is_empty() {
1184 return None;
1185 }
1186 let n = var.data.len();
1187 let mut min = f64::INFINITY;
1188 let mut max = f64::NEG_INFINITY;
1189 let mut sum = 0.0;
1190 for &v in &var.data {
1191 if v < min {
1192 min = v;
1193 }
1194 if v > max {
1195 max = v;
1196 }
1197 sum += v;
1198 }
1199 let mean = sum / n as f64;
1200 let var_sq = var.data.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / n as f64;
1201 Some(NetcdfVariableStats {
1202 name: var.name.clone(),
1203 min,
1204 max,
1205 mean,
1206 std_dev: var_sq.sqrt(),
1207 count: n,
1208 })
1209}
1210#[cfg(test)]
1211mod tests_netcdf_new {
1212 use super::*;
1213
1214 #[test]
1215 fn writer_basic_cdl() {
1216 let mut w = NetcdfWriter::new("test");
1217 w.add_dimension("time", 3);
1218 w.add_variable("temperature", &["time"], vec![300.0, 301.0, 302.0]);
1219 w.add_variable_attribute("units", "K");
1220 let cdl = w.finish();
1221 assert!(cdl.starts_with("netcdf test {"));
1222 assert!(cdl.contains("time = 3"));
1223 assert!(cdl.contains("double temperature(time)"));
1224 assert!(cdl.contains("temperature:units = \"K\""));
1225 assert!(cdl.contains("300"));
1226 }
1227 #[test]
1228 fn writer_unlimited_dimension() {
1229 let mut w = NetcdfWriter::new("sim");
1230 w.add_unlimited_dimension("frame", 5);
1231 w.add_dimension("atom", 10);
1232 let cdl = w.to_cdl();
1233 assert!(cdl.contains("UNLIMITED"));
1234 assert!(cdl.contains("5 currently"));
1235 }
1236 #[test]
1237 fn writer_global_attributes() {
1238 let mut w = NetcdfWriter::new("data");
1239 w.add_global_attribute("Conventions", "CF-1.8");
1240 w.add_global_attribute("title", "Test");
1241 let cdl = w.to_cdl();
1242 assert!(cdl.contains(":Conventions = \"CF-1.8\""));
1243 assert!(cdl.contains(":title = \"Test\""));
1244 }
1245 #[test]
1246 fn writer_coordinate_variable() {
1247 let mut w = NetcdfWriter::new("grid");
1248 w.add_dimension("x", 4);
1249 w.add_coordinate("x", vec![0.0, 1.0, 2.0, 3.0], "m");
1250 let cdl = w.to_cdl();
1251 assert!(cdl.contains("x:units = \"m\""));
1252 assert!(cdl.contains("0, 1, 2, 3"));
1253 }
1254 #[test]
1255 fn writer_n_dimensions_and_variables() {
1256 let mut w = NetcdfWriter::new("d");
1257 w.add_dimension("a", 1);
1258 w.add_dimension("b", 2);
1259 w.add_variable("v", &["a"], vec![1.0]);
1260 assert_eq!(w.n_dimensions(), 2);
1261 assert_eq!(w.n_variables(), 1);
1262 }
1263 #[test]
1264 fn writer_write_to_file() {
1265 let mut w = NetcdfWriter::new("out");
1266 w.add_dimension("t", 2);
1267 w.add_variable("time", &["t"], vec![0.0, 1.0]);
1268 let path = "/tmp/test_netcdf_writer.cdl";
1269 w.write_to_file(path).unwrap();
1270 let content = std::fs::read_to_string(path).unwrap();
1271 assert!(content.contains("netcdf out"));
1272 assert!(content.contains("time = 0, 1"));
1273 }
1274 #[test]
1275 fn reader_from_cdl_roundtrip() {
1276 let mut w = NetcdfWriter::new("rt");
1277 w.add_dimension("n", 3);
1278 w.add_variable("pressure", &["n"], vec![100.0, 101.0, 102.0]);
1279 w.add_variable_attribute("units", "Pa");
1280 w.add_global_attribute("source", "test");
1281 let cdl = w.finish();
1282 let reader = NetcdfReader::from_cdl(&cdl).unwrap();
1283 assert_eq!(reader.get_dimension("n"), Some(3));
1284 let p = reader.get_data("pressure").unwrap();
1285 assert_eq!(p.len(), 3);
1286 assert!((p[1] - 101.0).abs() < 1e-9);
1287 }
1288 #[test]
1289 fn reader_variable_names() {
1290 let mut w = NetcdfWriter::new("x");
1291 w.add_dimension("k", 2);
1292 w.add_variable("alpha", &["k"], vec![1.0, 2.0]);
1293 w.add_variable("beta", &["k"], vec![3.0, 4.0]);
1294 let cdl = w.finish();
1295 let reader = NetcdfReader::from_cdl(&cdl).unwrap();
1296 let names = reader.variable_names();
1297 assert!(names.contains(&"alpha"));
1298 assert!(names.contains(&"beta"));
1299 }
1300 #[test]
1301 fn reader_missing_variable_returns_none() {
1302 let w = NetcdfWriter::new("empty");
1303 let cdl = w.finish();
1304 let reader = NetcdfReader::from_cdl(&cdl).unwrap();
1305 assert!(reader.get_variable("nonexistent").is_none());
1306 assert!(reader.get_data("nonexistent").is_none());
1307 }
1308 #[test]
1309 fn particle_trajectory_write_read_roundtrip() {
1310 let times = vec![0.0, 1.0, 2.0];
1311 let positions = vec![
1312 vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
1313 vec![[1.1, 2.1, 3.1], [4.1, 5.1, 6.1]],
1314 vec![[1.2, 2.2, 3.2], [4.2, 5.2, 6.2]],
1315 ];
1316 let cdl = write_particle_trajectory_cdl(×, &positions, "ps", "angstrom");
1317 assert!(cdl.contains("n_frames"));
1318 assert!(cdl.contains("n_atoms"));
1319 assert!(cdl.contains("OxiPhysics"));
1320 let (rt_times, rt_pos) = read_particle_trajectory_cdl(&cdl).unwrap();
1321 assert_eq!(rt_times.len(), 3);
1322 assert!((rt_times[2] - 2.0).abs() < 1e-9);
1323 assert_eq!(rt_pos.len(), 3);
1324 assert_eq!(rt_pos[0].len(), 2);
1325 assert!((rt_pos[1][0][0] - 1.1).abs() < 1e-9);
1326 assert!((rt_pos[2][1][2] - 6.2).abs() < 1e-9);
1327 }
1328 #[test]
1329 fn particle_trajectory_empty() {
1330 let cdl = write_particle_trajectory_cdl(&[], &[], "ps", "nm");
1331 assert!(cdl.contains("netcdf trajectory"));
1332 assert!(cdl.contains("dimensions:"));
1333 assert!(cdl.contains("variables:"));
1334 }
1335 #[test]
1336 fn linspace_time_basic() {
1337 let t = linspace_time(0.0, 0.5, 5);
1338 assert_eq!(t.len(), 5);
1339 assert!((t[0] - 0.0).abs() < 1e-12);
1340 assert!((t[4] - 2.0).abs() < 1e-12);
1341 }
1342 #[test]
1343 fn linspace_time_single() {
1344 let t = linspace_time(3.0, 1.0, 1);
1345 assert_eq!(t, vec![3.0]);
1346 }
1347 #[test]
1348 fn uniform_grid_coords_sizes() {
1349 let (x, y, z) = uniform_grid_coords(4, 5, 6, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1350 assert_eq!(x.len(), 4);
1351 assert_eq!(y.len(), 5);
1352 assert_eq!(z.len(), 6);
1353 }
1354 #[test]
1355 fn uniform_grid_coords_values() {
1356 let (x, _y, _z) = uniform_grid_coords(3, 1, 1, [1.0, 0.0, 0.0], [0.5, 1.0, 1.0]);
1357 assert!((x[0] - 1.0).abs() < 1e-12);
1358 assert!((x[1] - 1.5).abs() < 1e-12);
1359 assert!((x[2] - 2.0).abs() < 1e-12);
1360 }
1361 #[test]
1362 fn cf_attrs_content() {
1363 let attrs = cf_global_attributes("My Title", "ACME Corp", "OxiPhysics 1.0");
1364 assert!(
1365 attrs
1366 .iter()
1367 .any(|(k, v)| k == "Conventions" && v.contains("CF"))
1368 );
1369 assert!(attrs.iter().any(|(k, _)| k == "title"));
1370 assert!(attrs.iter().any(|(k, _)| k == "institution"));
1371 }
1372 #[test]
1373 fn apply_cf_adds_attributes() {
1374 let mut file = NetCdfFile::new();
1375 apply_cf_conventions(&mut file, "My Dataset");
1376 assert_eq!(file.get_attribute("Conventions"), Some("CF-1.8"));
1377 assert_eq!(file.get_attribute("title"), Some("My Dataset"));
1378 }
1379 #[test]
1380 fn extended_stats_basic() {
1381 let var = NetCdfVariable::new("v", vec![], vec![1.0, 2.0, 3.0, 4.0, 5.0]);
1382 let s = extended_variable_stats(&var).unwrap();
1383 assert_eq!(s.count, 5);
1384 assert!((s.min - 1.0).abs() < 1e-12);
1385 assert!((s.max - 5.0).abs() < 1e-12);
1386 assert!((s.mean - 3.0).abs() < 1e-12);
1387 assert!((s.std_dev - 2.0f64.sqrt()).abs() < 1e-9);
1388 }
1389 #[test]
1390 fn extended_stats_range() {
1391 let var = NetCdfVariable::new("v", vec![], vec![-5.0, 5.0]);
1392 let s = extended_variable_stats(&var).unwrap();
1393 assert!((s.range() - 10.0).abs() < 1e-12);
1394 }
1395 #[test]
1396 fn extended_stats_empty() {
1397 let var = NetCdfVariable::new("empty", vec![], vec![]);
1398 assert!(extended_variable_stats(&var).is_none());
1399 }
1400 #[test]
1401 fn extended_stats_cv_zero_mean() {
1402 let var = NetCdfVariable::new("z", vec![], vec![0.0, 0.0, 0.0]);
1403 let s = extended_variable_stats(&var).unwrap();
1404 assert!((s.cv() - 0.0).abs() < f64::EPSILON * 10.0);
1405 }
1406 #[test]
1407 fn dimension_unlimited_check() {
1408 let d_fixed = NetCdfDimension {
1409 name: "x".to_string(),
1410 size: Some(10),
1411 };
1412 let d_unlim = NetCdfDimension {
1413 name: "t".to_string(),
1414 size: None,
1415 };
1416 assert!(!d_fixed.is_unlimited());
1417 assert!(d_unlim.is_unlimited());
1418 assert_eq!(d_fixed.effective_size(), 10);
1419 assert_eq!(d_unlim.effective_size(), 0);
1420 }
1421 #[test]
1422 fn merge_netcdf_files_basic() {
1423 let mut base = NetCdfFile::new();
1424 base.add_variable(NetCdfVariable::new("a", vec![], vec![1.0]));
1425 base.add_attribute("source", "base");
1426 let mut overlay = NetCdfFile::new();
1427 overlay.add_variable(NetCdfVariable::new("b", vec![], vec![2.0]));
1428 overlay.add_attribute("source", "overlay");
1429 let merged = merge_netcdf_files(&base, &overlay);
1430 assert!(merged.get_variable("a").is_some());
1431 assert!(merged.get_variable("b").is_some());
1432 assert_eq!(merged.get_attribute("source"), Some("overlay"));
1433 }
1434 #[test]
1435 fn inherit_global_attributes_basic() {
1436 let mut source = NetCdfFile::new();
1437 source.add_attribute("units", "SI");
1438 let mut target = NetCdfFile::new();
1439 target.add_variable(NetCdfVariable::new("T", vec![], vec![300.0]));
1440 inherit_global_attributes(&source, &mut target);
1441 let attr = target.get_variable("T").unwrap().get_attribute("units");
1442 assert_eq!(attr, Some("SI"));
1443 }
1444 #[test]
1445 fn reshape_2d_basic() {
1446 let data: Vec<f64> = (1..=6).map(|i| i as f64).collect();
1447 let r = reshape_2d(&data, 2, 3);
1448 assert_eq!(r.len(), 2);
1449 assert_eq!(r[0], vec![1.0, 2.0, 3.0]);
1450 assert_eq!(r[1], vec![4.0, 5.0, 6.0]);
1451 }
1452 #[test]
1453 fn variable_stats_basic() {
1454 let var = NetCdfVariable::new("v", vec![], vec![2.0, 4.0, 6.0]);
1455 let (min, max, mean) = variable_stats(&var).unwrap();
1456 assert!((min - 2.0).abs() < 1e-12);
1457 assert!((max - 6.0).abs() < 1e-12);
1458 assert!((mean - 4.0).abs() < 1e-12);
1459 }
1460 #[test]
1461 fn variable_stats_empty() {
1462 let var = NetCdfVariable::new("empty", vec![], vec![]);
1463 assert!(variable_stats(&var).is_none());
1464 }
1465 #[test]
1466 fn amber_trajectory_structure() {
1467 let n_atoms = 5;
1468 let n_frames = 3;
1469 let coords: Vec<f64> = (0..n_atoms * n_frames * 3).map(|i| i as f64).collect();
1470 let times = vec![0.0, 1.0, 2.0];
1471 let file = create_amber_trajectory(n_atoms, n_frames, coords, times, None, None);
1472 assert_eq!(file.get_dimension_size("atom"), Some(n_atoms));
1473 assert_eq!(file.get_dimension_size("spatial"), Some(3));
1474 assert_eq!(file.get_attribute("Conventions"), Some("AMBER"));
1475 let coords_var = file.get_variable("coordinates").unwrap();
1476 assert_eq!(coords_var.data.len(), n_atoms * n_frames * 3);
1477 }
1478 #[test]
1479 fn amber_extract_frame_coordinates() {
1480 let n_atoms = 2;
1481 let n_frames = 2;
1482 let coords = vec![
1483 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
1484 ];
1485 let file = create_amber_trajectory(n_atoms, n_frames, coords, vec![0.0, 1.0], None, None);
1486 let frame0 = extract_frame_coordinates(&file, 0).unwrap();
1487 assert_eq!(frame0.len(), 2);
1488 assert!((frame0[0][0] - 1.0).abs() < 1e-9);
1489 assert!((frame0[1][2] - 6.0).abs() < 1e-9);
1490 let frame1 = extract_frame_coordinates(&file, 1).unwrap();
1491 assert!((frame1[0][0] - 7.0).abs() < 1e-9);
1492 }
1493 #[test]
1494 fn amber_extract_out_of_range() {
1495 let file = create_amber_trajectory(2, 1, vec![0.0; 6], vec![0.0], None, None);
1496 assert!(extract_frame_coordinates(&file, 99).is_none());
1497 }
1498 #[test]
1499 fn coordinate_variable_units_attribute() {
1500 let var = create_coordinate_variable("time", vec![0.0, 1.0, 2.0], "ps");
1501 assert_eq!(var.get_attribute("units"), Some("ps"));
1502 assert_eq!(var.dims, vec!["time"]);
1503 }
1504 #[test]
1505 fn nc4_file_to_bytes_contains_magic() {
1506 let mut f = Nc4File::new();
1507 f.add_global_attribute("title", "Test");
1508 f.add_dimension("x", 10);
1509 let bytes = f.to_bytes();
1510 assert_eq!(&bytes[..5], b"OXNC4");
1511 }
1512 #[test]
1513 fn nc4_file_unlimited_extend() {
1514 let mut f = Nc4File::new();
1515 f.add_unlimited_dimension("time");
1516 f.extend_unlimited();
1517 f.extend_unlimited();
1518 assert_eq!(f.unlimited_size, 2);
1519 assert_eq!(f.get_dimension_size("time"), Some(2));
1520 }
1521 #[test]
1522 fn nc4_group_hierarchy() {
1523 let mut root = Nc4Group::new("root");
1524 let mut child = Nc4Group::new("child");
1525 child.add_variable(Nc4Variable::float64("v", vec![], vec![1.0, 2.0]));
1526 root.add_subgroup(child);
1527 assert_eq!(root.total_variable_count(), 1);
1528 let all = root.all_variables();
1529 assert_eq!(all.len(), 1);
1530 assert_eq!(all[0].name, "v");
1531 }
1532}
1533#[cfg(test)]
1534#[allow(dead_code)]
1535mod tests_netcdf_extra {
1536 use super::*;
1537
1538 #[test]
1539 fn ncdf_variable_new_empty_data() {
1540 let var = NetCdfVariable::new("pressure", vec!["time".to_string()], vec![]);
1541 assert!(var.is_empty());
1542 assert_eq!(var.len(), 0);
1543 }
1544 #[test]
1545 fn ncdf_variable_add_and_get_attribute() {
1546 let mut var = NetCdfVariable::new("temp", vec![], vec![1.0]);
1547 var.add_attribute("units", "K");
1548 assert_eq!(var.get_attribute("units"), Some("K"));
1549 assert_eq!(var.get_attribute("missing"), None);
1550 }
1551 #[test]
1552 fn ncdf_variable_multiple_attributes() {
1553 let mut var = NetCdfVariable::new("vel", vec!["atom".to_string()], vec![1.0, 2.0]);
1554 var.add_attribute("units", "angstrom/ps");
1555 var.add_attribute("long_name", "velocity");
1556 assert_eq!(var.get_attribute("long_name"), Some("velocity"));
1557 assert_eq!(var.attributes.len(), 2);
1558 }
1559 #[test]
1560 fn ncdf_dimension_unlimited() {
1561 let dim = NetCdfDimension {
1562 name: "time".to_string(),
1563 size: None,
1564 };
1565 assert!(dim.is_unlimited());
1566 assert_eq!(dim.effective_size(), 0);
1567 }
1568 #[test]
1569 fn ncdf_dimension_fixed() {
1570 let dim = NetCdfDimension {
1571 name: "atom".to_string(),
1572 size: Some(100),
1573 };
1574 assert!(!dim.is_unlimited());
1575 assert_eq!(dim.effective_size(), 100);
1576 }
1577 #[test]
1578 fn netcdf_file_add_and_get_variable() {
1579 let mut f = NetCdfFile::new();
1580 let var = NetCdfVariable::new("coords", vec!["atom".to_string()], vec![1.0, 2.0, 3.0]);
1581 f.add_variable(var);
1582 let got = f.get_variable("coords").unwrap();
1583 assert_eq!(got.data, vec![1.0, 2.0, 3.0]);
1584 }
1585 #[test]
1586 fn netcdf_file_get_variable_missing() {
1587 let f = NetCdfFile::new();
1588 assert!(f.get_variable("missing").is_none());
1589 }
1590 #[test]
1591 fn netcdf_file_add_dimension() {
1592 let mut f = NetCdfFile::new();
1593 f.add_dimension("atom", 50);
1594 assert_eq!(f.get_dimension_size("atom"), Some(50));
1595 }
1596 #[test]
1597 fn netcdf_file_unlimited_dimension() {
1598 let mut f = NetCdfFile::new();
1599 f.add_unlimited_dimension("time", 0);
1600 assert!(f.is_unlimited_dimension("time"));
1601 assert!(!f.is_unlimited_dimension("atom"));
1602 }
1603 #[test]
1604 fn netcdf_file_add_global_attribute() {
1605 let mut f = NetCdfFile::new();
1606 f.add_attribute("Conventions", "AMBER");
1607 assert_eq!(f.get_attribute("Conventions"), Some("AMBER"));
1608 }
1609 #[test]
1610 fn netcdf_file_variable_names() {
1611 let mut f = NetCdfFile::new();
1612 f.add_variable(NetCdfVariable::new("time", vec![], vec![]));
1613 f.add_variable(NetCdfVariable::new("coords", vec![], vec![]));
1614 let names = f.variable_names();
1615 assert!(names.contains(&"time"));
1616 assert!(names.contains(&"coords"));
1617 }
1618 #[test]
1619 fn netcdf_file_dimension_names() {
1620 let mut f = NetCdfFile::new();
1621 f.add_dimension("frame", 10);
1622 f.add_dimension("atom", 5);
1623 let names = f.dimension_names();
1624 assert_eq!(names.len(), 2);
1625 }
1626 #[test]
1627 fn ncf_text_roundtrip_basic() {
1628 let mut f = NetCdfFile::new();
1629 f.add_dimension("time", 3);
1630 let mut var = NetCdfVariable::new(
1631 "temperature",
1632 vec!["time".to_string()],
1633 vec![300.0, 310.0, 320.0],
1634 );
1635 var.add_attribute("units", "K");
1636 f.add_variable(var);
1637 let cdl = write_ncf_text(&f);
1638 let parsed = parse_ncf_text(&cdl).unwrap();
1639 let v = parsed.get_variable("temperature").unwrap();
1640 assert_eq!(v.data, vec![300.0, 310.0, 320.0]);
1641 }
1642 #[test]
1643 fn ncf_text_contains_dimension() {
1644 let mut f = NetCdfFile::new();
1645 f.add_dimension("atom", 42);
1646 let cdl = write_ncf_text(&f);
1647 assert!(cdl.contains("atom"));
1648 }
1649 #[test]
1650 fn ncf_text_contains_variable_attribute() {
1651 let mut f = NetCdfFile::new();
1652 let mut var = NetCdfVariable::new("v", vec![], vec![1.0]);
1653 var.add_attribute("units", "m/s");
1654 f.add_variable(var);
1655 let cdl = write_ncf_text(&f);
1656 assert!(cdl.contains("m/s"));
1657 }
1658 #[test]
1659 fn reshape_2d_basic() {
1660 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1661 let mat = reshape_2d(&data, 2, 3);
1662 assert_eq!(mat.len(), 2);
1663 assert_eq!(mat[0], vec![1.0, 2.0, 3.0]);
1664 assert_eq!(mat[1], vec![4.0, 5.0, 6.0]);
1665 }
1666 #[test]
1667 fn reshape_2d_single_row() {
1668 let data = vec![1.0, 2.0, 3.0];
1669 let mat = reshape_2d(&data, 1, 3);
1670 assert_eq!(mat, vec![vec![1.0, 2.0, 3.0]]);
1671 }
1672 #[test]
1673 fn reshape_2d_zero_rows() {
1674 let mat = reshape_2d(&[], 0, 3);
1675 assert!(mat.is_empty());
1676 }
1677 #[test]
1678 fn variable_stats_basic() {
1679 let var = NetCdfVariable::new("t", vec![], vec![1.0, 2.0, 3.0, 4.0, 5.0]);
1680 let (min, max, mean) = variable_stats(&var).unwrap();
1681 assert!((min - 1.0).abs() < 1e-12);
1682 assert!((max - 5.0).abs() < 1e-12);
1683 assert!((mean - 3.0).abs() < 1e-12);
1684 }
1685 #[test]
1686 fn variable_stats_empty_returns_none() {
1687 let var = NetCdfVariable::new("t", vec![], vec![]);
1688 assert!(variable_stats(&var).is_none());
1689 }
1690 #[test]
1691 fn amber_trajectory_n_frames_dimension() {
1692 let file = create_amber_trajectory(3, 2, vec![0.0; 18], vec![0.0, 1.0], None, None);
1693 assert_eq!(file.get_dimension_size("frame"), Some(2));
1694 assert_eq!(file.get_dimension_size("atom"), Some(3));
1695 }
1696 #[test]
1697 fn amber_trajectory_has_time_variable() {
1698 let file = create_amber_trajectory(2, 3, vec![0.0; 18], vec![0.0, 1.0, 2.0], None, None);
1699 assert!(file.get_variable("time").is_some());
1700 }
1701 #[test]
1702 fn amber_trajectory_has_coordinates_variable() {
1703 let file = create_amber_trajectory(
1704 2,
1705 1,
1706 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
1707 vec![0.0],
1708 None,
1709 None,
1710 );
1711 assert!(file.get_variable("coordinates").is_some());
1712 }
1713 #[test]
1714 fn amber_trajectory_amber_conventions_attribute() {
1715 let file = create_amber_trajectory(1, 1, vec![0.0; 3], vec![0.0], None, None);
1716 let conv = file.get_attribute("Conventions");
1717 assert!(conv.is_some());
1718 }
1719 #[test]
1720 fn coordinate_variable_data() {
1721 let var = create_coordinate_variable("x", vec![0.0, 1.0, 2.0], "nm");
1722 assert_eq!(var.data, vec![0.0, 1.0, 2.0]);
1723 assert_eq!(var.name, "x");
1724 }
1725 #[test]
1726 fn coordinate_variable_units_attr() {
1727 let var = create_coordinate_variable("time", vec![0.0, 0.5], "ps");
1728 assert_eq!(var.get_attribute("units"), Some("ps"));
1729 }
1730 #[test]
1731 fn nc4_data_type_float64_width() {
1732 assert_eq!(Nc4DataType::Float64.byte_width(), 8);
1733 }
1734 #[test]
1735 fn nc4_data_type_int32_width() {
1736 assert_eq!(Nc4DataType::Int32.byte_width(), 4);
1737 }
1738 #[test]
1739 fn nc4_data_type_type_names() {
1740 assert_eq!(Nc4DataType::Float64.type_name(), "double");
1741 assert_eq!(Nc4DataType::Int32.type_name(), "int");
1742 }
1743 #[test]
1744 fn nc4_variable_float64_constructor() {
1745 let v = Nc4Variable::float64("coords", vec!["atom".to_string()], vec![1.0, 2.0]);
1746 assert_eq!(v.name, "coords");
1747 assert_eq!(v.data_type, Nc4DataType::Float64);
1748 assert_eq!(v.float_data, vec![1.0, 2.0]);
1749 }
1750 #[test]
1751 fn nc4_variable_int32_constructor() {
1752 let v = Nc4Variable::int32("n", vec![], vec![1, 2, 3]);
1753 assert_eq!(v.data_type, Nc4DataType::Int32);
1754 assert_eq!(v.int_data, vec![1_i64, 2, 3]);
1755 }
1756 #[test]
1757 fn nc4_variable_add_get_attribute() {
1758 let mut v = Nc4Variable::float64("t", vec![], vec![]);
1759 v.add_attribute("units", "K");
1760 assert_eq!(v.get_attribute("units"), Some("K"));
1761 }
1762 #[test]
1763 fn nc4_variable_len_is_empty() {
1764 let v = Nc4Variable::float64("x", vec![], vec![1.0, 2.0, 3.0]);
1765 assert_eq!(v.len(), 3);
1766 assert!(!v.is_empty());
1767 let empty = Nc4Variable::float64("y", vec![], vec![]);
1768 assert!(empty.is_empty());
1769 }
1770 #[test]
1771 fn nc4_file_add_and_get_variable() {
1772 let mut f = Nc4File::new();
1773 f.add_variable(Nc4Variable::float64("temperature", vec![], vec![300.0]));
1774 let v = f.get_variable("temperature").unwrap();
1775 assert_eq!(v.float_data, vec![300.0]);
1776 }
1777 #[test]
1778 fn nc4_file_total_variable_count() {
1779 let mut f = Nc4File::new();
1780 f.add_variable(Nc4Variable::float64("a", vec![], vec![]));
1781 f.add_variable(Nc4Variable::float64("b", vec![], vec![]));
1782 assert_eq!(f.total_variable_count(), 2);
1783 }
1784 #[test]
1785 fn nc4_file_add_group_and_retrieve() {
1786 let mut f = Nc4File::new();
1787 let mut g = Nc4Group::new("traj");
1788 g.add_variable(Nc4Variable::float64("v", vec![], vec![1.0]));
1789 f.add_group(g);
1790 assert!(f.get_group("traj").is_some());
1791 assert!(f.get_group("missing").is_none());
1792 }
1793 #[test]
1794 fn nc4_file_global_attribute_roundtrip() {
1795 let mut f = Nc4File::new();
1796 f.add_global_attribute("title", "Test Run");
1797 assert_eq!(f.get_global_attribute("title"), Some("Test Run"));
1798 }
1799 #[test]
1800 fn nc4_file_is_unlimited_fixed_dim() {
1801 let mut f = Nc4File::new();
1802 f.add_dimension("atom", 50);
1803 assert!(!f.is_unlimited("atom"));
1804 }
1805 #[test]
1806 fn merge_netcdf_files_overlay_wins() {
1807 let mut base = NetCdfFile::new();
1808 base.add_variable(NetCdfVariable::new("temp", vec![], vec![100.0]));
1809 let mut overlay = NetCdfFile::new();
1810 overlay.add_variable(NetCdfVariable::new("temp", vec![], vec![200.0]));
1811 overlay.add_variable(NetCdfVariable::new("pressure", vec![], vec![1.0]));
1812 let merged = merge_netcdf_files(&base, &overlay);
1813 let temp = merged.get_variable("temp").unwrap();
1814 assert_eq!(temp.data, vec![200.0]);
1815 assert!(merged.get_variable("pressure").is_some());
1816 }
1817 #[test]
1818 fn parse_cdl_dimensions_basic() {
1819 let cdl = "netcdf x {\ndimensions:\n\tatom = 5 ;\n\tframe = 10 ;\nvariables:\n}";
1820 let dims = parse_cdl_dimensions(cdl);
1821 assert_eq!(dims.len(), 2);
1822 let names: Vec<&str> = dims.iter().map(|(n, _)| n.as_str()).collect();
1823 assert!(names.contains(&"atom"));
1824 assert!(names.contains(&"frame"));
1825 }
1826 #[test]
1827 fn parse_cdl_dimensions_empty() {
1828 let cdl = "netcdf x {\ndimensions:\nvariables:\n}";
1829 let dims = parse_cdl_dimensions(cdl);
1830 assert!(dims.is_empty());
1831 }
1832 #[test]
1833 fn netcdffile_hashmap_add_dimension() {
1834 let mut f = NetcdfFile::new();
1835 f.add_dimension("atom", 10);
1836 assert_eq!(f.dimensions.get("atom"), Some(&10));
1837 }
1838 #[test]
1839 fn netcdffile_write_cdl_contains_dims_and_vars() {
1840 let mut f = NetcdfFile::new();
1841 f.add_dimension("time", 5);
1842 f.variables.push(NetcdfVariable {
1843 name: "temperature".to_string(),
1844 dimensions: vec!["time".to_string()],
1845 data: vec![300.0; 5],
1846 units: "K".to_string(),
1847 long_name: "Temperature".to_string(),
1848 });
1849 let cdl = f.write_cdl();
1850 assert!(cdl.contains("time = 5"));
1851 assert!(cdl.contains("temperature"));
1852 assert!(cdl.contains("K"));
1853 }
1854 #[test]
1855 fn linspace_time_basic() {
1856 let t = linspace_time(0.0, 0.5, 5);
1857 assert_eq!(t.len(), 5);
1858 assert!((t[0] - 0.0).abs() < 1e-12);
1859 assert!((t[1] - 0.5).abs() < 1e-12);
1860 assert!((t[4] - 2.0).abs() < 1e-12);
1861 }
1862 #[test]
1863 fn linspace_time_empty() {
1864 let t = linspace_time(0.0, 1.0, 0);
1865 assert!(t.is_empty());
1866 }
1867 #[test]
1868 fn cf_global_attributes_has_required_keys() {
1869 let attrs = cf_global_attributes("My Run", "UniX", "MD");
1870 let keys: Vec<&str> = attrs.iter().map(|(k, _)| k.as_str()).collect();
1871 assert!(keys.contains(&"title") || attrs.iter().any(|(_, v)| v.contains("My Run")));
1872 }
1873}
1874#[allow(dead_code)]
1876pub fn write_cdl_dimensions<W: std::io::Write>(
1877 writer: &mut W,
1878 dims: &[NetcdfDimSpec],
1879) -> std::io::Result<()> {
1880 writeln!(writer, "dimensions:")?;
1881 for d in dims {
1882 writeln!(writer, "{}", d.to_cdl())?;
1883 }
1884 Ok(())
1885}
1886#[allow(dead_code)]
1888pub fn angstroms_to_nm(positions: &[[f64; 3]]) -> Vec<[f64; 3]> {
1889 positions
1890 .iter()
1891 .map(|p| [p[0] / 10.0, p[1] / 10.0, p[2] / 10.0])
1892 .collect()
1893}
1894#[allow(dead_code)]
1896pub fn nm_to_angstroms(positions: &[[f64; 3]]) -> Vec<[f64; 3]> {
1897 positions
1898 .iter()
1899 .map(|p| [p[0] * 10.0, p[1] * 10.0, p[2] * 10.0])
1900 .collect()
1901}
1902#[allow(dead_code)]
1905pub fn time_vector(t0: f64, dt: f64, n: usize) -> Vec<f64> {
1906 (0..n).map(|i| t0 + i as f64 * dt).collect()
1907}
1908#[allow(dead_code)]
1911pub fn msd_1d(positions: &[f64]) -> Vec<f64> {
1912 if positions.is_empty() {
1913 return vec![];
1914 }
1915 let n = positions.len();
1916 let p0 = positions[0];
1917 positions
1918 .iter()
1919 .map(|&p| (p - p0).powi(2))
1920 .collect::<Vec<_>>()[..n]
1921 .to_vec()
1922}
1923#[allow(dead_code)]
1925pub fn slice_mean(data: &[f64]) -> f64 {
1926 if data.is_empty() {
1927 return 0.0;
1928 }
1929 data.iter().sum::<f64>() / data.len() as f64
1930}
1931#[allow(dead_code)]
1933pub fn slice_variance(data: &[f64]) -> f64 {
1934 if data.len() < 2 {
1935 return 0.0;
1936 }
1937 let m = slice_mean(data);
1938 data.iter().map(|x| (x - m).powi(2)).sum::<f64>() / data.len() as f64
1939}
1940#[allow(dead_code)]
1942pub fn autocorrelation(data: &[f64], lag: usize) -> f64 {
1943 let n = data.len();
1944 if lag >= n {
1945 return 0.0;
1946 }
1947 let m = slice_mean(data);
1948 let var = slice_variance(data);
1949 if var == 0.0 {
1950 return 0.0;
1951 }
1952 let cov: f64 = (0..n - lag)
1953 .map(|i| (data[i] - m) * (data[i + lag] - m))
1954 .sum::<f64>()
1955 / (n - lag) as f64;
1956 cov / var
1957}