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:
objectOne stage requirement, checked against the stage’s runner interpreter.
- requirement#
What was checked.
Nonemarks the single entry a stage with no requirements yields: “needs nothing beyond the standard library” is a checked, reportable fact, not an omission.- Type:
RequirementorNone
- install_command#
The exact fix when unsatisfied:
<python> -m pip install <name>, ssh-prefixed when the probe ran on a remote.Nonewhen satisfied.
- matmelt.core.preflight.check_import(python, module)[source]#
Ask whether
pythoncan importmodule. Never raises.Probes via
<python> -c "import <module>", so the answer is about the runner’s interpreter rather than ours.- Parameters:
- Returns:
(satisfied, detail). A missing interpreter, a failing import, and a probe that hung past_PROBE_TIMEOUT_Sall come back as(False, why)– absence is a result here, never an exception.- Return type:
- 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) – TheRuntimemapping of stage name to backend, which makes the probe location-aware: a stage wired to a backend exposing the duck-typedcheck_remote_import(python, module)hook (ssh_execdoes) 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 thelocalbackend.
- Returns:
One entry per declared requirement, plus one
requirement=Noneentry for each stage that declares none.- Return type:
- matmelt.core.preflight.is_ready(statuses)[source]#
Decide whether a run may start.
- Parameters:
statuses (
Sequence[RequirementStatus]) – Whatpreflight_stagesreturned.- Returns:
Truewhen every checked requirement is satisfied. An empty list is vacuously ready.- Return type:
- class matmelt.core.preflight.BackendHealth(backend, kind, healthy, detail)[source]#
Bases:
objectOne configured backend’s reachability, as reported by
health_check.
- 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_checknever raises by contract; a backend that breaks the contract anyway is reported unhealthy with the violation named –doctormust never crash on the thing it exists to diagnose.- Return type:
- class matmelt.core.preflight.DoctorReport(backends=<factory>, requirements=<factory>)[source]#
Bases:
objectEverything
doctorshows: backend reachability + per-stage readiness.- backends#
One entry per configured backend, in config order.
- Type:
- requirements#
One entry per declared stage requirement.
- Type:
- matmelt.core.preflight.doctor_report(stages, stage_backends, backends)[source]#
Diagnose the whole environment in one call – what
doctorrenders.- Parameters:
stage_backends (
Mapping[str,ComputeBackend]) – Which backend runs each stage, so readiness is probed where the work will happen.backends (
Mapping[str,ComputeBackend]) – Every configured backend, checked for reachability.
- Returns:
Backend reachability first – an unreachable remote also explains its stages’ failed remote probes – then location-aware stage readiness.
- Return type:
- class matmelt.core.preflight.EnumerationReport(n_elements, per_size, total, oversized)[source]#
Bases:
objectHow many seeds
elementsxcombination_sizesfan out into.Mirrors
core.seeds.element_combinationsexactly, elements deduped and oversized sizes yielding zero.- total#
How many seeds a run would enqueue – the n-ary blast radius, visible before any work starts.
- Type:
- matmelt.core.preflight.enumeration_report(elements, sizes)[source]#
Count the seeds per combination size without materialising them.
- Parameters:
- Returns:
Counts computed with
math.comb, so a large element set is reported instantly rather than enumerated.- Return type:
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:
objectWhere the screen stage’s MACE model will come from.
Three cases: configured and present is
ok=Truewith nothing to do; configured but missing isok=False, a config error to fix; not configured at all isok=Truewithneeds_consent=True.- needs_consent#
Truewhen the MACE-MP foundation model would be downloaded on first use. The interactive command must surfacedetailand obtain consent – a download is never silent.- Type:
- 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 configuredmodel_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, mirroringpreflight_stages. When it exposes the duck-typedcheck_remote_file(path)hook (ssh_execdoes), 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 localstat, exactly right for thelocalbackend.
- Returns:
The classification, whose
detailis written to be shown verbatim.- Return type:
- 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. Loadingmace_mpdownloads the model when absent and is a no-op when already cached, so a success also verifies the cache. A missingmaceis detected and the detail namespip install mace-torch.- Return type:
- class matmelt.core.preflight.AiidaStatus(ok, detail)[source]#
Bases:
objectWhether 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.
- matmelt.core.preflight.aiida_status(python, profile, code_label, timeout_s=60.0)[source]#
Probe
load_profile+load_codevia the runner interpreter. Never raises.Intended to be called only when a
calculatestage is configured; the caller foldsokinto 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, orNonefor 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=Falsewith the reason, never as an exception.- Return type: