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]
| Key | Default | Meaning |
|---|---|---|
ion | required | Element symbol of the projectile (case-sensitive, "As") |
mass_amu | standard atomic weight | Projectile mass, u |
energy_ev | required | Incident energy, eV |
tilt_deg | 0 | Polar angle from the surface normal, [0, 90) |
azimuth_deg | 0 | Azimuth of the incidence plane |
divergence | none | Angular 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).
model | Width key | Meaning |
|---|---|---|
"gaussian" | sigma_deg | Standard 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_deg | Directions 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]
| Key | Default | Choices |
|---|---|---|
potential | "zbl" | zbl, kr-c, moliere, lenz-jensen |
screening_length | paired with the potential | universal, firsov, lindhard |
stopping | "lindhard-scharff" | lindhard-scharff, bethe-bloch, equipartition-ls-or, none (see below) |
free_path | "constant" | constant, energy-dependent |
min_cm_angle_deg | none | Required with, and only with, energy-dependent |
weak_collisions | 0 | 0 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_ev | required | The primary stops below this energy |
recoil_cutoff_ev | required | Recoils stop below this; keep it below the smallest E_s |
follow_recoils | true | Full cascades |
primary_surface_binding_ev | 0 | Surface 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):
| Set | Beam | Elements | Fitted energies | Fitted under |
|---|---|---|---|---|
es-sputter-ar-v1 | Ar | Si, Cu, Ag, Au | 196 to 10020 eV, normal incidence | the 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"]
| Key | Default | Meaning |
|---|---|---|
tables | none | Paths 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
| Key | Default | Meaning |
|---|---|---|
layers | required | Indices into the stack, front layer first, the substrate last (the index of physics.target). Each layer may belong to one crystal |
preset | required | Cubic lattice with a cited lattice constant (docs/crystal-orientation.md, docs/data-provenance.md) |
normal | required | Miller indices (hkl) of the surface plane (inward normal), integers, not all zero |
reference | required | Direction [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_deg | 0 | Rotation of the wafer about the surface normal |
thermal.temperature_k | none | Debye-model vibration at this temperature; sets the vibration amplitude only (no thermal expansion) |
thermal.debye_temperature_k | the preset’s cited value | Debye temperature |
p_max_nm, q_max_nm, search_length_nm | engine defaults | Search 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.
| Key | Default | Meaning |
|---|---|---|
fluence_cm2 | required | Total fluence of the run, ions/cm² |
ions_per_step | required | Ions per step; with max_change, the largest and first step |
max_change | absent (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_step | 1 | Adaptive 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_nm | one slab per layer | Split 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_cm3 | none | Total atom density, atoms/cm³; required with "fixed-number-density" |
atomic_volume_nm3.<Sym> | elemental solid volume from the element table | Atomic 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 defaults | e_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 |
erosion | false | Sputter 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]
| Key | Default | Meaning |
|---|---|---|
ions | required | Number of primary histories |
seed | required | Run seed |
threads | all cores | Worker threads; does not affect results and is not echoed |
[tally]
| Key | Default | Meaning |
|---|---|---|
depth_bin_nm | 1 | Bin width of the stopped-primary depth profile |
depth_bins | 1000 | Number of bins; the last also collects everything deeper |
per_ion | true | Write ions.csv |
lateral_bin_nm | 1 | Bin width of the lateral and radial profiles of stopped primaries |
lateral_bins | 100 | Bins 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_ev | beam energy | Upper edge of the escape-energy spectra, eV (from 0) |
escape_energy_bins | 100 | Escape-energy bins |
escape_polar_bins | 30 | Polar-angle bins over [0, 90) degrees from the outward surface normal |
dual_pearson | false | Also fit a dual-Pearson profile to the depth histogram |
voxel | absent | A [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]
| Key | Default | Meaning |
|---|---|---|
energy_ev | required | Kinetic energy of the primaries, eV: the vacuum energy with boundary = "step-barrier", the energy inside the first layer otherwise |
tilt_deg | 0 | Polar angle from the surface normal, [0, 90) |
azimuth_deg | 0 | Azimuth of the incidence plane |
landing_nm | [0, 0] | Landing point [y, z] of the beam centre on the front face, nm |
profile | point | [electron.beam.profile], lateral profile about the landing point (below) |
divergence | none | [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)
| Key | Default | Choices |
|---|---|---|
cutoff_ev | required | An 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_events | 10000000 | Collision and reflection cap per electron |
secondaries | "off" | off; kieft-bosch (Kieft and Bosch 2008) |
instantaneous_momentum, momentum_conservation | true | Options of kieft-bosch; an error with off |
boundary | "transparent" | transparent; step-barrier (inner-potential step with quantum transmission and refraction) |
quantum_transmission, refraction | true | Options of step-barrier; an error with transparent |
[electron.elastic]
| Key | Default | Choices |
|---|---|---|
model | "mott" | mott: Mott cross sections from radial-Dirac partial waves, independent-atom additivity (electron::elastic::table) |
potential | required | thomas-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) |
exchange | false | Furness-McCarthy exchange correction |
correlation_polarization | absent (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]
| Key | Default | Choices |
|---|---|---|
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_ev | 0 | Fermi 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).
| Key | Default | Meaning |
|---|---|---|
min_energy_ev | 10 | Lowest grid energy, eV |
max_energy_ev | the beam energy (plus the largest inner potential with the step barrier) | Highest grid energy, eV |
points_per_decade | 20 | Minimum 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.
| Key | Default | Meaning |
|---|---|---|
optical_elf | required | Path 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 |
band | none | Band 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) |
phonon | none (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) |
polaron | none (off) | Polaron trapping C exp(-γE): { c_per_nm, gamma_per_ev, provenance } |
mermin_fit | none (the default fit) | Options of this material’s Mermin oscillator fit, mermin-melf only: see below |
inner_shells | none (valence only) | Inner-shell ionisation channels of this material, penn-single-pole only: see below |
e_pair | none | Mean 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.
| Key | Default | Meaning |
|---|---|---|
oscillators | 3 | Number of oscillators, 1 to 16 |
weighting | "relative" | relative: residuals divided by ELF + relative_floor · max ELF, so the tails count; uniform: absolute residuals |
relative_floor | 0.01 | The 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):
| Key | Default | Meaning |
|---|---|---|
subshells | required | Subshells 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 |
binding | the embedded EADL2017 table | Path 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)
| Key | Default | Meaning |
|---|---|---|
se_bse_split_ev | 50 | Escaping electrons below it are slow (secondary), at or above it fast (backscattered) |
escape_energy_max_ev | beam energy | Upper edge of the escape-energy spectra (from 0), eV |
escape_energy_bins | 100 | Escape-energy bins |
escape_polar_max_deg | 90 | Upper edge of the polar-angle spectra (from 0, at most 180), degrees from the outward normal |
escape_polar_bins | 18 | Polar-angle bins |
cartesian | none | Deposition grid { x, y, z }, each { lo_nm, hi_nm, bins } (x is depth) |
cylindrical | none | Deposition grid { r, depth } about the beam axis, each { lo_nm, hi_nm, bins } (r.lo_nm >= 0) |
psf | none | Radial profile of the deposited energy in a depth slab and its point-spread-function fits, see below |
exposure | none | Distribution 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.
| Key | Default | Meaning |
|---|---|---|
depth_lo_nm, depth_hi_nm | required | The depth slab, nm (hi > lo) |
r_min_nm | required | Outer edge of the central disc and inner edge of the first log bin, nm (> 0) |
r_max_nm | required | Outer edge of the last bin, nm (> r_min_nm) |
bins | required | Log 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.
| Key | Default | Meaning |
|---|---|---|
depth_bins_per_layer | none (no profile) | Uniform depth bins over the middle of each layer (the whole layer without refinement), at least 1 |
refine_width_nm | none | Width of a refined band next to each face of each layer, nm; limited to half the layer. Needs depth_bins_per_layer |
refine_bins | 10 | Uniform bins in each refined band |
substrate_depth_nm | required with a profile and a substrate | Depth binned in a semi-infinite last layer, nm; deeper energy is outside_ev |
transmission | false | Count 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.
| Key | Default | Meaning |
|---|---|---|
lambda | required | Mean number of incident electrons |
beam | required | { kind = "point" }, { kind = "gaussian", sigma_nm } (per axis) or { kind = "uniform-disc", radius_nm } |
volumes | required | Array 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 |
bins | 200 | Histogram bins on (0, x_max_ev] |
x_max_ev | beam energy | Upper 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_tolerance | 1e-12 | Poisson tail mass dropped from the convolution |
[run] of an electron run: histories (required), seed (required) and
threads (all cores; not echoed, never changes results).