Deep, code-cited reference. See the readable summary for the public version. This page is the full provenance; a Verification addendum (run
wf_7fbcd118-848, 2026-06-08) at the bottom lists every point where the code differs from this text.
Data regenerated 2026-06-15 (relax-cell fix). The elastic numbers shown on the site were rebuilt from a standalone relax-cell campaign (59 potentials;
m3gnet_MP-2021.2.8-EFSwas not re-run and is no longer in the elastic track). The earlier deployed data had been computed without a cell-relaxed reference (onlyrelax_cell=Falsewas active), so it claimed the variable-cell reference described here but did not perform it. The regenerated data uses the variable-cell reference relaxation (relax_cell=True, §2) + fixed-cell deformations, so the data now matches this method. DFT references (dft_k_vrh/dft_g_vrh, DFT tensors) are method-independent and unchanged.
Elastic-Constant Calculation — Full Method, Provenance & Adversarial Verification
Benchmark: mlip-elemental-benchmark, Track C (mlip_elastic)
Question: full elastic stiffness tensor Cᵢⱼ + derived moduli (K, G, E, ν, Aᵁ) + Born
mechanical-stability for each of 834 MP elemental crystals × 59 MLIP potentials, compared
against Materials-Project DFT elastic references where available.
Method family: Materials-Project stress–strain (finite-deformation) via
pymatgen.analysis.elasticity, driven by an ASE calculator through
pyiron_workflow_atomistics.physics.elastic.
Documented & verified: 2026-06-08 · Author: Han
Note on the site methodology. The live site
methodology.mdTrack C text is essentially correct. The cell relaxation described below is a recent code addition (the reference relaxation now relaxes the cell via a filter), so a site that still describes a fixed-cell reference is stale, not wrong — it documented the earlier behaviour. The one genuine standing correction is the iteration cap (§6): the documentedmax_iterations=300is silently overridden to 10 000. Everything else in §1–§5 is the code-true method.
0. Source-of-truth files (every claim here is cited to these)
| Layer | File |
|---|---|
| Entry / DB-row builder | benchmark/elastic_compute.py |
| Algorithm (macro) | pyiron_workflow_atomistics/physics/elastic.py |
| ASE relax/stress engine | …/pyiron_workflow_atomistics/engine/ase.py |
| Calc-input dataclasses | …/pyiron_workflow_atomistics/engine/inputs.py |
| Per-deformation driver | …/pyiron_workflow_atomistics/physics/bulk.py (evaluate_structures) |
| DB worker | benchmark/elastic_worker.py |
| Schema | benchmark/schema_elastic.sql |
| Error metric | benchmark/eval/elastic_metrics.py |
| Rollup | benchmark/eval/build_rollups.py (elastic_rollup) |
1. End-to-end pipeline
structure.atoms_pickle (plain ASE Atoms, the potential-independent MP cell)
│
│ elastic_worker.process_one → elastic_compute.compute_elastic(atoms, calc, fmax=1e-2, max_iterations=300)
▼
calculate_elastic_constants(structure, engine=ASEEngine(calc), relax_initial=True, fmax=1e-2, max_iterations=300)
│
┌────┴─────────────────────────────────────────────────────────────────────┐
│ (1) REFERENCE RELAX — CalcInputMinimize(relax_cell=True) → ExpCellFilter │ full cell+ion
│ ref_structure = relaxed cell; eq_stress = its residual stress (GPa) │ relaxation
├───────────────────────────────────────────────────────────────────────────┤
│ (2) DEFORM — pymatgen DeformedStructureSet → 6 modes × 4 magnitudes = 24 │ Green-Lagrange
│ norm ±0.5 %, ±1 % ; shear ±3 %, ±6 % │ strains
├───────────────────────────────────────────────────────────────────────────┤
│ (3) PER-DEFORMATION — CalcInputMinimize(relax_cell=False) (ion relax at │ fixed-cell
│ fixed cell), then read final stress → 24 stress tensors (GPa) │ ion relax + stress
├───────────────────────────────────────────────────────────────────────────┤
│ (4) FIT — ElasticTensor.from_independent_strains(strains, stresses, │ least-squares
│ eq_stress=eq_stress, vasp=False) → voigt_symmetrized 6×6 C (GPa) │ 6×6 stiffness
├───────────────────────────────────────────────────────────────────────────┤
│ (5) SUMMARY — K/G (Voigt/Reuss/VRH), E, ν, Aᵁ, IEEE tensor, compliance, │ pymatgen
│ stiffness eigenvalues, mechanically_stable (general Born) │ elasticity
└───────────────────────────────────────────────────────────────────────────┘
│
▼ UPSERT into mlip_elastic (UNIQUE(structure_id, potential)), status done/error
1.5 The two relaxation variants (used at different steps)
Every relaxation in the workflow is ASE BFGS with fmax = 1e-2 eV/Å; they differ only in
what is allowed to move. The engine exposes both through CalcInputMinimize.relax_cell
(engine/inputs.py), and the elastic workflow deliberately uses both:
| Variant | relax_cell |
What moves | ASE mechanism | Used for |
|---|---|---|---|---|
| Fixed-cell | False |
ionic positions only; cell frozen | BFGS(atoms) (bare atoms, no filter) — engine/ase.py:208-225 |
the 24 deformed cells (§4): the strained cell must stay strained while ions relax, so the stress reflects that strain |
| Variable-cell (with filter) | True |
full lattice and ions | BFGS(ExpCellFilter(atoms)) — engine/ase.py:178-185 |
the reference cell (§2): find the MLIP's own equilibrium cell before deforming |
(The pwa elastic engine uses ExpCellFilter for the variable-cell variant. This is the
direct analogue of the two stability-ordering tracks — fixed-cell relax vs variable-cell
relax_cell, where that worker uses FrechetCellFilter.)
The variable-cell reference relaxation is the recently-added behaviour; earlier the reference was relaxed fixed-cell, which is what older methodology text describes.
2. Step 1 — reference relaxation (relax_initial=True) — variable-cell variant
What this step does: before any strain is applied, it moves the structure to the MLIP's
own equilibrium cell so the elastic constants are evaluated about a stress-free reference (any
residual stress is captured and subtracted in the fit). elastic.py:272-282.
- Variant: variable-cell (§1.5) —
CalcInputMinimize(relax_cell=True, …)(elastic.py:273-277) →ExpCellFilter(engine/ase.py:178-185): lattice and ions relax together, so the reference reaches the potential's true equilibrium volume and shape. - Optimiser: ASE
BFGS(ASEEngine.optimizer_classdefault,engine/ase.py:413). - Force tolerance:
fmax = 1e-2 eV/Å(passed byelastic_compute.py:13; mapped atengine/ase.py:458). - Step cap: 10 000 optimiser steps — not 300 (see §6.3).
- Output used downstream:
ref_structure(the relaxed cell) andeq_stress— the residual stress of the relaxed reference, converted to GPa (elastic.py:281,_reference_stress_gpa).eq_stressis subtracted in the fit so a non-zero residual reference stress does not bias the elastic constants.
If relax_initial=False, the input is deformed as-is and eq_stress=None. The campaign
always uses relax_initial=True (elastic_compute.py:16).
3. Step 2 — the deformation set (24 cells)
generate_mp_deformations, elastic.py:47-71. Uses
pymatgen.analysis.elasticity.strain.DeformedStructureSet on the relaxed reference cell:
norm_strains = (-0.01, -0.005, 0.005, 0.01)→ normal strains ±0.5 % and ±1 %.shear_strains = (-0.06, -0.03, 0.03, 0.06)→ shear strains ±3 % and ±6 %.- 3 normal + 3 shear independent modes × 4 magnitudes = 24 deformed cells.
- Each deformation's Green-Lagrange strain tensor (3×3) is captured and paired, in order,
with its stress for the fit (
elastic.py:70).
This is the Materials-Project standard deformation grid (the MP elasticity methodology uses the same ±0.5/±1 % normal, ±3/±6 % shear set).
4. Step 3 — per-deformation stress (fixed-cell variant)
What this step does: for each strained cell, it lets the ions settle within that fixed
strain and then reads the resulting stress. The 24 (strain, stress) pairs are the raw data
the stiffness tensor is fitted from. evaluate_structures (physics/bulk.py:63-75) runs each
of the 24 deformed cells through the engine configured with CalcInputMinimize(relax_cell=False, …) (elastic.py:266-269, 290-294):
- Variant: fixed-cell (§1.5) — each deformed cell is ion-relaxed with the cell held at
its strained shape (no filter;
engine/ase.py:208-225), BFGS,fmax=1e-2, step cap 10 000 — then its stress is read. The cell must stay strained (that is the whole point of the deformation), while relaxing the ions inside it removes the internal-coordinate contribution that would otherwise contaminate the fit. This is the standard MP stress–strain procedure. - Stress conversion (
extract_stresses_gpa→voigt_stress_to_gpa,elastic.py:23-27, 74-85): ASE Voigt stress in eV/ų, order[xx,yy,zz,yz,xz,xy], × 160.21766208 (EV_PER_A3_TO_GPA) → 3×3 tensor in GPa. Sign convention is tension-positive Cauchy (_ASE_STRESS_SIGN = +1.0), matching pymatgen. - A deformed cell that yields
final_stress_voigt = None(potential implements no stress) is a hardValueError(elastic.py:79-83) → the structure becomes a Track-Cerrorrow.
5. Steps 4–5 — fit and derived constants
5.1 Tensor fit — fit_elastic_tensor, elastic.py:88-108
ElasticTensor.from_independent_strains(strains, stresses, eq_stress=eq_stress, vasp=False)
→ ElasticTensor(et.voigt_symmetrized)
- Least-squares regression of the 24 stress–strain pairs onto the independent strain modes
(pymatgen).
eq_stress(the fit argument's literal name; = the relaxed reference's residual stress, §2) is subtracted. vasp=Falseselects ASE/pymatgen tension-positive sign convention (not VASP's).- The result is Voigt-symmetrised before any moduli are taken.
5.2 Derived constants — elastic_constants_summary, elastic.py:111-185 (all GPa unless noted)
| Field | Source | Notes |
|---|---|---|
K_Voigt,K_Reuss,K_VRH |
pymatgen et.k_voigt/k_reuss/k_vrh |
bulk modulus, Voigt/Reuss/Hill |
G_Voigt,G_Reuss,G_VRH |
pymatgen et.g_voigt/g_reuss/g_vrh |
shear modulus, Voigt/Reuss/Hill |
youngs_modulus |
9·K_VRH·G_VRH/(3·K_VRH+G_VRH) (elastic.py:167) |
from VRH K,G |
poisson_ratio |
et.homogeneous_poisson |
dimensionless |
universal_anisotropy |
et.universal_anisotropy |
dimensionless (Aᵁ) |
stiffness_eigenvalues |
eigvalsh(C_voigt) |
6 eigenvalues (GPa) |
elastic_tensor_ieee |
et.convert_to_ieee(pmg) |
falls back to unrotated tensor if IEEE rotation undefined (elastic.py:150-155) |
compliance_tensor_voigt |
et.compliance_tensor.voigt |
units 1/GPa |
mechanically_stable |
all(eigvalsh(C_voigt) > 1e-8 GPa) (elastic.py:159-161) |
general Born (positive-definite stiffness); see §6.3 |
5.3 The DB row — elastic_compute.py:25-45, elastic_worker.py
elastic_tensor= the IEEE-rotated Voigt 6×6 (d["elastic_tensor_ieee"]), pickled bytea.compliance_tensor,stiffness_eigenvaluespickled bytea; scalar moduli as float columns.relaxed_energy_per_atom,relaxed_volumefrom the reference-relaxed cell.n_relax_steps = None(not recorded;elastic_compute.py:43).- Key
UNIQUE(structure_id, potential); UPSERT;status ∈ {done, error}; on error the full traceback is stored inerror_msgand all physics columns (includingmechanically_stable) are NULL (elastic_worker.py:54-62, 91-93).
6. The one genuine standing correction + provenance notes
The site methodology and the code agree on the science. Every constant, formula, and step in
§1–§5 was independently re-checked by 8 adversarial agents that read the source and tried to
refute each claim (workflow verify-elastic-methodology, run wf_daca20a2-57b): all
confirmed at high confidence. What follows is the one item that should actually change in
the code/docs, plus provenance notes that explain why older text reads differently.
6.1 — ⚠️ Iteration cap: documented max_iterations=300 is silently overridden to 10 000
This is the real bug. elastic_compute.py:13 passes max_iterations=300, but the ASE dispatch
picks self.max_steps if self.max_steps else mi.max_iterations (engine/ase.py:459), and
ASEEngine.max_steps defaults to 10 000 (engine/ase.py:416) — which is truthy, so the
300 is never used. Effective optimiser cap = 10 000 steps for the reference and every
deformation. Usually benign (relaxations hit fmax long before either cap), but the intended
300 is dead. Action: either set ASEEngine(max_steps=300) to honour the intent, or update
docs to state the real cap is 10 000.
6.2 — Cell relaxation of the reference is a recent addition (older text is stale, not wrong)
The reference relaxation now uses the variable-cell variant (relax_cell=True → ExpCellFilter,
§2). This relaxes lattice + ions; earlier the reference was relaxed fixed-cell. Methodology text
that still says the reference is "ion relax at fixed cell" simply predates the change — it was
correct for the earlier code. The two variants (§1.5) are now both in play: variable-cell for the
reference, fixed-cell for the deformations. Update any stale text to reflect the variable-cell
reference; this is a doc refresh, not a correction of an error.
6.3 — Minor notes (no action needed)
fmaxprovenance: the macro default isfmax=1e-3(elastic.py:212) butcompute_elasticpassesfmax=1e-2(elastic_compute.py:13), so the campaign ran at 1e-2 eV/Å. Calling the macro directly would use the stricter 1e-3 — a reproducibility trap worth knowing.- Born criterion:
elastic_constants_summary's docstring says "cubic + general", but the code computes only the general criterion (all 6 Voigt-stiffness eigenvalues> 1e-8 GPa); there is no cubic branch (elastic.py:159-161). Behaviour is fine; the docstring overstates.
Verified correct (no change)
- 24-cell grid, ±0.5/±1 % normal, ±3/±6 % shear, Green-Lagrange strains ✓ (
elastic.py:50-51,210-211). - Stress→GPa factor 160.21766208, tension-positive Cauchy ✓ (
elastic.py:17-27). eq_stress= relaxed reference's residual stress, subtracted in fit,vasp=False✓ (elastic.py:104-107,281).youngs = 9KG/(3K+G)from VRH K,G ✓; compliance in 1/GPa ✓.mechanically_stablea real bool forstatus='done'rows; NULL only onerrorrows ✓.- IEEE-rotated tensor stored, with unrotated fallback ✓.
7. Metrics & rollup (how Cᵢⱼ becomes a leaderboard number)
- Per-structure error —
elastic_metrics.elastic_error(elastic_metrics.py):k_pct_err = 100·(K_MLIP − K_DFT)/K_DFT, likewiseg_pct_err; None if the DFT value is missing or zero (guards a divide-by-zero on near-zero DFT moduli). - Rollup —
build_rollups.elastic_rollupjoinsmlip_elastic(status='done') ⨝structureondft_k_vrh/dft_g_vrh, emitting one row per(potential, mp_id)withk_pct_err,g_pct_err,mechanically_stable. Only the ~308–310 structures with an MP DFT elastic reference get a scored error; the rest carry an MLIP prediction + Born-stability label with no DFT comparand. - Headline aggregation (site):
median|k_pct_err|andmedian|g_pct_err|(median of absolute %, because near-zero DFT moduli produce ±10⁶ %-tails that destroy a mean) andfrac_mech_stable(MLIP-only, no DFT comparand). Lower %err better; higher frac_stable better.
8. Reproduce one structure
PY=python # has pymatgen + pyiron_workflow_atomistics
# (model-specific potentials need their envs_elemental/<pot> env instead)
from benchmark.db import connect, from_bytea
from benchmark.elastic_compute import compute_elastic
from benchmark.worker import _resolve_factory
calc,_ = _resolve_factory("mace_MP-0")()
with connect() as c, c.cursor() as cur:
cur.execute("SELECT atoms_pickle FROM structure WHERE element='Cu' AND is_ground_state LIMIT 1")
atoms = from_bytea(cur.fetchone()[0])
res = compute_elastic(atoms, calc) # fmax=1e-2, full-cell ref relax, 24 deformations
print(res["k_vrh"], res["g_vrh"], res["mechanically_stable"])
Inputs that fully determine a row: the MP cell, the potential's calculator, fmax=1e-2, the
fixed 24-cell deformation grid, and the pymatgen fit. Everything else is bookkeeping.
Verification addendum (run wf_7fbcd118-848, 2026-06-08)
All 30 claims confirmed against the code. Standing items already noted in §6 of this doc and carried
to the summary: effective optimiser cap is 10000 steps, not 300 (engine/ase.py:416,459 shadows
max_iterations); campaign fmax=1e-2 (not the macro's 1e-3); Born stability is the general
positive-definite criterion only (eigenvalues > 1e-8 GPa), not "cubic + general" (the docstring
overstates). Note: on an errored re-run the UPSERT can overwrite a prior done row's physics with
NULLs (elastic_worker.py:41-50).