Skip to main content

energyml_section/read/
grids.rs

1//! The envelope of an IJK grid along a section (ADR-0015).
2//!
3//! A section does not show every cell wall of a grid: it shows where the grid
4//! **is** - its roof and its floor along the trace, the rock between them, and
5//! where it is cut by a fault or by nothing at all. The reading is the library's
6//! (`energyml_geometry::ijk`, `parent`): hexahedra with the file's own cell
7//! numbers, split coordinate lines honoured, K gaps honoured, undefined cells
8//! dropped. What is done here is the cut.
9//!
10//! # How
11//!
12//! A cell is cut **by the section's vertical plane**, as `section::cut` cuts a
13//! surface: its two faces across the layer (the ring of corners with the higher
14//! mean height is the roof, the other the floor) are each two triangles, and
15//! the cut of each is a short polyline in `(s, z)`. A cell the plane misses
16//! gives nothing; a cell is found by its plan box against each segment of the
17//! trace, so a grid of millions of cells pays for the few hundred the trace
18//! passes through.
19//!
20//! Cells of one column (`i`, `j`) are then stacked, highest first, into **runs** -
21//! consecutive layers with no K gap between them (the floor of one and the roof
22//! of the next are not at the same height there) - and a run is a polygon:
23//! the roof of its first cell, the floor of its last. A column's **roof** is the
24//! roof of its highest cell and its **floor** the floor of its lowest, whatever
25//! lies between.
26//!
27//! Neighbouring columns join when they **meet**: the end of one's roof is the
28//! start of the next's, to the millimetre. Where the roofs meet in `s` and not in
29//! `z`, the two columns do not share their pillars there - a split coordinate
30//! line, a fault - and the roof (or the floor) **jumps**: the span is cut, and
31//! the vertical step is a segment of `faults`. Where nothing meets in `s`, a
32//! column with no defined cell lies between (`CellGeometryIsDefined`, a
33//! sub-grid, a missing column): the span is cut, the fill is cut, and no step is
34//! drawn - a hole is not a fault.
35//!
36//! # What is exact and what is not
37//!
38//! Exact: which cells the plane meets, the roof and floor of each as the plane
39//! cuts them, every jump, every hole, the layer order and the K gaps.
40//!
41//! Approximated, and said where it matters:
42//!
43//! - a cell face is the **two triangles** `(0, 1, 2)` and `(0, 2, 3)` of its
44//!   ring: a warped face is cut along its diagonal, not along a bilinear patch
45//!   (the section view's surfaces are triangles all the same);
46//! - the sides of a cell, and of a run of cells, are the **straight** line from
47//!   the end of its roof to the end of its floor (a tilted pillar is one, a bent
48//!   one is not);
49//! - a cell the plane meets at **one face only** (the section clips an inclined
50//!   cell's corner) is left out, and counted in the cut's notes.
51
52use std::collections::{BTreeMap, HashMap};
53
54use energyml_geometry::geometry::{Patch, Topology};
55use energyml_geometry::section::{self, Cut};
56
57use crate::model::{CutShape, SectionTrace, SurfaceCut};
58
59type P = [f64; 2];
60
61/// One cell, with what finding it takes.
62#[derive(Clone, Copy, Debug)]
63struct Cell {
64    corners: [u32; 8],
65    /// `(i, j)`.
66    column: (u32, u32),
67    layer: u32,
68    /// Plan box, `[x min, y min, x max, y max]`.
69    bbox: [f64; 4],
70    /// The mean height of the eight corners: what orders a column.
71    mean_z: f64,
72}
73
74/// An IJK grid's cells in the scene's system, before any section cuts them -
75/// what a cache keeps while the trace moves.
76#[derive(Clone, Debug)]
77pub struct Grid {
78    points: Vec<[f64; 3]>,
79    cells: Vec<Cell>,
80}
81
82impl Grid {
83    /// The cells of a patch read by `energyml_geometry` and placed in the
84    /// scene. A patch that is not made of hexahedra is refused by name.
85    pub fn from_patch(patch: &Patch) -> Result<Grid, String> {
86        let Topology::Hexahedra {
87            cells, dimensions, ..
88        } = &patch.topology
89        else {
90            return Err("the grid did not read as hexahedra".to_string());
91        };
92        let [ni, nj, nk] = *dimensions;
93        let per_layer = ni * nj;
94        if per_layer == 0 {
95            return Err(format!("the grid is {ni}x{nj}x{nk}"));
96        }
97        let mut out = Vec::with_capacity(cells.len());
98        for (position, corners) in cells.iter().enumerate() {
99            // The cell's number in the file, never its position: a grid with a
100            // hole has dropped cells, and a position is then not a cell.
101            let Some(number) = patch.topology.cell_at(position) else {
102                continue;
103            };
104            let number = number as usize;
105            let (layer, column) = (number / per_layer, number % per_layer);
106            let at: Vec<[f64; 3]> = corners.iter().map(|c| patch.points[*c as usize]).collect();
107            if at.iter().any(|p| p.iter().any(|v| !v.is_finite())) {
108                continue;
109            }
110            let mut bbox = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
111            for p in &at {
112                bbox = [
113                    bbox[0].min(p[0]),
114                    bbox[1].min(p[1]),
115                    bbox[2].max(p[0]),
116                    bbox[3].max(p[1]),
117                ];
118            }
119            out.push(Cell {
120                corners: *corners,
121                column: ((column % ni) as u32, (column / ni) as u32),
122                layer: layer as u32,
123                bbox,
124                mean_z: at.iter().map(|p| p[2]).sum::<f64>() / 8.0,
125            });
126        }
127        if out.is_empty() {
128            return Err("the grid has no defined cell".to_string());
129        }
130        Ok(Grid {
131            points: patch.points.clone(),
132            cells: out,
133        })
134    }
135
136    /// The grid cut along every section; a section it does not meet has no entry.
137    pub fn cut(&self, sections: &[SectionTrace]) -> Vec<SurfaceCut> {
138        sections.iter().filter_map(|one| self.cut_along(one)).collect()
139    }
140
141    fn cut_along(&self, section: &SectionTrace) -> Option<SurfaceCut> {
142        let trace = &section.trace;
143        let length: f64 = trace
144            .windows(2)
145            .map(|p| (p[1][0] - p[0][0]).hypot(p[1][1] - p[0][1]))
146            .sum();
147        // What "the same place" is: a millimetre in a kilometre, as a pair of
148        // neighbouring cells computes the same point on their shared edge in
149        // two orders.
150        let tol_s = 1e-6 * length.max(1.0);
151
152        let mut cut_cells: Vec<CellCut> = Vec::new();
153        let mut clipped = 0usize;
154        for cell in &self.cells {
155            if !crosses_box(trace, cell.bbox) {
156                continue;
157            }
158            match self.cut_cell(cell, trace, tol_s) {
159                Tried::Cut(done) => cut_cells.push(done),
160                Tried::Clipped => clipped += 1,
161                Tried::Missed => {}
162            }
163        }
164        if cut_cells.is_empty() {
165            return None;
166        }
167        let z_max = cut_cells
168            .iter()
169            .flat_map(|c| c.top.iter().chain(&c.bottom))
170            .fold(1.0f64, |m, p| m.max(p[1].abs()));
171        let tol_z = 1e-6 * z_max;
172
173        // Columns, as the section meets them: a column the trace leaves and
174        // enters again is two stretches.
175        let mut columns: BTreeMap<(u32, u32), Vec<CellCut>> = BTreeMap::new();
176        for done in cut_cells {
177            columns.entry(done.column).or_default().push(done);
178        }
179        let mut pieces: Vec<Piece> = Vec::new();
180        for (_, mut cells) in columns {
181            cells.sort_by(|a, b| a.s_range().0.total_cmp(&b.s_range().0));
182            let mut stretches: Vec<Vec<CellCut>> = Vec::new();
183            let mut reach = f64::NEG_INFINITY;
184            for done in cells {
185                let (from, to) = done.s_range();
186                if stretches.is_empty() || from > reach + tol_s {
187                    stretches.push(Vec::new());
188                    reach = to;
189                } else {
190                    reach = reach.max(to);
191                }
192                stretches
193                    .last_mut()
194                    .expect("a stretch was just pushed")
195                    .push(done);
196            }
197            for stretch in stretches {
198                pieces.push(Piece::of(stretch, tol_z));
199            }
200        }
201        pieces.sort_by(|a, b| a.start().total_cmp(&b.start()));
202
203        let (top, mut faults) = join(pieces.iter().map(|p| &p.top), tol_s, tol_z);
204        let (bottom, floor_steps) = join(pieces.iter().map(|p| &p.bottom), tol_s, tol_z);
205        faults.extend(floor_steps);
206
207        let mut fills: Vec<Vec<P>> = Vec::new();
208        let mut open: HashMap<(u32, u32), usize> = HashMap::new();
209        let mut runs: Vec<Run> = Vec::new();
210        for piece in &pieces {
211            for run in &piece.runs {
212                let joined = open
213                    .get(&run.key)
214                    .copied()
215                    .filter(|at| runs[*at].meets(run, tol_s, tol_z));
216                match joined {
217                    Some(at) => runs[at].extend(run),
218                    None => {
219                        open.insert(run.key, runs.len());
220                        runs.push(run.clone());
221                    }
222                }
223            }
224        }
225        for run in runs {
226            let mut ring = run.upper;
227            ring.extend(run.lower.into_iter().rev());
228            if ring.len() >= 3 {
229                fills.push(ring);
230            }
231        }
232
233        let worth =
234            |spans: Vec<Vec<P>>| -> Vec<Vec<P>> { spans.into_iter().filter(|s| s.len() >= 2).collect() };
235        let mut notes = Vec::new();
236        if clipped > 0 {
237            notes.push(format!(
238                "{clipped} cell(s) of the grid are met by this section at one face only (a corner of an inclined \
239                 cell) and are left out of its envelope"
240            ));
241        }
242        Some(SurfaceCut {
243            section: section.key,
244            shape: CutShape::Envelope {
245                top: worth(top),
246                bottom: worth(bottom),
247                fills,
248                faults,
249            },
250            notes,
251            sources: Vec::new(),
252        })
253    }
254
255    fn cut_cell(&self, cell: &Cell, trace: &[P], tol: f64) -> Tried {
256        let at = |n: usize| self.points[cell.corners[n] as usize];
257        let ring_z = |from: usize| (from..from + 4).map(|n| at(n)[2]).sum::<f64>() / 4.0;
258        let (roof, floor) = if ring_z(0) >= ring_z(4) { (0, 4) } else { (4, 0) };
259        let chain = |from: usize| -> Vec<P> {
260            let local: Vec<[f64; 3]> = (from..from + 4).map(at).collect();
261            let found = section::cut(&local, &[[0, 1, 2], [0, 2, 3]], trace);
262            chain_of(&found, tol)
263        };
264        let (top, bottom) = (chain(roof), chain(floor));
265        match (top.is_empty(), bottom.is_empty()) {
266            (true, true) => Tried::Missed,
267            (false, false) => Tried::Cut(CellCut {
268                column: cell.column,
269                layer: cell.layer,
270                mean_z: cell.mean_z,
271                top,
272                bottom,
273            }),
274            _ => Tried::Clipped,
275        }
276    }
277}
278
279enum Tried {
280    Cut(CellCut),
281    Clipped,
282    Missed,
283}
284
285/// A cell, cut: its roof and its floor along the section, `(s, z)` by `s`.
286#[derive(Clone, Debug)]
287struct CellCut {
288    column: (u32, u32),
289    layer: u32,
290    mean_z: f64,
291    top: Vec<P>,
292    bottom: Vec<P>,
293}
294
295impl CellCut {
296    fn s_range(&self) -> (f64, f64) {
297        let ends = [
298            self.top.first(),
299            self.top.last(),
300            self.bottom.first(),
301            self.bottom.last(),
302        ];
303        let s = ends.iter().flatten().map(|p| p[0]);
304        (
305            s.clone().fold(f64::INFINITY, f64::min),
306            s.fold(f64::NEG_INFINITY, f64::max),
307        )
308    }
309}
310
311/// A column's cells across one stretch of the section: its roof, its floor, and
312/// the polygons of its runs.
313#[derive(Clone, Debug)]
314struct Piece {
315    top: Vec<P>,
316    bottom: Vec<P>,
317    runs: Vec<Run>,
318}
319
320impl Piece {
321    fn start(&self) -> f64 {
322        self.top[0][0].min(self.bottom[0][0])
323    }
324
325    /// Cells stacked, highest first; a run ends where the layers are not
326    /// consecutive or the floor of one is not the roof of the next.
327    fn of(mut cells: Vec<CellCut>, tol_z: f64) -> Piece {
328        cells.sort_by(|a, b| b.mean_z.total_cmp(&a.mean_z));
329        let mut runs: Vec<Run> = Vec::new();
330        let mut last: Option<&CellCut> = None;
331        for cell in &cells {
332            let continues = last.is_some_and(|prev| {
333                prev.layer.abs_diff(cell.layer) == 1 && touches(&prev.bottom, &cell.top, tol_z)
334            });
335            if continues {
336                let run = runs.last_mut().expect("a run is open");
337                run.lower = cell.bottom.clone();
338                run.key = (run.key.0.min(cell.layer), run.key.1.max(cell.layer));
339            } else {
340                runs.push(Run {
341                    key: (cell.layer, cell.layer),
342                    upper: cell.top.clone(),
343                    lower: cell.bottom.clone(),
344                });
345            }
346            last = Some(cell);
347        }
348        let highest = cells.first().expect("a stretch has a cell");
349        let lowest = cells.last().expect("a stretch has a cell");
350        Piece {
351            top: highest.top.clone(),
352            bottom: lowest.bottom.clone(),
353            runs,
354        }
355    }
356}
357
358/// Consecutive layers of a column: the polygon between the roof of the first
359/// and the floor of the last.
360#[derive(Clone, Debug)]
361struct Run {
362    /// The layers it spans, lowest and highest number.
363    key: (u32, u32),
364    upper: Vec<P>,
365    lower: Vec<P>,
366}
367
368impl Run {
369    fn meets(&self, next: &Run, tol_s: f64, tol_z: f64) -> bool {
370        let same = |a: Option<&P>, b: Option<&P>| match (a, b) {
371            (Some(a), Some(b)) => (a[0] - b[0]).abs() <= tol_s && (a[1] - b[1]).abs() <= tol_z,
372            _ => false,
373        };
374        same(self.upper.last(), next.upper.first()) && same(self.lower.last(), next.lower.first())
375    }
376
377    fn extend(&mut self, next: &Run) {
378        self.upper.extend(next.upper.iter().skip(1));
379        self.lower.extend(next.lower.iter().skip(1));
380    }
381}
382
383/// Whether the floor `above` rests on the roof `below` where they overlap in
384/// `s` - and `false` where they do not overlap at all, or where they are apart
385/// (a K gap).
386fn touches(above: &[P], below: &[P], tol_z: f64) -> bool {
387    let (Some(a0), Some(a1), Some(b0), Some(b1)) = (above.first(), above.last(), below.first(), below.last())
388    else {
389        return false;
390    };
391    let (from, to) = (a0[0].max(b0[0]), a1[0].min(b1[0]));
392    if from > to {
393        return false;
394    }
395    let mid = 0.5 * (from + to);
396    match (z_at(above, mid), z_at(below, mid)) {
397        (Some(za), Some(zb)) => (za - zb).abs() <= tol_z,
398        _ => false,
399    }
400}
401
402fn z_at(chain: &[P], s: f64) -> Option<f64> {
403    let first = chain.first()?;
404    if chain.len() == 1 {
405        return Some(first[1]);
406    }
407    for pair in chain.windows(2) {
408        if s >= pair[0][0] && s <= pair[1][0] {
409            let width = pair[1][0] - pair[0][0];
410            let t = if width <= f64::EPSILON {
411                0.0
412            } else {
413                (s - pair[0][0]) / width
414            };
415            return Some(pair[0][1] + (pair[1][1] - pair[0][1]) * t);
416        }
417    }
418    None
419}
420
421/// The cut of one face as a chain of `(s, z)` by `s`, welded points merged.
422fn chain_of(cuts: &[Cut], tol: f64) -> Vec<P> {
423    let mut points: Vec<P> = cuts.iter().flat_map(|c| c.profile).collect();
424    points.sort_by(|a, b| a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1])));
425    points.dedup_by(|b, a| (a[0] - b[0]).abs() <= tol * 1e-3 && (a[1] - b[1]).abs() <= tol * 1e-3);
426    points
427}
428
429/// Chains joined where they meet. Where one starts at the `s` another ends and
430/// at another height, the line jumps: a new span and a **step**; where it starts
431/// elsewhere, the new span has nothing to say of what lies between.
432fn join<'a>(chains: impl Iterator<Item = &'a Vec<P>>, tol_s: f64, tol_z: f64) -> (Vec<Vec<P>>, Vec<Vec<P>>) {
433    let mut spans: Vec<Vec<P>> = Vec::new();
434    let mut steps: Vec<Vec<P>> = Vec::new();
435    for chain in chains {
436        let Some(first) = chain.first() else { continue };
437        match spans.last_mut() {
438            Some(span) => {
439                let last = *span.last().expect("a span has a point");
440                if (first[0] - last[0]).abs() <= tol_s {
441                    if (first[1] - last[1]).abs() <= tol_z {
442                        span.extend(chain.iter().skip(1));
443                    } else {
444                        steps.push(vec![last, *first]);
445                        spans.push(chain.clone());
446                    }
447                } else {
448                    spans.push(chain.clone());
449                }
450            }
451            None => spans.push(chain.clone()),
452        }
453    }
454    (spans, steps)
455}
456
457/// Whether the trace passes through a plan box (the slab test, a hair wider than
458/// the box so a cell the trace only grazes is still cut, and says so itself).
459fn crosses_box(trace: &[P], bbox: [f64; 4]) -> bool {
460    let pad = 1e-9 * (1.0 + bbox[2].abs().max(bbox[3].abs()));
461    let (lo, hi) = ([bbox[0] - pad, bbox[1] - pad], [bbox[2] + pad, bbox[3] + pad]);
462    trace.windows(2).any(|pair| {
463        let (p, q) = (pair[0], pair[1]);
464        let (mut t0, mut t1) = (0.0f64, 1.0f64);
465        for axis in 0..2 {
466            let d = q[axis] - p[axis];
467            if d.abs() < 1e-15 {
468                if p[axis] < lo[axis] || p[axis] > hi[axis] {
469                    return false;
470                }
471            } else {
472                let (a, b) = ((lo[axis] - p[axis]) / d, (hi[axis] - p[axis]) / d);
473                t0 = t0.max(a.min(b));
474                t1 = t1.min(a.max(b));
475                if t0 > t1 {
476                    return false;
477                }
478            }
479        }
480        true
481    })
482}