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}