1use super::super::full_penn::DiimfpGrid;
7#[cfg(doc)]
8use super::super::full_penn::FullPenn;
9use super::super::full_penn::FULL_LOWEST_LOSS_FRACTION;
10use super::super::mermin::LOWEST_LOSS_FRACTION;
11use super::super::model::PennInelastic;
12use super::super::penn::SinglePolePenn;
13use super::{InelasticTableError, INITIAL_LOSS_PANELS, MAX_NODES};
14use crate::electron::data::{CrossSectionTable, ElectronDataError};
15use crate::electron::elastic::table::{logistic, logit};
16use rayon::prelude::*;
17
18#[derive(Debug, Clone, PartialEq)]
24pub(super) struct LinearDensity {
25 pub(super) x: Vec<f64>,
26 g: Vec<f64>,
27 cum: Vec<f64>,
29}
30
31impl LinearDensity {
32 fn from_nodes(x: Vec<f64>, g: Vec<f64>) -> Self {
33 let mut cum = Vec::with_capacity(x.len());
34 let mut acc = 0.0;
35 cum.push(0.0);
36 for i in 1..x.len() {
37 acc += 0.5 * (x[i] - x[i - 1]) * (g[i] + g[i - 1]);
38 cum.push(acc);
39 }
40 Self { x, g, cum }
41 }
42
43 pub(super) fn total(&self) -> f64 {
44 *self.cum.last().expect("non-empty")
45 }
46
47 fn first_moment(&self) -> f64 {
49 (1..self.x.len())
50 .map(|i| panel_exp_moment(self.x[i - 1], self.x[i], self.g[i - 1], self.g[i]))
51 .sum()
52 }
53
54 pub(super) fn quantile(&self, u: f64) -> f64 {
56 let n = self.x.len();
57 if u <= 0.0 {
58 return self.x[0];
59 }
60 if u >= 1.0 {
61 return self.x[n - 1];
62 }
63 let target = u * self.total();
64 let j = self.cum.partition_point(|&c| c < target).clamp(1, n - 1);
66 let (x0, x1) = (self.x[j - 1], self.x[j]);
67 let (g0, g1) = (self.g[j - 1], self.g[j]);
68 let h = x1 - x0;
69 let rem = (target - self.cum[j - 1]).max(0.0);
70 let disc = (g0 * g0 + 2.0 * (g1 - g0) * rem / h).max(0.0);
72 let den = g0 + disc.sqrt();
73 let t = if den > 0.0 { 2.0 * rem / den } else { 0.0 };
74 x0 + t.clamp(0.0, h)
75 }
76}
77
78fn panel_exp_moment(x0: f64, x1: f64, g0: f64, g1: f64) -> f64 {
80 let h = x1 - x0;
83 if h <= 0.0 {
84 return 0.0;
85 }
86 let m = (g1 - g0) / h;
87 let em1 = h.exp_m1();
88 x0.exp() * (g0 * em1 + m * (h * (em1 + 1.0) - em1))
89}
90
91pub(super) fn adaptive_density(
104 f: &(impl Fn(f64) -> f64 + ?Sized),
105 breaks: &[f64],
106 sub: usize,
107 tol: f64,
108 first_moment: bool,
109 rough: &[(f64, f64)],
110 rough_width: f64,
111) -> (LinearDensity, bool) {
112 let at_resolution = |a: f64, b: f64| {
113 b - a <= rough_width
114 && rough
115 .iter()
116 .any(|&(lo, hi)| a <= hi + rough_width && b >= lo - rough_width)
117 };
118 let mut x: Vec<f64> = Vec::new();
119 for w in breaks.windows(2) {
120 for i in 0..sub {
121 x.push(w[0] + (w[1] - w[0]) * i as f64 / sub as f64);
122 }
123 }
124 x.push(*breaks.last().expect("two breaks"));
125 let mut g: Vec<f64> = x.iter().map(|&xi| f(xi)).collect();
126 let mut mids: Vec<Option<f64>> = vec![None; x.len() - 1];
128 let span = x[x.len() - 1] - x[0];
129 loop {
130 for (i, m) in mids.iter_mut().enumerate() {
131 if m.is_none() {
132 *m = Some(f(0.5 * (x[i] + x[i + 1])));
133 }
134 }
135 let d = LinearDensity::from_nodes(x.clone(), g.clone());
136 let i0 = d.total();
137 let i1 = if first_moment { d.first_moment() } else { 0.0 };
138 let mut split = vec![false; mids.len()];
139 let mut any = false;
140 let mut capped = false;
141 for i in 0..mids.len() {
142 let h = x[i + 1] - x[i];
143 let xm = 0.5 * (x[i] + x[i + 1]);
147 if !(xm > x[i] && xm < x[i + 1]) {
148 continue;
149 }
150 let gm = mids[i].expect("filled");
151 let allowed = h / span * tol;
152 let e0 = (2.0 * h / 3.0 * (gm - 0.5 * (g[i] + g[i + 1]))).abs();
153 let mut bad = e0 > allowed * i0;
154 if first_moment {
155 let w = |xx: f64, gg: f64| xx.exp() * gg;
156 let lin = 0.5 * (w(x[i], g[i]) + w(x[i + 1], g[i + 1]));
157 let e1 = (2.0 * h / 3.0 * (w(xm, gm) - lin)).abs();
158 bad |= e1 > allowed * i1;
159 }
160 if bad && !at_resolution(x[i], x[i + 1]) {
161 if x.len() + 1 < MAX_NODES {
162 split[i] = true;
163 any = true;
164 } else {
165 capped = true;
166 }
167 }
168 }
169 if !any {
170 return (d, capped);
171 }
172 let mut nx = Vec::with_capacity(x.len() + 16);
173 let mut ng = Vec::with_capacity(x.len() + 16);
174 let mut nm = Vec::with_capacity(mids.len() + 16);
175 for i in 0..mids.len() {
176 nx.push(x[i]);
177 ng.push(g[i]);
178 if split[i] {
179 nx.push(0.5 * (x[i] + x[i + 1]));
180 ng.push(mids[i].expect("filled"));
181 nm.push(None);
182 nm.push(None);
183 } else {
184 nm.push(mids[i]);
185 }
186 }
187 nx.push(x[x.len() - 1]);
188 ng.push(g[g.len() - 1]);
189 x = nx;
190 g = ng;
191 mids = nm;
192 }
193}
194
195pub(super) trait LossModel: Sync {
201 fn elf_min_ev(&self) -> f64;
202 fn loss_min_ev(&self, _energy_ev: f64) -> f64 {
207 self.elf_min_ev()
208 }
209 fn rate_onset_ev(&self) -> Option<f64> {
217 Some(self.elf_min_ev())
218 }
219 fn window_break_ev(&self, _energy_ev: f64) -> Option<f64> {
225 None
226 }
227 fn fermi_energy_ev(&self) -> f64;
228 fn diimfp(&self, energy_ev: f64, loss_ev: f64) -> Result<f64, ElectronDataError>;
229 fn inverse_imfp(&self, energy_ev: f64) -> Result<f64, ElectronDataError>;
230 fn diimfp_grid(&self, _energy_ev: &[f64]) -> Result<Option<DiimfpGrid>, ElectronDataError> {
234 Ok(None)
235 }
236}
237
238impl LossModel for SinglePolePenn {
239 fn elf_min_ev(&self) -> f64 {
240 self.optical_elf().energy_ev()[0]
241 }
242 fn fermi_energy_ev(&self) -> f64 {
243 SinglePolePenn::fermi_energy_ev(self)
244 }
245 fn diimfp(&self, energy_ev: f64, loss_ev: f64) -> Result<f64, ElectronDataError> {
246 self.diimfp_per_m_ev(energy_ev, loss_ev)
247 }
248 fn inverse_imfp(&self, energy_ev: f64) -> Result<f64, ElectronDataError> {
249 Ok(self.imfp_and_stopping(energy_ev)?.inverse_imfp_per_m)
250 }
251}
252
253impl LossModel for PennInelastic {
254 fn elf_min_ev(&self) -> f64 {
255 self.optical_elf().energy_ev()[0]
256 }
257 fn loss_min_ev(&self, energy_ev: f64) -> f64 {
258 match self {
259 PennInelastic::Mermin(_) => LOWEST_LOSS_FRACTION * energy_ev,
262 PennInelastic::Full(_) => FULL_LOWEST_LOSS_FRACTION * energy_ev,
266 PennInelastic::SinglePole(_) => self.elf_min_ev(),
268 }
269 }
270 fn window_break_ev(&self, energy_ev: f64) -> Option<f64> {
271 let b = self.elf_min_ev();
272 match self {
273 PennInelastic::Full(_) if b > self.loss_min_ev(energy_ev) && b < energy_ev => Some(b),
274 _ => None,
275 }
276 }
277 fn rate_onset_ev(&self) -> Option<f64> {
278 match self {
279 PennInelastic::Mermin(_) => None,
282 PennInelastic::Full(_) => None,
285 PennInelastic::SinglePole(_) => Some(self.elf_min_ev()),
288 }
289 }
290 fn fermi_energy_ev(&self) -> f64 {
291 PennInelastic::fermi_energy_ev(self)
292 }
293 fn diimfp(&self, energy_ev: f64, loss_ev: f64) -> Result<f64, ElectronDataError> {
294 self.diimfp_per_m_ev(energy_ev, loss_ev)
295 }
296 fn inverse_imfp(&self, energy_ev: f64) -> Result<f64, ElectronDataError> {
297 Ok(self.imfp_and_stopping(energy_ev)?.inverse_imfp_per_m)
298 }
299 fn diimfp_grid(&self, energy_ev: &[f64]) -> Result<Option<DiimfpGrid>, ElectronDataError> {
300 match self {
301 PennInelastic::Full(m) => Ok(Some(m.diimfp_grid(energy_ev)?)),
302 _ => Ok(None),
303 }
304 }
305}
306
307fn row_diimfp<M: LossModel>(
310 penn: &M,
311 grid: Option<&DiimfpGrid>,
312 energy_ev: f64,
313 loss_ev: f64,
314) -> f64 {
315 grid.and_then(|g| g.diimfp_per_m_ev(energy_ev, loss_ev))
316 .unwrap_or_else(|| penn.diimfp(energy_ev, loss_ev).unwrap_or(0.0))
317}
318
319fn row_density<M: LossModel>(
323 penn: &M,
324 grid: Option<&DiimfpGrid>,
325 energy_ev: f64,
326 tol: f64,
327) -> (LinearDensity, bool) {
328 let w_lo = penn.loss_min_ev(energy_ev);
329 let f = |s: f64| {
330 let w = s.exp().min(energy_ev);
331 row_diimfp(penn, grid, energy_ev, w) * w
332 };
333 let (rough, rough_width) = match grid {
336 Some(g) => (g.unresolved_ln_loss_ev(), g.min_cell_width()),
337 None => (Vec::new(), 0.0),
338 };
339 let mut breaks = vec![w_lo.ln(), energy_ev.ln()];
340 if let Some(b) = penn.window_break_ev(energy_ev) {
341 breaks.insert(1, b.ln());
342 }
343 adaptive_density(
344 &f,
345 &breaks,
346 INITIAL_LOSS_PANELS,
347 tol,
348 true,
349 &rough,
350 rough_width,
351 )
352}
353
354pub fn loss_density_node_count(
364 model: &PennInelastic,
365 grid: Option<&DiimfpGrid>,
366 energy_ev: f64,
367 density_tolerance: f64,
368) -> usize {
369 row_density(model, grid, energy_ev, density_tolerance)
370 .0
371 .x
372 .len()
373}
374
375pub(super) fn loss_density<M: LossModel>(
377 penn: &M,
378 grid: Option<&DiimfpGrid>,
379 energy_ev: f64,
380 inverse_mfp: f64,
381 tol: f64,
382) -> Result<LinearDensity, InelasticTableError> {
383 let (d, capped) = row_density(penn, grid, energy_ev, tol);
384 if !(d.total() > 0.0 && d.total().is_finite()) {
385 return Err(InelasticTableError::EmptyDistribution {
386 energy_ev,
387 inverse_mfp_per_m: inverse_mfp,
388 integral: d.total(),
389 });
390 }
391 if capped {
392 return Err(InelasticTableError::DensityNodeLimit {
393 energy_ev,
394 max_nodes: MAX_NODES,
395 });
396 }
397 Ok(d)
398}
399
400pub(super) fn loss_quantile(d: &LinearDensity, energy_ev: f64, u: f64) -> f64 {
403 d.quantile(u).exp().min(energy_ev)
404}
405
406pub fn mean_loss_ev(probability: &[f64], quantiles: &[f64]) -> f64 {
409 probability
410 .windows(2)
411 .zip(quantiles.windows(2))
412 .map(|(p, q)| 0.5 * (p[1] - p[0]) * (q[0] + q[1]))
413 .sum()
414}
415
416pub fn stopping_power_ev_per_m(table: &CrossSectionTable, i: usize) -> Option<f64> {
420 let inv = *table.inverse_mfp_per_m().get(i)?;
421 Some(match table.quantiles(i) {
422 Some(q) => inv * mean_loss_ev(table.probability(), q),
423 None => 0.0,
424 })
425}
426
427pub(super) fn refine_probability(
429 rows: &[Option<LinearDensity>],
430 energy_ev: &[f64],
431 mut grid: Vec<f64>,
432 mut quantiles: Vec<Vec<f64>>,
433 tol: f64,
434) -> (Vec<f64>, Vec<Vec<f64>>) {
435 let means: Vec<f64> = quantiles
436 .iter()
437 .map(|q| {
438 if q.is_empty() {
439 0.0
440 } else {
441 mean_loss_ev(&grid, q)
442 }
443 })
444 .collect();
445 let mut todo: Vec<bool> = (0..grid.len() - 1)
447 .map(|s| s > 0 && s + 2 < grid.len())
448 .collect();
449 loop {
450 let segs: Vec<usize> = (0..todo.len()).filter(|&s| todo[s]).collect();
451 if segs.is_empty() || grid.len() + segs.len() > MAX_NODES {
452 return (grid, quantiles);
453 }
454 let umid: Vec<f64> = segs
455 .iter()
456 .map(|&s| logistic(0.5 * (logit(grid[s]) + logit(grid[s + 1]))))
457 .collect();
458 let exact: Vec<Vec<f64>> = rows
460 .par_iter()
461 .zip(energy_ev.par_iter())
462 .map(|(r, &e)| match r {
463 Some(d) => umid.iter().map(|&u| loss_quantile(d, e, u)).collect(),
464 None => Vec::new(),
465 })
466 .collect();
467 let mut split = vec![false; segs.len()];
468 for (k, &s) in segs.iter().enumerate() {
469 let du = grid[s + 1] - grid[s];
470 if !(umid[k] > grid[s] && umid[k] < grid[s + 1]) {
471 continue; }
473 let t = (umid[k] - grid[s]) / du;
476 for (i, q) in quantiles.iter().enumerate() {
477 if q.is_empty() {
478 continue;
479 }
480 let lin = q[s] + t * (q[s + 1] - q[s]);
481 if (lin - exact[i][k]).abs() * du > tol * means[i] * du.max(1.0e-4) {
482 split[k] = true;
483 break;
484 }
485 }
486 }
487 if !split.iter().any(|&b| b) {
488 return (grid, quantiles);
489 }
490 let mut ngrid = Vec::with_capacity(grid.len() + segs.len());
491 let mut nq: Vec<Vec<f64>> = quantiles
492 .iter()
493 .map(|q| {
494 Vec::with_capacity(if q.is_empty() {
495 0
496 } else {
497 q.len() + segs.len()
498 })
499 })
500 .collect();
501 let mut ntodo = Vec::with_capacity(todo.len() + segs.len());
502 let mut k = 0;
503 for s in 0..todo.len() {
504 ngrid.push(grid[s]);
505 for (i, q) in quantiles.iter().enumerate() {
506 if !q.is_empty() {
507 nq[i].push(q[s]);
508 }
509 }
510 let mut halves = false;
511 if todo[s] {
512 if split[k] {
513 ngrid.push(umid[k]);
514 for (i, q) in nq.iter_mut().enumerate() {
515 if !quantiles[i].is_empty() {
516 q.push(exact[i][k]);
517 }
518 }
519 halves = true;
520 }
521 k += 1;
522 }
523 ntodo.push(halves);
524 if halves {
525 ntodo.push(true);
526 }
527 }
528 ngrid.push(grid[grid.len() - 1]);
529 for (i, q) in quantiles.iter().enumerate() {
530 if !q.is_empty() {
531 nq[i].push(q[q.len() - 1]);
532 }
533 }
534 grid = ngrid;
535 quantiles = nq;
536 todo = ntodo;
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::super::build::{loss_rows, InelasticTableOptions};
543 use super::super::refine::row_rate;
544 use super::super::EnergyAxis;
545 use super::*;
546
547 fn stepped_row(c: f64) -> impl Fn(f64) -> f64 {
554 move |x: f64| {
555 let smooth = (-(x - 3.0).powi(2)).exp();
556 if (c..=c + 1.5e-4).contains(&x) {
557 smooth * (1.0 + 1e-3 + 1e-4 * (1e7 * x).sin())
558 } else {
559 smooth
560 }
561 }
562 }
563
564 #[test]
565 fn a_step_at_an_unresolved_cell_does_not_exhaust_the_nodes() {
566 let c = 3.0 - 0.5e-4;
570 let f = stepped_row(c);
571 let breaks = [0.0, 8.0];
572 let cells = [(c, c + 1.5e-4)];
573 let (d, capped) =
574 adaptive_density(&f, &breaks, INITIAL_LOSS_PANELS, 1e-5, true, &cells, 1e-4);
575 assert!(!capped);
576 let (smooth, _) = adaptive_density(
577 &|x: f64| (-(x - 3.0).powi(2)).exp(),
578 &breaks,
579 INITIAL_LOSS_PANELS,
580 1e-5,
581 true,
582 &[],
583 0.0,
584 );
585 let (n, n0) = (d.x.len(), smooth.x.len());
586 eprintln!("nodes: {n} with the stepped cell declared, {n0} for the smooth row");
587 assert!(n < n0 + 100, "{n} nodes vs {n0} (MAX_NODES = {MAX_NODES})");
590 let rel = (d.total() / smooth.total() - 1.0).abs();
593 assert!(rel < 1e-5, "{rel:e}");
594 let narrowest = d.x.windows(2).map(|w| w[1] - w[0]).fold(f64::MAX, f64::min);
596 assert!(narrowest > 1e-5, "{narrowest:e}");
597 }
598
599 #[test]
600 fn without_unresolved_cells_the_refinement_is_unchanged() {
601 let f = |x: f64| (-(x - 3.0).powi(2)).exp();
605 let breaks = [0.0, 8.0];
606 let plain = adaptive_density(&f, &breaks, INITIAL_LOSS_PANELS, 1e-5, true, &[], 0.0);
607 let far = adaptive_density(
608 &f,
609 &breaks,
610 INITIAL_LOSS_PANELS,
611 1e-5,
612 true,
613 &[(7.5, 7.5 + 1.5e-4)],
614 1e-4,
615 );
616 assert_eq!(plain, far);
617 }
618
619 fn jumped_row(at: &'static [f64], rel: f64) -> impl Fn(f64) -> f64 {
624 move |x: f64| {
625 let up = at.iter().filter(|&&a| x >= a).count() % 2;
626 (-(x - 3.0).powi(2)).exp() * (1.0 + rel * up as f64)
627 }
628 }
629
630 #[test]
631 fn a_jump_is_bisected_to_floating_point_resolution_and_no_further() {
632 const AT: &[f64] = &[2.8765432, 2.8812345, 3.1234567, 3.1301234];
637 let breaks = [0.0, 8.0];
638 let (d, capped) = adaptive_density(
639 &jumped_row(AT, 2e-5),
640 &breaks,
641 INITIAL_LOSS_PANELS,
642 1e-5,
643 true,
644 &[],
645 0.0,
646 );
647 let (smooth, _) = adaptive_density(
648 &jumped_row(&[], 0.0),
649 &breaks,
650 INITIAL_LOSS_PANELS,
651 1e-5,
652 true,
653 &[],
654 0.0,
655 );
656 let (n, n0) = (d.x.len(), smooth.x.len());
657 eprintln!("nodes: {n} with four jumps, {n0} for the smooth row");
658 assert!(!capped);
659 assert!(
661 n < n0 + 4 * 60,
662 "{n} nodes vs {n0} (MAX_NODES = {MAX_NODES})"
663 );
664 let narrowest = d.x.windows(2).map(|w| w[1] - w[0]).fold(f64::MAX, f64::min);
666 assert!(narrowest > 0.0, "{narrowest:e}");
667 assert!(narrowest <= 3.2 * f64::EPSILON, "{narrowest:e}");
668 let rel = (d.total() / smooth.total() - 1.0).abs();
670 assert!(rel < 1e-5, "{rel:e}");
671 }
672
673 struct NoisyRow;
676
677 impl LossModel for NoisyRow {
678 fn elf_min_ev(&self) -> f64 {
679 0.5
680 }
681 fn fermi_energy_ev(&self) -> f64 {
682 0.0
683 }
684 fn diimfp(&self, energy_ev: f64, loss_ev: f64) -> Result<f64, ElectronDataError> {
685 if !(loss_ev > 0.0 && loss_ev <= energy_ev) {
686 return Ok(0.0);
687 }
688 let h = loss_ev.to_bits().wrapping_mul(0x9e37_79b9_7f4a_7c15) >> 11;
690 Ok(1.0 + 1e-2 * (h as f64 / (1u64 << 53) as f64))
691 }
692 fn inverse_imfp(&self, energy_ev: f64) -> Result<f64, ElectronDataError> {
693 Ok(energy_ev - 0.5)
694 }
695 }
696
697 #[test]
698 fn a_row_that_reaches_the_node_bound_is_refused() {
699 let opts = InelasticTableOptions::new(vec![10.0]);
700 match loss_rows(&NoisyRow, &opts) {
701 Err(InelasticTableError::DensityNodeLimit {
702 energy_ev,
703 max_nodes,
704 }) => {
705 assert_eq!(energy_ev, 10.0);
706 assert_eq!(max_nodes, MAX_NODES);
707 }
708 Err(e) => panic!("unexpected error: {e}"),
709 Ok(_) => panic!("a row at the node bound was stored"),
710 }
711 let msg = loss_rows(&NoisyRow, &opts).unwrap_err().to_string();
712 assert!(msg.contains("within 20000 nodes"), "{msg}");
713 }
714
715 fn si_elf() -> Option<crate::electron::data::OpticalElf> {
718 let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
719 .join("../validation/data/optical/si_elf_yang2019.toml");
720 if !p.is_file() {
721 eprintln!("{} not found; Si check skipped", p.display());
722 return None;
723 }
724 Some(crate::electron::data::OpticalElf::from_toml_file(p).unwrap())
725 }
726
727 #[test]
728 fn mermin_rows_below_the_elf_table_open_at_the_rate_lower_limit() {
729 let Some(elf) = si_elf() else { return };
738 let ef = 13.46;
739 let model = PennInelastic::try_new(
740 crate::electron::inelastic::model::PennAlgorithm::Mermin,
741 elf,
742 )
743 .unwrap()
744 .with_fermi_energy_ev(ef)
745 .unwrap();
746 let elf_min = model.elf_min_ev();
747 assert_eq!(elf_min, 0.5);
748 let t_low = [0.2085, 0.2530, 0.4216];
749 let mut grid: Vec<f64> = t_low.iter().map(|t| ef + t).collect();
750 grid.push(ef + elf_min);
752 grid.push(ef + 1.0);
753 let opts = InelasticTableOptions::new(grid.clone()).with_axis(EnergyAxis::BandBottom);
754 let (inv, _, q) = loss_rows(&model, &opts).expect("rows inside (0, elf_min] build");
755 for (k, &e) in grid.iter().enumerate() {
756 let t = e - ef;
757 let w_min = model.loss_min_ev(t);
758 assert_eq!(w_min, LOWEST_LOSS_FRACTION * t);
759 assert_eq!(inv[k], model.inverse_imfp(t).unwrap(), "T = {t}");
760 assert!(inv[k] > 0.0, "T = {t}");
761 let row = &q[k];
762 assert!((row[0] / w_min - 1.0).abs() < 1e-12, "T = {t}: {}", row[0]);
765 assert!(row.iter().all(|&w| w > 0.0 && w <= t), "T = {t}");
766 assert!((row[row.len() - 1] - t).abs() <= 1e-12 * t, "T = {t}");
767 assert!(row.windows(2).all(|w| w[1] >= w[0]), "T = {t}");
768 }
769 }
770
771 #[test]
772 fn the_si_mermin_row_at_1000_ev_stops_at_floating_point_resolution() {
773 let Some(elf) = si_elf() else { return };
779 let model = PennInelastic::try_new(
780 crate::electron::inelastic::model::PennAlgorithm::Mermin,
781 elf,
782 )
783 .unwrap()
784 .with_fermi_energy_ev(13.46)
785 .unwrap();
786 let (low, _) = row_density(&model, None, 100.0, 1e-5);
787 let (d, capped) = row_density(&model, None, 1000.0, 1e-5);
788 let (n, n_low) = (d.x.len(), low.x.len());
789 eprintln!("nodes: {n} at T = 1000 eV, {n_low} at T = 100 eV");
790 assert!(!capped);
791 assert!(
792 n < 3 * n_low && n < MAX_NODES / 2,
793 "{n} nodes vs {n_low} at 100 eV (MAX_NODES = {MAX_NODES})"
794 );
795 assert_eq!(
796 n,
797 loss_density_node_count(&model, None, 1000.0, 1e-5),
798 "the public count is the row's"
799 );
800 assert!(d.x.windows(2).all(|w| w[1] > w[0]), "a zero-width panel");
801 let p = model.imfp_and_stopping(1000.0).unwrap();
803 let mean = d.first_moment() / d.total();
804 let want = p.stopping_ev_per_m / p.inverse_imfp_per_m;
805 let rel = (mean / want - 1.0).abs();
806 eprintln!("mean loss {mean} eV vs model {want} eV: {rel:e}");
807 assert!(rel < 1e-5, "{rel:e}");
808 }
809
810 struct WideWindow;
813
814 impl LossModel for WideWindow {
815 fn elf_min_ev(&self) -> f64 {
816 0.5
817 }
818 fn loss_min_ev(&self, energy_ev: f64) -> f64 {
819 0.01 * energy_ev
820 }
821 fn rate_onset_ev(&self) -> Option<f64> {
822 None
824 }
825 fn fermi_energy_ev(&self) -> f64 {
826 0.0
827 }
828 fn diimfp(&self, energy_ev: f64, loss_ev: f64) -> Result<f64, ElectronDataError> {
829 Ok(if loss_ev > 0.0 && loss_ev <= energy_ev {
830 1.0
831 } else {
832 0.0
833 })
834 }
835 fn inverse_imfp(&self, energy_ev: f64) -> Result<f64, ElectronDataError> {
836 Ok(energy_ev * 0.99)
837 }
838 }
839
840 #[test]
841 fn a_row_opens_at_the_model_lowest_loss() {
842 let opts = InelasticTableOptions::new(vec![0.3, 2.0]);
845 let (inv, prob, q) = loss_rows(&WideWindow, &opts).unwrap();
846 assert_eq!(inv, vec![0.3 * 0.99, 2.0 * 0.99]);
847 for (row, t) in q.iter().zip([0.3, 2.0]) {
848 assert!((row[0] / (0.01 * t) - 1.0).abs() < 1e-12, "{}", row[0]);
849 assert!((row[row.len() - 1] - t).abs() <= 1e-12 * t);
850 for (&u, &w) in prob.iter().zip(row) {
852 let want = 0.01 * t + u * 0.99 * t;
853 assert!((w - want).abs() <= 1e-4 * t, "u {u}: {w} vs {want}");
854 }
855 }
856 }
857
858 struct RateWithoutDensity;
860
861 impl LossModel for RateWithoutDensity {
862 fn elf_min_ev(&self) -> f64 {
863 0.5
864 }
865 fn fermi_energy_ev(&self) -> f64 {
866 0.0
867 }
868 fn diimfp(&self, _: f64, _: f64) -> Result<f64, ElectronDataError> {
869 Ok(0.0)
870 }
871 fn inverse_imfp(&self, _: f64) -> Result<f64, ElectronDataError> {
872 Ok(1.0e6)
873 }
874 }
875
876 #[test]
877 fn a_positive_rate_without_density_above_the_elf_table_is_still_refused() {
878 let opts = InelasticTableOptions::new(vec![0.25, 0.5]);
882 let (inv, _, q) = loss_rows(&RateWithoutDensity, &opts).unwrap();
883 assert_eq!(inv, vec![0.0, 0.0]);
884 assert!(q.iter().all(|r| r.is_empty()));
885 let opts = InelasticTableOptions::new(vec![0.25, 2.0]);
886 match loss_rows(&RateWithoutDensity, &opts) {
887 Err(InelasticTableError::EmptyDistribution { energy_ev, .. }) => {
888 assert_eq!(energy_ev, 2.0)
889 }
890 other => panic!("expected EmptyDistribution, got {other:?}"),
891 }
892 }
893
894 #[test]
895 fn full_penn_loss_window_opens_at_a_fraction_of_the_energy_and_has_no_onset() {
896 let elf = crate::electron::inelastic::drude::DrudeLorentz::new(vec![
901 crate::electron::inelastic::drude::DrudeLorentzOscillator::plasmon(15.0, 4.0),
902 ])
903 .unwrap()
904 .to_optical_elf("synthetic", 6.0, 2000.0, 100)
905 .unwrap();
906 use crate::electron::inelastic::model::PennAlgorithm;
907 let full = PennInelastic::try_new(PennAlgorithm::Full, elf.clone()).unwrap();
908 let single = PennInelastic::try_new(PennAlgorithm::SinglePole, elf).unwrap();
909 for t in [0.5, 6.0, 40.0] {
910 assert_eq!(full.loss_min_ev(t), FULL_LOWEST_LOSS_FRACTION * t);
911 assert_eq!(single.loss_min_ev(t), 6.0);
912 }
913 assert_eq!(full.window_break_ev(5.0), None);
914 assert_eq!(full.window_break_ev(6.0), None);
915 assert_eq!(full.window_break_ev(40.0), Some(6.0));
916 assert_eq!(single.window_break_ev(40.0), None);
917 assert!(row_rate(&full, EnergyAxis::ModelFermiLevel, 5.0).unwrap() > 0.0);
920 assert_eq!(
921 row_rate(&single, EnergyAxis::ModelFermiLevel, 5.0).unwrap(),
922 0.0
923 );
924 assert_eq!(full.rate_onset_ev(), None);
925 }
932}