diff --git a/src/quacc/recipes/aims/__init__.py b/src/quacc/recipes/aims/__init__.py new file mode 100644 index 0000000000..0b30376781 --- /dev/null +++ b/src/quacc/recipes/aims/__init__.py @@ -0,0 +1 @@ +"""Recipes for FHI-Aims.""" diff --git a/src/quacc/recipes/aims/_base.py b/src/quacc/recipes/aims/_base.py new file mode 100644 index 0000000000..1cbfd2ae8c --- /dev/null +++ b/src/quacc/recipes/aims/_base.py @@ -0,0 +1,117 @@ +"""Base jobs for FHI-aims.""" + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +from ase.calculators.aims import Aims, AimsProfile + +from quacc import get_settings +from quacc.runners.ase import Runner +from quacc.schemas.ase import Summarize +from quacc.utils.dicts import recursive_dict_merge +from quacc.utils.kpts import kspacing_to_kpts + +if TYPE_CHECKING: + from typing import Any + + from ase.atoms import Atoms + + from quacc.types import Filenames, RunSchema, SourceDirectory + + +def run_and_summarize( + atoms: Atoms, + calc_defaults: dict[str, Any] | None = None, + calc_swaps: dict[str, Any] | None = None, + additional_fields: dict[str, Any] | None = None, + copy_files: SourceDirectory | dict[SourceDirectory, Filenames] | None = None, +) -> RunSchema: + """ + Base function to carry out FHI-aims recipes. + + Parameters + ---------- + atoms + Atoms object + calc_defaults + The default calculator parameters. + calc_swaps + Custom kwargs for the FHI-aims calculator. Set a value to + `quacc.Remove` to remove a pre-existing key entirely. For a list of available + keys, refer to the [ase.calculators.aims.Aims][] calculator. + additional_fields + Any additional fields to supply to the summarizer. + copy_files + Files to copy (and decompress) from source to the runtime directory. + + Returns + ------- + RunSchema + Dictionary of results from [quacc.schemas.ase.Summarize.run][] + """ + calc = prep_calculator(atoms, calc_defaults=calc_defaults, calc_swaps=calc_swaps) + final_atoms = Runner(atoms, calc, copy_files=copy_files).run_calc() + + return Summarize(move_magmoms=True, additional_fields=additional_fields).run( + final_atoms, atoms + ) + + +def prep_calculator( + atoms: Atoms, + calc_defaults: dict[str, Any] | None = None, + calc_swaps: dict[str, Any] | None = None, +) -> Aims: + """ + Prepare the FHI-aims calculator. + + Parameters + ---------- + atoms + Atoms object + calc_defaults + The default calculator parameters. + calc_swaps + Custom kwargs for the FHI-aims calculator. Set a value to + `quacc.Remove` to remove a pre-existing key entirely. For a list of available + keys, refer to the [ase.calculators.aims.Aims][] calculator. + + Returns + ------- + Aims + The FHI-aims calculator. + """ + calc_flags = recursive_dict_merge(calc_defaults or {}, calc_swaps or {}) + settings = get_settings() + species_dir = calc_flags.pop("species_dir", None) + + if not any(atoms.pbc): + for key in ["kspacing", "k_grid", "k_grid_density"]: + if key in calc_flags: + calc_flags.pop(key) + elif ( + "kspacing" in calc_flags + and "k_grid" not in calc_flags + and "k_grid_density" not in calc_flags + ): + kspacing = calc_flags.pop("kspacing") + calc_flags["k_grid"] = kspacing_to_kpts(atoms, kspacing) + + if "spin" not in calc_flags and hasattr(atoms, "get_initial_magnetic_moments"): + magmoms = atoms.get_initial_magnetic_moments() + if magmoms is not None and any(abs(m) > 1e-6 for m in magmoms): + calc_flags["spin"] = "collinear" + + aims_cmd = f"{settings.AIMS_PARALLEL_CMD} {settings.AIMS_BIN}" + species_path = settings.AIMS_SPECIES_DEFAULTS + if species_dir: + species_path = Path(species_path) / species_dir + + return Aims( + profile=AimsProfile( + command=aims_cmd.strip(), default_species_directory=str(species_path) + ), + **calc_flags, + ) diff --git a/src/quacc/recipes/aims/core.py b/src/quacc/recipes/aims/core.py new file mode 100644 index 0000000000..149c7f4249 --- /dev/null +++ b/src/quacc/recipes/aims/core.py @@ -0,0 +1,219 @@ +"""Core recipes for FHI-aims.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +from quacc import job +from quacc.atoms.core import check_is_metal +from quacc.recipes.aims._base import run_and_summarize + +if TYPE_CHECKING: + from typing import Any + + from ase.atoms import Atoms + + from quacc.types import Filenames, RunSchema, SourceDirectory + + +BASE_SET_METAL = { + "occupation_type": "cold 0.1", + "relativistic": "atomic_zora scalar", + "charge_mix_param": 0.05, + "mixer": "pulay", + "n_max_pulay": 14, + "xc": "pbe", + "output_level": "normal", +} + +BASE_SET_AGNOSTIC = { + "relativistic": "atomic_zora scalar", + "mixer": "pulay", + "xc": "pbe", + "output_level": "normal", +} + +BASE_SET_NON_METAL = { + "occupation_type": "gaussian 0.01", + "relativistic": "atomic_zora scalar", + "xc": "pbe", + "charge_mix_param": 0.20, + "mixer": "pulay", + "output_level": "normal", +} + +KSPACING_METAL = 0.033 +KSPACING_AGNOSTIC = 0.033 +KSPACING_NON_METAL = 0.045 + + +@job +def static_job( + atoms: Atoms, + species_defaults: Literal[ + "light", "intermediate", "tight", "really_tight" + ] = "intermediate", + kspacing: float | None = None, + spin: Literal["none", "collinear", "non-collinear"] | None = None, + copy_files: SourceDirectory | dict[SourceDirectory, Filenames] | None = None, + additional_fields: dict[str, Any] | None = None, + agnostic_params: bool = False, + **calc_kwargs, +) -> RunSchema: + """ + Function to carry out a basic SCF calculation with FHI-aims. + + Parameters + ---------- + atoms + The Atoms object. + species_defaults + The level of accuracy for the basis set and integration grids. + Options: "light", "intermediate", "tight", "really_tight". + Default is "intermediate" which is suitable for production SCF calculations. + kspacing + The kpoint spacing in Å^-1. If not provided, defaults to + KSPACING_METAL (0.033) for metals and KSPACING_NON_METAL (0.045) for non-metals. + Ignored for aperiodic systems. + spin + Spin treatment. Options are "none", "collinear", or "non-collinear". + Default is None, which will automatically set to "collinear" if magnetic + moments are detected in the atoms object. + copy_files + Files to copy (and decompress) from source to the runtime directory. + additional_fields + Additional fields to add to the results dictionary. + agnostic_params + If True, uses minimal settings (no occupation_type or charge_mix_param) + letting FHI-aims determine these automatically. If False (default), + auto-detects whether the system is metallic or not. + **calc_kwargs + Custom kwargs for the FHI-aims calculator. For a list of available + keys, refer to the [ase.calculators.aims.Aims][] calculator. + + Returns + ------- + RunSchema + Dictionary of results, specified in [quacc.schemas.ase.Summarize.run][]. + See the type-hint for the data structure. + """ + if agnostic_params: + calc_defaults = BASE_SET_AGNOSTIC.copy() + default_kspacing = KSPACING_AGNOSTIC + else: + calc_defaults = ( + BASE_SET_METAL.copy() + if check_is_metal(atoms) + else BASE_SET_NON_METAL.copy() + ) + default_kspacing = ( + KSPACING_METAL if check_is_metal(atoms) else KSPACING_NON_METAL + ) + + calc_defaults["species_dir"] = species_defaults + + if kspacing is not None: + calc_defaults["kspacing"] = kspacing + else: + calc_defaults["kspacing"] = default_kspacing + + if spin is not None: + calc_defaults["spin"] = spin + + return run_and_summarize( + atoms, + calc_defaults=calc_defaults, + calc_swaps=calc_kwargs, + additional_fields={"name": "FHI-aims Static"} | (additional_fields or {}), + copy_files=copy_files, + ) + + +@job +def relax_job( + atoms: Atoms, + species_defaults: Literal[ + "light", "intermediate", "tight", "really_tight" + ] = "light", + kspacing: float | None = None, + spin: Literal["none", "collinear", "non-collinear"] | None = None, + relax_cell: bool = False, + copy_files: SourceDirectory | dict[SourceDirectory, Filenames] | None = None, + additional_fields: dict[str, Any] | None = None, + agnostic_params: bool = False, + **calc_kwargs, +) -> RunSchema: + """ + Function to carry out a structure relaxation with FHI-aims internal optimizer. + + Parameters + ---------- + atoms + The Atoms object. + species_defaults + The level of accuracy for the basis set and integration grids. + Options: "light", "intermediate", "tight", "really_tight". + Default is "light" which is suitable for geometry optimizations. + kspacing + The kpoint spacing in Å^-1. If not provided, defaults to + KSPACING_METAL (0.033) for metals and KSPACING_NON_METAL (0.045) for non-metals. + Ignored for aperiodic systems. + spin + Spin treatment. Options are "none", "collinear", or "non-collinear". + Default is None, which will automatically set to "collinear" if magnetic + moments are detected in the atoms object. + relax_cell + Whether to relax the cell or not. + copy_files + Files to copy (and decompress) from source to the runtime directory. + additional_fields + Additional fields to add to the results dictionary. + agnostic_params + If True, uses minimal settings (no occupation_type or charge_mix_param) + letting FHI-aims determine these automatically. If False (default), + auto-detects whether the system is metallic or not. + **calc_kwargs + Custom kwargs for the FHI-aims calculator. For a list of available + keys, refer to the [ase.calculators.aims.Aims][] calculator. + + Returns + ------- + RunSchema + Dictionary of results from [quacc.schemas.ase.Summarize.run][]. + See the type-hint for the data structure. + """ + if agnostic_params: + calc_defaults = BASE_SET_AGNOSTIC.copy() + default_kspacing = KSPACING_AGNOSTIC + else: + calc_defaults = ( + BASE_SET_METAL.copy() + if check_is_metal(atoms) + else BASE_SET_NON_METAL.copy() + ) + default_kspacing = ( + KSPACING_METAL if check_is_metal(atoms) else KSPACING_NON_METAL + ) + + calc_defaults["species_dir"] = species_defaults + + if kspacing is not None: + calc_defaults["kspacing"] = kspacing + else: + calc_defaults["kspacing"] = default_kspacing + + if spin is not None: + calc_defaults["spin"] = spin + + calc_defaults["relax_geometry"] = "bfgs 1E-2" + + if relax_cell: + calc_defaults["relax_unit_cell"] = "full" + + return run_and_summarize( + atoms, + calc_defaults=calc_defaults, + calc_swaps=calc_kwargs, + additional_fields={"name": "FHI-aims Relax"} | (additional_fields or {}), + copy_files=copy_files, + ) diff --git a/src/quacc/settings.py b/src/quacc/settings.py index 13211cc87a..cbc77790af 100644 --- a/src/quacc/settings.py +++ b/src/quacc/settings.py @@ -385,6 +385,33 @@ class QuaccSettings(BaseSettings): ), ) + # --------------------------- + # FHI-aims Settings + # --------------------------- + AIMS_BIN: Path = Field( + Path("aims.x"), description="Path to the FHI-aims executable." + ) + + AIMS_PARALLEL_CMD: str = Field( + "", + description=( + """ + Parallelization command to run FHI-aims. For example: 'mpirun -np 4'. + Note that this does not include the executable name. + """ + ), + ) + + AIMS_SPECIES_DEFAULTS: Path = Field( + Path("defaults_2020"), + description=( + """ + Path to the species_defaults directory containing the FHI-aims basis sets. + This should point to a specific species defaults set (e.g., defaults_2020), + which contains subdirectories like 'light', 'intermediate', 'tight', etc. + """ + ), + ) # --------------------------- # Q-Chem Settings # ---------------------------