Skip to main content

energyml_section/read/
mod.rs

1//! What one package gives the calculation (ADR-0015).
2//!
3//! The only stage that touches a store. Everything leaves here **in the scene's
4//! system** - reprojected when the object's own differs - and a
5//! surface in time leaves carried onto the depth axis by the scene's time
6//! factor, so [`crate::assemble`] needs no source.
7//!
8//! Two halves, two owners: [`surfaces`] and [`wells`].
9//! This file only loops; neither half decides anything for the other.
10
11pub mod cache;
12pub mod finite;
13pub mod frame;
14pub mod grids;
15pub mod package;
16pub mod surfaces;
17pub mod wells;
18
19pub use cache::{PackageCache, Work};
20
21use std::collections::HashSet;
22
23use crate::read::cache::HeadSlot;
24use crate::read::wells::HeadRead;
25use crate::spatial::Rect;
26
27use energyml_domain::ArraySource as ValueSource;
28use energyml_domain::reproject::Reprojector;
29
30use energyml_core::registry::DynObject;
31use energyml_domain::color::GraphicalSet;
32use energyml_domain::package_index::PackageIndex;
33
34use crate::model::{Asked, Contribution, SectionTrace, System};
35
36/// The frames that may represent a wellbore, by its lowercase uuid.
37pub type FramesOf<'a> = dyn Fn(&str) -> Vec<Box<dyn DynObject>> + 'a;
38
39/// The colour the file gives an object, by its **full uri** (version included
40/// when it declares one - two versions are two objects), as a face that has
41/// already resolved it answers - `None` when the file gives none.
42pub type ColourOf<'a> = dyn Fn(&str) -> Option<String> + 'a;
43
44/// What the package says that a [`ValueSource`] cannot be asked for - each
45/// needs a listing of the package, which only the face holding it can make.
46#[derive(Default)]
47pub struct PackageContext<'a> {
48    /// The package's `GraphicalInformationSet`s, read, each with its uuid -
49    /// where the colour of a stratigraphic unit and of a surface come from.
50    /// Empty: nothing has a colour from the file.
51    pub graphics: &'a [(String, GraphicalSet)],
52    /// The frames that may represent a wellbore (a lowercase uuid) -
53    /// `strata_by_md::of_representation`'s `frames_of`, asked only for an
54    /// object that states no unit itself. `None`: a silent object is not
55    /// painted from its sisters.
56    pub frames_of: Option<&'a FramesOf<'a>>,
57    /// The package's index - with it, a surface's colour follows the whole
58    /// inheritance order (collections, models), the one the 3D viewer's ladder
59    /// walks (`color_inherit`). `None`: the representation, its interpretation
60    /// and its feature only.
61    pub index: Option<&'a PackageIndex>,
62    /// The face's own resolver of the file's colours, when it has one in cache -
63    /// the front end holds every drawn object's ladder already (the front end can pass
64    /// its resolver to the calculation so that the search is not redone). Given, it is asked **instead of** the
65    /// cascade: no set is merged, no walk is made. It answers the **file's**
66    /// colour (the ladder's), never a colour chosen for the scene - those travel
67    /// in the view (`View::colours`).
68    pub colour_of: Option<&'a ColourOf<'a>>,
69}
70
71/// What reading needs to know of the scene, beyond the sections.
72pub struct Scene<'a> {
73    /// The scene's system - what every point is reprojected into. `None`: a
74    /// scene with none; an object that states one is then refused by name.
75    pub system: Option<&'a System>,
76    /// The scene's time scale (`model::Inputs::time_factor`).
77    pub time_factor: Option<f64>,
78    /// The geodesy backend for a reprojection (`energyml-proj` in the faces).
79    pub reprojector: &'a dyn Reprojector,
80}
81
82/// How much a section's windows are grown beyond its widest side, as a share -
83/// the slack that keeps rounding from dropping a head that sits exactly on the
84/// corridor's edge. Zero would be exact on paper and wrong on a float.
85pub const CORRIDOR_SLACK: f64 = 1e-9;
86
87/// The windows a head must fall in to be read: for every segment of every
88/// section, its bounding box grown by the section's widest side (plus
89/// [`CORRIDOR_SLACK`]) - a superset of the region `Corridor::project` keeps,
90/// refuses for a reason, or is within reach of: a head that classifies is
91/// within `max(left, right)` of a segment or of an end. `None` when there is
92/// no section to bound by.
93pub fn corridor_windows(sections: &[SectionTrace]) -> Option<Vec<Rect>> {
94    let mut windows = Vec::new();
95    for st in sections {
96        let reach = st.left.max(st.right);
97        let margin = reach * (1.0 + CORRIDOR_SLACK) + 1e-6;
98        if !margin.is_finite() {
99            // A width that is not a number: `assemble` will refuse the section;
100            // reading everything is the answer that does not guess.
101            return None;
102        }
103        for pair in st.trace.windows(2) {
104            windows.push(Rect::new(pair[0], pair[1])?.grown(margin));
105        }
106    }
107    (!windows.is_empty()).then_some(windows)
108}
109
110/// One package's contribution: its wells read, its surfaces and grids cut
111/// along every section, and what could not be read.
112///
113/// One object that fails never fails the call: it becomes a
114/// [`crate::model::Refused`].
115///
116/// `well_context` is what a `ValueSource` cannot be asked for - the package's colour
117/// sets and the frames of a wellbore, both needing a listing of the package -
118/// which the face that holds the package supplies (`read::wells`). Its default
119/// gives units no colour and does not paint a silent frame from its sisters.
120pub fn contribution(
121    source: &dyn ValueSource,
122    asked: &Asked,
123    sections: &[SectionTrace],
124    scene: &Scene<'_>,
125    well_context: &PackageContext<'_>,
126) -> Contribution {
127    contribution_cached(
128        source,
129        asked,
130        sections,
131        scene,
132        well_context,
133        &mut PackageCache::new(),
134    )
135}
136
137/// [`contribution`], answered from what `cache` kept of this package where it
138/// is still exact (`cache` explains when) - the one implementation: the
139/// uncached call is this one with a cache thrown away.
140// The closures return a refusal, which carries the object's whole identity on
141// purpose (as `read_surface` says).
142#[allow(clippy::result_large_err)]
143pub fn contribution_cached(
144    source: &dyn ValueSource,
145    asked: &Asked,
146    sections: &[SectionTrace],
147    scene: &Scene<'_>,
148    well_context: &PackageContext<'_>,
149    cache: &mut PackageCache,
150) -> Contribution {
151    contribution_bounded(
152        source,
153        asked,
154        sections,
155        scene,
156        well_context,
157        cache,
158        Bound::Corridor,
159    )
160}
161
162/// Which wells of a selection are read.
163#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164pub enum Bound {
165    /// Only the wells whose head is near a section (the default): the others
166    /// are told by their head, read from no array, and `assemble` refuses them
167    /// as too far exactly as it would have after a full read.
168    Corridor,
169    /// Every asked well, in full - what the corridor's answer is checked against.
170    Everything,
171}
172
173/// [`contribution_cached`], saying how far the wells are read ([`Bound`]).
174#[allow(clippy::result_large_err, clippy::too_many_arguments)]
175pub fn contribution_bounded(
176    source: &dyn ValueSource,
177    asked: &Asked,
178    sections: &[SectionTrace],
179    scene: &Scene<'_>,
180    well_context: &PackageContext<'_>,
181    cache: &mut PackageCache,
182    bound: Bound,
183) -> Contribution {
184    cache.hold_for(&asked.store, asked.revision, scene);
185    let mut out = Contribution {
186        store: asked.store.clone(),
187        ..Contribution::default()
188    };
189    let read_in_full = |cache: &mut PackageCache, uri: &str, out: &mut Contribution| {
190        // Nothing that is not finite leaves here (`finite`): a `NaN` becomes a
191        // `null` in JSON, and the catalogue refused every package for it.
192        match cache
193            .well(uri, || {
194                wells::read_well_with(source, &asked.store, uri, scene, well_context)
195            })
196            .and_then(finite::finite_well)
197        {
198            Ok((well, note)) => {
199                out.wells.push(well);
200                out.notes.extend(note);
201            }
202            Err(refused) => out.refused.push(refused),
203        }
204    };
205    match corridor_windows(sections).filter(|_| bound == Bound::Corridor && !asked.wells.is_empty()) {
206        // No section, nothing to bound by: every asked well is read.
207        None => {
208            for uri in &asked.wells {
209                read_in_full(cache, uri, &mut out);
210            }
211        }
212        Some(windows) => {
213            // The heads first (no array), then only the wells whose head is near
214            // a section.
215            let slots: Vec<Option<(usize, HeadRead)>> = asked
216                .wells
217                .iter()
218                .map(|uri| {
219                    match cache.head(uri, || wells::read_head(source, &asked.store, uri, scene).ok()) {
220                        HeadSlot::Head { at, head } => Some((*at, (**head).clone())),
221                        HeadSlot::Unusable => None,
222                    }
223                })
224                .collect();
225            let near = cache.heads_in(&windows);
226            // A wellbore with a member near a section is read whole, so that
227            // the fold ranks all its members as it always did. One whose members
228            // are **all** far is neither read nor folded: each member is a
229            // head-only stub, refused on its own row as too far (no
230            // merging far away). The catalogue's drawn part is
231            // the same; the rows of far wells lose their `Merged`, and a far
232            // well whose full read would have failed (or noted a dropped node)
233            // now says "farther than" instead - both accepted.
234            let near_wellbores: HashSet<&str> = slots
235                .iter()
236                .flatten()
237                .filter(|(at, _)| near.contains(at))
238                .filter_map(|(_, head)| head.wellbore.as_deref())
239                .collect();
240            for (uri, slot) in asked.wells.iter().zip(&slots) {
241                match slot {
242                    Some((at, head))
243                        if !near.contains(at)
244                            && !head
245                                .wellbore
246                                .as_deref()
247                                .is_some_and(|wellbore| near_wellbores.contains(wellbore)) =>
248                    {
249                        out.wells.push(head.stub());
250                    }
251                    _ => read_in_full(cache, uri, &mut out),
252                }
253            }
254        }
255    }
256    // Every set of the package, merged once for all its surfaces - the one
257    // reading of several (`GraphicalSet::merged`, as a front end's graphical-set reader).
258    // Not when the face resolves colours itself: nothing would read the sets;
259    // and only when a surface is read at all.
260    let mut colours: Option<Option<GraphicalSet>> = None;
261    for uri in &asked.surfaces {
262        let read = cache.surface(uri, sections, || {
263            let set = colours
264                .get_or_insert_with(|| {
265                    (well_context.colour_of.is_none() && !well_context.graphics.is_empty()).then(|| {
266                        GraphicalSet::merged(well_context.graphics.iter().map(|(_, set)| set.clone()))
267                    })
268                })
269                .as_ref();
270            let colouring = surfaces::Colouring {
271                set,
272                index: well_context.index,
273                resolver: well_context.colour_of,
274            };
275            surfaces::prepare_surface(source, &asked.store, uri, scene, colouring)
276        });
277        match read {
278            Ok(mut surface) => {
279                finite::finite_surface(&mut surface);
280                out.surfaces.push(surface);
281            }
282            Err(refused) => out.refused.push(refused),
283        }
284    }
285    out
286}