Skip to main content

Module transport

Module transport 

Source
Expand description

Event-by-event electron transport in a layered stack, on precomputed cross-section tables, with secondary electrons and surface barriers.

This is the low-energy electron Monte Carlo loop of Kieft and Bosch, J. Phys. D: Appl. Phys. 41, 215310 (2008), doi:10.1088/0022-3727/41/21/215310 (the scheme on which Nebula is built): an electron at (r, direction, E) in a layer flies a free path drawn from the total inverse mean free path (the sum of the elastic and inelastic CrossSectionTable rates of the layer at E), then picks the elastic or the inelastic channel with probability proportional to the two rates, samples the polar angle θ or the energy loss W from that channel’s inverse CDF, and updates its state. An electron ends when E falls below the stopping threshold or it leaves the target. The loop itself is written from the paper; the secondary electron kinematics (crate::electron::secondary) and the potential step (crate::electron::boundary) are ported from Nebula with attribution.

§Targets other than a stack

Transport<G> is generic over Geometry; Transport::new and Transport::with_band_structures build the layered Transport<Stack>, Transport::on_geometry and Transport::on_geometry_with_band_structures any other geometry (for example a crate::geometry::VoxelGrid). Tables, band parameters, channels and thresholds are indexed by material (Geometry::material_index), and ElectronState::layer is the electron’s material index (the layer index, for a stack). The flight uses Geometry::flight: a face between two regions of one material is not an event and the free path is not redrawn there. A primary starts at Geometry::entry_point. An electron leaving through a lateral face of a voxel grid ends with Fate::Escaped(Face::Side), whatever the EscapeRule. The Boundary::Interface indices are region indices. The step barrier is applied to the x component of the direction, the face normal of a stack only, so the on_geometry constructors accept BoundaryModel::Transparent only. For a stack the results are those of before the generalisation, bit for bit.

§Model

  • Free flight. s = -ln(1 - u) / (λ_el⁻¹ + λ_inel⁻¹), u uniform in [0, 1). Rates are interpolated linearly in energy on each table’s own grid and held constant beyond its ends.
  • Layer boundaries are crossed exactly. If the drawn path is longer than the distance to the layer face along the flight direction, the electron moves to the face (its depth is set to the face value exactly), meets the face (below) and redraws the path with the rates of the layer it is then in. This is exact because the free path is memoryless.
  • Elastic collision. θ from the table, azimuth uniform on [0, 2π); the energy is unchanged (no recoil energy).
  • Inelastic collision. W from the table (clamped to E); the energy becomes E - W. With SecondaryModel::Off the flight direction is unchanged and nothing else happens. With SecondaryModel::KieftBosch W is further clamped to E - E_F (the primary cannot end below the Fermi level, as in Nebula’s loss table), and the event may liberate a secondary electron (energy E_F + W - B, direction by the Ivanchenko method) and deflect the primary; see crate::electron::secondary. The stopping threshold of every layer must then exceed its Fermi energy, or secondaries would multiply without end.
  • Insulator channels (opt-in per layer). A layer given InsulatorChannels through Transport::with_insulator_channels adds up to three more rates to the total: Fröhlich LO-phonon emission and absorption, and polaron trapping (models and sources in crate::electron::phonon). A phonon event changes the energy by ∓ħω and deflects the electron by θ drawn from the Fröhlich angular distribution, azimuth uniform. A trapped electron deposits all of its remaining energy where it is trapped and its history ends with Fate::PolaronTrapped. Layers default to no insulator channel, which is the right choice for a metal; the per-layer choice is in LayerMetadata.
  • Inner-shell channels (opt-in per layer). A layer given ShellChannelTables through Transport::with_inner_shells adds one rate per shell j, read from that shell’s table like the others; the layer’s inelastic table is then the valence channel. The construction is channel-first: the inelastic rate is the sum of the valence and shell rates, a channel is chosen with probability proportional to its rate (below), and the loss ω is drawn from the chosen channel’s own inverse CDF. (The conditional choice of crate::electron::inelastic::ShellResolvedChannels::sample_channel belongs with a loss drawn from the total DIIMFP and is not used.) A shell channel is closed (rate zero) when the electron cannot pay its binding energy B_j: when E <= B_j without a secondary model and E - E_F <= B_j with one. An open channel’s ω is kept in [B_j, E] ([B_j, E - E_F] with a secondary model), so the primary never ends below the Fermi level and the shell’s secondary energy is never negative. With SecondaryModel::Off the event only takes ω; with SecondaryModel::KieftBosch it liberates an electron of energy E_F + ω - B_j and leaves B_j - E_F in the solid (crate::electron::secondary, “Inner-shell events”). Layers default to no shell channel, which is exactly the valence-only transport.
  • Channel choice. With the rates in the fixed order elastic, inelastic (valence), phonon emission, phonon absorption, polaron trapping, then the inner shells in the order given, the channel is the first whose cumulative rate exceeds u · total; channels with zero rate are never chosen (a rounding overshoot falls to the last channel with a positive rate). A layer without shells sums and compares exactly the five rates of before.
  • Faces. With BoundaryModel::Transparent a face changes nothing: the electron enters the next layer or leaves the target at its energy and direction. With BoundaryModel::StepBarrier every face is a step in the inner potential (vacuum outside the target has U = 0), crossed with the quantum-mechanical transmission probability and refraction of crate::electron::boundary, or reflected specularly; an electron below the barrier is always reflected. Energies inside a layer are then measured from that layer’s band bottom and an escaped electron carries its vacuum energy E - U. The back face under EscapeRule::FrontOnly absorbs without a barrier.
  • Inverse CDFs between grid energies are interpolated linearly in energy between the two bracketing rows, at the same cumulative probability (a zero-rate bracketing row is ignored).

§Electrons of one history

A history starts with its primary. With BoundaryModel::StepBarrier the primary is incident from vacuum on the front face, its Primary energy is a vacuum energy, and it first meets the front-face step (it may be reflected there, ending the history as Fate::Escaped through Face::Front). Secondaries are pushed on a last-in, first-out stack and each is transported in full, like the primary, once the electron before it has ended; secondaries of secondaries are pushed on the same stack. The history ends when the stack is empty.

An electron stops when its energy falls below the stopping threshold of its layer: the cutoff, measured from the band bottom (CutoffReference::BandBottom) or from the vacuum level (CutoffReference::VacuumLevel, threshold U + cutoff, Nebula’s rule in source/drivers/cpu/cpu_driver.inl at the commit named in crate::electron::boundary). A secondary below the threshold of its layer is not created; its energy is recorded as deposited.

§Energy reference of the inelastic table

The transport looks up both tables of a layer at the electron’s kinetic energy E as it measures it: from the band bottom of the layer (with the step barrier, the vacuum energy plus the inner potential U). The models of crate::electron::inelastic have their own axis instead: T, the energy above the model’s Fermi energy, with kinematics on T' = T + E_F(model) and losses up to T (Shinotsuka et al. 2017, eqs. (2)-(3), as in crate::electron::inelastic::penn). The table has to bridge the two, and it does so when it is built (crate::electron::inelastic::table, “Energy axis”):

  • Band-bottom axis (EnergyAxis::BandBottom, with the model’s Fermi energy set to the band’s): the row at E is the model’s at T = E - E_F, so its kinematics are on T' = E and its losses stop at E - E_F. This is the convention of the cstool table compiler which builds Nebula’s tables (rows at the kinetic energy K, kinematics on K, losses below K - F, the “Fermi correction” of compile_full_imfp_icdf in cstool/dielectric_function/compile.py, Nebula-simulator/cstool commit 0c739eb), and F is what cstool passes there, BandStructure::min_excitation_ev: the Fermi energy of a metal, the conduction-band bottom of an insulator. It is how lindhard run builds the table of every material with a band. The losses of such a table are below the clamp of SecondaryModel::KieftBosch above, which therefore does not act (test band_bottom_table_never_reaches_the_fermi_clamp, tests/electron_secondaries.rs). One exception remains, for either axis: between the last empty row of a table and its first row with losses the rate is interpolated from zero and the loss is drawn from that first row, so it can exceed what an electron below that row may lose; there the clamps (W <= E, and W <= E - E_F under Kieft-Bosch) are still the bound.
  • Model axis (EnergyAxis::ModelFermiLevel, the builder’s default, and what lindhard run builds for a material without a band, with [electron.inelastic] fermi_energy_ev): the row read at E is the model’s at T = E. With the model’s Fermi energy 0 the kinematics are right (T' = E) but the losses reach E; in a layer with a band and the Kieft-Bosch model the clamp takes every loss beyond E - E_F to E - E_F, leaving the primary at the Fermi level and giving the secondary its whole energy. Setting the model’s Fermi energy to the band’s on this axis counts E_F twice (T' = E + E_F). The tests inelastic_table_is_read_at_the_band_bottom_energy and losses_beyond_the_fermi_level_are_clamped_to_it pin how the transport reads and clamps such a table.

Inner-shell channel tables (crate::electron::inelastic::table::build_shell_channel_tables) are built on the axis of the options they are given, like the valence table, and are read the same way, at the band-bottom energy E. lindhard run does not build them yet (no input, #156); a library caller picks the axis. On top of the table, the transport applies the shell’s own limits on the band-bottom axis: the channel is closed unless the largest allowed loss (E, or E - E_F under the secondary model) exceeds B_j, and ω is clamped into [B_j, that loss]. Near an edge, a table on the model axis (losses up to E above the model’s Fermi energy) has losses above E - E_F that this clamp moves down to E - E_F, as for valence losses; a band-bottom table’s losses are already below E - E_F, apart from the interpolation exception above.

Measured effect (#173). Until #241 lindhard run built every table on the model axis with Fermi energy 0. With the single-pole Penn default, the Al input of validation/experiments/se_yield.py (E_F = 11.66 eV) then sent 78 to 82 % of the inelastic events of electrons 5 to 20 eV above the Fermi level into the clamp (Al 400 eV, 200 histories). A diagnostic table in the cstool convention (the model’s rows at T = E - E_F, relabelled E, so on a grid shifted by E_F) removed every clamped event and changed δ by a few per cent, against a factor of about 5 to explain (1000 histories, seed 1): single pole Al 7.61 to 8.17 and Au 3.23 to 3.37 (800 eV); Mermin Al 1.34 to 1.26 (400 eV). With 200 histories: single pole Cu 1.67 to 1.80, Mermin Au 2.62 to 2.58 and Cu 2.02 (800 eV). The gap between the single pole and Mermin was the same in both. The axis was therefore a real mismatch but not the cause of the δ overestimate of #173; that is the single-pole model’s inelastic mean free path at low energy (crate::electron::inelastic::penn, “Low energies”).

With the band-bottom table lindhard run builds since #241 (on the input’s own grid), the same Al input at 400 eV (200 histories, seed 1, so about ±0.1 to 0.2 in δ) has no clamped event in any energy bin, and δ is 6.44 against 6.89 with the table built as before. The commands are in lindhard-cli/examples/inelastic_low_energy.rs (modes clamp and legacy).

§Random draw order

The order of draws from the per-history stream is part of the contract (the tests replay it). With the step barrier, the primary first draws one uniform for its entry. Then, per flight with a nonzero total rate: one uniform for the path. If a collision happens, one for the channel, then θ and the azimuth (elastic), W (inelastic), or cos θ and the azimuth (phonon emission or absorption) or nothing (polaron trapping). Under the Kieft-Bosch secondary model an inelastic event that liberates an electron then draws the secondary’s azimuth and, with the instantaneous momentum on, two more uniforms. An inner-shell event draws one uniform for ω, then, under the Kieft-Bosch model, the secondary’s azimuth and, with the instantaneous momentum on, two more uniforms (a shell event always liberates an electron). With no insulator or inner-shell channel switched on, the draws are exactly those of a run without them. A path drawn for a flight that ends at a layer face is discarded; with the step barrier, the face draws one uniform if the electron has the normal energy to get over (and none if it is surely reflected). A flight with zero total rate draws nothing. Secondaries continue on the same stream, in stack order, after the electron before them ends.

§Reproducibility

Transport::run goes through crate::rng::run_particles: history i uses the stream (seed, i), and chunk tallies are merged in chunk order, so a tally whose ElectronTally::merge is order-faithful (a sum, or a concatenation) is bit-identical at any thread count.

§Hooks

The ElectronTally trait mirrors crate::ion::bca::BcaTally: every event hook has a no-op default. Per-electron hooks (step, elastic, inelastic, secondary, interface, barrier, reflected, stopped, escaped, absorbed, phonon, polaron_trapped) fire for the primary and for every secondary; ElectronTally::begin_secondary and ElectronTally::end_secondary bracket each secondary, and ElectronTally::end_history comes last, with the primary’s fate. The insulator channels report through ElectronTally::phonon and ElectronTally::polaron_trapped. An inner-shell event calls ElectronTally::inelastic like a valence one, then ElectronTally::inner_shell with the shell, then (with a secondary model) ElectronTally::secondary.

ElectronTally::table_lookup is a diagnostic hook, also with a no-op default: it reports, per channel, whether each rate evaluation read the table inside its energy grid or used the constant continuation beyond an end (GridCoverage). It draws nothing and changes nothing, so a tally that uses it sees the same histories as one that does not. crate::tally::TableCoverageTally counts these evaluations. The rates of inner-shell channel tables are not reported to it.

Re-exports§

pub use crate::geometry::Face;

Structs§

BeamSpread
Where a run’s primaries land and how they diverge: a finite lateral profile about a landing point on the front face, and an angular spread about the nominal Primary::direction. The default is a point beam without divergence, which draws no random numbers, so a run with it is bit-identical to one made before this type existed.
ElectronState
The state of an electron after an event. Positions are [x, y, z] in metres, x the depth.
InnerShellMetadata
Identity of one inner-shell channel table of a layer, for LayerMetadata.
LayerMetadata
Identity of the tables used in one layer, for RunMetadata.
LayerTables
The tables of one layer.
Primary
A primary electron: it starts on the front face (x = 0, y = z = 0) in the first layer, or, with BoundaryModel::StepBarrier, just outside it in vacuum (then energy_ev is the vacuum energy and the direction must point into the target).
RunMetadata
What a run was configured with, for output metadata.
SummaryTally
A minimal summary tally: counts, path length and energy bookkeeping.
Transport
The electron transport engine: a stack, the tables of each layer, their band parameters (optional) and a configuration.
TransportConfig
Run configuration, recorded in the RunMetadata.
TransportRun
The result of Transport::run.

Enums§

BeamProfile
Lateral profile of the beam on the front face, about its landing point. Lengths are metres.
Boundary
A face an electron met: between two layers, or a face of the target.
BoundaryModel
What a face does to an electron.
CutoffReference
Where the energy cutoff is measured from.
EscapeRule
What happens when an electron reaches a face of the target. Whether it can get out at all is decided by the BoundaryModel; an electron that does leave never returns.
Fate
How a history (or one electron of it) ended.
GridCoverage
Where an energy lies against the energy grid of one table, as reported to ElectronTally::table_lookup.
PhononEvent
Which way a Fröhlich LO-phonon event went.
TableChannel
One of the two cross-section tables of a layer (LayerTables).
TransportError
Errors from building a Transport or starting a run.

Traits§

ElectronTally
Event hooks called by Transport. All default to no-ops except merge. The ElectronState passed is the state after the event.