diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c57522cbf..8b6789253b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added -- The `WORKFLOW_ENGINE` quacc setting now accepts `None` +- The `WORKFLOW_ENGINE` quacc setting now accepts `None`. +- A `DEBUG` quacc setting as been added. ### Changed +- The way to run complex, dynamic flows has been modified to rely on `functools.partial()` instead of kwargs. See the updated documentation. - Refactored test suite ## [0.4.5] diff --git a/docs/user/basics/wflow_syntax.md b/docs/user/basics/wflow_syntax.md index 10c7999761..035b7860c7 100644 --- a/docs/user/basics/wflow_syntax.md +++ b/docs/user/basics/wflow_syntax.md @@ -43,7 +43,7 @@ To help enable interoperability between workflow engines, quacc offers a unified
| Quacc | Covalent | - | ------------------- | ---------------------------------| + | ------------------- | --------------------------------- | | `#!Python @job` | `#!Python @delayed` | | `#!Python @flow` | No effect | | `#!Python @subflow` | `#!Python delayed(...).compute()` | diff --git a/docs/user/recipes/recipes_intro.md b/docs/user/recipes/recipes_intro.md index 89e9f7ea26..f0f1600b8a 100644 --- a/docs/user/recipes/recipes_intro.md +++ b/docs/user/recipes/recipes_intro.md @@ -157,7 +157,7 @@ print(result) 'volume': 11.761470249999999} ``` -### A Simple Mixed-Code Workflow +### A Mixed-Code Workflow ```mermaid graph LR @@ -272,6 +272,47 @@ print(result2) 'volume': 11.761470249999999} ``` +### A More Complex Workflow + +```mermaid +graph LR + A[Input] --> B(Make Slabs) + B --> C(Slab Relax) --> G(Slab Static) --> K[Output] + B --> D(Slab Relax) --> H(Slab Static) --> K[Output] + B --> E(Slab Relax) --> I(Slab Static) --> K[Output] + B --> F(Slab Relax) --> J(Slab Static) --> K[Output]; +``` + +In this example, we will run a pre-made workflow that generates a set of slabs from a bulk structure and then runs a structure relaxation and static calculation on each slab. We will specifically highlight an example where we want to override the default parameters of one step in the recipe, in this case to tighten the force tolerance for the slab relaxation. + +!!! Tip + + Unsure what arguments a given function takes? Check out the [API documentation](https://quantum-accelerators.github.io/quacc/reference/quacc/recipes/emt/slabs.html). + +```python +from functools import partial +from ase.build import bulk +from quacc.recipes.emt.core import relax_job +from quacc.recipes.emt.slabs import bulk_to_slabs_flow + +# Define the Atoms object +atoms = bulk("Cu") + +# Define the workflow +custom_relax_job = partial(relax_job, opt_params={"fmax": 1e-4}) # (1)! +result = bulk_to_slabs_flow(atoms, custom_relax_job=custom_relax_job) + +# Print the result +print(result) +``` + +1. We have used a [partial function](https://www.learnpython.org/en/Partial_functions) here, which is a way to create a new function with specific arguments already applied. In other words, `#!Python opt_params={"fmax": 1e-4}` will be set as a keyword argument in the `relax_job` function by default. The same could be achieved, albeit more verbosely, as follows: + + ```python + def custom_relax_job(*args, **kwargs): + return relax_job(*args, opt_params={"fmax": 1e-4}, **kwargs) + ``` + ## Concluding Comments At this point, you now have the basic idea of how quacc recipes work! diff --git a/src/quacc/__init__.py b/src/quacc/__init__.py index 1317604929..2d6f38f77f 100644 --- a/src/quacc/__init__.py +++ b/src/quacc/__init__.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING from ase.atoms import Atoms -from ase.io.jsonio import decode, encode from quacc.settings import QuaccSettings from quacc.wflow_tools.decorators import Flow, Job, Subflow, flow, job, subflow @@ -17,6 +16,8 @@ def atoms_as_dict(s: Atoms) -> dict[str, Any]: + from ase.io.jsonio import encode + # Uses Monty's MSONable spec # Normally, we would want to this to be a wrapper around atoms.todict() with @module and # @class key-value pairs inserted. However, atoms.todict()/atoms.fromdict() does not currently @@ -25,6 +26,8 @@ def atoms_as_dict(s: Atoms) -> dict[str, Any]: def atoms_from_dict(d: dict[str, Any]) -> Atoms: + from ase.io.jsonio import decode + # Uses Monty's MSONable spec # Normally, we would want to have this be a wrapper around atoms.fromdict() # that just ignores the @module/@class key-value pairs. However, atoms.todict()/atoms.fromdict() @@ -41,3 +44,9 @@ def atoms_from_dict(d: dict[str, Any]) -> Atoms: # Load the settings SETTINGS = QuaccSettings() + +if SETTINGS.DEBUG: + import logging + + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) diff --git a/src/quacc/recipes/common/defects.py b/src/quacc/recipes/common/defects.py index d28bcd26eb..d67e40e793 100644 --- a/src/quacc/recipes/common/defects.py +++ b/src/quacc/recipes/common/defects.py @@ -7,7 +7,7 @@ from quacc.atoms.defects import make_defects_from_bulk if TYPE_CHECKING: - from typing import Callable + from typing import Any from ase.atoms import Atoms @@ -19,7 +19,7 @@ def bulk_to_defects_subflow( atoms: Atoms, relax_job: Job, static_job: Job | None = None, - make_defects_fn: Callable = make_defects_from_bulk, + make_defects_kwargs: dict[str, Any] | None = None, ) -> list[dict]: """ Workflow consisting of: @@ -38,8 +38,8 @@ def bulk_to_defects_subflow( The relaxation function. static_job The static function. - make_defects_fn - The function for generating defects. + make_defects_kwargs + Keyword arguments for [quacc.atoms.defects.make_defects_from_bulk][] Returns ------- @@ -47,13 +47,13 @@ def bulk_to_defects_subflow( List of dictionary of results """ - defects = make_defects_fn(atoms) + defects = make_defects_from_bulk(atoms, **make_defects_kwargs) results = [] for defect in defects: result = relax_job(defect) - if static_job: + if static_job is not None: result = static_job(result["atoms"]) results.append(result) diff --git a/src/quacc/recipes/common/slabs.py b/src/quacc/recipes/common/slabs.py index 88af5d6713..812c142b97 100644 --- a/src/quacc/recipes/common/slabs.py +++ b/src/quacc/recipes/common/slabs.py @@ -7,7 +7,7 @@ from quacc.atoms.slabs import make_adsorbate_structures, make_slabs_from_bulk if TYPE_CHECKING: - from typing import Callable + from typing import Any from ase.atoms import Atoms @@ -19,7 +19,7 @@ def bulk_to_slabs_subflow( atoms: Atoms, relax_job: Job, static_job: Job | None = None, - make_slabs_fn: Callable = make_slabs_from_bulk, + make_slabs_kwargs: dict[str, Any] | None = None, ) -> list[dict]: """ Workflow consisting of: @@ -38,16 +38,18 @@ def bulk_to_slabs_subflow( The relaxation function. static_job The static function. - make_slabs_fn - The function for generating slabs. + make_slabs_kwargs + Additional keyword arguments to pass to + [quacc.atoms.slabs.make_slabs_from_bulk][] Returns ------- list[dict] List of schemas. """ + make_slabs_kwargs = make_slabs_kwargs or {} - slabs = make_slabs_fn(atoms) + slabs = make_slabs_from_bulk(atoms, **make_slabs_kwargs) results = [] for slab in slabs: @@ -67,7 +69,7 @@ def slab_to_ads_subflow( adsorbate: Atoms, relax_job: Job, static_job: Job | None, - make_ads_fn: Callable = make_adsorbate_structures, + make_ads_kwargs: dict[str, Any] | None = None, ) -> list[dict]: """ Workflow consisting of: @@ -88,22 +90,24 @@ def slab_to_ads_subflow( The slab releaxation job. static_job The slab static job. - make_ads_fn - The function to generate slab-adsorbate structures. + make_ads_kwargs + Additional keyword arguments to pass to + [quacc.atoms.slabs.make_adsorbate_structures][] Returns ------- list[dict] List of schemas. """ + make_ads_kwargs = make_ads_kwargs or {} - slabs = make_ads_fn(atoms, adsorbate) + slabs = make_adsorbate_structures(atoms, adsorbate, **make_ads_kwargs) results = [] for slab in slabs: result = relax_job(slab) - if static_job: + if static_job is not None: result = static_job(result["atoms"]) results.append(result) diff --git a/src/quacc/recipes/emt/defects.py b/src/quacc/recipes/emt/defects.py index e5cb5e81f3..04312c99c2 100644 --- a/src/quacc/recipes/emt/defects.py +++ b/src/quacc/recipes/emt/defects.py @@ -1,15 +1,14 @@ """Defect recipes for EMT.""" from __future__ import annotations -from functools import partial from typing import TYPE_CHECKING from pymatgen.analysis.defects.generators import VacancyGenerator from quacc import flow -from quacc.atoms.defects import make_defects_from_bulk from quacc.recipes.common.defects import bulk_to_defects_subflow from quacc.recipes.emt.core import relax_job, static_job +from quacc.utils.dicts import merge_dicts if TYPE_CHECKING: from typing import Any @@ -23,6 +22,7 @@ VoronoiInterstitialGenerator, ) + from quacc import Job from quacc.schemas._aliases.ase import OptSchema, RunSchema @@ -38,10 +38,10 @@ def bulk_to_defects_flow( | VoronoiInterstitialGenerator ) = VacancyGenerator, defect_charge: int = 0, - make_defects_kwargs: dict[str, Any] | None = None, + custom_relax_job: Job | None = None, + custom_static_job: Job | None = None, run_static: bool = True, - defect_relax_kwargs: dict[str, Any] | None = None, - defect_static_kwargs: dict[str, Any] | None = None, + make_defects_kwargs: dict[str, Any] | None = None, ) -> list[RunSchema | OptSchema]: """ Workflow consisting of: @@ -60,15 +60,13 @@ def bulk_to_defects_flow( Defect generator defect_charge Charge state of the defect + custom_relax_job + Relaxation job, which defaults to [quacc.recipes.emt.core.relax_job][]. + custom_static_job + Static job, which defaults to [quacc.recipes.emt.core.static_job][]. make_defects_kwargs Keyword arguments to pass to [quacc.atoms.defects.make_defects_from_bulk][] - run_static - Whether to run the static calculation. - defect_relax_kwargs - Additional keyword arguments to pass to [quacc.recipes.emt.core.relax_job][]. - defect_static_kwargs - Additional keyword arguments to pass to [quacc.recipes.emt.core.static_job][]. Returns ------- @@ -76,18 +74,15 @@ def bulk_to_defects_flow( List of dictionary of results from [quacc.schemas.ase.summarize_run][] or [quacc.schemas.ase.summarize_opt_run][] """ - make_defects_kwargs = make_defects_kwargs or {} - defect_relax_kwargs = defect_relax_kwargs or {} - defect_static_kwargs = defect_static_kwargs or {} + make_defects_kwargs = merge_dicts( + make_defects_kwargs, {"defect_gen": defect_gen, "defect_charge": defect_charge} + ) return bulk_to_defects_subflow( atoms, - partial(relax_job, **defect_relax_kwargs), - static_job=partial(static_job, **defect_static_kwargs) if run_static else None, - make_defects_fn=partial( - make_defects_from_bulk, - defect_gen=defect_gen, - defect_charge=defect_charge, - **make_defects_kwargs, - ), + relax_job if custom_relax_job is None else custom_relax_job, + static_job=(static_job if custom_static_job is None else custom_static_job) + if run_static + else None, + make_defects_kwargs=make_defects_kwargs, ) diff --git a/src/quacc/recipes/emt/phonons.py b/src/quacc/recipes/emt/phonons.py index e331a8b414..5fcc71204c 100644 --- a/src/quacc/recipes/emt/phonons.py +++ b/src/quacc/recipes/emt/phonons.py @@ -1,7 +1,6 @@ """Phonon recipes for EMT""" from __future__ import annotations -from functools import partial from typing import TYPE_CHECKING from quacc import flow @@ -9,11 +8,10 @@ from quacc.recipes.emt.core import static_job if TYPE_CHECKING: - from typing import Any - from ase.atoms import Atoms from numpy.typing import ArrayLike + from quacc import Job from quacc.schemas._aliases.phonons import PhononSchema @@ -26,7 +24,7 @@ def phonon_flow( t_step: float = 10, t_min: float = 0, t_max: float = 1000, - static_job_kwargs: dict[str, Any] | None = None, + custom_static_job: Job | None = None, ) -> PhononSchema: """ Carry out a phonon calculation. @@ -47,20 +45,19 @@ def phonon_flow( Min temperature (K). t_max Max temperature (K). - static_job_kwargs - Additional keyword arguments for [quacc.recipes.emt.core.static_job][] - for the force calculations. + static_job + The static job for the force calculations, which defaults + to [quacc.recipes.emt.core.static_job][] Returns ------- PhononSchema Dictionary of results from [quacc.schemas.phonons.summarize_phonopy][] """ - static_job_kwargs = static_job_kwargs or {} return phonon_flow_( atoms, - partial(static_job, **static_job_kwargs), + static_job if custom_static_job is None else custom_static_job, supercell_matrix=supercell_matrix, atom_disp=atom_disp, t_step=t_step, diff --git a/src/quacc/recipes/emt/slabs.py b/src/quacc/recipes/emt/slabs.py index c789639a37..cb048a93ba 100644 --- a/src/quacc/recipes/emt/slabs.py +++ b/src/quacc/recipes/emt/slabs.py @@ -1,11 +1,9 @@ """Slab recipes for EMT.""" from __future__ import annotations -from functools import partial from typing import TYPE_CHECKING from quacc import flow -from quacc.atoms.slabs import make_slabs_from_bulk from quacc.recipes.common.slabs import bulk_to_slabs_subflow from quacc.recipes.emt.core import relax_job, static_job @@ -14,16 +12,17 @@ from ase.atoms import Atoms + from quacc import Job from quacc.schemas._aliases.ase import OptSchema, RunSchema @flow def bulk_to_slabs_flow( atoms: Atoms, - make_slabs_kwargs: dict[str, Any] | None = None, + custom_relax_job: Job | None = None, + custom_static_job: Job | None = None, run_static: bool = True, - slab_relax_kwargs: dict[str, Any] | None = None, - slab_static_kwargs: dict[str, Any] | None = None, + make_slabs_kwargs: dict[str, Any] | None = None, ) -> list[RunSchema | OptSchema]: """ Workflow consisting of: @@ -38,15 +37,15 @@ def bulk_to_slabs_flow( ---------- atoms Atoms object + custom_relax_job + The relaxation job, which defaults to [quacc.recipes.emt.core.relax_job][]. + custom_static_job + The static job, which defaults to [quacc.recipes.emt.core.static_job][]. + run_static + Whether to run static calculations. make_slabs_kwargs Additional keyword arguments to pass to [quacc.atoms.slabs.make_slabs_from_bulk][] - run_static - Whether to run the static calculation. - slab_relax_kwargs - Additional keyword arguments to pass to [quacc.recipes.emt.core.relax_job][]. - slab_static_kwargs - Additional keyword arguments to pass to [quacc.recipes.emt.core.static_job][]. Returns ------- @@ -55,13 +54,11 @@ def bulk_to_slabs_flow( [OptSchema][quacc.schemas.ase.summarize_opt_run] for each slab. """ - make_slabs_kwargs = make_slabs_kwargs or {} - slab_relax_kwargs = slab_relax_kwargs or {} - slab_static_kwargs = slab_static_kwargs or {} - return bulk_to_slabs_subflow( atoms, - partial(relax_job, **slab_relax_kwargs), - static_job=partial(static_job, **slab_static_kwargs) if run_static else None, - make_slabs_fn=partial(make_slabs_from_bulk, **make_slabs_kwargs), + relax_job if custom_relax_job is None else custom_relax_job, + static_job=(static_job if custom_static_job is None else custom_relax_job) + if run_static + else None, + make_slabs_kwargs=make_slabs_kwargs, ) diff --git a/src/quacc/recipes/tblite/phonons.py b/src/quacc/recipes/tblite/phonons.py index 1f18319060..b64b1a7d5c 100644 --- a/src/quacc/recipes/tblite/phonons.py +++ b/src/quacc/recipes/tblite/phonons.py @@ -1,19 +1,17 @@ """Phonon recipes for TBLite""" from __future__ import annotations -from functools import partial from typing import TYPE_CHECKING from quacc import flow -from quacc.recipes.common.phonons import phonon_flow as phonon_flow_ +from quacc.recipes.common.phonons import phonon_flow as common_phonon_flow from quacc.recipes.tblite.core import static_job if TYPE_CHECKING: - from typing import Any - from ase.atoms import Atoms from numpy.typing import ArrayLike + from quacc import Job from quacc.schemas._aliases.phonons import PhononSchema @@ -26,7 +24,7 @@ def phonon_flow( t_step: float = 10, t_min: float = 0, t_max: float = 1000, - static_job_kwargs: dict[str, Any] | None = None, + custom_static_job: Job | None = None, ) -> PhononSchema: """ Carry out a phonon calculation. @@ -47,20 +45,18 @@ def phonon_flow( Min temperature (K). t_max Max temperature (K). - static_job_kwargs - Keyword arguments for [quacc.recipes.tblite.core.static_job][] - for the force calculations. + custom_static_job + Static job, which defaults to [quacc.recipes.tblite.core.static_job][] Returns ------- PhononSchema Dictionary of results from [quacc.schemas.phonons.summarize_phonopy][] """ - static_job_kwargs = static_job_kwargs or {} - return phonon_flow_( + return common_phonon_flow( atoms, - partial(static_job, **static_job_kwargs), + static_job if custom_static_job is None else custom_static_job, supercell_matrix=supercell_matrix, atom_disp=atom_disp, t_step=t_step, diff --git a/src/quacc/recipes/vasp/mp.py b/src/quacc/recipes/vasp/mp.py index a21dacf7e8..9b66386b2e 100644 --- a/src/quacc/recipes/vasp/mp.py +++ b/src/quacc/recipes/vasp/mp.py @@ -24,10 +24,9 @@ from quacc.recipes.vasp._base import base_fn if TYPE_CHECKING: - from typing import Any - from ase.atoms import Atoms + from quacc import Job from quacc.schemas._aliases.vasp import MPRelaxFlowSchema, VaspSchema @@ -127,9 +126,7 @@ def mp_relax_job( @flow def mp_relax_flow( - atoms: Atoms, - prerelax_job_kwargs: dict[str, Any] | None = None, - relax_job_kwargs: dict[str, Any] | None = None, + atoms: Atoms, prerelax_job: Job = mp_prerelax_job, relax_job: Job = mp_relax_job ) -> MPRelaxFlowSchema: """ Workflow consisting of: @@ -142,31 +139,28 @@ def mp_relax_flow( ---------- atoms Atoms object for the structure. - prerelax_job_kwargs - Additional keyword arguments to pass to [quacc.recipes.vasp.mp.mp_prerelax_job][]. - relax_job_kwargs - Additional keyword arguments to pass to [quacc.recipes.vasp.mp.mp_relax_job][]. + prerelax_job + Pre-relaxation job, which defaults to [quacc.recipes.vasp.mp.mp_prerelax_job][]. + relax_job + Relaxation job, which defaults to [quacc.recipes.vasp.mp.mp_relax_job][]. Returns ------- MPRelaxFlowSchema Dictionary of results """ - prerelax_job_kwargs = prerelax_job_kwargs or {} - relax_job_kwargs = relax_job_kwargs or {} # Run the prerelax - prerelax_results = mp_prerelax_job(atoms, **prerelax_job_kwargs) + prerelax_results = prerelax_job(atoms) # Run the relax - relax_results = mp_relax_job( + relax_results = relax_job( prerelax_results["atoms"], bandgap=prerelax_results["output"]["bandgap"], copy_files=[ Path(prerelax_results["dir_name"]) / "CHGCAR", Path(prerelax_results["dir_name"]) / "WAVECAR", ], - **relax_job_kwargs, ) relax_results["prerelax"] = prerelax_results diff --git a/src/quacc/recipes/vasp/slabs.py b/src/quacc/recipes/vasp/slabs.py index 47ea25a623..5c2a3cb647 100644 --- a/src/quacc/recipes/vasp/slabs.py +++ b/src/quacc/recipes/vasp/slabs.py @@ -1,11 +1,9 @@ """Recipes for slabs.""" from __future__ import annotations -from functools import partial from typing import TYPE_CHECKING from quacc import flow, job -from quacc.atoms.slabs import make_adsorbate_structures, make_slabs_from_bulk from quacc.recipes.common.slabs import bulk_to_slabs_subflow, slab_to_ads_subflow from quacc.recipes.vasp._base import base_fn @@ -15,11 +13,12 @@ from ase.atoms import Atoms + from quacc import Job from quacc.schemas._aliases.vasp import VaspSchema @job -def slab_static_job( +def static_job( atoms: Atoms, preset: str | None = "SlabSet", copy_files: str | Path | list[str | Path] | None = None, @@ -69,7 +68,7 @@ def slab_static_job( @job -def slab_relax_job( +def relax_job( atoms: Atoms, preset: str | None = "SlabSet", copy_files: str | Path | list[str | Path] | None = None, @@ -122,9 +121,9 @@ def slab_relax_job( def bulk_to_slabs_flow( atoms: Atoms, make_slabs_kwargs: dict[str, Any] | None = None, + custom_relax_job: Job | None = None, + custom_static_job: Job | None = None, run_static: bool = True, - slab_relax_kwargs: dict[str, Any] | None = None, - slab_static_kwargs: dict[str, Any] | None = None, ) -> list[VaspSchema]: """ Workflow consisting of: @@ -141,12 +140,12 @@ def bulk_to_slabs_flow( Atoms object make_slabs_kwargs Additional keyword arguments to pass to [quacc.atoms.slabs.make_slabs_from_bulk][] + custom_relax_job + Relaxation job, which defaults to [quacc.recipes.vasp.slabs.slab_relax_job][]. + custom_static_job + Static job, which defaults to [quacc.recipes.vasp.slabs.slab_static_job][]. run_static - Whether to run the static calculation. - slab_relax_kwargs - Additional keyword arguments to pass to [quacc.recipes.vasp.slabs.slab_relax_job][]. - slab_static_kwargs - Additional keyword arguments to pass to [quacc.recipes.vasp.slabs.slab_static_job][]. + Whether to run static calculations. Returns ------- @@ -154,19 +153,13 @@ def bulk_to_slabs_flow( List of dictionary results from [quacc.schemas.vasp.vasp_summarize_run][] """ - make_slabs_kwargs = make_slabs_kwargs or {} - slab_relax_kwargs = slab_relax_kwargs or {} - slab_static_kwargs = slab_static_kwargs or {} - - relax_job = partial(slab_relax_job, **slab_relax_kwargs) - static_job = partial(slab_static_job, **slab_static_kwargs) - make_slabs_fn = partial(make_slabs_from_bulk, **make_slabs_kwargs) - return bulk_to_slabs_subflow( atoms, - relax_job, - static_job=static_job if run_static else None, - make_slabs_fn=make_slabs_fn, + relax_job if custom_relax_job is None else custom_relax_job, + static_job=(static_job if custom_static_job is None else custom_static_job) + if run_static + else None, + make_slabs_kwargs=make_slabs_kwargs, ) @@ -174,10 +167,10 @@ def bulk_to_slabs_flow( def slab_to_ads_flow( slab: Atoms, adsorbate: Atoms, - make_ads_kwargs: dict[str, Any] | None = None, + custom_relax_job: Job | None = None, + custom_static_job: Job | None = None, run_static: bool = True, - slab_relax_kwargs: dict[str, Any] | None = None, - slab_static_kwargs: dict[str, Any] | None = None, + make_ads_kwargs: dict[str, Any] | None = None, ) -> list[VaspSchema]: """ Workflow consisting of: @@ -194,14 +187,14 @@ def slab_to_ads_flow( Atoms object for the slab structure. adsorbate Atoms object for the adsorbate. + custom_relax_job + Relaxation job, which defaults to [quacc.recipes.vasp.slabs.slab_relax_job][]. + custom_static_job + Static job, which defaults to [quacc.recipes.vasp.slabs.slab_static_job][]. + run_static + Whether to run static calculations. make_ads_kwargs Additional keyword arguments to pass to [quacc.atoms.slabs.make_adsorbate_structures][] - run_static - Whether to run the static calculation. - slab_relax_kwargs - Additional keyword arguments to pass to [quacc.recipes.vasp.slabs.slab_relax_job][]. - slab_static_kwargs - Additional keyword arguments to pass to [quacc.recipes.vasp.slabs.slab_static_job][]. Returns ------- @@ -209,16 +202,12 @@ def slab_to_ads_flow( List of dictionaries of results from [quacc.schemas.vasp.vasp_summarize_run][] """ - make_ads_kwargs = make_ads_kwargs or {} - slab_relax_kwargs = slab_relax_kwargs or {} - slab_static_kwargs = slab_static_kwargs or {} - return slab_to_ads_subflow( slab, adsorbate, - partial(slab_relax_job, **slab_relax_kwargs), - static_job=partial(slab_static_job, **slab_static_kwargs) + relax_job if custom_relax_job is None else custom_relax_job, + static_job=(static_job if custom_static_job is None else custom_static_job) if run_static else None, - make_ads_fn=partial(make_adsorbate_structures, **make_ads_kwargs), + make_ads_kwargs=make_ads_kwargs, ) diff --git a/src/quacc/runners/ase.py b/src/quacc/runners/ase.py index f885ee983a..bc15ee69a7 100644 --- a/src/quacc/runners/ase.py +++ b/src/quacc/runners/ase.py @@ -1,6 +1,7 @@ """Utility functions for running ASE calculators with ASE-based methods.""" from __future__ import annotations +import sys from typing import TYPE_CHECKING import numpy as np @@ -11,6 +12,7 @@ from monty.dev import requires from monty.os.path import zpath +from quacc import SETTINGS from quacc.atoms.core import copy_atoms from quacc.runners.prep import calc_cleanup, calc_setup from quacc.utils.dicts import merge_dicts @@ -159,7 +161,10 @@ def run_opt( # Set defaults optimizer_kwargs = merge_dicts( - {"logfile": tmpdir / "opt.log", "restart": tmpdir / "opt.pckl"}, + { + "logfile": "-" if SETTINGS.DEBUG else tmpdir / "opt.log", + "restart": tmpdir / "opt.pckl", + }, optimizer_kwargs, ) run_kwargs = run_kwargs or {} @@ -240,7 +245,7 @@ def run_vib( vib.run() # Summarize run - vib.summary(log=str(tmpdir / "vib_summary.log")) + vib.summary(log=sys.stdout if SETTINGS.DEBUG else str(tmpdir / "vib_summary.log")) # Perform cleanup operations calc_cleanup(tmpdir, job_results_dir) diff --git a/src/quacc/schemas/ase.py b/src/quacc/schemas/ase.py index f37d5fe4ee..6490ce4162 100644 --- a/src/quacc/schemas/ase.py +++ b/src/quacc/schemas/ase.py @@ -388,9 +388,13 @@ def summarize_ideal_gas_thermo( results = { "results": { "energy": igt.potentialenergy, - "enthalpy": igt.get_enthalpy(temperature), - "entropy": igt.get_entropy(temperature, pressure * 10**5), - "gibbs_energy": igt.get_gibbs_energy(temperature, pressure * 10**5), + "enthalpy": igt.get_enthalpy(temperature, verbose=SETTINGS.DEBUG), + "entropy": igt.get_entropy( + temperature, pressure * 10**5, verbose=SETTINGS.DEBUG + ), + "gibbs_energy": igt.get_gibbs_energy( + temperature, pressure * 10**5, verbose=SETTINGS.DEBUG + ), "zpe": igt.get_ZPE_correction(), } } diff --git a/src/quacc/settings.py b/src/quacc/settings.py index 62bae99920..bcec15b292 100644 --- a/src/quacc/settings.py +++ b/src/quacc/settings.py @@ -9,7 +9,6 @@ import psutil from maggma.core import Store -from monty.json import MontyDecoder from pydantic import Field, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -323,15 +322,24 @@ class QuaccSettings(BaseSettings): "config.yml", description="Path to NewtonNet YAML settings file" ) + # --------------------------- + # Debug Settings + # --------------------------- + DEBUG: bool = Field( + False, + description=( + "Whether to run in debug mode. This will set the logging level to DEBUG, " + "ASE logs (e.g. optimizations, vibrations, thermo) are printed to stdout." + ), + ) + # --8<-- [end:settings] @field_validator("WORKFLOW_ENGINE") @classmethod def validate_workflow_engine(cls, v: Optional[str]) -> Optional[str]: """Validate the workflow engine""" - if v and v.lower() == "local": - return None - return v + return None if v and v.lower() == "local" else v @field_validator("RESULTS_DIR", "SCRATCH_DIR") @classmethod @@ -366,6 +374,8 @@ def expand_paths(cls, v: Optional[Path]) -> Optional[Path]: @field_validator("PRIMARY_STORE") def generate_store(cls, v: Union[str, Store]) -> Store: """Generate the Maggma store""" + from monty.json import MontyDecoder + return MontyDecoder().decode(v) if isinstance(v, str) else v model_config = SettingsConfigDict(env_prefix="quacc_") diff --git a/tests/core/recipes/emt_recipes/test_emt_defect_recipes.py b/tests/core/recipes/emt_recipes/test_emt_defect_recipes.py index 1bb2f1e86c..6877700695 100644 --- a/tests/core/recipes/emt_recipes/test_emt_defect_recipes.py +++ b/tests/core/recipes/emt_recipes/test_emt_defect_recipes.py @@ -1,8 +1,11 @@ +from functools import partial + import pytest from ase.build import bulk pytest.importorskip("pymatgen.analysis.defects") +from quacc.recipes.emt.core import relax_job from quacc.recipes.emt.defects import bulk_to_defects_flow @@ -10,14 +13,16 @@ def test_bulk_to_defects_flow(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) atoms = bulk("Cu") output = bulk_to_defects_flow( - atoms, defect_relax_kwargs={"opt_params": {"fmax": 5}} + atoms, custom_relax_job=partial(relax_job, opt_params={"fmax": 5}) ) assert len(output) == 1 assert len(output[0]["atoms"]) == 107 atoms = bulk("Cu") output = bulk_to_defects_flow( - atoms, run_static=False, defect_relax_kwargs={"opt_params": {"fmax": 5}} + atoms, + custom_relax_job=partial(relax_job, opt_params={"fmax": 5}), + run_static=False, ) assert len(output) == 1 assert len(output[0]["atoms"]) == 107 diff --git a/tests/core/recipes/emt_recipes/test_emt_recipes.py b/tests/core/recipes/emt_recipes/test_emt_recipes.py index f832a45779..119275251e 100644 --- a/tests/core/recipes/emt_recipes/test_emt_recipes.py +++ b/tests/core/recipes/emt_recipes/test_emt_recipes.py @@ -1,3 +1,5 @@ +from functools import partial + import numpy as np import pytest from ase.build import bulk, molecule @@ -94,7 +96,7 @@ def test_slab_dynamic_jobs(tmp_path, monkeypatch): outputs = bulk_to_slabs_flow( atoms, run_static=False, - slab_relax_kwargs={"opt_params": {"fmax": 1.0}, "asap_cutoff": True}, + custom_relax_job=partial(relax_job, opt_params={"fmax": 1.0}, asap_cutoff=True), ) assert len(outputs) == 4 assert outputs[0]["nsites"] == 80 diff --git a/tests/core/recipes/tblite_recipes/test_tblite_phonons.py b/tests/core/recipes/tblite_recipes/test_tblite_phonons.py index 551971da23..0e5515e9c4 100644 --- a/tests/core/recipes/tblite_recipes/test_tblite_phonons.py +++ b/tests/core/recipes/tblite_recipes/test_tblite_phonons.py @@ -1,6 +1,9 @@ +from functools import partial + import pytest from ase.build import bulk +from quacc.recipes.tblite.core import static_job as static_job_ from quacc.recipes.tblite.phonons import phonon_flow pytest.importorskip("tblite.ase") @@ -10,6 +13,8 @@ def test_phonon_flow(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) atoms = bulk("Cu") - output = phonon_flow(atoms, static_job_kwargs={"method": "GFN1-xTB"}) + output = phonon_flow( + atoms, custom_static_job=partial(static_job_, method="GFN1-xTB") + ) assert output["results"]["force_constants"].shape == (8, 8, 3, 3) assert len(output["results"]["thermal_properties"]["temperatures"]) == 101 diff --git a/tests/core/recipes/vasp_recipes/mocked/test_vasp_recipes.py b/tests/core/recipes/vasp_recipes/mocked/test_vasp_recipes.py index 2ef4c98430..0eaac567ab 100644 --- a/tests/core/recipes/vasp_recipes/mocked/test_vasp_recipes.py +++ b/tests/core/recipes/vasp_recipes/mocked/test_vasp_recipes.py @@ -1,3 +1,5 @@ +from functools import partial + import pytest from ase.build import bulk, molecule @@ -5,12 +7,10 @@ from quacc.recipes.vasp.core import double_relax_job, relax_job, static_job from quacc.recipes.vasp.mp import mp_prerelax_job, mp_relax_flow, mp_relax_job from quacc.recipes.vasp.qmof import qmof_relax_job -from quacc.recipes.vasp.slabs import ( - bulk_to_slabs_flow, - slab_relax_job, - slab_static_job, - slab_to_ads_flow, -) +from quacc.recipes.vasp.slabs import bulk_to_slabs_flow +from quacc.recipes.vasp.slabs import relax_job as slab_relax_job +from quacc.recipes.vasp.slabs import slab_to_ads_flow +from quacc.recipes.vasp.slabs import static_job as slab_static_job DEFAULT_SETTINGS = SETTINGS.model_copy() @@ -218,7 +218,9 @@ def test_slab_dynamic_jobs(tmp_path, monkeypatch): assert [output["parameters"]["nsw"] == 0 for output in outputs] outputs = bulk_to_slabs_flow( - atoms, slab_relax_kwargs={"preset": "SlabSet", "nelmin": 6}, run_static=False + atoms, + custom_relax_job=partial(slab_relax_job, preset="SlabSet", nelmin=6), + run_static=False, ) assert len(outputs) == 4 assert outputs[0]["nsites"] == 45 @@ -230,7 +232,7 @@ def test_slab_dynamic_jobs(tmp_path, monkeypatch): assert [output["parameters"]["encut"] == 450 for output in outputs] outputs = bulk_to_slabs_flow( - atoms, slab_static_kwargs={"preset": "SlabSet", "nelmin": 6} + atoms, custom_relax_job=partial(slab_relax_job, preset="SlabSet", nelmin=6) ) assert len(outputs) == 4 assert outputs[0]["nsites"] == 45 @@ -257,7 +259,7 @@ def test_slab_dynamic_jobs(tmp_path, monkeypatch): outputs = slab_to_ads_flow( atoms, adsorbate, - slab_relax_kwargs={"preset": "SlabSet", "nelmin": 6}, + custom_relax_job=partial(slab_relax_job, preset="SlabSet", nelmin=6), run_static=False, ) @@ -267,7 +269,9 @@ def test_slab_dynamic_jobs(tmp_path, monkeypatch): assert [output["parameters"]["encut"] == 450 for output in outputs] outputs = slab_to_ads_flow( - atoms, adsorbate, slab_static_kwargs={"preset": "SlabSet", "nelmin": 6} + atoms, + adsorbate, + custom_static_job=partial(slab_static_job, preset="SlabSet", nelmin=6), ) assert [output["nsites"] == 82 for output in outputs] diff --git a/tests/core/schemas/test_cclib_schema.py b/tests/core/schemas/test_cclib_schema.py index 9a15408a3c..f0fa4a7545 100644 --- a/tests/core/schemas/test_cclib_schema.py +++ b/tests/core/schemas/test_cclib_schema.py @@ -105,7 +105,6 @@ def test_cclib_summarize_run(tmp_path, monkeypatch): MontyDecoder().process_decoded(d) # Make sure default dir works - cwd = os.getcwd() monkeypatch.chdir(run1) cclib_summarize_run(atoms, ".log") diff --git a/tests/core/settings/test_settings.py b/tests/core/settings/test_settings.py index 9a53613ad9..dd270be482 100644 --- a/tests/core/settings/test_settings.py +++ b/tests/core/settings/test_settings.py @@ -29,16 +29,17 @@ def teardown_function(): def test_file_v1(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) - assert QuaccSettings().GZIP_FILES is True + assert QuaccSettings().DEBUG is False with open("quacc_test.yaml", "w") as f: - f.write("GZIP_FILES: false\nWORKFLOW_ENGINE: local") + f.write("GZIP_FILES: false\nWORKFLOW_ENGINE: local\nDEBUG: true") monkeypatch.setenv( "QUACC_CONFIG_FILE", os.path.join(os.getcwd(), "quacc_test.yaml") ) assert QuaccSettings().GZIP_FILES is False assert QuaccSettings().WORKFLOW_ENGINE is None + assert QuaccSettings().DEBUG is True os.remove("quacc_test.yaml") diff --git a/tests/covalent/.quacc.yaml b/tests/covalent/.quacc.yaml index f8ed9bce19..6e31f74b59 100644 --- a/tests/covalent/.quacc.yaml +++ b/tests/covalent/.quacc.yaml @@ -1,3 +1,4 @@ +DEBUG: true RESULTS_DIR: ./.test_results SCRATCH_DIR: ./.test_scratch WORKFLOW_ENGINE: covalent diff --git a/tests/covalent/test_covalent_recipes.py b/tests/covalent/test_covalent_recipes.py index bfcb342e7e..efc2fb41c2 100644 --- a/tests/covalent/test_covalent_recipes.py +++ b/tests/covalent/test_covalent_recipes.py @@ -1,3 +1,5 @@ +from functools import partial + import pytest from ase.build import bulk @@ -10,6 +12,22 @@ ) from quacc.recipes.emt.core import relax_job # skipcq: PYL-C0412 +from quacc.recipes.emt.slabs import bulk_to_slabs_flow # skipcq: PYL-C0412 + + +def test_covalent_functools(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + atoms = bulk("Cu") + dispatch_id = ct.dispatch(bulk_to_slabs_flow)( + atoms, + custom_relax_job=partial(relax_job, opt_params={"fmax": 0.1}), + run_static=False, + ) + output = ct.get_result(dispatch_id, wait=True) + assert output.status == "COMPLETED" + assert len(output.result) == 4 + assert "atoms" in output.result[-1] + assert output.result[-1]["fmax"] == 0.1 def test_phonon_flow(tmp_path, monkeypatch): diff --git a/tests/dask/.quacc.yaml b/tests/dask/.quacc.yaml index f43841626a..64eca58c6f 100644 --- a/tests/dask/.quacc.yaml +++ b/tests/dask/.quacc.yaml @@ -1,4 +1,5 @@ CREATE_UNIQUE_DIR: true +DEBUG: true RESULTS_DIR: ./.test_results SCRATCH_DIR: ./.test_scratch WORKFLOW_ENGINE: dask diff --git a/tests/dask/test_dask_recipes.py b/tests/dask/test_dask_recipes.py index c317168068..a058ac9742 100644 --- a/tests/dask/test_dask_recipes.py +++ b/tests/dask/test_dask_recipes.py @@ -1,3 +1,5 @@ +from functools import partial + import pytest from ase.build import bulk @@ -12,10 +14,29 @@ from dask.distributed import default_client from quacc.recipes.emt.core import relax_job # skipcq: PYL-C0412 +from quacc.recipes.emt.slabs import bulk_to_slabs_flow # skipcq: PYL-C0412 client = default_client() +def test_dask_functools(tmp_path, monkeypatch): + from dask import delayed as delayed_ + + monkeypatch.chdir(tmp_path) + atoms = bulk("Cu") + delayed = bulk_to_slabs_flow( + atoms, + custom_relax_job=delayed_( + partial(relax_job.__wrapped__, opt_params={"fmax": 0.1}) + ), + run_static=False, + ) + result = client.gather(client.compute(delayed)) + assert len(result) == 4 + assert "atoms" in result[-1] + assert result[-1]["fmax"] == 0.1 + + def test_dask_phonon_flow(tmp_path, monkeypatch): pytest.importorskip("phonopy") from quacc.recipes.emt.phonons import phonon_flow diff --git a/tests/dask/test_dask_tutorials.py b/tests/dask/test_dask_tutorials.py index f6c0401c12..1c0b4eaad0 100644 --- a/tests/dask/test_dask_tutorials.py +++ b/tests/dask/test_dask_tutorials.py @@ -1,7 +1,7 @@ import pytest from ase.build import bulk, molecule -from quacc import SETTINGS, job, subflow +from quacc import SETTINGS dask = pytest.importorskip("dask") pytestmark = pytest.mark.skipif( @@ -109,13 +109,7 @@ def test_tutorial2c(tmp_path, monkeypatch): # Define the workflow def workflow(atoms): relaxed_bulk = relax_job(atoms) - return bulk_to_slabs_flow( - relaxed_bulk["atoms"], - run_static=False, - # slab_relax_kwargs={ - # "opt_params": {"optimizer_kwargs": {"logfile": "-"}} - # }, # this is for easy debugging) # (1)! - ) + return bulk_to_slabs_flow(relaxed_bulk["atoms"], run_static=False) # (1)! # Define the Atoms object atoms = bulk("Cu") @@ -129,60 +123,3 @@ def workflow(atoms): # Print the results assert len(result) == 4 assert "atoms" in result[0] - - -def test_comparison1(tmp_path, monkeypatch): - monkeypatch.chdir(tmp_path) - - @job # (1)! - def add(a, b): - return a + b - - @job - def mult(a, b): - return a * b - - def workflow(a, b, c): # (2)! - return mult(add(a, b), c) - - result = client.compute(workflow(1, 2, 3)).result() # 9 - assert result == 9 - - -def test_comparison2(tmp_path, monkeypatch): - monkeypatch.chdir(tmp_path) - - @job - def add(a, b): - return a + b - - @job - def make_more(val): - return [val] * 3 - - @subflow # (1)! - def add_distributed(vals, c): - return [add(val, c) for val in vals] - - delayed1 = add(1, 2) - delayed2 = make_more(delayed1) - delayed3 = add_distributed(delayed2, 3) - - assert dask.compute(*client.gather(delayed3)) == (6, 6, 6) - - -def test_comparison3(tmp_path, monkeypatch): - monkeypatch.chdir(tmp_path) - - @job # (1)! - def add(a, b): - return a + b - - @job - def mult(a, b): - return a * b - - delayed1 = add(1, 2) - delayed2 = mult(delayed1, 3) - - assert client.compute(delayed2).result() == 9 diff --git a/tests/jobflow/.quacc.yaml b/tests/jobflow/.quacc.yaml index c3fede4b9d..c77b66c79e 100644 --- a/tests/jobflow/.quacc.yaml +++ b/tests/jobflow/.quacc.yaml @@ -1,3 +1,4 @@ +DEBUG: true RESULTS_DIR: ./.test_results SCRATCH_DIR: ./.test_scratch WORKFLOW_ENGINE: jobflow diff --git a/tests/parsl/.quacc.yaml b/tests/parsl/.quacc.yaml index d5484451e1..2974142821 100644 --- a/tests/parsl/.quacc.yaml +++ b/tests/parsl/.quacc.yaml @@ -1,4 +1,5 @@ CREATE_UNIQUE_DIR: true +DEBUG: true RESULTS_DIR: ./.test_results SCRATCH_DIR: ./.test_scratch WORKFLOW_ENGINE: parsl diff --git a/tests/parsl/test_parsl_recipes.py b/tests/parsl/test_parsl_recipes.py index acb63e9488..6323a36f99 100644 --- a/tests/parsl/test_parsl_recipes.py +++ b/tests/parsl/test_parsl_recipes.py @@ -1,4 +1,5 @@ import contextlib +from functools import partial import pytest from ase.build import bulk @@ -10,7 +11,9 @@ SETTINGS.WORKFLOW_ENGINE != "parsl", reason="This test requires the Parsl workflow engine", ) + from quacc.recipes.emt.core import relax_job # skipcq: PYL-C0412 +from quacc.recipes.emt.slabs import bulk_to_slabs_flow # skipcq: PYL-C0412 def setup_module(): @@ -22,6 +25,19 @@ def teardown_module(): parsl.clear() +def test_parsl_functools(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + atoms = bulk("Cu") + result = bulk_to_slabs_flow( + atoms, + custom_relax_job=partial(relax_job, opt_params={"fmax": 0.1}), + run_static=False, + ).result() + assert len(result) == 4 + assert "atoms" in result[-1] + assert result[-1]["fmax"] == 0.1 + + def test_phonon_flow(tmp_path, monkeypatch): pytest.importorskip("phonopy") from quacc.recipes.emt.phonons import phonon_flow diff --git a/tests/parsl/test_parsl_tutorials.py b/tests/parsl/test_parsl_tutorials.py index 91094704e4..0afb564a8c 100644 --- a/tests/parsl/test_parsl_tutorials.py +++ b/tests/parsl/test_parsl_tutorials.py @@ -115,13 +115,7 @@ def test_tutorial2c(tmp_path, monkeypatch): # Define the workflow def workflow(atoms): relaxed_bulk = relax_job(atoms) - return bulk_to_slabs_flow( - relaxed_bulk["atoms"], - run_static=False, - # slab_relax_kwargs={ - # "opt_params": {"optimizer_kwargs": {"logfile": "-"}} - # }, # this is for easy debugging - ) + return bulk_to_slabs_flow(relaxed_bulk["atoms"], run_static=False) # (1)! # Define the Atoms object atoms = bulk("Cu") diff --git a/tests/redun/.quacc.yaml b/tests/redun/.quacc.yaml index 8ab130afdc..e5892c4b96 100644 --- a/tests/redun/.quacc.yaml +++ b/tests/redun/.quacc.yaml @@ -1,3 +1,4 @@ +DEBUG: true RESULTS_DIR: ./.test_results SCRATCH_DIR: ./.test_scratch WORKFLOW_ENGINE: redun diff --git a/tests/redun/test_redun_recipes.py b/tests/redun/test_redun_recipes.py new file mode 100644 index 0000000000..3675ece791 --- /dev/null +++ b/tests/redun/test_redun_recipes.py @@ -0,0 +1,36 @@ +from functools import partial + +import pytest +from ase.build import bulk + +from quacc import SETTINGS + +redun = pytest.importorskip("redun") +pytestmark = pytest.mark.skipif( + SETTINGS.WORKFLOW_ENGINE != "redun", + reason="This test requires the Redun workflow engine", +) + + +@pytest.fixture() +def scheduler(): + return redun.Scheduler() + + +from quacc.recipes.emt.core import relax_job # skipcq: PYL-C0412 +from quacc.recipes.emt.slabs import bulk_to_slabs_flow # skipcq: PYL-C0412 + + +def test_redun_functools(tmp_path, monkeypatch, scheduler): + monkeypatch.chdir(tmp_path) + atoms = bulk("Cu") + result = scheduler.run( + bulk_to_slabs_flow( + atoms, + custom_relax_job=partial(relax_job, opt_params={"fmax": 0.1}), + run_static=False, + ) + ) + assert len(result) == 4 + assert "atoms" in result[-1] + assert result[-1]["fmax"] == 0.1