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}