Skip to main content

lindhard/electron/inelastic/
table.rs

1//! Inelastic sampling tables from the single-pole Penn DIIMFP (issue #94):
2//! an [`electron::data::CrossSectionTable`](crate::electron::data::CrossSectionTable)
3//! with [`SamplingAxis::InelasticEnergyLoss`] (the inverse mean free path and
4//! the inverse CDF of the energy loss `W` at every energy of a caller-chosen
5//! grid), and a sampler of the momentum transfer `q` given `(E, W)`, from
6//! which the deflection angle follows.
7//!
8//! Both are built from [`SinglePolePenn`] only; no data and no equation beyond
9//! the model of [`super::penn`] enter here (the elementary kinematics of a
10//! binary collision aside, see below). [`build_inelastic_table_for_model`]
11//! runs the same energy-loss table procedure on any of the three models of
12//! [`super::model::PennInelastic`] (single pole, full Penn, Mermin-ELF),
13//! using that model's own DIIMFP and inverse IMFP; nothing else changes.
14//!
15//! # Energy-loss table
16//!
17//! At incident energy `E` the loss distribution is the DIIMFP at fixed `E`,
18//! `p(E, W)` ([`SinglePolePenn::diimfp_per_m_ev`]), normalised:
19//! `F(W) = ∫_0^W p dW' / ∫_0^E p dW'`. The table stores
20//!
21//! * `λ⁻¹(E)` from [`SinglePolePenn::imfp_and_stopping`] (the closed-form
22//!   reduction of #18, not the integral of `p` computed here; the two agree
23//!   to the model's tolerance, 1e-5 in the #18 tests), and
24//! * the quantiles `W(u_j)` with `F(W(u_j)) = u_j` on a probability grid
25//!   shared by all rows.
26//!
27//! **Density model.** `W` spans decades, so the integrand is handled in
28//! `s = ln W`, where `p dW = p W ds`. `g(s) = p W` is sampled on adaptively
29//! chosen nodes between the lowest loss of the model's rate (the lowest
30//! tabulated ELF energy, below which `p = 0`, for the single-pole model; see
31//! "Rows below the ELF table" for the Mermin and full Penn models) and `E`,
32//! and taken to be
33//! *linear between nodes*. Starting from
34//! [`INITIAL_LOSS_PANELS`] equal panels, a panel is split at its midpoint
35//! while the Simpson-minus-trapezoid estimate of its error in either
36//! `∫ g ds` or `∫ W g ds` exceeds `tol · I · h/H` (`I` the whole integral, `h`
37//! the panel width, `H` the whole width; `tol` =
38//! [`InelasticTableOptions::density_tolerance`]). The quantiles are then the
39//! *exact* inverse of this piecewise-quadratic CDF (a quadratic per panel,
40//! solved without cancellation), so the density model is the only
41//! approximation of the loss distribution and its error is controlled by that
42//! tolerance.
43//!
44//! **Full Penn rows.** For the full Penn model the density is built on the
45//! same nodes from the DIIMFP of a [`DiimfpGrid`] tabulated once for the
46//! table's energies ([`FullPenn::diimfp_grid`], #256), not from the direct
47//! nested integrals: its interpolation error, held to the model tolerance
48//! (1e-4, relative or relative to the row's mean density), is a second
49//! approximation of the loss distribution, next to the density tolerance.
50//! It is also smooth in `W`, where the direct DIIMFP carries quadrature
51//! noise at the model tolerance, which is above the default density
52//! tolerance and would drive the panel refinement to [`MAX_NODES`].
53//!
54//! **Unresolved grid cells.** Where the grid could not hold its
55//! interpolation to the model tolerance even at its narrowest cell (1e-4 in
56//! `ln ω`), it leaves the cell to the direct model
57//! ([`DiimfpGrid::unresolved_cells`]). The row integrand then switches model
58//! at each edge of such a cell, a step of the order of the model tolerance,
59//! and inside it carries the direct model's quadrature noise. The split test
60//! above does not depend on the panel width for a step (both sides scale
61//! with `h`), so a step above about `3 · tol` of the row's mean density
62//! would be bisected down to floating-point resolution and then padded with
63//! duplicate nodes up to [`MAX_NODES`], each a direct evaluation. So a panel
64//! within one grid-cell width (1e-4) of an unresolved cell is not split
65//! once it is no wider than that width: that is the finest scale at which
66//! the grid tested the DIIMFP, and below it the split test measures the
67//! noise and the step, not the shape. What such a panel can still miss is
68//! bounded by its width times the step, `1e-4 · (model tolerance) · g`,
69//! per cell edge, far below the model tolerance of the row. Rows that do
70//! not come near an unresolved cell, and every model without a grid, are
71//! refined exactly as before. [`loss_density_node_count`] reports the node
72//! count of a row.
73//!
74//! **Floating-point resolution** (#345). Every model is a numerical integral
75//! at its own tolerance, so its DIIMFP carries small jumps where the
76//! adaptive quadrature changes its subdivision between two adjacent `W`.
77//! The Mermin DIIMFP ([`super::mermin`], integration tolerance 1e-5, the
78//! same as the default density tolerance) has them: the Si row at
79//! `T = 1000 eV` of the committed Yang et al. (2019) ELF (model Fermi energy
80//! 13.46 eV) has 18, of 0.4 to 7 times 1e-5 of the local density, between
81//! `W` = 20 and 976 eV. A jump is not a shape the density can resolve: as
82//! for the model switch at an unresolved cell, the split test of a panel
83//! across it does not fall with the width, so the panel is bisected until
84//! its midpoint rounds onto one of its ends. Such a panel (one ulp of
85//! `ln W` wide) is not split: its midpoint is not a new node, and splitting
86//! it would only add zero-width panels, up to [`MAX_NODES`] of them. This is
87//! this project's choice of floor, with no tuned width: it is the point
88//! where bisection can no longer place a node. A jump bisected to it costs
89//! about 50 nodes (the levels from the starting panel to one ulp), and
90//! leaves nothing of the density unresolved: the panel holding it has no
91//! interior. Before #345 the zero-width panels were added until the row
92//! reached [`MAX_NODES`]; they carry no integral (`h = 0`), so a row whose
93//! other panels had converged is stored the same, with fewer evaluations
94//! (the Si row above: 20 099 nodes before, 5 792 now).
95//!
96//! **Node bound.** A row whose refinement still has a failing panel when it
97//! reaches [`MAX_NODES`] nodes is refused,
98//! [`InelasticTableError::DensityNodeLimit`], rather than stored with that
99//! panel unresolved (the convention of the rate refinement's row bound,
100//! below).
101//!
102//! **Probability grid.** The stored inverse CDF is interpolated linearly in
103//! `u` ([`CrossSectionTable::inverse_cdf`]). Its first moment, which is what
104//! sets the stopping power `S = λ⁻¹ ⟨W⟩` with `⟨W⟩ = ∫_0^1 W(u) du`, is
105//! dominated by the upper tail of `W`, so the starting grid
106//! ([`logistic_probability_grid`]) is uniform in `t = ln(u/(1-u))`, and it is
107//! **refined adaptively**, shared by all rows: a segment is split at its
108//! logistic midpoint when `δ Δu > tol μ max(Δu, 1e-4)` at any energy, with `δ`
109//! the difference between the exact quantile there and the linear
110//! interpolation and `μ` the row's mean loss (the criterion of the elastic
111//! tables, [`crate::electron::elastic::table`]). The test
112//! `stopping_power_from_the_loss_cdf_matches_the_model` holds
113//! [`stopping_power_ev_per_m`] to 1e-3 of `S(E)` of #18 at every grid
114//! energy.
115//!
116//! Rows where `λ⁻¹ = 0` (no loss allowed) are stored empty, and so are rows
117//! with `T` at or below the lowest loss of the model's rate (module docs,
118//! "Energy axis"). The table covers
119//! the direct single-pole model only: no exchange and no surface or band-gap
120//! structure (see [`super::penn`]). Inner shells get one table each, beside
121//! the valence table, from [`build_shell_channel_tables`]. A model with the
122//! exchange correction enabled ([`SinglePolePenn::with_exchange`]) is
123//! rejected by [`build_inelastic_table`] and [`MomentumTransferSampler`].
124//!
125//! # Energy axis
126//!
127//! The models take `T`, the kinetic energy above the model's Fermi level,
128//! with kinematics on `T' = T + E_F` and losses up to `T` (S2017 eqs.
129//! (2)-(3), [`super::penn`]). By default ([`EnergyAxis::ModelFermiLevel`])
130//! a table is stored on that axis: the row at `E` is the model's at `T = E`.
131//!
132//! [`EnergyAxis::BandBottom`] stores it on the band-bottom axis instead: the
133//! row at `E` is the model's at `T = E - E_F`, so its kinematics are on
134//! `T' = E` and its losses stop at `E - E_F`; rows with `T <= 0` are stored
135//! empty. With `E_F` the band's, this is the convention of the table
136//! compiler of Nebula, `compile_full_imfp_icdf` in
137//! `cstool/dielectric_function/compile.py` (Nebula-simulator/cstool commit
138//! `0c739eb3fcc3fe5297e74c601ac4a9546db596cf`, BSD-3-Clause): it evaluates
139//! every row at the electron's kinetic energy `K` (the momenta of the
140//! kinematic limits are those of `K` and `K - ω`) and keeps only the losses
141//! `ω < K - F` (its "Fermi correction"), `F` the energy its caller
142//! `compile_full_penn` (`apps/cstool.py`, same commit) passes, the band's
143//! `get_min_excitation()` (`cstool/input_data/band_structure.py`): the Fermi
144//! energy of a metal, the conduction-band bottom `W_v + E_g` of an
145//! insulator or semiconductor. The model's own `E_F` is the `F` of the axis,
146//! so there is one Fermi energy, not two. The table builder knows no band;
147//! the caller sets the model's Fermi energy (`lindhard run` does so per
148//! material, `docs/cli.md`).
149//!
150//! **Rows below the ELF table.** The sampled loss window of a row is
151//! `(W_min, T]`, `W_min` the lowest loss the model's rate `λ⁻¹(T)` counts,
152//! so that the stored rate and the sampled losses cover the same window:
153//!
154//! * single-pole Penn: `W_min = elf_min`, the lowest tabulated ELF energy.
155//!   Its DIIMFP is zero below it ([`super::penn`]), so its rate counts no
156//!   loss there;
157//! * full Penn: `W_min = 1e-6 T` ([`super::full_penn`], its
158//!   `LOWEST_LOSS_FRACTION`). The DIIMFP of the full model is *not* zero
159//!   below `elf_min`: each Lindhard gas of the expansion has an
160//!   electron-hole continuum down to `ω -> 0` at `q > 0` (S2017 eq. (4)),
161//!   and the model's rate and stopping power integrate every loss from 0. A
162//!   window opened at `elf_min` (until #361) left the losses below it out of
163//!   the sampled distribution while the stored rate counted them: on the Si
164//!   ELF tabulated from 0.5 eV the share of `λ⁻¹` below 0.5 eV is 4.7e-3 at
165//!   `T` = 10 eV, and the table's `S = λ⁻¹ ⟨W⟩` was biased high by the
166//!   same amount. The lower limit is a numerical floor, not a model cutoff
167//!   (a literal zero cannot bound the `ln W` axis): `p ∝ ω` at small `ω`,
168//!   so the rate below `W` falls as `W²` and the stopping power as `W³`,
169//!   and the omitted share of both is below the model tolerance by orders
170//!   of magnitude (`tests/penn_full.rs` bounds it against the direct
171//!   DIIMFP). The row's rate is the model's own; the sampled distribution
172//!   and `⟨W⟩` cover `(1e-6 T, T]`, which the model's `0 < ω <= T` equals up
173//!   to that omitted share. The DIIMFP grid reaches down to the floor of the
174//!   lowest row energy, so these losses are read from it, not computed
175//!   directly;
176//! * Mermin-ELF: `W_min = 1e-8 T`, the lower limit of the `ω` integral of
177//!   its rate and stopping power ([`super::mermin`], the same constant). Its
178//!   fitted Drude-Lorentz ELF has no cutoff at `elf_min` (the omission of the
179//!   threshold step is a documented choice of that model), so the DIIMFP
180//!   below `elf_min` is the fit's extension below the tabulated range and is
181//!   part of both the rate and the sampled loss distribution (#342). Before
182//!   #342 the window opened at `elf_min` for every model, and a Mermin row
183//!   drew the losses its rate counts below `elf_min` as losses above it: for
184//!   the Si ELF tabulated from 0.5 eV (default fit, model Fermi energy
185//!   13.46 eV) the window `(0.5 eV, T]` held 28 % of the rate at `T` = 0.6
186//!   eV, 72 % at 1 eV, 92 % at 2 eV and 98.4 % at 5 eV.
187//!
188//! A row with `0 < T <= W_min` has an empty window, so no loss can be drawn
189//! there, and it is stored empty (inverse mean free path 0, no quantiles),
190//! as a row with `T <= 0` is, on either axis (#340): without this the `ln W`
191//! window `[ln W_min, ln T]` is reversed (or of zero width at `T = W_min`)
192//! and the density integrates to a negative value (or 0). For the Penn
193//! single-pole model these are the rows with `0 < T <= elf_min`. A Mermin
194//! or full Penn row has `W_min < T` at every `T > 0`, so it is never empty
195//! for this reason: its rows with `0 < T <= elf_min`, stored empty from #340
196//! to #342 (Mermin) or #361 (full Penn), carry the model's rate and losses
197//! in `(W_min, T]`.
198//!
199//! # Rate refinement
200//!
201//! On the band-bottom axis the rate `λ⁻¹(E)` is zero at `E_F` and climbs
202//! by two orders of magnitude within about 20 eV above it, and the transport
203//! interpolates it linearly between rows (`crate::electron::transport`,
204//! "Free flight"). A log grid fine enough there is far finer than needed
205//! everywhere else (#291, #339). [`InelasticTableOptions::rate_refinement`]
206//! ([`RateRefinement`], off by default) adds rows to this one table instead:
207//! every cell of the grid that overlaps `E - E_F` in
208//! [`RateRefinement::window_ev`] ([`RATE_WINDOW_EV`] by default) is bisected
209//! at its midpoint in `E` while the linear interpolation of the exact row
210//! rate between the cell's ends differs from the exact rate at any of the
211//! cell's test points by more than [`RateRefinement::tolerance`]
212//! ([`RATE_TOLERANCE`]) of it. The test points are the cell's quartiles and
213//! midpoint (`rate_test_points`) and, where those pass, the estimated
214//! position of the largest error: the vertex of the parabola through the
215//! largest of the sampled errors and its two neighbours (`rate_error_peak`).
216//! Children that no longer overlap the window are not tested, so the
217//! bisection never approaches the zero of the rate at `E_F`, and cells
218//! outside the window are never split. Only the scalar rate is evaluated
219//! while refining (the row rule of the table: zero for an empty row); each
220//! level's test points are evaluated in parallel and merged in sorted order,
221//! so the grid is the same on any thread count. The rows are then built as
222//! any other, from the merged grid, which contains the given grid. A
223//! refinement that would add more than [`RateRefinement::max_rows`] rows is
224//! an error naming the cell that still fails (a bound against a rate the
225//! bisection cannot resolve, not a tuned value), and so is a cell still to
226//! test that can no longer be bisected because its midpoint rounds to an
227//! end. A step in the rate splits one cell per level, so it reaches
228//! rounding (about 50 levels) before any useful row bound: that second
229//! error, not the row bound, is what stops it.
230//!
231//! **Rate onset** (#357). The single-pole rate is
232//! identically zero for `E - E_F <= elf_min` ("Rows below the ELF table",
233//! `W_min = elf_min`) and positive above it: a step at the onset
234//! `E_F + elf_min`, which the transport's linear interpolation from the
235//! zero row below it cannot follow at any row density. Where the onset lies
236//! at or above the window start (an ELF tabulated from 2 eV or more, at the
237//! default window), a cell is tested only if it overlaps the window and its
238//! lower end is at or above the onset: the cell straddling the onset and
239//! the cells below it are left as given, the interpolation across the
240//! straddling cell being the model's own step. Testing them would bisect
241//! towards the step until its cell is exhausted to rounding, the error
242//! above, and so refuse an ELF that builds without the option. Where the
243//! onset lies below the window start nothing changes: the bisection's
244//! children that leave the window are not tested, so it never reaches the
245//! step, as it never reaches the zero at `E_F`. Every committed ELF starts
246//! at or below 0.5 eV (`validation/data/optical/`), so for none of them
247//! does the onset change the refined grid. The Mermin rate is positive for
248//! every `T > 0` (it counts losses down to `1e-8 T`), and so is the full
249//! Penn rate (down to `1e-6 T`, #361: it was grouped with the single-pole
250//! onset before), so those models have no onset and keep the full window.
251//!
252//! The test is a criterion, not a bound. A test of the midpoint alone
253//! (#351) left cells whose error peaked off the midpoint above the
254//! tolerance, and so did one of the quartiles and midpoint (the Al single
255//! pole from 5 eV at 10 per decade, 1.0008 % at 6.1 eV, between the first
256//! quartile and the midpoint of its cell); the parabolic vertex locates such
257//! a peak without a tuned number of samples. The acceptance check is the dense
258//! evaluation of the `rate` mode of
259//! `lindhard-cli/examples/inelastic_low_energy.rs` (every 0.1 eV over the
260//! window, `docs/validation.md`). The table's other grid (the elastic table
261//! of a run, or a table without the option) is unchanged: the transport
262//! reads each table on its own [`CrossSectionTable::energy_ev`].
263//!
264//! # Momentum-transfer sampler
265//!
266//! Given `(E, W)`, the DIIMFP integrand in `q` is, in `u = ln q`
267//! (S2017 eq. (2), as in [`super::penn`]), `Im[-1/ε(q, W)]` on
268//! `q- <= q <= q+`, `q± = k ± k'`, `k = sqrt(2T')`, `k' = sqrt(2(T' - W))`
269//! (Hartree units, `T' = E + E_F`), cut to the `q` where the plasma frequency
270//! of the pole lies in the tabulated ELF range. [`MomentumTransferSampler`] is
271//! a **direct sampler built per `(E, W)`**: the integrand is sampled on
272//! adaptive nodes (breaks at the kinematic limits and at every ELF knot, 8
273//! panels per smooth segment to start, the same refinement rule as above with
274//! the zeroth moment only and tolerance [`DEFAULT_Q_TOLERANCE`]), linear in
275//! `ln q` between nodes, and inverted exactly. The density used is therefore
276//! the model's to within that tolerance, and the histogram test compares it
277//! with an independent quadrature of [`SinglePolePenn::loss_function`] at 1e6
278//! samples. It is not a table: building one costs the integrand evaluations of
279//! one DIIMFP slice, so a transport loop should keep the sampler for repeated
280//! draws at one `(E, W)` and expect that cost for each new pair. A tabulated
281//! `(E, W, u)` form is left for later work.
282//!
283//! The deflection angle follows from momentum conservation of the primary,
284//! `q² = k² + k'² - 2 k k' cos θ`, i.e.
285//! `cos θ = (k² + k'² - q²)/(2 k k')` ([`MomentumTransferSampler::cos_theta`]),
286//! clamped to `[-1, 1]`; `q = q-` is `θ = 0` and `q = q+` is `θ = π`. This is
287//! the elementary kinematics of the loss `W` (the final wavenumber `k'` fixed
288//! by energy conservation) with the model's own `q±`.
289//!
290//! # Determinism
291//!
292//! Rows (and the profiles of a full-Penn [`DiimfpGrid`]) are built in
293//! parallel with rayon and collected in grid order; each
294//! row, and the probability refinement (a parallel map over rows followed by
295//! a serial split decision), is a fixed sequence of floating-point
296//! operations, so tables are bit-identical on any thread count (tested).
297//! Samplers consume uniforms `u` supplied by the caller (for example from
298//! [`crate::rng::stream`]) and keep no state of their own.
299
300mod build;
301mod density;
302mod refine;
303mod sampler;
304
305pub use build::{
306    build_inelastic_table, build_inelastic_table_for_model, build_shell_channel_tables,
307    default_probability_grid, InelasticTableOptions, ShellResolvedTables,
308};
309pub use density::{loss_density_node_count, mean_loss_ev, stopping_power_ev_per_m};
310pub use refine::{
311    RateRefinement, DEFAULT_RATE_REFINEMENT_MAX_ROWS, RATE_TOLERANCE, RATE_WINDOW_EV,
312};
313pub use sampler::MomentumTransferSampler;
314
315#[cfg(doc)]
316use super::full_penn::{DiimfpGrid, FullPenn};
317use super::penn::SinglePolePenn;
318use crate::electron::data::ElectronDataError;
319#[cfg(doc)]
320use crate::electron::data::{CrossSectionTable, SamplingAxis};
321#[cfg(doc)]
322use crate::electron::elastic::table::logistic_probability_grid;
323
324/// Number of equal panels (in `ln W`) the loss density starts from.
325pub const INITIAL_LOSS_PANELS: usize = 64;
326/// Default tolerance of the loss-density model (see the module docs).
327pub const DEFAULT_DENSITY_TOLERANCE: f64 = 1.0e-5;
328/// Default target of the probability-grid refinement, relative to the mean
329/// loss of each row.
330pub const DEFAULT_REFINE_TOLERANCE: f64 = 1.0e-4;
331/// Default tolerance of the momentum-transfer density model.
332pub const DEFAULT_Q_TOLERANCE: f64 = 1.0e-6;
333/// Default probability tail of the starting grid (see
334/// [`logistic_probability_grid`]).
335pub const DEFAULT_PROBABILITY_TAIL: f64 = 1.0e-12;
336/// Default logistic step of the starting probability grid.
337pub const DEFAULT_LOGISTIC_STEP: f64 = 0.5;
338/// Upper bound on the number of nodes of one density model and on the size of
339/// the refined probability grid.
340pub const MAX_NODES: usize = 20_000;
341
342/// Errors from the table builder and the samplers.
343#[derive(Debug, thiserror::Error)]
344pub enum InelasticTableError {
345    /// An input violates an invariant.
346    #[error("invalid {what}: {reason}")]
347    Invalid {
348        /// Which quantity.
349        what: &'static str,
350        /// What is wrong with it.
351        reason: String,
352    },
353    /// The model reports a loss rate but the density has no support.
354    #[error(
355        "no loss distribution at {energy_ev} eV (inverse mean free path \
356         {inverse_mfp_per_m} m^-1, density integral {integral})"
357    )]
358    EmptyDistribution {
359        /// Incident energy, eV.
360        energy_ev: f64,
361        /// The model's inverse mean free path, m⁻¹.
362        inverse_mfp_per_m: f64,
363        /// The integral of the sampled density.
364        integral: f64,
365    },
366    /// The loss density of a row still failed its split test when it reached
367    /// [`MAX_NODES`] nodes (module docs, "Floating-point resolution").
368    #[error(
369        "the loss density at {energy_ev} eV is not resolved to its tolerance \
370         within {max_nodes} nodes"
371    )]
372    DensityNodeLimit {
373        /// Incident energy, eV.
374        energy_ev: f64,
375        /// The node bound, [`MAX_NODES`].
376        max_nodes: usize,
377    },
378    /// The model or the assembled table failed.
379    #[error(transparent)]
380    Data(#[from] ElectronDataError),
381}
382
383fn invalid(what: &'static str, reason: impl Into<String>) -> InelasticTableError {
384    InelasticTableError::Invalid {
385        what,
386        reason: reason.into(),
387    }
388}
389
390/// The table and the q sampler cover the direct (no-exchange) model only:
391/// the q sampler integrates the direct integrand and the table provenance
392/// does not record an exchange setting. Reject a model with the exchange
393/// correction enabled rather than build something inconsistent.
394fn check_no_exchange(penn: &SinglePolePenn) -> Result<(), InelasticTableError> {
395    match penn.exchange() {
396        Some(x) => Err(invalid(
397            "Penn model",
398            format!(
399                "exchange correction (below {} eV) is not supported by the inelastic \
400                 table or the momentum-transfer sampler",
401                x.applies_below_ev()
402            ),
403        )),
404        None => Ok(()),
405    }
406}
407
408// ---------------------------------------------------------------------------
409// Energy-loss table
410
411/// The energy axis a table is stored on (module docs, "Energy axis").
412#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
413pub enum EnergyAxis {
414    /// The model's own axis: a row at energy `E` is the model's at `T = E`,
415    /// the kinetic energy above the model's Fermi level, with losses up to
416    /// `E`.
417    #[default]
418    ModelFermiLevel,
419    /// The band-bottom axis: a row at energy `E` is the model's at
420    /// `T = E - E_F`, `E_F` the model's Fermi energy, so the kinematics use
421    /// `T' = T + E_F = E` and the losses stop at `E - E_F`. Rows with
422    /// `T <= 0` are stored empty. This is the convention of cstool's
423    /// `compile_full_imfp_icdf` (module docs).
424    BandBottom,
425}