Skip to main content

plot3d/
block.rs

1use std::collections::HashMap;
2
3use crate::Float;
4
5#[derive(Clone, Debug)]
6pub struct Block {
7    pub imax: usize,
8    pub jmax: usize,
9    pub kmax: usize,   // 2D supported via kmax == 1
10    pub x: Vec<Float>, // length = imax*jmax*kmax
11    pub y: Vec<Float>,
12    pub z: Vec<Float>,
13}
14
15/// Data for a single block boundary face (a 2D grid of coordinates).
16#[derive(Clone, Debug)]
17pub struct FaceData {
18    pub x: Vec<Float>,
19    pub y: Vec<Float>,
20    pub z: Vec<Float>,
21    /// Dimensions of the face grid `(nu, nv)`.
22    pub dims: (usize, usize),
23}
24
25impl Block {
26    pub fn new(
27        imax: usize,
28        jmax: usize,
29        kmax: usize,
30        x: Vec<Float>,
31        y: Vec<Float>,
32        z: Vec<Float>,
33    ) -> Self {
34        let n = imax * jmax * kmax;
35        assert_eq!(x.len(), n);
36        assert_eq!(y.len(), n);
37        assert_eq!(z.len(), n);
38        Self {
39            imax,
40            jmax,
41            kmax,
42            x,
43            y,
44            z,
45        }
46    }
47
48    /// Total number of grid points (imax * jmax * kmax).
49    #[inline]
50    pub fn npoints(&self) -> usize {
51        self.imax * self.jmax * self.kmax
52    }
53
54    /// Convert (i, j, k) indices to a flat 1-D index (i-fastest ordering).
55    #[inline]
56    pub fn idx(&self, i: usize, j: usize, k: usize) -> usize {
57        // i–j–k order (i fastest)
58        debug_assert!(i < self.imax && j < self.jmax && k < self.kmax);
59        (k * self.jmax + j) * self.imax + i
60    }
61
62    /// Return (x, y, z) coordinates at grid point (i, j, k).
63    #[inline]
64    pub fn xyz(&self, i: usize, j: usize, k: usize) -> (Float, Float, Float) {
65        let idx = self.idx(i, j, k);
66        (self.x[idx], self.y[idx], self.z[idx])
67    }
68
69    /// Return the x-coordinate at grid point (i, j, k).
70    #[inline]
71    pub fn x_at(&self, i: usize, j: usize, k: usize) -> Float {
72        self.x[self.idx(i, j, k)]
73    }
74
75    /// Return the y-coordinate at grid point (i, j, k).
76    #[inline]
77    pub fn y_at(&self, i: usize, j: usize, k: usize) -> Float {
78        self.y[self.idx(i, j, k)]
79    }
80
81    /// Return the z-coordinate at grid point (i, j, k).
82    #[inline]
83    pub fn z_at(&self, i: usize, j: usize, k: usize) -> Float {
84        self.z[self.idx(i, j, k)]
85    }
86
87    /// Reference to the full x-coordinate array.
88    #[inline]
89    pub fn x_slice(&self) -> &[Float] {
90        &self.x
91    }
92
93    /// Reference to the full y-coordinate array.
94    #[inline]
95    pub fn y_slice(&self) -> &[Float] {
96        &self.y
97    }
98
99    /// Reference to the full z-coordinate array.
100    #[inline]
101    pub fn z_slice(&self) -> &[Float] {
102        &self.z
103    }
104
105    /// Coordinate slice by axis index: 0=x, 1=y, 2=z.
106    #[inline]
107    pub fn axis_slice(&self, axis_idx: usize) -> &[Float] {
108        match axis_idx {
109            0 => &self.x,
110            1 => &self.y,
111            _ => &self.z,
112        }
113    }
114
115    #[inline]
116    pub fn centroid(&self) -> (Float, Float, Float) {
117        let n = self.npoints() as Float;
118        let sum_x: Float = self.x.iter().sum();
119        let sum_y: Float = self.y.iter().sum();
120        let sum_z: Float = self.z.iter().sum();
121        (sum_x / n, sum_y / n, sum_z / n)
122    }
123
124    /// Print the XYZ coordinates at `(i, j, k)` in a readable format.
125    pub fn print_xyz(&self, i: usize, j: usize, k: usize) {
126        let (x, y, z) = self.xyz(i, j, k);
127        println!("XYZ at (i={i}, j={j}, k={k}) is ({x:.6}, {y:.6}, {z:.6})");
128    }
129
130    pub fn shifted(&self, amount: Float, axis: char) -> Block {
131        let mut new = self.clone();
132        new.shift_in_place(amount, axis);
133        new
134    }
135
136    pub fn shift_in_place(&mut self, amount: Float, axis: char) {
137        if amount == 0.0 {
138            return;
139        }
140        match axis.to_ascii_lowercase() {
141            'x' => {
142                for v in &mut self.x {
143                    *v += amount;
144                }
145            }
146            'y' => {
147                for v in &mut self.y {
148                    *v += amount;
149                }
150            }
151            'z' => {
152                for v in &mut self.z {
153                    *v += amount;
154                }
155            }
156            _ => {}
157        }
158    }
159
160    /// Alias for [`Block::npoints`]. Returns `imax * jmax * kmax`.
161    #[inline]
162    pub fn size(&self) -> usize {
163        self.npoints()
164    }
165
166    /// Multiply all coordinates by `factor` in place.
167    pub fn scale(&mut self, factor: Float) {
168        for v in &mut self.x {
169            *v *= factor;
170        }
171        for v in &mut self.y {
172            *v *= factor;
173        }
174        for v in &mut self.z {
175            *v *= factor;
176        }
177    }
178
179    /// Return a new block with coordinates scaled by `factor`.
180    pub fn scaled(&self, factor: Float) -> Block {
181        let mut new = self.clone();
182        new.scale(factor);
183        new
184    }
185
186    /// Convert to cylindrical coordinates about the given rotation axis.
187    ///
188    /// Returns `(r, theta)` where `r` and `theta` are computed in the plane
189    /// perpendicular to the rotation axis.
190    /// Each vector has `npoints()` elements in the same index order as `x/y/z`.
191    pub fn cylindrical(&self, rotation_axis: char) -> (Vec<Float>, Vec<Float>) {
192        let n = self.npoints();
193        let mut r = Vec::with_capacity(n);
194        let mut theta = Vec::with_capacity(n);
195        for idx in 0..n {
196            let (a, b) = match rotation_axis.to_ascii_lowercase() {
197                'x' => (self.y[idx], self.z[idx]),
198                'y' => (self.z[idx], self.x[idx]),
199                'z' => (self.x[idx], self.y[idx]),
200                _ => (self.y[idx], self.z[idx]), // default to x-axis
201            };
202            r.push((a * a + b * b).sqrt());
203            theta.push(a.atan2(b));
204        }
205        (r, theta)
206    }
207
208    /// Compute volume of each cell using the Davies-Salmond hexahedral method.
209    ///
210    /// Returns a flat vector of length `imax * jmax * kmax` where cell `(i, j, k)` with
211    /// `1 <= i < imax, 1 <= j < jmax, 1 <= k < kmax` stores its volume at
212    /// index `(k * jmax + j) * imax + i`. Boundary entries (where any index is 0) are zero.
213    ///
214    /// Reference: Davies & Salmond, AIAA Journal, vol 23, No 6, pp 954-956, 1985.
215    pub fn cell_volumes(&self) -> Vec<Float> {
216        let (ni, nj, nk) = (self.imax, self.jmax, self.kmax);
217        let n = ni * nj * nk;
218        let idx = |i: usize, j: usize, k: usize| -> usize { (k * nj + j) * ni + i };
219
220        // 9 auxiliary face-area component arrays
221        let mut a = vec![vec![0.0; n]; 9];
222
223        // Face csi=const (i varies freely, j and k >= 1)
224        for k in 1..nk {
225            for j in 1..nj {
226                for i in 0..ni {
227                    let dx1 = self.x[idx(i, j, k - 1)] - self.x[idx(i, j - 1, k)];
228                    let dy1 = self.y[idx(i, j, k - 1)] - self.y[idx(i, j - 1, k)];
229                    let dz1 = self.z[idx(i, j, k - 1)] - self.z[idx(i, j - 1, k)];
230
231                    let dx2 = self.x[idx(i, j, k)] - self.x[idx(i, j - 1, k - 1)];
232                    let dy2 = self.y[idx(i, j, k)] - self.y[idx(i, j - 1, k - 1)];
233                    let dz2 = self.z[idx(i, j, k)] - self.z[idx(i, j - 1, k - 1)];
234
235                    let id = idx(i, j, k);
236                    a[0][id] = (dy1 * dz2 - dz1 * dy2) * 0.5;
237                    a[1][id] = (dz1 * dx2 - dx1 * dz2) * 0.5;
238                    a[2][id] = (dx1 * dy2 - dy1 * dx2) * 0.5;
239                }
240            }
241        }
242
243        // Face eta=const (j varies freely, i and k >= 1)
244        for k in 1..nk {
245            for j in 0..nj {
246                for i in 1..ni {
247                    let dx1 = self.x[idx(i, j, k)] - self.x[idx(i - 1, j, k - 1)];
248                    let dy1 = self.y[idx(i, j, k)] - self.y[idx(i - 1, j, k - 1)];
249                    let dz1 = self.z[idx(i, j, k)] - self.z[idx(i - 1, j, k - 1)];
250
251                    let dx2 = self.x[idx(i, j, k - 1)] - self.x[idx(i - 1, j, k)];
252                    let dy2 = self.y[idx(i, j, k - 1)] - self.y[idx(i - 1, j, k)];
253                    let dz2 = self.z[idx(i, j, k - 1)] - self.z[idx(i - 1, j, k)];
254
255                    let id = idx(i, j, k);
256                    a[3][id] = (dy1 * dz2 - dz1 * dy2) * 0.5;
257                    a[4][id] = (dz1 * dx2 - dx1 * dz2) * 0.5;
258                    a[5][id] = (dx1 * dy2 - dy1 * dx2) * 0.5;
259                }
260            }
261        }
262
263        // Face zeta=const (k varies freely, i and j >= 1)
264        for k in 0..nk {
265            for j in 1..nj {
266                for i in 1..ni {
267                    let dx1 = self.x[idx(i, j, k)] - self.x[idx(i - 1, j - 1, k)];
268                    let dy1 = self.y[idx(i, j, k)] - self.y[idx(i - 1, j - 1, k)];
269                    let dz1 = self.z[idx(i, j, k)] - self.z[idx(i - 1, j - 1, k)];
270
271                    let dx2 = self.x[idx(i - 1, j, k)] - self.x[idx(i, j - 1, k)];
272                    let dy2 = self.y[idx(i - 1, j, k)] - self.y[idx(i, j - 1, k)];
273                    let dz2 = self.z[idx(i - 1, j, k)] - self.z[idx(i, j - 1, k)];
274
275                    let id = idx(i, j, k);
276                    a[6][id] = (dy1 * dz2 - dz1 * dy2) * 0.5;
277                    a[7][id] = (dz1 * dx2 - dx1 * dz2) * 0.5;
278                    a[8][id] = (dx1 * dy2 - dy1 * dx2) * 0.5;
279                }
280            }
281        }
282
283        // Compute cell volumes from the 6 face centroids and face-area vectors
284        let mut v = vec![0.0; n];
285        for k in 1..nk {
286            for j in 1..nj {
287                for i in 1..ni {
288                    // 6 face centroids (cf[face][component])
289                    let mut cf = [[0.0; 3]; 6];
290                    // Face 0: i-1 face (csi=const, low side)
291                    cf[0][0] = self.x[idx(i - 1, j - 1, k - 1)]
292                        + self.x[idx(i - 1, j - 1, k)]
293                        + self.x[idx(i - 1, j, k - 1)]
294                        + self.x[idx(i - 1, j, k)];
295                    cf[0][1] = self.y[idx(i - 1, j - 1, k - 1)]
296                        + self.y[idx(i - 1, j - 1, k)]
297                        + self.y[idx(i - 1, j, k - 1)]
298                        + self.y[idx(i - 1, j, k)];
299                    cf[0][2] = self.z[idx(i - 1, j - 1, k - 1)]
300                        + self.z[idx(i - 1, j - 1, k)]
301                        + self.z[idx(i - 1, j, k - 1)]
302                        + self.z[idx(i - 1, j, k)];
303                    // Face 1: i face (csi=const, high side)
304                    cf[1][0] = self.x[idx(i, j - 1, k - 1)]
305                        + self.x[idx(i, j - 1, k)]
306                        + self.x[idx(i, j, k - 1)]
307                        + self.x[idx(i, j, k)];
308                    cf[1][1] = self.y[idx(i, j - 1, k - 1)]
309                        + self.y[idx(i, j - 1, k)]
310                        + self.y[idx(i, j, k - 1)]
311                        + self.y[idx(i, j, k)];
312                    cf[1][2] = self.z[idx(i, j - 1, k - 1)]
313                        + self.z[idx(i, j - 1, k)]
314                        + self.z[idx(i, j, k - 1)]
315                        + self.z[idx(i, j, k)];
316                    // Face 2: j-1 face (eta=const, low side)
317                    cf[2][0] = self.x[idx(i - 1, j - 1, k - 1)]
318                        + self.x[idx(i - 1, j - 1, k)]
319                        + self.x[idx(i, j - 1, k - 1)]
320                        + self.x[idx(i, j - 1, k)];
321                    cf[2][1] = self.y[idx(i - 1, j - 1, k - 1)]
322                        + self.y[idx(i - 1, j - 1, k)]
323                        + self.y[idx(i, j - 1, k - 1)]
324                        + self.y[idx(i, j - 1, k)];
325                    cf[2][2] = self.z[idx(i - 1, j - 1, k - 1)]
326                        + self.z[idx(i - 1, j - 1, k)]
327                        + self.z[idx(i, j - 1, k - 1)]
328                        + self.z[idx(i, j - 1, k)];
329                    // Face 3: j face (eta=const, high side)
330                    cf[3][0] = self.x[idx(i - 1, j, k - 1)]
331                        + self.x[idx(i - 1, j, k)]
332                        + self.x[idx(i, j, k - 1)]
333                        + self.x[idx(i, j, k)];
334                    cf[3][1] = self.y[idx(i - 1, j, k - 1)]
335                        + self.y[idx(i - 1, j, k)]
336                        + self.y[idx(i, j, k - 1)]
337                        + self.y[idx(i, j, k)];
338                    cf[3][2] = self.z[idx(i - 1, j, k - 1)]
339                        + self.z[idx(i - 1, j, k)]
340                        + self.z[idx(i, j, k - 1)]
341                        + self.z[idx(i, j, k)];
342                    // Face 4: k-1 face (zeta=const, low side)
343                    cf[4][0] = self.x[idx(i - 1, j - 1, k - 1)]
344                        + self.x[idx(i - 1, j, k - 1)]
345                        + self.x[idx(i, j - 1, k - 1)]
346                        + self.x[idx(i, j, k - 1)];
347                    cf[4][1] = self.y[idx(i - 1, j - 1, k - 1)]
348                        + self.y[idx(i - 1, j, k - 1)]
349                        + self.y[idx(i, j - 1, k - 1)]
350                        + self.y[idx(i, j, k - 1)];
351                    cf[4][2] = self.z[idx(i - 1, j - 1, k - 1)]
352                        + self.z[idx(i - 1, j, k - 1)]
353                        + self.z[idx(i, j - 1, k - 1)]
354                        + self.z[idx(i, j, k - 1)];
355                    // Face 5: k face (zeta=const, high side)
356                    cf[5][0] = self.x[idx(i - 1, j - 1, k)]
357                        + self.x[idx(i - 1, j, k)]
358                        + self.x[idx(i, j - 1, k)]
359                        + self.x[idx(i, j, k)];
360                    cf[5][1] = self.y[idx(i - 1, j - 1, k)]
361                        + self.y[idx(i - 1, j, k)]
362                        + self.y[idx(i, j - 1, k)]
363                        + self.y[idx(i, j, k)];
364                    cf[5][2] = self.z[idx(i - 1, j - 1, k)]
365                        + self.z[idx(i - 1, j, k)]
366                        + self.z[idx(i, j - 1, k)]
367                        + self.z[idx(i, j, k)];
368
369                    let mut vol12 = 0.0;
370                    for nn in 0..2usize {
371                        let sign = if nn == 0 { -1.0 } else { 1.0 };
372                        for l in 0..3usize {
373                            // i-1+nn for csi face, j-1+nn for eta face, k-1+nn for zeta face
374                            vol12 += sign
375                                * (cf[nn][l] * a[l][idx(i - 1 + nn, j, k)]
376                                    + cf[2 + nn][l] * a[3 + l][idx(i, j - 1 + nn, k)]
377                                    + cf[4 + nn][l] * a[6 + l][idx(i, j, k - 1 + nn)]);
378                        }
379                    }
380                    v[idx(i, j, k)] = vol12 / 12.0;
381                }
382            }
383        }
384        v
385    }
386
387    /// Return the six boundary faces as a map keyed by face name.
388    ///
389    /// Keys: `"imin"`, `"imax"`, `"jmin"`, `"jmax"`, `"kmin"`, `"kmax"`.
390    /// Each [`FaceData`] contains the X, Y, Z coordinates of all nodes on that face,
391    /// stored with the two varying indices in their natural order.
392    pub fn get_faces(&self) -> HashMap<&'static str, FaceData> {
393        let mut map = HashMap::with_capacity(6);
394
395        // Helper to extract a face by iterating over the two free dimensions.
396        let extract = |fix_axis: usize, fix_val: usize| -> FaceData {
397            let (nu, nv) = match fix_axis {
398                0 => (self.jmax, self.kmax), // i-const: j varies first, k second
399                1 => (self.imax, self.kmax), // j-const: i varies first, k second
400                _ => (self.imax, self.jmax), // k-const: i varies first, j second
401            };
402            let cap = nu * nv;
403            let mut xf = Vec::with_capacity(cap);
404            let mut yf = Vec::with_capacity(cap);
405            let mut zf = Vec::with_capacity(cap);
406            for v in 0..nv {
407                for u in 0..nu {
408                    let (i, j, k) = match fix_axis {
409                        0 => (fix_val, u, v),
410                        1 => (u, fix_val, v),
411                        _ => (u, v, fix_val),
412                    };
413                    let id = self.idx(i, j, k);
414                    xf.push(self.x[id]);
415                    yf.push(self.y[id]);
416                    zf.push(self.z[id]);
417                }
418            }
419            FaceData {
420                x: xf,
421                y: yf,
422                z: zf,
423                dims: (nu, nv),
424            }
425        };
426
427        map.insert("imin", extract(0, 0));
428        map.insert("imax", extract(0, self.imax - 1));
429        map.insert("jmin", extract(1, 0));
430        map.insert("jmax", extract(1, self.jmax - 1));
431        map.insert("kmin", extract(2, 0));
432        map.insert("kmax", extract(2, self.kmax - 1));
433        map
434    }
435
436    /// Extract a sub-block defined by inclusive index ranges.
437    pub fn sub_block(
438        &self,
439        i_range: std::ops::RangeInclusive<usize>,
440        j_range: std::ops::RangeInclusive<usize>,
441        k_range: std::ops::RangeInclusive<usize>,
442    ) -> Block {
443        let ni = i_range.end() - i_range.start() + 1;
444        let nj = j_range.end() - j_range.start() + 1;
445        let nk = k_range.end() - k_range.start() + 1;
446        let cap = ni * nj * nk;
447        let mut x = Vec::with_capacity(cap);
448        let mut y = Vec::with_capacity(cap);
449        let mut z = Vec::with_capacity(cap);
450        for k in k_range {
451            for j in j_range.clone() {
452                for i in i_range.clone() {
453                    let id = self.idx(i, j, k);
454                    x.push(self.x[id]);
455                    y.push(self.y[id]);
456                    z.push(self.z[id]);
457                }
458            }
459        }
460        Block::new(ni, nj, nk, x, y, z)
461    }
462}