Command layer#
One run_* entry point per CLI command; cli.py stays a thin dispatcher.
The command layer: one module per CLI command (Phase 5).
Each module exposes a run_<command>(...) -> int function that does the
work and returns a process exit code; cli.py stays a thin Typer
dispatcher that parses options and raises typer.Exit with that code.
Dependency arrows point inward (commands/ -> core/ / backends/ /
config/); all human-facing output goes through ui/console.py.
Shared exit-code convention (5.1b): 0 = success / all checks passed, 1 = problems found or the command refused to proceed, 2 = config or usage error.
- matmelt.commands.DEFAULT_CONFIG_FILENAME = 'matmelt.yaml'#
Where every command looks for the config file unless
--configsays otherwise – and the filenameinitscaffolds.
- matmelt.commands.LEGACY_CONFIG_FILENAME = 'materials-cli.yaml'#
The pre-rename filename, scaffolded by
materials-cli0.1.x. A workspace created before the MatMelt rename keeps working untouched: seeresolve_config_path().
- matmelt.commands.STATE_DB_FILENAME = 'state.sqlite'#
The run’s state database, under
run.work_dir.runcreates it;status/resultsread it (WAL mode – safe while a run is live).
- matmelt.commands.resolve_config_path(config_path)[source]#
Return the config file to read, honouring the pre-rename filename.
The MatMelt rename changed the default config filename from
materials-cli.yamltomatmelt.yaml. A workspace scaffolded bymaterials-cli0.1.x must not break, so when the caller took the default and nomatmelt.yamlexists, an existingmaterials-cli.yamlbeside it is used instead and a deprecation note is printed once, on stderr (--jsonstdout stays parseable).An explicit
--configis always honoured verbatim – the fallback only ever fills in for the default, and never invents a file that is not there (a missing config still fails inload_config, naming the new name).
The init command: scaffold a starter config, then check the environment (5.1c).
Two halves, both promises from the 2026-07-08 policy: the user gets a
commented, immediately-valid matmelt.yaml to edit (never
overwriting one that exists – exit 1, file untouched), and then the doctor
checks run against it so the very first contact with the tool names exactly
what is missing and the exact install command for it.
The scaffold always lands before the checks run: an environment with missing science packages still gets its config file (that is the normal first-install experience – the report tells the user what to do next), and the exit code is the doctor’s own verdict.
- matmelt.commands.init.TEMPLATE = '# MatMelt configuration (scaffolded by `matmelt init`).\n# Any string value may be an environment reference: "env:NAME".\n\nrun:\n name: my-first-run\n work_dir: ./materials-work # run workspace: state DB + per-material dirs\n elements: [Na, Cl] # the element set the funnel explores\n # combination_sizes: [2, 3] # [2] = binaries, [2, 3] adds ternaries, ...;\n # # 1 = single elements (elemental solids).\n # # Omitted = ALL sizes 1..len(elements).\n\npipeline:\n stop_after: extract # last stage this run executes. A mid-funnel\n # # stop is a pause: materials rest at that\n # # stage\'s output, resumable by a later run --\n # # only the funnel\'s last stage retires DONE.\n retry_budget: 3 # per-stage fault retries before FAILED\n # lease_timeout_s: 900 # orphaned-claim recovery age (seconds)\n # pass_interval_s: 1.0 # sleep between orchestrator passes\n\nbackends: # the WHERE: machines that run jobs\n laptop:\n kind: local\n options:\n work_dir: ./materials-jobs # backend job scratch\n max_parallel: 2 # jobs in flight at once\n # A remote over SSH (inputs are staged per job; the remote only needs the\n # science packages in remote_python\'s venv -- never this tool):\n # cluster:\n # kind: ssh_exec\n # options:\n # host: cluster.example.org\n # user: env:CLUSTER_USER\n # remote_python: /opt/science-venv/bin/python\n # max_parallel: 4\n\nstages: # the WHAT: the funnel, in order\n compose: # SMACT charge-balanced compositions\n backend: laptop\n options:\n python: python3 # interpreter with smact installed\n threshold: 2 # max SMACT stoichiometric coefficient\n # max_unary_atoms: 4 # single-element seeds emit X1..Xn allotrope\n # # stoichiometries (default: threshold)\n structure: # PyXtal symmetry-constrained candidates\n backend: laptop\n options: {python: python3} # interpreter with pyxtal installed\n screen: # MACE/ASE variable-cell relaxation\n backend: laptop\n options:\n python: python3 # interpreter with mace-torch installed\n device: cpu # cpu | cuda\n # model_path: /path/to/mace.model # unset = MACE-MP foundation model\n # # (offered as a consented download)\n # calculate: # OPTIONAL higher-fidelity DFTB+ stage.\n # backend: laptop # Runs on YOUR OWN AiiDA: the stage\'s python\n # options: # needs aiida-core + aiida-mydftbp installed,\n # # and a running AiiDA profile + daemon.\n # python: python3 # interpreter with aiida-core + aiida-mydftbp\n # code_label: dftbplus@cluster # REQUIRED: `verdi code list` shows yours\n # skf_prefix: /opt/skf/ # REQUIRED: abs dir of .skf files where DFTB+ runs\n # # profile: my-profile # AiiDA profile (unset = your default)\n # # skf_local_path: ./skf # local .skf dir to create the SKF node from\n # # skf_label: ptbp_complete_set # label of the stored SKF FolderData\n # # kpoints: [4, 4, 4] # SupercellFolding k-mesh\n # # max_wallclock_seconds: 3600\n # # queue_name: dftb # scheduler queue/partition\n extract: # OPTIMADE-shaped record.json (stdlib only)\n backend: laptop\n'#
valid as written (
doctorruns on it immediately), with the knobs a first run needs visible and the remote story sketched in comments. Science interpreters are per-stage options – the user points each stage at the python that has its package.- Type:
The starter config
- matmelt.commands.init.run_init(config_path)[source]#
Scaffold a starter config, then diagnose it.
- Parameters:
config_path (
str) – Where to write. An existing file is never overwritten.- Returns:
1when the file exists or cannot be written; otherwise whateverdoctorconcludes about the freshly-written config.- Return type:
Notes
The scaffold lands before the checks run, so a user whose environment is not ready still gets a config to edit rather than nothing at all.
The template is valid as written – a test wires it through
load_configandbuild_runtime, so it can never rot into an example that does not load.
The run command: preflight gate, seed, then drain the funnel (5.1d).
The productized mock_test.md driver. Order of operations:
Config –
load_config->build_runtime+build_stages(exit 2 on any config problem, YAML paths included).Preflight gate – the enumeration report first (the n-ary blast radius is visible before anything else), then
preflight_stagesover the active stages only – the samestop_afterprefix the orchestrator will execute, so a stage this run never reaches cannot block it. Not ready -> exit 1 with one exact install command per problem, before any work starts (the 3.6 promise).Model consent – when the screen stage is active and no
model_pathis configured, the foundation-model download is disclosed and consent obtained (--yesfor non-interactive runs); declining aborts cleanly. With consent, the cache is warmed up front via the user’s own interpreter – except when screen runs on a remote backend, where the model must live in the remote’s cache and will be fetched there on first use (disclosed). A configured-but-missing model path is a config problem: refuse.Seed + drain –
element_combinationsseeds the store (idempotent – rerunning a finished or interrupted run is always safe), then a single-threaded drain loop:run_passuntil every material is terminal, one progress line per pass with activity.--max-passesis the escape hatch for smoke tests; Ctrl-C exits cleanly (exit 0) – state and in-flight jobs are durable, and the same command resumes.
--materials/--materials-file make the run selective: only the
named materials – and their descendants, via the orchestrator’s lineage
walk – are seeded, claimed, and finalized. Unselected materials sit
untouched in whatever state they hold, so the drain condition switches to
only-mode’s “a pass with zero activity and nothing in flight”.
A mid-funnel ``stop_after`` (naming any stage but the configured last one) uses the same drain condition: its materials rest at the stop stage’s output state – live, resumable by a later run – and never finalize, so an “every material terminal” drain would spin forever.
Exit codes: 0 = the drain completed or was cleanly interrupted/limited
(science FAILED verdicts are results, reported in the summary – not a
process failure); 1 = refused to start; 2 = config error.
- matmelt.commands.run.run_run(config_path, *, assume_yes=False, max_passes=None, only=None, materials=None, materials_file=None)[source]#
Run the funnel a config describes, to completion or until interrupted.
- Parameters:
config_path (
str) – The config file to run.assume_yes (
bool, default:False) – Accept the foundation-model download consent without prompting, for unattended runs.max_passes (
int|None, default:None) – Stop after this many orchestrator passes. An escape hatch for debugging; the default drains.only (
str|None, default:None) – Run exactly one stage on demand. The preflight gates just that stage, seeding is skipped unless it iscompose– freshNEWitems would otherwise sit unconsumed forever – and the drain stops when the stage has no work left. Conflicts loudly withpipeline.stop_after.materials (
str|None, default:None) – Comma-separated material ids to restrict this run to.materials_file (
str|None, default:None) – A file of ids, one per line, with blanks and#comments skipped. Combines withmaterials, and both combine freely withonly. The selection covers each named material and its descendants, so fan-out children are eligible automatically.
- Returns:
0when the run drained or was interrupted cleanly – science failures are results, not process failures, so a run whose materials were all screened out still exits 0.1when the environment is not ready or consent was declined;2for a config or usage error.- Return type:
Notes
Ctrl-C is a supported way to stop: in-flight jobs stay recorded, and the next
runre-attaches to them instead of resubmitting.
The status command: a read-only view of the run’s durable state (5.1e).
Opens the state database under run.work_dir (WAL mode – safe to read
while a run is live in another terminal) and reports the per-state
counts, every terminally-failed material with the stage and reason it died
on, and the in-flight job ledger. It changes nothing and never claims.
--state <state> switches to the picking-list view: the material ids
currently sitting at that state (with their parents), i.e. exactly what a
user browses to build a run --materials selection.
Exit codes: 0 = state shown (failed materials are content, not a command failure), 1 = no run state exists yet, 2 = config error / unknown state name.
- matmelt.commands.status.run_status(config_path, *, as_json=False, state=None)[source]#
Show what the run’s state database currently records.
- Parameters:
config_path (
str) – The config file naming the workspace to read.as_json (
bool, default:False) – Emit a JSON payload on stdout instead of Rich tables.state (
str|None, default:None) – List the material ids sitting at one lifecycle state, instead of the tally. Case-insensitive. This is the picking list forrun --materialsandretry.
- Returns:
0on success,1when there is no run state yet,2for a config error or an unknown state name.- Return type:
Notes
Read-only: the database is byte-identical afterwards, and WAL mode makes this safe to run while a job is in flight.
The monitor command: a live, read-only dashboard over the run’s state.
Where status is a one-shot report, monitor watches: it opens the
state database as an observer (StateStore(..., read_only=True) – the
engine itself rejects writes, so watching a live run can never corrupt it)
and redraws the five-panel dashboard (run overview, stage pipeline, running
now, recent completions, recent failures) every --interval seconds until
Ctrl-C. --once prints a single snapshot and exits – the
scriptable/CI-friendly leg.
The wiring mirrors the Orchestrator’s constructor discipline: this command
hands core.monitor.build_snapshot plain names and mappings derived from
the config (stage order, stage -> backend name, backend name -> kind), so
the core stays free of config imports. Two probes touch infrastructure, both
throttled: backend health (constructed via build_runtime, asked
health_check through core.preflight.check_backends, cached by
_HealthCache – once at startup, then at most every
HEALTH_PROBE_TTL_S seconds) and per-job CPU/GPU usage
(core.cpusample.UsageSampler over the in-flight ledger – local backends
every tick, remote ones on the samplers’ own TTL).
Exit codes: 0 = watched and stopped cleanly (Ctrl-C included – stopping a dashboard is not a failure), 1 = no run state exists yet, 2 = config or usage error.
- matmelt.commands.monitor.HEALTH_PROBE_TTL_S = 30.0#
How long one backend-health probe stays fresh (seconds). Probes can be slow (
ssh_execdoes a network round-trip), so the dashboard re-probes on this cadence instead of on every refresh tick.
- matmelt.commands.monitor.run_monitor(config_path, *, interval=2.0, once=False, max_ticks=None, track=None, track_file=None)[source]#
Watch a live run on a refreshing dashboard. Read-only.
- Parameters:
config_path (
str) – The config file naming the workspace to watch.interval (
float, default:2.0) – Seconds between refreshes. Must be positive.once (
bool, default:False) – Print a single snapshot instead of entering the live loop – what a script or a CI log wants.max_ticks (
int|None, default:None) – Bound the live loop’s updates. The testing knob;Nonewatches until Ctrl-C.track (
str|None, default:None) – Comma-separated material ids to follow.track_file (
str|None, default:None) – A file of ids, one per line. Either option switches the third panel from recent failures to the tracked-materials detail for exactly those ids, including honest “not found” rows for ids the store does not know.
- Returns:
0on a clean exit, Ctrl-C included;1when there is no run state;2for a config error, a non-positive interval, or an unusable tracking selection.- Return type:
Notes
The store is opened read-only, enforced by the SQLite engine rather than by discipline, so watching a run can never disturb it. Backend health is cached rather than probed per tick, and remote usage sampling is throttled behind its own TTL.
The results command: list finished materials; export their records (5.1e).
DONE materials come from the state database; each one’s extracted
record.json (the OPTIMADE-shaped output of the extract stage) is read
from its item directory under run.work_dir. A funnel configured
without an extract stage legitimately has DONE materials without records –
they are listed and counted, never an error. (A mid-funnel stop_after
run produces no DONE materials at all: they rest at the stop stage’s
output, resumable.) A record that exists but cannot be parsed is
reported loudly and skipped, and the export stays honest about how many
records it carries.
--export PATH writes the records as one JSON array (each element
{"id": ..., "record": {...}} – the database handoff), atomically.
--json prints the same content plus the bookkeeping to stdout.
Exit codes: 0 = listed/exported, 1 = no run state exists yet, 2 = config error.
- matmelt.commands.results.run_results(config_path, *, as_json=False, export=None)[source]#
List the run’s finished materials, and optionally export their records.
- Parameters:
- Returns:
0on success,1when there is no run state or the export could not be written,2for a config error.- Return type:
Notes
A
DONEmaterial with norecord.jsonis first-class here, not an error: a run stopped beforeextractfinishes materials that never produced a record. Records that exist but cannot be read are skipped loudly.Read-only: the state database is byte-identical afterwards.
The doctor command: diagnose config, backends, and science readiness (5.1b).
A read-only aggregation of the machinery that already exists: load_config
-> build_runtime + build_stages (any config problem is an exit-2 with
its YAML path), then core.preflight.doctor_report (backend reachability +
location-aware per-stage requirement probes), the seed-enumeration report
(the n-ary blast radius), and – when a screen stage is configured – the
MACE model status (including the will-not-download-without-consent
disclosure).
The command renders and exits; it fixes nothing and never crashes on the thing it diagnoses. Exit codes: 0 = everything healthy and ready, 1 = problems found (the report names each one and its exact fix), 2 = config error.
- matmelt.commands.doctor.run_doctor(config_path, *, as_json=False)[source]#
Diagnose the environment a config describes.
- Parameters:
- Returns:
0when every check passed,1when any problem was found,2for a config error.- Return type:
Notes
Three things are checked that the import probes alone cannot see: backend reachability, the screen stage’s
model_path(config, not an importable dependency – and stat-ed where the stage runs), and, only when acalculatestage is configured, whether its AiiDA profile loads and its code label resolves.Stage readiness is probed on the interpreter that will actually run each runner, so a remotely-wired stage is checked on its remote and any install command is ssh-prefixed accordingly.
The retry command: user-driven recovery of FAILED materials (M4).
FAILED is terminal – a material that exhausted its retry budget never
re-runs on its own. This command lets a user restart such materials from the
CLI instead of hand-editing state.sqlite: each selected FAILED material is
reset to the live state it was claimable from before it failed (the output
state of its last completed stage), so the next matmelt run re-claims
and re-runs the stage it died on. No item-dir files are touched (the stage
re-runs and overwrites) and the completions timeline is preserved.
Selection (all combinable, AND-ed; default = every FAILED material):
* --stage <name> – only materials that failed at that funnel stage.
* --materials / --materials-file – only the named material ids.
Safety: a preview clusters the matches by (stage, reason) and the command asks
for confirmation before mutating (--yes skips it; --dry-run previews and
exits without changing anything). --json emits the plan/result and, being
non-interactive, applies only with --yes.
Exit codes: 0 = done (including a clean decline or an empty match), 1 = no run state exists yet, 2 = config or usage error.
- matmelt.commands.retry.run_retry(config_path, *, materials=None, materials_file=None, stage=None, dry_run=False, assume_yes=False, as_json=False)[source]#
Reset selected failed materials so a later run re-runs them.
- Parameters:
config_path (
str) – The config file naming the workspace.materials (
str|None, default:None) – Comma-separated ids to retry.materials_file (
str|None, default:None) – A file of ids, one per line. Combines withmaterials.stage (
str|None, default:None) – Retry only materials that failed at this stage.dry_run (
bool, default:False) – Show the preview and change nothing.assume_yes (
bool, default:False) – Skip the confirmation prompt.as_json (
bool, default:False) – Emit a JSON payload. Applies only together withassume_yes, since a prompt on stdout would corrupt the payload.
- Returns:
0on success, including when nothing matched or the user declined;1when there is no run state;2for a config error, an unknown stage, or an unusable selection.- Return type:
Notes
Each material resumes at the state its own timeline implies, so a retry re-runs the stage that failed rather than the whole funnel, and the completions timeline is preserved. The flow is preview, then prompt, then result – a user sees what will change before agreeing to it.
The cif command: export CIF files for calculated materials (M10).
A material that passed the calculate stage has its DFTB+ result in
calc_result.json – but not the geometry DFTB+ relaxed to. That
geometry (geom.out.gen) lives only in the user’s AiiDA repository, so
this command:
picks the materials that passed calculate (state
CALCULATEDor beyond, with aconvergedcalc_result.json), optionally narrowed by--materials/--materials-file;fetches their relaxed geometries in one batched call to
cif_runner.py, run under the calculate stage’s ownpython– the interpreter the user’s AiiDA lives in (theaiida_statusprecedent; this package never imports aiida);writes
<out-dir>/<id>.cif– P1, with the WorkChain/CalcJob pks and the headline results as_mcli_*provenance items.
Default output is <work_dir>/cifs/; item directories keep holding stage
artifacts only, and the exported set is one folder to open in VESTA or hand
to a database.
Materials that never converged (or never reached calculate) are skipped and counted, not failed – they simply have no calculated structure yet. A material whose geometry cannot be fetched is a failure, reported with its reason.
Exit codes: 0 = every eligible material exported (including “nothing matched”), 1 = no run state / the fetch failed / some materials could not be exported, 2 = config or usage error.
- matmelt.commands.cif.RUNNER_PATH = PosixPath('/home/wisdom/Desktop/ARISE/LAB_ARISE/MVP_CLI_V0/src/matmelt/core/stages/cif_runner.py')#
The self-contained fetch runner, run under the user’s AiiDA interpreter.
- matmelt.commands.cif.run_cif(config_path, *, materials=None, materials_file=None, out_dir=None, as_json=False)[source]#
Export CIF files for the materials that passed the calculate stage.
- Parameters:
config_path (
str) – The config file naming the workspace. It must configure acalculatestage: without one, no material has a DFTB+ relaxed geometry to export.materials (
str|None, default:None) – Comma-separated ids to export.materials_file (
str|None, default:None) – A file of ids, one per line. Combines withmaterials. Selection is by id only – an export is a flat set of files, not a lineage traversal.out_dir (
str|None, default:None) – Where the CIFs go. Defaults to<work_dir>/cifs.as_json (
bool, default:False) – Emit a JSON payload on stdout instead of Rich output.
- Returns:
0when the export completed, even with per-material skips or failures;1when nothing could be exported at all;2for a config error, a missing calculate stage, or an unusable selection.- Return type:
Notes
Eligibility is state
CALCULATEDor beyond and a convergedcalc_result.json. Anything else is a counted skip with a stated reason, never a failure: a material that did not converge simply has no calculated structure to export.Geometries come from the result document’s cache when a recent enough calculate run wrote one, and only the remainder is fetched through AiiDA – so a fully-cached workspace spawns no AiiDA process at all.
The store is opened read-only.
The update command: upgrade MatMelt in place (5.2b).
The counterpart of install.sh: the installer records what it installed
into <venv>/matmelt.spec (a PyPI name, a git URL, or a local
path), and this command re-runs pip install --upgrade <that spec> with
the interpreter it is running under – an update always comes from the same
source the install did, never a guessed one.
A copy that was not installed by install.sh (a dev checkout, a plain
pip install, a system python) has no spec file, and upgrading it here
would be a guess about how the user manages that environment – refused with
the right command for each case named instead.
Exit codes: 0 = updated, 1 = refused or pip failed.
- matmelt.commands.update.SPEC_FILENAME = 'matmelt.spec'#
Written by install.sh next to the venv’s bin/ – the update contract.
- matmelt.commands.update.run_update()[source]#
Upgrade this installation from the spec its installer recorded.
- Returns:
pip’s own exit code on an attempted upgrade, or
1when this installation cannot be updated this way – a system Python, or a venv with no recorded spec, each refused with the command that would work.- Return type:
Notes
The new version is reported by a fresh interpreter: this process is still running the old code, so asking it would print the version that was just replaced.