Output files
lindhard run input.toml --out dir/ writes summary.json and up to five
CSV profiles into dir/. The first run shows excerpts of
each from a real run. The models behind the damage figures are on
Displacement damage.
This page is the output section of
docs/cli.md,
included here so there is one copy.
summary.json
| Key | Content |
|---|---|
format | {"name": "lindhard-summary", "version": 1} |
software | Crate version and git_describe of the binary |
input | The input as run: defaults filled in, CLI overrides applied, run.threads removed. Deserializes back to the same lindhard::input::Input, so a run can be reproduced from its own header |
physics.models | Every model in use: role, name, citation |
physics.crystal | Only with [[crystal]]: the engine’s metadata of each crystal (see [[crystal]]) |
physics.stopping_tables | Only with [stopping]: path, SHA-256, provenance and range of each user table (see [stopping]) |
physics.engine | Cutoffs, free path, weak collisions, electronic-loss mode, seed, chunk size as passed to the engine |
physics.scattering_table | Angle-table grid and its measured interpolation error |
physics.target | Each layer: extent (nm; back_nm is null for a substrate), atom density, and the fully resolved material (every E_d, E_b, E_s) |
results.histories | Primaries run |
results.primaries | stopped, backscattered, transmitted; mean and standard deviation of the stopped-primary depth, nm (stopped_depth_mean_nm, stopped_depth_std_nm; the same numbers as results.range.depth.mean_nm and std_dev_nm, the projected range and straggle, kept under their original keys) |
results.recoils | Atoms displaced, sputtered (left through the front face), transmitted |
results.yields | The above per incident ion |
results.energy_budget_ev_per_ion | Where the incident energy went, per ion, and the largest per-history relative bookkeeping residual |
results.range | Where the beam particles came to rest, lengths in nm. depth: n, mean_nm (projected range Rp), std_dev_nm (straggle), skewness, kurtosis (beta, Gaussian 3) and the standard error of each (null below two stopped primaries). pearson_iv: the Pearson IV density with those moments (m, nu, a_nm, lambda_nm), or null with the reason in pearson_iv_error. dual_pearson (or dual_pearson_error): only with tally.dual_pearson = true; head fraction, head and tail components, chi-square of the fit and of the single Pearson IV. lateral_y, lateral_z, radial: moments of the lateral positions. layers: stopped and depth moments by the layer where the particle stopped |
results.damage | nrt: Norgett-Robinson-Torrens and Kinchin-Pease displacement estimates from the primary knock-on atom damage energies (pka_count, pka_energy_ev, damage_energy_ev, nrt_displacements, kinchin_pease_displacements). cascade: defects counted event by event in the simulated cascades (displacements, replacements, vacancies, interstitials). The two are different quantities and are reported separately; see lindhard::ion::damage for the conventions, including the approximation used for compounds. Totals over all ions, with per_ion and a layers breakdown |
results.sputtering | yield_per_ion and by_element: target atoms leaving the front face, with count, per_ion and mean_energy_ev for each element |
results.escapes | backscatter_coefficient, transmission_coefficient, energy_reflection_coefficient (energy carried out of the front face by the beam particles, as a fraction of the incident energy), and species: every species (beam first) through the front and back face, with count, per_ion and mean_energy_ev |
files | Names of the other files written (null if not written) |
run | threads, table_build_s, transport_s, ions_per_s |
depth_profile.csv
depth_lo_nm,depth_hi_nm,stopped_primaries,fraction_per_nm: stopped primaries
per depth bin, and that count per incident ion per nm. The last row’s upper
edge is inf (overflow) and its density is empty.
lateral_profile.csv
quantity,lo_nm,hi_nm,count,per_ion_per_nm: stopped primaries by y, z and
radial distance (quantity is y, z or radial), for the grid set by
lateral_bin_nm and lateral_bins. Each quantity ends with an underflow
row (lo_nm = -inf) and an overflow row (hi_nm = inf) with empty density.
damage_profile.csv
depth_lo_nm,depth_hi_nm,vacancies,interstitials,replacements, then the same
three per incident ion per nm: the cascade defect counts by depth, on the
depth grid. vacancies is displacements minus replacements. The last row,
with depth_hi_nm = inf, holds everything deeper than the grid (empty
densities). The totals equal results.damage.cascade.
escape_spectra.csv
species_z,symbol,beam,face,spectrum,lo,hi,count,per_ion_per_unit: the energy
(spectrum = energy_ev, lo/hi in eV) and polar-angle (polar_deg, degrees
from the outward normal) spectra of every species leaving each face (front
or back). Each spectrum ends with -inf and inf rows for entries outside
the grid, with empty densities. The density is per incident ion per eV or per
degree.
ions.csv
index,fate,x_nm,y_nm,z_nm,energy_ev,dir_x,dir_y,dir_z,layer: the final
state of every primary, by history index. fate is stopped,
backscattered or transmitted; for escaped primaries the position is on the
face and the energy and direction are outside the target. x is depth.
Dynamic runs ([dynamic])
A run with a [dynamic] table writes dynamic_summary.json,
dynamic_steps.csv and dynamic_composition.csv instead of the static files.
dynamic_summary.json (format lindhard-dynamic-summary): the echoed
input (with dynamic), physics-model list models, the species,
totals (ions, steps, rejected attempts, fluence, slab count and thickness
before and after, yields per ion), files and the timing run object.
dynamic_steps.csv: one row per accepted step; step 0 is the initial target.
step, first_index (global index of the step’s first ion), ions,
ions_done, fluence_cm2 (delivered so far), attempts (more than 1 if the
adaptive bound rejected the step), max_change, clamped, removed_slabs
(slabs that emptied), n_slabs, surface_nm (cumulative surface recession
in nm; always 0 with erosion = false), thickness_nm (total of the finite
slabs), then cumulative counts since the start: cum_backscattered,
cum_transmitted, cum_stopped_in_target, cum_stopped_in_substrate, cum_sputtered, sputter_yield (atoms per ion so
far), cum_recoils_transmitted and cum_sputtered_<Sym> per element.
dynamic_composition.csv: the slab profile after every step (step 0 is the
initial target), one row per step and slab: step, slab (0 is the front),
front_nm, back_nm, thickness_nm, then per element
atoms_per_cm2_<Sym> and fraction_<Sym> (atom fraction). Slabs that
emptied are gone from later steps.
Electron runs: electron_summary.json and electron_*.csv
An electron run writes these instead of the ion files.
electron_summary.json (format {"name": "lindhard-electron-summary", "version": 1}):
| Key | Content |
|---|---|
software | As for an ion run |
input | The input as run: defaults filled in (including tables.max_energy_ev, tally.escape_energy_max_ev and the secondary and barrier options), CLI overrides applied, run.threads removed. Deserializes to lindhard::input::electron::ElectronInput |
physics.models | Every model in use: role, name, citation (transport loop, elastic model and potential, corrections, inelastic model, secondaries, barrier, phonon and polaron channels, SE/BSE split) |
physics.transport | The engine’s RunMetadata: cutoff and its reference, escape rule, event cap, secondary and boundary models, seed, histories, chunk size, the primary, and per layer its extent (m), the model and provenance strings of both tables, the band parameters, phonon and polaron channels with their provenance |
physics.target | Each layer: extent (nm), atom density and the resolved material |
physics.materials | Each material: the ELF file (path, resolved_path, sha256, its material and provenance, energy range and point count), band, phonon, polaron, and for elastic_table and inelastic_table their model, material, provenance, cache format_version, energy range and grid sizes, source ("built" or "cache") and cache (null without --table-cache, else the table file’s path, sha256 and key_sha256) |
results | The ElectronReport (lindhard::tally::ElectronReport), lengths in m and energies in eV, summed over all histories unless named per primary: histories, metadata (split and its source, cutoff, stopping thresholds, tally settings), fates of the primaries, event_caps (see below), budget (the energy balance and its relative_imbalance; deposits are measured from the band bottom, so with secondaries in a layer with a Fermi energy deposited_ev includes the Fermi-sea energy of liberated conduction electrons and can exceed the energy imparted, which is incident_ev - escaped_ev = deposited_ev + trapped_ev + barrier_ev - fermi_sea_ev - phonon_absorbed_ev), yields (backscatter_eta, secondary_delta, total_sigma, transmitted), front and back (counts, energies, slow and fast classes), deposition (per_layer_ev; for each grid its binning, inside_ev and outside_ev), generation_volume, stopping_points (all electrons that fell below the stopping threshold, and under primaries the primaries alone: the penetration depth of stopped primaries), table_coverage (see below). The histograms and grid cells are in the CSV files, not here |
files | Names of the CSV files (null if not written) |
run | threads, table_build_s, transport_s, histories_per_s |
results.table_coverage is a numerical diagnostic: one entry per layer
(layer), and for its elastic and inelastic table the grid bounds
energy_min_ev and energy_max_ev and the counts below (E < energy_min_ev: the first row’s rate and distribution were used), within
(both bounds included: interpolated, or a row read exactly) and above (E > energy_max_ev: the last row’s were used), over primaries and secondaries.
They count rate evaluations, not collisions: the transport evaluates both
tables of the electron’s layer once before every free flight, including
flights cut short at a layer face, the flight after a face reflection and
flights with zero total rate, so the totals exceed the number of elastic and
inelastic events, and a layer’s elastic and inelastic totals are equal. They
are not fractions of path length or of deposited energy either. Nonzero
below or above counts say that part of the transport used the constant
continuation of a table beyond its grid ([electron.tables]); they do not say
how much that changed the result. Summaries written before the key existed
lack it.
results.event_caps is another numerical diagnostic, of the collision cap
([electron.transport] max_events): secondary_tracks is the number of
secondary electrons the cap cut off (one per capped track) and
affected_histories the number of primary histories in which the primary or
at least one secondary was cut off (each history counted once, however many
of its electrons were capped). Both are counts, not energies; the energy the
capped electrons still carried is budget.event_cap_ev. The primaries’ own
caps stay in fates.event_capped, so fates.event_capped = 0 alone does not
show that the secondary cascades ran to completion: check
event_caps.affected_histories. A capped track is a truncated one, not a
physical fate. Summaries written before the key existed read back with both
counts zero.
electron_escape_spectra.csv: face,spectrum,class,lo,hi,count,per_primary_per_unit.
For each face (front, back): the energy spectrum of all escaping electrons
(spectrum = energy_ev, class = all, eV) and the polar-angle spectrum of
each class (polar_deg, slow or fast, degrees from the outward normal).
Each spectrum ends with -inf and inf rows for entries outside the grid,
with empty densities. The density is per primary per eV or per degree.
electron_deposition_cylindrical.csv (with tally.cylindrical):
ir,ix,r_lo_nm,r_hi_nm,depth_lo_nm,depth_hi_nm,energy_ev,ev_per_primary_per_nm3,
one row per cell. electron_deposition_cartesian.csv (with
tally.cartesian): ix,iy,iz,x_lo_nm,x_hi_nm,y_lo_nm,y_hi_nm,z_lo_nm,z_hi_nm,energy_ev,ev_per_primary_per_nm3.
Energy deposited outside a grid is outside_ev in the summary. Like budget.deposited_ev, the cell energies are
measured from the band bottom and, with secondaries, include Fermi-sea energy
the beam did not supply.
electron_tables.csv: material,energy_ev,elastic_inverse_mfp_per_nm,inelastic_inverse_mfp_per_nm,inelastic_mean_loss_ev,inelastic_stopping_ev_per_nm,
the tables the run used, per material and grid energy (the stopping power is
λ⁻¹ ⟨W⟩ of the stored loss distribution).
Cross-section table cache (--table-cache)
Building the elastic and inelastic tables is the slow part of a short
electron run (tens of seconds with penn-single-pole, far longer with
penn-full; see docs/validation.md, “Electron oracles”). With
--table-cache DIR, lindhard run looks each table up in DIR (created if
missing) and builds and stores only the ones it does not find, so a series of
runs that differ only in seed, history count, tallies or transport settings
builds its tables once.
Each entry is two files, named by the SHA-256 of a key document:
<kind>-<sha256>.toml, the table in the versioned cache form of
lindhard::electron::data::CrossSectionTable, read back with that type’s
loader, and <kind>-<sha256>.key.json, the key itself. The key spells out
every input the table depends on:
- the key schema version and the table cache
format_version; - the build: crate version,
git describe --always --dirty, and the SHA-256 of the runninglindhardexecutable, so any rebuild that changes the code (an uncommitted edit included) misses, and a rebuilt binary never reuses an older binary’s table; - the table kind and the exact energy grid (
electron.tables, after the defaults are filled in); - the material’s name, composition and density;
- elastic: the potential, the exchange and correlation-polarization corrections with all their inputs, the starting probability grid and the refinement tolerance;
- inelastic: the model (
electron.inelastic.model), its Fermi energy, the SHA-256 and provenance of the optical ELF file, and the material’s band parameters.
Every f64 is written in shortest round-trip form, so a change in the last
bit of any number is a different key. A lookup must find the stored key
equal, byte for byte, to the run’s own (a mismatch under the same hash means
the file was edited, and is an error naming the differing field); the table
file is then loaded and validated by the library, and its axis and energy grid
are checked against the run. A file found under the run’s key that fails any
of these checks is an error, never a silent rebuild. A table of another cache
format_version can never be found, since the version is part of the key.
Writes go to a temporary file renamed into place, the table before its key.
A cached table is the built one bit for bit (the TOML cache form round-trips
every f64), so outputs do not depend on whether the tables were built or
read, nor on the thread count. Only physics.materials.*_table.source and
.cache, and the timings in run, differ. lindhard-cli/tests/examples.rs
checks a building run, a storing run and reading runs on 1 and 8 threads
against each other. Remove the directory to reclaim space; nothing else
prunes it.
Reusing an output directory
--out may name an existing directory; the run overwrites the files it
writes and creates the directory if needed. The CLI also owns the reserved
optional file names of the run’s mode. After a successful run, an optional
file the run did not produce is removed if present: ions.csv (without
tally.per_ion), and electron_deposition_cartesian.csv or
electron_deposition_cylindrical.csv (without the matching deposition grid).
A missing file is not an error; a failed removal is, and names the path. The
summary is written last and lists only files that exist. Other files in the
directory are never touched, and no cleanup happens between ion, electron and
dynamic runs. Do not keep your own data under a reserved name.
Reproducibility
Everything in summary.json except the trailing run object, and every CSV
file, is a function of the input and the binary only: byte-identical for the
same input and seed at any thread count (for a dynamic run: the same three
files, with run the only thread-dependent part of dynamic_summary.json).
For an electron run the same holds for electron_summary.json (apart from
run) and every electron_*.csv: the tables are built bit-identically on any
thread count, and histories run in chunks of a fixed size (16, recorded as
physics.transport.chunk_size) merged in chunk order.
lindhard-cli/tests/examples.rs checks this on 1 and 4 threads (1, 2 and 8
for the dynamic example; 1 and 4 for the electron example, and with
--table-cache 1 and 8). Floats are
written in shortest round-trip form.
Compatibility and extension
Consumers must ignore keys they do not know. New results are added as new keys, never by changing existing ones:
- New tallies become new objects under
results, with their settings as new keys under[tally]and their profiles as new CSV files listed underfiles.results.range,results.damage,results.sputteringandresults.escapeswere added this way, without a version bump. physics.crystal(the[[crystal]]metadata) was added this way, without a version bump; an amorphous run does not carry it.- New model choices become new values of the existing
[physics]keys, or new keys with defaults, so existing inputs keep their meaning. format.versionis bumped only when an existing key is removed or changes meaning.
The electron schema and its output follow the same rules, with
lindhard-electron-summary counting its versions separately:
- A new library model becomes a new value of an existing key
(
electron.inelastic.model,electron.elastic.potential,electron.transport.secondaries,electron.transport.boundary, abandkind, aphononpreset) and a newphysics.modelsentry; existing values keep their meaning and defaults never change. - New per-material data (for example a subshell binding-energy table, once
inner-shell channels reach the transport loop, or a precomputed
cross-section cache) becomes a new optional key of
[electron.materials.<name>]that names a file. It must be read with thelindhard::electron::dataloader of its type, so data without a provenance is refused, and recorded underphysics.materialswith its path, SHA-256 and provenance. - The cross-section table cache is the one exception to the rule above, by
decision (#168): it is a command-line flag (
--table-cache DIR), not an input key, because it never changes a result (as with--threads, the input and its echo stay the same whether or not tables are reused), and a content-addressed directory keyed on every input of the build cannot name a stale file the way a hand-written path can. Its tables are still read with thelindhard::electron::dataloader and echoed underphysics.materialswith their path, SHA-256 and provenance. - A new tally becomes a key under
[electron.tally], an object underresultsand, for profiles, a new CSV file listed underfiles. - Every table keeps
deny_unknown_fields, and every default is echoed, so an input written today still means the same thing.