Source code for matmelt.commands.init

"""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.
"""

from __future__ import annotations

from pathlib import Path

from matmelt.commands.doctor import run_doctor
from matmelt.ui.console import render_error, render_init_created

#: The starter config: 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.
TEMPLATE = """\
# MatMelt configuration (scaffolded by `matmelt init`).
# Any string value may be an environment reference: "env:NAME".

run:
  name: my-first-run
  work_dir: ./materials-work      # run workspace: state DB + per-material dirs
  elements: [Na, Cl]              # the element set the funnel explores
  # combination_sizes: [2, 3]     # [2] = binaries, [2, 3] adds ternaries, ...;
  #                               # 1 = single elements (elemental solids).
  #                               # Omitted = ALL sizes 1..len(elements).

pipeline:
  stop_after: extract             # last stage this run executes. A mid-funnel
  #                               # stop is a pause: materials rest at that
  #                               # stage's output, resumable by a later run --
  #                               # only the funnel's last stage retires DONE.
  retry_budget: 3                 # per-stage fault retries before FAILED
  # lease_timeout_s: 900          # orphaned-claim recovery age (seconds)
  # pass_interval_s: 1.0          # sleep between orchestrator passes

backends:                         # the WHERE: machines that run jobs
  laptop:
    kind: local
    options:
      work_dir: ./materials-jobs  # backend job scratch
      max_parallel: 2             # jobs in flight at once
  # A remote over SSH (inputs are staged per job; the remote only needs the
  # science packages in remote_python's venv -- never this tool):
  # cluster:
  #   kind: ssh_exec
  #   options:
  #     host: cluster.example.org
  #     user: env:CLUSTER_USER
  #     remote_python: /opt/science-venv/bin/python
  #     max_parallel: 4

stages:                           # the WHAT: the funnel, in order
  compose:                        # SMACT charge-balanced compositions
    backend: laptop
    options:
      python: python3             # interpreter with smact installed
      threshold: 2                # max SMACT stoichiometric coefficient
      # max_unary_atoms: 4        # single-element seeds emit X1..Xn allotrope
      #                           # stoichiometries (default: threshold)
  structure:                      # PyXtal symmetry-constrained candidates
    backend: laptop
    options: {python: python3}    # interpreter with pyxtal installed
  screen:                         # MACE/ASE variable-cell relaxation
    backend: laptop
    options:
      python: python3             # interpreter with mace-torch installed
      device: cpu                 # cpu | cuda
      # model_path: /path/to/mace.model  # unset = MACE-MP foundation model
      #                                  # (offered as a consented download)
  # calculate:                    # OPTIONAL higher-fidelity DFTB+ stage.
  #   backend: laptop             # Runs on YOUR OWN AiiDA: the stage's python
  #   options:                    # needs aiida-core + aiida-mydftbp installed,
  #                               # and a running AiiDA profile + daemon.
  #     python: python3           #   interpreter with aiida-core + aiida-mydftbp
  #     code_label: dftbplus@cluster   # REQUIRED: `verdi code list` shows yours
  #     skf_prefix: /opt/skf/     # REQUIRED: abs dir of .skf files where DFTB+ runs
  #     # profile: my-profile     #   AiiDA profile (unset = your default)
  #     # skf_local_path: ./skf   #   local .skf dir to create the SKF node from
  #     # skf_label: ptbp_complete_set   # label of the stored SKF FolderData
  #     # kpoints: [4, 4, 4]      #   SupercellFolding k-mesh
  #     # max_wallclock_seconds: 3600
  #     # queue_name: dftb        #   scheduler queue/partition
  extract:                        # OPTIMADE-shaped record.json (stdlib only)
    backend: laptop
"""


[docs] def run_init(config_path: str) -> int: """Scaffold a starter config, then diagnose it. Parameters ---------- config_path Where to write. An existing file is never overwritten. Returns ------- int ``1`` when the file exists or cannot be written; otherwise whatever ``doctor`` concludes about the freshly-written config. 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. """ path = Path(config_path) if path.exists(): render_error( f"init: {path} already exists; refusing to overwrite it. " f"Edit it directly, or pass --config to scaffold elsewhere." ) return 1 try: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(TEMPLATE, encoding="utf-8") except OSError as exc: render_error(f"init: cannot write {path}: {exc}") return 1 render_init_created(str(path)) return run_doctor(config_path)