Skip to main content

plot3d/
block.rs

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