Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

TOML input reference

An input file is TOML with these tables: [beam], [materials] (optional), [target], [physics], [stopping] (optional), [run] and [tally] (optional). An electron run has an [electron] table instead of [beam], [physics] and [tally] (section “Electron runs” below). The physics behind each [physics] choice is in the physics manual: potentials and screening lengths, electronic stopping, the free-path conventions and weak collisions, and user stopping tables. The first run walks through a complete file, and the repository’s examples/ directory has more.

This page is the input section of docs/cli.md, included here so there is one copy.

Units are in the key names: energies in eV (_ev), lengths in nm (_nm), angles in degrees (_deg), densities in g/cm³. Every table rejects unknown keys. The schema types are lindhard::input (shared with future front ends).

[beam]

KeyDefaultMeaning
ionrequiredElement symbol of the projectile (case-sensitive, "As")
mass_amustandard atomic weightProjectile mass, u
energy_evrequiredIncident energy, eV
tilt_deg0Polar angle from the surface normal, [0, 90)
azimuth_deg0Azimuth of the incidence plane
divergencenoneAngular spread, the [beam.divergence] table below

[beam.divergence] (optional)

A finite angular spread of the ion beam about the nominal tilt_deg / azimuth_deg direction. Absent: every primary has the nominal direction and the run is bit-for-bit what it was before the table existed. Static ion runs only; with a [dynamic] target it is an error (beam.divergence), and an [electron] run has no such key (unknown key error).

modelWidth keyMeaning
"gaussian"sigma_degStandard deviation of the angular deviation in each of two orthogonal planes through the nominal direction (the polar deviation is then Rayleigh distributed, with mean sigma sqrt(pi/2)). It is not a cone width.
"uniform-cone"half_angle_degDirections uniform in solid angle out to the cone half-angle.

Widths are finite and in [0, 10] degrees: the Gaussian law is a small-angle (plane-angle) reading, and the inward conditioning below is documented for small spreads only. The sampling laws are those of lindhard::ion::crystal::Divergence (docs/crystal-orientation.md).

How it is applied. Each primary’s direction is drawn once, at the entry point, from the nominal direction and the history’s own random stream, so it applies to the primary from the start, including while it crosses an amorphous screen layer before a crystal substrate; recoils keep their collision-generated directions. The crystal orientation is built from the nominal tilt and azimuth and does not move. The result is the inward- conditioned distribution: a drawn direction that does not point into the target (depth component not positive) is rejected and redrawn, at most 1000 attempts per primary, after which the run fails with an error instead of hanging (for the accepted widths one draw is inward with probability about one half or more, so this does not happen in practice). Near-grazing nominal tilts and wide spreads therefore give a distribution truncated at the surface, not the plain Gaussian or cone. The draws come from a copy of the history’s stream at word 2^65, disjoint from the transport draws, the thermal-vibration segment (2^66) and the lattice-shift segment (2^67), so turning divergence on changes no other random draw, and results do not depend on the thread count.

The summary’s physics.beam_divergence records the resolved model, width (width_rad and width_deg, with width_kind sigma_per_plane or cone_half_angle), the incidence policy inward-conditioned, the attempt bound and the stream segment; the echoed input.beam.divergence is the input as written. Both are absent without the table, and format.version stays 1 (see “Compatibility and extension”). In Python, Beam(..., divergence_model="gaussian" | "uniform-cone", divergence_deg=...) is the same table. Example: b_5keV_si_crystal_divergence.toml.

This is an input capability: it makes a finite-spread run possible and reproducible. It is not an experimentally validated channeling prediction, and it does not resolve the known deviations of the crystal model (#225).

[materials.<name>]

Named materials, in the form of lindhard::material::MaterialSpec:

[materials.SiO2]
density_g_cm3 = 2.2                    # required for compounds
elements = [
  { symbol = "Si", atom_fraction = 1.0 },
  { symbol = "O", atom_fraction = 2.0, e_d_ev = 20.0, e_s_ev = 2.0 },
]

Each element takes symbol or z, atom_fraction or mass_fraction (relative weights, normalised), and optional e_d_ev, e_b_ev, e_s_ev. Defaults for the energies come from the element table where one is tabulated; see lindhard/src/material.rs.

[target]

[target]
substrate = "Si"            # optional semi-infinite substrate

[[target.layers]]           # finite layers, front to back
material = "SiO2"
thickness_nm = 10.0

A material (in a layer or as the substrate) is either a key of [materials], an element symbol (the pure element at its tabulated density), or an inline material table with the same keys as [materials.<name>]. A [materials] key wins over an element symbol of the same name. Without a substrate the target has a back face and particles can be transmitted.

[physics]

KeyDefaultChoices
potential"zbl"zbl, kr-c, moliere, lenz-jensen
screening_lengthpaired with the potentialuniversal, firsov, lindhard
stopping"lindhard-scharff"lindhard-scharff, bethe-bloch, equipartition-ls-or, none (see below)
free_path"constant"constant, energy-dependent
min_cm_angle_degnoneRequired with, and only with, energy-dependent
weak_collisions00 to 3: weak collisions beyond p_max per collision step (Moller and Eckstein, IPP 9/64 (1988)); constant free path only. See the ion::bca docs, “Weak collisions”
primary_cutoff_evrequiredThe primary stops below this energy
recoil_cutoff_evrequiredRecoils stop below this; keep it below the smallest E_s
follow_recoilstrueFull cascades
primary_surface_binding_ev0Surface barrier for the beam species
tuning"none"Opt-in phenomenological calibration: the name of a versioned factor set (see below)

stopping = "none" switches electronic stopping off: particles lose energy only in nuclear collisions, and the energy budget’s electronic terms are zero. It is not a realistic setting for any target. It exists for like-for-like transport comparisons against codes run with their electronic stopping off (the *_nuclear level-2 problems, validation.md) and for nuclear-only studies. It has no energy range, so no validity warning is ever raised for it; physics.models lists it as none, with nothing for Bragg additivity to sum unless [stopping] tables are given.

[physics.energies.<symbol>] sets e_d_ev, e_b_ev and/or e_s_ev for that element in every layer that contains it, after (so overriding) the material’s own values. An element with no tabulated default and no value set is an error naming the layer and the key to set.

tuning (phenomenological calibration, not a published model choice). "none" or omission leaves the physics and the echoed input exactly as without the key. A named set scales the surface binding energy E_s by a per-element factor fitted to measured data. Rules: the factor multiplies the resolved E_s (an explicit [physics.energies] or material value if given, else the elemental default), once per layer, after overrides; the global element table and the collision algorithm are untouched. The pilot supports static ion runs on single-element layers only, with a beam species the set was fitted for and target elements the set lists; compounds, [dynamic] targets, other beams, unlisted elements and unknown set names are rejected with a physics.tuning error. A beam energy outside the set’s fitted range, or a tilted beam, runs with a warning (an extrapolation). summary.json then has physics.tuning with the set, its version and provenance, and per layer the original E_s, the factor and the effective E_s. Tuned results must be reported next to, never in place of, untuned ones.

Shipped sets (fit record and held-out scores: docs/data-provenance.md, “Tuning factor sets”; a new version ships under a new name):

SetBeamElementsFitted energiesFitted under
es-sputter-ar-v1ArSi, Cu, Ag, Au196 to 10020 eV, normal incidencethe matched level-3 sputter settings (docs/validation.md, section 3)

The factors are a calibration of yields under those settings; other settings (potential, E_d, cutoffs, weak collisions) were not part of the fit, and the set does not claim better accuracy for them.

[stopping]

Optional. Supplies user stopping tables for the electronic stopping of particular (ion, target element) pairs.

[stopping]
tables = ["tables/b_in_si.toml", "tables/p_in_si.toml"]
KeyDefaultMeaning
tablesnonePaths of table files, one per pair

Absent, the input means what it always meant (format.version is unchanged and nothing is echoed).

Table file. The lindhard::ion::stopping::table::StoppingTable format:

provenance = "Author, Journal vol, page (year), Table N"   # required
ion_z = 5
ion_mass_amu = 11.0093            # optional; default: standard atomic weight
target_z = 14
energy_ev = [1.0e2, 1.0e3, 1.0e4]                 # strictly increasing, eV
stopping_ev_1e15_cm2 = [10.0, 30.0, 60.0]         # eV 1e-15 cm^2 per atom

Interpolation is piecewise linear in ln S versus ln E. provenance is mandatory (data without an origin is not admitted). Tables from any source, SRIM and ICRU included, are admissible; the provenance names the source (book edition, table and page, or program, version and file).

Paths. A relative path resolves against the directory of the input file (not the current directory). The echoed input keeps the path as written.

Composition with [physics] stopping. A table replaces the [physics] stopping model for exactly the pair it declares (ion_z, target_z), including recoils of that species when follow_recoils is on. Every other pair uses the [physics] stopping model. A pair with a table is never silently served by the model: a query outside the table’s energy range, or for a different ion mass, is an error that stops the run. Nothing is extrapolated. Declare each pair once. A table for a pair that cannot occur in the run (including a table for a target element’s recoils when follow_recoils = false) is accepted with a warning that it is unused.

Recoil species. Tables are keyed by (ion_z, target_z) only, so with follow_recoils = true a table whose ion_z is a target element also serves every recoil of that element. Recoils carry the standard atomic weight and are followed down to physics.recoil_cutoff_ev, so such a table is checked up front against both: its ion_mass_amu must be the standard weight, and it must start at or below recoil_cutoff_ev; either failure is an error. A consequence is that an isotopic self-ion beam (e.g. beam.mass_amu = 27.9769 for Si into Si) cannot take a table for its own pair while recoils are followed: drop the table, use the standard weight, or set follow_recoils = false. A recoil-species table that ends below the largest energy the beam can transfer to that element warns. Tables cannot be combined with stopping = "equipartition-ls-or" (that mode carries its own Lindhard-Scharff/Oen-Robinson loss and would ignore them). They can be combined with stopping = "none": the declared pairs use their tables and every other pair has no electronic stopping. That is coherent but easy to misread, so check physics.models (it lists both none and each user-table).

Errors name the field (stopping.tables[0]): an unknown key in [stopping], a missing or unreadable file, invalid table contents (including a missing provenance), a duplicate pair, a table whose ion mass differs from the beam ion’s, a table whose range does not contain the beam energy, and the recoil-species checks above. A table that starts above physics.primary_cutoff_ev warns, because the run fails if a projectile slows below it.

Provenance in the output. Tables are user data, so the run records them. summary.json has physics.stopping_tables, one entry per table: path (as written), resolved_path (absolute where possible), sha256 of the file’s bytes, the table’s provenance string, ion_z, ion_mass_amu, target_z and the energy range. physics.models lists each as user-table with the path and provenance as its source. The key is absent without [stopping].

[[crystal]] (optional)

Optional. Runs the named stack layers on an explicit cubic lattice (the engine’s crystal flight model, Bca::with_crystal) instead of the amorphous random-target model. Every layer not named stays amorphous; absent, the input means what it always meant and nothing is echoed. One entry per crystal; several entries give different lattices or orientations in different layers.

[beam]
ion = "B"
energy_ev = 5000.0
tilt_deg = 7.0        # the crystal's tilt
azimuth_deg = 22.0    # the crystal's twist, from `reference` towards n x r

[[crystal]]
layers = [0]            # stack layer indices; the substrate is the last one
preset = "Si"           # "Si", "Ge", "GaAs" or "3C-SiC"
normal = [0, 0, 1]      # (hkl) of the surface; the inward normal
reference = [0, 1, 0]   # [uvw] in the surface plane: the zero of the azimuth
wafer_rotation_deg = 0.0

[crystal.thermal]       # optional; absent: a static lattice
temperature_k = 300.0
# debye_temperature_k = 640.0
KeyDefaultMeaning
layersrequiredIndices into the stack, front layer first, the substrate last (the index of physics.target). Each layer may belong to one crystal
presetrequiredCubic lattice with a cited lattice constant (docs/crystal-orientation.md, docs/data-provenance.md)
normalrequiredMiller indices (hkl) of the surface plane (inward normal), integers, not all zero
referencerequiredDirection [uvw] in the surface plane (h u + k v + l w = 0). It is lab +y at zero wafer rotation; there is no hidden default zero for the azimuth
wafer_rotation_deg0Rotation of the wafer about the surface normal
thermal.temperature_knoneDebye-model vibration at this temperature; sets the vibration amplitude only (no thermal expansion)
thermal.debye_temperature_kthe preset’s cited valueDebye temperature
p_max_nm, q_max_nm, search_length_nmengine defaultsSearch parameters (ion::bca::crystal, “Choice of the search parameters”): largest partner impact parameter (nearest-neighbour distance), simultaneous-collision window (5 % of it) and search segment length (lattice constant; speed only)

Angles. beam.tilt_deg and beam.azimuth_deg are the engine’s tilt and twist; there is no second beam direction. Tilt is the polar angle from the inward surface normal, azimuth is measured in the surface plane from reference towards n x reference; docs/crystal-orientation.md states the full convention and a worked example.

Checks (lindhard check and run, before transport). Errors name the key (crystal[i].normal, .reference, .layers, .preset, .thermal.temperature_k, …): a zero or non-integer index triple, a reference not in the surface plane, a layer that does not exist or is already assigned, a preset whose elements are not in the layer’s material or whose atom density differs from the lattice’s by more than 5 %, and a non-positive search length. Unsupported combinations are errors, never a silent amorphous fallback: a [dynamic] target, an [electron] run, physics.free_path other than "constant", physics.weak_collisions > 0, and physics.tuning (the only set was fitted for amorphous runs). physics.stopping = "equipartition-ls-or" is allowed with a warning: its local Oen-Robinson loss has constants that are not verified against the paper.

Output. summary.json gains physics.crystal, one object per crystal in input order, exactly as the engine reports it (Bca::crystal_metadata): regions, the lattice constant and its temperature, normal_hkl, reference_uvw, tilt_rad, twist_rad, wafer_rotation_rad, the resolved search parameters p_max_m, q_max_m, search_length_m (metres), thermal (its input, the per-species RMS displacement, the search margin and the sampling rule) and, when it applies, electronic_constants_unverified: true. physics.models gains a crystal transport entry. An amorphous input gets neither. format.version stays 1: the key is an addition (see “Compatibility and extension”).

Scope and caveats. Static, layered ion targets with the cubic presets only; hexagonal lattices, custom cells and dose-dependent crystal damage are not exposed (beam divergence is: see [beam.divergence]). This is an interface to the existing engine, not a validation: the known deviations of its channeled ranges are tracked separately (issue #225) and nothing here claims they are resolved. Examples: b_5keV_si_crystal.toml and as_50keV_sio2_on_si_crystal.toml (an amorphous oxide over a crystal substrate); neither is a validated prediction.

[dynamic] (optional)

Makes the run fluence-dependent: the target composition is updated as the fluence builds up (sputter erosion, build-up of implanted atoms). Without the table nothing changes: the run, its output files and its bytes are those of a static run. The model, its conventions and its limits are in lindhard::ion::dynamic and the book chapter on dynamic composition.

run.ions is the number of histories of the whole run and fluence_cm2 the fluence they represent, so each ion stands for fluence_cm2 / run.ions ions/cm². The ions are delivered in steps; after each step the grid is updated from that step’s events (a recoil is subtracted where it is created and added where it stops, a stopped beam ion is added, an escaped atom is a loss; the substrate is an immutable reservoir) and relaxed. Primary i of the run always uses the random stream (seed, i), whatever the step sizes and thread count.

KeyDefaultMeaning
fluence_cm2requiredTotal fluence of the run, ions/cm²
ions_per_steprequiredIons per step; with max_change, the largest and first step
max_changeabsent (fixed steps)Adaptive steps: largest relative composition change of a slab per step, the largest absolute change in atoms/m² of one element in one slab, divided by that slab’s atoms/m². A larger step is discarded and retried from the same first ion with fewer ions, which consumes no ions of the run; the step doubles again after a step below half the bound
min_ions_per_step1Adaptive only: the smallest step. At this size a step is accepted whatever its change, and a removal beyond what a slab holds is capped at what it holds (clamped column). A fixed run that removes more than a slab holds fails, naming the slab: use smaller steps or max_change
slab_nmone slab per layerSplit each finite layer into equal slabs at most this thick; the composition is tracked per slab
relaxation"ideal-mixing"How thickness follows inventory: "ideal-mixing" (additive atomic volumes) or "fixed-number-density"
number_density_cm3noneTotal atom density, atoms/cm³; required with "fixed-number-density"
atomic_volume_nm3.<Sym>elemental solid volume from the element tableAtomic volume, nm³/atom, per element (ideal mixing). Required for an element with no tabulated solid density (a gas)
energies.<Sym>[physics.energies.<Sym>], then element defaultse_d_ev, e_b_ev, e_s_ev of an element that enters the target during the run (the beam species, for example). Elements already in a layer keep that layer’s energies
erosionfalseSputter erosion: sputtered atoms are removed from the front of the target (slab 0 first, then deeper slabs) instead of from the slab where they were displaced, and the surface recedes. Must be a boolean
retention_limit.<Sym>none (off)Retention limit of an implanted species: the largest atom fraction of that element a slab may hold, in (0, 1). Atoms above it are re-emitted: removed from the slab and from the target. No default value is supplied. Written as a [dynamic.retention_limit] table, e.g. As = 0.05

With erosion = false the front surface stays at x = 0: swelling moves the interior interfaces and the back face of the slabs, not the front surface (the surface_nm column is that fixed frame, always 0). With erosion = true the lost thickness is removed from the front and the grid is re-anchored so the current surface is again x = 0; surface_nm is then the cumulative recession R in nm and a depth x in the output is x + R in the original frame. The recession of a step is the volume of the removed atoms per area under the chosen relaxation (sum Z removed_Z v_Z, or sum removed / n), and removal equals the sputtered counts per element, so no atom is created or lost. If a step sputters more of an element than the slabs hold, the excess is not removed and the element is counted in the clamped column. With a substrate, atoms sputtered from the substrate remove nothing from the slabs, so the recession falls short of Y F / n once the film is thin. With a retention_limit the excess over the limit is removed after each step (model and assumptions: the book’s dynamic-composition page); the steps file gains cum_re_emitted_atoms_per_cm2_<Sym> columns, the atoms/cm² re-emitted so far per element. Without one the output is unchanged. dynamic_summary.json totals gain recession_nm only with erosion on. Depths in the output are measured from the front surface of that step. A dynamic run needs E_d for every element that can occur, including the beam species. The Python bindings run static inputs only.

[run]

KeyDefaultMeaning
ionsrequiredNumber of primary histories
seedrequiredRun seed
threadsall coresWorker threads; does not affect results and is not echoed

[tally]

KeyDefaultMeaning
depth_bin_nm1Bin width of the stopped-primary depth profile
depth_bins1000Number of bins; the last also collects everything deeper
per_iontrueWrite ions.csv
lateral_bin_nm1Bin width of the lateral and radial profiles of stopped primaries
lateral_bins100Bins per side of the beam axis: y and z span [-lateral_bins * lateral_bin_nm, +lateral_bins * lateral_bin_nm) nm, the radial distance [0, lateral_bins * lateral_bin_nm) nm
escape_energy_max_evbeam energyUpper edge of the escape-energy spectra, eV (from 0)
escape_energy_bins100Escape-energy bins
escape_polar_bins30Polar-angle bins over [0, 90) degrees from the outward surface normal
dual_pearsonfalseAlso fit a dual-Pearson profile to the depth histogram
voxelabsentA [tally.voxel] table: also write the per-element 3D voxel tally (below)

[tally.voxel] takes origin_nm (corner of the first voxel, [x, y, z]), spacing_nm (voxel edge lengths) and dims (voxel counts), all three-element arrays; x is the depth and the beam starts at y = z = 0. It is accepted for static ion runs only. Events outside the grid are summed in the file header, never dropped.

The depth grid (depth_bin_nm, depth_bins, from the front face) is shared by the range histogram, the dual-Pearson fit and the defect profiles. Particles outside any grid are counted in explicit underflow and overflow entries, never dropped. Every count and per-ion value is for the same incident ions.

Electron runs ([electron])

An input with an [electron] table runs the low-energy electron engine (lindhard::electron::transport) instead of the ion BCA, with the full electron tally (lindhard::tally::FullElectronTally). It exposes what the library does and adds no physics; every choice and every data provenance is written to the output, so a result can be reproduced from its own header. The schema types are lindhard::input::electron. [materials] and [target] are the ion run’s tables (above); [beam], [physics], [stopping], [tally] and [dynamic] are not accepted. Example: ../examples/electron/e_10keV_si.toml.

[electron.beam]
energy_ev = 10000.0

[electron.transport]
cutoff_ev = 1.0
cutoff_reference = "vacuum-level"
secondaries = "kieft-bosch"
boundary = "step-barrier"

[electron.elastic]
potential = "thomas-fermi-yukawa"

[electron.inelastic]
model = "penn-single-pole"

[electron.materials.Si]
optical_elf = "si_elf.toml"
band = { kind = "insulator", valence_band_width_ev = 10.0, band_gap_ev = 2.0, affinity_ev = 3.0, provenance = "..." }

[target]
substrate = "Si"

[run]
histories = 1000
seed = 1

[electron.beam]

KeyDefaultMeaning
energy_evrequiredKinetic energy of the primaries, eV: the vacuum energy with boundary = "step-barrier", the energy inside the first layer otherwise
tilt_deg0Polar angle from the surface normal, [0, 90)
azimuth_deg0Azimuth of the incidence plane
landing_nm[0, 0]Landing point [y, z] of the beam centre on the front face, nm
profilepoint[electron.beam.profile], lateral profile about the landing point (below)
divergencenone[electron.beam.divergence], the same table as the ion beam’s [beam.divergence] (gaussian with sigma_deg, or uniform-cone with half_angle_deg; at most 10 degrees)

Primaries start on the front face (just outside it with the step barrier) at the landing point, which defaults to y = z = 0. [electron.beam.profile] selects the lateral profile by model: point (the default: every primary lands at the landing point), gaussian (sigma_nm, the standard deviation of each of y and z), disc (radius_nm, uniform in area) or rectangle (width_y_nm, width_z_nm, full widths, uniform). Sizes must be positive. Each history draws its start position, then its direction, from its own random stream, so results do not depend on the thread count. Divergence is conditioned on directions into the target, as for ions. The default (a point beam, no divergence) draws no extra random numbers, so existing inputs give identical results for a fixed seed. Through the library, use Transport::run_beam with a BeamSpread; Transport::run is the point beam. The slab has no lateral edge, so a spot is never partly off the face here.

[electron.transport] (lindhard::electron::transport::TransportConfig)

KeyDefaultChoices
cutoff_evrequiredAn electron stops below this energy
cutoff_reference"band-bottom"band-bottom; vacuum-level (the threshold is U + cutoff: electrons that can no longer leave are not followed)
escape_rule"both-faces"both-faces; front-only (the back face absorbs)
max_events10000000Collision and reflection cap per electron
secondaries"off"off; kieft-bosch (Kieft and Bosch 2008)
instantaneous_momentum, momentum_conservationtrueOptions of kieft-bosch; an error with off
boundary"transparent"transparent; step-barrier (inner-potential step with quantum transmission and refraction)
quantum_transmission, refractiontrueOptions of step-barrier; an error with transparent

[electron.elastic]

KeyDefaultChoices
model"mott"mott: Mott cross sections from radial-Dirac partial waves, independent-atom additivity (electron::elastic::table)
potentialrequiredthomas-fermi-yukawa: the Thomas-Fermi Yukawa stand-in; salvat-dhfs: the Salvat et al. (1987) DHFS potentials (Table I coefficients, Z = 1..92; data-provenance.md)
exchangefalseFurness-McCarthy exchange correction
correlation_polarizationabsent (off)A table: polarizability.<Sym> = { bohr3 = ..., source = "..." } for every target element (the source is required), optional b_pol_squared (absent: Seltzer’s rule, which needs every table energy above 50 eV) and outer_radius_bohr (50)

The corrections are solved per grid energy with the chosen potential’s own Poisson density: the stand-in’s, or the DHFS density of Salvat et al. (1987) Eq. (12) (AtomicElastic::compute_corrected); the elastic table’s model and provenance strings name them and every polarizability with its source.

[electron.inelastic]

KeyDefaultChoices
model"penn-single-pole"penn-single-pole, penn-full, mermin-melf (electron::inelastic::PennAlgorithm). The full Penn and Mermin models integrate numerically and build tables far more slowly. The single-pole model’s mean free path is much longer than the other two below about 30 eV (Al: up to 23 times), which inflates the secondary yield; see electron::inelastic::penn, “Low energies” (#173)
fermi_energy_ev0Fermi energy of the model, eV, for the inelastic tables of materials without a band: the table is on the model’s own axis (energy above the model’s Fermi level, losses up to that energy). Must be 0 if any material has a band (a nonzero value is refused): see below

Energy axis of the inelastic table. The transport reads a table at the electron’s kinetic energy E above the band bottom of its layer. For every material with a band the inelastic table is built on that axis, with the model’s Fermi energy taken from the band: the row at E is the model’s at E - E_F, so the kinematics use E and no loss exceeds E - E_F. E_F is the band’s minimum excitation energy: the Fermi energy of a metal (fermi_ev, or the free-electron value), the conduction-band bottom valence_band_width_ev + band_gap_ev of an insulator. Table energies at or below it have no losses. This is the convention of the table compiler of Nebula (compile_full_imfp_icdf, cstool commit 0c739eb; see electron::inelastic::table, “Energy axis”, and electron::transport, “Energy reference of the inelastic table”). fermi_energy_ev plays no part for such a material, which is why it must be 0: adding it would count the Fermi energy twice. A material without a band gets the table of the model’s own axis with fermi_energy_ev, read at E as it is.

Migration. Before this rule, fermi_energy_ev applied to every material and tables ignored the band; losses beyond E - E_F were clamped by the secondary model. An input with a band and a nonzero fermi_energy_ev now fails, naming the key: remove the key. Results of runs with a band change (see CHANGELOG.md).

[electron.tables]: one log-spaced energy grid shared by the elastic and inelastic tables of every material. The inelastic table of a material with a band holds extra rows besides (#339; “Grid for a material with a band”, below).

KeyDefaultMeaning
min_energy_ev10Lowest grid energy, eV
max_energy_evthe beam energy (plus the largest inner potential with the step barrier)Highest grid energy, eV
points_per_decade20Minimum points per decade

The transport holds the rates of the first and last rows beyond the grid; a grid that starts above the lowest stopping threshold or ends below the largest possible energy warns.

Grid for a material with a band (#291). On the band-bottom axis the inelastic rate is zero at E_F and climbs steeply above it (within the first few eV, and for the Al single pole again at 15 to 17 eV), and the transport interpolates it linearly between grid rows. None of 10, 20 or 40 points per decade resolves that region to 1 %; at 80 the Mermin tables of Al and Si pass and the single poles still miss it. The largest relative error of the interpolated 1/λ over E - E_F = 2 to 20 eV (Al and Si, 400 eV inputs) is 4 to 75 % at the default 20 points per decade and 2 to 27 % at 40, depending on material and model; the single-pole model is worse than Mermin. Full table in validation.md, “Table grid above the Fermi level”; reproduce with lindhard-cli/examples/inelastic_low_energy.rs, mode rate. The library can add rows to the inelastic table from E_F upward (InelasticTableOptions::rate_refinement, #339), which brings every one of those tables to about 1 % at both the default grid and the grid of validation/experiments/se_yield.py. lindhard run uses it for the inelastic table of every material with a band (#339): every cell of the grid overlapping E - E_F = 2 to 20 eV is bisected while the linear interpolation of the rate misses the exact rate by more than 1 % at the cell’s quartiles, midpoint or estimated error peak. The table then has the grid’s rows and the added ones (a few tens: Al and Si, 5 to 25 rows at these grids); physics.materials.inelastic_table.energies counts them. The elastic table, and the inelastic table of a material without a band, keep exactly the grid’s rows. A denser grid is not needed for this region.

[electron.materials.<name>]: the electron data of each material the target uses, keyed by the name the target gives it (a [materials] key or an element symbol; an inline target material is an error in an electron run). Every name the target uses needs an entry; an unused entry warns.

KeyDefaultMeaning
optical_elfrequiredPath of an optical ELF file, relative to the input file’s directory, in the lindhard::electron::data::OpticalElf TOML form (material, provenance, energy_ev, elf). It is read with that type’s loader, so a file without a provenance is refused, as is any invalid table
bandnoneBand parameters, required with kieft-bosch, step-barrier or vacuum-level: { kind = "metal", fermi_ev, work_function_ev, provenance }, { kind = "insulator", valence_band_width_ev, band_gap_ev, affinity_ev, provenance } or { kind = "free-electron-metal", valence_electrons_per_atom, work_function_ev, provenance } (lindhard::electron::boundary::BandStructure; a blank provenance is refused). Either metal kind also takes an optional valence_binding_ev: the electron a valence loss liberates is bound that far below the Fermi level instead of at it, and a smaller loss frees none (BandStructure::with_valence_binding_ev; absent by default; its source belongs in provenance)
phononnone (off)Fröhlich LO-phonon channel, polar insulators only: { hbar_omega_ev, eps_static, eps_high_frequency, temperature_k, provenance }, or { preset = "sio2-63mev" | "sio2-153mev", temperature_k } (the library’s cited SiO₂ values)
polaronnone (off)Polaron trapping C exp(-γE): { c_per_nm, gamma_per_ev, provenance }
mermin_fitnone (the default fit)Options of this material’s Mermin oscillator fit, mermin-melf only: see below
inner_shellsnone (valence only)Inner-shell ionisation channels of this material, penn-single-pole only: see below
e_pairnoneMean energy per electron-hole pair, { ev, source }: ev positive, source a required non-blank citation (for example the Klein 1968 rule for a semiconductor, or a measured SiO₂ value). No default is supplied. Without it the summary reports the ionisation energy of the material’s layers and no pair count. Post-processing only: transport does not read it

[electron.materials.<name>.mermin_fit] (lindhard::electron::inelastic::MerminFitOptions): the mermin-melf model fits Drude-Lorentz oscillators to each material’s optical ELF (fit_mermin_oscillators) and extends them in momentum with the Mermin dielectric function (Mermin, Phys. Rev. B 1, 2362 (1970); de Vera et al., Int. J. Mol. Sci. 23, 6121 (2022)). This optional table sets the fit for one material; without it the fit uses MerminFitOptions::default() and results are unchanged. With any other electron.inelastic.model the table is an error, since nothing would use it.

KeyDefaultMeaning
oscillators3Number of oscillators, 1 to 16
weighting"relative"relative: residuals divided by ELF + relative_floor · max ELF, so the tails count; uniform: absolute residuals
relative_floor0.01The floor of relative, in (0, 1]; an error with uniform

The starting point and the iteration cap of the fit are not exposed. The fit is recorded, oscillator by oscillator, in the inelastic table’s model string; the fit also reports its f-sum and P_eff sum rules against the table’s (MerminFit), which are worth checking when choosing the options. Three oscillators do not describe every ELF: the default fit of the Cu ELF in validation/data/optical recovers 57 % of its f-sum and 67 % of its P_eff (docs/validation.md, “Cu: the Mermin IMFP and the default oscillator fit”), so such a material should use more. More oscillators can also give a very wide one, far wider than the table’s energy range, whose f-sum lies above the table (#311). Measured on that Cu ELF (same section, “Weighting options and the wide oscillator”): at 10 oscillators the default weighting gives one (f-sum 188 times the table’s, unconverged), relative_floor = 0.1 and 1 do not (f-sum 0.79 and 0.87 of the table’s, P_eff 0.98 and 1.03, converged), and uniform is worse (4e4 times). At 16 oscillators none of relative_floor 1e-3, 1e-2, 0.1, 1 or uniform avoids it (31 to 3e33 times the table’s f-sum), including fits that report convergence. So, whatever the options, check the fit’s f-sum and P_eff against the table’s in the fit report (MerminFit); convergence alone and a plausible IMFP do not show a sound fit. For example:

[electron.inelastic]
model = "mermin-melf"

[electron.materials.Cu]
optical_elf = "cu_elf.toml"

[electron.materials.Cu.mermin_fit]
oscillators = 6

The echoed input repeats the table, with its defaults filled in, only when it is given.

No optical or band data of any real material is committed (data-provenance.md); the data files are the user’s, and their terms are the user’s concern. Subshell binding-energy tables (SubshellBindingTable) are read through the optional binding key of inner_shells, below.

[electron.materials.<name>.inner_shells] adds inner-shell ionisation channels to a material (lindhard::electron::transport::Transport::with_inner_shells, attached to every layer of that material):

KeyDefaultMeaning
subshellsrequiredSubshells of every element of the material that become channels: "K", "L1", "L2", "L3" (the only ones with a hydrogenic formula). An empty list, a repeat, any other label and M and above are refused
bindingthe embedded EADL2017 tablePath of a binding-energy table in the TOML form of SubshellBindingTable (relative to the input file’s directory). It is read with that type’s loader, so a table without a provenance is refused, and so is one that lacks an element of the material or a requested subshell of it
[electron.inelastic]
model = "penn-single-pole"

[electron.materials.Si.inner_shells]
subshells = ["K", "L1", "L2", "L3"]
# binding = "my_binding.toml"   # optional override of the EADL2017 table

Each channel is the shell’s hydrogenic optical ELF (electron::inelastic::hydrogenic_shell_elfs, atom density from the material) tabulated with single-pole Penn on the energy axis of the material’s inelastic table (build_shell_channel_tables). Limits, as in the library: single-pole Penn per channel, no exchange correction, and the material’s valence ELF must not already contain the shells (no partition rule is applied: an ELF that covers the shell edges counts them twice). A material with no inner_shells table runs exactly as before. The summary records the subshells and the table under physics.materials[].inner_shells (binding_source is "file", with the file’s path and sha256, or "embedded EADL2017", plus binding_provenance), the channel in physics.models, and the channels per layer in physics.transport. The channel tables are built on every run and are not part of the table cache.

[electron.tally] (lindhard::tally::ElectronTallyConfig)

KeyDefaultMeaning
se_bse_split_ev50Escaping electrons below it are slow (secondary), at or above it fast (backscattered)
escape_energy_max_evbeam energyUpper edge of the escape-energy spectra (from 0), eV
escape_energy_bins100Escape-energy bins
escape_polar_max_deg90Upper edge of the polar-angle spectra (from 0, at most 180), degrees from the outward normal
escape_polar_bins18Polar-angle bins
cartesiannoneDeposition grid { x, y, z }, each { lo_nm, hi_nm, bins } (x is depth)
cylindricalnoneDeposition grid { r, depth } about the beam axis, each { lo_nm, hi_nm, bins } (r.lo_nm >= 0)
psfnoneRadial profile of the deposited energy in a depth slab and its point-spread-function fits, see below
exposurenoneDistribution of the energy deposited in small volumes by a finite number of electrons, see below

[electron.tally.psf] (lindhard::tally::PsfConfig, fit_psf): the energy deposited in the slab depth_lo_nm <= x < depth_hi_nm, binned in the distance from the beam axis on a central disc plus log-spaced bins, with per-history errors, and double- and triple-Gaussian fits to it.

KeyDefaultMeaning
depth_lo_nm, depth_hi_nmrequiredThe depth slab, nm (hi > lo)
r_min_nmrequiredOuter edge of the central disc and inner edge of the first log bin, nm (> 0)
r_max_nmrequiredOuter edge of the last bin, nm (> r_min_nm)
binsrequiredLog bins between r_min_nm and r_max_nm (the central disc is one more)
fits["double", "triple"]Forms to fit, any subset of "double" and "triple" (may be empty)
normalization"slab-total""slab-total" fixes the fit’s energy scale to the slab energy; "free" fits it

[electron.tally.dose] (lindhard::tally::DoseConfig, module lindhard::tally::dose): the energy and the dose in each layer per primary, a depth-dose profile through the stack and the electrons crossing each interface, all with per-history standard errors. Absent, none of it is tallied and nothing else changes.

KeyDefaultMeaning
depth_bins_per_layernone (no profile)Uniform depth bins over the middle of each layer (the whole layer without refinement), at least 1
refine_width_nmnoneWidth of a refined band next to each face of each layer, nm; limited to half the layer. Needs depth_bins_per_layer
refine_bins10Uniform bins in each refined band
substrate_depth_nmrequired with a profile and a substrateDepth binned in a semi-infinite last layer, nm; deeper energy is outside_ev
transmissionfalseCount the electrons crossing each interface, forward and back, primaries and secondaries separately, with a spectrum on the escape energy binning

Conventions. The target is laterally infinite, so the energy deposited in a layer per primary divided by the layer’s areal mass density ρ t is the absorbed dose per unit incident fluence, in Gy m² (multiply by a fluence in m⁻²). A semi-infinite last layer has no finite areal mass and is reported as energy only. Deposits use the tally’s point convention (an inelastic loss at the collision point, the sub-threshold remainder where the electron stops) and are measured from the band bottom, so with a secondary model they include the Fermi-sea energy of liberated conduction electrons and can sum to more than the energy the beam imparted. A deposit is binned by the layer it was made in, so the bins of a layer sum to its layer total. A crossing is counted each time an electron crosses an interface, at its kinetic energy on the new side. The standard error of a quantity is that of the mean of the per-history totals (the primary and its secondaries together), NaN for fewer than two histories.

[electron.tally.voxel]: the same fields as the ion [tally.voxel] (origin_nm, spacing_nm, dims; same validation). Tallies the deposited energy per voxel, by channel (electronic_ev, lattice_ev, rest_ev), and writes it as damage_export.csv with engine = electron (see Damage export format). The voxel energies plus outside sum to results.budget.deposited_ev. The species, defect and displacement columns are empty or zero: the electron tally has no species axis. Deposits are point events, so a flight is not split across voxel faces. Without the table no file is written, and one left in a reused output directory is removed.

[electron.tally.exposure] (lindhard::tally::exposure): the energy X that one primary history (the primary and its secondaries) deposits in each target volume, binned into a histogram with an exact bin at X = 0, and the distribution of S = X_1 + ... + X_N for N ~ Poisson(lambda) electrons. E[S] = lambda E[X] and Var[S] = lambda E[X^2] use the exact moment sums; quantiles and threshold probabilities come from a Poisson-weighted n-fold convolution of the binned X and are reported as low / central / high: the three values with every bin at its lower edge, centre and upper edge (the true quantile and P(S > b) lie in [low, high], the true P(S < a) in [high, low]; histories above x_max_ev are clamped into the last bin and counted in overflow_fraction). Valid only for a laterally uniform target: the landing offset of the beam is drawn per history and the volume shifted by it. A volume extending past the film collects only the part inside it. A lambda too large for the convolution gives the exact moments and a note instead of the distribution.

KeyDefaultMeaning
lambdarequiredMean number of incident electrons
beamrequired{ kind = "point" }, { kind = "gaussian", sigma_nm } (per axis) or { kind = "uniform-disc", radius_nm }
volumesrequiredArray of { name, shape = "cylinder", radius_nm } or { name, shape = "box", half_y_nm, half_z_nm }, each with depth_lo_nm, depth_hi_nm and offset_y_nm, offset_z_nm (default 0) from the beam axis
bins200Histogram bins on (0, x_max_ev]
x_max_evbeam energyUpper edge of the histogram, eV
quantiles[0.05, 0.5, 0.95]Probabilities whose quantiles of S are reported
below_ev, above_ev[]Thresholds for P(S < a) and P(S > b), eV
tail_tolerance1e-12Poisson tail mass dropped from the convolution

[run] of an electron run: histories (required), seed (required) and threads (all cores; not echoed, never changes results).