Skip to main content

energyml_section/read/
cache.rs

1//! What one package keeps between two calculations - so that a face which
2//! recomputes (the front end, on every change of trace or selection) reads each
3//! object once.
4//!
5//! The calculation can exploit the optimisations the front end already caches, so
6//! as not to recompute what the front end already has. The front end's own caches - its
7//! package index, its colour walks - come in through
8//! [`crate::read::PackageContext`]; this is the section's own, kept by the face
9//! next to the package (one per package, as the wasm `Session` is).
10//!
11//! # What depends on what
12//!
13//! | kept | depends on | so a change of trace… |
14//! |---|---|---|
15//! | a well, read ([`WellRead`]) | the package, the scene's system | reads nothing again: wells are projected in `assemble` |
16//! | a surface, placed ([`Prepared`]) | the package, the scene's system | reads nothing again |
17//! | a surface's cut along one line | the above **and that line** (its polyline, nothing else) | cuts only the lines it has not cut: another section, a cut; a width, a key, an order, none |
18//! | a well's head (`HeadSlot`) and the spatial index over the heads | the package, the scene's system | reads nothing again: the sections only choose which wells to read |
19//!
20//! The package is told by its **revision** (`Asked::revision`, the store's):
21//! another revision, another store id or another scene and everything is
22//! dropped - a stale well is a well drawn where the file no longer puts it.
23//! A cut depends on the **line** and on nothing else of the section - not its
24//! corridor, not its key, not the other sections - so it is kept under
25//! `(surface, line)`, several per surface (ADR-0010),
26//! and going A → B → A cuts nothing the third time. A line the surface misses
27//! is kept too: it is an answer. What is kept carries no section key; the key
28//! of the section asking is written on it when it is served.
29//!
30//! **Only what read is kept**: a refusal is said again, never remembered (the
31//! rule of the wasm `PieceCache`), so a backend loaded since can answer.
32//!
33//! [`Work`] counts what was done, so the tests count work and never time
34//! (`energyml-domain`'s `color_scopes` does the same).
35
36use std::collections::{HashMap, HashSet};
37use std::sync::Arc;
38
39use crate::model::{SectionTrace, SurfaceCut, SurfaceRead, WellRead};
40use crate::read::surfaces::Prepared;
41use crate::read::wells::HeadRead;
42use crate::spatial::{Index, Rect};
43
44/// What a calculation did, summed since the cache was made.
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46pub struct Work {
47    /// Wells read from the package.
48    pub wells_read: usize,
49    /// Surfaces read from the package and placed in the scene.
50    pub surfaces_read: usize,
51    /// Surfaces cut along at least one line they had not been cut along.
52    pub surfaces_cut: usize,
53    /// Lines cut, one surface each: a surface along three new lines is
54    /// `surfaces_cut + 1` and `sections_cut + 3`.
55    pub sections_cut: usize,
56    /// Objects answered from the cache without any of the above.
57    pub reused: usize,
58    /// Heads read (no array) to decide which wells the corridor asks for.
59    pub heads_computed: usize,
60    /// Windows asked of the spatial index over the heads.
61    pub spatial_queries: usize,
62}
63
64/// What the cache knows of one well's head.
65#[derive(Clone, Debug)]
66pub(crate) enum HeadSlot {
67    /// The head, read, and where the well sits in the spatial index.
68    Head { at: usize, head: Box<HeadRead> },
69    /// The head could not be told (a refusal, a non-finite position): the
70    /// well is read in full, and the full read says what is wrong.
71    Unusable,
72}
73
74/// What the cache is exact for: the package's identity and revision, and the
75/// scene its objects were placed in.
76#[derive(Clone, Debug, PartialEq)]
77struct Key {
78    store: String,
79    revision: u64,
80    epsg: Option<u64>,
81    unit: Option<String>,
82    time_factor: Option<u64>,
83}
84
85/// A section's line, as the cut's identity: the bits of its polyline's `f64`s -
86/// an exact key, never a rounded one.
87type Line = Vec<[u64; 2]>;
88
89fn line_of(section: &SectionTrace) -> Line {
90    section
91        .trace
92        .iter()
93        .map(|p| [p[0].to_bits(), p[1].to_bits()])
94        .collect()
95}
96
97/// One package's reads, kept - see the module.
98#[derive(Default)]
99pub struct PackageCache {
100    key: Option<Key>,
101    wells: HashMap<String, WellRead>,
102    surfaces: HashMap<String, Arc<Prepared>>,
103    /// The cuts of each surface by the line they were made along; `None` is
104    /// "the line misses the surface". See [`Line`].
105    cuts: HashMap<(String, Line), Option<SurfaceCut>>,
106    work: Work,
107    /// Every well's head, by uri.
108    heads: HashMap<String, HeadSlot>,
109    /// The heads as points; the payload is the position in `head_uris`.
110    spatial: Index<usize>,
111    head_uris: Vec<String>,
112}
113
114impl PackageCache {
115    /// An empty cache.
116    pub fn new() -> Self {
117        Self::default()
118    }
119
120    /// What was done since the cache was made.
121    pub fn work(&self) -> Work {
122        self.work
123    }
124
125    /// Drop everything - what a face does when it closes the package.
126    pub fn clear(&mut self) {
127        self.key = None;
128        self.wells.clear();
129        self.surfaces.clear();
130        self.cuts.clear();
131        self.heads.clear();
132        self.spatial = Index::new();
133        self.head_uris.clear();
134    }
135
136    /// Keep what is exact for this package, revision and scene; drop the rest.
137    pub(crate) fn hold_for(&mut self, store: &str, revision: u64, scene: &crate::read::Scene<'_>) {
138        let key = Key {
139            store: store.to_string(),
140            revision,
141            epsg: scene.system.map(|s| s.epsg),
142            unit: scene.system.map(|s| s.unit.clone()),
143            time_factor: scene.time_factor.map(f64::to_bits),
144        };
145        if self.key.as_ref() != Some(&key) {
146            self.clear();
147            self.key = Some(key);
148        }
149    }
150
151    /// A well, read once.
152    pub(crate) fn well<E>(
153        &mut self,
154        uri: &str,
155        read: impl FnOnce() -> Result<WellRead, E>,
156    ) -> Result<WellRead, E> {
157        if let Some(kept) = self.wells.get(uri) {
158            self.work.reused += 1;
159            return Ok(kept.clone());
160        }
161        let well = read()?;
162        self.work.wells_read += 1;
163        self.wells.insert(uri.to_string(), well.clone());
164        Ok(well)
165    }
166
167    /// The head of a well, computed once (`read` tells it; no array is read).
168    pub(crate) fn head(&mut self, uri: &str, read: impl FnOnce() -> Option<HeadRead>) -> &HeadSlot {
169        if !self.heads.contains_key(uri) {
170            self.work.heads_computed += 1;
171            let slot = match read() {
172                Some(head) if head.head.iter().all(|v| v.is_finite()) => {
173                    let at = self.head_uris.len();
174                    self.head_uris.push(uri.to_string());
175                    if let Some(rect) = Rect::point([head.head[0], head.head[1]]) {
176                        self.spatial.insert(rect, at);
177                    }
178                    HeadSlot::Head {
179                        at,
180                        head: Box::new(head),
181                    }
182                }
183                _ => HeadSlot::Unusable,
184            };
185            self.heads.insert(uri.to_string(), slot);
186        }
187        &self.heads[uri]
188    }
189
190    /// The positions (`HeadSlot::Head::at`) of the heads inside any of `windows`.
191    pub(crate) fn heads_in(&mut self, windows: &[Rect]) -> HashSet<usize> {
192        let mut found = HashSet::new();
193        for window in windows {
194            self.work.spatial_queries += 1;
195            self.spatial.visit(window, &mut |at| {
196                found.insert(*at);
197            });
198        }
199        found
200    }
201
202    /// A surface cut along `sections`: placed once, and cut once **per line** -
203    /// only the lines it has not been cut along are cut now. The answer is the
204    /// one a fresh cache gives (the cuts in the order of `sections`, a missed
205    /// line left out, each labelled with the key of the section asking).
206    pub(crate) fn surface<E>(
207        &mut self,
208        uri: &str,
209        sections: &[SectionTrace],
210        prepare: impl FnOnce() -> Result<Prepared, E>,
211    ) -> Result<SurfaceRead, E> {
212        let lines: Vec<Line> = sections.iter().map(line_of).collect();
213        // One representative per line the surface has not been cut along.
214        let mut seen: HashSet<&Line> = HashSet::new();
215        let missing: Vec<usize> = lines
216            .iter()
217            .enumerate()
218            .filter(|(_, line)| {
219                !self.cuts.contains_key(&(uri.to_string(), (*line).clone())) && seen.insert(line)
220            })
221            .map(|(at, _)| at)
222            .collect();
223
224        let prepared = match self.surfaces.get(uri) {
225            Some(kept) => Arc::clone(kept),
226            None => {
227                let placed = Arc::new(prepare()?);
228                self.work.surfaces_read += 1;
229                self.surfaces.insert(uri.to_string(), Arc::clone(&placed));
230                placed
231            }
232        };
233        if missing.is_empty() {
234            self.work.reused += 1;
235        } else {
236            let asked: Vec<SectionTrace> = missing.iter().map(|at| sections[*at].clone()).collect();
237            let made = crate::read::surfaces::cut_prepared(&prepared, &asked);
238            self.work.surfaces_cut += 1;
239            self.work.sections_cut += asked.len();
240            for (at, one) in missing.iter().zip(&asked) {
241                let found = made.cuts.iter().find(|cut| cut.section == one.key).cloned();
242                self.cuts.insert((uri.to_string(), lines[*at].clone()), found);
243            }
244        }
245
246        let cuts = sections
247            .iter()
248            .zip(&lines)
249            .filter_map(|(section, line)| {
250                let kept = self.cuts.get(&(uri.to_string(), line.clone()))?.as_ref()?;
251                Some(SurfaceCut {
252                    section: section.key,
253                    ..kept.clone()
254                })
255            })
256            .collect();
257        Ok(SurfaceRead {
258            object: prepared.object.clone(),
259            role: prepared.role,
260            time: prepared.time,
261            colour: prepared.colour.clone(),
262            age: prepared.age,
263            feature: prepared.feature.clone(),
264            cuts,
265        })
266    }
267}