Deep, code-cited reference. See the readable summary for the public version. This page is the full provenance: exactly what the GB kernel computes per (potential × elemental cubic crystal), which file and line produces each number, how the streaming DB sinks + walltime/CUDA/atom-count caps keep a 30-relaxation study durable, how the coverage clamp is defined, and how the site turns model-vs-model agreement into the "Consensus dev" column.
No DFT reference. This track has no accuracy metric. The leaderboard default-sorts by coverage (a clamp metric, §8); the only cross-model signal is consensus — the median
gb_energyover the 15 curated KEEP potentials (§9), which measures "most typical", not "most correct" (11 of 15 KEEP are GRACE-family, so it leans GRACE) and is coverage-confounded. Refer to /grain-boundary for the numbers; this page describes only the method.
Grain-Boundary Energetics — Full Method & Provenance
Benchmark: mlip-elemental-benchmark, Track GB (mlip_gb / mlip_gb_bulk) ·
github.com/ligerzero-ai/mlip-elemental-benchmark
Question: per (potential × elemental cubic crystal): relax bulk → equilibrium a0 (EOS) →
enumerate the deterministic CSL grain-boundary set at a0 → pure_gb_study each boundary →
gb_energy (J/m²), excess volume, GB-plane surface energy, rigid & relaxed work of separation, full
decohesion profile. One mlip_gb row per boundary; one mlip_gb_bulk row per (potential, structure)
carrying n_gb_total.
Method family: GRACE pure_gb_evaluation (calculate_single_gb_sequential) ported to a generic
ASE engine via pyiron_workflow_atomistics.physics.grain_boundary.pure_gb_study.
Curated scope: 15 KEEP potentials × 36 industrial structures (deliverable); 51 potentials have some
GB data.
Provenance caveat.
physics/*line numbers are from the source checkout at/dais/u/hmai/2026_06_12_polycrystal/_src/pyiron_workflow_atomistics/…(the package the campaign envs install) and may drift a few lines against another installed wheel; function names are stable.benchmark/*citations are exact against/u/hmai/surface_campaign_port/mlip-elemental-benchmark.
0. Source-of-truth files (every claim here is cited to these)
| Layer | File |
|---|---|
| GB kernel (EOS → build → study; sinks + caps) | benchmark/gb_compute.py |
| DB worker (claim queue; streaming UPSERTs; recovery hooks) | benchmark/gb_worker.py |
| Coverage clamp metric | benchmark/gb_status.py |
| Parquet export (clamp + consensus scaffolding) | benchmark/export_gb_to_parquet.py |
Schema (mlip_gb / mlip_gb_bulk / mlip_gb_claim_queue / mlip_gb_eligible) |
benchmark/schema_gb.sql |
a0/EOS macro |
pyiron_workflow_atomistics/physics/bulk.py (optimise_cubic_lattice_parameter) |
| CSL enumeration + construction | …/physics/_grain_boundary_code/searcher.py |
| The study macro | …/physics/grain_boundary.py (pure_gb_study) |
| Consensus / site CSVs | tools/regen_gb_from_export.py (site repo) |
1. End-to-end pipeline
structure.atoms_pickle (ASE Atoms; potential-independent MP cell) + element, basis (from mlip_gb_eligible)
│
│ gb_worker.process_one → gb_compute.compute_gb(element, basis, atoms, calc, sinks…)
▼
┌──────────────────────────────────────────────────────────────────────────────────────────┐
│ (A) BULK EOS relax_bulk → optimise_cubic_lattice_parameter → a0, E0/atom, V0/atom, B │ CPU
│ (or skipped via prebuilt / bulk_override cache) → mlip_gb_bulk row │ (bulk_sink)
├──────────────────────────────────────────────────────────────────────────────────────────┤
│ (B) CSL BUILD build_gb_set → get_gb_code_df_with_structures at a0 → deterministic GB set │ CPU
│ (axes × sigma; MAX_ATOMS pre-filter; MAX_BUILT_ATOMS post-build filter) │
├──────────────────────────────────────────────────────────────────────────────────────────┤
│ (C) PER GB study_one_gb → pure_gb_study (≈30 relaxations): │ GPU
│ grain-length opt (stage1 3-ext + stage2 5-ext, interp min) → gb_energy, │ (gb_sink,
│ excess volume; +20 Å vacuum relax → surface_energy; segregation supercell; │ one commit
│ Voronoi GB-plane ID; rigid+relaxed cleavage scan → Wsep + decohesion profile │ per GB)
└──────────────────────────────────────────────────────────────────────────────────────────┘
│ streamed per-GB into mlip_gb (UPSERT, UNIQUE(structure_id, potential, gb_id))
▼
gb_status.py clamp coverage · export_gb_to_parquet.py · tools/regen_gb_from_export.py (consensus)
Every relaxation in the kernel is ASE with fmax = 0.01 eV/Å and a 500-step cap
(FMAX_RELAX, MAX_STEPS = 0.01, 500, gb_compute.py:150), wired through
make_engines (gb_compute.py:171-184): a minimize engine
(CalcInputMinimize(force_convergence_tolerance=FMAX_RELAX, relax_cell=False)) and a static engine.
2. Step A — bulk EOS → equilibrium a0
relax_bulk (gb_compute.py:186-209) builds a seed conventional cubic cell
(ase.build.bulk(element, basis, a=a0_guess, cubic=True), gb_compute.py:196) and runs the
Birch–Murnaghan EOS macro optimise_cubic_lattice_parameter
(physics/bulk.py:103-145), with strain_range=(-0.02, 0.02), num_points=11
(EOS_STRAIN_RANGE, EOS_NUM_POINTS, gb_compute.py:149).
a0= the cube root of the EOS-fitted equilibrium volume:get_cubic_equil_lat_paramreturnsv0 ** (1/3)(physics/bulk.py:148-151), fed byeos_volume_scan(physics/bulk.py:154+,eos_type="birchmurnaghan").E0/atom,V0/atom —equil_energy_per_atom,equil_volume_per_atom(physics/bulk.py:129-134);Bis the fitted bulk modulus in GPa (schema_gb.sql:44notes the eV→GPa conversion is already inside the macro).a0_guessis derived from the conventional cell volume, notmean(cell.lengths()):a0_guess = (BASIS_NATOMS[basis] · V/atom) ** (1/3)(gb_compute.py:417-419), because the input cells instructureare often primitive (fcc Cu = 1-atom, edge ≠ conventionala).- Convergence is derived (the macro has no converged flag): finite
a0/E0,V0 > 0, and drift|a0 − a0_guess|/a0_guess < 0.25(gb_compute.py:206-207).
EOS-skip caches (optional). If a prebuilt dict or bulk_override lookup is supplied, the EOS is
skipped and a0/V0/E0 are taken from the precomputed relax-export cell (bit-identical to the in-worker
EOS, immune to its failure modes) — compute_gb branches at gb_compute.py:385-428; the worker loads
these via _load_prebuilt / _bulk_override (gb_worker.py:113-142).
Only the four basis strings in BASIS_OK = {bcc, fcc, sc, diamond} (gb_compute.py:151) are
supported; anything else returns status='unsupported' with no rows (gb_compute.py:381-383).
3. Step B — deterministic CSL enumeration at a0
build_gb_set (gb_compute.py:211-240) calls get_gb_code_df_with_structures
(searcher.py:485+) with the potential's own a0 and V0, so the boundary geometry is built at the
model's equilibrium lattice, deduplicate=True:
- Axes — first-pass campaign uses the anchored subset
AXES_ANCHOR = [[1,0,0],[1,1,0],[1,1,1]](gb_compute.py:120-121), the GRACEpureGBanchor set; the full 13-axis setAXES_FULLis env-overridable viaGB_AXES(a JSON list) with no code edit.SIGMA_LIMITdefaults to 30 (gb_compute.py:122). - Enumeration —
searcher._get_gbcode_df(searcher.py:192-226) walkssigmaup tosigma_limitpicking the first(theta, m, n)per Σ (searcher.py:202-210), then filters the minimal CSL bicrystal byn_atoms <= max_atoms(searcher.py:225).gb_idencodes axis+Σ+m+n+GB1+GB2 (gb_id_of,gb_compute.py:166-169), so trimmed-scope and later broad-scope rows coexist without collision. - Construction —
_get_gbcode_df_with_structures(searcher.py:229-309) builds each boundary in parallel, applyingmin_inplane_gb_length=10andreq_length_grain=15(REQ_LENGTH_GRAIN, GRAIN_LENGTH_AXIS = 15, 0,gb_compute.py:145) after themax_atomsfilter, so the built cell can far exceedmax_atoms. - Two distinct size caps (
gb_compute.py:124-138):GB_MAX_ATOMS(default 10000) caps the pre-expansion minimal cell inside gb_code;GB_MAX_BUILT_ATOMScapslen(built structure)— the post-expansion cell actually relaxed — as a post-build filter (gb_compute.py:235-238, and the cache-path re-applicationgb_compute.py:399-403). Cells over the cap are dropped and counted asn_gb_skipped_max_atoms;n_gb_totalis the count actually kept.
n_gb_total (written to mlip_gb_bulk, §7) is the denominator of the coverage clamp (§8): the
canonical GB count for that (potential, structure) at the cap in force when it was built.
4. Step C — the pure_gb_study macro (≈30 relaxations per boundary)
study_one_gb (gb_compute.py:286-360) runs pure_gb_study
(physics/grain_boundary.py:1273-1433) for one boundary, passing the study params fixed in the kernel
(gb_compute.py:146-148): EXT_STAGE1=[-0.2, 0.3, 0.8], EXT_STAGE2=[-0.05,-0.025,0,0.025,0.05],
GB_NORMAL_AXIS="c", VACUUM_LENGTH=20.0. The macro chains, in order:
4.1 Grain-length optimisation → gb_energy, excess volume
full_gb_length_optimization (grain_boundary.py:246-333) is a two-stage scan along the boundary
normal, each stage gb_length_optimiser (grain_boundary.py:157-210):
- Extend the cell along axis
cby each extension,scale_atoms=True(get_extended_struct_list,grain_boundary.py:21-30), and relax each. Stage 1 uses the 3-point coarseextensions_stage1(:263-271); Stage 2 re-seeds from the stage-1 interpolated minimum and refines with the 5-pointextensions_stage2(:273-279). - Interpolate the minimum — a degree-2 polynomial fit of energy vs. grain length picks the convex
minimiser (
fit_polynomial_extremum,:55-83;get_interp_min_energy_structure_from_forloop_df,:86-109), giving the converged GB and its energy at the interpolated length. gb_energy(get_GB_energy,:112-120):γ_GB = (E_gb − N·E0)/(2·A) × 16.021766208(eV/Ų → J/m²), areaAthe in-plane cross-section, factor 2 for the two boundaries in the periodic bicrystal. Excess volume (get_GB_exc_volume,:123-131):(V_gb − N·V0)/A/2.
These map to stage2_opt_GBEnergy / stage2_opt_excvol (returned at grain_boundary.py:1423-1424)
→ gb_energy / excess_volume (gb_compute.py:313-314).
4.2 Vacuum relax → GB-plane surface energy
add_vacuum inserts vacuum_length=20 Å along c (grain_boundary.py:1331-1335); the slab is
relaxed (gb_with_vacuum_rel, grain_boundary.py:1337-1341); _get_surface_energy
(grain_boundary.py:1254-1259) gives
σ = (E_vac − E_novac) / A × 16.021766208 / 2 (two exposed surfaces → /2). Stored as
surface_energy, with the vacuum slab kept as surface_structure (gb_compute.py:316, 322).
This is the GB-plane surface energy — the free surface exposed by cleaving the boundary slab — not the clean low-index facet of the surface-energy track. Never cross-compare the two.
4.3 Segregation supercell, GB-plane identification
The relaxed GB is expanded to a segregation supercell (create_supercell_with_min_dimensions →
relax, grain_boundary.py:1346-1355). The GB plane is located by a Voronoi site featuriser
(find_gb_plane(featuriser=voronoi_site_featuriser), grain_boundary.py:1301, 1369-1379), whose
Cartesian location (gb_cart) seeds the cleavage scan.
4.4 Rigid + relaxed cleavage → work of separation + decohesion profile
rigid_and_relaxed_cleavage_study (grain_boundary.py:1088-1123) cleaves the vacuum slab on each
candidate plane around the GB and computes cleavage energy two ways: rigid (static engine, no
relaxation) and relaxed (minimize engine). Per plane, get_results_df
(grain_boundary.py:965-1008) computes
E_cleave = (E − E_uncleaved) / A × 16.0218 J/m² — no factor of 2 here because a vacuum cell
exposes a single GB (grain_boundary.py:996-998). The work of separation is the minimum cleavage
energy over the scanned planes (get_min_energy_from_cleavage_study, grain_boundary.py:1436-1439),
returned as work_of_separation_rigid / work_of_separation_relaxed (gb_compute.py:318-319).
The kernel derives two extra products from the cleavage dataframes (gb_compute.py:251-284):
cleaved_structure_relaxed— the relaxed fracture config at the weakest (min-cleavage_energy) plane (_min_cleavage_structure).cleavage_profile(JSONB) — the compact per-plane{coord, cleavage_energy, energy}list for both rigid and relaxed scans plusvacuum_length(_cleavage_profile). Rigid cleaved structures are not stored (trivially reconstructable from the relaxed GB + coord +vacuum_length).
Any exception (including a gb_code sys.exit) makes the GB converged=False with a truncated
traceback in error (gb_compute.py:328-334); the row is still written (as status='error').
5. Durability: streaming sinks + walltime / CUDA / atom-count caps
A full study is many relaxations and can take an hour+, so the kernel is built to lose at most one in-flight GB on any kill.
- Streaming sinks (
gb_compute.py:362-463).compute_gbcallsbulk_sink(bulk, n_total, n_skip)the instant the EOS+enumeration finish, thengb_sink(gb)the instant each GB is computed (gb_compute.py:430-461). The worker's_write_bulk/_write_gbclosures UPSERT-and-commit per call (gb_worker.py:253-295), so a resume skips already-committed GBs viaalready_done_gb_ids(gb_compute.py:438-439). - Fresh engines per GB (
gb_compute.py:440-443):ASEEnginecaches its working directory on first use, so each boundary gets its own(emin, estatic)— mirroring the provencalculate_single_gb_sequentialdeepcopy-per-GB. - SIGALRM soft cap + watchdog backstop (
_time_limit,gb_compute.py:64-110):GB_STUDY_TIMEOUT_S(default 5400 s = 90 min,gb_compute.py:19) armssignal.alarm; on overrun it raisesTimeoutErrorinsidewf.run()(GB →status='error', worker continues). Because a SIGALRM delivered to a long C/CUDA call is deferred indefinitely (studies ran 13 h past the alarm), a daemon threados._exit(137)s the workerGB_STUDY_KILL_MARGIN_S(default 120 s) past the cap (:89-104), first firing_TIMEOUT_HOOK→_on_timeout(gb_worker.py:144-181), which on a fresh connection writes the hung GBstatus='timeout'and re-queues the structure. - Poisoned-context CUDA guard (
study_one_gb,gb_compute.py:335-357): an unrecoverable CUDA fault (_is_unrecoverable_cuda_error,gb_compute.py:43-61— matches "illegal memory access", "device-side assert", etc., but excludes recoverable OOM) corrupts the whole process context, so the kernel fires_CUDA_FAULT_HOOK→_on_cuda_fault(gb_worker.py:184-206) to re-queue the structure with no terminal row (node-transient → retry, not skip), then hard-exits withGB_CUDA_EXIT_CODE = 138(gb_compute.py:41) so SLURM re-claims it. GB_MAX_BUILT_ATOMScost cap (§3,gb_compute.py:235-238, 399-403) drops the guaranteed-fail large-cell tail before the expensive relax; thread pinning (gb_compute.py:11-13)setdefaultsOMP/MKL/OPENBLAS/…_NUM_THREADS=1before numpy/torch import so a GPU-packed worker doesn't grab all cores and thrash.
6. The DELETE-on-claim queue + resume policy
Work is dispatched from mlip_gb_claim_queue (schema_gb.sql:52-58, PK (potential, structure_id)).
process_one (gb_worker.py:209-342) claims by atomically deleting rows under SKIP LOCKED
(CLAIM, gb_worker.py:41-45) — shown here in a plain (non-executed) fence:
DELETE FROM mlip_gb_claim_queue
WHERE (potential, structure_id) IN (
SELECT potential, structure_id FROM mlip_gb_claim_queue
WHERE potential=%s ORDER BY structure_id LIMIT %s FOR UPDATE SKIP LOCKED)
RETURNING structure_id;
Deleting on claim makes the claim its own lock: no two workers take
the same (potential, structure). A CLAIM_FILTERED variant (gb_worker.py:49-53) restricts claims to
the GB_ONLY_STRUCTURE_IDS allowlist (gb_worker.py:23-28), leaving every other structure parked in
the queue — how a run is focused on the 36 industrial ids without trimming the queue.
Resume skip-set (gb_worker.py:34-39): by default a GB already at status ∈ {done, error, timeout} is skipped on re-run; GB_RETRY_ERRORS=1 drops error from the skip-set,
GB_RETRY_TIMEOUTS=1 drops timeout, and GB_RECOMPUTE_ALL=1 recomputes everything (the UPSERT
overwrites in place — no DELETE needed). Excluded elements (default noble gases,
gb_worker.py:20-21) simply leave the compute set.
7. Schema & the two row types (schema_gb.sql)
mlip_gb(schema_gb.sql:2-26) — one row per boundary,UNIQUE (structure_id, potential, gb_id); carriesgb_energy,excess_volume,surface_energy,work_of_separation_rigid/relaxed,final_energy, three bytea structures (final_structure,surface_structure,cleaved_structure_relaxed), thecleavage_profileJSONB,converged, andstatus ∈ {pending, done, error, timeout}. Written viaGB_UPSERT(gb_worker.py:81-104).mlip_gb_bulk(schema_gb.sql:39-50) — one row per (potential, structure), PK(potential, structure_id); carriesa0,bulk_energy_per_atom,bulk_volume_per_atom,bulk_modulus,bulk_converged, and the coverage denominatorsn_gb_total/n_gb_skipped_max_atoms. Written viaBULK_UPSERT(gb_worker.py:55-68).mlip_gb_claim_queue(schema_gb.sql:52-58) andmlip_gb_eligible(the cubic corpus withelement/basis,schema_gb.sql:62-66).
8. Coverage clamp metric (gb_status.py, and the export)
The naive metric Σ(terminal rows)/Σ(n_gb_total) exceeds 100 % because the numerator is
inflated by (a) legacy enumeration-wave rows and (b) duplicate gb_id representations of the same
physical GB (the gb_id embeds raw GB1/GB2 plane vectors, and different waves wrote equivalent planes
under different conventions) — gb_status.py:5-15. Because those duplicates don't string-match across
conventions, intersecting a freshly-built canonical set is also unreliable.
The fix (clamp). A structure cannot be more than 100 % done, so clamp the distinct terminal count
at n_gb_total per structure (gb_status.py:16-23):
coverage = Σ_struct min(distinct_terminal_gb_id, n_gb_total) / Σ_struct n_gb_total (≤ 100%)
Implemented as sum(LEAST(term, n_gb_total)) / sum(n_gb_total) over terminal statuses
{done, error, timeout} (gb_status.py:69-83, LEAST at :79). It is an upper bound — extras can
pad up to the cap but never past it. structs_full counts structures at the cap. The same clamp
restricted to the 36 industrial ids is clamp_pct_industrial, the benchmark gate, computed in the
export's pot_summary_sql (export_gb_to_parquet.py:128-169, industrial LEAST(...) at :140) and
surfaced per potential in gb_potential_summary.parquet. gb_status.py hard-codes the 36
IDS_DEFAULT and the 15 KEEP (gb_status.py:35-41); the export mirrors them in INDUSTRIAL_IDS /
KEEP_POTENTIALS (export_gb_to_parquet.py:38-46).
9. Consensus (the only cross-model signal) — tools/regen_gb_from_export.py
With no DFT truth, the site scores each potential by deviation from the KEEP consensus. The site
tool tools/regen_gb_from_export.py reads the parquet export and writes the vendored CSVs under
src/assets/elemental/gb/.
- Analysis universe — converged rows with finite
gb_energy(regen_gb_from_export.py:232-234). - Boundary-level consensus (
build_consensus,:72-82): for each(structure_id, gb_id), the mediangb_energy(and medianwork_of_separation_rigid) over the 15 KEEP potentials, kept only wheren_keep ≥ MIN_KEEP_FOR_CONSENSUS = 5(:43, :81). - Per-potential deviation (
pot_consensus_metrics,:85-114):gb_energy_consensus_mae(mean |Δ| vs consensus),_rmse,wsep_consensus_mae, andspearman_vs_consensus(scipy-free rank correlation,_spearman,:58-69), withn_boundaries_scored. - "Consensus dev" column —
build_structure_rollup(:117-164) computesgb_consensus_absdev = mean|E_gb − consensus|per (potential, structure) over consensus-valid boundaries (:148-153); the site column is the median over structures of that. The KEEP median + Q1/Q3 band per structure (the spread overlay) isbuild_struct_consensus(:167-192). - Leaderboard sort (
build_potentials,:195-223): default order is coverage (clamp_pct_industrial, thenstructs_with_data), not consensus (which is coverage-confounded — a potential that only did a few easy boundaries scores a tiny MAE). Thestructs_with_dataguard (distinct structures with ≥1 converged GB,:200-205) stops an all-error potential being flattered by a 100 % terminal clamp.
10. Known caveats & provenance notes
nequip_OAM-Lis AOTI-broken on B200 — 0 usable GB rows despite a 100 % terminal clamp (all-error/timeoutrows count toward the clamp); thestructs_with_dataguard (regen_gb_from_export.py:200-205) stops it topping the board, and it is flagged on the leaderboard.- Consensus leans GRACE / is coverage-confounded — 11 of 15 KEEP are GRACE-family ("most typical"
≈ "most GRACE-like"); always read
n_boundaries_scorednext to any MAE. Consensus is agreement, never correctness. - GB-plane surface energy ≠ surface track — §4.2: the cleaved-slab facet is not a clean low-index surface; do not cross-compare tracks.
n_gb_totalis cap-relative — structures built under an earlier (higher)GB_MAX_BUILT_ATOMScarry a larger denominator, so a partially-done heavy structure reads lower than under a lower cap (gb_status.py:25-28).
11. Reproduce one boundary
PY=python # env with pyiron_workflow_atomistics + a model calculator
from benchmark.db import connect, from_bytea
from benchmark.gb_compute import compute_gb
from benchmark.worker import _resolve_factory
calc, _ = _resolve_factory("mace_medium-omat-0")()
with connect() as c, c.cursor() as cur:
cur.execute("SELECT element, basis FROM mlip_gb_eligible WHERE structure_id=%s", (6,))
element, basis = cur.fetchone()
cur.execute("SELECT atoms_pickle FROM structure WHERE id=%s", (6,))
atoms = from_bytea(cur.fetchone()[0])
res = compute_gb(element, basis, atoms, calc, scratch_root="/tmp/gb_demo") # EOS → build → study
print(res["n_gb_total"], [g["gb_energy"] for g in res["gbs"]])
Inputs that fully determine the rows: the MP cell + (element, basis), the potential's calculator, the
fixed study params (gb_compute.py:146-150), and the CSL enumeration at the model's own a0. The DB
sinks, caps, and queue are durability plumbing around this deterministic core.
See the leaderboard → · Readable methodology → · Repo: ligerzero-ai/mlip-elemental-benchmark → · ← Methodology overview