The stage contract#
A stage is autonomous. It declares what it reads, declares what it writes, checks both, builds job descriptions, and judges results. Nothing else in the system knows what a composition or a space group is — which is what lets the orchestrator be small and the stages be replaceable.
Four obligations#
Every stage implements the same four-part contract:
Step |
Obligation |
|---|---|
|
Refuse clearly if the material lacks what this stage needs |
|
Turn a material into a portable job description |
|
Turn a finished job into a verdict |
|
Confirm the promised files actually appeared |
The first and last exist because of a specific failure: starting in the
middle. A funnel is a chain of stages that each assume the previous one ran.
Point screen at a material with no structure file and the honest outcomes are
“refuse, saying exactly what is missing” or “produce garbage confidently”. The
contract makes the first mandatory:
stage 'screen' cannot run on item 'Cl1Na1_3D-1': missing or empty required
input file(s) ['structure.cif'] in materials-work/Cl1Na1_3D-1 (one candidate
crystal from the structure stage)
An empty file counts as missing. A truncated write is a real thing that happens,
and a zero-byte structure.cif is not a structure.
validate_outputs closes the other end: a job that exits 0 without producing
what it promised has not succeeded. Exit codes are a claim; the files are the
evidence.
Verdicts and faults are different, and the difference is doctrine#
When a job comes back, the stage decides which of two things happened:
A verdict is the science answering. The relaxation converged to something unphysical; the DFTB+ WorkChain exhausted its own healing ladders. Re-running reproduces it exactly, so retrying is pure waste. A terminal verdict fails the material immediately, at attempt 1, and spends no retry budget.
A fault is everything else: a crashed process, a missing input, an
unreachable backend, a result file that will not parse. It might not recur, so
it is retried until retry_budget is spent.
The rule when it is genuinely unclear is written into the code and worth stating plainly: when in doubt, fault. Wrongly calling something a verdict discards a material forever on the strength of a guess. Wrongly calling it a fault costs some compute and then fails it anyway. The costs are not symmetric, so the tie-break is not neutral.
This is why an unparseable verdict file is a fault while a parsed FAILED
verdict is terminal, and why a crashed relaxation — which could be an
out-of-memory kill — never counts as science.
Declare every file#
A stage names every file it reads and every file it writes, in requires,
produces and the job’s artifact lists. On a local run this looks like
bookkeeping, because everything already sits in the material’s directory. It
stops looking like bookkeeping the moment a stage runs on another machine: the
declaration is the transport manifest. Anything undeclared does not travel.
This has bitten in practice, which is why it is a rule rather than a
suggestion — a fan-out stage that wrote a whole children/ subtree while
declaring only the manifest worked perfectly on a laptop and failed every
material as soon as the stage ran over SSH.
The declaration also lets the fake backend fabricate outputs in tests, so a stage’s whole lifecycle is exercisable with no science installed at all.
The science ships as a standalone script#
Each stage is really a pair: a thin Stage class that decides and judges, and a
self-contained runner script that computes. The runner imports the standard
library and its own science package — never MatMelt. It is shipped to the
backend with each job and executed there.
Three properties follow, and each one is load-bearing:
Remote machines need nothing of ours. A job carries its own runner, so there
is no package to install on a cluster and no PYTHONPATH to arrange. See
Remote execution.
Every stage is debuggable alone. The runner is an ordinary program with
--help:
$ python3 screen_runner.py --help
$ python3 structure_runner.py --composition composition.json --dims 3D
When a stage misbehaves, you can run exactly what it ran, in your own shell, with no orchestrator, no database and no config.
The heavy dependency never reaches the base install. Science imports live
inside runner functions, so pip install matmelt pulls in no torch, and a
missing package produces an actionable message naming the exact install command
rather than an import error at startup.
Options are the contract too#
A stage’s constructor signature is its option contract. Config options are unpacked onto it and validated there, so a typo or an out-of-range value is caught while the run is being wired — not after two hundred jobs have failed identically. That is also why the documentation of stage options can be checked against the code automatically: there is exactly one place they are defined.