Skip to main content

oxiphysics_io/amr_io/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::fs::File;
6use std::io::{self, Read, Write};
7
8#[allow(unused_imports)]
9use super::functions::*;
10use super::functions::{CHECKPOINT_MAGIC, CHECKPOINT_VERSION};
11
12/// A collection of AMR cells at one refinement level.
13#[derive(Debug, Clone)]
14pub struct AmrGrid {
15    /// Refinement level this grid represents.
16    pub level_meta: AmrLevel,
17    /// All cells in this grid.
18    pub cells: Vec<AmrCell>,
19    /// Names of the data fields stored in each cell.
20    pub field_names: Vec<String>,
21}
22impl AmrGrid {
23    /// Create a new empty grid.
24    pub fn new(level_meta: AmrLevel, field_names: Vec<String>) -> Self {
25        Self {
26            level_meta,
27            cells: Vec::new(),
28            field_names,
29        }
30    }
31    /// Add a cell to the grid.
32    pub fn add_cell(&mut self, cell: AmrCell) {
33        self.cells.push(cell);
34    }
35    /// Axis-aligned bounding box of all cells: `[i_min,j_min,k_min, i_max,j_max,k_max]`.
36    pub fn integer_bounding_box(&self) -> Option<[i64; 6]> {
37        if self.cells.is_empty() {
38            return None;
39        }
40        let mut mn = self.cells[0].coords;
41        let mut mx = self.cells[0].coords;
42        for c in &self.cells {
43            for d in 0..3 {
44                if c.coords[d] < mn[d] {
45                    mn[d] = c.coords[d];
46                }
47                if c.coords[d] > mx[d] {
48                    mx[d] = c.coords[d];
49                }
50            }
51        }
52        Some([mn[0], mn[1], mn[2], mx[0], mx[1], mx[2]])
53    }
54    /// Physical bounding box `[x_min,y_min,z_min, x_max,y_max,z_max]`.
55    pub fn physical_bounding_box(&self) -> Option<[f64; 6]> {
56        let ibb = self.integer_bounding_box()?;
57        let cs = self.level_meta.cell_size;
58        let o = &self.level_meta.domain_bounds;
59        Some([
60            o[0] + ibb[0] as f64 * cs,
61            o[1] + ibb[1] as f64 * cs,
62            o[2] + ibb[2] as f64 * cs,
63            o[0] + (ibb[3] + 1) as f64 * cs,
64            o[1] + (ibb[4] + 1) as f64 * cs,
65            o[2] + (ibb[5] + 1) as f64 * cs,
66        ])
67    }
68    /// Number of cells in this grid.
69    pub fn cell_count(&self) -> usize {
70        self.cells.len()
71    }
72}
73/// Coarsening operations: merge sibling blocks and project field data to the
74/// coarser level.
75pub struct AmrCoarsening;
76impl AmrCoarsening {
77    /// Coarsen fine-level cells into a new coarse grid.
78    ///
79    /// For each group of fine cells whose coordinates map to the same coarse
80    /// cell (via integer division by `ratio`), the average field value is
81    /// stored in the coarse cell.
82    pub fn coarsen_grid(fine: &AmrGrid, coarse_level_meta: AmrLevel, ratio: i64) -> AmrGrid {
83        let mut coarse = AmrGrid::new(coarse_level_meta, fine.field_names.clone());
84        let num_fields = fine.field_names.len();
85        let mut sums: std::collections::HashMap<[i64; 3], (Vec<f64>, usize)> =
86            std::collections::HashMap::new();
87        for cell in &fine.cells {
88            let ck = [
89                cell.coords[0] / ratio,
90                cell.coords[1] / ratio,
91                cell.coords[2] / ratio,
92            ];
93            let entry = sums
94                .entry(ck)
95                .or_insert_with(|| (vec![0.0_f64; num_fields], 0));
96            for f in 0..num_fields {
97                entry.0[f] += cell.data.get(f).copied().unwrap_or(0.0_f64);
98            }
99            entry.1 += 1;
100        }
101        let level_val = fine.level_meta.level.saturating_sub(1);
102        for (ck, (sum, cnt)) in sums {
103            let avg: Vec<f64> = sum.iter().map(|&s| s / cnt as f64).collect();
104            coarse.add_cell(AmrCell::new(level_val, ck, avg));
105        }
106        coarse
107    }
108    /// Remove cells from a hierarchy level that are covered at finer levels.
109    ///
110    /// A coarse cell at `coarse_level` is "covered" when all children at
111    /// `coarse_level + 1` exist in `fine_grid`.
112    pub fn remove_covered_cells(coarse_grid: &mut AmrGrid, fine_grid: &AmrGrid, ratio: i64) {
113        let fine_coords: std::collections::HashSet<[i64; 3]> =
114            fine_grid.cells.iter().map(|c| c.coords).collect();
115        coarse_grid.cells.retain(|cc| {
116            let mut all_covered = true;
117            for dz in 0..ratio {
118                for dy in 0..ratio {
119                    for dx in 0..ratio {
120                        let child = [
121                            cc.coords[0] * ratio + dx,
122                            cc.coords[1] * ratio + dy,
123                            cc.coords[2] * ratio + dz,
124                        ];
125                        if !fine_coords.contains(&child) {
126                            all_covered = false;
127                        }
128                    }
129                }
130            }
131            !all_covered
132        });
133    }
134}
135/// Export an AMR mesh to VTK UnstructuredGrid format (.vtu).
136pub struct AmrMeshExport;
137impl AmrMeshExport {
138    /// Write an AMR hierarchy as a VTK UnstructuredGrid XML file.
139    ///
140    /// Each AMR cell becomes a VTK hexahedral cell (type 12).
141    pub fn write_vtu(path: &str, hierarchy: &AmrHierarchy, field_idx: usize) -> io::Result<()> {
142        let mut all_pts: Vec<[f64; 3]> = Vec::new();
143        let mut connectivity: Vec<usize> = Vec::new();
144        let mut offsets: Vec<usize> = Vec::new();
145        let mut types: Vec<u8> = Vec::new();
146        let mut field_vals: Vec<f64> = Vec::new();
147        let mut offset = 0usize;
148        for grid in &hierarchy.levels {
149            let cs = grid.level_meta.cell_size;
150            let o = &grid.level_meta.domain_bounds;
151            for cell in &grid.cells {
152                let x0 = o[0] + cell.coords[0] as f64 * cs;
153                let y0 = o[1] + cell.coords[1] as f64 * cs;
154                let z0 = o[2] + cell.coords[2] as f64 * cs;
155                let x1 = x0 + cs;
156                let y1 = y0 + cs;
157                let z1 = z0 + cs;
158                let base = all_pts.len();
159                all_pts.push([x0, y0, z0]);
160                all_pts.push([x1, y0, z0]);
161                all_pts.push([x1, y1, z0]);
162                all_pts.push([x0, y1, z0]);
163                all_pts.push([x0, y0, z1]);
164                all_pts.push([x1, y0, z1]);
165                all_pts.push([x1, y1, z1]);
166                all_pts.push([x0, y1, z1]);
167                for p in base..base + 8 {
168                    connectivity.push(p);
169                }
170                offset += 8;
171                offsets.push(offset);
172                types.push(12);
173                field_vals.push(cell.data.get(field_idx).copied().unwrap_or(0.0_f64));
174            }
175        }
176        let num_pts = all_pts.len();
177        let num_cells = offsets.len();
178        let mut f = File::create(path)?;
179        writeln!(f, r#"<?xml version="1.0"?>"#)?;
180        writeln!(
181            f,
182            r#"<VTKFile type="UnstructuredGrid" version="0.1" byte_order="LittleEndian">"#
183        )?;
184        writeln!(f, r#"  <UnstructuredGrid>"#)?;
185        writeln!(
186            f,
187            r#"    <Piece NumberOfPoints="{num_pts}" NumberOfCells="{num_cells}">"#
188        )?;
189        writeln!(f, r#"      <Points>"#)?;
190        writeln!(
191            f,
192            r#"        <DataArray type="Float64" NumberOfComponents="3" format="ascii">"#
193        )?;
194        for pt in &all_pts {
195            writeln!(f, "          {:.10} {:.10} {:.10}", pt[0], pt[1], pt[2])?;
196        }
197        writeln!(f, r#"        </DataArray>"#)?;
198        writeln!(f, r#"      </Points>"#)?;
199        writeln!(f, r#"      <Cells>"#)?;
200        writeln!(
201            f,
202            r#"        <DataArray type="Int64" Name="connectivity" format="ascii">"#
203        )?;
204        let conn_str: Vec<String> = connectivity.iter().map(|&c| c.to_string()).collect();
205        writeln!(f, "          {}", conn_str.join(" "))?;
206        writeln!(f, r#"        </DataArray>"#)?;
207        writeln!(
208            f,
209            r#"        <DataArray type="Int64" Name="offsets" format="ascii">"#
210        )?;
211        let off_str: Vec<String> = offsets.iter().map(|&o| o.to_string()).collect();
212        writeln!(f, "          {}", off_str.join(" "))?;
213        writeln!(f, r#"        </DataArray>"#)?;
214        writeln!(
215            f,
216            r#"        <DataArray type="UInt8" Name="types" format="ascii">"#
217        )?;
218        let typ_str: Vec<String> = types.iter().map(|&t| t.to_string()).collect();
219        writeln!(f, "          {}", typ_str.join(" "))?;
220        writeln!(f, r#"        </DataArray>"#)?;
221        writeln!(f, r#"      </Cells>"#)?;
222        writeln!(f, r#"      <CellData>"#)?;
223        writeln!(
224            f,
225            r#"        <DataArray type="Float64" Name="field_{field_idx}" format="ascii">"#
226        )?;
227        let fv_str: Vec<String> = field_vals.iter().map(|&v| format!("{v:.10}")).collect();
228        writeln!(f, "          {}", fv_str.join(" "))?;
229        writeln!(f, r#"        </DataArray>"#)?;
230        writeln!(f, r#"      </CellData>"#)?;
231        writeln!(f, r#"    </Piece>"#)?;
232        writeln!(f, r#"  </UnstructuredGrid>"#)?;
233        writeln!(f, r#"</VTKFile>"#)?;
234        Ok(())
235    }
236}
237/// A single AMR cell.
238#[derive(Debug, Clone)]
239pub struct AmrCell {
240    /// Refinement level this cell belongs to.
241    pub level: usize,
242    /// Integer grid coordinates `(i, j, k)` within the level grid.
243    pub coords: [i64; 3],
244    /// Field data stored at this cell (one value per field).
245    pub data: Vec<f64>,
246}
247impl AmrCell {
248    /// Create a new cell.
249    pub fn new(level: usize, coords: [i64; 3], data: Vec<f64>) -> Self {
250        Self {
251            level,
252            coords,
253            data,
254        }
255    }
256    /// Compute the physical centre of this cell given level metadata.
257    pub fn physical_center(&self, lvl: &AmrLevel) -> [f64; 3] {
258        let cs = lvl.cell_size;
259        let ox = lvl.domain_bounds[0];
260        let oy = lvl.domain_bounds[1];
261        let oz = lvl.domain_bounds[2];
262        [
263            ox + (self.coords[0] as f64 + 0.5) * cs,
264            oy + (self.coords[1] as f64 + 0.5) * cs,
265            oz + (self.coords[2] as f64 + 0.5) * cs,
266        ]
267    }
268}
269/// Metadata for one refinement level in an AMR hierarchy.
270#[derive(Debug, Clone)]
271pub struct AmrLevel {
272    /// Zero-based level index (0 = coarsest).
273    pub level: usize,
274    /// Cell size at this refinement level (uniform isotropic).
275    pub cell_size: f64,
276    /// Domain bounds `[x_min, y_min, z_min, x_max, y_max, z_max]`.
277    pub domain_bounds: [f64; 6],
278}
279impl AmrLevel {
280    /// Create a new refinement level.
281    pub fn new(level: usize, cell_size: f64, domain_bounds: [f64; 6]) -> Self {
282        Self {
283            level,
284            cell_size,
285            domain_bounds,
286        }
287    }
288    /// Refinement ratio relative to the coarsest level (level 0): `2^level`.
289    pub fn refinement_ratio(&self) -> usize {
290        1 << self.level
291    }
292}
293/// Binary checkpoint/restart for an AMR hierarchy.
294///
295/// Format (little-endian binary):
296/// ```text
297/// [magic: u32][version: u32][time: f64][cycle: u64]
298/// [num_levels: u32]
299/// For each level:
300///   [level: u32][cell_size: f64][domain_bounds: 6×f64]
301///   [num_field_names: u32]
302///   For each field name:
303///     [len: u32][bytes: len×u8]
304///   [num_cells: u32]
305///   For each cell:
306///     [coords: 3×i64][num_data: u32][data: num_data×f64]
307/// ```
308pub struct AmrCheckpoint;
309impl AmrCheckpoint {
310    /// Write a checkpoint file.
311    pub fn write(path: &str, hierarchy: &AmrHierarchy) -> io::Result<()> {
312        let mut f = File::create(path)?;
313        f.write_all(&CHECKPOINT_MAGIC.to_le_bytes())?;
314        f.write_all(&CHECKPOINT_VERSION.to_le_bytes())?;
315        f.write_all(&hierarchy.time.to_le_bytes())?;
316        f.write_all(&hierarchy.cycle.to_le_bytes())?;
317        f.write_all(&(hierarchy.levels.len() as u32).to_le_bytes())?;
318        for grid in &hierarchy.levels {
319            let lm = &grid.level_meta;
320            f.write_all(&(lm.level as u32).to_le_bytes())?;
321            f.write_all(&lm.cell_size.to_le_bytes())?;
322            for &b in &lm.domain_bounds {
323                f.write_all(&b.to_le_bytes())?;
324            }
325            f.write_all(&(grid.field_names.len() as u32).to_le_bytes())?;
326            for name in &grid.field_names {
327                let bytes = name.as_bytes();
328                f.write_all(&(bytes.len() as u32).to_le_bytes())?;
329                f.write_all(bytes)?;
330            }
331            f.write_all(&(grid.cells.len() as u32).to_le_bytes())?;
332            for cell in &grid.cells {
333                for &c in &cell.coords {
334                    f.write_all(&c.to_le_bytes())?;
335                }
336                f.write_all(&(cell.data.len() as u32).to_le_bytes())?;
337                for &d in &cell.data {
338                    f.write_all(&d.to_le_bytes())?;
339                }
340            }
341        }
342        Ok(())
343    }
344    /// Read a checkpoint file written by `AmrCheckpoint::write`.
345    pub fn read(path: &str) -> io::Result<AmrHierarchy> {
346        let mut raw = Vec::new();
347        File::open(path)?.read_to_end(&mut raw)?;
348        let mut cursor = 0usize;
349        macro_rules! read_u32 {
350            () => {{
351                let v =
352                    u32::from_le_bytes(raw[cursor..cursor + 4].try_into().map_err(|_| {
353                        io::Error::new(io::ErrorKind::InvalidData, "short read u32")
354                    })?);
355                cursor += 4;
356                v
357            }};
358        }
359        macro_rules! read_u64 {
360            () => {{
361                let v =
362                    u64::from_le_bytes(raw[cursor..cursor + 8].try_into().map_err(|_| {
363                        io::Error::new(io::ErrorKind::InvalidData, "short read u64")
364                    })?);
365                cursor += 8;
366                v
367            }};
368        }
369        macro_rules! read_i64 {
370            () => {{
371                let v =
372                    i64::from_le_bytes(raw[cursor..cursor + 8].try_into().map_err(|_| {
373                        io::Error::new(io::ErrorKind::InvalidData, "short read i64")
374                    })?);
375                cursor += 8;
376                v
377            }};
378        }
379        macro_rules! read_f64 {
380            () => {{
381                let v =
382                    f64::from_le_bytes(raw[cursor..cursor + 8].try_into().map_err(|_| {
383                        io::Error::new(io::ErrorKind::InvalidData, "short read f64")
384                    })?);
385                cursor += 8;
386                v
387            }};
388        }
389        let magic = read_u32!();
390        if magic != CHECKPOINT_MAGIC {
391            return Err(io::Error::new(io::ErrorKind::InvalidData, "bad magic"));
392        }
393        let _version = read_u32!();
394        let time = read_f64!();
395        let cycle = read_u64!();
396        let num_levels = read_u32!() as usize;
397        let mut hierarchy = AmrHierarchy::new();
398        hierarchy.time = time;
399        hierarchy.cycle = cycle;
400        for _ in 0..num_levels {
401            let level = read_u32!() as usize;
402            let cell_size = read_f64!();
403            let mut domain_bounds = [0.0f64; 6];
404            for b in &mut domain_bounds {
405                *b = read_f64!();
406            }
407            let lm = AmrLevel::new(level, cell_size, domain_bounds);
408            let num_fields = read_u32!() as usize;
409            let mut field_names = Vec::with_capacity(num_fields);
410            for _ in 0..num_fields {
411                let len = read_u32!() as usize;
412                let bytes = raw[cursor..cursor + len].to_vec();
413                cursor += len;
414                field_names.push(
415                    String::from_utf8(bytes)
416                        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bad utf8"))?,
417                );
418            }
419            let mut grid = AmrGrid::new(lm, field_names);
420            let num_cells = read_u32!() as usize;
421            for _ in 0..num_cells {
422                let i = read_i64!();
423                let j = read_i64!();
424                let k = read_i64!();
425                let nd = read_u32!() as usize;
426                let mut data = Vec::with_capacity(nd);
427                for _ in 0..nd {
428                    data.push(read_f64!());
429                }
430                grid.add_cell(AmrCell::new(level, [i, j, k], data));
431            }
432            hierarchy.add_level(grid);
433        }
434        Ok(hierarchy)
435    }
436}
437/// A multi-level AMR hierarchy.
438#[derive(Debug, Clone)]
439pub struct AmrHierarchy {
440    /// Grids ordered from coarsest (index 0) to finest.
441    pub levels: Vec<AmrGrid>,
442    /// Simulation time (if applicable).
443    pub time: f64,
444    /// Cycle number (iteration count).
445    pub cycle: u64,
446}
447impl AmrHierarchy {
448    /// Create a new empty hierarchy.
449    pub fn new() -> Self {
450        Self {
451            levels: Vec::new(),
452            time: 0.0,
453            cycle: 0,
454        }
455    }
456    /// Add a grid level (must be added in order from coarsest to finest).
457    pub fn add_level(&mut self, grid: AmrGrid) {
458        self.levels.push(grid);
459    }
460    /// Number of refinement levels.
461    pub fn num_levels(&self) -> usize {
462        self.levels.len()
463    }
464    /// Total number of cells across all levels.
465    pub fn total_cells(&self) -> usize {
466        self.levels.iter().map(|g| g.cell_count()).sum()
467    }
468    /// Coarsen a fine-level field value to the parent level.
469    ///
470    /// Given a fine-level cell index and field index, returns the
471    /// average of all child cells that map to the same coarse cell.
472    pub fn coarsen_field(&self, fine_level: usize, field_idx: usize) -> Vec<f64> {
473        if fine_level == 0 || fine_level >= self.levels.len() {
474            return Vec::new();
475        }
476        let ratio = 2i64;
477        let coarse = &self.levels[fine_level - 1];
478        let fine = &self.levels[fine_level];
479        let mut sums: std::collections::HashMap<[i64; 3], (f64, usize)> =
480            std::collections::HashMap::new();
481        for fc in &fine.cells {
482            let ck = [
483                fc.coords[0] / ratio,
484                fc.coords[1] / ratio,
485                fc.coords[2] / ratio,
486            ];
487            let val = fc.data.get(field_idx).copied().unwrap_or(0.0);
488            let entry = sums.entry(ck).or_insert((0.0, 0));
489            entry.0 += val;
490            entry.1 += 1;
491        }
492        coarse
493            .cells
494            .iter()
495            .map(|cc| {
496                if let Some((sum, cnt)) = sums.get(&cc.coords) {
497                    sum / *cnt as f64
498                } else {
499                    cc.data.get(field_idx).copied().unwrap_or(0.0)
500                }
501            })
502            .collect()
503    }
504    /// Refine (interpolate) a coarse-level field to the next finer level.
505    ///
506    /// Uses nearest-neighbour injection from parent cell.
507    pub fn refine_field(&self, coarse_level: usize, field_idx: usize) -> Vec<f64> {
508        if coarse_level + 1 >= self.levels.len() {
509            return Vec::new();
510        }
511        let ratio = 2i64;
512        let coarse = &self.levels[coarse_level];
513        let fine = &self.levels[coarse_level + 1];
514        let lookup: std::collections::HashMap<[i64; 3], f64> = coarse
515            .cells
516            .iter()
517            .map(|c| (c.coords, c.data.get(field_idx).copied().unwrap_or(0.0)))
518            .collect();
519        fine.cells
520            .iter()
521            .map(|fc| {
522                let ck = [
523                    fc.coords[0] / ratio,
524                    fc.coords[1] / ratio,
525                    fc.coords[2] / ratio,
526                ];
527                lookup.get(&ck).copied().unwrap_or(0.0)
528            })
529            .collect()
530    }
531}
532/// Writer for AMR data in VTK XML format.
533pub struct AmrWriter;
534impl AmrWriter {
535    /// Write an AMR hierarchy as a VTK HierarchicalBoxDataSet XML file.
536    pub fn write_vtk(path: &str, hierarchy: &AmrHierarchy) -> io::Result<()> {
537        let mut f = File::create(path)?;
538        writeln!(f, r#"<?xml version="1.0"?>"#)?;
539        writeln!(
540            f,
541            r#"<VTKFile type="vtkHierarchicalBoxDataSet" version="1.1" byte_order="LittleEndian">"#
542        )?;
543        writeln!(f, r#"  <vtkHierarchicalBoxDataSet>"#)?;
544        for (li, grid) in hierarchy.levels.iter().enumerate() {
545            writeln!(f, r#"    <Block level="{li}">"#)?;
546            for cell in &grid.cells {
547                let cs = grid.level_meta.cell_size;
548                let o = &grid.level_meta.domain_bounds;
549                let x0 = o[0] + cell.coords[0] as f64 * cs;
550                let y0 = o[1] + cell.coords[1] as f64 * cs;
551                let z0 = o[2] + cell.coords[2] as f64 * cs;
552                let x1 = x0 + cs;
553                let y1 = y0 + cs;
554                let z1 = z0 + cs;
555                writeln!(
556                    f,
557                    r#"      <DataSet index="0" amr_box="{} {} {} {} {} {}">"#,
558                    cell.coords[0],
559                    cell.coords[1],
560                    cell.coords[2],
561                    cell.coords[0],
562                    cell.coords[1],
563                    cell.coords[2]
564                )?;
565                writeln!(
566                    f,
567                    r#"        <!-- bounds: {x0:.6} {y0:.6} {z0:.6} {x1:.6} {y1:.6} {z1:.6} -->"#
568                )?;
569                writeln!(f, r#"        <PointData>"#)?;
570                for (fi, fname) in grid.field_names.iter().enumerate() {
571                    let val = cell.data.get(fi).copied().unwrap_or(0.0);
572                    writeln!(
573                        f,
574                        r#"          <DataArray Name="{fname}" NumberOfTuples="1" format="ascii">{val:.10}</DataArray>"#
575                    )?;
576                }
577                writeln!(f, r#"        </PointData>"#)?;
578                writeln!(f, r#"      </DataSet>"#)?;
579            }
580            writeln!(f, r#"    </Block>"#)?;
581        }
582        writeln!(f, r#"  </vtkHierarchicalBoxDataSet>"#)?;
583        writeln!(f, r#"</VTKFile>"#)?;
584        Ok(())
585    }
586}
587/// A block of field data on an AMR mesh patch.
588///
589/// Each block covers a rectangular region of cells at a single refinement level.
590#[derive(Debug, Clone)]
591pub struct AmrBlock {
592    /// Refinement level of this block.
593    pub level: usize,
594    /// Lower-left integer corner `(i, j, k)`.
595    pub lo: [i64; 3],
596    /// Upper-right integer corner (inclusive) `(i, j, k)`.
597    pub hi: [i64; 3],
598    /// Number of cells in each direction: `[nx, ny, nz]`.
599    pub dims: [usize; 3],
600    /// Whether this block should be refined next.
601    pub refinement_flag: bool,
602    /// Field data stored in flat row-major order (z-major).
603    ///
604    /// Length = `nx * ny * nz * num_fields`.
605    pub data: Vec<f64>,
606    /// Number of fields stored per cell.
607    pub num_fields: usize,
608}
609impl AmrBlock {
610    /// Create a new zero-filled block.
611    pub fn new(level: usize, lo: [i64; 3], hi: [i64; 3], num_fields: usize) -> Self {
612        let dims = [
613            (hi[0] - lo[0] + 1) as usize,
614            (hi[1] - lo[1] + 1) as usize,
615            (hi[2] - lo[2] + 1) as usize,
616        ];
617        let total = dims[0] * dims[1] * dims[2] * num_fields;
618        Self {
619            level,
620            lo,
621            hi,
622            dims,
623            refinement_flag: false,
624            data: vec![0.0_f64; total],
625            num_fields,
626        }
627    }
628    /// Total number of cells.
629    pub fn cell_count(&self) -> usize {
630        self.dims[0] * self.dims[1] * self.dims[2]
631    }
632    /// Flat index of cell `(i, j, k)` for field `f`.
633    pub fn index(&self, i: usize, j: usize, k: usize, f: usize) -> usize {
634        ((k * self.dims[1] + j) * self.dims[0] + i) * self.num_fields + f
635    }
636    /// Read field `f` at cell `(i, j, k)`.
637    pub fn get(&self, i: usize, j: usize, k: usize, f: usize) -> f64 {
638        self.data[self.index(i, j, k, f)]
639    }
640    /// Write field `f` at cell `(i, j, k)`.
641    pub fn set(&mut self, i: usize, j: usize, k: usize, f: usize, val: f64) {
642        let idx = self.index(i, j, k, f);
643        self.data[idx] = val;
644    }
645    /// Fill all cells of field `f` with `val`.
646    pub fn fill_field(&mut self, f: usize, val: f64) {
647        for k in 0..self.dims[2] {
648            for j in 0..self.dims[1] {
649                for i in 0..self.dims[0] {
650                    self.set(i, j, k, f, val);
651                }
652            }
653        }
654    }
655    /// Maximum value of field `f` across all cells.
656    pub fn field_max(&self, f: usize) -> f64 {
657        let mut m = f64::NEG_INFINITY;
658        for k in 0..self.dims[2] {
659            for j in 0..self.dims[1] {
660                for i in 0..self.dims[0] {
661                    let v = self.get(i, j, k, f);
662                    if v > m {
663                        m = v;
664                    }
665                }
666            }
667        }
668        m
669    }
670    /// Minimum value of field `f` across all cells.
671    pub fn field_min(&self, f: usize) -> f64 {
672        let mut m = f64::INFINITY;
673        for k in 0..self.dims[2] {
674            for j in 0..self.dims[1] {
675                for i in 0..self.dims[0] {
676                    let v = self.get(i, j, k, f);
677                    if v < m {
678                        m = v;
679                    }
680                }
681            }
682        }
683        m
684    }
685}
686/// Method used to flag cells for refinement.
687#[derive(Debug, Clone, PartialEq)]
688pub enum RefinementMethod {
689    /// Flag cells where the gradient of the specified field exceeds `threshold`.
690    GradientBased {
691        /// Index of the field to differentiate.
692        field_idx: usize,
693        /// Gradient magnitude threshold.
694        threshold: f64,
695    },
696    /// Flag cells where the curvature (Laplacian) exceeds `threshold`.
697    CurvatureBased {
698        /// Index of the field.
699        field_idx: usize,
700        /// Laplacian threshold.
701        threshold: f64,
702    },
703    /// Flag cells by a user-supplied predicate (cell data must exceed `value`).
704    UserDefined {
705        /// Index of the field.
706        field_idx: usize,
707        /// Threshold value.
708        value: f64,
709    },
710}
711/// Statistics about an AMR hierarchy.
712#[derive(Debug, Clone)]
713pub struct AmrStats {
714    /// Number of cells per level.
715    pub cells_per_level: Vec<usize>,
716    /// Total number of cells.
717    pub total_cells: usize,
718    /// Estimated memory usage in bytes (f64 per field per cell).
719    pub memory_bytes: usize,
720}
721impl AmrStats {
722    /// Compute statistics for a hierarchy.
723    pub fn compute(hierarchy: &AmrHierarchy) -> Self {
724        let cells_per_level: Vec<usize> = hierarchy.levels.iter().map(|g| g.cell_count()).collect();
725        let total_cells = cells_per_level.iter().sum();
726        let fields = hierarchy
727            .levels
728            .first()
729            .and_then(|g| g.field_names.len().into())
730            .unwrap_or(0);
731        let memory_bytes = total_cells * fields * std::mem::size_of::<f64>();
732        Self {
733            cells_per_level,
734            total_cells,
735            memory_bytes,
736        }
737    }
738}
739/// Reader for AMR data in the simple VTK XML format written by `AmrWriter`.
740pub struct AmrReader;
741impl AmrReader {
742    /// Read an AMR hierarchy previously written by `AmrWriter::write_vtk`.
743    ///
744    /// This is a lightweight text parser for the specific format we write;
745    /// it does not aim to support arbitrary VTK XML files.
746    pub fn read_vtk(path: &str) -> io::Result<AmrHierarchy> {
747        let mut raw = String::new();
748        File::open(path)?.read_to_string(&mut raw)?;
749        let mut hierarchy = AmrHierarchy::new();
750        let mut current_level: Option<usize> = None;
751        let mut current_coords: Option<[i64; 3]> = None;
752        let mut current_data: Vec<f64> = Vec::new();
753        let mut current_fields: Vec<String> = Vec::new();
754        let mut grid_map: std::collections::BTreeMap<usize, AmrGrid> =
755            std::collections::BTreeMap::new();
756        for line in raw.lines() {
757            let trimmed = line.trim();
758            if trimmed.starts_with(r#"<Block level=""#) {
759                if let Some(l) = parse_attr(trimmed, "level") {
760                    current_level = l.parse::<usize>().ok();
761                    if let Some(lv) = current_level {
762                        grid_map.entry(lv).or_insert_with(|| {
763                            AmrGrid::new(
764                                AmrLevel::new(
765                                    lv,
766                                    1.0 / (1 << lv) as f64,
767                                    [0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
768                                ),
769                                Vec::new(),
770                            )
771                        });
772                    }
773                }
774            } else if trimmed.starts_with("<DataSet") {
775                if let Some(ab) = parse_attr(trimmed, "amr_box") {
776                    let nums: Vec<i64> = ab
777                        .split_whitespace()
778                        .filter_map(|s| s.parse().ok())
779                        .collect();
780                    if nums.len() >= 3 {
781                        current_coords = Some([nums[0], nums[1], nums[2]]);
782                        current_data.clear();
783                        current_fields.clear();
784                    }
785                }
786            } else if trimmed.starts_with("<DataArray") {
787                if let (Some(name), Some(val_str)) =
788                    (parse_attr(trimmed, "Name"), parse_inline_value(trimmed))
789                {
790                    current_fields.push(name.to_string());
791                    current_data.push(val_str.trim().parse::<f64>().unwrap_or(0.0));
792                }
793            } else if trimmed == "</DataSet>"
794                && let (Some(lv), Some(coords)) = (current_level, current_coords.take())
795                && let Some(grid) = grid_map.get_mut(&lv)
796            {
797                if grid.field_names.is_empty() {
798                    grid.field_names = current_fields.clone();
799                }
800                grid.cells
801                    .push(AmrCell::new(lv, coords, current_data.clone()));
802            }
803        }
804        for (_, grid) in grid_map {
805            hierarchy.add_level(grid);
806        }
807        Ok(hierarchy)
808    }
809}
810/// A node in the `AmrtTree` oct-tree.
811#[derive(Debug, Clone)]
812pub struct AmrtNode {
813    /// Refinement level (0 = coarsest).
814    pub level: usize,
815    /// Morton code at this node's position.
816    pub morton: u64,
817    /// Integer grid coordinates `(i, j, k)` at this level.
818    pub coords: [u32; 3],
819    /// Indices of up to 8 children (oct-tree).  `None` = no child.
820    pub children: [Option<usize>; 8],
821    /// Index of the parent node (`None` for the root).
822    pub parent: Option<usize>,
823    /// True if this node is a leaf.
824    pub is_leaf: bool,
825    /// 6-neighbour indices (±X, ±Y, ±Z). `None` if no same-level neighbour.
826    pub neighbors: [Option<usize>; 6],
827}
828impl AmrtNode {
829    /// Create a new leaf node.
830    pub fn leaf(level: usize, coords: [u32; 3], parent: Option<usize>) -> Self {
831        let [i, j, k] = coords;
832        Self {
833            level,
834            morton: morton_encode(i, j, k),
835            coords,
836            children: [None; 8],
837            parent,
838            is_leaf: true,
839            neighbors: [None; 6],
840        }
841    }
842    /// Octant index for a child at relative position `(dx, dy, dz)` where each
843    /// component is 0 or 1.
844    pub fn octant_index(dx: usize, dy: usize, dz: usize) -> usize {
845        dx | (dy << 1) | (dz << 2)
846    }
847}
848/// Interpolation utilities for AMR fields.
849pub struct AmrFieldInterp;
850impl AmrFieldInterp {
851    /// Linearly interpolate a field value at a physical point `(x, y, z)`.
852    ///
853    /// Searches for the finest level cell containing the point and returns
854    /// its field value at `field_idx`.  Falls back to coarser levels if the
855    /// point is not covered at the finest level.
856    pub fn interpolate_at(
857        hierarchy: &AmrHierarchy,
858        point: [f64; 3],
859        field_idx: usize,
860    ) -> Option<f64> {
861        for grid in hierarchy.levels.iter().rev() {
862            let cs = grid.level_meta.cell_size;
863            let o = &grid.level_meta.domain_bounds;
864            let i = ((point[0] - o[0]) / cs).floor() as i64;
865            let j = ((point[1] - o[1]) / cs).floor() as i64;
866            let k = ((point[2] - o[2]) / cs).floor() as i64;
867            for cell in &grid.cells {
868                if cell.coords == [i, j, k] {
869                    return cell.data.get(field_idx).copied();
870                }
871            }
872        }
873        None
874    }
875    /// Bilinear interpolation within a grid level at a physical point.
876    ///
877    /// Uses the four surrounding cells in the XY plane (Z ignored).
878    /// Returns `None` if fewer than one corner cell is found.
879    pub fn bilinear_xy(grid: &AmrGrid, x: f64, y: f64, field_idx: usize) -> Option<f64> {
880        let cs = grid.level_meta.cell_size;
881        let ox = grid.level_meta.domain_bounds[0];
882        let oy = grid.level_meta.domain_bounds[1];
883        let fi = (x - ox) / cs - 0.5;
884        let fj = (y - oy) / cs - 0.5;
885        let i0 = fi.floor() as i64;
886        let j0 = fj.floor() as i64;
887        let tx = fi - fi.floor();
888        let ty = fj - fj.floor();
889        let lookup: std::collections::HashMap<[i64; 3], f64> = grid
890            .cells
891            .iter()
892            .map(|c| (c.coords, c.data.get(field_idx).copied().unwrap_or(0.0)))
893            .collect();
894        let k0 = 0i64;
895        let v00 = lookup.get(&[i0, j0, k0]).copied()?;
896        let v10 = lookup.get(&[i0 + 1, j0, k0]).copied().unwrap_or(v00);
897        let v01 = lookup.get(&[i0, j0 + 1, k0]).copied().unwrap_or(v00);
898        let v11 = lookup.get(&[i0 + 1, j0 + 1, k0]).copied().unwrap_or(v00);
899        let v = v00 * (1.0 - tx) * (1.0 - ty)
900            + v10 * tx * (1.0 - ty)
901            + v01 * (1.0 - tx) * ty
902            + v11 * tx * ty;
903        Some(v)
904    }
905}
906/// Applies refinement criteria to an `AmrHierarchy`.
907///
908/// Flags cells for refinement or coarsening based on the chosen method.
909pub struct AmrRefinementCriteria {
910    /// The method used to flag cells.
911    pub method: RefinementMethod,
912}
913impl AmrRefinementCriteria {
914    /// Create refinement criteria with the given method.
915    pub fn new(method: RefinementMethod) -> Self {
916        Self { method }
917    }
918    /// Flag cells in `grid` for refinement.
919    ///
920    /// Returns a `Vec`bool` parallel to `grid.cells` where `true` means the
921    /// cell should be refined.
922    pub fn flag_refinement(&self, grid: &AmrGrid) -> Vec<bool> {
923        match &self.method {
924            RefinementMethod::GradientBased {
925                field_idx,
926                threshold,
927            } => self.flag_gradient(grid, *field_idx, *threshold),
928            RefinementMethod::CurvatureBased {
929                field_idx,
930                threshold,
931            } => self.flag_curvature(grid, *field_idx, *threshold),
932            RefinementMethod::UserDefined { field_idx, value } => grid
933                .cells
934                .iter()
935                .map(|c| c.data.get(*field_idx).copied().unwrap_or(0.0_f64) > *value)
936                .collect(),
937        }
938    }
939    fn flag_gradient(&self, grid: &AmrGrid, field_idx: usize, threshold: f64) -> Vec<bool> {
940        let lookup: std::collections::HashMap<[i64; 3], f64> = grid
941            .cells
942            .iter()
943            .map(|c| (c.coords, c.data.get(field_idx).copied().unwrap_or(0.0_f64)))
944            .collect();
945        grid.cells
946            .iter()
947            .map(|cell| {
948                let v = cell.data.get(field_idx).copied().unwrap_or(0.0_f64);
949                let mut grad_sq = 0.0_f64;
950                for d in 0..3i64 {
951                    let mut plus_c = cell.coords;
952                    let mut minus_c = cell.coords;
953                    plus_c[d as usize] += 1;
954                    minus_c[d as usize] -= 1;
955                    let vp = lookup.get(&plus_c).copied().unwrap_or(v);
956                    let vm = lookup.get(&minus_c).copied().unwrap_or(v);
957                    let g = (vp - vm) * 0.5_f64;
958                    grad_sq += g * g;
959                }
960                grad_sq.sqrt() > threshold
961            })
962            .collect()
963    }
964    fn flag_curvature(&self, grid: &AmrGrid, field_idx: usize, threshold: f64) -> Vec<bool> {
965        let lookup: std::collections::HashMap<[i64; 3], f64> = grid
966            .cells
967            .iter()
968            .map(|c| (c.coords, c.data.get(field_idx).copied().unwrap_or(0.0_f64)))
969            .collect();
970        grid.cells
971            .iter()
972            .map(|cell| {
973                let v = cell.data.get(field_idx).copied().unwrap_or(0.0_f64);
974                let mut laplacian = 0.0_f64;
975                for d in 0..3i64 {
976                    let mut plus_c = cell.coords;
977                    let mut minus_c = cell.coords;
978                    plus_c[d as usize] += 1;
979                    minus_c[d as usize] -= 1;
980                    let vp = lookup.get(&plus_c).copied().unwrap_or(v);
981                    let vm = lookup.get(&minus_c).copied().unwrap_or(v);
982                    laplacian += vp + vm - 2.0_f64 * v;
983                }
984                laplacian.abs() > threshold
985            })
986            .collect()
987    }
988}
989/// Serialise/deserialise an AMR hierarchy to a simple JSON-like text format.
990///
991/// Format:
992/// ```text
993/// {"time":`f64`,"cycle":`u64`,"levels":[
994///   {"level":`usize`,"cell_size":`f64`,"cells":[
995///     {"coords":[i,j,k],"data":[...]},...
996///   ]},...
997/// ]}
998/// ```
999pub struct AmrSerializer;
1000impl AmrSerializer {
1001    /// Serialise an `AmrHierarchy` to a JSON string.
1002    pub fn to_json(hierarchy: &AmrHierarchy) -> String {
1003        let mut out = String::new();
1004        out.push_str(&format!(
1005            "{{\"time\":{:.15e},\"cycle\":{},\"levels\":[",
1006            hierarchy.time, hierarchy.cycle
1007        ));
1008        for (li, grid) in hierarchy.levels.iter().enumerate() {
1009            if li > 0 {
1010                out.push(',');
1011            }
1012            out.push_str(&format!(
1013                "{{\"level\":{},\"cell_size\":{:.15e},\"domain\":[{},{},{},{},{},{}],\"fields\":[",
1014                grid.level_meta.level,
1015                grid.level_meta.cell_size,
1016                grid.level_meta.domain_bounds[0],
1017                grid.level_meta.domain_bounds[1],
1018                grid.level_meta.domain_bounds[2],
1019                grid.level_meta.domain_bounds[3],
1020                grid.level_meta.domain_bounds[4],
1021                grid.level_meta.domain_bounds[5],
1022            ));
1023            for (fi, name) in grid.field_names.iter().enumerate() {
1024                if fi > 0 {
1025                    out.push(',');
1026                }
1027                out.push('"');
1028                out.push_str(name);
1029                out.push('"');
1030            }
1031            out.push_str("],\"cells\":[");
1032            for (ci, cell) in grid.cells.iter().enumerate() {
1033                if ci > 0 {
1034                    out.push(',');
1035                }
1036                out.push_str(&format!(
1037                    "{{\"coords\":[{},{},{}],\"data\":[",
1038                    cell.coords[0], cell.coords[1], cell.coords[2]
1039                ));
1040                for (di, &d) in cell.data.iter().enumerate() {
1041                    if di > 0 {
1042                        out.push(',');
1043                    }
1044                    out.push_str(&format!("{:.15e}", d));
1045                }
1046                out.push_str("]}");
1047            }
1048            out.push_str("]}}");
1049        }
1050        out.push_str("]}");
1051        out
1052    }
1053    /// Deserialise an `AmrHierarchy` from JSON produced by `to_json`.
1054    ///
1055    /// This is a minimal hand-written parser for the specific format we emit.
1056    /// It does not handle arbitrary JSON.
1057    pub fn from_json(s: &str) -> Option<AmrHierarchy> {
1058        let time = extract_f64_after(s, "\"time\":")?;
1059        let cycle = extract_u64_after(s, "\"cycle\":")?;
1060        let mut hierarchy = AmrHierarchy::new();
1061        hierarchy.time = time;
1062        hierarchy.cycle = cycle;
1063        let mut search_from = 0;
1064        loop {
1065            let level_pos = s[search_from..].find("\"level\":")?;
1066            let abs_pos = search_from + level_pos;
1067            let level_val = extract_usize_at(&s[abs_pos + 8..])?;
1068            let cs_pos = s[abs_pos..].find("\"cell_size\":")?;
1069            let cell_size = extract_f64_after(&s[abs_pos + cs_pos..], "\"cell_size\":")?;
1070            let dom_pos = s[abs_pos..].find("\"domain\":")?;
1071            let dom_start = abs_pos + dom_pos + 9;
1072            let domain = extract_f64_array_6(&s[dom_start..])?;
1073            let fields_pos = s[abs_pos..].find("\"fields\":")?;
1074            let fields_start = abs_pos + fields_pos + 9;
1075            let field_names = extract_string_array(&s[fields_start..]);
1076            let lm = AmrLevel::new(level_val, cell_size, domain);
1077            let mut grid = AmrGrid::new(lm, field_names);
1078            let cells_pos = s[abs_pos..].find("\"cells\":")?;
1079            let cells_start = abs_pos + cells_pos + 8;
1080            let arr_start = cells_start + s[cells_start..].find('[')?;
1081            let arr_end = find_matching_bracket(&s[arr_start..])? + arr_start;
1082            let cells_str = &s[arr_start + 1..arr_end];
1083            let mut cell_from = 0;
1084            while let Some(obj_start) = cells_str[cell_from..].find('{') {
1085                let abs_obj = cell_from + obj_start;
1086                let obj_end_rel = find_matching_brace(&cells_str[abs_obj..])?;
1087                let obj_str = &cells_str[abs_obj..abs_obj + obj_end_rel + 1];
1088                let coords = extract_i64_array_3(obj_str)?;
1089                let data = extract_f64_array_vec(obj_str);
1090                grid.add_cell(AmrCell::new(level_val, coords, data));
1091                cell_from = abs_obj + obj_end_rel + 1;
1092            }
1093            hierarchy.add_level(grid);
1094            search_from = abs_pos + 8;
1095            if s[search_from..].find("\"level\":").is_none() {
1096                break;
1097            }
1098        }
1099        Some(hierarchy)
1100    }
1101}
1102/// Oct-tree AMR data structure.
1103///
1104/// Manages a spatially recursive oct-tree where each node can be refined into
1105/// 8 children.  Nodes are stored in a flat `Vec` for cache-friendly traversal.
1106#[derive(Debug, Default)]
1107pub struct AmrtTree {
1108    /// Flat storage of all nodes.
1109    pub nodes: Vec<AmrtNode>,
1110    /// Maximum allowed refinement level.
1111    pub max_level: usize,
1112    /// Domain bounds `\[x_min, y_min, z_min, x_max, y_max, z_max\]`.
1113    pub domain: [f64; 6],
1114}
1115impl AmrtTree {
1116    /// Create a new empty oct-tree.
1117    pub fn new(domain: [f64; 6], max_level: usize) -> Self {
1118        Self {
1119            nodes: Vec::new(),
1120            max_level,
1121            domain,
1122        }
1123    }
1124    /// Initialise the tree with a single root node at level 0.
1125    pub fn init_root(&mut self) {
1126        self.nodes.clear();
1127        self.nodes.push(AmrtNode::leaf(0, [0, 0, 0], None));
1128    }
1129    /// Refine the leaf at `node_idx` into 8 children.
1130    ///
1131    /// Returns `false` if the node is already at `max_level` or is not a leaf.
1132    pub fn refine(&mut self, node_idx: usize) -> bool {
1133        if node_idx >= self.nodes.len() {
1134            return false;
1135        }
1136        if !self.nodes[node_idx].is_leaf {
1137            return false;
1138        }
1139        let parent_level = self.nodes[node_idx].level;
1140        if parent_level >= self.max_level {
1141            return false;
1142        }
1143        let [pi, pj, pk] = self.nodes[node_idx].coords;
1144        let child_level = parent_level + 1;
1145        let mut child_indices = [usize::MAX; 8];
1146        for dz in 0..2usize {
1147            for dy in 0..2usize {
1148                for dx in 0..2usize {
1149                    let ci = pi * 2 + dx as u32;
1150                    let cj = pj * 2 + dy as u32;
1151                    let ck = pk * 2 + dz as u32;
1152                    let child_node = AmrtNode::leaf(child_level, [ci, cj, ck], Some(node_idx));
1153                    let child_idx = self.nodes.len();
1154                    child_indices[AmrtNode::octant_index(dx, dy, dz)] = child_idx;
1155                    self.nodes.push(child_node);
1156                }
1157            }
1158        }
1159        let mut children_opts = [None; 8];
1160        for (k, &ci) in child_indices.iter().enumerate() {
1161            children_opts[k] = Some(ci);
1162        }
1163        self.nodes[node_idx].children = children_opts;
1164        self.nodes[node_idx].is_leaf = false;
1165        true
1166    }
1167    /// Coarsen a set of 8 sibling leaves back to their parent.
1168    ///
1169    /// Returns `false` if any sibling is not a leaf.
1170    pub fn coarsen(&mut self, parent_idx: usize) -> bool {
1171        if parent_idx >= self.nodes.len() || self.nodes[parent_idx].is_leaf {
1172            return false;
1173        }
1174        let children: Vec<usize> = self.nodes[parent_idx]
1175            .children
1176            .iter()
1177            .filter_map(|&c| c)
1178            .collect();
1179        for &ci in &children {
1180            if !self.nodes[ci].is_leaf {
1181                return false;
1182            }
1183        }
1184        for &ci in &children {
1185            self.nodes[ci].is_leaf = false;
1186        }
1187        self.nodes[parent_idx].children = [None; 8];
1188        self.nodes[parent_idx].is_leaf = true;
1189        true
1190    }
1191    /// Count active (non-deleted) leaf nodes.
1192    pub fn leaf_count(&self) -> usize {
1193        self.nodes.iter().filter(|n| n.is_leaf).count()
1194    }
1195    /// Depth of the tree (maximum level among all active leaves).
1196    pub fn max_active_level(&self) -> usize {
1197        self.nodes
1198            .iter()
1199            .filter(|n| n.is_leaf)
1200            .map(|n| n.level)
1201            .max()
1202            .unwrap_or(0)
1203    }
1204    /// Physical cell size at a given refinement level.
1205    pub fn cell_size_at(&self, level: usize) -> [f64; 3] {
1206        let nx = (self.domain[3] - self.domain[0]) / (1u64 << level) as f64;
1207        let ny = (self.domain[4] - self.domain[1]) / (1u64 << level) as f64;
1208        let nz = (self.domain[5] - self.domain[2]) / (1u64 << level) as f64;
1209        [nx, ny, nz]
1210    }
1211    /// Physical centre of the node at `node_idx`.
1212    pub fn node_centre(&self, node_idx: usize) -> [f64; 3] {
1213        let n = &self.nodes[node_idx];
1214        let cs = self.cell_size_at(n.level);
1215        [
1216            self.domain[0] + (n.coords[0] as f64 + 0.5) * cs[0],
1217            self.domain[1] + (n.coords[1] as f64 + 0.5) * cs[1],
1218            self.domain[2] + (n.coords[2] as f64 + 0.5) * cs[2],
1219        ]
1220    }
1221}