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}