From df1ae359a9358b056510c3ee7de51a913ac0b3fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 08:06:11 +0000 Subject: [PATCH 1/5] Bump emmet-core from 0.84.3rc3 to 0.84.3rc4 (#1065) Bumps [emmet-core](https://github.com/materialsproject/emmet) from 0.84.3rc3 to 0.84.3rc4. - [Release notes](https://github.com/materialsproject/emmet/releases) - [Changelog](https://github.com/materialsproject/emmet/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/materialsproject/emmet/compare/0.84.3rc3...0.84.3rc4) --- updated-dependencies: - dependency-name: emmet-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7dbca815d..da349ed3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ strict = [ "click==8.1.7", "custodian==2024.10.16", "dscribe==2.1.1", - "emmet-core==0.84.3rc3", + "emmet-core==0.84.3rc4", "ijson==3.3.0", "jobflow==0.1.18", "lobsterpy==0.4.9", From eaed47415efc7c3ce8cbefc28a803c95ef9a993b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 08:20:24 +0000 Subject: [PATCH 2/5] Bump jobflow from 0.1.18 to 0.1.19 (#1067) Bumps [jobflow](https://github.com/materialsproject/jobflow) from 0.1.18 to 0.1.19. - [Release notes](https://github.com/materialsproject/jobflow/releases) - [Changelog](https://github.com/materialsproject/jobflow/blob/main/CHANGELOG.md) - [Commits](https://github.com/materialsproject/jobflow/compare/v0.1.18...v0.1.19) --- updated-dependencies: - dependency-name: jobflow dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index da349ed3e..196c0b227 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ strict = [ "dscribe==2.1.1", "emmet-core==0.84.3rc4", "ijson==3.3.0", - "jobflow==0.1.18", + "jobflow==0.1.19", "lobsterpy==0.4.9", "mdanalysis==2.7.0", "monty==2024.10.21", From 4244da96896419f93668b89cfee67f065a51c902 Mon Sep 17 00:00:00 2001 From: "J. George" Date: Mon, 25 Nov 2024 17:19:17 +0100 Subject: [PATCH 3/5] add openmm description to docs (#1069) --- docs/user/codes/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/user/codes/index.md b/docs/user/codes/index.md index fcb3b5f08..38bde5106 100644 --- a/docs/user/codes/index.md +++ b/docs/user/codes/index.md @@ -6,4 +6,5 @@ The section gives the instructions for codes supported by atomate2. ```{toctree} vasp +openmm ``` From 4b84d781cc5beced4df81333aab063997e9b0a08 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Mon, 25 Nov 2024 16:52:45 -0500 Subject: [PATCH 4/5] fix minor code formatting issues + typos (#1070) --- docs/user/codes/vasp.md | 7 +- src/atomate2/common/flows/anharmonicity.py | 2 +- src/atomate2/common/flows/gruneisen.py | 2 +- src/atomate2/common/flows/phonons.py | 2 +- src/atomate2/common/flows/qha.py | 27 +++---- src/atomate2/common/jobs/anharmonicity.py | 18 ++--- src/atomate2/common/jobs/gruneisen.py | 8 +- src/atomate2/common/jobs/qha.py | 4 +- src/atomate2/common/schemas/phonons.py | 13 ++-- tests/vasp/flows/test_phonons.py | 88 +++++++--------------- 10 files changed, 65 insertions(+), 106 deletions(-) diff --git a/docs/user/codes/vasp.md b/docs/user/codes/vasp.md index e0b63ab51..5e1e299b4 100644 --- a/docs/user/codes/vasp.md +++ b/docs/user/codes/vasp.md @@ -1,4 +1,5 @@ (codes.vasp)= + # VASP At present, most workflows in atomate2 use the Vienna *ab initio* simulation package @@ -260,21 +261,19 @@ With the help of phonopy, these forces are then converted into a dynamical matri The dynamical matrices of three structures are then used as an input to the phonopy Grueneisen api to compute mode-dependent Grueneisen parameters. - ### Quasi-harmonic Workflow + Uses the quasi-harmonic approximation with the help of Phonopy to compute thermodynamic properties. First, a tight relaxation is performed. Subsequently, several optimizations at different constant volumes are performed. At each of the volumes, an additional phonon run is performed as well. Afterwards, equation of state fits are performed with phonopy. - - ### Equation of State Workflow + An equation of state workflow is implemented. First, a tight relaxation is performed. Subsequently, several optimizations at different constant volumes are performed. Additional static calculations might be performed afterwards to arrive at more accurate energies. Then, an equation of state fit is performed with pymatgen. - ### LOBSTER Perform bonding analysis with [LOBSTER](http://cohp.de/) and [LobsterPy](https://github.com/jageo/lobsterpy) diff --git a/src/atomate2/common/flows/anharmonicity.py b/src/atomate2/common/flows/anharmonicity.py index 5d9067bc4..e2d753459 100644 --- a/src/atomate2/common/flows/anharmonicity.py +++ b/src/atomate2/common/flows/anharmonicity.py @@ -81,7 +81,7 @@ def make( A previous calculation directory to use for copying outputs. Default is None. born: Optional[list[Matrix3D]] - Instead of recomputing born charges and epsilon, these values can also be + Instead of recomputing Born charges and epsilon, these values can also be provided manually. If born and epsilon_static are provided, the born run will be skipped it can be provided in the VASP convention with information for every atom in unit cell. Please be careful when converting structures diff --git a/src/atomate2/common/flows/gruneisen.py b/src/atomate2/common/flows/gruneisen.py index 700aa0b79..0977f9ebc 100644 --- a/src/atomate2/common/flows/gruneisen.py +++ b/src/atomate2/common/flows/gruneisen.py @@ -40,7 +40,7 @@ class BaseGruneisenMaker(Maker, ABC): generated for all the three structures (ground state, expanded and shrunk volume) and accurate forces are computed for these structures. With the help of phonopy, these forces are then converted into a dynamical matrix. This dynamical matrix of - three structures is then used as an input for the phonopy Grueneisen api + three structures is then used as an input for the phonopy Grueneisen API to compute Grueneisen parameters. diff --git a/src/atomate2/common/flows/phonons.py b/src/atomate2/common/flows/phonons.py index 2604a08dd..95d65e5f0 100644 --- a/src/atomate2/common/flows/phonons.py +++ b/src/atomate2/common/flows/phonons.py @@ -187,7 +187,7 @@ def make( prev_dir : str or Path or None A previous calculation directory to use for copying outputs. born: Matrix3D - Instead of recomputing born charges and epsilon, these values can also be + Instead of recomputing Born charges and epsilon, these values can also be provided manually. If born and epsilon_static are provided, the born run will be skipped it can be provided in the VASP convention with information for every atom in unit cell. Please be careful when converting structures diff --git a/src/atomate2/common/flows/qha.py b/src/atomate2/common/flows/qha.py index 6c5aa64be..74c33209c 100644 --- a/src/atomate2/common/flows/qha.py +++ b/src/atomate2/common/flows/qha.py @@ -25,28 +25,24 @@ from atomate2.common.flows.phonons import BasePhononMaker from atomate2.forcefields.jobs import ForceFieldRelaxMaker from atomate2.vasp.jobs.core import BaseVaspMaker + supported_eos = frozenset(("vinet", "birch_murnaghan", "murnaghan")) @dataclass class CommonQhaMaker(Maker, ABC): - """ - Use the quasi-harmonic approximation. + """Use the quasi-harmonic approximation. - First relax a structure. - Then we scale the relaxed structure, and - then compute harmonic phonons for each scaled - structure with Phonopy. - Finally, we compute the Gibbs free energy and - other thermodynamic properties available from - the quasi-harmonic approximation. + First relax a structure. Then we scale the relaxed structure, and then compute + harmonic phonons for each scaled structure with Phonopy. Finally, we compute the + Gibbs free energy and other thermodynamic properties available from the + quasi-harmonic approximation. Note: We do not consider electronic free energies so far. This might be problematic for metals (see e.g., Wolverton and Zunger, Phys. Rev. B, 52, 8813 (1994).) - Note: Magnetic Materials have never been computed with - this workflow. + Note: Magnetic Materials have never been computed with this workflow. Parameters ---------- @@ -84,7 +80,6 @@ class CommonQhaMaker(Maker, ABC): with 3 90 degree angles get_supercell_size_kwargs: dict kwargs that will be passed to get_supercell_size to determine supercell size - """ name: str = "QHA Maker" @@ -128,10 +123,7 @@ def make( .Flow, a QHA flow """ if self.eos_type not in supported_eos: - raise ValueError( - "EOS not supported.", - "Please choose 'vinet', 'birch_murnaghan', 'murnaghan'", - ) + raise ValueError(f"EOS not supported. Choose one of {set(supported_eos)}") qha_jobs = [] @@ -148,7 +140,7 @@ def make( eos_job = self.eos.make(structure) qha_jobs.append(eos_job) - # implement a supercell job to get matrix for just the equillibrium structure + # implement a supercell job to get matrix for just the equilibrium structure if supercell_matrix is None: supercell = get_supercell_size( eos_output=eos_job.output, @@ -162,7 +154,6 @@ def make( supercell_matrix = supercell.output # pass the matrix to the phonon_jobs, allow to set a consistent matrix instead - phonon_jobs = get_phonon_jobs( phonon_maker=self.phonon_maker, eos_output=eos_job.output, diff --git a/src/atomate2/common/jobs/anharmonicity.py b/src/atomate2/common/jobs/anharmonicity.py index e44aa7f86..5ebace0e9 100644 --- a/src/atomate2/common/jobs/anharmonicity.py +++ b/src/atomate2/common/jobs/anharmonicity.py @@ -44,15 +44,16 @@ class ImaginaryModeError(Exception): def __init__(self, largest_mode: float) -> None: self.largest_mode = largest_mode - self.message = f"""Structure has imaginary modes: the largest optical - eigenmode {largest_mode} < 0.0001""" + self.message = ( + f"Structure has imaginary modes: the largest optical eigenmode " + f"{largest_mode} < 0.0001" + ) super().__init__(self.message) @job def get_phonon_supercell( - structure: Structure, - supercell_matrix: np.ndarray, + structure: Structure, supercell_matrix: np.ndarray ) -> Structure: """Get the phonon supercell of a structure. @@ -68,12 +69,9 @@ def get_phonon_supercell( Structure The phonopy structure """ - cell = get_phonopy_structure(structure) - phonon = Phonopy( - cell, - supercell_matrix, - ) - return get_pmg_structure(phonon.supercell) + unit_cell = get_phonopy_structure(structure) + phonopy = Phonopy(unit_cell, supercell_matrix) + return get_pmg_structure(phonopy.supercell) def get_sigma_per_site( diff --git a/src/atomate2/common/jobs/gruneisen.py b/src/atomate2/common/jobs/gruneisen.py index 8e1fa14c2..d78fceb63 100644 --- a/src/atomate2/common/jobs/gruneisen.py +++ b/src/atomate2/common/jobs/gruneisen.py @@ -17,6 +17,8 @@ from atomate2.common.schemas.phonons import PhononBSDOSDoc if TYPE_CHECKING: + from pathlib import Path + from pymatgen.core.structure import Structure from atomate2.common.flows.phonons import BasePhononMaker @@ -128,11 +130,11 @@ def run_phonon_jobs( ) def compute_gruneisen_param( code: str, - phonopy_yaml_paths_dict: dict, - phonon_imaginary_modes_info: dict, + phonopy_yaml_paths_dict: dict[str, Path], + phonon_imaginary_modes_info: dict[str, bool], kpath_scheme: str, symprec: float, - mesh: tuple | float = (20, 20, 20), + mesh: tuple[int, int, int] | float = (20, 20, 20), structure: Structure = None, **compute_gruneisen_param_kwargs, ) -> GruneisenParameterDocument: diff --git a/src/atomate2/common/jobs/qha.py b/src/atomate2/common/jobs/qha.py index 361086772..51f657cd7 100644 --- a/src/atomate2/common/jobs/qha.py +++ b/src/atomate2/common/jobs/qha.py @@ -56,9 +56,7 @@ def get_supercell_size( ) -@job( - data=[PhononBSDOSDoc], -) +@job(data=[PhononBSDOSDoc]) def get_phonon_jobs( phonon_maker: BasePhononMaker, eos_output: dict, supercell_matrix: list[list[float]] ) -> Flow: diff --git a/src/atomate2/common/schemas/phonons.py b/src/atomate2/common/schemas/phonons.py index 73b53c9e2..99e95c4c1 100644 --- a/src/atomate2/common/schemas/phonons.py +++ b/src/atomate2/common/schemas/phonons.py @@ -201,7 +201,7 @@ class PhononBSDOSDoc(StructureMetadata, extra="allow"): # type: ignore[call-arg born: Optional[list[Matrix3D]] = Field( None, - description="born charges as computed from phonopy. Only for symmetrically " + description="Born charges as computed from phonopy. Only for symmetrically " "different atoms", ) @@ -280,7 +280,7 @@ def from_forces_born( epsilon_static: Matrix3D The high-frequency dielectric constant born: Matrix3D - born charges + Born charges **kwargs: additional arguments """ @@ -329,7 +329,7 @@ def from_forces_born( ) else: raise ValueError( - "Number of born charges does not agree with number of atoms" + "Number of Born charges does not agree with number of atoms" ) if code == "vasp" and not np.all(np.isclose(borns, 0.0)): phonon.nac_params = { @@ -595,7 +595,8 @@ def get_kpath( **kpath_kwargs: additional parameters that can be passed to this method as a dict """ - if kpath_scheme in ("setyawan_curtarolo", "latimer_munro", "hinuma"): + valid_schemes = {"setyawan_curtarolo", "latimer_munro", "hinuma", "seekpath"} + if kpath_scheme in (valid_schemes - {"seekpath"}): high_symm_kpath = HighSymmKpath( structure, path_type=kpath_scheme, symprec=symprec, **kpath_kwargs ) @@ -604,7 +605,9 @@ def get_kpath( high_symm_kpath = KPathSeek(structure, symprec=symprec, **kpath_kwargs) kpath = high_symm_kpath._kpath # noqa: SLF001 else: - raise ValueError(f"Unexpected {kpath_scheme=}") + raise ValueError( + f"Unexpected {kpath_scheme=}, must be one of {valid_schemes}" + ) path = copy.deepcopy(kpath["path"]) diff --git a/tests/vasp/flows/test_phonons.py b/tests/vasp/flows/test_phonons.py index ddb6ac14d..4348dbdba 100644 --- a/tests/vasp/flows/test_phonons.py +++ b/tests/vasp/flows/test_phonons.py @@ -265,66 +265,41 @@ def test_phonon_wf_vasp_only_displacements_kpath( responses = run_locally(job, create_folders=True, ensure_success=True) # validate the outputs - # print(type(responses)) - assert isinstance(responses[job.jobs[-1].uuid][1].output, PhononBSDOSDoc) + ph_doc = responses[job.jobs[-1].uuid][1].output + assert isinstance(ph_doc, PhononBSDOSDoc) assert_allclose( - responses[job.jobs[-1].uuid][1].output.free_energies, + ph_doc.free_energies, [5776.14995034, 5617.74737777, 4725.50269363, 3043.81827626, 694.49078355], atol=1e-3, ) + assert isinstance(ph_doc.phonon_bandstructure, PhononBandStructureSymmLine) + assert isinstance(ph_doc.phonon_dos, PhononDos) + assert isinstance(ph_doc.thermal_displacement_data, ThermalDisplacementData) + assert isinstance(ph_doc.structure, Structure) + assert_allclose(ph_doc.temperatures, [0, 100, 200, 300, 400]) + assert ph_doc.has_imaginary_modes is False + force_const = ph_doc.force_constants.force_constants[0][0][0][0] + assert force_const == pytest.approx(13.032324) + assert isinstance(ph_doc.jobdirs, PhononJobDirs) + assert isinstance(ph_doc.uuids, PhononUUIDs) + assert ph_doc.total_dft_energy is None + assert ph_doc.born is None + assert ph_doc.epsilon_static is None + assert ph_doc.supercell_matrix == ((-1, 1, 1), (1, -1, 1), (1, 1, -1)) + assert ph_doc.primitive_matrix == ((1, 0, 0), (0, 1, 0), (0, 0, 1)) + assert ph_doc.code == "vasp" assert isinstance( - responses[job.jobs[-1].uuid][1].output.phonon_bandstructure, - PhononBandStructureSymmLine, - ) - assert isinstance(responses[job.jobs[-1].uuid][1].output.phonon_dos, PhononDos) - assert isinstance( - responses[job.jobs[-1].uuid][1].output.thermal_displacement_data, - ThermalDisplacementData, - ) - assert isinstance(responses[job.jobs[-1].uuid][1].output.structure, Structure) - assert_allclose( - responses[job.jobs[-1].uuid][1].output.temperatures, [0, 100, 200, 300, 400] - ) - assert responses[job.jobs[-1].uuid][1].output.has_imaginary_modes is False - assert_allclose( - responses[job.jobs[-1].uuid][1].output.force_constants.force_constants[0][0][0][ - 0 - ], - 13.032324, - ) - assert isinstance(responses[job.jobs[-1].uuid][1].output.jobdirs, PhononJobDirs) - assert isinstance(responses[job.jobs[-1].uuid][1].output.uuids, PhononUUIDs) - assert responses[job.jobs[-1].uuid][1].output.total_dft_energy is None - assert responses[job.jobs[-1].uuid][1].output.born is None - assert responses[job.jobs[-1].uuid][1].output.epsilon_static is None - assert_allclose( - responses[job.jobs[-1].uuid][1].output.supercell_matrix, - [[-1.0, 1.0, 1.0], [1.0, -1.0, 1.0], [1.0, 1.0, -1.0]], - ) - assert_allclose( - responses[job.jobs[-1].uuid][1].output.primitive_matrix, - [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - atol=1e-8, - ) - assert responses[job.jobs[-1].uuid][1].output.code == "vasp" - assert isinstance( - responses[job.jobs[-1].uuid][1].output.phonopy_settings, + ph_doc.phonopy_settings, PhononComputationalSettings, ) - assert responses[job.jobs[-1].uuid][1].output.phonopy_settings.npoints_band == 101 - assert ( - responses[job.jobs[-1].uuid][1].output.phonopy_settings.kpath_scheme - == kpath_scheme - ) - assert ( - responses[job.jobs[-1].uuid][1].output.phonopy_settings.kpoint_density_dos - == 7_000 - ) + assert ph_doc.phonopy_settings.npoints_band == 101 + assert ph_doc.phonopy_settings.kpath_scheme == kpath_scheme + assert ph_doc.phonopy_settings.kpoint_density_dos == 7_000 -# test supply of born charges, epsilon, DFT energy, supercell +# test supply of Born charges, epsilon, DFT energy, supercell def test_phonon_wf_vasp_only_displacements_add_inputs_raises( mock_vasp, clean_dir, si_structure: Structure ): @@ -337,16 +312,9 @@ def test_phonon_wf_vasp_only_displacements_add_inputs_raises( # automatically use fake VASP and write POTCAR.spec during the test mock_vasp(ref_paths, fake_run_vasp_kwargs) - born = [ - [[0, 0, 0], [0, 0, 0], [0, 0, 0]], - [[0, 0, 0], [0, 0, 0], [0, 0, 0]], - [[0, 0, 0], [0, 0, 0], [0, 0, 0.1]], - ] - epsilon_static = [ - [5.25, 0, 0], - [0, 5.25, 0], - [0, 0, 5.25], - ] + born = np.zeros((3, 3)) + born[-1, -1] = 0.1 + epsilon_static = 5.25 * np.eye(3) total_dft_energy_per_formula_unit = -5 job = PhononMaker( @@ -368,7 +336,7 @@ def test_phonon_wf_vasp_only_displacements_add_inputs_raises( run_locally(job, create_folders=True, ensure_success=True) -# test supply of born charges, epsilon, DFT energy, supercell +# test supply of Born charges, epsilon, DFT energy, supercell def test_phonon_wf_vasp_only_displacements_add_inputs( mock_vasp, clean_dir, si_structure: Structure ): From 634976608fc5d324e426226ad1e2ccd17797cec3 Mon Sep 17 00:00:00 2001 From: Aaron Kaplan <33381112+esoteric-ephemera@users.noreply.github.com> Date: Tue, 26 Nov 2024 07:59:59 -0800 Subject: [PATCH 5/5] Docs update, forcefield elastic convenience maker, forcefield enum hydration (#1072) * update EOS docs * udpate docs with implementation details * add convenience constructor method for forcefield ElasticMaker * allow MLFF enum to treat str(MLFF.) as valid member * precommit * add small MLFF test --- docs/user/codes/vasp.md | 34 ++++++++++++- src/atomate2/forcefields/__init__.py | 14 +++++ src/atomate2/forcefields/flows/elastic.py | 62 ++++++++++++++++++++--- src/atomate2/forcefields/flows/eos.py | 7 ++- tests/forcefields/flows/test_elastic.py | 20 ++++++-- tests/forcefields/test_utils.py | 6 +++ 6 files changed, 129 insertions(+), 14 deletions(-) diff --git a/docs/user/codes/vasp.md b/docs/user/codes/vasp.md index 5e1e299b4..d9788e1a9 100644 --- a/docs/user/codes/vasp.md +++ b/docs/user/codes/vasp.md @@ -270,9 +270,39 @@ Afterwards, equation of state fits are performed with phonopy. ### Equation of State Workflow -An equation of state workflow is implemented. First, a tight relaxation is performed. Subsequently, several optimizations at different constant +An equation of state (EOS) workflow is implemented. First, a tight relaxation is performed. Subsequently, several optimizations at different constant volumes are performed. Additional static calculations might be performed afterwards to arrive at more -accurate energies. Then, an equation of state fit is performed with pymatgen. +accurate energies. Then, an EOS fit is performed with pymatgen. + +The output of the workflow is, by default, a dictionary containing the energy and volume data generated with DFT, in addition to fitted equation of state parameters for all models currently available in pymatgen (Murnaghan, Birch-Murnaghan, Poirier-Tarantola, and Vinet/UBER). + +#### Materials Project-compliant workflows + +If the user wishes to reproduce the EOS data currently in the Materials Project, they should use the atomate 1-compatible `MPLegacy`-prefixed flows (and jobs and input sets). For performing updated PBE-GGA EOS flows with Materials Project-compliant parameters, the user should use the `MPGGA`-prefixed classes. Lastly, the `MPMetaGGA`-prefixed classes allow the user to perform Materials Project-compliant r2SCAN EOS workflows. + +**Summary:** For Materials Project-compliant equation of state (EOS) workflows, the user should use: +* `MPGGAEosMaker` for faster, lower-accuracy calculation with the PBE-GGA +* `MPMetaGGAEosMaker` for higher-accuracy but slower calculations with the r2SCAN meta-GGA +* `MPLegacyEosMaker` for consistency with the PBE-GGA data currently distributed by the Materials Project + +#### Implementation details + +The Materials Project-compliant EOS flows, jobs, and sets currently use three prefixes to indicate their usage. +* `MPGGA`: MP-compatible PBE-GGA (current) +* `MPMetaGGA`: MP-compatible r2SCAN meta-GGA (current) +* `MPLegacy`: a reproduction of the atomate 1 implementation, described in + K. Latimer, S. Dwaraknath, K. Mathew, D. Winston, and K.A. Persson, npj Comput. Materials **vol. 4**, p. 40 (2018), DOI: 10.1038/s41524-018-0091-x + + For reference, the original atomate workflows can be found here: + * [`atomate.vasp.workflows.base.wf_bulk_modulus`](https://github.com/hackingmaterials/atomate/blob/main/atomate/vasp/workflows/presets/core.py#L564) + * [`atomate.vasp.workflows.base.bulk_modulus.get_wf_bulk_modulus`](https://github.com/hackingmaterials/atomate/blob/main/atomate/vasp/workflows/base/bulk_modulus.py#L21) + +In the original atomate 1 workflow and the atomate2 `MPLegacyEosMaker`, the k-point density is **extremely** high. This is despite the convergence tests in the supplementary information +of Latimer *et al.* not showing strong sensitivity when the "number of ***k***-points per reciprocal atom" (KPPRA) is at least 3,000. + +To make the `MPGGAEosMaker` and `MPMetaGGAEosMaker` more tractable for high-throughput jobs, their input sets (`MPGGAEos{Relax,Static}SetGenerator` and `MPMetaGGAEos{Relax,Static}SetGenerator` respectively) still use the highest ***k***-point density in standard Materials Project jobs, `KSPACING = 0.22` Å-1, which is comparable to KPPRA = 3,000. + +This choice is justified by Fig. S12 of the supplemantary information of Latimer *et al.*, which shows that all fitted EOS parameters (equilibrium energy $E_0$, equilibrium volume $V_0$, bulk modulus $B_0$, and bulk modulus pressure derivative $B_1$) do not deviate by more than 1.5%, and typically by less than 0.1%, from well-converged values when KPPRA = 3,000. ### LOBSTER diff --git a/src/atomate2/forcefields/__init__.py b/src/atomate2/forcefields/__init__.py index e16c08865..59e25c1be 100644 --- a/src/atomate2/forcefields/__init__.py +++ b/src/atomate2/forcefields/__init__.py @@ -3,6 +3,10 @@ from __future__ import annotations from enum import Enum +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any class MLFF(Enum): # TODO inherit from StrEnum when 3.11+ @@ -17,6 +21,16 @@ class MLFF(Enum): # TODO inherit from StrEnum when 3.11+ Nequip = "Nequip" SevenNet = "SevenNet" + @classmethod + def _missing_(cls, value: Any) -> Any: + """Allow input of str(MLFF) as valid enum.""" + if isinstance(value, str): + value = value.split("MLFF.")[-1] + for member in cls: + if member.value == value: + return member + return None + def _get_formatted_ff_name(force_field_name: str | MLFF) -> str: """ diff --git a/src/atomate2/forcefields/flows/elastic.py b/src/atomate2/forcefields/flows/elastic.py index 597f5be22..df2cc945c 100644 --- a/src/atomate2/forcefields/flows/elastic.py +++ b/src/atomate2/forcefields/flows/elastic.py @@ -3,11 +3,24 @@ from __future__ import annotations from dataclasses import dataclass, field +from typing import TYPE_CHECKING from atomate2 import SETTINGS from atomate2.common.flows.elastic import BaseElasticMaker +from atomate2.forcefields import MLFF, _get_formatted_ff_name from atomate2.forcefields.jobs import ForceFieldRelaxMaker +if TYPE_CHECKING: + from typing import Any + + from typing_extensions import Self + +# default options for the forcefield makers in ElasticMaker +_DEFAULT_RELAX_KWARGS: dict[str, Any] = { + "force_field_name": "CHGNet", + "relax_kwargs": {"fmax": 0.00001}, +} + @dataclass class ElasticMaker(BaseElasticMaker): @@ -62,16 +75,12 @@ class ElasticMaker(BaseElasticMaker): symprec: float = SETTINGS.SYMPREC bulk_relax_maker: ForceFieldRelaxMaker | None = field( default_factory=lambda: ForceFieldRelaxMaker( - force_field_name="CHGNet", - relax_cell=True, - relax_kwargs={"fmax": 0.00001}, + relax_cell=True, **_DEFAULT_RELAX_KWARGS ) ) elastic_relax_maker: ForceFieldRelaxMaker | None = field( default_factory=lambda: ForceFieldRelaxMaker( - force_field_name="CHGNet", - relax_cell=False, - relax_kwargs={"fmax": 0.00001}, + relax_cell=False, **_DEFAULT_RELAX_KWARGS ) ) # constant volume relaxation max_failed_deformations: int | float | None = None @@ -89,3 +98,44 @@ def prev_calc_dir_argname(self) -> str | None: Note: this is only applicable if a relax_maker is specified; i.e., two calculations are performed for each ordering (relax -> static) """ + + @classmethod + def from_force_field_name( + cls, + force_field_name: str | MLFF, + mlff_kwargs: dict | None = None, + **kwargs, + ) -> Self: + """ + Create an elastic flow from a forcefield name. + + Parameters + ---------- + force_field_name : str or .MLFF + The name of the force field. + mlff_kwargs : dict or None (default) + kwargs to pass to `ForceFieldRelaxMaker`. + **kwargs + Additional kwargs to pass to ElasticMaker. + + Returns + ------- + ElasticMaker + """ + default_kwargs: dict[str, Any] = { + **_DEFAULT_RELAX_KWARGS, + **(mlff_kwargs or {}), + "force_field_name": _get_formatted_ff_name(force_field_name), + } + return cls( + name=f"{str(force_field_name).split('MLFF.')[-1]} elastic", + bulk_relax_maker=ForceFieldRelaxMaker( + relax_cell=True, + **default_kwargs, + ), + elastic_relax_maker=ForceFieldRelaxMaker( + relax_cell=False, + **default_kwargs, + ), + **kwargs, + ) diff --git a/src/atomate2/forcefields/flows/eos.py b/src/atomate2/forcefields/flows/eos.py index 184498a0d..21b39a75e 100644 --- a/src/atomate2/forcefields/flows/eos.py +++ b/src/atomate2/forcefields/flows/eos.py @@ -1,4 +1,4 @@ -"""Flows to generate EOS fits using CHGNet, M3GNet, or MACE.""" +"""Flows to generate EOS fits using machine learned interatomic potentials.""" from __future__ import annotations @@ -62,6 +62,7 @@ def from_force_field_name( cls, force_field_name: str | MLFF, relax_initial_structure: bool = True, + **kwargs, ) -> Self: """ Create an EOS flow from a forcefield name. @@ -72,6 +73,9 @@ def from_force_field_name( The name of the force field. relax_initial_structure: bool = True Whether to relax the initial structure before performing an EOS fit. + **kwargs + Additional kwargs to pass to ElasticMaker + Returns ------- @@ -89,6 +93,7 @@ def from_force_field_name( force_field_name=force_field_name, relax_cell=False ), static_maker=None, + **kwargs, ) diff --git a/tests/forcefields/flows/test_elastic.py b/tests/forcefields/flows/test_elastic.py index 1a64df267..ac3000001 100644 --- a/tests/forcefields/flows/test_elastic.py +++ b/tests/forcefields/flows/test_elastic.py @@ -7,7 +7,10 @@ from atomate2.forcefields.jobs import ForceFieldRelaxMaker -def test_elastic_wf_with_mace(clean_dir, si_structure, test_dir): +@pytest.mark.parametrize("convenience_constructor", [True, False]) +def test_elastic_wf_with_mace( + clean_dir, si_structure, test_dir, convenience_constructor: bool +): si_prim = SpacegroupAnalyzer(si_structure).get_primitive_standard_structure() model_path = f"{test_dir}/forcefields/mace/MACE.model" common_kwds = { @@ -16,10 +19,17 @@ def test_elastic_wf_with_mace(clean_dir, si_structure, test_dir): "relax_kwargs": {"fmax": 0.00001}, } - flow = ElasticMaker( - bulk_relax_maker=ForceFieldRelaxMaker(**common_kwds, relax_cell=True), - elastic_relax_maker=ForceFieldRelaxMaker(**common_kwds, relax_cell=False), - ).make(si_prim) + if convenience_constructor: + common_kwds.pop("force_field_name") + flow = ElasticMaker.from_force_field_name( + force_field_name="MACE", + mlff_kwargs=common_kwds, + ).make(si_prim) + else: + flow = ElasticMaker( + bulk_relax_maker=ForceFieldRelaxMaker(**common_kwds, relax_cell=True), + elastic_relax_maker=ForceFieldRelaxMaker(**common_kwds, relax_cell=False), + ).make(si_prim) # run the flow or job and ensure that it finished running successfully responses = run_locally(flow, create_folders=True, ensure_success=True) diff --git a/tests/forcefields/test_utils.py b/tests/forcefields/test_utils.py index 023482ea2..b43eb6ff0 100644 --- a/tests/forcefields/test_utils.py +++ b/tests/forcefields/test_utils.py @@ -4,6 +4,12 @@ from atomate2.forcefields.utils import ase_calculator +@pytest.mark.parametrize(("force_field"), [mlff.value for mlff in MLFF]) +def test_mlff(force_field: str): + mlff = MLFF(force_field) + assert mlff == MLFF(str(mlff)) == MLFF(str(mlff).split(".")[-1]) + + @pytest.mark.parametrize(("force_field"), ["CHGNet", "MACE"]) def test_ext_load(force_field: str): decode_dict = {