Preflight and diagnostics#

Proving a run can work before it starts — what doctor renders.

Dependency preflight: prove each stage is runnable BEFORE any work starts (3.6).

The machinery behind three user-facing promises (policy 2026-07-08): the science stack is user-owned and never bundled; init/run tell the user exactly what a run needs and the exact install command for anything missing, before the first job; doctor shows per-stage readiness. This module is the thin hook those (Phase-5) commands call – it computes, it never renders (all human output lives in ui/console.py) and it never raises for a missing dependency: absence is a result here, not an error.

Checks probe each stage’s declared runner interpreter (stage.runner_python) with a subprocess import – the orchestrator’s own venv is deliberately irrelevant, because the runner executes in the user’s backend python, which is where the science stack must live. Without a probe, a missing package surfaces only as N failed jobs with exit-3 stderr; with it, the run refuses up front with one actionable line per problem.

Also here: the seed-enumeration report (how many combinations the configured elements x combination_sizes fan out into – the n-ary blast radius, visible before any work) and the MACE model status/download helpers (the consent itself belongs to the interactive command; download_foundation_model documents that it must never be called without it).

matmelt.core.preflight.MACE_CACHE_DIR = '~/.cache/mace'#

Where mace caches downloaded foundation models (its own convention).

class matmelt.core.preflight.RequirementStatus(stage, python, requirement, satisfied, detail, install_command=None, where='local')[source]#

Bases: object

One stage requirement, checked against the stage’s runner interpreter.

stage#

The stage that declared the requirement.

Type:

str

python#

The interpreter probed – the stage’s runner_python, never ours.

Type:

str

requirement#

What was checked. None marks the single entry a stage with no requirements yields: “needs nothing beyond the standard library” is a checked, reportable fact, not an omission.

Type:

Requirement or None

satisfied#

Whether the probe succeeded.

Type:

bool

detail#

Human-readable evidence – the import’s own last error line when it failed.

Type:

str

install_command#

The exact fix when unsatisfied: <python> -m pip install <name>, ssh-prefixed when the probe ran on a remote. None when satisfied.

Type:

str or None

where#

Where the probe actually ran – "local", or the remote backend’s target. An answer is only as good as its location.

Type:

str

matmelt.core.preflight.check_import(python, module)[source]#

Ask whether python can import module. Never raises.

Probes via <python> -c "import <module>", so the answer is about the runner’s interpreter rather than ours.

Parameters:
  • python (str) – The interpreter to probe.

  • module (str) – The import name to try.

Returns:

(satisfied, detail). A missing interpreter, a failing import, and a probe that hung past _PROBE_TIMEOUT_S all come back as (False, why) – absence is a result here, never an exception.

Return type:

tuple

matmelt.core.preflight.preflight_stages(stages, stage_backends=None)[source]#

Check every stage’s declared requirements, one entry per requirement.

Pure bookkeeping over the probes – no rendering, no raising. The caller (run’s gate, doctor’s table) decides what an unsatisfied entry means.

Parameters:
  • stages (Sequence[Stage]) – The stages to check, in any order.

  • stage_backends (Mapping[str, ComputeBackend] | None, default: None) – The Runtime mapping of stage name to backend, which makes the probe location-aware: a stage wired to a backend exposing the duck-typed check_remote_import(python, module) hook (ssh_exec does) is probed on that remote – the science stack must exist in the interpreter that will actually run the staged runner, not in ours – and its install command is ssh-prefixed accordingly. Without the mapping, or for a backend without the hook, the probe runs locally, which is exactly right for the local backend.

Returns:

One entry per declared requirement, plus one requirement=None entry for each stage that declares none.

Return type:

list of RequirementStatus

matmelt.core.preflight.is_ready(statuses)[source]#

Decide whether a run may start.

Parameters:

statuses (Sequence[RequirementStatus]) – What preflight_stages returned.

Returns:

True when every checked requirement is satisfied. An empty list is vacuously ready.

Return type:

bool

class matmelt.core.preflight.BackendHealth(backend, kind, healthy, detail)[source]#

Bases: object

One configured backend’s reachability, as reported by health_check.

backend#

The backend’s configured name.

Type:

str

kind#

The implementing class’s name, which is what doctor shows as the backend’s type.

Type:

str

healthy#

Whether it can take work right now.

Type:

bool

detail#

The backend’s own explanation, secret-free by contract.

Type:

str

matmelt.core.preflight.check_backends(backends)[source]#

Ask every configured backend whether it is usable. Never raises.

Parameters:

backends (Mapping[str, ComputeBackend]) – The configured backends by name. Iteration order is the mapping’s, which is the config file’s.

Returns:

One entry per backend, in that same order. health_check never raises by contract; a backend that breaks the contract anyway is reported unhealthy with the violation named – doctor must never crash on the thing it exists to diagnose.

Return type:

list of BackendHealth

class matmelt.core.preflight.DoctorReport(backends=<factory>, requirements=<factory>)[source]#

Bases: object

Everything doctor shows: backend reachability + per-stage readiness.

backends#

One entry per configured backend, in config order.

Type:

list of BackendHealth

requirements#

One entry per declared stage requirement.

Type:

list of RequirementStatus

property ok: bool#

Whether the environment is fit to run.

Returns:

True when every backend is healthy and every requirement is satisfied. It is what the doctor command’s exit code is built from.

Return type:

bool

matmelt.core.preflight.doctor_report(stages, stage_backends, backends)[source]#

Diagnose the whole environment in one call – what doctor renders.

Parameters:
Returns:

Backend reachability first – an unreachable remote also explains its stages’ failed remote probes – then location-aware stage readiness.

Return type:

DoctorReport

class matmelt.core.preflight.EnumerationReport(n_elements, per_size, total, oversized)[source]#

Bases: object

How many seeds elements x combination_sizes fan out into.

Mirrors core.seeds.element_combinations exactly, elements deduped and oversized sizes yielding zero.

n_elements#

How many distinct elements were configured.

Type:

int

per_size#

{size: count} for each requested combination size.

Type:

dict

total#

How many seeds a run would enqueue – the n-ary blast radius, visible before any work starts.

Type:

int

oversized#

Configured sizes larger than the element set. These are silent no-ops during enumeration, and are named here so the user learns why, say, [2, 5] over three elements produced only pairs.

Type:

tuple of int

matmelt.core.preflight.enumeration_report(elements, sizes)[source]#

Count the seeds per combination size without materialising them.

Parameters:
  • elements (Sequence[str]) – The configured element symbols; duplicates are ignored.

  • sizes (Sequence[int]) – The configured combination sizes.

Returns:

Counts computed with math.comb, so a large element set is reported instantly rather than enumerated.

Return type:

EnumerationReport

Examples

>>> report = enumeration_report(["Na", "Cl", "O"], [2, 5])
>>> report.total, report.oversized
(3, (5,))
class matmelt.core.preflight.ModelStatus(ok, needs_consent, detail)[source]#

Bases: object

Where the screen stage’s MACE model will come from.

Three cases: configured and present is ok=True with nothing to do; configured but missing is ok=False, a config error to fix; not configured at all is ok=True with needs_consent=True.

ok#

Whether screening can proceed as configured.

Type:

bool

True when the MACE-MP foundation model would be downloaded on first use. The interactive command must surface detail and obtain consent – a download is never silent.

Type:

bool

detail#

The full disclosure or the exact problem, shown to the user verbatim.

Type:

str

matmelt.core.preflight.mace_model_status(model_path, model_size='medium', backend=None)[source]#

Classify the configured (or absent) MACE model path.

Parameters:
  • model_path (str) – The screen stage’s configured model_path. Empty means “use the foundation model”.

  • model_size (str, default: 'medium') – Which foundation model size the disclosure should name.

  • backend (ComputeBackend | None, default: None) – The backend the screen stage is wired to, which makes the check location-aware, mirroring preflight_stages. When it exposes the duck-typed check_remote_file(path) hook (ssh_exec does), the file is probed on that remote – the model lives in the remote’s cache, not on our disk. Without the hook, or without a backend, the check is a local stat, exactly right for the local backend.

Returns:

The classification, whose detail is written to be shown verbatim.

Return type:

ModelStatus

matmelt.core.preflight.download_foundation_model(python, model_size='medium', timeout_s=1800.0)[source]#

Fetch the MACE-MP foundation model into the mace cache, via the user’s python.

The caller owns consent. This must only run after the user explicitly agreed to the download disclosed by mace_model_status – it is the mechanism behind that offer, never an automatic step.

Parameters:
  • python (str) – The interpreter to download with – the one the screen runner will use, so the model lands in the cache that run will read.

  • model_size (str, default: 'medium') – Which foundation model to fetch.

  • timeout_s (float, default: 1800.0) – How long to allow. The default is generous: this is a hundreds-of-megabytes download.

Returns:

(ok, detail). Never raises. Loading mace_mp downloads the model when absent and is a no-op when already cached, so a success also verifies the cache. A missing mace is detected and the detail names pip install mace-torch.

Return type:

tuple

class matmelt.core.preflight.AiidaStatus(ok, detail)[source]#

Bases: object

Whether the calculate stage’s AiiDA configuration is usable.

The requirement probes already answer “is aiida importable?”. This is the config-level check they cannot see, following the mace-model precedent: does the named profile load, and does the configured DFTB+ code label exist? Both are probed in the stage’s own runner interpreter – the AiiDA install that will actually submit the WorkChains, not ours.

ok#

Whether the profile loads and the code is found.

Type:

bool

detail#

The problem to fix, or the confirmation. Never a secret: only profile and code labels, plus the last line of the probe’s own error.

Type:

str

matmelt.core.preflight.aiida_status(python, profile, code_label, timeout_s=60.0)[source]#

Probe load_profile + load_code via the runner interpreter. Never raises.

Intended to be called only when a calculate stage is configured; the caller folds ok into the doctor verdict.

Parameters:
  • python (str) – The calculate stage’s runner interpreter, whose AiiDA install is the one that will submit the WorkChains.

  • profile (str | None) – The AiiDA profile to load, or None for the user’s default.

  • code_label (str) – The configured DFTB+ code label to resolve.

  • timeout_s (float, default: 60.0) – How long to allow the probe.

Returns:

A missing aiida, an unknown profile, and an unknown code label all come back as ok=False with the reason, never as an exception.

Return type:

AiidaStatus