1#[cfg(doc)]
6use super::super::full_penn::{DiimfpGrid, FullPenn};
7use super::super::inner_shell::ShellResolvedChannels;
8use super::super::model::PennInelastic;
9use super::super::penn::SinglePolePenn;
10use super::density::{loss_density, loss_quantile, refine_probability, LinearDensity, LossModel};
11use super::refine::{refinement_provenance, row_rate, with_refined_grid, RateRefinement};
12use super::{
13 check_no_exchange, invalid, EnergyAxis, InelasticTableError, DEFAULT_DENSITY_TOLERANCE,
14 DEFAULT_LOGISTIC_STEP, DEFAULT_PROBABILITY_TAIL, DEFAULT_REFINE_TOLERANCE,
15};
16use crate::electron::data::{
17 CrossSectionTable, CrossSectionTableParts, SamplingAxis, ShellChannelTable,
18};
19use crate::electron::elastic::table::{logistic_probability_grid, material_identity};
20use crate::material::Material;
21use rayon::prelude::*;
22
23#[derive(Debug, Clone, PartialEq)]
25pub struct InelasticTableOptions {
26 pub energy_ev: Vec<f64>,
30 pub axis: EnergyAxis,
33 pub probability: Vec<f64>,
37 pub density_tolerance: f64,
39 pub refine_tolerance: Option<f64>,
42 pub rate_refinement: Option<RateRefinement>,
47}
48
49impl InelasticTableOptions {
50 pub fn new(energy_ev: Vec<f64>) -> Self {
52 Self {
53 energy_ev,
54 axis: EnergyAxis::ModelFermiLevel,
55 probability: default_probability_grid(),
56 density_tolerance: DEFAULT_DENSITY_TOLERANCE,
57 refine_tolerance: Some(DEFAULT_REFINE_TOLERANCE),
58 rate_refinement: None,
59 }
60 }
61
62 pub fn with_axis(mut self, axis: EnergyAxis) -> Self {
64 self.axis = axis;
65 self
66 }
67
68 pub fn with_rate_refinement(mut self, refinement: RateRefinement) -> Self {
71 self.rate_refinement = Some(refinement);
72 self
73 }
74}
75
76pub fn default_probability_grid() -> Vec<f64> {
79 logistic_probability_grid(DEFAULT_PROBABILITY_TAIL, DEFAULT_LOGISTIC_STEP)
80 .expect("valid default grid")
81}
82
83fn check_options(o: &InelasticTableOptions) -> Result<(), InelasticTableError> {
84 let p = &o.probability;
85 let ok = p.len() >= 2
86 && p[0] == 0.0
87 && p[p.len() - 1] == 1.0
88 && p.iter().all(|x| x.is_finite())
89 && p.windows(2).all(|w| w[1] > w[0]);
90 if !ok {
91 return Err(invalid(
92 "probability grid",
93 "need >= 2 finite, strictly increasing points from exactly 0 to exactly 1",
94 ));
95 }
96 if !(o.density_tolerance.is_finite() && (1.0e-9..=1.0e-2).contains(&o.density_tolerance)) {
97 return Err(invalid(
98 "density tolerance",
99 format!("{} (need 1e-9 ..= 1e-2)", o.density_tolerance),
100 ));
101 }
102 if let Some(t) = o.refine_tolerance {
103 if !(t.is_finite() && t > 0.0) {
104 return Err(invalid(
105 "refine tolerance",
106 format!("{t} (need finite, > 0)"),
107 ));
108 }
109 }
110 if let Some(r) = &o.rate_refinement {
111 if o.axis != EnergyAxis::BandBottom {
112 return Err(invalid(
113 "rate refinement",
114 "only the band-bottom energy axis is refined (EnergyAxis::BandBottom)",
115 ));
116 }
117 let (lo, hi) = r.window_ev;
118 if !(lo.is_finite() && hi.is_finite() && lo > 0.0 && hi > lo) {
119 return Err(invalid(
120 "rate refinement window",
121 format!("{lo}..{hi} eV above E_F (need finite, 0 < low < high)"),
122 ));
123 }
124 if !(r.tolerance.is_finite() && r.tolerance > 0.0) {
125 return Err(invalid(
126 "rate refinement tolerance",
127 format!("{} (need finite, > 0)", r.tolerance),
128 ));
129 }
130 }
131 Ok(())
132}
133
134pub fn build_inelastic_table(
143 penn: &SinglePolePenn,
144 material: &Material,
145 options: &InelasticTableOptions,
146) -> Result<CrossSectionTable, InelasticTableError> {
147 check_no_exchange(penn)?;
148 check_options(options)?;
149 let (options, added) = with_refined_grid(penn, options)?;
150 let options = options.as_ref();
151 let (inverse_mfp_per_m, probability, quantiles) = loss_rows(penn, options)?;
152 let energy = &options.energy_ev;
153 let elf = penn.optical_elf();
154 let model = format!(
155 "lindhard {} electron::inelastic::table: single-pole Penn DIIMFP (nonrelativistic), \
156 W inverse CDF from an adaptive piecewise-linear density in ln W",
157 env!("CARGO_PKG_VERSION")
158 );
159 let provenance = format!(
160 "computed by lindhard {} from published formulas (see electron::inelastic docs); \
161 optical ELF: {} ({}); Fermi energy {} eV; integration tolerance {}; \
162 density tolerance {}; {} energies {}..{} eV; {} probability points",
163 env!("CARGO_PKG_VERSION"),
164 elf.material(),
165 elf.provenance(),
166 penn.fermi_energy_ev(),
167 penn.relative_tolerance(),
168 options.density_tolerance,
169 energy.len(),
170 energy[0],
171 energy[energy.len() - 1],
172 probability.len()
173 ) + &axis_provenance(penn, options)
174 + &refinement_provenance(options, added);
175 Ok(CrossSectionTable::new(CrossSectionTableParts {
176 model,
177 material: material_identity(material),
178 provenance,
179 axis: SamplingAxis::InelasticEnergyLoss,
180 energy_ev: energy.clone(),
181 inverse_mfp_per_m,
182 probability,
183 quantiles,
184 })?)
185}
186
187pub fn build_inelastic_table_for_model(
204 model: &PennInelastic,
205 material: &Material,
206 options: &InelasticTableOptions,
207) -> Result<CrossSectionTable, InelasticTableError> {
208 if let PennInelastic::SinglePole(p) = model {
209 check_no_exchange(p)?;
210 }
211 check_options(options)?;
212 let (options, added) = with_refined_grid(model, options)?;
213 let options = options.as_ref();
214 let (inverse_mfp_per_m, probability, quantiles) = loss_rows(model, options)?;
215 let energy = &options.energy_ev;
216 let elf = model.optical_elf();
217 let identity = model.model_identity();
218 let via = match model {
220 PennInelastic::Full(_) => " (from the T-independent loss grid FullPenn::diimfp_grid)",
221 _ => "",
222 };
223 let model_text = format!(
224 "lindhard {} electron::inelastic::table: {identity} DIIMFP{via}, \
225 W inverse CDF from an adaptive piecewise-linear density in ln W",
226 env!("CARGO_PKG_VERSION")
227 );
228 let provenance = format!(
229 "computed by lindhard {} from published formulas (see electron::inelastic docs); \
230 model: {identity}; optical ELF: {} ({}); density tolerance {}; \
231 {} energies {}..{} eV; {} probability points",
232 env!("CARGO_PKG_VERSION"),
233 elf.material(),
234 elf.provenance(),
235 options.density_tolerance,
236 energy.len(),
237 energy[0],
238 energy[energy.len() - 1],
239 probability.len()
240 ) + &axis_provenance(model, options)
241 + &refinement_provenance(options, added);
242 Ok(CrossSectionTable::new(CrossSectionTableParts {
243 model: model_text,
244 material: material_identity(material),
245 provenance,
246 axis: SamplingAxis::InelasticEnergyLoss,
247 energy_ev: energy.clone(),
248 inverse_mfp_per_m,
249 probability,
250 quantiles,
251 })?)
252}
253
254fn model_energies_ev<M: LossModel>(penn: &M, options: &InelasticTableOptions) -> Vec<f64> {
258 match options.axis {
259 EnergyAxis::ModelFermiLevel => options.energy_ev.clone(),
260 EnergyAxis::BandBottom => {
261 let ef = penn.fermi_energy_ev();
262 options.energy_ev.iter().map(|&e| e - ef).collect()
263 }
264 }
265}
266
267fn axis_provenance<M: LossModel>(penn: &M, options: &InelasticTableOptions) -> String {
271 match options.axis {
272 EnergyAxis::ModelFermiLevel => String::new(),
273 EnergyAxis::BandBottom => format!(
274 "; energy axis: band bottom (row at E is the model's at T = E - {} eV)",
275 penn.fermi_energy_ev()
276 ),
277 }
278}
279
280#[derive(Debug, Clone, PartialEq)]
285pub struct ShellResolvedTables {
286 pub valence: CrossSectionTable,
289 pub shells: Vec<ShellChannelTable>,
291}
292
293pub fn build_shell_channel_tables(
329 channels: &ShellResolvedChannels,
330 material: &Material,
331 options: &InelasticTableOptions,
332 binding_provenance: &str,
333) -> Result<ShellResolvedTables, InelasticTableError> {
334 let valence = build_inelastic_table(channels.valence(), material, options)?;
335 let mut shells = Vec::with_capacity(channels.inner_shells().len());
336 for (i, s) in channels.inner_shells().iter().enumerate() {
337 let b = s.binding_energy_ev;
338 let base = build_inelastic_table(channels.shell_model(i), material, options)?;
339 let mut parts = base.parts().clone();
340 let label = s.subshell.label();
341 parts.model = format!("{} [inner-shell channel Z = {} {label}]", parts.model, s.z);
342 parts.provenance = format!(
343 "{}; inner-shell channel Z = {} {label}, binding energy {b} eV ({}), \
344 losses floored at the binding energy",
345 parts.provenance,
346 s.z,
347 binding_provenance.trim()
348 );
349 for row in &mut parts.quantiles {
350 for w in row.iter_mut() {
351 *w = w.max(b);
352 }
353 }
354 let table = CrossSectionTable::new(parts)?;
355 shells.push(ShellChannelTable::new(
356 s.z,
357 s.subshell,
358 b,
359 binding_provenance,
360 table,
361 )?);
362 }
363 Ok(ShellResolvedTables { valence, shells })
364}
365
366type LossRows = (Vec<f64>, Vec<f64>, Vec<Vec<f64>>);
369
370pub(super) fn loss_rows<M: LossModel>(
371 penn: &M,
372 options: &InelasticTableOptions,
373) -> Result<LossRows, InelasticTableError> {
374 let energy = &model_energies_ev(penn, options);
376 let grid = match options.axis {
380 EnergyAxis::ModelFermiLevel => penn.diimfp_grid(energy)?,
381 EnergyAxis::BandBottom => {
382 let lossy: Vec<f64> = energy.iter().copied().filter(|&e| e > 0.0).collect();
383 penn.diimfp_grid(&lossy)?
384 }
385 };
386 type Row = Result<(f64, Option<LinearDensity>), InelasticTableError>;
389 let results: Vec<Row> = energy
390 .par_iter()
391 .map(|&e| {
392 let inv = row_rate(penn, options.axis, e)?;
393 if inv > 0.0 {
394 let d = loss_density(penn, grid.as_ref(), e, inv, options.density_tolerance)?;
395 Ok((inv, Some(d)))
396 } else {
397 Ok((0.0, None))
398 }
399 })
400 .collect();
401 let mut inverse_mfp_per_m = Vec::with_capacity(energy.len());
402 let mut rows: Vec<Option<LinearDensity>> = Vec::with_capacity(energy.len());
403 for r in results {
404 let (inv, d) = r?;
405 inverse_mfp_per_m.push(inv);
406 rows.push(d);
407 }
408 let probability = &options.probability;
409 let quantiles: Vec<Vec<f64>> = rows
410 .par_iter()
411 .zip(energy.par_iter())
412 .map(|(r, &e)| match r {
413 Some(d) => probability
414 .iter()
415 .map(|&u| loss_quantile(d, e, u))
416 .collect(),
417 None => Vec::new(),
418 })
419 .collect();
420 let (probability, quantiles) = match options.refine_tolerance {
421 Some(tol) => refine_probability(&rows, energy, probability.clone(), quantiles, tol),
422 None => (probability.clone(), quantiles),
423 };
424 Ok((inverse_mfp_per_m, probability, quantiles))
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[test]
432 fn rate_refinement_needs_the_band_bottom_axis_and_a_valid_window() {
433 let mut o = InelasticTableOptions::new(vec![5.0, 20.0])
434 .with_rate_refinement(RateRefinement::default());
435 assert!(check_options(&o)
436 .unwrap_err()
437 .to_string()
438 .contains("band-bottom"));
439 o.axis = EnergyAxis::BandBottom;
440 assert!(check_options(&o).is_ok());
441 for (window, tol) in [((3.0, 2.0), 0.01), ((0.0, 2.0), 0.01), ((2.0, 20.0), 0.0)] {
442 let r = o.rate_refinement.as_mut().unwrap();
443 r.window_ev = window;
444 r.tolerance = tol;
445 assert!(check_options(&o).is_err(), "{window:?} {tol}");
446 }
447 }
448}