Runner scripts#
Each stage ships as a thin class plus a self-contained runner script. The runner is what actually does the science, and it is the only part of MatMelt that ever reaches a compute backend: the stage declares it as an input artifact, the backend stages it into the job’s private directory, and the job executes it there.
Three properties follow from that design, and they are contracts rather than conveniences:
A runner imports only the standard library and its own science packages — never
matmelt. This is what lets a remote machine run MatMelt jobs while holding none of MatMelt’s code. A test asserts it for every runner by parsing the import graph.Every science import is lazy, inside a function body. A runner therefore loads — and answers
--help— on a machine with no science stack at all, which is also why building these docs needs no SMACT, PyXtal, MACE, ASE or AiiDA installed.A missing dependency exits 3 with the exact
pip installcommand and the path of the interpreter that lacked it. Guessing the install name is not good enough:maceis installed asmace-torch.
Running one by hand#
Because a runner is self-contained, you can execute it directly on a material’s directory to debug a stage in isolation, without the orchestrator, the state store, or a config file:
$ cd ~/matmelt-work/Cl1Na1_3D-225
$ python3 ~/path/to/screen_runner.py --help
$ python3 ~/path/to/screen_runner.py --cif structure.cif --device cpu
The exit codes are uniform across all of them: 0 the work completed, 2 a usage error
or an unusable input, 3 a missing dependency. Note that “completed” is not “the science
succeeded” — a screening that rejects a structure still exits 0, because it did screen
it. The verdict lives in the file the runner wrote, which is exactly the separation the
stage relies on when it interprets the result.
MatMelt compose runner: SMACT charge-balance ONE element combination.
Self-contained by design – imports stdlib + smact only, never matmelt – because it is staged to the compute backend per job and executed there by the user’s own python; remotes never receive our package code. It is equally runnable by hand for isolated debugging:
python compose_runner.py --elements Cl Na
python compose_runner.py --help
Given one element combination (any arity: binary, ternary, …), it asks
SMACT for the charge-balanced compositions and writes the fan-out result into
the current directory (or --out-dir):
children.json # JSON list of child ids (may be [])
children/<formula>/composition.json # one seed file per child
A child id is the reduced formula with explicit counts (Cl1Na1,
Cl2Na1O3): deterministic, filesystem-safe, unambiguous. Compositions that
reduce to the same formula under different oxidation-state assignments merge
into one child carrying every assignment.
Unary (single-element) seeds are special-cased. SMACT’s charge-neutrality
filter yields nothing for one element (no charge-balanced unary compound), so
a single --elements X emits the elemental compositions X1..Xn directly
(n = --max-unary-atoms, defaulting to --threshold) – allotrope
stoichiometries like C2 or C60 for the structure stage to find cells
for. Elemental compositions are deliberately never gcd-reduced (reduction
would collapse every Xn into X1) and never touch smact at all.
Exit codes: 0 = completed, including zero viable compositions (an
empty fan-out is a valid scientific result, not an error); 2 = bad
arguments (argparse); 3 = smact is not importable in this python
environment (stderr says exactly what to install); 1 = unexpected error
(traceback on stderr).
- matmelt.core.stages.compose_runner.reduced_counts(counts)[source]#
Reduce a stoichiometry to its smallest whole-number form.
- Parameters:
counts (
list[int]) – Atom counts per element, all at least 1.- Returns:
The counts divided by their greatest common divisor.
- Return type:
Examples
>>> reduced_counts([2, 2]) [1, 1] >>> reduced_counts([4, 6, 2]) [2, 3, 1]
- matmelt.core.stages.compose_runner.formula_id(symbols, counts)[source]#
Build the child id for one composition.
- Parameters:
- Returns:
Symbols with explicit counts, so
Cl1Na1rather thanClNa. Writing the 1s keeps every id unambiguous and deterministic, which matters because this string becomes a directory name and a material id.- Return type:
Examples
>>> formula_id(["Cl", "Na"], [1, 1]) 'Cl1Na1'
- matmelt.core.stages.compose_runner.merge_compositions(raw)[source]#
Fold SMACT’s charge-balanced tuples into unique children.
- Parameters:
raw (
list[tuple[Any,Any,Any]]) –(symbols, ox_states, counts)tuples assmact_filteryields them.- Returns:
{formula_id: composition}, where each composition carriesformula,elements,stoichiometryandoxidation_states. Stoichiometries are reduced first, so entries landing on the same formula – the same compound reached through different oxidation-state assignments, or an unreduced multiple – merge into one composition carrying every distinct assignment.- Return type:
- matmelt.core.stages.compose_runner.unary_compositions(element, max_atoms)[source]#
Enumerate elemental compositions for a single-element seed.
Charge balance cannot apply to one element, so SMACT is never consulted – and never imported – on this path.
- Parameters:
- Returns:
{formula_id: composition}forX1throughX<max_atoms>, mirroringmerge_compositions’s shape sowrite_childrenis reused unchanged. The stoichiometry is the atom count per formula unit and is deliberately not gcd-reduced:C2andC60are distinct allotrope candidates, not multiples ofC1. The oxidation state of a neutral elemental solid is 0.- Return type:
Examples
>>> sorted(unary_compositions("C", 3)) ['C1', 'C2', 'C3']
- matmelt.core.stages.compose_runner.discover(elements, threshold)[source]#
Ask SMACT for the charge-balanced compositions of one element combination.
The single SMACT touchpoint in this file. The import is inside the function body, which is what lets the module load – and
--helprun – in an environment with no science stack at all.- Parameters:
- Returns:
(symbols, ox_states, counts)per charge-balanced candidate.- Return type:
- Raises:
ImportError – When smact is not installed in this interpreter.
maincatches it and exits 3 with the exact install command.
- matmelt.core.stages.compose_runner.write_children(compositions, out_dir)[source]#
Write one directory per composition, then the manifest.
- Parameters:
- Return type:
Notes
The manifest is the driver’s signal that the fan-out result is complete, so it is written after every child directory and via tmp-then-rename. A crash mid-run therefore leaves child directories but no manifest – never a manifest naming children that do not exist.
- matmelt.core.stages.compose_runner.main(argv=None)[source]#
Run the compose stage for one seed.
- Parameters:
argv (
list[str] |None, default:None) – Command-line arguments.Nonereadssys.argv.- Returns:
The process exit code:
0when the fan-out completed, including a fan-out into zero children;2for a usage error (argparse’s own);3when smact is missing, with the exactpip installcommand and this interpreter’s path printed to stderr.- Return type:
MatMelt structure runner: PyXtal-generate candidates for ONE composition.
Self-contained by design – imports stdlib + pyxtal only, never matmelt – because it is staged to the compute backend per job and executed there by the user’s own python; remotes never receive our package code. It is equally runnable by hand for isolated debugging:
python structure_runner.py --composition composition.json
python structure_runner.py --composition composition.json --dims 3D 2D
python structure_runner.py --help
Given one composition (formula + elements + stoichiometry), it
asks PyXtal for one random symmetry-constrained crystal in every group of each
requested dimensionality. Each dimensionality is swept over its full
symmetry family – 3D = 230 space groups, 2D = 80 layer groups, 1D = 75 rod
groups (--dims, default 3D). Groups whose constraints cannot be met are
skipped – but counted and reported, never swallowed silently. The fan-out
result lands in the current directory (or --out-dir):
children.json # JSON list of child ids (may be [])
children/<Formula>_<DIM>-<SG>/structure.cif # the candidate crystal
children/<Formula>_<DIM>-<SG>/structure.json # provenance: dim, SG, symbol, lattice
A child id is <formula>_<DIM>-<space-group-number> (Cl1Na1_3D-225):
the dimensionality tag and group number are joined with - so the last
underscore stays the formula boundary, and space-group symbols (P2_1/c,
which carry path separators) never appear. Deterministic and filesystem-safe.
Exit codes: 0 = completed, including zero valid structures (an empty
fan-out is a valid scientific result, not an error); 2 = bad arguments or
an unreadable/malformed composition file; 3 = pyxtal is not importable in
this python environment (stderr says exactly what to install); 1 =
unexpected error (traceback on stderr).
- matmelt.core.stages.structure_runner.DIM_GROUPS: dict[str, tuple[int, int]] = {'1D': (1, 75), '2D': (2, 80), '3D': (3, 230)}#
Dimensionality tag -> (PyXtal
dimargument, highest group number in that family). 3D = 230 space groups, 2D = 80 layer groups, 1D = 75 rod groups. Each dimensionality is swept over its own full family (a group number above a family’s count does not exist for it).
- matmelt.core.stages.structure_runner.REQUIRED_KEYS = ('formula', 'elements', 'stoichiometry')#
Keys one composition file must carry (the compose stage’s child seed shape).
- matmelt.core.stages.structure_runner.load_composition(path)[source]#
Read and validate one composition file.
The same rules
StructureStage.composition_ofenforces, repeated here because the runner must stay debuggable standalone – it cannot import the stage that normally checks its input.- Parameters:
path (
Path) – Thecomposition.jsonto read.- Returns:
The decoded composition.
- Return type:
- Raises:
ValueError – Naming the defect: unreadable, not valid JSON, not an object, a missing required key, a formula that is not alphanumeric (it names child directories), or elements and stoichiometry that do not line up.
- matmelt.core.stages.structure_runner.child_id(formula, dim_tag, space_group)[source]#
Build the child id for one generated structure.
- Parameters:
- Returns:
<formula>_<dim>-<group>. The dim tag and group number join with-so the last underscore stays the formula boundary.- Return type:
Examples
>>> child_id("Cl1Na1", "3D", 225) 'Cl1Na1_3D-225'
- class matmelt.core.stages.structure_runner.GenerationReport(children=<factory>, n_rejected=0, n_errors=0, error_examples=<factory>)[source]#
Bases:
objectWhat happened across the swept groups – nothing is silent.
- n_rejected#
Groups where a crystal was generated but its symmetry constraints were not met (
crystal.validfalse). A normal outcome, not an error.- Type:
- matmelt.core.stages.structure_runner.generate(composition, out_dir, crystal_factory, dims=('3D',), group_limit=None)[source]#
Try each group of each requested dimensionality once, writing valid crystals.
- Parameters:
composition (
dict[str,Any]) – The validated composition to build structures for.out_dir (
Path) – The job’s working directory; children land underchildren/<child_id>/.crystal_factory (
Callable[[],Any]) – Returns a fresh pyxtal-like object per attempt. This is the seam that keeps the loop testable without pyxtal installed.dims (
Sequence[str], default:('3D',)) – Which dimensionalities to sweep.group_limit (
tuple[int,int] |None, default:None) – An optional inclusive group range, applied per family. Useful for hand-debugging, where sweeping 230 groups is not the point.
- Returns:
The children written plus the counted failures. Each generated structure gets a
structure.cifand astructure.jsonrecording its group, lattice type, and volume.- Return type:
Notes
A failing group never stops the sweep: it is counted, separately for symmetry rejections and raised errors, and the loop continues. The report exists precisely so that no failure is silent.
- matmelt.core.stages.structure_runner.write_manifest(children, out_dir)[source]#
Write the fan-out manifest last, atomically.
- Parameters:
- Return type:
Notes
The manifest is the driver’s signal that the fan-out result is complete, so it lands only after every child directory exists, via tmp-then-rename. A crash mid-run leaves child directories but no manifest, never a manifest naming children that do not exist.
- matmelt.core.stages.structure_runner.main(argv=None)[source]#
Run the structure stage for one composition.
- Parameters:
argv (
list[str] |None, default:None) – Command-line arguments.Nonereadssys.argv.- Returns:
The process exit code:
0when the sweep completed, including a fan-out into zero children, with the per-group tally printed;2for a usage error or an unusable composition file;3when pyxtal is missing, with the exactpip installcommand and this interpreter’s path printed to stderr.- Return type:
MatMelt screen runner: MACE vc-relax + classify ONE candidate structure.
Self-contained by design – imports stdlib + mace/ase only, never matmelt – because it is staged to the compute backend per job and executed there by the user’s own python; remotes never receive our package code. It is equally runnable by hand for isolated debugging:
python screen_runner.py --cif structure.cif
python screen_runner.py --help
It reads one CIF, runs a variable-cell relaxation (LBFGS over a cell filter – forces and stress minimized together, the vc-relax equivalent), then classifies the result on physical-sanity grounds only:
screen_result.json # category RELAXED|FAILED + failure_reasons + metrics
relaxed.cif # written on RELAXED only (a stale one is removed)
Classification is convergence-based (energy window, finite energy, bond
overlap, volume change, residual stress, final fmax, energy rise) – NOT a
stability threshold: a high-energy metastable structure is information, not
a rejection. float64 is the default dtype – required for a reliable
stress tensor in vc-relax.
A FAILED category is a normal completed screening (exit 0) – the stage
turns it into a terminal science verdict. Exit codes: 0 = screening
completed (either category); 2 = bad arguments or unreadable --cif;
3 = mace/ase not importable in this python environment (stderr says
exactly what to install); 1 = unexpected error, e.g. a crashed
relaxation (traceback on stderr – the stage treats it as a retryable
fault, never a science rejection).
- matmelt.core.stages.screen_runner.EV_PER_A3_TO_GPA = 160.21766208#
eV/A^3 -> GPa.
- matmelt.core.stages.screen_runner.SANITY = {'energy_ceil_eV_atom': 10.0, 'energy_floor_eV_atom': -35.0, 'fmax_acceptance_eV_A': 0.5, 'max_energy_rise_eV_atom': 0.01, 'max_stress_GPa': 2.0, 'max_vol_change_pct': 60.0, 'min_bond_A': 1.2}#
Physical-sanity bounds. Deliberately constants, not CLI knobs (only the model and the main relaxation settings are configurable): they flag MACE extrapolation / unphysical geometries, and loosening them per run would make verdicts incomparable across a database.
- matmelt.core.stages.screen_runner.classify(initial, final)[source]#
Classify a finished relaxation as
RELAXEDorFAILED.A pure function over plain metric dicts, so every bound is unit-testable with no science stack installed.
- Parameters:
initial (
dict[str,Any]) – Observables before relaxation. Needse_eV_atomandvolume_A3.final (
dict[str,Any]) – Observables after relaxation. Needse_eV_atom,fmax_eV_Aandvolume_A3;stress_max_GPaandmin_bond_Amay beNone, which skips that check rather than failing the structure – an observable the calculator could not supply is not evidence against a crystal.
- Returns:
(category, reasons)where category is"RELAXED"when no check fired and"FAILED"otherwise, andreasonsholds one human-readable sentence per failed check.- Return type:
Notes
The checks are convergence and physical sanity: the energy window, NaN energies, overlapping atoms, runaway volume change, residual stress, the fmax acceptance gate, and an optimizer that walked uphill. Thresholds live in the module’s
SANITYconstants rather than in CLI options, so verdicts stay comparable across a whole database.Deliberately absent: any rejection on absolute energy ranking. Metastable is information, not failure.
- matmelt.core.stages.screen_runner.write_verdict(verdict, out_dir)[source]#
Write the verdict atomically, scrubbing any stale relaxed structure.
- Parameters:
- Return type:
Notes
A non-
RELAXEDverdict must never sit next to a leftoverrelaxed.ciffrom an earlier attempt, because everything downstream trusts that pairing – so the stale file is removed first.
- matmelt.core.stages.screen_runner.screen(sci, args, out_dir)[source]#
Read, variable-cell relax, and classify one structure.
- Parameters:
- Returns:
The verdict document: category, failure reasons, whether the optimizer converged, the step count, the initial and final observables, and the settings used – so a stored verdict is reproducible from its own record.
- Return type:
Notes
The relaxation is LBFGS over a cell filter, i.e. atoms and cell relax together. Convergence is not itself the verdict: a run that stops at the step limit can still pass
classify’s acceptance gate, and one that converges can still be rejected as unphysical.
- matmelt.core.stages.screen_runner.main(argv=None)[source]#
Screen one candidate structure.
- Parameters:
argv (
list[str] |None, default:None) – Command-line arguments.Nonereadssys.argv.- Returns:
The process exit code:
0when screening completed, whichever way the verdict went – aFAILEDverdict is a successful screening, and the science lives in the written document;2for a usage error or an unreadable structure file;3when mace-torch is missing, with the exactpip installcommand and this interpreter’s path printed to stderr.- Return type:
Notes
A crashed relaxation deliberately propagates as an unhandled exception (exit 1 with a traceback). The stage reads that as a retryable fault, which is what keeps an infrastructure failure from ever being recorded as a science rejection.
MatMelt calculate runner: DFTB+ via the user’s own AiiDA, ONE material.
Self-contained by design – imports stdlib + aiida/aiida_mydftbp/ase only, never matmelt – because it is staged to the compute backend per job and executed there by the user’s own python; remotes never receive our package code. It is equally runnable by hand for isolated debugging:
python calculate_runner.py --id Cl1Na1_225 --code-label dftbplus@cluster \
--skf-prefix /opt/skf/
python calculate_runner.py --help
The user’s environment provides everything AiiDA: aiida-core, the
aiida-mydftbp plugin (whose mydftbp.single WorkChain does the
self-healing DFTB+ retries itself), a configured profile + code, and a
running daemon. This runner is the launcher/extractor around that, per
material:
prepare –
relaxed.cif->.gengeometry +dftb_in.hsdfrom an embedded template (SKF prefix, k-mesh from the options);ensure the labelled Slater-Koster
FolderDataexists (find-or-create from--skf-local-path; missing and no local path = loud failure, never a silent stall);submit one
mydftbp.singleWorkChain and writecalc_submission.jsonBEFORE polling, so a killed run re-attaches to the same WorkChain instead of resubmitting;poll to terminal, then extract every healing attempt (parameters with SKF absolute paths stripped, per-attempt diffs) into an atomic
calc_result.json.
Exit codes: 0 = calculation completed and calc_result.json written
(converged or not – science lives in the JSON, the stage judges); 2 =
bad arguments or unreadable --cif; 3 = aiida/aiida_mydftbp/ase not
importable in this python environment (stderr says exactly what to
install); 1 = unexpected error (traceback on stderr – the stage treats
it as a retryable fault).
Every function below takes plain arguments (the AiiDA symbols ride in an
api dict built by _load_aiida), so the whole orchestration is
testable with fakes and no aiida installed – the dev-venv hard rule.
- matmelt.core.stages.calculate_runner.GEOMETRY_NAME = 'geom.out.gen'#
What DFTB+ writes the relaxed geometry to, given the template’s
OutputPrefix = "geom.out"; retrieved by the plugin, cached intocalc_result.jsonso exports need no AiiDA round-trip.
- matmelt.core.stages.calculate_runner.max_angular_momentum(symbol)[source]#
Give the
MaxAngularMomentumshell for one element.- Parameters:
symbol (
str) – The element symbol.- Returns:
"s"for the s-block,"d"for the d-block,"f"for the lanthanides and actinides, and"p"for the p-block – which is also the fallback for an unknown symbol, since it is the commonest case and DFTB+ would otherwise refuse to start.- Return type:
Examples
>>> max_angular_momentum("Na"), max_angular_momentum("Fe") ('s', 'd')
- matmelt.core.stages.calculate_runner.species_from_gen(gen_text)[source]#
Read the element symbols from DFTB+
.gentext.- Parameters:
gen_text (
str) – The file’s contents. The species row is line 2 of the header.- Returns:
The symbols, in the order the
.genfile indexes them.- Return type:
- Raises:
ValueError – When the text is too short to hold a header, or the species row does not look like element symbols.
- matmelt.core.stages.calculate_runner.build_hsd(gen_text, skf_prefix, kpoints)[source]#
Assemble
dftb_in.hsdfrom the embedded template.- Parameters:
gen_text (
str) – The geometry, embedded inline asGenFormat.skf_prefix (
str) – Directory of the.skffiles on the machine where DFTB+ runs. Normalized to a trailing slash, because DFTB+ concatenates it withEl1-El2.skfverbatim.kpoints (
tuple[int,int,int]) – TheSupercellFoldingk-mesh diagonal.
- Returns:
The complete HSD input.
- Return type:
Notes
MaxAngularMomentumis derived per species. The document’s shape is not arbitrary: it must round-trip through the aiida-mydftbp plugin’s HSD parser, so the keys that plugin’s self-healing ladders tune –MaxSteps,Optimizer,Convergence,SCC,KPointsAndWeights– are all written in the forms it parses explicitly.
- matmelt.core.stages.calculate_runner.write_gen(cif_path, gen_path)[source]#
Convert a CIF to a DFTB+
.genfile, in fractional coordinates.- Parameters:
- Return type:
Notes
The runner’s only ase touchpoint besides
_load_aiida, imported lazily so everything above stays runnable anywhere. A missing ase has already surfaced as exit 3 before this is ever called.
- matmelt.core.stages.calculate_runner.SKF_PATH_REDACTED = '"<stripped>"'#
What replaces the SKF directory in extracted parameters. The only place an absolute machine path leaks into the plugin’s parameter dicts is the SlaterKosterFiles passthrough string under Hamiltonian.DFTB.
- matmelt.core.stages.calculate_runner.strip_skf_paths(params)[source]#
Copy
paramswith the SKF directory path redacted.- Parameters:
params (
dict[str,Any]) – A parameter dict as the plugin’s HSD parser produced it.- Returns:
A deep copy with the
Prefixvalue replaced. The plugin keepsSlaterKosterFilesas a_raw_Npassthrough string insideHamiltonian.DFTB, and that is the one place an absolute machine path leaks into a record – extracted documents must never carry it. Anything unexpected is left untouched: redaction is best-effort over a copy, never a failure.- Return type:
- matmelt.core.stages.calculate_runner.param_diff(old, new, _prefix='')[source]#
Diff two parameter dicts – what the plugin’s healing actually changed.
- Parameters:
- Returns:
{dotted.path: change}, where change is{"from": ..., "to": ...}for a changed value and{"added": ...}or{"removed": ...}for a key present on one side only. Two dict values recurse; a type change compares whole.- Return type:
Notes
Keys starting with
_are skipped at every level:_raw_*passthrough strings and_healingmetadata are not parameter changes.Examples
>>> param_diff({"Driver": {"MaxSteps": 100}}, {"Driver": {"MaxSteps": 500}}) {'Driver.MaxSteps': {'from': 100, 'to': 500}}
- matmelt.core.stages.calculate_runner.shape_attempt(pk, attempt, exit_status, params, previous_params, result)[source]#
Shape one WorkChain attempt as a plain JSON-safe dict.
- Parameters:
pk (
int) – The CalcJob’s primary key in the user’s AiiDA database.attempt (
int) – Its 1-based position in the healing chain.exit_status (
int|None) – How that attempt ended, orNonewhen unavailable.params (
dict[str,Any] |None) – The parameters it ran with, orNonewhen unavailable.previous_params (
dict[str,Any] |None) – The previous attempt’s parameters, for the diff.result (
dict[str,Any] |None) – Its parsed output, orNone.
- Returns:
The attempt record.
healingcarries the plugin’s_healingmetadata,Nonewhen the attempt predates it or parameters are unavailable;paramsare stored SKF-stripped; andparam_changes_from_previousisNonefor the first attempt and aparam_diffotherwise – which is what makes a healing chain readable as “it raised MaxSteps, then loosened convergence”.- Return type:
- matmelt.core.stages.calculate_runner.shape_result(material_id, single_wc_pk, uuid, converged, exit_status, attempts, final_geometry=None)[source]#
Assemble the top-level
calc_result.jsondocument.- Parameters:
material_id (
str) – The material this calculation belongs to.single_wc_pk (
int) – Themydftbp.singleWorkChain’s primary key.uuid (
str) – Its uuid, which is what a re-attach verifies against.converged (
bool) – Whether the WorkChain reached a converged result.exit_status (
int|None) – Its exit status, which the stage maps to a verdict.attempts (
list[dict[str,Any]]) – The shaped attempt records, in chronological order.final_geometry (
dict[str,Any] |None, default:None) – The geometry DFTB+ actually relaxed to.
- Returns:
The result document.
- Return type:
Notes
final_resultis the last attempt’s parsed output only when the WorkChain converged, andNoneotherwise: failed attempt history is data too, but only a converged result may be consumed as one.final_geometryis cached here so consumers never need to reach into the AiiDA repository – the attempts’ ownparamshold only what each attempt started from. It is captured whether or not the WorkChain converged (the top-levelconvergedflag is what says it may be consumed as a result), and isNonewhen it could not be read.
- matmelt.core.stages.calculate_runner.ensure_skf_folder(orm, label, local_path)[source]#
Find the labelled Slater-Koster
FolderData, or create it – loudly.- Parameters:
- Returns:
The stored
FolderDatanode. An existing node is reused, so a second run never duplicates it.- Return type:
Any- Raises:
RuntimeError – When no node carries the label and either no
local_pathwas given or it holds no.skffiles. The message names exactly what to configure.
Notes
A missing SKF node is never a quiet skip – that silent stall is the documented defect this function exists to fix. The failure surfaces as a clean exit-1 message rather than a traceback or a run that appears to be working.
- matmelt.core.stages.calculate_runner.submit_workchain(api, args, hsd_path)[source]#
Submit one self-healing
mydftbp.singleWorkChain for this material.- Parameters:
- Returns:
The submitted WorkChain node. Submission is non-blocking: the node is handed to the daemon, which is what lets the runner be killed without losing the calculation.
- Return type:
Any
Notes
skf_filesis passed even though the WorkChain’s defaultuse_remote_skf_path=Truemeans the bytes are never uploaded – it is a required input, and it records provenance.
- matmelt.core.stages.calculate_runner.load_submission(out_dir)[source]#
Read the recorded submission, if this material has one.
- Parameters:
out_dir (
Path) – The job’s working directory.- Returns:
{"pk": ..., "uuid": ...}, orNonewhen no record exists or it is unusable – which the caller treats as “submit a fresh one”. A record that exists but is corrupt warns on stderr first: silently double-submitting a DFTB+ calculation is expensive.- Return type:
- matmelt.core.stages.calculate_runner.attach_or_submit(api, args, hsd_path)[source]#
Re-attach to the recorded WorkChain, or submit and record a fresh one.
- Parameters:
- Returns:
The WorkChain node this run will follow.
- Return type:
Any
Notes
calc_submission.jsonis written immediately after submission, before any polling. That ordering is the whole point: a killed run re-attaches to the same WorkChain on rerun and never double-submits, which was proven live by SIGKILLing the CLI mid-poll and finding exactly one WorkChain in the database afterwards.A record that no longer loads, or whose uuid disagrees with the loaded node, is treated as stale – warned about, then replaced. A quiet double-submit is never acceptable here.
- matmelt.core.stages.calculate_runner.poll_until_terminated(api, pk, interval_s)[source]#
Reload the WorkChain node until it reports terminated.
- Parameters:
- Returns:
The terminated node.
- Return type:
Any
Notes
The daemon does the work; this only watches. Killing the runner mid-poll therefore costs nothing – the WorkChain keeps running, and the next run re-attaches to it.
- matmelt.core.stages.calculate_runner.extract_attempts(api, workchain)[source]#
Collect every CalcJob attempt under the WorkChain, oldest first.
- Parameters:
- Returns:
One shaped record per attempt, in chronological order: parameters SKF-stripped, healing metadata captured, and each diffed against the previous attempt that had parameters. This is the full history of what the plugin’s self-healing tried – which MVP_V0’s extractor never pulled.
- Return type:
Notes
Attempts are found by following
CALL_CALClinks, since the plugin submits one CalcJob per healing attempt, and sorted by creation time rather than by link order, which is not guaranteed to be chronological.
- matmelt.core.stages.calculate_runner.extract_final_geometry(api, workchain, name='geom.out.gen')[source]#
Read the geometry the last attempt relaxed to, from its retrieved folder.
- Parameters:
- Returns:
{"gen": ..., "source_calc_pk": ..., "source": ...}, orNonewhen it could not be read.- Return type:
Notes
Best-effort by design. A calculation that converged is not less converged because its geometry could not be read, so every failure here is a stderr warning and a
None– never a non-zero exit, and never a lost result document.Caching this into
calc_result.jsonis what letsmatmelt cifexport a structure without reaching back into the AiiDA repository at all.
- matmelt.core.stages.calculate_runner.run(api, args)[source]#
Drive one material end to end, with the profile already loaded.
Prepare the geometry and HSD input, attach to or submit the WorkChain, poll it to termination, extract every attempt, and write the result atomically.
- Parameters:
- Returns:
0. The science lives in the written document, not in the exit code: a WorkChain that failed still produced a successful extraction, and the stage reads the verdict out ofcalc_result.json.- Return type:
- matmelt.core.stages.calculate_runner.main(argv=None)[source]#
Calculate one screened-in material with DFTB+ through the user’s AiiDA.
- Parameters:
argv (
list[str] |None, default:None) – Command-line arguments.Nonereadssys.argv.- Returns:
The process exit code:
0when a result document was written, whatever the WorkChain’s own outcome;1for an expected, actionable failure such as the SKF ensure refusing, printed as a message rather than a traceback;2for a usage error or an unreadable structure file;3when the AiiDA stack is missing, with the exact install command and this interpreter’s path printed to stderr. Anything unexpected propagates with its traceback.- Return type:
MatMelt extract runner: assemble ONE material’s OPTIMADE-shaped record.
Self-contained AND stdlib only – extraction is bookkeeping, not science, so unlike the other runners there is nothing to install and no exit-3 path: it runs under any python, local or remote. It is equally runnable by hand:
python extract_runner.py --id Cl1Na1_225
python extract_runner.py --help
Inputs (all in the cwd, or under --in-dir):
relaxed.cif– the screened structure (required); parsed by a minimal built-in CIF reader targeting ASE-written files. Geometry is best-effort: a parse failure degrades to a geometry-less record with a stderr warning – never a crash (composition then comes from the metadata).screen_result.json– must saycategory: RELAXED; extract serves only screened-in materials (exit 2 otherwise, stated clearly).structure.json– optional provenance from the structure stage (space group, lattice type); folded in when present.calc_result.json– optional DFTB+ outcome from the calculate stage (WorkChain status + the full healing-attempt history, SKF paths already stripped by its runner); folded under_mcli.calculatewhen present.
Output: record.json (written atomically) – one OPTIMADE structures
-shaped document: standard attributes (elements, formulas, sites, lattice)
plus the non-standard screening/provenance extras namespaced under
_mcli, per the OPTIMADE custom-field convention.
Exit codes: 0 = record written; 2 = bad arguments, a missing or
malformed input, a non-RELAXED verdict, or no composition source at all;
1 = unexpected error (traceback on stderr).
- matmelt.core.stages.extract_runner.parse_cif(text)[source]#
Parse cell parameters and atom sites out of one CIF document.
A minimal reader targeting the shape ASE writes: scalar
_cell_*tags plus oneloop_whose_atom_site_*columns may come in any order. Extra loops are skipped, and the site symbol falls back to_atom_site_labelwhen no type symbol column exists.- Parameters:
text (
str) – The CIF document.- Returns:
{"cell": (a, b, c, alpha, beta, gamma), "sites": [(symbol, fx, fy, fz), ...]}.- Return type:
- Raises:
ValueError – Naming what is missing – a cell tag, the coordinate columns, a short row, or any atom sites at all. The caller decides whether that is fatal;
maindegrades loudly rather than failing the record.
- matmelt.core.stages.extract_runner.cell_to_lattice(a, b, c, alpha, beta, gamma)[source]#
Convert the six cell parameters into lattice vectors.
- Parameters:
- Returns:
Three lattice vectors (rows) in angstrom, in the standard crystallographic convention:
aalong x,bin the xy-plane. Right-angle float noise is snapped to honest zeros, sincecos(90 deg)is ~6e-17 rather than 0.- Return type:
Examples
>>> cell_to_lattice(4.0, 4.0, 4.0, 90.0, 90.0, 90.0) [[4.0, 0.0, 0.0], [0.0, 4.0, 0.0], [0.0, 0.0, 4.0]]
- matmelt.core.stages.extract_runner.frac_to_cart(frac, lattice)[source]#
Convert fractional coordinates to cartesian ones.
- matmelt.core.stages.extract_runner.chemical_formulas(counts_by_element)[source]#
Build the OPTIMADE chemistry fields for one composition.
- Parameters:
counts_by_element (
dict[str,int]) –{symbol: count}for the whole structure.- Returns:
The OPTIMADE fields:
elements(sorted) andnelements;elements_ratiosas fractions of the total, per sorted element;chemical_formula_reduced, gcd-reduced with a count of 1 omitted;chemical_formula_descriptive, the same but keeping every count; andchemical_formula_anonymous, reduced counts descending and lettered A, B, …- Return type:
- Raises:
ValueError – When there are no elements – a record with no chemistry serves nobody.
Examples
>>> fields = chemical_formulas({"Na": 4, "Cl": 4}) >>> fields["chemical_formula_reduced"], fields["chemical_formula_anonymous"] ('ClNa', 'AB')
- matmelt.core.stages.extract_runner.structure_block(parsed)[source]#
Build the OPTIMADE geometry attributes from a parsed CIF.
- Parameters:
- Returns:
lattice_vectors,cartesian_site_positions,nsites,species_at_sites,species, and the periodicity fields. Every site is treated as fully occupied by a single element, which is what a generated-and-relaxed structure always is.- Return type:
- matmelt.core.stages.extract_runner.build_record(material_id, screen, parsed_cif, meta, calc=None)[source]#
Assemble the OPTIMADE
structures-shaped document.- Parameters:
material_id (
str) – The record’s id.screen (
dict[str,Any]) – The screening verdict, whose category, convergence, step count, final observables and settings are carried into the record.parsed_cif (
dict[str,Any] |None) – The parsed relaxed structure, orNonewhen the CIF could not be read. Geometry is best-effort: without it the record is still written, with_mcli.geometry_sourceset tonull.meta (
dict[str,Any]) – The structure stage’s provenance – space group, dimensionality, lattice type – and the fallback composition source.calc (
dict[str,Any] |None, default:None) – The calculate stage’s result document, folded verbatim under_mcli.calculatewhen given, healing history included. LeftNone, the record is byte-for-byte what a calculate-less funnel always produced.
- Returns:
The complete record, with everything MatMelt-specific namespaced under
_mcliper the OPTIMADE custom-field convention.- Return type:
- Raises:
ValueError – When neither the CIF nor the metadata can supply a composition.
- matmelt.core.stages.extract_runner.write_record(record, out_dir)[source]#
Write
record.jsonatomically.
- matmelt.core.stages.extract_runner.main(argv=None)[source]#
Assemble one screened-in material’s record.
- Parameters:
argv (
list[str] |None, default:None) – Command-line arguments.Nonereadssys.argv.- Returns:
The process exit code:
0when the record was written;2for a usage error, an unusable screening verdict, a non-RELAXEDverdict, or a structure that yields no composition from any source. There is no exit 3 here – the runner is stdlib-only, so it has no dependency to be missing.- Return type:
Notes
Geometry is best-effort and degrades loudly: an unparseable CIF prints a warning to stderr and the record is still written from the structure stage’s metadata, with
_mcli.geometry_sourceset tonull. The optionalstructure.jsonandcalc_result.jsoninputs degrade the same way. Only a total absence of composition is fatal.
MatMelt cif runner: fetch DFTB+ relaxed geometries out of AiiDA (M10).
Self-contained by design – imports stdlib + aiida only, never
matmelt – because it is executed by the user’s own python (the one
their AiiDA lives in), exactly like the stage runners beside it. It is the
one exception to the “one runner per funnel stage” rule: it belongs to the
cif command, not to a stage, and is never staged to a compute backend.
Why it exists: calc_result.json records every healing attempt’s input
parameters, so the geometry embedded there is what DFTB+ started from
(the MACE-relaxed cell from the screen stage). The geometry DFTB+ relaxed
to is geom.out.gen in the final CalcJob’s retrieved folder, which
lives only in the AiiDA repository. This runner reads it back.
It works in batches – one profile load serves thousands of materials:
python cif_runner.py --batch batch.json --out geometries.json
python cif_runner.py --id Cl1Na1_225 --wc-pk 13524 --uuid abc-123
python cif_runner.py --help
--batch takes a JSON list of {"id", "wc_pk", "uuid"?, "calc_pk"?}
entries. The output document is {"results": [{"id", "wc_pk", "calc_pk",
"gen", "error", "note"}, ...]} – one row per requested material, in the
requested order. A material that cannot be resolved gets an error
string and a null geometry: one bad material never costs the batch, and
the caller decides what a failure means.
Resolution mirrors the calculate runner’s own extraction: load the
WorkChain, verify its uuid when one was recorded (pks are per-profile –
this is what stops a profile switch from exporting a stranger’s structure),
walk its CALL_CALC CalcJob children, and take the most recent by
creation time – the attempt that converged. A recorded calc_pk that
disagrees with it is reported in note; the database, not the record,
decides.
Exit codes: 0 = the batch ran and the document was written (per-material
failures live inside it); 2 = bad arguments or an unreadable/malformed
batch file; 3 = aiida not importable in this python environment (stderr
says exactly what to install); 1 = the profile could not be loaded or
the output could not be written.
- matmelt.core.stages.cif_runner.GEOMETRY_NAME = 'geom.out.gen'#
What DFTB+ writes the relaxed geometry to, given our HSD template’s
OutputPrefix = "geom.out"– and what the plugin retrieves.
- matmelt.core.stages.cif_runner.load_batch(path)[source]#
Read and validate the batch request.
- Parameters:
path (
str|Path) – The JSON request file thecifcommand wrote.- Returns:
One normalised entry per material, each with
id,wc_pk,uuidandcalc_pkkeys.- Return type:
- Raises:
ValueError – Naming the defect: unreadable, not valid JSON, not a list, or an entry without a non-empty string
idand an integerwc_pk.uuidandcalc_pkare optional.
Notes
Validation happens before AiiDA is touched at all, so a typo in the request costs a message rather than a profile load.
- matmelt.core.stages.cif_runner.read_object(node, name)[source]#
Read one stored file as text.
- Parameters:
- Returns:
Its contents, decoded when stored as bytes.
- Return type:
- Raises:
RuntimeError – When the file is absent. The message lists what is there, which is what turns “no geometry” into a diagnosable answer.
- matmelt.core.stages.cif_runner.final_calcjob(api, workchain)[source]#
Find the WorkChain’s most recent CalcJob – its last healing attempt.
- Parameters:
- Returns:
The newest
CALL_CALCCalcJob by creation time.- Return type:
Any- Raises:
RuntimeError – When the WorkChain has no attempts to read a geometry from.
Notes
The same traversal the calculate runner extracts attempts with, so “the final attempt” means the same thing in both documents.
- matmelt.core.stages.cif_runner.fetch_geometry(api, entry, geometry_name)[source]#
Fetch one material’s relaxed geometry as a result row. Never raises.
- Parameters:
- Returns:
A row with
id,wc_pk,calc_pk,gen,errorandnote. Any failure – an unloadable pk, a uuid mismatch, no attempts, no retrieved folder, an absent geometry file – comes back aserrortext with anullgeometry, so the batch always completes and the caller reports per material.- Return type:
Notes
A recorded uuid that disagrees with the loaded node is a refusal, not a warning: exporting a different node’s geometry under this material’s id would silently corrupt the export. A recorded
calc_pkthat disagrees is only anote, and the database wins.
- matmelt.core.stages.cif_runner.run(api, args)[source]#
Fetch every requested geometry and emit the result document.
- Parameters:
- Returns:
0when the document was emitted, even if some rows carry errors – a per-material failure is data, not a process failure.1only when the output file itself could not be written.- Return type:
Notes
Progress and the final tally go to stderr, leaving stdout clean for the result document when no
--outis given.
- matmelt.core.stages.cif_runner.main(argv=None)[source]#
Fetch relaxed DFTB+ geometries out of the user’s AiiDA repository.
- Parameters:
argv (
list[str] |None, default:None) – Command-line arguments.Nonereadssys.argv.- Returns:
The process exit code:
0when the result document was emitted;1when the output could not be written;2for a usage error or an unusable batch file;3when aiida-core is missing, with the exact install command and this interpreter’s path printed to stderr.- Return type: