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

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

KeyContent
format{"name": "lindhard-summary", "version": 1}
softwareCrate version and git_describe of the binary
inputThe 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.modelsEvery model in use: role, name, citation
physics.crystalOnly with [[crystal]]: the engine’s metadata of each crystal (see [[crystal]])
physics.stopping_tablesOnly with [stopping]: path, SHA-256, provenance and range of each user table (see [stopping])
physics.engineCutoffs, free path, weak collisions, electronic-loss mode, seed, chunk size as passed to the engine
physics.scattering_tableAngle-table grid and its measured interpolation error
physics.targetEach 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.historiesPrimaries run
results.primariesstopped, 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.recoilsAtoms displaced, sputtered (left through the front face), transmitted
results.yieldsThe above per incident ion
results.energy_budget_ev_per_ionWhere the incident energy went, per ion, and the largest per-history relative bookkeeping residual
results.rangeWhere 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.damagenrt: 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.sputteringyield_per_ion and by_element: target atoms leaving the front face, with count, per_ion and mean_energy_ev for each element
results.escapesbackscatter_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
filesNames of the other files written (null if not written)
runthreads, table_build_s, transport_s, ions_per_s

software.git_describe freshness. The CLI’s build script stamps git describe --always --dirty, and reruns it when a build input that the description reads changes: tracked sources and manifests of lindhard and lindhard-cli (plus the workspace Cargo.toml, Cargo.lock and rust-toolchain.toml), the index, HEAD, the branch ref, packed-refs and refs/heads/refs/tags of the common git directory (so linked worktrees follow shared tags and branches). A non-empty LINDHARD_GIT_DESCRIBE in the build environment is used verbatim (trimmed) and git is not consulted; outside a git checkout the value is unknown. Limitations: -dirty is a flag, not a fingerprint, so two different dirty builds report the same string; untracked files never affect the -dirty flag, while git describe --dirty covers the whole tracked checkout, so an edit to any tracked file (docs, tests, other crates included) marks the stamp -dirty whenever the script next reruns, but a tracked file outside the watched set does not by itself trigger a rerun, so the stamp can stay stale until a watched input or the index changes (for example after git add); and it describes the checkout at build time, not the commit of a binary built elsewhere.

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.

voxel_tally.csv and voxel_tally.vtk

Written only with a [tally.voxel] table (and removed from a reused output directory when the table is absent); summary.json lists them under files. They are the library’s VoxelReport exports (format version 2, see Voxel tallies): per occupied voxel, the beam atoms implanted, the aggregate displacements, the energy deposited by channel, and for every species index of the run (0 is the beam, then the target elements in order of first appearance) vacancies_<i>_<symbol>, interstitials_<i>_<symbol> and replacements_<i>_<symbol>. The header # species = 0:B,1:Si,2:O names the species. Vacancies are placed at the displacement site and interstitials where the displaced atom stopped; the definitions and the cutoff dependence of replacements are those of Displacement damage. Summed over all voxels, the header’s outside sums and all species they equal the results.damage.cascade totals exactly.

damage_export.csv

Written with a [tally.voxel] table, removed from a reused output directory without it, and listed under files in summary.json. The same voxel map as voxel_tally.csv under the versioned exchange header (engine, version, seed, SHA-256 of the input echo, units); see Damage export format.

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}):

KeyContent
softwareAs for an ion run
inputThe 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.modelsEvery 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.transportThe 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.targetEach layer: extent (nm), atom density and the resolved material
physics.materialsEach 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)
resultsThe 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
results.doseOnly with tally.dose: layers, one entry per layer with index, name, thickness_m, areal_mass_kg_m2, energy_ev and dose_gy_m2 (each {mean, std_err} per primary; the dose is per unit incident fluence in Gy m², null with the thickness and areal mass for a semi-infinite layer), and transmission, four entries per interface (forward and back, primary and secondary) with interface, from_layer, to_layer, secondary, count and energy_ev ({mean, std_err} per primary) and energy_fraction (of the total incident energy). The per-bin depth profile and the spectra are in the CSV files, so results.dose.depth and each entry’s spectrum are omitted
results.deposition.exposureOnly with tally.exposure (else null): lambda, beam and per volume the volume, mean_ev, second_moment_ev2 with standard errors, zero_fraction, the histogram sums (histories, zero_count, bin_width_ev, overflow_count, sum_x to sum_x4; the per-bin counts are in electron_exposure_histogram.csv) and compound (mean_ev, variance_ev2 with errors, p_zero, k_max, truncated_mass, distribution with quantiles, below and above, each value a low / central / high bracket). electron_exposure_histogram.csv: volume,bin,lo_ev,hi_ev,count,fraction, bin 0 being X = 0
results.psfOnly with tally.psf: the profile totals (histories, depth_lo_m, depth_hi_m, bins, total_ev and total_std_err_ev of the slab, beyond_ev outside r_max_nm with its error) and per fit model, source, normalization, parameter_names, values, std_errors, reduced_chi2, dof and converged. A fit that fails (for example on an empty profile) is listed in fit_errors with the error text and does not stop the run. With tally.psf the per-bin profile is not repeated in the summary: the results.deposition.psf key is omitted. Without tally.psf that key is present and null. The per-bin profile (bin edges and centre, area, energy and its error, areal density and its error, and the fitted energy of the bin for each fit that succeeded) is in electron_psf_profile.csv; the fitted parameters and the chi2, dof, reduced_chi2, converged and iterations of each fit are in electron_psf_parameters.csv. Residuals are not written; the residual of a bin is energy_ev - model_<model>_ev
filesNames of the CSV files (null if not written)
runthreads, table_build_s, transport_s, histories_per_s

results.damage (electron runs; an ion run’s results.damage is the NRT block above) splits the deposited energy into ionisation (electronic excitation, the source of electron-hole pairs) and lattice heating, and tests the beam against the displacement threshold. beam_energy_ev is the beam energy of the input and residual_channel is "ionisation". Per entry of layers: layer, material, ionisation_ev and lattice_ev (summed over histories) and the same _per_primary; with an e_pair for the layer’s material also e_pair_ev, e_pair_source and pairs_per_primary = ionisation_ev_per_primary / e_pair_ev (the keys are absent without e_pair; the top-level pairs_per_primary, the sum over layers, is present only when every layer that has ionisation energy has an e_pair); and knock_on, one object per element: element, z, t_max_ev = 2E(E + 2mc²)/(Mc²) (relativistic, free atom at rest, head-on, M the atomic weight times the atomic mass unit), e_d_ev, can_displace (T_max >= E_d) and threshold_beam_ev (the beam energy at which T_max = E_d). An element without E_d shows "not set" in those three fields. E_d is the material’s value ([materials] e_d_ev, or the element default where the element table has one). It is a threshold test only: no displacement cross section or dpa. For a step-barrier run E is the vacuum energy of the beam.

Conventions of the split (lindhard::tally::electron, “The ionisation and lattice channels”). Ionisation is the energy left in the solid at inelastic events plus the residual energy of electrons that fell below the cutoff (budget.residual_ev, the cutoff-dependent term; placed here by convention). Lattice is LO-phonon emission plus polaron traps (budget.phonon_emitted_ev + budget.polaron_ev; both are zero without the insulator channels). Ionisation plus lattice equals deposited_ev in the budget, per layer (deposition.per_layer_ionisation_ev, per_layer_lattice_ev) and per cell, to floating-point rounding, and like all deposits includes Fermi-sea energy with secondaries. Elastic nuclear recoil energy is not tallied by the electron engine and is in neither channel. budget.ionisation_ev and budget.lattice_ev are the totals.

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,ionisation_ev,lattice_ev, 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,ionisation_ev,lattice_ev. ionisation_ev + lattice_ev = energy_ev per cell (the two columns are new and last, in eV summed over histories). Energy deposited outside a grid is outside_ev in the summary (with outside_ionisation_ev and outside_lattice_ev). 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.

Charge (results.charge, and with a grid electron_charge_cylindrical.csv / electron_charge_cartesian.csv). Electron number, not energy, counted as integers per incident electron:

  • every electron that ends inside the target counts +1 (stopped) where it ends, whatever the fate except escape: below the stopping threshold, trapped without an interaction, absorbed at the back face, polaron-trapped, or cut off by the collision cap;
  • every liberated electron counts -1 (created, the hole it leaves) at the collision, followed or not. One below the stopping threshold is not followed: it is dropped where it was made and counted +1 there, so the two cancel. The stopping threshold follows electron.transport.cutoff_ev and cutoff_reference, so the cutoff decides how much of the creation and stopping is attributed to followed secondaries and where;
  • an electron leaving through a face carries its charge out (escaped_front, escaped_back, by generation).

primary is the incident electron, secondary any electron the secondary model created. A created_by_* count is attributed to the generation of the electron whose collision made the hole. The balance is exact: incident + created = stopped + escaped, results.charge.imbalance is 0 on every run, and the net electron number left in the target, stopped - created, equals incident - escaped. results.charge.per_layer has the counts and the squared per-history net charge for each layer; with a grid, results.charge.cylindrical and .cartesian keep the grid and the totals inside_stopped, inside_created, and cells.outside for positions outside it.

The CSV columns after the cell edges (same cells and order as the deposition files) are stopped_primary,stopped_secondary,created_by_primary,created_by_secondary,net,net_per_primary,net_per_primary_se. net_per_primary is net per incident electron (an excess of electrons is positive); its standard error uses the net charge each history left in the cell, so correlated stops in one cell are not treated as independent. Summing the cylindrical rows over ir gives the depth profile. Counts are integers, so the files are identical on any thread count. Summaries written before the key existed read back with an empty charge report.

electron_psf_profile.csv (with tally.psf): one row per radial bin (the central disc first), r_lo_m,r_hi_m,r_center_m,area_m2,energy_ev,std_err_ev,density_ev_per_m2,density_std_err_ev_per_m2, then a model_<model>_ev column (the fitted energy of the bin) for each fit that succeeded. electron_psf_parameters.csv: model,parameter,value,std_error, the fitted parameters (lengths in m, energies in eV), then chi2, dof and reduced_chi2 rows per fit, then converged (1 if the iterations stopped on a converged step, 0 if the iteration budget ran out) and iterations (the integer count used). These metadata rows, like the chi2, dof and reduced_chi2 rows, have an empty std_error cell and are not fitted parameters, so a consumer that reads every row as a parameter must skip them. Parameters from a fit with converged 0 need inspection of the fit diagnostics before use.

electron_depth_dose.csv (with tally.dose.depth_bins_per_layer): one row per depth bin, bin,layer,x_lo_nm,x_hi_nm,energy_ev,energy_ev_std_err,ev_per_nm,ev_per_nm_std_err,dose_gy_m2,dose_gy_m2_std_err, per primary; the dose cells are empty in a semi-infinite layer. electron_interface_transmission.csv (with tally.dose.transmission and at least two layers): one row per interface, direction, class (primary or secondary) and spectrum bin, interface,from_layer,to_layer,class,count_per_primary,count_std_err,energy_ev_per_primary,energy_std_err,energy_fraction,bin,e_lo_ev,e_hi_ev,crossings, the totals repeated on each row of a spectrum.

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 inelastic-table energy (the stopping power is λ⁻¹ ⟨W⟩ of the stored loss distribution). The elastic column is empty at the rows the rate refinement adds to the inelastic table of a material with a band (#339), which the elastic table does not have.

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 three 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; <kind>-<sha256>.sha256, the hex SHA-256 of the table file’s bytes; 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 running lindhard executable, 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 model (electron.elastic.model), 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, the material’s band parameters (they set the table’s axis and Fermi energy for a material with a band) and, with mermin-melf, the material’s resolved Mermin fit options (mermin_fit; no table and a table of the defaults give the same key), and the rate refinement of a material with a band (rate_refinement: its window, tolerance and row bound; null without a band).

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’s bytes must then hash to the SHA-256 stored beside it: a table edited or corrupted anywhere, even in a single cross-section or probability value that still parses, is refused with an error naming the file, before it is parsed, and a missing .sha256 file is an error too. The table is then loaded and validated by the library, and its axis and energy grid are checked against the run: an elastic table must have exactly the run’s grid, an inelastic table must contain it in order (it may hold the refinement’s rows as well). A file found under the run’s key that fails any of these checks is an error, never a silent rebuild; remove the entry’s files to rebuild it. 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 first, then its hash, then its key, so a key on disk always has its table and hash beside it.

Concurrent runs. Several runs may share one cache directory at once (threads of one process or separate lindhard processes, such as a seed sweep started in parallel on a cold cache). Each entry has a lock file, <kind>-<sha256>.lock, and a run holds the entry’s lock while it looks the entry up, builds the table and stores it. So for one key:

  • one run builds the table; every other run waits for it, then finds the stored entry (the lookup is made only once the lock is held) and reads it. A run that has to wait for another process prints note: waiting for another process to finish the table cache entry <lock file> on standard error. A run whose build fails stores nothing, and the next waiting run builds in turn;
  • runs that need different tables (another material, kind or key) take different locks and do not wait for each other;
  • there is no timeout: a run waits as long as the holder is alive, since a build can take minutes. A holder that exits or is killed releases its lock (the operating system drops it with the process), so it never blocks the next run. A holder that is stopped but alive (for example suspended with Ctrl-Z) does block it. A waiting run can be interrupted safely;
  • a holder killed while storing can leave temporary files (.<file>.tmp.<pid>.<n>) and a table without its key. The key is the commit marker, so that entry was never stored: the next run to take the lock removes the temporary files and rebuilds it. A stored entry that fails any check above is still an error, never a rebuild.

Temporary files are created under names unique to the process and the write, and removed if the write fails. The lock files are empty and stay in the directory; removing them while no run uses the cache is harmless.

Read-only caches. A cache the run can read but not write (a read-only directory or file system, or one whose lock files belong to another user) is still used for what it holds. When an entry’s lock file cannot be created or opened for writing because permission is denied or the file system is read-only, the run looks the entry up without the lock. An entry that is there is read after every check above, the SHA-256 of the table’s bytes included; the key is the commit marker, so a key that can be read always has its complete table beside it. An entry that is not there is an error naming the directory (... has no entry for this run's <kind> table of <material> and is not writable ...): the run does not build a table it could not store. Make the directory writable, use another directory, or run without --table-cache. If only the directory is read-only and the entry’s lock file exists and is writable by the run, the lock is taken as usual; the outcome is the same (the run checks that it can create a file in the directory before it builds).

The lock is an operating-system advisory lock on the lock file (flock on Unix, LockFileEx on Windows, through Rust’s std::fs::File::lock), and inside one process the run also excludes its own threads without relying on it. This is supported on a local file system. On a network file system (NFS, SMB) whether locks are honoured between machines depends on the server and mount options, and is not guaranteed; share a cache between machines at your own risk, or give each machine its own. If the file system refuses the lock altogether the run fails with an error naming the lock file (use a local directory, or run without --table-cache). Older lindhard builds take no lock, so do not point one at a directory a current run is writing.

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; lindhard-cli/tests/table_cache_concurrency.rs checks the concurrency contract above (threads and processes on one cold entry build once, different entries build at the same time, a killed holder does not block, a read-only cache serves its entries and refuses a miss). Remove the directory to reclaim space; nothing else prunes it.

Exposure: lindhard exposure

Turns a fitted point-spread function into the dose of a list of shots (a pattern exposure). It does not transport anything: the dose is the analytic sum of Gaussians described in the module documentation of lindhard::exposure, so it is fast and exact for point shots. --direct adds an independent transport run of the same shots (see “Direct mode” below).

lindhard exposure --shots shots.csv \
    --psf-parameters out/electron_psf_parameters.csv \
    --blur-m 1e-8 --x0-m -5e-7 --y0-m -5e-7 --dx-m 1e-8 --dy-m 1e-8 \
    --nx 100 --ny 100 --threshold-fraction 0.5 -o exposure/

PSF. Either a parameters file written by an electron run with tally.psf (--psf-parameters; --model double_gaussian|triple_gaussian picks a fit, default the triple fit if present), or typed in: --total-ev, --alpha-m, --beta-m, --eta and, for the triple form, --gamma-m and --nu. The dose is only as good as the fit; check its reduced chi-squared in electron_summary.json first. The film is assumed laterally uniform.

Shot list (--shots): CSV with a header; required columns x_m, y_m, weight (number of primaries, or a relative dwell); optional blur_m (extra beam width of this shot), shape (point, disc, rect), size_m (disc radius, or rectangle width) and size2_m (rectangle height, default size_m). Blank lines and # lines are skipped. Lengths are in metres.

Beam blur (--blur-m, b in exp(-r^2/b^2)) adds in quadrature to every width of the PSF (w^2 -> w^2 + b^2, exact for Gaussians; derivation in the module docs), together with each shot’s blur_m. Shaped spots are a fixed --subsamples x --subsamples midpoint sub-sampling of the spot (default 16); the error is stated in the module docs.

Grid: --x0-m --y0-m --dx-m --dy-m --nx --ny; the dose is evaluated at the grid points. --threads never changes the results (each grid point sums the shots in list order).

Contours. With --threshold (eV/m^2) or --threshold-fraction (of the largest grid dose), iso-dose contours are found by marching squares. A grid point is inside when its dose is at or above the threshold; each contour has the inside on its left, so outer boundaries have positive area and holes negative. A contour that leaves the grid is open and has no area or centroid (enlarge the grid). The area is the polygon area, so it differs from the smooth contour’s by O(h^2) in the grid spacing.

Files written to the output directory:

FileContents
exposure_dose.csvx_m,y_m,dose_ev_per_m2, one row per grid point, rows in grid order (x fastest)
exposure_contours.csvwith a threshold: one row per contour vertex, with the contour’s index, closed, area and centroid
exposure_features.csvwith a threshold: one row per contour: contour,closed,vertices,signed_area_m2,centroid_x_m,centroid_y_m
exposure_summary.jsonshot count, the PSF, blur, grid, largest dose, the large-area dose per unit weight density split into the forward (alpha) part and the long-range (backscatter) part, the contour count, and (with --direct) the direct block (settings and the number of flagged points; null otherwise)
exposure_direct.csvwith --direct: x_m,y_m,dose_ev_per_m2,std_err_ev_per_m2 from transport, rows in grid order
exposure_comparison.csvwith --direct: x_m,y_m,convolution_ev_per_m2,direct_ev_per_m2,combined_std_err_ev_per_m2,deviation_sigmas,flagged

The large-area dose of a uniform exposure of w primaries per m^2 is w E (the PSF integrates to E), of which the forward term gives the share 1 / (1 + eta [+ nu]).

Direct mode: --direct

The convolution above assumes a laterally uniform film. --direct target.toml also evaluates the same shot list by electron transport, as an independent answer at the same grid points. The model, frame and error are in the module documentation of lindhard::exposure (“Direct Monte Carlo exposure”); in short:

  • target.toml is an electron input ([electron], see “Electron runs”). Its layers, cross-section tables and beam energy are used; where the primaries land (and the spot) comes from the shot list, so the input’s own beam profile, landing point and divergence are not used. The target is that input’s laterally uniform layer stack: a laterally varying (voxel) target is available through the library and not yet through TOML.
  • Each shot runs --direct-histories primaries (split over --direct-replicas independent replicas, at least 2) from the shot position with its spot shape; a point shot with blur b is a Gaussian beam exp(-r^2/b^2). A shaped spot with a nonzero blur is refused. Exposure coordinates are offsets from the target’s entry point, x and y being the geometry’s y and z.
  • The energy deposited in the film slab --slab-lo-m (default 0) to --slab-hi-m (required) is tallied per grid cell and divided by the cell area; the point is the cell centre. The result is the mean over the replicas with its standard error (Monte Carlo noise only), reported next to the convolution averaged over the same cell.
  • A point is flagged in exposure_comparison.csv when the doses differ by more than --tolerance-sigmas (default 5) combined standard errors, sqrt(se_direct^2 + se_conv^2), where se_conv is --conv-rel-err times the convolution dose (default 0: the PSF’s own uncertainty is not propagated). Points where both doses are below --floor-fraction (default 0.01) of the largest dose are not judged, as the run cannot resolve them.
  • Seeds follow the run’s thread-count-independent scheme (one SplitMix64 mix per replica and shot), so the files are identical for any --threads.

The PSF flags (--psf-parameters or the typed-in values) are still needed for the comparison.

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), and electron_psf_profile.csv and electron_psf_parameters.csv (without tally.psf), electron_exposure_histogram.csv (without tally.exposure), damage_export.csv (without electron.tally.voxel), and electron_depth_dose.csv and electron_interface_transmission.csv (without the matching tally.dose part). A missing file is not an error; a failed removal is, and names the path. The summary is written last (also for dynamic runs, after both CSVs, so a failed CSV write leaves no new summary) and lists only files that exist.

Once output starts, the run first removes the previous summary of its own mode (summary.json, electron_summary.json or dynamic_summary.json) and only then replaces CSVs. The new summary is written to a freshly created temporary sibling (created exclusively, so an existing file or symlink of the same name is never touched; another name is used) and renamed into place after every other write succeeds. So, after a failed run, the absence of the mode’s summary means the output set is incomplete (it may hold a mix of old and new CSVs); a summary that is present describes a completed run. If the previous summary cannot be removed, the run fails naming its path before any CSV changes. A failure before output starts (simulation or serialization) leaves existing outputs as they were. This is not a transaction: the CSVs are replaced in place, and concurrent runs into one directory are not supported. 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 under files. results.range, results.damage, results.sputtering and results.escapes were 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. physics.beam_divergence (the [beam.divergence] metadata) likewise.
  • New model choices become new values of the existing [physics] keys, or new keys with defaults, so existing inputs keep their meaning.
  • format.version is 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, a band kind, a phonon preset) and a new physics.models entry; existing values keep their meaning and defaults never change.
  • New per-material data (for example a precomputed cross-section cache) becomes a new optional key of [electron.materials.<name>] that names a file. It must be read with the lindhard::electron::data loader of its type, so data without a provenance is refused, and recorded under physics.materials with 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 the lindhard::electron::data loader and echoed under physics.materials with their path, SHA-256 and provenance.
  • A new tally becomes a key under [electron.tally], an object under results and, for profiles, a new CSV file listed under files.
  • Every table keeps deny_unknown_fields, and every default is echoed, so an input written today still means the same thing.