Source code for prop_sapt.cli

"""
Command-line interface of propSAPT::

    prop-sapt run CONFIG GEOMETRY [GEOMETRY ...] [--memory 32 --threads 8 ...]
    prop-sapt basis fetch SOURCE NAME --elements He,Li [--dir basis]

`run` runs the tasks of a configuration file (see `prop_sapt.config`) for each geometry in
turn; flags override the values in the file. `basis fetch` writes a basis set missing from
Psi4's library into a file for `basis_dirs` (see `prop_sapt.basis_fetch`).
Exit status: 0 on success, 1 if a calculation failed, 2 for an invalid configuration,
command line or basis set request.
"""

import argparse
import json
import sys
import traceback
from pathlib import Path

from .config import ConfigError, OVERRIDE_KEYS, load_config, resolve_config


[docs] def build_parser() -> argparse.ArgumentParser: """Builds the `prop-sapt` argument parser.""" parser = argparse.ArgumentParser( prog="prop-sapt", description="Run propSAPT calculations.", ) subparsers = parser.add_subparsers(dest="command", required=True) run_parser = subparsers.add_parser( "run", help="Run the tasks of a configuration file for one or more dimer geometries.", description=( "Run the tasks of a YAML or JSON configuration file for each dimer geometry " "in turn, stopping at the first failure. Flags override the file." ), ) run_parser.add_argument( "config", help="Path to the YAML or JSON configuration file." ) run_parser.add_argument( "geometries", nargs="+", metavar="GEOMETRY", help="Path to a dimer geometry file in Psi4 format (e.g. .p4m).", ) run_parser.add_argument( "--memory", help="Memory in GB, or a string such as '32 GB' or '500 MB'." ) run_parser.add_argument("--threads", type=int, help="Number of threads.") run_parser.add_argument("--reference", help="Reference: RHF or RKS.") run_parser.add_argument("--functional", help="DFT functional (RKS only).") run_parser.add_argument( "--grac-a", dest="grac_A", type=float, help="GRAC shift of monomer A in Hartree (RKS only).", ) run_parser.add_argument( "--grac-b", dest="grac_B", type=float, help="GRAC shift of monomer B in Hartree (RKS only).", ) run_parser.add_argument("--basis", help="Orbital basis set.") run_parser.add_argument( "--df-basis", dest="df_basis", help="Density-fitting basis stem, extended with -jkfit and -ri (default: --basis).", ) run_parser.add_argument( "--output-dir", dest="output_dir", help="Output directory; '{geometry}' expands to the geometry file stem.", ) run_parser.add_argument( "--dry-run", action="store_true", help="Print the resolved configuration and the Psi4 options, then exit.", ) _add_basis_parser(subparsers) return parser
def _add_basis_parser(subparsers) -> None: """Adds the `prop-sapt basis` commands.""" from .basis_fetch import SOURCES basis_parser = subparsers.add_parser( "basis", help="Manage basis set files for the 'basis_dirs' of a run configuration.", ) basis_subparsers = basis_parser.add_subparsers(dest="basis_command", required=True) fetch_parser = basis_subparsers.add_parser( "fetch", help="Write a basis set missing from Psi4's library into a Psi4-format file.", description=( "Fetch a basis set from ccRepo (needs ccrepo_utility) or the Basis Set Exchange " "(needs basis_set_exchange) and write it into DIR/<name>.gbs, for use through " "'basis_dirs'. Run it once where those packages are installed; calculations " "then read the file." ), ) fetch_parser.add_argument( "source", choices=sorted(SOURCES), help="Where to fetch from." ) fetch_parser.add_argument( "name", help="Basis set name at the source, e.g. aug-cc-pVTZ_MP2FIT or def2-universal-jkfit.", ) fetch_parser.add_argument( "--elements", required=True, help="Comma-separated element symbols, e.g. He,Li." ) fetch_parser.add_argument( "--dir", dest="directory", default="basis", help="Directory to write into (default: basis).", ) fetch_parser.add_argument( "--as", dest="as_name", help="Basis set name to use in propSAPT, which also names the file (default: SOURCE-NAME).", ) fetch_parser.add_argument( "--force", action="store_true", help="Overwrite an existing file." )
[docs] def main(argv: list[str] | None = None) -> int: """Entry point of the `prop-sapt` command. Args: argv (list[str] | None, optional): Command-line arguments without the program name. Defaults to `sys.argv[1:]`. Returns: int: Exit status. """ args = build_parser().parse_args(argv) if args.command == "run": return _run_command(args) if args.command == "basis": return _basis_fetch_command(args) return 2 # unreachable: argparse requires a known command
def _run_command(args: argparse.Namespace) -> int: """Runs `prop-sapt run`.""" overrides = {key: getattr(args, key) for key in OVERRIDE_KEYS} try: config_path = Path(args.config) config = resolve_config( load_config(config_path), base_dir=config_path.resolve().parent, overrides=overrides, source=str(config_path), ) missing = [ geometry for geometry in args.geometries if not Path(geometry).is_file() ] if missing: raise ConfigError(f"Geometry file(s) not found: {', '.join(missing)}.") # every geometry writes the same filenames, so each needs its own directory output_dirs = [ Path(config.output_dir(Path(geometry).stem)).resolve() for geometry in args.geometries ] if len(set(output_dirs)) < len(output_dirs): raise ConfigError( "Several geometries would write into the same output directory; " "put '{geometry}' in 'output.dir' (or --output-dir) and use distinct " "geometry file names." ) except ConfigError as err: print(f"prop-sapt: error: {err}", file=sys.stderr) return 2 if args.dry_run: summary = { "config": config.to_dict(), "psi4_options": config.effective_psi4_options(), "geometries": [ { "geometry_file": str(Path(geometry).resolve()), "output_dir": str( Path(config.output_dir(Path(geometry).stem)).resolve() ), } for geometry in args.geometries ], } print(json.dumps(summary, indent=2)) return 0 from .runner import run for geometry in args.geometries: try: run(config, geometry) except Exception: traceback.print_exc() print(f"prop-sapt: calculation failed for '{geometry}'.", file=sys.stderr) return 1 return 0 def _basis_fetch_command(args: argparse.Namespace) -> int: """Runs `prop-sapt basis fetch`.""" from .basis_fetch import BasisFetchError, fetch_basis_file try: path, basis_name = fetch_basis_file( args.source, args.name, args.elements.split(","), directory=args.directory, as_name=args.as_name, force=args.force, ) except BasisFetchError as err: print(f"prop-sapt: error: {err}", file=sys.stderr) return 2 lower = basis_name.lower() if "jkfit" in lower: option = "DF_BASIS_SCF" elif "mp2fit" in lower or "rifit" in lower or lower.endswith("-ri"): option = "DF_BASIS_SAPT" else: option = "BASIS" element = args.elements.split(",")[0].strip().capitalize() print(f"Wrote {path}") print( f"Use it as basis set '{basis_name}' with the directory listed in the configuration" ) print(f"(relative to the configuration file), e.g. for {element}:") print(f" basis_dirs: [{path.parent}]") print(f" basis_overrides: {{{element}: {{{option}: {basis_name}}}}}") return 0