---
description: Every key of matmelt.yaml — the run, pipeline, backends and stages blocks, each backend kind's options, and each stage's options with defaults.
---

# Configuration reference

One YAML file drives every command. `matmelt init` writes a commented starter
version of it; this page is the complete list of what may go in it.

Which file a command reads, and the `materials-cli.yaml` fallback, are covered
in {ref}`the CLI reference <which-file-config-resolves-to>`.

## The shape

```yaml
run:
  name: my-first-run
  work_dir: ./materials-work
  elements: [Na, Cl]
  combination_sizes: [2]

pipeline:
  stop_after: extract
  retry_budget: 3

backends:
  laptop:
    kind: local
    options:
      work_dir: ./materials-jobs
      max_parallel: 2

stages:
  compose:
    backend: laptop
    options:
      python: python3
  structure:
    backend: laptop
    options:
      python: python3
  screen:
    backend: laptop
    options:
      python: python3
      device: cpu
  extract:
    backend: laptop
```

Four top-level sections, and the split between the last two is the architecture
in one file: **`backends:` is *where* work runs, `stages:` is *what* runs**, and
a stage names its backend by name only. Moving the expensive stage to a cluster
is an edit to one line of `stages:`, never to any Python.

`run:`, `backends:` and `stages:` are required. `pipeline:` may be omitted
entirely — every key in it has a default.

## `run:`

This run's identity and scientific inputs.

| Key | Default | Meaning |
|---|---|---|
| `name` | *required* | A short label, shown by `status` and `results`. |
| `work_dir` | *required* | Root of the workspace: the state database and every per-material directory live under it. |
| `elements` | *required* | The element set `compose` enumerates over. At least one, none empty. |
| `combination_sizes` | *all sizes* | How many elements each seed combines. |

`combination_sizes: [2]` explores binaries, `[2, 3]` adds ternaries, and `1` is
legal — it seeds elemental solids. **Omitting the key, or writing `[]`, means
every size from 1 to the number of distinct elements**, which is the widest
search and grows combinatorially. Sizes are de-duplicated and sorted, and a size
larger than your element count yields nothing (`doctor` names it rather than
silently enumerating zero seeds).

Enumeration cost is reported before any work starts — by `doctor`, and by `run`
in its preflight — so you see the seed count before committing a machine to it.

## `pipeline:`

How the funnel executes. Every key is optional.

| Key | Default | Meaning |
|---|---|---|
| `stop_after` | `null` | Name of the last stage this run executes. |
| `retry_budget` | `3` | Per-stage fault retries before a material goes `FAILED`. Minimum `1`. |
| `lease_timeout_s` | `900.0` | Age at which a claim counts as orphaned and returns to its queue. Must be `> 0`. |
| `pass_interval_s` | `1.0` | Sleep between orchestrator passes. May be `0`. |

`stop_after` must name a stage you configured, and it is mutually exclusive with
`run --only` (using both is exit `2`). **A mid-funnel stop is a pause, not a
retirement:** materials rest at that stage's output state, live and claimable, so
raising the value, removing the key, or running `matmelt run --only <stage>`
continues them in the same workspace. Only a run that executes through the
configured *last* stage retires its materials as `DONE`.

`retry_budget` counts **faults** — a crashed job, a missing input, an
unreachable backend. It does not apply to science verdicts: a material screened
out by MACE, or a DFTB+ WorkChain that exhausted the plugin's own healing, fails
terminally at the first attempt, because re-running reproduces it. That
distinction is visible in `status`, where a fault shows `Attempts: 3` and a
verdict shows `Attempts: 1`.

These three numeric keys reject YAML booleans on purpose: `true` would otherwise
be coerced to `1` and quietly change the run.

## `backends:`

A mapping of your own names to compute targets. At least one is required, and
several stages may share one (they then share a single instance, and its
`max_parallel` applies across them).

| Key | Default | Meaning |
|---|---|---|
| `kind` | *required* | Which implementation: `local`, `ssh_exec`, or another registered kind. |
| `options` | `{}` | That kind's own settings — free-form here, validated by the backend itself. |

Options are deliberately not part of this schema: each kind needs different
keys, and each backend's constructor rejects what it cannot use. An invalid
option is a config error naming the `backends.<name>` path, raised while the run
is being wired rather than after jobs start.

### `kind: local`

Runs jobs as subprocesses on this machine.

| Option | Default | Meaning |
|---|---|---|
| `work_dir` | a fresh temp dir | Root for per-job directories: the wrapper script, both captured streams, the exit-code sentinel, the manifest. |
| `max_parallel` | `1` | How many jobs may run at once. |
| `gpu` | `false` | Advertise a GPU in this backend's capabilities. |

Set `work_dir` explicitly for any real run. The default is a temporary directory,
which is fine for a smoke test but is not somewhere you want to look for a job's
`stderr` afterwards.

### `kind: ssh_exec`

Runs jobs on another machine over SSH, staging files per job.

| Option | Default | Meaning |
|---|---|---|
| `host` | *required* | An `~/.ssh/config` alias, a hostname, or `user@host`. |
| `user` | — | SSH user, when `host` carries none. |
| `ssh_binary` | `ssh` | SSH client to invoke. |
| `rsync_binary` | `rsync` | rsync client to invoke. |
| `remote_python` | — | Override for `argv[0]` remotely. Normally unnecessary — the stage's own `python` option already names the remote interpreter — but this wins when both are set. |
| `remote_work_dir` | `/tmp/matmelt-jobs` | Root for per-job remote directories, each removed when the job is released. |
| `shared_filesystem` | `false` | Skip data staging, assuming the remote sees the job's `cwd` at the same absolute path. |
| `use_slurm` | `false` | Submit with `sbatch` and track the SLURM job id instead of a PID. |
| `partition` | — | SLURM partition, when `use_slurm` is on. |
| `max_parallel` | `1` | How many jobs may be in flight on this host. |
| `gpu` | `false` | Advertise a GPU in this backend's capabilities. |

Two properties of this backend are load-bearing and easy to assume wrongly.
**The remote never receives MatMelt's code**: a job ships the stage's
self-contained runner script plus the files that stage declared, and nothing
else — there is no package to install there and no `PYTHONPATH` to arrange. And
**data moves by declarative staging** by default, so nothing depends on the two
machines sharing a filesystem; `shared_filesystem: true` is an optimisation you
opt into when they genuinely do.

Authentication is your SSH setup's business, not MatMelt's: connections run with
`BatchMode=yes`, so key-based access must already work non-interactively. No
password is ever read, stored, or logged.

## `stages:`

The funnel stages, each naming its backend.

| Key | Default | Meaning |
|---|---|---|
| `backend` | *required* | The *name* of an entry under `backends:`. |
| `options` | `{}` | Stage-specific settings, validated by that stage's constructor. |

The order you write stages in is cosmetic — the funnel order is fixed:

    compose → structure → screen → [calculate] → extract

You may configure a **prefix or an ordered subsequence** of it. `calculate` is
optional and may be skipped; every other stage before the furthest one you
configure must be present, and `compose` always anchors the funnel. A config
that skips a required stage is refused by name (`missing [structure]`) before
anything runs.

Every stage takes a `python` option: the interpreter, **on the backend that
stage is wired to**, that has its science package installed. This is what
`doctor` probes — remotely when the stage runs remotely — and what its install
hints target. It is never assumed to be the interpreter running MatMelt itself.

### `compose`

Turns one element-combination seed into candidate compositions. Fan-out; needs
SMACT.

| Option | Default | Meaning |
|---|---|---|
| `python` | `python3` | Interpreter on this stage's backend. |
| `threshold` | `2` | Maximum stoichiometric coefficient SMACT explores. Higher finds more exotic stoichiometries and costs combinatorially more. |
| `max_unary_atoms` | follows `threshold` | Cap for single-element seeds, which emit elemental compositions `X1..Xn`. Set it separately to reach, say, `C60` without enlarging the multi-element search. |

### `structure`

Turns one composition into candidate crystals, one per symmetry group tried.
Fan-out; needs PyXtal.

| Option | Default | Meaning |
|---|---|---|
| `python` | `python3` | Interpreter on this stage's backend. |
| `dimensions` | `[3D]` | Which dimensionalities to generate. A bare `3D` works as well as a list. |

Each dimensionality sweeps its **own full symmetry family**:

| Tag | Family | Groups tried |
|---|---|---|
| `3D` | space groups | 1–230 |
| `2D` | layer groups | 1–80 |
| `1D` | rod groups | 1–75 |

So `dimensions: [3D, 2D]` tries 310 groups per composition, and costs
accordingly. Candidate ids carry the tag — `Cl1Na1_3D-225`, `Cl1Na1_2D-72` — and
so does each candidate's `structure.json`, which surfaces in exported records
under `_mcli.provenance.dimension`. An unknown tag is refused at load time with
the valid set named. Everything downstream treats every candidate as an ordinary
crystal.

### `screen`

Relaxes one candidate with MACE and keeps or rejects it. This is the funnel's
first terminal science verdict. Needs `mace-torch`.

| Option | Default | Meaning |
|---|---|---|
| `python` | `python3` | Interpreter on this stage's backend. |
| `model_path` | `""` | A local MACE model file. Empty means the MACE-MP foundation model. |
| `model_size` | `medium` | Foundation-model size when `model_path` is empty: `small`, `medium` or `large`. |
| `device` | `cpu` | Device passed to MACE: `cpu`, `cuda`, `mps` or `auto`. |
| `dtype` | `float64` | Torch dtype: `float64` or `float32`. |
| `fmax` | `0.05` | Force convergence target, eV/Å. |
| `max_steps` | `200` | Relaxation step cap. |

`float64` is the default because a variable-cell relaxation needs it for a
reliable stress tensor; `float32` stays available as an explicit speed trade.

Leaving `model_path` empty means the foundation model is downloaded to MACE's
cache on first use. That is **disclosed before it happens**, never silent:
`doctor` reports the size and cache location, and `run` asks for consent
(`--yes` grants it non-interactively). For a first run, `model_size: small`
(~32 MB) is a better trade than the default.

### `calculate` — optional

Runs one screened-in material through DFTB+ via **your own AiiDA**. Needs
`aiida-core`, `aiida-mydftbp` and `ase` in this stage's `python`, plus a working
profile, daemon and code.

| Option | Default | Meaning |
|---|---|---|
| `code_label` | *required* | Label of your configured AiiDA code for DFTB+, e.g. `dftbplus@cluster`. `verdi code list` shows them. |
| `skf_prefix` | *required* | Absolute directory of the `.skf` files **on the machine where DFTB+ runs**, baked into the HSD input. |
| `python` | `python3` | Interpreter holding your AiiDA installation. |
| `profile` | default profile | AiiDA profile to load. |
| `skf_label` | `ptbp_complete_set` | Label of the stored Slater–Koster `FolderData` node. |
| `skf_local_path` | — | Local `.skf` directory used to *create* that node when no node with `skf_label` exists. Without it, a missing node is a loud, actionable failure rather than a silent stall. |
| `kpoints` | `[4, 4, 4]` | k-mesh diagonal; exactly three integers ≥ 1. |
| `max_wallclock_seconds` | `3600` | Walltime requested per WorkChain attempt. |
| `queue_name` | code's default | Scheduler queue for the WorkChain's jobs. |
| `poll_interval_s` | `30.0` | Seconds between WorkChain status polls. |

This stage is **not** wired to a remote through `ssh_exec`: remoteness is
AiiDA's job here — the computer and queue belong to the code you configured, and
MatMelt's backend only needs to reach the interpreter that talks to your AiiDA
profile.

### `extract`

Assembles the final OPTIMADE-shaped `record.json`, folding in the calculate
result when there is one.

| Option | Default | Meaning |
|---|---|---|
| `python` | `python3` | Any interpreter — this stage's runner is standard-library only. |

Extract is the one stage with **no** science dependency at all. That is a
contract, checked by the test suite, not an accident of the current
implementation: `doctor` always reports it ready.

## `env:NAME` references

Any string value may be written as `env:VARNAME`, and the loader substitutes
that environment variable's value:

```yaml
backends:
  cluster:
    kind: ssh_exec
    options:
      host: env:CLUSTER_HOST
      user: env:CLUSTER_USER
```

This is how a shareable config file stays free of hostnames, usernames and
anything else you would not commit. An unset variable is a config error naming
both the variable and its YAML path — never an empty string silently substituted.
Substitution happens before validation, so `lease_timeout_s: env:LEASE` works:
the resolved string is coerced into the typed field.

## What the loader rejects

Every model forbids unknown keys, so a typo is an error rather than a setting
that silently does nothing. Field-level problems are reported **together**, one
line each, with dotted YAML paths — you fix them in one pass rather than one per
run:

```console
$ matmelt doctor
Config error: matmelt.yaml: 2 configuration errors:
  - pipeline.retry_budget: Value error, must be a number, not a boolean
  - pipeline.stop_afterr: Extra inputs are not permitted
```

The cross-reference checks run **after** every field validates, so a broken
reference surfaces once the fields are clean, reported against the file as a
whole:

```console
$ matmelt doctor
Config error: matmelt.yaml: 1 configuration error:
  - <config>: Value error, stages.screen.backend: references undefined backend 'gpubox'; defined backends: ['laptop']
```

The checks, beyond each field's own type:

- every `stages.<name>.backend` names a defined backend;
- `pipeline.stop_after`, when set, names a configured stage;
- at least one backend and at least one stage exist;
- the configured stages form an ordered subsequence of the funnel, with
  `calculate` optional;
- each stage's `options` are accepted by that stage's constructor, and each
  backend's by its own.

All of these are exit code `2`.
