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 --config says otherwise – and the filename init scaffolds.

matmelt.commands.LEGACY_CONFIG_FILENAME = 'materials-cli.yaml'#

The pre-rename filename, scaffolded by materials-cli 0.1.x. A workspace created before the MatMelt rename keeps working untouched: see resolve_config_path().

matmelt.commands.STATE_DB_FILENAME = 'state.sqlite'#

The run’s state database, under run.work_dir. run creates it; status / results read 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.yaml to matmelt.yaml. A workspace scaffolded by materials-cli 0.1.x must not break, so when the caller took the default and no matmelt.yaml exists, an existing materials-cli.yaml beside it is used instead and a deprecation note is printed once, on stderr (--json stdout stays parseable).

An explicit --config is 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 in load_config, naming the new name).

Parameters:

config_path (str) – The path the command was given – the shared --config option.

Returns:

config_path unchanged, or materials-cli.yaml when that legacy file is the only one present and the default was requested.

Return type:

str

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 (doctor runs 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:

1 when the file exists or cannot be written; otherwise whatever doctor concludes about the freshly-written config.

Return type:

int

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_config and build_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:

  1. Config – load_config -> build_runtime + build_stages (exit 2 on any config problem, YAML paths included).

  2. Preflight gate – the enumeration report first (the n-ary blast radius is visible before anything else), then preflight_stages over the active stages only – the same stop_after prefix 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).

  3. Model consent – when the screen stage is active and no model_path is configured, the foundation-model download is disclosed and consent obtained (--yes for 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.

  4. Seed + drain – element_combinations seeds the store (idempotent – rerunning a finished or interrupted run is always safe), then a single-threaded drain loop: run_pass until every material is terminal, one progress line per pass with activity. --max-passes is 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 is compose – fresh NEW items would otherwise sit unconsumed forever – and the drain stops when the stage has no work left. Conflicts loudly with pipeline.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 with materials, and both combine freely with only. The selection covers each named material and its descendants, so fan-out children are eligible automatically.

Returns:

0 when 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. 1 when the environment is not ready or consent was declined; 2 for a config or usage error.

Return type:

int

Notes

Ctrl-C is a supported way to stop: in-flight jobs stay recorded, and the next run re-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 for run --materials and retry.

Returns:

0 on success, 1 when there is no run state yet, 2 for a config error or an unknown state name.

Return type:

int

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_exec does 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; None watches 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:

0 on a clean exit, Ctrl-C included; 1 when there is no run state; 2 for a config error, a non-positive interval, or an unusable tracking selection.

Return type:

int

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:
  • config_path (str) – The config file naming the workspace to read.

  • as_json (bool, default: False) – Emit a JSON payload on stdout instead of a Rich table.

  • export (str | None, default: None) – Write the collected records to this path as a JSON array, atomically.

Returns:

0 on success, 1 when there is no run state or the export could not be written, 2 for a config error.

Return type:

int

Notes

A DONE material with no record.json is first-class here, not an error: a run stopped before extract finishes 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:
  • config_path (str) – The config file to read.

  • as_json (bool, default: False) – Emit the report as a JSON payload on stdout instead of Rich tables.

Returns:

0 when every check passed, 1 when any problem was found, 2 for a config error.

Return type:

int

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 a calculate stage 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 with materials.

  • 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 with assume_yes, since a prompt on stdout would corrupt the payload.

Returns:

0 on success, including when nothing matched or the user declined; 1 when there is no run state; 2 for a config error, an unknown stage, or an unusable selection.

Return type:

int

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:

  1. picks the materials that passed calculate (state CALCULATED or beyond, with a converged calc_result.json), optionally narrowed by --materials / --materials-file;

  2. fetches their relaxed geometries in one batched call to cif_runner.py, run under the calculate stage’s own python – the interpreter the user’s AiiDA lives in (the aiida_status precedent; this package never imports aiida);

  3. 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 a calculate stage: 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 with materials. 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:

0 when the export completed, even with per-material skips or failures; 1 when nothing could be exported at all; 2 for a config error, a missing calculate stage, or an unusable selection.

Return type:

int

Notes

Eligibility is state CALCULATED or beyond and a converged calc_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 1 when 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:

int

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.