Skip to main content

plot3d/
metrics.rs

1//! Finite-volume geometry metrics for structured Plot3D blocks.
2//!
3//! This module computes the geometric quantities needed by finite-volume CFD
4//! solvers on structured hexahedral grids:
5//!
6//! - **Cell volumes** via the Davies-Salmond divergence-theorem method
7//! - **Face area vectors** (projected areas with direction) for I-, J-, K-faces
8//! - **Cell centroids** (arithmetic mean of the 8 corner nodes)
9//!
10//! All outputs are flat `Vec<Float>` arrays in row-major (i-fastest) order so
11//! they can be consumed directly by solver kernels without reshaping.
12
13use crate::block::Block;
14use crate::Float;
15
16// ---------------------------------------------------------------------------
17// Face area vector storage
18// ---------------------------------------------------------------------------
19
20/// Projected face-area vectors for all three face families in a structured block.
21///
22/// In a structured grid with node dimensions `(ni, nj, nk)`, there are three
23/// families of interior/boundary faces:
24///
25/// | Family | Count | Constant index | Varies |
26/// |--------|-------|----------------|--------|
27/// | I-face | `ni * (nj-1) * (nk-1)` | i = 0..ni-1 | j, k |
28/// | J-face | `(ni-1) * nj * (nk-1)` | j = 0..nj-1 | i, k |
29/// | K-face | `(ni-1) * (nj-1) * nk` | k = 0..nk-1 | i, j |
30///
31/// Each face is a quadrilateral formed by 4 grid nodes.  The area vector is
32/// half the cross product of the two diagonals of the quad:
33///
34/// ```text
35///   S = 0.5 * (diagonal_1 x diagonal_2)
36/// ```
37///
38/// The sign convention gives outward normals in the +i, +j, +k directions
39/// respectively, so the area vector of I-face `(i,j,k)` points from cell
40/// `(i-1,j,k)` toward cell `(i,j,k)`.
41#[derive(Clone, Debug)]
42pub struct FaceMetrics {
43    // ------ I-faces (constant-i surfaces) ------
44    // Dimensions: ni * (nj-1) * (nk-1)
45    // Flat index: i + ni*j + ni*(nj-1)*k  where j in 0..nj-1, k in 0..nk-1
46    /// x-component of I-face area vectors.
47    pub si_x: Vec<Float>,
48    /// y-component of I-face area vectors.
49    pub si_y: Vec<Float>,
50    /// z-component of I-face area vectors.
51    pub si_z: Vec<Float>,
52    /// x-coordinate of I-face centroid (arithmetic mean of the 4 face nodes).
53    pub ci_x: Vec<Float>,
54    /// y-coordinate of I-face centroid.
55    pub ci_y: Vec<Float>,
56    /// z-coordinate of I-face centroid.
57    pub ci_z: Vec<Float>,
58
59    // ------ J-faces (constant-j surfaces) ------
60    // Dimensions: (ni-1) * nj * (nk-1)
61    // Flat index: i + (ni-1)*j + (ni-1)*nj*k  where i in 0..ni-1, j in 0..nj, k in 0..nk-1
62    /// x-component of J-face area vectors.
63    pub sj_x: Vec<Float>,
64    /// y-component of J-face area vectors.
65    pub sj_y: Vec<Float>,
66    /// z-component of J-face area vectors.
67    pub sj_z: Vec<Float>,
68    /// x-coordinate of J-face centroid.
69    pub cj_x: Vec<Float>,
70    /// y-coordinate of J-face centroid.
71    pub cj_y: Vec<Float>,
72    /// z-coordinate of J-face centroid.
73    pub cj_z: Vec<Float>,
74
75    // ------ K-faces (constant-k surfaces) ------
76    // Dimensions: (ni-1) * (nj-1) * nk
77    // Flat index: i + (ni-1)*j + (ni-1)*(nj-1)*k  where i in 0..ni-1, j in 0..nj-1, k in 0..nk
78    /// x-component of K-face area vectors.
79    pub sk_x: Vec<Float>,
80    /// y-component of K-face area vectors.
81    pub sk_y: Vec<Float>,
82    /// z-component of K-face area vectors.
83    pub sk_z: Vec<Float>,
84    /// x-coordinate of K-face centroid.
85    pub ck_x: Vec<Float>,
86    /// y-coordinate of K-face centroid.
87    pub ck_y: Vec<Float>,
88    /// z-coordinate of K-face centroid.
89    pub ck_z: Vec<Float>,
90}
91
92// ---------------------------------------------------------------------------
93// Cell volumes
94// ---------------------------------------------------------------------------
95
96/// Compute the volume of every hexahedral cell in a structured block.
97///
98/// # Method
99///
100/// Each cell `(i,j,k)` is a hexahedron bounded by 8 corner nodes.  The volume
101/// is computed using the divergence theorem applied to the identity field
102/// `F = r` (position vector):
103///
104/// ```text
105///   V = (1/3) * integral_over_surface( r . dS )
106/// ```
107///
108/// For a hexahedron with 6 planar quadrilateral faces this becomes a sum over
109/// the 6 faces.  Each face area vector `S_face` is computed as half the cross
110/// product of the face diagonals, and the face centroid `r_face` is the
111/// average of its 4 corner nodes:
112///
113/// ```text
114///   V = (1/3) * sum_{f=0..5} ( r_face_f . S_face_f )
115/// ```
116///
117/// where the sign convention has outward normals on the +i, +j, +k faces and
118/// inward normals on the -i, -j, -k faces (handled by diagonal ordering).
119///
120/// This is algebraically equivalent to the Davies-Salmond method (AIAA J.,
121/// vol. 23, no. 6, pp. 954-956, 1985) and is exact for trilinear hexahedra.
122///
123/// # Indexing
124///
125/// Returns a flat `Vec<Float>` of length `(ni-1) * (nj-1) * (nk-1)` where
126/// `ni = block.imax`, etc.  Cell `(i,j,k)` with `0 <= i < ni-1` is stored at:
127///
128/// ```text
129///   cell_id = i + (ni-1)*j + (ni-1)*(nj-1)*k
130/// ```
131pub fn compute_cell_volumes(block: &Block) -> Vec<Float> {
132    let ni = block.imax;
133    let nj = block.jmax;
134    let nk = block.kmax;
135
136    let nci = ni - 1; // number of cells in i
137    let ncj = nj - 1;
138    let nck = nk - 1;
139    let ncells = nci * ncj * nck;
140
141    let mut volumes = vec![0.0 as Float; ncells];
142
143    // Closure: flat node index in block arrays
144    let nidx = |i: usize, j: usize, k: usize| -> usize { (k * nj + j) * ni + i };
145
146    // Closure: flat cell index in output array
147    let cidx = |i: usize, j: usize, k: usize| -> usize { (k * ncj + j) * nci + i };
148
149    for k in 0..nck {
150        for j in 0..ncj {
151            for i in 0..nci {
152                // ----------------------------------------------------------
153                // 8 corner nodes of hex cell (i,j,k)
154                //
155                //   n0 = (i,   j,   k  )    n4 = (i,   j,   k+1)
156                //   n1 = (i+1, j,   k  )    n5 = (i+1, j,   k+1)
157                //   n2 = (i,   j+1, k  )    n6 = (i,   j+1, k+1)
158                //   n3 = (i+1, j+1, k  )    n7 = (i+1, j+1, k+1)
159                // ----------------------------------------------------------
160                let p = |ii: usize, jj: usize, kk: usize| -> [Float; 3] {
161                    let id = nidx(ii, jj, kk);
162                    [block.x[id], block.y[id], block.z[id]]
163                };
164
165                let n0 = p(i, j, k);
166                let n1 = p(i + 1, j, k);
167                let n2 = p(i, j + 1, k);
168                let n3 = p(i + 1, j + 1, k);
169                let n4 = p(i, j, k + 1);
170                let n5 = p(i + 1, j, k + 1);
171                let n6 = p(i, j + 1, k + 1);
172                let n7 = p(i + 1, j + 1, k + 1);
173
174                // ----------------------------------------------------------
175                // 6 faces of the hex cell, each a quadrilateral.
176                //
177                // For each face we compute:
178                //   face_centroid = (1/4) * sum of 4 corners
179                //   face_area_vec = 0.5 * (diag1 x diag2)
180                //
181                // Diagonal ordering is chosen so the area vector points
182                // outward for +i/+j/+k faces and inward for -i/-j/-k faces.
183                // The divergence theorem then gives V = (1/3) * sum(r . S).
184                // ----------------------------------------------------------
185
186                let mut vol = 0.0 as Float;
187
188                // Face list: (corner_a, corner_b, corner_c, corner_d)
189                // Diagonals are a-c and b-d. Order matters for sign.
190                //
191                // I-low  face (i   const): nodes n0, n2, n6, n4  => diag n0-n6, n4-n2
192                // I-high face (i+1 const): nodes n1, n3, n7, n5  => diag n1-n7, n3-n5
193                // J-low  face (j   const): nodes n0, n1, n5, n4  => diag n0-n5, n1-n4
194                // J-high face (j+1 const): nodes n2, n3, n7, n6  => diag n2-n7, n6-n3
195                // K-low  face (k   const): nodes n0, n1, n3, n2  => diag n0-n3, n1-n2
196                // K-high face (k+1 const): nodes n4, n5, n7, n6  => diag n4-n7, n6-n5
197
198                let faces: [([Float; 3], [Float; 3], [Float; 3], [Float; 3]); 6] = [
199                    // I-low:  outward normal points in -i direction
200                    // We use diagonal order so cross product points -i,
201                    // which when dotted with centroid and summed gives
202                    // the correct signed contribution.
203                    (n0, n4, n6, n2),
204                    // I-high: outward normal points in +i direction
205                    (n1, n3, n7, n5),
206                    // J-low: outward normal points in -j direction
207                    (n0, n1, n5, n4),
208                    // J-high: outward normal points in +j direction
209                    (n2, n6, n7, n3),
210                    // K-low: outward normal points in -k direction
211                    (n0, n2, n3, n1),
212                    // K-high: outward normal points in +k direction
213                    (n4, n5, n7, n6),
214                ];
215
216                for (a, b, c, d) in &faces {
217                    // Face centroid (un-normalized, factor 1/4 absorbed later)
218                    let cx = a[0] + b[0] + c[0] + d[0];
219                    let cy = a[1] + b[1] + c[1] + d[1];
220                    let cz = a[2] + b[2] + c[2] + d[2];
221
222                    // Diagonals of the quad: a->c and b->d
223                    let d1 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
224                    let d2 = [d[0] - b[0], d[1] - b[1], d[2] - b[2]];
225
226                    // Area vector = 0.5 * (d1 x d2)
227                    let sx = 0.5 * (d1[1] * d2[2] - d1[2] * d2[1]);
228                    let sy = 0.5 * (d1[2] * d2[0] - d1[0] * d2[2]);
229                    let sz = 0.5 * (d1[0] * d2[1] - d1[1] * d2[0]);
230
231                    // Contribution: (1/3) * (centroid/4) . area_vec
232                    // = (1/12) * centroid_sum . area_vec
233                    vol += cx * sx + cy * sy + cz * sz;
234                }
235
236                // Divide by 12: factor of (1/3) from divergence theorem
237                // times (1/4) from un-normalized centroid sum.
238                volumes[cidx(i, j, k)] = (vol / 12.0).abs();
239            }
240        }
241    }
242
243    volumes
244}
245
246// ---------------------------------------------------------------------------
247// Face area vectors
248// ---------------------------------------------------------------------------
249
250/// Compute projected face-area vectors for all three face families.
251///
252/// # I-faces (constant-i surfaces)
253///
254/// An I-face at index `(i, j, k)` is the quadrilateral formed by the 4 nodes:
255///
256/// ```text
257///   (i, j, k),  (i, j+1, k),  (i, j+1, k+1),  (i, j, k+1)
258/// ```
259///
260/// There are `ni` such faces in the i-direction (including the two boundary
261/// faces at `i=0` and `i=ni-1`), and `(nj-1) * (nk-1)` faces in each
262/// i-plane, giving `ni * (nj-1) * (nk-1)` I-faces total.
263///
264/// The area vector is:
265///
266/// ```text
267///   S_i = 0.5 * (diag1 x diag2)
268/// ```
269///
270/// where the diagonals connect opposite corners of the quad.  The sign
271/// convention points S_i in the +i direction (from cell `i-1` to cell `i`).
272///
273/// # J-faces and K-faces
274///
275/// Analogous construction for j-constant and k-constant surfaces.
276///
277/// # Indexing
278///
279/// Within each family the flat index is i-fastest:
280///
281/// - I-face `(i,j,k)`:  `i + ni * j + ni * (nj-1) * k`
282///   with `i in 0..ni`, `j in 0..nj-1`, `k in 0..nk-1`
283///
284/// - J-face `(i,j,k)`:  `i + (ni-1) * j + (ni-1) * nj * k`
285///   with `i in 0..ni-1`, `j in 0..nj`, `k in 0..nk-1`
286///
287/// - K-face `(i,j,k)`:  `i + (ni-1) * j + (ni-1) * (nj-1) * k`
288///   with `i in 0..ni-1`, `j in 0..nj-1`, `k in 0..nk`
289pub fn compute_face_metrics(block: &Block) -> FaceMetrics {
290    let ni = block.imax;
291    let nj = block.jmax;
292    let nk = block.kmax;
293
294    // Node index helper
295    let nidx = |i: usize, j: usize, k: usize| -> usize { (k * nj + j) * ni + i };
296
297    // ---- I-faces: ni * (nj-1) * (nk-1) ----
298    let n_ifaces = ni * (nj - 1) * (nk - 1);
299    let mut si_x = vec![0.0 as Float; n_ifaces];
300    let mut si_y = vec![0.0 as Float; n_ifaces];
301    let mut si_z = vec![0.0 as Float; n_ifaces];
302    let mut ci_x = vec![0.0 as Float; n_ifaces];
303    let mut ci_y = vec![0.0 as Float; n_ifaces];
304    let mut ci_z = vec![0.0 as Float; n_ifaces];
305
306    for k in 0..(nk - 1) {
307        for j in 0..(nj - 1) {
308            for i in 0..ni {
309                // Quad corners on the i-constant plane:
310                //   p0 = (i, j,   k  )
311                //   p1 = (i, j+1, k  )
312                //   p2 = (i, j+1, k+1)
313                //   p3 = (i, j,   k+1)
314                //
315                // Diagonals: p0->p2 and p1->p3
316                // Cross product gives area vector pointing in +i direction.
317                let p0 = nidx(i, j, k);
318                let p1 = nidx(i, j + 1, k);
319                let p2 = nidx(i, j + 1, k + 1);
320                let p3 = nidx(i, j, k + 1);
321
322                let d1x = block.x[p2] - block.x[p0];
323                let d1y = block.y[p2] - block.y[p0];
324                let d1z = block.z[p2] - block.z[p0];
325
326                let d2x = block.x[p3] - block.x[p1];
327                let d2y = block.y[p3] - block.y[p1];
328                let d2z = block.z[p3] - block.z[p1];
329
330                let fid = i + ni * j + ni * (nj - 1) * k;
331                si_x[fid] = 0.5 * (d1y * d2z - d1z * d2y);
332                si_y[fid] = 0.5 * (d1z * d2x - d1x * d2z);
333                si_z[fid] = 0.5 * (d1x * d2y - d1y * d2x);
334
335                // Face centroid = arithmetic mean of the 4 corner nodes.
336                // This matches Fortran's ccCoord formula
337                // (M_ccMBMesh.F:2528-2530).
338                ci_x[fid] = 0.25 * (block.x[p0] + block.x[p1] + block.x[p2] + block.x[p3]);
339                ci_y[fid] = 0.25 * (block.y[p0] + block.y[p1] + block.y[p2] + block.y[p3]);
340                ci_z[fid] = 0.25 * (block.z[p0] + block.z[p1] + block.z[p2] + block.z[p3]);
341            }
342        }
343    }
344
345    // ---- J-faces: (ni-1) * nj * (nk-1) ----
346    let n_jfaces = (ni - 1) * nj * (nk - 1);
347    let mut sj_x = vec![0.0 as Float; n_jfaces];
348    let mut sj_y = vec![0.0 as Float; n_jfaces];
349    let mut sj_z = vec![0.0 as Float; n_jfaces];
350    let mut cj_x = vec![0.0 as Float; n_jfaces];
351    let mut cj_y = vec![0.0 as Float; n_jfaces];
352    let mut cj_z = vec![0.0 as Float; n_jfaces];
353
354    for k in 0..(nk - 1) {
355        for j in 0..nj {
356            for i in 0..(ni - 1) {
357                // Quad corners on the j-constant plane:
358                //   p0 = (i,   j, k  )
359                //   p1 = (i,   j, k+1)
360                //   p2 = (i+1, j, k+1)
361                //   p3 = (i+1, j, k  )
362                //
363                // Diagonals: p0->p2 and p1->p3
364                // Cross product gives area vector pointing in +j direction.
365                let p0 = nidx(i, j, k);
366                let p1 = nidx(i, j, k + 1);
367                let p2 = nidx(i + 1, j, k + 1);
368                let p3 = nidx(i + 1, j, k);
369
370                let d1x = block.x[p2] - block.x[p0];
371                let d1y = block.y[p2] - block.y[p0];
372                let d1z = block.z[p2] - block.z[p0];
373
374                let d2x = block.x[p3] - block.x[p1];
375                let d2y = block.y[p3] - block.y[p1];
376                let d2z = block.z[p3] - block.z[p1];
377
378                let fid = i + (ni - 1) * j + (ni - 1) * nj * k;
379                sj_x[fid] = 0.5 * (d1y * d2z - d1z * d2y);
380                sj_y[fid] = 0.5 * (d1z * d2x - d1x * d2z);
381                sj_z[fid] = 0.5 * (d1x * d2y - d1y * d2x);
382
383                cj_x[fid] = 0.25 * (block.x[p0] + block.x[p1] + block.x[p2] + block.x[p3]);
384                cj_y[fid] = 0.25 * (block.y[p0] + block.y[p1] + block.y[p2] + block.y[p3]);
385                cj_z[fid] = 0.25 * (block.z[p0] + block.z[p1] + block.z[p2] + block.z[p3]);
386            }
387        }
388    }
389
390    // ---- K-faces: (ni-1) * (nj-1) * nk ----
391    let n_kfaces = (ni - 1) * (nj - 1) * nk;
392    let mut sk_x = vec![0.0 as Float; n_kfaces];
393    let mut sk_y = vec![0.0 as Float; n_kfaces];
394    let mut sk_z = vec![0.0 as Float; n_kfaces];
395    let mut ck_x = vec![0.0 as Float; n_kfaces];
396    let mut ck_y = vec![0.0 as Float; n_kfaces];
397    let mut ck_z = vec![0.0 as Float; n_kfaces];
398
399    for k in 0..nk {
400        for j in 0..(nj - 1) {
401            for i in 0..(ni - 1) {
402                // Quad corners on the k-constant plane:
403                //   p0 = (i,   j,   k)
404                //   p1 = (i+1, j,   k)
405                //   p2 = (i+1, j+1, k)
406                //   p3 = (i,   j+1, k)
407                //
408                // Diagonals: p0->p2 and p1->p3
409                // Cross product gives area vector pointing in +k direction.
410                let p0 = nidx(i, j, k);
411                let p1 = nidx(i + 1, j, k);
412                let p2 = nidx(i + 1, j + 1, k);
413                let p3 = nidx(i, j + 1, k);
414
415                let d1x = block.x[p2] - block.x[p0];
416                let d1y = block.y[p2] - block.y[p0];
417                let d1z = block.z[p2] - block.z[p0];
418
419                let d2x = block.x[p3] - block.x[p1];
420                let d2y = block.y[p3] - block.y[p1];
421                let d2z = block.z[p3] - block.z[p1];
422
423                let fid = i + (ni - 1) * j + (ni - 1) * (nj - 1) * k;
424                sk_x[fid] = 0.5 * (d1y * d2z - d1z * d2y);
425                sk_y[fid] = 0.5 * (d1z * d2x - d1x * d2z);
426                sk_z[fid] = 0.5 * (d1x * d2y - d1y * d2x);
427
428                ck_x[fid] = 0.25 * (block.x[p0] + block.x[p1] + block.x[p2] + block.x[p3]);
429                ck_y[fid] = 0.25 * (block.y[p0] + block.y[p1] + block.y[p2] + block.y[p3]);
430                ck_z[fid] = 0.25 * (block.z[p0] + block.z[p1] + block.z[p2] + block.z[p3]);
431            }
432        }
433    }
434
435    FaceMetrics {
436        si_x, si_y, si_z, ci_x, ci_y, ci_z,
437        sj_x, sj_y, sj_z, cj_x, cj_y, cj_z,
438        sk_x, sk_y, sk_z, ck_x, ck_y, ck_z,
439    }
440}
441
442// ---------------------------------------------------------------------------
443// Cell centers
444// ---------------------------------------------------------------------------
445
446/// Compute the geometric center of every cell as the arithmetic mean of its
447/// 8 corner node coordinates.
448///
449/// # Returns
450///
451/// A tuple `(xc, yc, zc)` of flat `Vec<Float>`, each of length
452/// `(ni-1) * (nj-1) * (nk-1)`.  Cell `(i,j,k)` is stored at flat index:
453///
454/// ```text
455///   cell_id = i + (ni-1)*j + (ni-1)*(nj-1)*k
456/// ```
457///
458/// where `0 <= i < ni-1`, `0 <= j < nj-1`, `0 <= k < nk-1`.
459///
460/// The cell center is simply:
461///
462/// ```text
463///   x_c = (1/8) * sum of x-coordinates of the 8 corner nodes
464/// ```
465///
466/// (and analogously for y and z).  This is exact for parallelepipeds and a
467/// reasonable approximation for mildly skewed hexahedra.
468pub fn compute_cell_centers(block: &Block) -> (Vec<Float>, Vec<Float>, Vec<Float>) {
469    let ni = block.imax;
470    let nj = block.jmax;
471    let nk = block.kmax;
472
473    let nci = ni - 1;
474    let ncj = nj - 1;
475    let nck = nk - 1;
476    let ncells = nci * ncj * nck;
477
478    let mut xc = vec![0.0 as Float; ncells];
479    let mut yc = vec![0.0 as Float; ncells];
480    let mut zc = vec![0.0 as Float; ncells];
481
482    // Node index helper
483    let nidx = |i: usize, j: usize, k: usize| -> usize { (k * nj + j) * ni + i };
484
485    // Cell index helper
486    let cidx = |i: usize, j: usize, k: usize| -> usize { (k * ncj + j) * nci + i };
487
488    let eighth: Float = 0.125;
489
490    for k in 0..nck {
491        for j in 0..ncj {
492            for i in 0..nci {
493                let cid = cidx(i, j, k);
494
495                // Indices of the 8 corner nodes
496                let n0 = nidx(i, j, k);
497                let n1 = nidx(i + 1, j, k);
498                let n2 = nidx(i, j + 1, k);
499                let n3 = nidx(i + 1, j + 1, k);
500                let n4 = nidx(i, j, k + 1);
501                let n5 = nidx(i + 1, j, k + 1);
502                let n6 = nidx(i, j + 1, k + 1);
503                let n7 = nidx(i + 1, j + 1, k + 1);
504
505                xc[cid] = eighth
506                    * (block.x[n0] + block.x[n1] + block.x[n2] + block.x[n3]
507                        + block.x[n4] + block.x[n5] + block.x[n6] + block.x[n7]);
508
509                yc[cid] = eighth
510                    * (block.y[n0] + block.y[n1] + block.y[n2] + block.y[n3]
511                        + block.y[n4] + block.y[n5] + block.y[n6] + block.y[n7]);
512
513                zc[cid] = eighth
514                    * (block.z[n0] + block.z[n1] + block.z[n2] + block.z[n3]
515                        + block.z[n4] + block.z[n5] + block.z[n6] + block.z[n7]);
516            }
517        }
518    }
519
520    (xc, yc, zc)
521}
522
523// ---------------------------------------------------------------------------
524// Tests
525// ---------------------------------------------------------------------------
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use crate::block::Block;
531
532    /// Build a unit cube block with `n` nodes per edge (n >= 2).
533    /// Grid is uniformly spaced [0,1]^3.
534    fn unit_cube_block(n: usize) -> Block {
535        let total = n * n * n;
536        let mut x = Vec::with_capacity(total);
537        let mut y = Vec::with_capacity(total);
538        let mut z = Vec::with_capacity(total);
539        let h = 1.0 / (n as f64 - 1.0);
540        for k in 0..n {
541            for j in 0..n {
542                for i in 0..n {
543                    x.push(i as f64 * h);
544                    y.push(j as f64 * h);
545                    z.push(k as f64 * h);
546                }
547            }
548        }
549        Block::new(n, n, n, x, y, z)
550    }
551
552    #[test]
553    fn test_cell_volumes_unit_cube() {
554        // A single cell: 2x2x2 nodes => 1 cell of volume 1.0
555        let block = unit_cube_block(2);
556        let vols = compute_cell_volumes(&block);
557        assert_eq!(vols.len(), 1);
558        assert!((vols[0] - 1.0).abs() < 1e-12, "Expected volume 1.0, got {}", vols[0]);
559    }
560
561    #[test]
562    fn test_cell_volumes_subdivided() {
563        // 3x3x3 nodes => 2x2x2 = 8 cells, each of volume 0.125
564        let block = unit_cube_block(3);
565        let vols = compute_cell_volumes(&block);
566        assert_eq!(vols.len(), 8);
567        for (idx, v) in vols.iter().enumerate() {
568            assert!(
569                (v - 0.125).abs() < 1e-12,
570                "Cell {} expected volume 0.125, got {}",
571                idx,
572                v
573            );
574        }
575    }
576
577    #[test]
578    fn test_cell_volumes_total() {
579        // 4x4x4 nodes => 27 cells, total volume should be 1.0
580        let block = unit_cube_block(4);
581        let vols = compute_cell_volumes(&block);
582        assert_eq!(vols.len(), 27);
583        let total: f64 = vols.iter().sum();
584        assert!((total - 1.0).abs() < 1e-12, "Total volume {}", total);
585    }
586
587    #[test]
588    fn test_cell_centers_unit_cube() {
589        // 2x2x2 nodes => 1 cell, center at (0.5, 0.5, 0.5)
590        let block = unit_cube_block(2);
591        let (xc, yc, zc) = compute_cell_centers(&block);
592        assert_eq!(xc.len(), 1);
593        assert!((xc[0] - 0.5).abs() < 1e-12);
594        assert!((yc[0] - 0.5).abs() < 1e-12);
595        assert!((zc[0] - 0.5).abs() < 1e-12);
596    }
597
598    #[test]
599    fn test_cell_centers_subdivided() {
600        // 3x3x3 => 8 cells. Cell (0,0,0) should have center (0.25, 0.25, 0.25).
601        let block = unit_cube_block(3);
602        let (xc, yc, zc) = compute_cell_centers(&block);
603        assert_eq!(xc.len(), 8);
604        // Cell (0,0,0)
605        assert!((xc[0] - 0.25).abs() < 1e-12);
606        assert!((yc[0] - 0.25).abs() < 1e-12);
607        assert!((zc[0] - 0.25).abs() < 1e-12);
608        // Cell (1,1,1) at index 1 + 2*1 + 2*2*1 = 7
609        assert!((xc[7] - 0.75).abs() < 1e-12);
610        assert!((yc[7] - 0.75).abs() < 1e-12);
611        assert!((zc[7] - 0.75).abs() < 1e-12);
612    }
613
614    #[test]
615    fn test_face_metrics_unit_cube() {
616        // 2x2x2 nodes => each face family has certain counts.
617        // I-faces: 2 * 1 * 1 = 2
618        // J-faces: 1 * 2 * 1 = 2
619        // K-faces: 1 * 1 * 2 = 2
620        let block = unit_cube_block(2);
621        let fm = compute_face_metrics(&block);
622
623        assert_eq!(fm.si_x.len(), 2);
624        assert_eq!(fm.sj_x.len(), 2);
625        assert_eq!(fm.sk_x.len(), 2);
626
627        // For a unit cube, I-faces should have area vector magnitude 1.0
628        // pointing in x-direction: si_x = 1.0, si_y = 0, si_z = 0
629        for idx in 0..2 {
630            assert!(
631                (fm.si_x[idx].abs() - 1.0).abs() < 1e-12,
632                "I-face {} si_x = {}",
633                idx,
634                fm.si_x[idx]
635            );
636            assert!(fm.si_y[idx].abs() < 1e-12);
637            assert!(fm.si_z[idx].abs() < 1e-12);
638        }
639
640        // J-faces: area vector in y-direction
641        for idx in 0..2 {
642            assert!(fm.sj_x[idx].abs() < 1e-12);
643            assert!(
644                (fm.sj_y[idx].abs() - 1.0).abs() < 1e-12,
645                "J-face {} sj_y = {}",
646                idx,
647                fm.sj_y[idx]
648            );
649            assert!(fm.sj_z[idx].abs() < 1e-12);
650        }
651
652        // K-faces: area vector in z-direction
653        for idx in 0..2 {
654            assert!(fm.sk_x[idx].abs() < 1e-12);
655            assert!(fm.sk_y[idx].abs() < 1e-12);
656            assert!(
657                (fm.sk_z[idx].abs() - 1.0).abs() < 1e-12,
658                "K-face {} sk_z = {}",
659                idx,
660                fm.sk_z[idx]
661            );
662        }
663    }
664
665    #[test]
666    fn test_face_metrics_count_subdivided() {
667        // 4x3x5 nodes
668        let ni = 4;
669        let nj = 3;
670        let nk = 5;
671        let total = ni * nj * nk;
672        let x: Vec<f64> = (0..total).map(|_| 0.0).collect();
673        let y = x.clone();
674        let z = x.clone();
675        let block = Block::new(ni, nj, nk, x, y, z);
676        let fm = compute_face_metrics(&block);
677
678        assert_eq!(fm.si_x.len(), ni * (nj - 1) * (nk - 1));
679        assert_eq!(fm.sj_x.len(), (ni - 1) * nj * (nk - 1));
680        assert_eq!(fm.sk_x.len(), (ni - 1) * (nj - 1) * nk);
681    }
682}