"""CLI: extract converged magnetic moments from a tree of WIEN2k ``.scf`` files.
Layer: **WIEN2k-specific**.
Beyond tabulating the per-site moments, it **checks that every configuration
reached its intended magnetic state** — a config whose moment collapsed to ~0
(non-magnetic solution) or drifted far from the generated ``|MAGMOM| = 2·S``
has an unreliable total energy and would poison a J / Jring extraction.
"""
from __future__ import annotations
import argparse
import os
import re
import sys
from mag4.analysis.wien2k import (
build_table,
find_cases,
find_report,
parse_report_target_moment,
)
def _build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="mag4-moments",
description="Tabulate WIEN2k per-site magnetic moments across "
"configurations and verify each reached its target state.")
p.add_argument("atoms", nargs="*",
help="Atom-index range 'FIRST LAST' (e.g. 33 47), "
"optionally followed by explicit config directories. "
"Omit to be prompted for the range.")
p.add_argument("--code", choices=["wien2k", "vasp"], default="wien2k",
help="DFT code to read moments from: 'wien2k' → per-sphere "
":MMI: in <dir>/<name>.scf (default); 'vasp' → per-ion "
"'magnetization (x)' block in <dir>/OUTCAR "
"(needs LORBIT=10/11).")
p.add_argument("--scffile", default=None, metavar="NAME",
help="WIEN2k only. Basename (with or without '.scf') of the "
"SCF file inside each config directory — use when runs "
"are saved side by side as e.g. pbe.scf / r2scan.scf. "
"Default: <dirname>/<dirname>.scf.")
p.add_argument("--moment", type=float, default=None, metavar="MU",
help="Expected per-site |moment| in μ_B (= the generated "
"|MAGMOM| = 2·S). Each config is flagged if any site "
"is off this by more than --tol.")
p.add_argument("--spin", type=float, default=None, metavar="S",
help="Spin S; sets the expected |moment| to 2·S when "
"--moment is not given.")
p.add_argument("--tol", type=float, default=0.5, metavar="MU",
help="Tolerance (μ_B) for the target check (default 0.5).")
return p
[docs]
def main(argv=None) -> int:
args = _build_parser().parse_args(argv)
# Positional parsing: leading integers are the atom range; the rest are
# explicit config directories (backward compatible with the old CLI).
atom_first, atom_last, explicit_dirs, num_count = None, None, [], 0
for a in args.atoms:
if num_count < 2 and re.match(r"^\d+$", a):
if num_count == 0:
atom_first = int(a)
else:
atom_last = int(a)
num_count += 1
else:
explicit_dirs.append(a)
if atom_first is None:
atom_first = int(input(" First atom index (e.g. 9): ").strip())
if atom_last is None:
atom_last = int(input(" Last atom index (e.g. 16): ").strip())
atom_indices = list(range(atom_first, atom_last + 1))
base_dir = "."
# Target per-site |moment| = |MAGMOM| = 2·S. Priority: --moment, then
# --spin, then the generated value recorded in the report (four_state_report
# .dat carries "MAGMOM (S = …, |MAGMOM| = 2·S = …)").
target = args.moment
if target is None and args.spin is not None:
target = 2.0 * args.spin
if target is None:
rpt = find_report(base_dir)
if rpt is not None:
target = parse_report_target_moment(rpt)
if target is not None:
print(f" Target |μ| = {target:g} μ_B "
f"(2·S, read from {os.path.basename(rpt)})")
if target is None:
print(" Note: no target moment (pass --moment/--spin, or run in the "
"compound dir with its report) — checking for missing data only.")
if explicit_dirs:
cases = explicit_dirs
else:
cases = find_cases(base_dir, scf_basename=args.scffile, code=args.code)
if not cases:
where = ("an 'OUTCAR'" if args.code == "vasp"
else f"a '{args.scffile}.scf'" if args.scffile
else "any '.scf'")
print(f"Error: no subdirectory under '{os.path.abspath(base_dir)}' "
f"contains {where} file.")
if args.code == "wien2k" and args.scffile:
print(" Check the --scffile basename, or drop it to auto-scan "
"for <dirname>.scf.")
return 1
print(f"\n Auto-detected {len(cases)} configuration(s):")
for c in cases:
print(f" {os.path.basename(c)}")
compound = os.path.basename(os.path.abspath(base_dir))
output_file = f"{compound}_moments.txt"
lines, summary = build_table(cases, atom_indices,
scf_basename=args.scffile,
target=target, tol=args.tol, code=args.code)
with open(output_file, "w", encoding="utf-8") as fout:
for line in lines:
print(line)
fout.write(line + "\n")
print(f"\n Results saved to: {os.path.abspath(output_file)}")
# Exit code reflects the target check so scripts/pipelines can gate on it.
if summary["n_data"] == 0:
print(" ✗ No moments were read — every SCF file was missing "
"(see warnings).")
return 2
if summary["failures"]:
return 3
return 0
if __name__ == "__main__": # pragma: no cover
sys.exit(main())