1use std::fs::File;
11use std::io::{BufRead, BufReader, Write};
12
13#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct XarrayCoordinate {
19 pub name: String,
21 pub values: Vec<f64>,
23 pub units: String,
25}
26
27impl XarrayCoordinate {
28 pub fn new(name: impl Into<String>, values: Vec<f64>, units: impl Into<String>) -> Self {
30 Self {
31 name: name.into(),
32 values,
33 units: units.into(),
34 }
35 }
36}
37
38#[allow(dead_code)]
42#[derive(Debug, Clone)]
43pub struct XarrayDataArray {
44 pub name: String,
46 pub dims: Vec<String>,
48 pub shape: Vec<usize>,
50 pub data: Vec<f64>,
52 pub coords: Vec<XarrayCoordinate>,
54 pub attrs: Vec<(String, String)>,
56}
57
58impl XarrayDataArray {
59 pub fn new(name: impl Into<String>, dims: Vec<String>, shape: Vec<usize>) -> Self {
61 let total: usize = shape.iter().product();
62 Self {
63 name: name.into(),
64 dims,
65 shape,
66 data: vec![0.0; total],
67 coords: Vec::new(),
68 attrs: Vec::new(),
69 }
70 }
71
72 pub fn set_coord(&mut self, coord: XarrayCoordinate) {
74 if let Some(pos) = self.coords.iter().position(|c| c.name == coord.name) {
75 self.coords[pos] = coord;
76 } else {
77 self.coords.push(coord);
78 }
79 }
80
81 pub fn ndim(&self) -> usize {
83 self.dims.len()
84 }
85
86 pub fn size(&self) -> usize {
88 self.shape.iter().product()
89 }
90
91 pub fn get(&self, indices: &[usize]) -> f64 {
93 let flat = linear_index(indices, &self.shape);
94 self.data[flat]
95 }
96
97 pub fn set(&mut self, indices: &[usize], value: f64) {
99 let flat = linear_index(indices, &self.shape);
100 self.data[flat] = value;
101 }
102}
103
104#[allow(dead_code)]
108#[derive(Debug, Clone)]
109pub struct XarrayDataset {
110 pub name: String,
112 pub variables: Vec<XarrayDataArray>,
114 pub attrs: Vec<(String, String)>,
116}
117
118impl XarrayDataset {
119 pub fn new(name: impl Into<String>) -> Self {
121 Self {
122 name: name.into(),
123 variables: Vec::new(),
124 attrs: Vec::new(),
125 }
126 }
127
128 pub fn add_variable(&mut self, var: XarrayDataArray) {
130 self.variables.push(var);
131 }
132
133 pub fn variable_count(&self) -> usize {
135 self.variables.len()
136 }
137
138 pub fn get_variable(&self, name: &str) -> Option<&XarrayDataArray> {
140 self.variables.iter().find(|v| v.name == name)
141 }
142}
143
144pub fn linear_index(indices: &[usize], shape: &[usize]) -> usize {
150 debug_assert_eq!(indices.len(), shape.len());
151 let mut flat = 0usize;
152 let mut stride = 1usize;
153 for d in (0..shape.len()).rev() {
154 flat += indices[d] * stride;
155 stride *= shape[d];
156 }
157 flat
158}
159
160pub fn write_csv_xarray(dataset: &XarrayDataset, path: &str) -> Result<(), String> {
167 for var in &dataset.variables {
168 let file_path = format!("{}_{}.csv", path, var.name);
169 let mut f =
170 File::create(&file_path).map_err(|e| format!("cannot create {file_path}: {e}"))?;
171 let header = var.dims.join(",") + ",value\n";
173 f.write_all(header.as_bytes())
174 .map_err(|e| format!("write error: {e}"))?;
175 let n = var.size();
177 let ndim = var.ndim();
178 let mut indices = vec![0usize; ndim];
179 for flat in 0..n {
180 let row: Vec<String> = indices.iter().map(|v| v.to_string()).collect();
181 let line = row.join(",") + "," + &var.data[flat].to_string() + "\n";
182 f.write_all(line.as_bytes())
183 .map_err(|e| format!("write error: {e}"))?;
184 for d in (0..ndim).rev() {
186 indices[d] += 1;
187 if indices[d] < var.shape[d] {
188 break;
189 }
190 indices[d] = 0;
191 }
192 }
193 }
194 Ok(())
195}
196
197pub fn read_csv_xarray(path: &str) -> Result<XarrayDataArray, String> {
202 let f = File::open(path).map_err(|e| format!("cannot open {path}: {e}"))?;
203 let reader = BufReader::new(f);
204 let mut lines = reader.lines();
205
206 let header_line = lines
207 .next()
208 .ok_or("empty file")?
209 .map_err(|e| format!("read error: {e}"))?;
210 let headers: Vec<String> = header_line
211 .split(',')
212 .map(|s| s.trim().to_string())
213 .collect();
214 let ndim = headers.len().saturating_sub(1);
215 let dim_names: Vec<String> = headers[..ndim].to_vec();
216
217 let mut rows: Vec<Vec<usize>> = Vec::new();
218 let mut values: Vec<f64> = Vec::new();
219
220 for line in lines {
221 let line = line.map_err(|e| format!("read error: {e}"))?;
222 if line.trim().is_empty() {
223 continue;
224 }
225 let parts: Vec<&str> = line.split(',').collect();
226 if parts.len() < ndim + 1 {
227 continue;
228 }
229 let idx: Vec<usize> = parts[..ndim]
230 .iter()
231 .map(|s| s.trim().parse::<usize>().unwrap_or(0))
232 .collect();
233 let val: f64 = parts[ndim].trim().parse::<f64>().unwrap_or(0.0);
234 rows.push(idx);
235 values.push(val);
236 }
237
238 let mut shape = vec![0usize; ndim];
240 for row in &rows {
241 for d in 0..ndim {
242 if row[d] + 1 > shape[d] {
243 shape[d] = row[d] + 1;
244 }
245 }
246 }
247
248 let total: usize = if shape.is_empty() {
249 0
250 } else {
251 shape.iter().product()
252 };
253 let mut data = vec![0.0f64; total];
254 for (idx, &val) in rows.iter().zip(values.iter()) {
255 if idx.len() == ndim {
256 let flat = linear_index(idx, &shape);
257 if flat < data.len() {
258 data[flat] = val;
259 }
260 }
261 }
262
263 Ok(XarrayDataArray {
264 name: "loaded".to_string(),
265 dims: dim_names,
266 shape,
267 data,
268 coords: Vec::new(),
269 attrs: Vec::new(),
270 })
271}
272
273pub fn xarray_to_vtk_structured(
279 dataset: &XarrayDataset,
280 var_name: &str,
281 path: &str,
282) -> Result<(), String> {
283 let var = dataset
284 .get_variable(var_name)
285 .ok_or_else(|| format!("variable '{var_name}' not found"))?;
286 if var.shape.len() != 3 {
287 return Err(format!(
288 "variable '{var_name}' must have 3 dimensions, has {}",
289 var.shape.len()
290 ));
291 }
292 let (nx, ny, nz) = (var.shape[0], var.shape[1], var.shape[2]);
293 let mut f = File::create(path).map_err(|e| format!("cannot create {path}: {e}"))?;
294 writeln!(f, "# vtk DataFile Version 3.0").map_err(|e| e.to_string())?;
295 writeln!(f, "XarrayExport").map_err(|e| e.to_string())?;
296 writeln!(f, "ASCII").map_err(|e| e.to_string())?;
297 writeln!(f, "DATASET STRUCTURED_POINTS").map_err(|e| e.to_string())?;
298 writeln!(f, "DIMENSIONS {} {} {}", nx, ny, nz).map_err(|e| e.to_string())?;
299 writeln!(f, "ORIGIN 0 0 0").map_err(|e| e.to_string())?;
300 writeln!(f, "SPACING 1 1 1").map_err(|e| e.to_string())?;
301 writeln!(f, "POINT_DATA {}", nx * ny * nz).map_err(|e| e.to_string())?;
302 writeln!(f, "SCALARS {} double 1", var_name).map_err(|e| e.to_string())?;
303 writeln!(f, "LOOKUP_TABLE default").map_err(|e| e.to_string())?;
304 for &val in &var.data {
305 writeln!(f, "{val}").map_err(|e| e.to_string())?;
306 }
307 Ok(())
308}
309
310pub fn resample_linear(arr: &XarrayDataArray, new_shape: Vec<usize>) -> XarrayDataArray {
317 let ndim = arr.ndim();
318 assert_eq!(ndim, new_shape.len(), "shape rank mismatch");
319 let new_total: usize = new_shape.iter().product();
320 let mut out = XarrayDataArray::new(arr.name.clone(), arr.dims.clone(), new_shape.clone());
321 for flat in 0..new_total {
322 let mut tmp = flat;
324 let mut new_idx = vec![0usize; ndim];
325 for d in (0..ndim).rev() {
326 new_idx[d] = tmp % new_shape[d];
327 tmp /= new_shape[d];
328 }
329 let src_coords: Vec<f64> = new_idx
331 .iter()
332 .enumerate()
333 .map(|(d, &ni)| {
334 if new_shape[d] <= 1 {
335 0.0
336 } else {
337 ni as f64 * (arr.shape[d] as f64 - 1.0) / (new_shape[d] as f64 - 1.0)
338 }
339 })
340 .collect();
341 let src_idx: Vec<usize> = src_coords
343 .iter()
344 .enumerate()
345 .map(|(d, &sc)| (sc.round() as usize).min(arr.shape[d].saturating_sub(1)))
346 .collect();
347 out.data[flat] = arr.get(&src_idx);
348 }
349 out
350}
351
352pub fn time_average(arr: &XarrayDataArray, time_dim: usize) -> XarrayDataArray {
356 let ndim = arr.ndim();
357 assert!(time_dim < ndim);
358 let mut out_shape = arr.shape.clone();
359 out_shape[time_dim] = 1;
360 let mut out = XarrayDataArray::new(
361 arr.name.clone() + "_tavg",
362 arr.dims.clone(),
363 out_shape.clone(),
364 );
365 let nt = arr.shape[time_dim];
366 let out_total: usize = out_shape.iter().product();
368 for flat_out in 0..out_total {
369 let mut tmp = flat_out;
370 let mut out_idx = vec![0usize; ndim];
371 for d in (0..ndim).rev() {
372 out_idx[d] = tmp % out_shape[d];
373 tmp /= out_shape[d];
374 }
375 let mut sum = 0.0f64;
376 let mut src_idx = out_idx.clone();
377 for t in 0..nt {
378 src_idx[time_dim] = t;
379 sum += arr.get(&src_idx);
380 }
381 out.data[flat_out] = sum / nt as f64;
382 }
383 out
384}
385
386pub fn spatial_gradient(arr: &XarrayDataArray, dim: usize, dx: f64) -> XarrayDataArray {
391 assert!(dim < arr.ndim());
392 let mut out = XarrayDataArray::new(
393 arr.name.clone() + "_grad",
394 arr.dims.clone(),
395 arr.shape.clone(),
396 );
397 let n_dim = arr.shape[dim];
398 let total = arr.size();
399 for flat in 0..total {
400 let mut tmp = flat;
401 let mut idx = vec![0usize; arr.ndim()];
402 for d in (0..arr.ndim()).rev() {
403 idx[d] = tmp % arr.shape[d];
404 tmp /= arr.shape[d];
405 }
406 let i = idx[dim];
407 let grad = if i == 0 {
408 let mut idx_p = idx.clone();
410 idx_p[dim] = 1.min(n_dim - 1);
411 (arr.get(&idx_p) - arr.get(&idx)) / dx
412 } else if i == n_dim - 1 {
413 let mut idx_m = idx.clone();
415 idx_m[dim] = i - 1;
416 (arr.get(&idx) - arr.get(&idx_m)) / dx
417 } else {
418 let mut idx_p = idx.clone();
420 let mut idx_m = idx.clone();
421 idx_p[dim] = i + 1;
422 idx_m[dim] = i - 1;
423 (arr.get(&idx_p) - arr.get(&idx_m)) / (2.0 * dx)
424 };
425 out.data[flat] = grad;
426 }
427 out
428}
429
430#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
439 fn test_linear_index_1d() {
440 assert_eq!(linear_index(&[3], &[10]), 3);
441 }
442
443 #[test]
444 fn test_linear_index_2d() {
445 assert_eq!(linear_index(&[1, 2], &[3, 4]), 6);
447 }
448
449 #[test]
450 fn test_linear_index_3d() {
451 assert_eq!(linear_index(&[1, 2, 3], &[2, 3, 4]), 23);
453 }
454
455 #[test]
456 fn test_linear_index_origin() {
457 assert_eq!(linear_index(&[0, 0, 0], &[5, 5, 5]), 0);
458 }
459
460 #[test]
461 fn test_linear_index_last_element() {
462 let shape = [2, 3, 4];
463 let idx = [1, 2, 3];
464 assert_eq!(linear_index(&idx, &shape), 2 * 3 * 4 - 1);
465 }
466
467 #[test]
470 fn test_data_array_size() {
471 let arr = XarrayDataArray::new("t", vec!["x".into(), "y".into()], vec![3, 4]);
472 assert_eq!(arr.size(), 12);
473 }
474
475 #[test]
476 fn test_data_array_size_empty_shape() {
477 let arr = XarrayDataArray::new("t", vec![], vec![]);
478 assert_eq!(arr.size(), 1); }
480
481 #[test]
482 fn test_data_array_ndim() {
483 let arr =
484 XarrayDataArray::new("t", vec!["x".into(), "y".into(), "z".into()], vec![2, 3, 4]);
485 assert_eq!(arr.ndim(), 3);
486 }
487
488 #[test]
489 fn test_data_array_set_get_roundtrip() {
490 let mut arr = XarrayDataArray::new("t", vec!["x".into(), "y".into()], vec![3, 4]);
491 arr.set(&[1, 2], 42.0);
492 assert!((arr.get(&[1, 2]) - 42.0).abs() < 1e-12);
493 }
494
495 #[test]
496 fn test_data_array_initial_zeros() {
497 let arr = XarrayDataArray::new("t", vec!["x".into()], vec![5]);
498 assert!(arr.data.iter().all(|&v| v == 0.0));
499 }
500
501 #[test]
502 fn test_data_array_set_coord() {
503 let mut arr = XarrayDataArray::new("t", vec!["x".into()], vec![3]);
504 let coord = XarrayCoordinate::new("x", vec![0.0, 1.0, 2.0], "m");
505 arr.set_coord(coord);
506 assert_eq!(arr.coords.len(), 1);
507 }
508
509 #[test]
510 fn test_data_array_set_coord_replace() {
511 let mut arr = XarrayDataArray::new("t", vec!["x".into()], vec![3]);
512 arr.set_coord(XarrayCoordinate::new("x", vec![0.0, 1.0, 2.0], "m"));
513 arr.set_coord(XarrayCoordinate::new("x", vec![0.0, 0.5, 1.0], "m"));
514 assert_eq!(arr.coords.len(), 1);
515 assert!((arr.coords[0].values[1] - 0.5).abs() < 1e-12);
516 }
517
518 #[test]
521 fn test_dataset_new_empty() {
522 let ds = XarrayDataset::new("test");
523 assert_eq!(ds.variable_count(), 0);
524 }
525
526 #[test]
527 fn test_dataset_add_variable_increases_count() {
528 let mut ds = XarrayDataset::new("test");
529 ds.add_variable(XarrayDataArray::new("u", vec!["x".into()], vec![4]));
530 assert_eq!(ds.variable_count(), 1);
531 }
532
533 #[test]
534 fn test_dataset_add_two_variables() {
535 let mut ds = XarrayDataset::new("test");
536 ds.add_variable(XarrayDataArray::new("u", vec!["x".into()], vec![4]));
537 ds.add_variable(XarrayDataArray::new("v", vec!["x".into()], vec![4]));
538 assert_eq!(ds.variable_count(), 2);
539 }
540
541 #[test]
542 fn test_dataset_get_variable() {
543 let mut ds = XarrayDataset::new("test");
544 ds.add_variable(XarrayDataArray::new("temp", vec!["x".into()], vec![4]));
545 let var = ds.get_variable("temp");
546 assert!(var.is_some());
547 assert_eq!(var.unwrap().name, "temp");
548 }
549
550 #[test]
551 fn test_dataset_get_variable_missing() {
552 let ds = XarrayDataset::new("test");
553 assert!(ds.get_variable("nosuchvar").is_none());
554 }
555
556 #[test]
559 fn test_time_average_reduces_shape() {
560 let arr = XarrayDataArray::new("u", vec!["time".into(), "x".into()], vec![4, 3]);
561 let avg = time_average(&arr, 0);
562 assert_eq!(avg.shape[0], 1);
563 assert_eq!(avg.shape[1], 3);
564 }
565
566 #[test]
567 fn test_time_average_correct_value() {
568 let mut arr = XarrayDataArray::new("u", vec!["t".into(), "x".into()], vec![4, 1]);
569 for t in 0..4 {
570 arr.set(&[t, 0], t as f64);
571 }
572 let avg = time_average(&arr, 0);
573 assert!((avg.get(&[0, 0]) - 1.5).abs() < 1e-12);
574 }
575
576 #[test]
577 fn test_time_average_second_dim() {
578 let arr = XarrayDataArray::new("u", vec!["x".into(), "y".into()], vec![3, 4]);
579 let avg = time_average(&arr, 1);
580 assert_eq!(avg.shape, vec![3, 1]);
581 }
582
583 #[test]
586 fn test_spatial_gradient_constant_array_zero() {
587 let mut arr = XarrayDataArray::new("u", vec!["x".into()], vec![5]);
588 for i in 0..5 {
589 arr.set(&[i], 3.0);
590 }
591 let grad = spatial_gradient(&arr, 0, 1.0);
592 assert!(grad.data.iter().all(|&g| g.abs() < 1e-12));
593 }
594
595 #[test]
596 fn test_spatial_gradient_linear_array() {
597 let mut arr = XarrayDataArray::new("u", vec!["x".into()], vec![5]);
598 for i in 0..5 {
599 arr.set(&[i], i as f64);
600 }
601 let grad = spatial_gradient(&arr, 0, 1.0);
602 assert!((grad.get(&[2]) - 1.0).abs() < 1e-12);
604 }
605
606 #[test]
607 fn test_spatial_gradient_shape_preserved() {
608 let arr = XarrayDataArray::new("u", vec!["x".into(), "y".into()], vec![3, 4]);
609 let grad = spatial_gradient(&arr, 0, 0.1);
610 assert_eq!(grad.shape, arr.shape);
611 }
612
613 #[test]
616 fn test_resample_same_shape() {
617 let mut arr = XarrayDataArray::new("u", vec!["x".into()], vec![4]);
618 for i in 0..4 {
619 arr.set(&[i], i as f64);
620 }
621 let out = resample_linear(&arr, vec![4]);
622 assert_eq!(out.shape, vec![4]);
623 }
624
625 #[test]
626 fn test_resample_upscale_shape() {
627 let arr = XarrayDataArray::new("u", vec!["x".into()], vec![3]);
628 let out = resample_linear(&arr, vec![6]);
629 assert_eq!(out.shape, vec![6]);
630 }
631
632 #[test]
633 fn test_resample_downscale_shape() {
634 let arr = XarrayDataArray::new("u", vec!["x".into()], vec![8]);
635 let out = resample_linear(&arr, vec![4]);
636 assert_eq!(out.shape, vec![4]);
637 }
638
639 #[test]
642 fn test_write_read_csv_roundtrip() {
643 let mut ds = XarrayDataset::new("test");
644 let mut arr = XarrayDataArray::new("temperature", vec!["x".into(), "y".into()], vec![2, 3]);
645 arr.set(&[0, 1], 3.125);
646 arr.set(&[1, 2], 2.72);
647 ds.add_variable(arr);
648 let path = "/tmp/xarray_test_roundtrip";
649 write_csv_xarray(&ds, path).expect("write failed");
650 let loaded = read_csv_xarray(&format!("{path}_temperature.csv")).expect("read failed");
651 assert!((loaded.get(&[0, 1]) - 3.125).abs() < 1e-9);
652 assert!((loaded.get(&[1, 2]) - 2.72).abs() < 1e-9);
653 }
654
655 #[test]
656 fn test_write_csv_creates_file() {
657 let mut ds = XarrayDataset::new("test2");
658 ds.add_variable(XarrayDataArray::new("v", vec!["x".into()], vec![3]));
659 write_csv_xarray(&ds, "/tmp/xarray_test2").expect("write failed");
660 assert!(std::path::Path::new("/tmp/xarray_test2_v.csv").exists());
661 }
662
663 #[test]
666 fn test_vtk_export_creates_file() {
667 let mut ds = XarrayDataset::new("vtk_test");
668 let arr = XarrayDataArray::new(
669 "pressure",
670 vec!["x".into(), "y".into(), "z".into()],
671 vec![2, 2, 2],
672 );
673 ds.add_variable(arr);
674 xarray_to_vtk_structured(&ds, "pressure", "/tmp/xarray_test_pressure.vtk")
675 .expect("vtk failed");
676 assert!(std::path::Path::new("/tmp/xarray_test_pressure.vtk").exists());
677 }
678
679 #[test]
680 fn test_vtk_export_wrong_dims_returns_err() {
681 let mut ds = XarrayDataset::new("vtk_test2");
682 let arr = XarrayDataArray::new("u2d", vec!["x".into(), "y".into()], vec![2, 2]);
683 ds.add_variable(arr);
684 let res = xarray_to_vtk_structured(&ds, "u2d", "/tmp/xarray_test_u2d.vtk");
685 assert!(res.is_err());
686 }
687
688 #[test]
689 fn test_coordinate_new() {
690 let c = XarrayCoordinate::new("time", vec![0.0, 1.0, 2.0], "s");
691 assert_eq!(c.name, "time");
692 assert_eq!(c.values.len(), 3);
693 }
694}