1use super::build::InelasticTableOptions;
6use super::density::LossModel;
7use super::{invalid, EnergyAxis, InelasticTableError};
8use rayon::prelude::*;
9
10pub const RATE_TOLERANCE: f64 = 0.01;
25
26pub const RATE_WINDOW_EV: (f64, f64) = (2.0, 20.0);
36
37pub const DEFAULT_RATE_REFINEMENT_MAX_ROWS: usize = 1000;
45
46#[derive(Debug, Clone, Copy, PartialEq)]
49pub struct RateRefinement {
50 pub window_ev: (f64, f64),
53 pub tolerance: f64,
56 pub max_rows: usize,
58}
59
60impl Default for RateRefinement {
61 fn default() -> Self {
64 Self {
65 window_ev: RATE_WINDOW_EV,
66 tolerance: RATE_TOLERANCE,
67 max_rows: DEFAULT_RATE_REFINEMENT_MAX_ROWS,
68 }
69 }
70}
71
72pub(super) fn row_rate<M: LossModel>(
77 penn: &M,
78 axis: EnergyAxis,
79 t: f64,
80) -> Result<f64, InelasticTableError> {
81 if axis == EnergyAxis::BandBottom && t <= 0.0 {
82 return Ok(0.0);
86 }
87 if t <= penn.loss_min_ev(t) {
88 return Ok(0.0);
92 }
93 let inv = penn.inverse_imfp(t)?;
94 Ok(if inv > 0.0 { inv } else { 0.0 })
95}
96
97fn rate_test_points(a: f64, b: f64) -> [f64; 3] {
111 let m = 0.5 * (a + b);
112 [0.5 * (a + m), m, 0.5 * (m + b)]
113}
114
115fn rate_error_peak(a: f64, b: f64, e: [f64; 3]) -> Option<f64> {
125 let s = [0.0, e[0], e[1], e[2], 0.0];
126 let k = [2, 1, 3]
128 .into_iter()
129 .fold(2, |k, j| if s[j].abs() > s[k].abs() { j } else { k });
130 let (l, c, r) = (s[k - 1], s[k], s[k + 1]);
131 let curvature = l - 2.0 * c + r;
132 if c == 0.0 || curvature == 0.0 || !curvature.is_finite() {
133 return None;
134 }
135 let h = 0.25 * (b - a);
136 let xk = a + h * k as f64;
137 let v = xk + 0.5 * h * (l - r) / curvature;
138 (v > a && v < b && v.is_finite()).then_some(v)
139}
140
141#[derive(Debug, Clone, Copy)]
143struct RateMiss {
144 rel: f64,
147 at: f64,
149 interpolated: f64,
151 exact: f64,
153}
154
155fn refined_energy_grid<M: LossModel>(
159 penn: &M,
160 options: &InelasticTableOptions,
161) -> Result<Option<Vec<f64>>, InelasticTableError> {
162 let Some(r) = options.rate_refinement else {
163 return Ok(None);
164 };
165 let ef = penn.fermi_energy_ev();
166 let (lo, hi) = (ef + r.window_ev.0, ef + r.window_ev.1);
167 let grid = &options.energy_ev;
168 let overlaps = |a: f64, b: f64| b > lo && a < hi;
169 let onset = penn.rate_onset_ev().map(|w| ef + w).filter(|&o| o >= lo);
173 let tested = |a: f64, b: f64| overlaps(a, b) && onset.is_none_or(|o| a >= o);
174 let Some(first) = grid.windows(2).position(|w| overlaps(w[0], w[1])) else {
176 return Ok(Some(grid.clone()));
177 };
178 let last = grid
179 .windows(2)
180 .rposition(|w| overlaps(w[0], w[1]))
181 .expect("one cell");
182 let rate = |e: f64| row_rate(penn, options.axis, e - ef);
183 let mut x: Vec<f64> = grid[first..=last + 1].to_vec();
186 let mut y: Vec<f64> = x.par_iter().map(|&e| rate(e)).collect::<Result<_, _>>()?;
187 let mut todo: Vec<bool> = x.windows(2).map(|w| tested(w[0], w[1])).collect();
190 let mut added = 0usize;
191 loop {
192 let cells: Vec<usize> = (0..todo.len()).filter(|&i| todo[i]).collect();
193 if let Some(&i) = cells.iter().find(|&&i| {
199 let m = 0.5 * (x[i] + x[i + 1]);
200 !(m > x[i] && m < x[i + 1])
201 }) {
202 return Err(invalid(
203 "rate refinement",
204 format!(
205 "cannot hold the rate to {} between E - E_F = {} and {} eV: cell {}..{} eV \
206 is exhausted to rounding and its parent still failed at a test point (end \
207 rates {:e} and {:e} 1/m)",
208 r.tolerance,
209 r.window_ev.0,
210 r.window_ev.1,
211 x[i],
212 x[i + 1],
213 y[i],
214 y[i + 1]
215 ),
216 ));
217 }
218 if cells.is_empty() {
219 break;
220 }
221 let points: Vec<[f64; 3]> = cells
225 .iter()
226 .map(|&i| rate_test_points(x[i], x[i + 1]))
227 .collect();
228 let exact: Vec<f64> = points
229 .par_iter()
230 .flat_map_iter(|p| p.iter().copied())
231 .map(rate)
232 .collect::<Result<_, _>>()?;
233 let miss = |i: usize, e: f64, m: f64| {
238 let lin = y[i] + (e - x[i]) / (x[i + 1] - x[i]) * (y[i + 1] - y[i]);
239 let d = lin - m;
240 let rel = if d == 0.0 {
241 0.0
242 } else if m > 0.0 {
243 d / m
244 } else {
245 f64::INFINITY
246 };
247 RateMiss {
248 rel,
249 at: e,
250 interpolated: lin,
251 exact: m,
252 }
253 };
254 let mut worst: Vec<RateMiss> = Vec::with_capacity(cells.len());
255 let mut peaks: Vec<(usize, f64)> = Vec::new();
256 for (k, (&i, (p, ex))) in cells
257 .iter()
258 .zip(points.iter().zip(exact.as_chunks::<3>().0))
259 .enumerate()
260 {
261 let m: Vec<RateMiss> = p.iter().zip(ex).map(|(&e, &v)| miss(i, e, v)).collect();
262 let w = m
263 .iter()
264 .fold(m[1], |a, &b| if b.rel.abs() > a.rel.abs() { b } else { a });
265 if w.rel.abs() <= r.tolerance {
266 if let Some(e) = rate_error_peak(x[i], x[i + 1], [m[0].rel, m[1].rel, m[2].rel]) {
267 peaks.push((k, e));
268 }
269 }
270 worst.push(w);
271 }
272 let peak_exact: Vec<f64> = peaks
275 .par_iter()
276 .map(|&(_, e)| rate(e))
277 .collect::<Result<_, _>>()?;
278 for (&(k, e), &v) in peaks.iter().zip(&peak_exact) {
279 let p = miss(cells[k], e, v);
280 if p.rel.abs() > worst[k].rel.abs() {
281 worst[k] = p;
282 }
283 }
284 let split: Vec<bool> = worst.iter().map(|w| w.rel.abs() > r.tolerance).collect();
287 let n_split = split.iter().filter(|&&s| s).count();
288 if n_split == 0 {
289 break;
290 }
291 if added + n_split > r.max_rows {
292 let k = split.iter().position(|&s| s).expect("one split");
293 let i = cells[k];
294 let RateMiss {
295 at,
296 interpolated: lin,
297 exact: m,
298 ..
299 } = worst[k];
300 return Err(invalid(
301 "rate refinement",
302 format!(
303 "more than {} rows needed to hold the rate to {} between E - E_F = {} and \
304 {} eV (cell {}..{} eV still fails at {} eV: interpolated {:e}, exact {:e} \
305 1/m)",
306 r.max_rows,
307 r.tolerance,
308 r.window_ev.0,
309 r.window_ev.1,
310 x[i],
311 x[i + 1],
312 at,
313 lin,
314 m
315 ),
316 ));
317 }
318 added += n_split;
319 let mut nx = Vec::with_capacity(x.len() + n_split);
320 let mut ny = Vec::with_capacity(x.len() + n_split);
321 let mut ntodo = Vec::with_capacity(todo.len() + n_split);
322 let mut k = 0;
323 for i in 0..todo.len() {
324 nx.push(x[i]);
325 ny.push(y[i]);
326 if k < cells.len() && cells[k] == i {
327 if split[k] {
328 let m = points[k][1];
330 nx.push(m);
331 ny.push(exact[3 * k + 1]);
332 ntodo.push(tested(x[i], m));
333 ntodo.push(tested(m, x[i + 1]));
334 } else {
335 ntodo.push(false);
336 }
337 k += 1;
338 } else {
339 ntodo.push(false);
340 }
341 }
342 nx.push(x[x.len() - 1]);
343 ny.push(y[y.len() - 1]);
344 x = nx;
345 y = ny;
346 todo = ntodo;
347 }
348 let mut out = Vec::with_capacity(grid.len() + added);
349 out.extend_from_slice(&grid[..first]);
350 out.extend_from_slice(&x);
351 out.extend_from_slice(&grid[last + 2..]);
352 Ok(Some(out))
353}
354
355pub(super) fn with_refined_grid<'a, M: LossModel>(
358 penn: &M,
359 options: &'a InelasticTableOptions,
360) -> Result<(std::borrow::Cow<'a, InelasticTableOptions>, usize), InelasticTableError> {
361 Ok(match refined_energy_grid(penn, options)? {
362 None => (std::borrow::Cow::Borrowed(options), 0),
363 Some(energy_ev) => {
364 let added = energy_ev.len() - options.energy_ev.len();
365 let mut o = options.clone();
366 o.energy_ev = energy_ev;
367 (std::borrow::Cow::Owned(o), added)
368 }
369 })
370}
371
372pub(super) fn refinement_provenance(options: &InelasticTableOptions, added: usize) -> String {
375 match &options.rate_refinement {
376 None => String::new(),
377 Some(r) => format!(
378 "; rate refinement (#339): {added} rows added where the linear interpolation of \
379 the rate missed it by more than {} at a cell's quartiles, midpoint or estimated \
380 error peak, E - E_F = {}..{} eV",
381 r.tolerance, r.window_ev.0, r.window_ev.1
382 ),
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::electron::data::ElectronDataError;
390 use crate::electron::inelastic::mermin::LOWEST_LOSS_FRACTION;
391 use crate::electron::inelastic::model::PennInelastic;
392
393 struct SteepRate(fn(f64) -> f64);
396
397 impl LossModel for SteepRate {
398 fn elf_min_ev(&self) -> f64 {
399 0.05
400 }
401 fn fermi_energy_ev(&self) -> f64 {
402 10.0
403 }
404 fn diimfp(&self, energy_ev: f64, loss_ev: f64) -> Result<f64, ElectronDataError> {
405 Ok(if loss_ev > 0.0 && loss_ev <= energy_ev {
406 1.0
407 } else {
408 0.0
409 })
410 }
411 fn inverse_imfp(&self, energy_ev: f64) -> Result<f64, ElectronDataError> {
412 Ok((self.0)(energy_ev))
413 }
414 }
415
416 fn band_options(grid: Vec<f64>) -> InelasticTableOptions {
417 InelasticTableOptions::new(grid)
418 .with_axis(EnergyAxis::BandBottom)
419 .with_rate_refinement(RateRefinement::default())
420 }
421
422 fn steep(t: f64) -> f64 {
425 1e6 * t.powi(3) / (1.0 + (t / 15.0).powi(4))
426 }
427
428 #[test]
429 fn rate_refinement_adds_rows_only_in_the_window_and_meets_the_test() {
430 let model = SteepRate(steep);
431 let grid = vec![5.0, 9.0, 14.0, 22.0, 35.0, 60.0, 100.0];
432 let o = band_options(grid.clone());
433 let refined = refined_energy_grid(&model, &o).unwrap().unwrap();
434 assert!(refined.len() > grid.len());
435 assert!(refined.windows(2).all(|w| w[1] > w[0]));
436 let mut it = refined.iter();
438 assert!(grid.iter().all(|g| it.any(|r| r == g)));
439 let (lo, hi) = (10.0 + RATE_WINDOW_EV.0, 10.0 + RATE_WINDOW_EV.1);
440 for e in refined.iter().filter(|e| !grid.contains(e)) {
443 let k = grid.partition_point(|g| g < e);
444 let (a, b) = (grid[k - 1], grid[k]);
445 assert!(b > lo && a < hi, "{e} in the cell {a}..{b}");
446 }
447 let rate = |e: f64| row_rate(&model, EnergyAxis::BandBottom, e - 10.0).unwrap();
450 for w in refined.windows(2) {
451 if w[1] > lo && w[0] < hi {
452 for e in rate_test_points(w[0], w[1]) {
453 let m = rate(e);
454 let lin = rate(w[0]) + (e - w[0]) / (w[1] - w[0]) * (rate(w[1]) - rate(w[0]));
455 assert!((lin - m).abs() <= RATE_TOLERANCE * m, "{w:?} at {e}");
456 }
457 }
458 }
459 let mut off = o.clone();
461 off.rate_refinement = None;
462 assert_eq!(refined_energy_grid(&model, &off).unwrap(), None);
463 }
464
465 #[test]
466 fn rate_refinement_is_the_same_on_any_thread_count() {
467 let model = SteepRate(steep);
468 let o = band_options(vec![5.0, 9.0, 14.0, 22.0, 35.0, 60.0, 100.0]);
469 let on = |threads: usize| {
470 rayon::ThreadPoolBuilder::new()
471 .num_threads(threads)
472 .build()
473 .unwrap()
474 .install(|| refined_energy_grid(&model, &o).unwrap())
475 };
476 let one = on(1);
477 assert_eq!(on(3), one);
478 assert_eq!(on(8), one);
479 }
480
481 #[test]
482 fn rate_refinement_of_a_step_stops_at_the_row_bound() {
483 let model = SteepRate(|t| if t < 7.3 { 1e6 } else { 2e6 });
486 let mut o = band_options(vec![5.0, 9.0, 14.0, 22.0, 35.0, 60.0, 100.0]);
487 o.rate_refinement.as_mut().unwrap().max_rows = 30;
488 let err = refined_energy_grid(&model, &o).unwrap_err().to_string();
489 assert!(err.contains("more than 30 rows"), "{err}");
490 assert!(err.contains("cell 17."), "{err}");
491 }
492
493 #[test]
494 fn rate_refinement_of_a_step_is_an_error_at_the_default_bound() {
495 let model = SteepRate(|t| if t < 7.3 { 1e6 } else { 2e6 });
500 let o = band_options(vec![5.0, 9.0, 14.0, 22.0, 35.0, 60.0, 100.0]);
501 assert_eq!(
502 o.rate_refinement.unwrap().max_rows,
503 DEFAULT_RATE_REFINEMENT_MAX_ROWS
504 );
505 let err = refined_energy_grid(&model, &o).unwrap_err().to_string();
506 assert!(err.contains("exhausted to rounding"), "{err}");
507 assert!(err.contains("cell 17."), "{err}");
508 }
509
510 #[test]
511 fn rate_test_points_are_the_quartiles_and_midpoint() {
512 let p = rate_test_points(2.0, 6.0);
513 assert_eq!(p, [3.0, 4.0, 5.0]);
514 let (a, b) = (0.1, 0.7);
516 let p = rate_test_points(a, b);
517 assert_eq!(p[0], rate_test_points(a, p[1])[1]);
518 assert_eq!(p[2], rate_test_points(p[1], b)[1]);
519 }
520
521 #[test]
522 fn rate_error_peak_is_the_vertex_of_the_parabola_at_the_largest_sample() {
523 let g = |x: f64| 1.69 - (x - 1.3) * (x - 1.3);
527 let v = rate_error_peak(0.0, 4.0, [g(1.0), g(2.0), g(3.0)]).unwrap();
528 assert!((v - 1.3).abs() < 1e-12, "{v}");
529 let v = rate_error_peak(0.0, 4.0, [-g(1.0), -g(2.0), -g(3.0)]).unwrap();
531 assert!((v - 1.3).abs() < 1e-12, "{v}");
532 let v = rate_error_peak(0.0, 4.0, [0.75, 1.0, 0.75]).unwrap();
534 assert!((v - 2.0).abs() < 1e-12, "{v}");
535 assert_eq!(rate_error_peak(0.0, 4.0, [0.0; 3]), None);
536 }
537
538 #[test]
539 fn a_window_above_the_grid_adds_no_rows() {
540 let model = SteepRate(steep);
541 let grid = vec![1.0, 5.0, 11.0];
542 let o = band_options(grid.clone());
543 assert_eq!(refined_energy_grid(&model, &o).unwrap(), Some(grid));
544 }
545
546 struct OnsetRate {
552 elf_min: f64,
553 loss_min_fraction: Option<f64>,
554 onset: Option<f64>,
555 rate: fn(f64) -> f64,
556 }
557
558 impl OnsetRate {
559 fn penn(elf_min: f64, rate: fn(f64) -> f64) -> Self {
562 Self {
563 elf_min,
564 loss_min_fraction: None,
565 onset: Some(elf_min),
566 rate,
567 }
568 }
569 }
570
571 impl LossModel for OnsetRate {
572 fn elf_min_ev(&self) -> f64 {
573 self.elf_min
574 }
575 fn loss_min_ev(&self, energy_ev: f64) -> f64 {
576 self.loss_min_fraction
577 .map_or(self.elf_min, |f| f * energy_ev)
578 }
579 fn rate_onset_ev(&self) -> Option<f64> {
580 self.onset
581 }
582 fn fermi_energy_ev(&self) -> f64 {
583 10.0
584 }
585 fn diimfp(&self, energy_ev: f64, loss_ev: f64) -> Result<f64, ElectronDataError> {
586 Ok(if loss_ev > 0.0 && loss_ev <= energy_ev {
587 1.0
588 } else {
589 0.0
590 })
591 }
592 fn inverse_imfp(&self, energy_ev: f64) -> Result<f64, ElectronDataError> {
593 Ok((self.rate)(energy_ev))
594 }
595 }
596
597 fn onset_steep(t: f64) -> f64 {
600 let s = (t - 6.0).max(0.0);
601 1e6 * s * (1.0 + s * s) / (1.0 + (s / 15.0).powi(4))
602 }
603
604 fn assert_cells_pass<M: LossModel>(model: &M, refined: &[f64], from: f64) {
607 let ef = model.fermi_energy_ev();
608 let (lo, hi) = (ef + RATE_WINDOW_EV.0, ef + RATE_WINDOW_EV.1);
609 let rate = |e: f64| row_rate(model, EnergyAxis::BandBottom, e - ef).unwrap();
610 for w in refined.windows(2) {
611 if w[1] > lo && w[0] < hi && w[0] >= from {
612 for e in rate_test_points(w[0], w[1]) {
613 let m = rate(e);
614 let lin = rate(w[0]) + (e - w[0]) / (w[1] - w[0]) * (rate(w[1]) - rate(w[0]));
615 assert!((lin - m).abs() <= RATE_TOLERANCE * m, "{w:?} at {e}");
616 }
617 }
618 }
619 }
620
621 fn assert_subsequence(grid: &[f64], refined: &[f64]) {
623 assert!(refined.windows(2).all(|w| w[1] > w[0]));
624 let mut it = refined.iter();
625 assert!(grid.iter().all(|g| it.any(|r| r == g)));
626 }
627
628 #[test]
629 fn rate_refinement_starts_at_an_onset_above_the_window() {
630 let model = OnsetRate::penn(6.0, onset_steep);
635 let onset = 16.0;
636 for grid in [
637 vec![5.0, 9.0, 14.0, 17.0, 22.0, 35.0, 60.0, 100.0],
638 vec![5.0, 9.0, 14.0, 16.0, 22.0, 35.0, 60.0, 100.0],
640 ] {
641 let o = band_options(grid.clone());
642 let refined = refined_energy_grid(&model, &o).unwrap().unwrap();
643 assert!(refined.len() > grid.len(), "{grid:?}");
644 assert_subsequence(&grid, &refined);
645 for e in refined.iter().filter(|e| !grid.contains(e)) {
647 let k = grid.partition_point(|g| g < e);
648 assert!(
649 grid[k - 1] >= onset,
650 "{e} in the cell {}..{}",
651 grid[k - 1],
652 grid[k]
653 );
654 }
655 assert_cells_pass(&model, &refined, onset);
656 }
657 let unhooked = OnsetRate {
660 onset: None,
661 ..OnsetRate::penn(6.0, onset_steep)
662 };
663 let o = band_options(vec![5.0, 9.0, 14.0, 17.0, 22.0, 35.0, 60.0, 100.0]);
664 let err = refined_energy_grid(&unhooked, &o).unwrap_err().to_string();
665 assert!(err.contains("exhausted to rounding"), "{err}");
666 assert!(err.contains("cell 15.") || err.contains("cell 16"), "{err}");
667 }
668
669 #[test]
670 fn an_onset_below_the_window_leaves_the_refinement_unchanged() {
671 let grid = vec![5.0, 9.0, 10.3, 14.0, 22.0, 35.0, 60.0, 100.0];
676 let o = band_options(grid.clone());
677 let with = OnsetRate::penn(0.5, steep);
678 let without = OnsetRate {
679 onset: None,
680 ..OnsetRate::penn(0.5, steep)
681 };
682 let refined = refined_energy_grid(&with, &o).unwrap().unwrap();
683 assert_eq!(refined, refined_energy_grid(&without, &o).unwrap().unwrap());
684 assert!(refined.iter().any(|&e| e > 10.3 && e < 14.0), "{refined:?}");
685 let at_start = OnsetRate::penn(RATE_WINDOW_EV.0, onset_steep);
687 let refined = refined_energy_grid(&at_start, &o).unwrap().unwrap();
688 assert!(
689 !refined.iter().any(|&e| e > 10.3 && e < 14.0),
690 "{refined:?}"
691 );
692 assert_subsequence(&grid, &refined);
693 }
694
695 #[test]
696 fn a_rate_without_an_onset_keeps_the_full_window() {
697 let model = OnsetRate {
701 elf_min: 6.0,
702 loss_min_fraction: Some(LOWEST_LOSS_FRACTION),
703 onset: None,
704 rate: steep,
705 };
706 let grid = vec![5.0, 9.0, 14.0, 17.0, 22.0, 35.0, 60.0, 100.0];
707 let refined = refined_energy_grid(&model, &band_options(grid.clone()))
708 .unwrap()
709 .unwrap();
710 assert_subsequence(&grid, &refined);
711 assert!(refined.iter().any(|&e| e > 12.0 && e < 16.0), "{refined:?}");
712 assert_cells_pass(&model, &refined, f64::NEG_INFINITY);
713 }
714
715 #[test]
716 fn rate_refinement_from_an_onset_is_the_same_on_any_thread_count() {
717 let model = OnsetRate::penn(6.0, onset_steep);
718 let o = band_options(vec![5.0, 9.0, 14.0, 17.0, 22.0, 35.0, 60.0, 100.0]);
719 let on = |threads: usize| {
720 rayon::ThreadPoolBuilder::new()
721 .num_threads(threads)
722 .build()
723 .unwrap()
724 .install(|| refined_energy_grid(&model, &o).unwrap())
725 };
726 let one = on(1);
727 assert_eq!(on(3), one);
728 assert_eq!(on(8), one);
729 }
730
731 #[test]
732 fn the_single_pole_model_has_a_rate_onset_and_the_full_and_mermin_models_none() {
733 let elf = crate::electron::inelastic::drude::DrudeLorentz::new(vec![
735 crate::electron::inelastic::drude::DrudeLorentzOscillator::plasmon(15.0, 4.0),
736 ])
737 .unwrap()
738 .to_optical_elf("synthetic", 6.0, 2000.0, 200)
739 .unwrap();
740 use crate::electron::inelastic::model::PennAlgorithm;
741 for (algorithm, onset) in [
742 (PennAlgorithm::SinglePole, Some(6.0)),
743 (PennAlgorithm::Full, None),
746 (PennAlgorithm::Mermin, None),
747 ] {
748 let model = PennInelastic::try_new(algorithm, elf.clone()).unwrap();
749 assert_eq!(model.rate_onset_ev(), onset, "{algorithm:?}");
750 let below = row_rate(&model, EnergyAxis::ModelFermiLevel, 5.0).unwrap();
753 assert_eq!(below > 0.0, onset.is_none(), "{algorithm:?}: {below}");
754 }
755 let model = PennInelastic::new(PennAlgorithm::SinglePole, elf)
758 .with_fermi_energy_ev(10.0)
759 .unwrap();
760 let grid = vec![5.0, 9.0, 14.0, 17.0, 22.0, 35.0, 60.0, 100.0];
761 let refined = refined_energy_grid(&model, &band_options(grid.clone()))
762 .unwrap()
763 .unwrap();
764 assert!(refined.len() > grid.len(), "{refined:?}");
765 assert_subsequence(&grid, &refined);
766 for e in refined.iter().filter(|e| !grid.contains(e)) {
767 assert!(*e > 17.0, "{e}");
768 }
769 assert_cells_pass(&model, &refined, 16.0);
770 }
771}