diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index f2cc2a1b32..640f542e7e 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -60,16 +60,26 @@ jobs: run: | micromamba install -n a2 -c conda-forge enumlib packmol bader --yes + - name: Install ALA-Mode + run: | + micromamba activate a2 + micromamba install -n a2 -c conda-forge "numpy<=2.2" scipy h5py compilers “libblas=*=*mkl” spglib boost eigen cmake ipython mkl-include openmpi --yes + git clone https://github.com/ttadano/ALM.git # do I need to modify this? + cd ALM # do I need to modify this? + cd python + python setup.py build # do I need to modify this? + uv pip install -e . # do I need to modify this? + - name: Install dependencies run: | micromamba activate a2 python -m pip install --upgrade pip mkdir -p ~/.abinit/pseudos cp -r tests/test_data/abinit/pseudos/ONCVPSP-PBE-SR-PDv0.4 ~/.abinit/pseudos - uv pip install .[strict,strict-forcefields,tests,abinit,approxneb] + uv pip install .[strict,strict-forcefields,tests,abinit,approxneb,pheasy] uv pip install torch-runstats torch_dftd uv pip install --no-deps nequip==0.5.6 - + - name: Install pymatgen from master if triggered by pymatgen repo dispatch if: github.event_name == 'repository_dispatch' && github.event.action == 'pymatgen-ci-trigger' run: | diff --git a/pyproject.toml b/pyproject.toml index d8db4332c9..36b213bef3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "PyYAML", "click", "custodian>=2024.4.18", - "emmet-core>=v0.84.10rc2", + "emmet-core>=v0.84.10rc2,<0.85.0rc0", "jobflow>=0.1.11", "monty>=2024.12.10", "numpy", @@ -44,6 +44,7 @@ amset = ["amset>=0.4.15", "pydash"] cclib = ["cclib>=1.8.1"] mp = ["mp-api>=0.37.5"] phonons = ["phonopy>=1.10.8", "seekpath>=2.0.0"] +pheasy = ["hiphive==1.3.1", "numpy<=2.2", "pheasy==0.0.2"] lobster = ["ijson>=3.2.2", "lobsterpy>=0.4.0"] defects = [ "dscribe>=1.2.0", diff --git a/src/atomate2/common/flows/pheasy.py b/src/atomate2/common/flows/pheasy.py new file mode 100644 index 0000000000..e68f3ab37d --- /dev/null +++ b/src/atomate2/common/flows/pheasy.py @@ -0,0 +1,364 @@ +"""Flow for calculating (an)harmonic FCs and phonon energy renorma. with pheasy.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Literal + +from atomate2.common.flows.phonons import BasePhononMaker as PurePhonopyMaker +from atomate2.common.jobs.pheasy import ( + generate_frequencies_eigenvectors, + generate_phonon_displacements, + get_supercell_size, + run_phonon_displacements, +) + +if TYPE_CHECKING: + from pathlib import Path + + from emmet.core.math import Matrix3D + from jobflow import Flow, Job + from pymatgen.core.structure import Structure + + from atomate2.aims.jobs.base import BaseAimsMaker + from atomate2.forcefields.jobs import ForceFieldRelaxMaker, ForceFieldStaticMaker + from atomate2.vasp.jobs.base import BaseVaspMaker + +SUPPORTED_CODES = frozenset(("vasp", "aims", "forcefields")) + + +@dataclass +class BasePhononMaker(PurePhonopyMaker, ABC): + """Maker to calculate harmonic phonons with LASSO-based ML code Pheasy. + + Calculate the zero-K harmonic phonons of a material and higher-order FCs. + Initially, a tight structural relaxation is performed to obtain a structure + without forces on the atoms. Subsequently, supercells with all atoms displaced + by a small amplitude (generally using 0.01 A) are generated and accurate forces + are computed for these structures for the second order force constants. With the + help of pheasy (LASSO technique), these forces are then converted into a dynamical + matrix. In this Workflow, we separate the harmonic phonon calculations and + anharmonic force constants calculations. To correct for polarization effects, a + correction of the dynamical matrix based on BORN charges can be performed. Finally, + phonon densities of states, phonon band structures and thermodynamic properties + are computed. For the anharmonic force constants, the supercells with all atoms + displaced by a larger amplitude (generally using 0.08 A) are generated and accurate + forces are computed for these structures. With the help of pheasy (LASSO technique), + the third- and fourth-order force constants are extracted at once. + + .. Note:: + It is heavily recommended to symmetrize the structure before passing it to + this flow. Otherwise, a different space group might be detected and too many + displacement calculations will be required for pheasy phonon calculation. It + is recommended to check the convergence parameters here and adjust them if + necessary. The default might not be strict enough for your specific case. + Additionally, for high-throughoput calculations, it is recommended to calculate + the residual forces on the atoms in the supercell after the relaxation. Then the + forces on displaced supercells can deduct the residual forces to reduce the + error in the dynamical matrix. + + Parameters + ---------- + name : str + Name of the flow produced by this maker. + sym_reduce : bool + Whether to reduce the number of deformations using symmetry. + symprec : float + Symmetry precision to use in the + reduction of symmetry to find the primitive/conventional cell + (use_primitive_standard_structure, use_conventional_standard_structure) + and to handle all symmetry-related tasks in pheasy, we recommend to + use the value of 1e-3. + displacement: float + displacement distance for phonons, for most cases 0.01 A is a good choice, + but it can be increased to 0.02 A for heavier elements. + num_displaced_supercells: int + number of displacements to be generated using a random-displacement approach + for harmonic phonon calculations. The default value is 0 and the number of + displacements is automatically determined by the number of atoms in the + supercell and its space group. + cal_anhar_fcs: bool + if set to True, anharmonic force constants(FCs) up to fourth-order FCs will + be calculated. The default value is False, and only harmonic phonons will + be calculated. + displacement_anhar: float + displacement distance for anharmonic force constants(FCs) up to fourth-order + FCs, for most cases 0.08 A is a good choice, but it can be increased to 0.1 A. + num_disp_anhar: int + number of displacements to be generated using a random-displacement approach + for anharmonic phonon calculations. The default value is 0 and the number of + displacements is automatically determined by the number of atoms in the + supercell, cutoff distance for anharmonic FCs its space group. generally, + 50 large-distance displacements are enough for most cases. + fcs_cutoff_radius: list + cutoff distance for anharmonic force constants(FCs) up to fourth-order FCs. + The default value is [-1, 12, 10], which means that the cutoff distance for + second-order FCs is the Wigner-Seitz cell boundary and the cutoff distance + for third-order FCs is 12 Borh, and the cutoff distance for fourth-order FCs + is 10 Bohr. Generally, the default value is good enough. + min_length: float + minimum length of lattice constants will be used to create the supercell, + the default value is 14.0 A. In most cases, the default value is good + enough, but it can be increased for larger supercells. + prefer_90_degrees: bool + if set to True, supercell algorithm will first try to find a supercell + with 3 90 degree angles. + get_supercell_size_kwargs: dict + kwargs that will be passed to get_supercell_size to determine supercell size + use_symmetrized_structure: str + allowed strings: "primitive", "conventional", None + + - "primitive" will enforce to start the phonon computation + from the primitive standard structure + according to Setyawan, W., & Curtarolo, S. (2010). + High-throughput electronic band structure calculations: + Challenges and tools. Computational Materials Science, + 49(2), 299-312. doi:10.1016/j.commatsci.2010.05.010. + This makes it possible to use certain k-path definitions + with this workflow. Otherwise, we must rely on seekpath + - "conventional" will enforce to start the phonon computation + from the conventional standard structure + according to Setyawan, W., & Curtarolo, S. (2010). + High-throughput electronic band structure calculations: + Challenges and tools. Computational Materials Science, + 49(2), 299-312. doi:10.1016/j.commatsci.2010.05.010. + We will however use seekpath and primitive structures + as determined by from phonopy to compute the phonon band structure + bulk_relax_maker: .ForceFieldRelaxMaker, .BaseAimsMaker, .BaseVaspMaker, or None + A maker to perform a tight relaxation on the bulk. + Set to ``None`` to skip the + bulk relaxation + static_energy_maker: .ForceFieldRelaxMaker, .BaseAimsMaker, .BaseVaspMaker, or None + A maker to perform the computation of the DFT energy on the bulk. + Set to ``None`` to skip the + static energy computation + born_maker: .ForceFieldStaticMaker, .BaseAsimsMaker, .BaseVaspMaker, or None + Maker to compute the BORN charges. + phonon_displacement_maker: .ForceFieldStaticMaker, .BaseAimsMaker, .BaseVaspMaker + Maker used to compute the forces for a supercell. + generate_frequencies_eigenvectors_kwargs : dict + Keyword arguments passed to :obj:`generate_frequencies_eigenvectors`. + create_thermal_displacements: bool + Bool that determines if thermal_displacement_matrices are computed + kpath_scheme: str + scheme to generate kpoints. Please be aware that + you can only use seekpath with any kind of cell + Otherwise, please use the standard primitive structure + Available schemes are: + "seekpath", "hinuma", "setyawan_curtarolo", "latimer_munro". + "seekpath" and "hinuma" are the same definition but + seekpath can be used with any kind of unit cell as + it relies on phonopy to handle the relationship + to the primitive cell and not pymatgen + code: str + determines the dft or force field code. + store_force_constants: bool + if True, force constants will be stored + socket: bool + If True, use the socket for the calculation + + """ + + name: str = "phonon" + sym_reduce: bool = True + symprec: float = 1e-3 + displacement: float = 0.01 + num_displaced_supercells: int = 0 + cal_anhar_fcs: bool = False + displacement_anhar: float = 0.08 + num_disp_anhar: int = 0 + fcs_cutoff_radius: list = field( + default_factory=lambda: [-1, 12, 10] + ) # units in Bohr + renorm_phonon: bool = False + renorm_temp: list = field(default_factory=lambda: [100, 700, 100]) + cal_ther_cond: bool = False + ther_cond_mesh: list = field(default_factory=lambda: [20, 20, 20]) + ther_cond_temp: list = field(default_factory=lambda: [100, 700, 100]) + min_length: float | None = 8.0 + max_atoms: float | None = 200 + force_90_degrees: bool = True + force_diagonal: bool = True + get_supercell_size_kwargs: dict = field(default_factory=dict) + use_symmetrized_structure: Literal["primitive", "conventional"] | None = None + bulk_relax_maker: ForceFieldRelaxMaker | BaseVaspMaker | BaseAimsMaker | None = None + static_energy_maker: ForceFieldRelaxMaker | BaseVaspMaker | BaseAimsMaker | None = ( + None + ) + born_maker: ForceFieldStaticMaker | BaseVaspMaker | None = None + phonon_displacement_maker: ForceFieldStaticMaker | BaseVaspMaker | BaseAimsMaker = ( + None + ) + create_thermal_displacements: bool = False + generate_frequencies_eigenvectors_kwargs: dict = field(default_factory=dict) + kpath_scheme: str = "seekpath" + code: str = None + store_force_constants: bool = True + socket: bool = False + + def get_displacements( + self, structure: Structure, supercell_matrix: Matrix3D + ) -> Job | Flow: + """ + Get displaced supercells. + + Parameters + ---------- + structure: Structure + supercell_matrix: Matrix3D + + Returns + ------- + Job|Flow + """ + return generate_phonon_displacements( + structure=structure, + supercell_matrix=supercell_matrix, + displacement=self.displacement, + num_displaced_supercells=self.num_displaced_supercells, + cal_anhar_fcs=self.cal_anhar_fcs, + displacement_anhar=self.displacement_anhar, + num_disp_anhar=self.num_disp_anhar, + fcs_cutoff_radius=self.fcs_cutoff_radius, + sym_reduce=self.sym_reduce, + symprec=self.symprec, + use_symmetrized_structure=self.use_symmetrized_structure, + kpath_scheme=self.kpath_scheme, + code=self.code, + ) + + def run_displacements( + self, + displacements: Job | Flow, + prev_dir: str | Path | None, + structure: Structure, + supercell_matrix: Matrix3D, + ) -> Job | Flow: + """ + Perform displacement calculations. + + Parameters + ---------- + displacements: Job | Flow + prev_dir: str | Path | None + structure: Structure + supercell_matrix: Matrix3D + + Returns + ------- + Job | Flow + """ + # perform the phonon displacement calculations + return run_phonon_displacements( + displacements=displacements.output, + structure=structure, + supercell_matrix=supercell_matrix, + phonon_maker=self.phonon_displacement_maker, + socket=self.socket, + prev_dir_argname=self.prev_calc_dir_argname, + prev_dir=prev_dir, + ) + + def get_results( + self, + born: Matrix3D, + born_run_job_dir: str, + born_run_uuid: str, + displacement_calcs: Job | Flow, + epsilon_static: Matrix3D, + optimization_run_job_dir: str, + optimization_run_uuid: str, + static_run_job_dir: str, + static_run_uuid: str, + structure: Structure, + supercell_matrix: Matrix3D | None, + total_dft_energy: float, + ) -> Job | Flow: + """ + Calculate the harmonic phonons etc. + + Parameters + ---------- + born: Matrix3D + born_run_job_dir: str + born_run_uuid: str + displacement_calcs: Job | Flow + epsilon_static: Matrix3D + optimization_run_job_dir:str + optimization_run_uuid:str + static_run_job_dir:str + static_run_uuid:str + structure: Structure + supercell_matrix: Matrix3D + total_dft_energy: float + + Returns + ------- + Job | Flow + """ + return generate_frequencies_eigenvectors( + supercell_matrix=supercell_matrix, + displacement=self.displacement, + num_displaced_supercells=self.num_displaced_supercells, + cal_anhar_fcs=self.cal_anhar_fcs, + displacement_anhar=self.displacement_anhar, + num_disp_anhar=self.num_disp_anhar, + fcs_cutoff_radius=self.fcs_cutoff_radius, + renorm_phonon=self.renorm_phonon, + renorm_temp=self.renorm_temp, + cal_ther_cond=self.cal_ther_cond, + ther_cond_mesh=self.ther_cond_mesh, + ther_cond_temp=self.ther_cond_temp, + sym_reduce=self.sym_reduce, + symprec=self.symprec, + use_symmetrized_structure=self.use_symmetrized_structure, + kpath_scheme=self.kpath_scheme, + code=self.code, + structure=structure, + displacement_data=displacement_calcs.output, + epsilon_static=epsilon_static, + born=born, + total_dft_energy=total_dft_energy, + static_run_job_dir=static_run_job_dir, + static_run_uuid=static_run_uuid, + born_run_job_dir=born_run_job_dir, + born_run_uuid=born_run_uuid, + optimization_run_job_dir=optimization_run_job_dir, + optimization_run_uuid=optimization_run_uuid, + create_thermal_displacements=self.create_thermal_displacements, + store_force_constants=self.store_force_constants, + **self.generate_frequencies_eigenvectors_kwargs, + ) + + def get_supercell_matrix(self, structure: Structure) -> Job | Flow: + """ + Get supercell matrix. + + Parameters + ---------- + structure: Structure + + Returns + ------- + Job|Flow + """ + return get_supercell_size( + structure, + self.min_length, + self.max_atoms, + self.force_90_degrees, + self.force_diagonal, + ) + + @property + @abstractmethod + def prev_calc_dir_argname(self) -> str | None: + """Name of argument informing static maker of previous calculation directory. + + As this differs between different DFT codes (e.g., VASP, CP2K), it + has been left as a property to be implemented by the inheriting class. + + Note: this is only applicable if a relax_maker is specified; i.e., two + calculations are performed for each ordering (relax -> static) + """ diff --git a/src/atomate2/common/flows/phonons.py b/src/atomate2/common/flows/phonons.py index 9e6a21c90b..1469ecd48b 100644 --- a/src/atomate2/common/flows/phonons.py +++ b/src/atomate2/common/flows/phonons.py @@ -23,6 +23,7 @@ from typing import Literal from emmet.core.math import Matrix3D + from jobflow import Job from pymatgen.core.structure import Structure from atomate2.aims.jobs.base import BaseAimsMaker @@ -268,14 +269,7 @@ def make( # if supercell_matrix is None, supercell size will be determined after relax # maker to ensure that cell lengths are really larger than threshold if supercell_matrix is None: - supercell_job = get_supercell_size( - structure=structure, - min_length=self.min_length, - max_length=self.max_length, - prefer_90_degrees=self.prefer_90_degrees, - allow_orthorhombic=self.allow_orthorhombic, - **self.get_supercell_size_kwargs, - ) + supercell_job = self.get_supercell_matrix(structure) jobs.append(supercell_job) supercell_matrix = supercell_job.output @@ -306,27 +300,12 @@ def make( total_dft_energy = compute_total_energy_job.output # get a phonon object from phonopy - displacements = generate_phonon_displacements( - structure=structure, - supercell_matrix=supercell_matrix, - displacement=self.displacement, - sym_reduce=self.sym_reduce, - symprec=self.symprec, - use_symmetrized_structure=self.use_symmetrized_structure, - kpath_scheme=self.kpath_scheme, - code=self.code, - ) + displacements = self.get_displacements(structure, supercell_matrix) jobs.append(displacements) # perform the phonon displacement calculations - displacement_calcs = run_phonon_displacements( - displacements=displacements.output, - structure=structure, - supercell_matrix=supercell_matrix, - phonon_maker=self.phonon_displacement_maker, - socket=self.socket, - prev_dir_argname=self.prev_calc_dir_argname, - prev_dir=prev_dir, + displacement_calcs = self.run_displacements( + displacements, prev_dir, structure, supercell_matrix ) jobs.append(displacement_calcs) @@ -349,7 +328,85 @@ def make( born_run_job_dir = born_job.output.dir_name born_run_uuid = born_job.output.uuid - phonon_collect = generate_frequencies_eigenvectors( + phonon_collect = self.get_results( + born, + born_run_job_dir, + born_run_uuid, + displacement_calcs, + epsilon_static, + optimization_run_job_dir, + optimization_run_uuid, + static_run_job_dir, + static_run_uuid, + structure, + supercell_matrix, + total_dft_energy, + ) + + jobs.append(phonon_collect) + + # create a flow including all jobs for a phonon computation + return Flow(jobs, phonon_collect.output) + + def get_supercell_matrix(self, structure: Structure) -> Job | Flow: + """ + Get supercell matrix. + + Parameters + ---------- + structure: Structure + + Returns + ------- + Job|Flow + """ + return get_supercell_size( + structure=structure, + min_length=self.min_length, + max_length=self.max_length, + prefer_90_degrees=self.prefer_90_degrees, + allow_orthorhombic=self.allow_orthorhombic, + **self.get_supercell_size_kwargs, + ) + + def get_results( + self, + born: Matrix3D, + born_run_job_dir: str, + born_run_uuid: str, + displacement_calcs: Job | Flow, + epsilon_static: Matrix3D, + optimization_run_job_dir: str, + optimization_run_uuid: str, + static_run_job_dir: str, + static_run_uuid: str, + structure: Structure, + supercell_matrix: Matrix3D | None, + total_dft_energy: float, + ) -> Job | Flow: + """ + Calculate the harmonic phonons etc. + + Parameters + ---------- + born: Matrix3D + born_run_job_dir: str + born_run_uuid: str + displacement_calcs: Job | Flow + epsilon_static: Matrix3D + optimization_run_job_dir:str + optimization_run_uuid:str + static_run_job_dir:str + static_run_uuid:str + structure: Structure + supercell_matrix: Matrix3D + total_dft_energy: float + + Returns + ------- + Job | Flow + """ + return generate_frequencies_eigenvectors( supercell_matrix=supercell_matrix, displacement=self.displacement, sym_reduce=self.sym_reduce, @@ -373,10 +430,62 @@ def make( **self.generate_frequencies_eigenvectors_kwargs, ) - jobs.append(phonon_collect) + def run_displacements( + self, + displacements: Job | Flow, + prev_dir: str | Path | None, + structure: Structure, + supercell_matrix: Matrix3D, + ) -> Job | Flow: + """ + Perform displacement calculations. - # create a flow including all jobs for a phonon computation - return Flow(jobs, phonon_collect.output) + Parameters + ---------- + displacements: Job | Flow + prev_dir: str | Path | None + structure: Structure + supercell_matrix: Matrix3D + + Returns + ------- + Job | Flow + """ + return run_phonon_displacements( + displacements=displacements.output, + structure=structure, + supercell_matrix=supercell_matrix, + phonon_maker=self.phonon_displacement_maker, + socket=self.socket, + prev_dir_argname=self.prev_calc_dir_argname, + prev_dir=prev_dir, + ) + + def get_displacements( + self, structure: Structure, supercell_matrix: Matrix3D + ) -> Job | Flow: + """ + Get displaced supercells. + + Parameters + ---------- + structure: Structure + supercell_matrix: Matrix3D + + Returns + ------- + Job|Flow + """ + return generate_phonon_displacements( + structure=structure, + supercell_matrix=supercell_matrix, + displacement=self.displacement, + sym_reduce=self.sym_reduce, + symprec=self.symprec, + use_symmetrized_structure=self.use_symmetrized_structure, + kpath_scheme=self.kpath_scheme, + code=self.code, + ) @property @abstractmethod diff --git a/src/atomate2/common/jobs/pheasy.py b/src/atomate2/common/jobs/pheasy.py new file mode 100644 index 0000000000..5c35c39c7f --- /dev/null +++ b/src/atomate2/common/jobs/pheasy.py @@ -0,0 +1,473 @@ +"""Jobs for running phonon calculations.""" + +from __future__ import annotations + +import contextlib +import logging +import warnings +from typing import TYPE_CHECKING + +import numpy as np +from jobflow import Flow, Response, job +from phonopy import Phonopy +from pymatgen.core import Structure +from pymatgen.io.phonopy import get_phonopy_structure, get_pmg_structure +from pymatgen.phonon.bandstructure import PhononBandStructureSymmLine +from pymatgen.phonon.dos import PhononDos +from pymatgen.transformations.advanced_transformations import ( + CubicSupercellTransformation, +) + +from atomate2.common.schemas.phonons import get_factor + +if TYPE_CHECKING: + from pathlib import Path + + from emmet.core.math import Matrix3D + + from atomate2.aims.jobs.base import BaseAimsMaker + from atomate2.forcefields.jobs import ForceFieldStaticMaker + from atomate2.vasp.jobs.base import BaseVaspMaker + +# move to here to avoid circular import +from atomate2.common.schemas.pheasy import Forceconstants, PhononBSDOSDoc + +logger = logging.getLogger(__name__) + + +@job +def get_supercell_size( + structure: Structure, + min_length: float, + max_atoms: int, + force_90_degrees: bool, + force_diagonal: bool, +) -> list[list[float]]: + """ + Determine supercell size with given min_length and max_length. + + Parameters + ---------- + structure: Structure Object + Input structure that will be used to determine supercell + min_length: float + minimum length of cell in Angstrom + max_length: float + maximum length of cell in Angstrom + prefer_90_degrees: bool + if True, the algorithm will try to find a cell with 90 degree angles first + allow_orthorhombic: bool + if True, orthorhombic supercells are allowed + **kwargs: + Additional parameters that can be set. + """ + transformation = CubicSupercellTransformation( + min_length=min_length, + max_atoms=max_atoms, + force_90_degrees=force_90_degrees, + force_diagonal=force_diagonal, + angle_tolerance=1e-2, + allow_orthorhombic=False, + ) + transformation.apply_transformation(structure=structure) + return transformation.transformation_matrix.transpose().tolist() + + +@job(data=[Structure]) +def generate_phonon_displacements( + structure: Structure, + supercell_matrix: np.array, + displacement: float, + num_displaced_supercells: int, + cal_anhar_fcs: bool, + displacement_anhar: float, + num_disp_anhar: int, + fcs_cutoff_radius: list[int], + sym_reduce: bool, + symprec: float, + use_symmetrized_structure: str | None, + kpath_scheme: str, + code: str, +) -> list[Structure]: + """ + + Generate small-distance perturbed structures with phonopy based on two ways. + + (we will directly use the pheasy to generate the supercell in the near future) + 1. finite-displacment method (one displaced atom) when the displacement number + is less than 3. 2. random-displacement method (all-displaced atoms) when the + displacement number is more than 3. + + Parameters + ---------- + structure: Structure object + Fully optimized input structure for phonon run + supercell_matrix: np.array + array to describe supercell matrix + displacement: float + displacement in Angstrom (default: 0.01) + num_displaced_supercells: int + number of displaced supercells defined by users + sym_reduce: bool + if True, symmetry will be used to generate displacements + symprec: float + precision to determine symmetry + use_symmetrized_structure: str or None + primitive, conventional or None + kpath_scheme: str + scheme to generate kpath + code: + code to perform the computations + + """ + warnings.warn( + "Initial magnetic moments will not be considered for the determination " + "of the symmetry of the structure and thus will be removed now.", + stacklevel=1, + ) + cell = get_phonopy_structure( + structure.remove_site_property(property_name="magmom") + if "magmom" in structure.site_properties + else structure + ) + factor = get_factor(code) + + # a bit of code repetition here as I currently + # do not see how to pass the phonopy object? + if use_symmetrized_structure == "primitive" and kpath_scheme == "seekpath": + primitive_matrix: np.ndarray | str = np.eye(3) + else: + primitive_matrix = "auto" + + # TARP: THIS IS BAD! Including for discussions sake + if cell.magnetic_moments is not None and primitive_matrix == "auto": + if np.any(cell.magnetic_moments != 0.0): + raise ValueError( + "For materials with magnetic moments, " + "use_symmetrized_structure must be 'primitive'" + ) + cell.magnetic_moments = None + # create the phonopy object to get some information + # for the displacement generation in ALM code. + phonon = Phonopy( + cell, + supercell_matrix, + primitive_matrix=primitive_matrix, + factor=factor, + symprec=symprec, + is_symmetry=sym_reduce, + ) + + # 1. the ALM module is used to determine how many free parameters + # (irreducible force constants) of second order force constants (FCs) + # within the supercell. + # 2. Based on the number of free parameters, we can determine how many + # displaced supercells we need to use to extract the second order force + # constants. Generally, the number of free parameters should be less than + # 3 * natom(supercell) * num_displaced_supercells. However, the full rank + # of matrix can not always guarantee the accurate result sometimes, you + # may need to displace more random configurations. At least use one or + # two more configurations based on the suggested number of displacements. + + try: + from alm import ALM + except ImportError: + logger.exception( + "Error importing ALM. Please ensure the 'alm' library is installed." + ) + + supercell_ph = phonon.supercell + lattice = supercell_ph.cell + positions = supercell_ph.scaled_positions + numbers = supercell_ph.numbers + natom = len(numbers) + + # get the number of free parameters of 2ND FCs from ALM, labeled as n_fp + with ALM(lattice, positions, numbers) as alm: + alm.define(1) + alm.suggest() + n_fp = alm._get_number_of_irred_fc_elements(1) # noqa: SLF001 + + # get the number of displaced supercells based on the number of free parameters + num = int(np.ceil(n_fp / (3.0 * natom))) + + # get the number of displaced supercells from phonopy to compared with the number + # of 3, if the number of displaced supercells is less than 3, we will use the finite + # displacement method to generate the supercells. Otherwise, we will use the random + # displacement method to generate the supercells. + phonon.generate_displacements(distance=displacement) + num_disp_f = len(phonon.displacements) + if num_disp_f > 3: + num_d = int(np.ceil(num * 1.8)) + 1 + else: + pass + + logger.info( + "The number of free parameters of Second Order Force Constants is %s", n_fp + ) + logger.info("") + + logger.info( + "The Number of Equations Used to Obtain the 2ND FCs is %s", 3 * natom * num + ) + logger.info("") + + logger.warning( + "Be Careful!!! Full Rank of Matrix cannot always guarantee the correct result\ + sometimes.\n" + "If the total atoms in the supercell are less than 100 and\n" + "lattice constants are less than 10 angstroms,\n" + "I highly suggest displacing more random configurations.\n" + "At least use one or two more configurations based on the suggested\ + number of displacements." + ) + logger.info("") + + if num_disp_f > 3: + if num_displaced_supercells != 0: + phonon.generate_displacements( + distance=displacement, + number_of_snapshots=num_displaced_supercells, + random_seed=103, + ) + else: + phonon.generate_displacements( + distance=displacement, + number_of_snapshots=num_d, + random_seed=103, + ) + else: + pass + + supercells = phonon.supercells_with_displacements + displacements = [get_pmg_structure(cell) for cell in supercells] + + # Here, the ALM module is used to determine how many free parameters of third and + # fourth order force constants (FCs) within the specific supercell. + if cal_anhar_fcs: + # Due to the cutoff radius of the force constants use the unit of Borh in ALM, + # we need to convert the cutoff radius from Angstrom to Bohr. + with ALM(lattice * 1.89, positions, numbers) as alm: + # Define the force constants up to fourth order with a list of + # cutoff radius + alm.define(3, fcs_cutoff_radius) + # Perform symmetry analysis and suggest irreducible force constants. + alm.suggest() + # Get the number of irreducible elements for both 3RD- and 4TH-order + # force constants + n_rd_anh = alm._get_number_of_irred_fc_elements( # noqa: SLF001 + 2 + ) + alm._get_number_of_irred_fc_elements(3) # noqa: SLF001 + # we can determine how many displaced supercells we need to use to extract + # the 3rd and 4th order force constants, and we can add a scaling factor + # to reduce the number of displaced supercells due to we use the lasso + # technique. + num_d_anh = int(np.ceil(n_rd_anh / (3.0 * natom))) + num_dis_cells_anhar = num_disp_anhar if num_disp_anhar != 0 else num_d_anh + + num_dis_cells_anhar = 20 + # generate the supercells for anharmonic force constants + phonon.generate_displacements( + distance=displacement_anhar, + number_of_snapshots=num_dis_cells_anhar, + random_seed=103, + ) + supercells = phonon.supercells_with_displacements + displacements += [get_pmg_structure(cell) for cell in supercells] + else: + pass + + # add the equilibrium structure to the list for calculating + # the residual forces. + displacements.append(get_pmg_structure(phonon.supercell)) + return displacements + + +@job( + output_schema=PhononBSDOSDoc, + data=[PhononDos, PhononBandStructureSymmLine, Forceconstants], +) +def generate_frequencies_eigenvectors( + structure: Structure, + supercell_matrix: np.array, + displacement: float, + displacement_anhar: float, + num_displaced_supercells: int, + num_disp_anhar: int, + cal_anhar_fcs: bool, + fcs_cutoff_radius: list[int], + renorm_phonon: bool, + renorm_temp: list[int], + cal_ther_cond: bool, + ther_cond_mesh: list[int], + ther_cond_temp: list[int], + sym_reduce: bool, + symprec: float, + use_symmetrized_structure: str | None, + kpath_scheme: str, + code: str, + displacement_data: dict[str, list], + total_dft_energy: float, + epsilon_static: Matrix3D = None, + born: Matrix3D = None, + **kwargs, +) -> PhononBSDOSDoc: + """ + Analyze the phonon runs and summarize the results. + + Parameters + ---------- + structure: Structure object + Fully optimized structure used for phonon runs + supercell_matrix: np.array + array to describe supercell + displacement: float + displacement in Angstrom used for supercell computation + sym_reduce: bool + if True, symmetry will be used in phonopy + symprec: float + precision to determine symmetry + use_symmetrized_structure: str + primitive, conventional, None are allowed + kpath_scheme: str + kpath scheme for phonon band structure computation + code: str + code to run computations + displacement_data: dict + outputs from displacements + total_dft_energy: float + total DFT energy in eV per cell + epsilon_static: Matrix3D + The high-frequency dielectric constant + born: Matrix3D + Born charges + kwargs: dict + Additional parameters that are passed to PhononBSDOSDoc.from_forces_born + """ + return PhononBSDOSDoc.from_forces_born( + structure=structure.remove_site_property(property_name="magmom") + if "magmom" in structure.site_properties + else structure, + supercell_matrix=supercell_matrix, + displacement=displacement, + num_displaced_supercells=num_displaced_supercells, + cal_anhar_fcs=cal_anhar_fcs, + displacement_anhar=displacement_anhar, + num_disp_anhar=num_disp_anhar, + fcs_cutoff_radius=fcs_cutoff_radius, + renorm_phonon=renorm_phonon, + renorm_temp=renorm_temp, + cal_ther_cond=cal_ther_cond, + ther_cond_mesh=ther_cond_mesh, + ther_cond_temp=ther_cond_temp, + sym_reduce=sym_reduce, + symprec=symprec, + use_symmetrized_structure=use_symmetrized_structure, + kpath_scheme=kpath_scheme, + code=code, + displacement_data=displacement_data, + total_dft_energy=total_dft_energy, + epsilon_static=epsilon_static, + born=born, + **kwargs, + ) + + +# I did not directly import this job from the phonon module +# because I modified the job to pass the displaced structures +# to the output. +@job(data=["forces", "displaced_structures"]) +def run_phonon_displacements( + displacements: list[Structure], + structure: Structure, + supercell_matrix: Matrix3D, + phonon_maker: BaseVaspMaker | ForceFieldStaticMaker | BaseAimsMaker = None, + prev_dir: str | Path = None, + prev_dir_argname: str = None, + socket: bool = False, +) -> Flow: + """ + Run phonon displacements. + + Note, this job will replace itself with N displacement calculations, + or a single socket calculation for all displacements. + + Parameters + ---------- + displacements: Sequence + All displacements to calculate + structure: Structure object + Fully optimized structure used for phonon computations. + supercell_matrix: Matrix3D + supercell matrix for meta data + phonon_maker : .BaseVaspMaker or .ForceFieldStaticMaker or .BaseAimsMaker + A maker to use to generate dispacement calculations + prev_dir: str or Path + The previous working directory + prev_dir_argname: str + argument name for the prev_dir variable + socket: bool + If True use the socket-io interface to increase performance + """ + phonon_jobs = [] + outputs: dict[str, list] = { + "displacement_number": [], + "forces": [], + "uuids": [], + "dirs": [], + "displaced_structures": [], + } + phonon_job_kwargs = {} + if prev_dir is not None and prev_dir_argname is not None: + phonon_job_kwargs[prev_dir_argname] = prev_dir + + if socket: + phonon_job = phonon_maker.make(displacements, **phonon_job_kwargs) + info = { + "original_structure": structure, + "supercell_matrix": supercell_matrix, + "displaced_structures": displacements, + } + phonon_job.update_maker_kwargs( + {"_set": {"write_additional_data->phonon_info:json": info}}, dict_mod=True + ) + phonon_jobs.append(phonon_job) + outputs["displacement_number"] = list(range(len(displacements))) + outputs["uuids"] = [phonon_job.output.uuid] * len(displacements) + outputs["dirs"] = [phonon_job.output.dir_name] * len(displacements) + outputs["forces"] = phonon_job.output.output.all_forces + # add the displaced structures, still need to be careful with the order, + # experimental feature + outputs["displaced_structures"] = displacements + else: + for idx, displacement in enumerate(displacements): + if prev_dir is not None: + phonon_job = phonon_maker.make(displacement, prev_dir=prev_dir) + else: + phonon_job = phonon_maker.make(displacement) + phonon_job.append_name(f" {idx + 1}/{len(displacements)}") + + # Explicitly set preserve_fworker here! + phonon_job.update_config({"manager_config": {"_preserve_fworker": True}}) + + # we will add some meta data + info = { + "displacement_number": idx, + "original_structure": structure, + "supercell_matrix": supercell_matrix, + "displaced_structure": displacement, + } + with contextlib.suppress(Exception): + phonon_job.update_maker_kwargs( + {"_set": {"write_additional_data->phonon_info:json": info}}, + dict_mod=True, + ) + phonon_jobs.append(phonon_job) + outputs["displacement_number"].append(idx) + outputs["uuids"].append(phonon_job.output.uuid) + outputs["dirs"].append(phonon_job.output.dir_name) + outputs["forces"].append(phonon_job.output.output.forces) + outputs["displaced_structures"].append(displacement) + + displacement_flow = Flow(phonon_jobs, outputs) + return Response(replace=displacement_flow) diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py new file mode 100644 index 0000000000..a87c01005f --- /dev/null +++ b/src/atomate2/common/schemas/pheasy.py @@ -0,0 +1,1005 @@ +"""Schemas for phonon documents.""" + +import copy +import logging +import pickle +import shlex +import subprocess +from pathlib import Path +from typing import Union + +import numpy as np + +# import lib by jiongzhi zheng +from ase.io import read +from emmet.core.math import Matrix3D +from emmet.core.structure import StructureMetadata +from hiphive import ( + ClusterSpace, + ForceConstantPotential, + ForceConstants, + enforce_rotational_sum_rules, +) +from hiphive.cutoffs import estimate_maximum_cutoff +from hiphive.utilities import extract_parameters +from monty.json import MSONable +from phonopy import Phonopy +from phonopy.file_IO import parse_FORCE_CONSTANTS, write_force_constants_to_hdf5 +from phonopy.interface.vasp import write_vasp +from phonopy.phonon.band_structure import get_band_qpoints_and_path_connections +from phonopy.structure.symmetry import symmetrize_borns_and_epsilon +from pydantic import Field +from pymatgen.core import Structure +from pymatgen.io.phonopy import ( + get_ph_bs_symm_line, + get_ph_dos, + get_phonopy_structure, + get_pmg_structure, +) +from pymatgen.io.vasp import Kpoints +from pymatgen.phonon.bandstructure import PhononBandStructureSymmLine +from pymatgen.phonon.dos import PhononDos +from pymatgen.phonon.plotter import PhononBSPlotter, PhononDosPlotter +from pymatgen.symmetry.bandstructure import HighSymmKpath +from pymatgen.symmetry.kpath import KPathSeek +from typing_extensions import Self + +# import some classmethod directly from phonons +from atomate2.common.schemas.phonons import ( + PhononComputationalSettings, + PhononJobDirs, + PhononUUIDs, + ThermalDisplacementData, + get_factor, +) + +logger = logging.getLogger(__name__) + + +class Forceconstants(MSONable): + """A force constants class.""" + + def __init__(self, force_constants: list[list[Matrix3D]]) -> None: + self.force_constants = force_constants + + +class PhononBSDOSDoc(StructureMetadata, extra="allow"): # type: ignore[call-arg] + """Collection of all data produced by the phonon workflow.""" + + structure: Structure | None = Field( + None, description="Structure of Materials Project." + ) + + phonon_bandstructure: PhononBandStructureSymmLine | None = Field( + None, + description="Phonon band structure object.", + ) + + phonon_dos: PhononDos | None = Field( + None, + description="Phonon density of states object.", + ) + + free_energies: list[float] | None = Field( + None, + description="vibrational part of the free energies in J/mol per " + "formula unit for temperatures in temperature_list", + ) + + heat_capacities: list[float] | None = Field( + None, + description="heat capacities in J/K/mol per " + "formula unit for temperatures in temperature_list", + ) + + internal_energies: list[float] | None = Field( + None, + description="internal energies in J/mol per " + "formula unit for temperatures in temperature_list", + ) + entropies: list[float] | None = Field( + None, + description="entropies in J/(K*mol) per formula unit" + "for temperatures in temperature_list ", + ) + + temperatures: list[int] | None = Field( + None, + description="temperatures at which the vibrational" + " part of the free energies" + " and other properties have been computed", + ) + + total_dft_energy: float | None = Field("total DFT energy per formula unit in eV") + + has_imaginary_modes: bool | None = Field( + None, description="if true, structure has imaginary modes" + ) + + # needed, e.g. to compute Grueneisen parameter etc + force_constants: Forceconstants | None = Field( + None, description="Force constants between every pair of atoms in the structure" + ) + + born: list[Matrix3D] | None = Field( + None, + description="born charges as computed from phonopy. Only for symmetrically " + "different atoms", + ) + + epsilon_static: Matrix3D | None = Field( + None, description="The high-frequency dielectric constant" + ) + + supercell_matrix: Matrix3D = Field("matrix describing the supercell") + primitive_matrix: Matrix3D = Field( + "matrix describing relationship to primitive cell" + ) + + code: str = Field("String describing the code for the computation") + + phonopy_settings: PhononComputationalSettings = Field( + "Field including settings for Phonopy" + ) + + thermal_displacement_data: ThermalDisplacementData | None = Field( + "Includes all data of the computation of the thermal displacements" + ) + + jobdirs: PhononJobDirs | None = Field( + "Field including all relevant job directories" + ) + + uuids: PhononUUIDs | None = Field("Field including all relevant uuids") + + @classmethod + def from_forces_born( + cls, + structure: Structure, + supercell_matrix: np.array, + displacement: float, + num_displaced_supercells: int, + cal_anhar_fcs: bool, + displacement_anhar: float, + num_disp_anhar: int, + fcs_cutoff_radius: list[int], + renorm_phonon: bool, + cal_ther_cond: bool, + ther_cond_mesh: list[int], + ther_cond_temp: list[int], + sym_reduce: bool, + symprec: float, + use_symmetrized_structure: Union[str, None], + kpath_scheme: str, + code: str, + displacement_data: dict[str, list], + total_dft_energy: float, + epsilon_static: Matrix3D = None, + born: Matrix3D = None, + **kwargs, + ) -> Self: + """Generate collection of phonon data. + + Parameters + ---------- + structure: Structure object + supercell_matrix: numpy array describing the supercell + displacement: float + size of displacement in angstrom + num_displaced_supercells: int + number of displaced supercells + cal_anhar_fcs: bool + if True, anharmonic force constants will be computed + displacement_anhar: float + size of displacement in angstrom for anharmonic force constants + num_disp_anhar: int + number of displaced supercells for anharmonic force constants + fcs_cutoff_radius: list + cutoff radius for force constants + sym_reduce: bool + if True, phonopy will use symmetry + symprec: float + precision to determine kpaths, + primitive cells and symmetry in phonopy and pymatgen + use_symmetrized_structure: str + primitive, conventional or None + kpath_scheme: str + kpath scheme to generate phonon band structure + code: str + which code was used for computation + displacement_data: + output of the displacement data + total_dft_energy: float + total energy in eV per cell + epsilon_static: Matrix3D + The high-frequency dielectric constant + born: Matrix3D + born charges + **kwargs: + additional arguments + """ + factor = get_factor(code) + # This opens the opportunity to add support for other codes + # that are supported by phonopy + + cell = get_phonopy_structure(structure) + + if use_symmetrized_structure == "primitive": + primitive_matrix: Union[np.ndarray, str] = np.eye(3) + else: + primitive_matrix = "auto" + + # TARP: THIS IS BAD! Including for discussions sake + if cell.magnetic_moments is not None and primitive_matrix == "auto": + if np.any(cell.magnetic_moments != 0.0): + raise ValueError( + "For materials with magnetic moments, " + "use_symmetrized_structure must be 'primitive'" + ) + cell.magnetic_moments = None + + # Create the phonon object using the phonopy API to write the POSCAR and + # SPOSCAR files for the input of pheasy code. + phonon = Phonopy( + cell, + supercell_matrix, + primitive_matrix=primitive_matrix, + factor=factor, + symprec=symprec, + is_symmetry=sym_reduce, + ) + + # Write the POSCAR and SPOSCAR files for the input of pheasy code + supercell = phonon._supercell # noqa: SLF001 + write_vasp("POSCAR", cell) + write_vasp("SPOSCAR", supercell) + + # get the force-displacement dataset from previous calculations + dataset_forces = [np.array(forces) for forces in displacement_data["forces"]] + dataset_forces_array = np.array(dataset_forces) + # save to csv file + import csv + + with open("dataset_forces.csv", "w") as file: + writer = csv.writer(file) + writer.writerows(dataset_forces_array) + + # To deduct the residual forces on an equilibrium structure to eliminate the + # fitting error + dataset_forces_array_rr = dataset_forces_array - dataset_forces_array[-1, :, :] + + # force matrix on the displaced structures + dataset_forces_array_disp = dataset_forces_array_rr[:-1, :, :] + # dataset_disps = [ + # np.array(disps.cart_coords) + # for disps in displacement_data["displaced_structures"] + # ] + + # get the displacement dataset + # dataset_disps_array_rr = np.round( + # (dataset_disps - supercell.get_positions()), + # decimals=16 + # ).astype('double') + # dataset_disps_array_use = dataset_disps_array_rr[:-1, :, :] + + # To handle the large dispalced distance in the dataset + dataset_disps = [ + np.array(disps.frac_coords) + for disps in displacement_data["displaced_structures"] + ] + logger.info(f"dataset_disps = {dataset_disps}") + # save to csv file + import csv + + with open("dataset_disps.csv", "w") as file: + writer = csv.writer(file) + writer.writerows(dataset_disps) + dataset_disps_array_rr = np.round( + (dataset_disps - supercell.get_scaled_positions()), decimals=16 + ).astype("double") + # save to csv file + with open("dataset_disps_array_rr.csv", "w") as file: + writer = csv.writer(file) + writer.writerows(dataset_disps_array_rr) + dataset_disps_array_rr = np.where( + dataset_disps_array_rr > 0.5, + dataset_disps_array_rr - 1.0, + dataset_disps_array_rr, + ) + dataset_disps_array_rr = np.where( + dataset_disps_array_rr < -0.5, + dataset_disps_array_rr + 1.0, + dataset_disps_array_rr, + ) + + # Transpose the displacement array on the + # last two axes (atoms and coordinates) + dataset_disps_array_rr_transposed = np.transpose( + dataset_disps_array_rr, (0, 2, 1) + ) + + # Perform matrix multiplication with the transposed supercell.cell + # 'ij' for supercell.cell.T and + # 'nkj' for the transposed dataset_disps_array_rr + dataset_disps_array_rr_cartesian = np.einsum( + "ij,njk->nik", supercell.cell.T, dataset_disps_array_rr_transposed + ) + # Transpose back to the original format + dataset_disps_array_rr_cartesian = np.transpose( + dataset_disps_array_rr_cartesian, (0, 2, 1) + ) + + dataset_disps_array_use = dataset_disps_array_rr_cartesian[:-1, :, :] + + # separate the dataset into harmonic and anharmonic parts + if cal_anhar_fcs: + try: + from alm import ALM + except ImportError: + logger.exception( + "Error importing ALM. Please ensure the 'alm'library is installed." + ) + + supercell_ph = phonon.supercell + lattice = supercell_ph.cell + positions = supercell_ph.scaled_positions + numbers = supercell_ph.numbers + natom = len(numbers) + + # get the number of free parameters of 2ND FCs from ALM, labeled as n_fp + with ALM(lattice, positions, numbers) as alm: + alm.define(1) + alm.suggest() + n_fp = alm._get_number_of_irred_fc_elements(1) # noqa: SLF001 + + # get the number of displaced supercells based on the + # number of free parameters + num = int(np.ceil(n_fp / (3.0 * natom))) + + # get the number of displaced supercells from phonopy to compared + # with the number of 3, if the number of displaced supercells is + # less than 3, we will use the finite displacement method to generate + # the supercells. Otherwise, we will use the random displacement + # method to generate the supercells. + phonon.generate_displacements(distance=displacement) + num_disp_f = len(phonon.displacements) + if num_disp_f > 3: + num_d = int(np.ceil(num * 1.8)) + num_har = num_d + else: + num_har = num_disp_f + else: + num_har = dataset_disps_array_use.shape[0] + + if cal_anhar_fcs: + dataset_disps_array_use_har = dataset_disps_array_use[:num_har, :, :] + dataset_forces_array_disp_har = dataset_forces_array_disp[:num_har, :, :] + with open("disp_matrix.pkl", "wb") as file: + pickle.dump(dataset_disps_array_use_har, file) + with open("force_matrix.pkl", "wb") as file: + pickle.dump(dataset_forces_array_disp_har, file) + + else: + with open("disp_matrix.pkl", "wb") as file: + pickle.dump(dataset_disps_array_use, file) + with open("force_matrix.pkl", "wb") as file: + pickle.dump(dataset_forces_array_disp, file) + + # get the born charges and dielectric constant + if born is not None and epsilon_static is not None: + if len(structure) == len(born): + borns, epsilon = symmetrize_borns_and_epsilon( + ucell=phonon.unitcell, + borns=np.array(born), + epsilon=np.array(epsilon_static), + symprec=symprec, + primitive_matrix=phonon.primitive_matrix, + supercell_matrix=phonon.supercell_matrix, + is_symmetry=kwargs.get("symmetrize_born", True), + ) + else: + raise ValueError( + "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 = { + "born": borns, + "dielectric": epsilon, + "factor": 14.399652, + } + # Other codes could be added here + + else: + borns = None + epsilon = None + + prim = read("POSCAR") + supercell = read("SPOSCAR") + + # Create the clusters and orbitals for second order force constants + # For the variables: --w, --nbody, they are used to specify the order of the + # force constants. in the near future, we will add the option to specify the + # order of the force constants. And these two variables can be defined by the + # users. + pheasy_cmd_1 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} " + f"-s -w 2 --symprec {float(symprec)} --nbody 2" + ) + + # Create the null space to further reduce the free parameters for + # specific force constants and make them physically correct. + pheasy_cmd_2 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -c --symprec " + f"{float(symprec)} -w 2" + ) + + # Generate the Compressive Sensing matrix,i.e., displacement matrix + # for the input of machine leaning method.i.e., LASSO, + pheasy_cmd_3 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -w 2 -d " + f"--symprec {float(symprec)} " + f"--ndata {int(num_har)} --disp_file" + ) + + # Here we set a criteria to determine which method to use to generate the + # force constants. If the number of displacements is larger than 3, we + # will use the LASSO method to generate the force constants. Otherwise, + # we will use the least-squred method to generate the force constants. + phonon.generate_displacements(distance=displacement) + disps = phonon.displacements + num_judge = len(disps) + + if num_judge > 3: + # Calculate the force constants using the LASSO method due to the + # random-displacement method Obviously, the rotaional invariance + # constraint, i.e., tag: --rasr BHH, is enforced during the + # fitting process. + pheasy_cmd_4 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -f --full_ifc " + f"-w 2 --symprec {float(symprec)} " + f"-l LASSO --std --rasr BHH --ndata {int(num_har)}" + ) + + else: + # Calculate the force constants using the least-squred method + pheasy_cmd_4 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -f --full_ifc " + f"-w 2 --symprec {float(symprec)} " + f"--rasr BHH --ndata {int(num_har)}" + ) + + logger.info("Start running pheasy in cluster") + + subprocess.call(shlex.split(pheasy_cmd_1)) + logger.info(f"all files in cwd after cmd_1 are {list(Path.cwd().iterdir())}") + subprocess.call(shlex.split(pheasy_cmd_2)) + logger.info(f"all files in cwd after cmd_2 are {list(Path.cwd().iterdir())}") + subprocess.call(shlex.split(pheasy_cmd_3)) + logger.info(f"all files in cwd after cmd_3 are {list(Path.cwd().iterdir())}") + # print the cwd + logger.info(f"path before running cmd_4 is {Path.cwd()}") + subprocess.call(shlex.split(pheasy_cmd_4)) + logger.info(f"path after running cmd_4 is {Path.cwd()}") + # print all the files in the current directory + logger.info(f"all files in cwd after cmd_4 are {list(Path.cwd().iterdir())}") + + # When this code is run on Github tests, it is failing because it is + # not able to find the FORCE_CONSTANTS file. This is because the file is + # somehow getting generated in some temp directory. Can you fix the bug? + cwd = Path.cwd() + fc_file = cwd / "FORCE_CONSTANTS" + + if cal_anhar_fcs: + # subprocess.call("rm -f disp_matrix.pkl force_matrix.pkl", shell=True) + subprocess.run( + ["/bin/rm", "-f", "disp_matrix.pkl", "force_matrix.pkl"], check=True + ) + dataset_disps_array_use_anahr = dataset_disps_array_use[num_har:, :, :] + dataset_forces_array_disp_anhar = dataset_forces_array_disp[num_har:, :, :] + with open("disp_matrix.pkl", "wb") as file: + pickle.dump(dataset_disps_array_use_anahr, file) + with open("force_matrix.pkl", "wb") as file: + pickle.dump(dataset_forces_array_disp_anhar, file) + num_anhar = dataset_disps_array_use_anahr.shape[0] + else: + pass + + # We next begin to generate the anharmonic force constants up to fourth + # order using the LASSO method + + if cal_anhar_fcs: + pheasy_cmd_5 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -s -w 4 --symprec " + f"{float(symprec)} " + f"--nbody 2 3 3 --c3 {float(fcs_cutoff_radius[1] / 1.89)} " + f"--c4 {float(fcs_cutoff_radius[2] / 1.89)}" + ) + logger.info("pheasy_cmd_5 = %s", pheasy_cmd_5) + + pheasy_cmd_6 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -c --symprec " + f"{float(symprec)} -w 4" + ) + logger.info("pheasy_cmd_6 = %s", pheasy_cmd_6) + pheasy_cmd_7 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -w 4 -d --symprec " + f"{float(symprec)} " + f"--ndata {int(num_anhar)} --disp_file" + ) + logger.info("pheasy_cmd_7 = %s", pheasy_cmd_7) + pheasy_cmd_8 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -f -w 4 --fix_fc2 " + f"--symprec {float(symprec)} " + f"--ndata {int(num_anhar)} " + ) + logger.info("pheasy_cmd_8 = %s", pheasy_cmd_8) + logger.info("Start running pheasy in cluster") + + subprocess.call(shlex.split(pheasy_cmd_5)) + subprocess.call(shlex.split(pheasy_cmd_6)) + subprocess.call(shlex.split(pheasy_cmd_7)) + subprocess.call(shlex.split(pheasy_cmd_8)) + else: + pass + + # begin to renormzlize the phonon energies + if renorm_phonon: + pheasy_cmd_9 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -f -w 4 --fix_fc2 " + f"--hdf5 --symprec {float(symprec)} " + f"--ndata {int(num_anhar)}" + ) + + logger.info("Start running pheasy in cluster") + subprocess.call(shlex.split(pheasy_cmd_9)) + + # write the born charges and dielectric constant to the pheasy format + + else: + pass + + # begin to convert the force constants to the phonopy and phono3py format + # for the further lattice thermal conductivity calculations + if cal_ther_cond: + # convert the 2ND order force constants to the phonopy format + fc_phonopy_text = parse_FORCE_CONSTANTS(filename="FORCE_CONSTANTS") + write_force_constants_to_hdf5(fc_phonopy_text, filename="fc2.hdf5") + + # convert the 3RD order force constants to the phonopy format + + prim_hiphive = read("POSCAR") + supercell_hiphive = read("SPOSCAR") + fcs = ForceConstants.read_shengBTE( + supercell_hiphive, "FORCE_CONSTANTS_3RD", prim_hiphive + ) + fcs.write_to_phono3py("fc3.hdf5") + + phono3py_cmd = ( + f"phono3py --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} {int(supercell_matrix[2][2])} " + f"--fc2 --fc3 --br --isotope --wigner " + f"--mesh {ther_cond_mesh[0]} {ther_cond_mesh[1]} {ther_cond_mesh[2]} " + f"--tmin {ther_cond_temp[0]} --tmax {ther_cond_temp[1]} " + f"--tstep {ther_cond_temp[2]}" + ) + + subprocess.call(shlex.split(phono3py_cmd)) + else: + pass + + # Read the force constants from the output file of pheasy code + force_constants = parse_FORCE_CONSTANTS(filename=fc_file) + phonon.force_constants = force_constants + # symmetrize the force constants to make them physically correct based on + # the space group symmetry of the crystal structure. + phonon.symmetrize_force_constants() + + # with phonopy.load("phonopy.yaml") the phonopy API can be used + phonon.save("phonopy.yaml") + + # get phonon band structure + kpath_dict, kpath_concrete = PhononBSDOSDoc.get_kpath( + structure=get_pmg_structure(phonon.primitive), + kpath_scheme=kpath_scheme, + symprec=symprec, + ) + + npoints_band = kwargs.get("npoints_band", 101) + qpoints, connections = get_band_qpoints_and_path_connections( + kpath_concrete, npoints=kwargs.get("npoints_band", 101) + ) + + # phonon band structures will always be computed + filename_band_yaml = "phonon_band_structure.yaml" + filename_band_hdf5 = "phonon_band_structure.hdf5" + + # TODO: potentially add kwargs to avoid computation of eigenvectors + phonon.run_band_structure( + qpoints, + path_connections=connections, + with_eigenvectors=kwargs.get("band_structure_eigenvectors", False), + is_band_connection=kwargs.get("band_structure_eigenvectors", False), + ) + phonon.write_yaml_band_structure(filename=filename_band_yaml) + phonon.write_hdf5_band_structure(filename=filename_band_hdf5) + bs_symm_line = get_ph_bs_symm_line( + filename_band_yaml, labels_dict=kpath_dict, has_nac=born is not None + ) + new_plotter = PhononBSPlotter(bs=bs_symm_line) + new_plotter.save_plot( + filename=kwargs.get("filename_bs", "phonon_band_structure.pdf"), + units=kwargs.get("units", "THz"), + ) + + # will determine if imaginary modes are present in the structure + imaginary_modes = bs_symm_line.has_imaginary_freq( + tol=kwargs.get("tol_imaginary_modes", 1e-5) + ) + + # If imaginary modes are present, we first use the hiphive code to enforce + # some symmetry constraints to eliminate the imaginary modes (generally work + # for small imaginary modes near Gamma point). If the imaginary modes are + # still present, we will use the pheasy code to generate the force constants + # using a shorter cutoff (10 A) to eliminate the imaginary modes, also we + # just want to remove the imaginary modes near Gamma point. In the future, + # we will only use the pheasy code to do the job. + + if imaginary_modes: + # Define a cluster space using the largest cutoff you can + max_cutoff = estimate_maximum_cutoff(supercell) - 0.01 + cutoffs = [max_cutoff] # only second order needed + cs = ClusterSpace(prim, cutoffs) + + # import the phonopy force constants using the correct supercell also + # provided by phonopy + fcs = ForceConstants.read_phonopy(supercell, "FORCE_CONSTANTS") + + # Find the parameters that best fits the force constants given you + # cluster space + parameters = extract_parameters(fcs, cs) + + # Enforce the rotational sum rules + parameters_rot = enforce_rotational_sum_rules( + cs, parameters, ["Huang", "Born-Huang"], alpha=1e-6 + ) + + # use the new parameters to make a fcp and then create the force + # constants and write to a phonopy file + fcp = ForceConstantPotential(cs, parameters_rot) + fcs = fcp.get_force_constants(supercell) + fcs.write_to_phonopy("FORCE_CONSTANTS_new", format="text") + + force_constants = parse_FORCE_CONSTANTS(filename="FORCE_CONSTANTS_new") + phonon.force_constants = force_constants + phonon.symmetrize_force_constants() + + phonon.run_band_structure( + qpoints, path_connections=connections, with_eigenvectors=True + ) + phonon.write_yaml_band_structure(filename=filename_band_yaml) + bs_symm_line = get_ph_bs_symm_line( + filename_band_yaml, labels_dict=kpath_dict, has_nac=born is not None + ) + + new_plotter = PhononBSPlotter(bs=bs_symm_line) + + new_plotter.save_plot( + filename=kwargs.get("filename_bs", "phonon_band_structure.pdf"), + units=kwargs.get("units", "THz"), + ) + + # new_plotter.save_plot("phonon_band_structure.eps", + # img_format=kwargs.get("img_format", "eps"), + # units=kwargs.get("units", "THz"),) + + imaginary_modes_hiphive = bs_symm_line.has_imaginary_freq( + tol=kwargs.get("tol_imaginary_modes", 1e-5) + ) + + else: + imaginary_modes_hiphive = False + imaginary_modes = False + + # Using a shorter cutoff (10 A) to generate the force constants to + # eliminate the imaginary modes near Gamma point in phesay code + if imaginary_modes_hiphive: + pheasy_cmd_11 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -s -w 2 --c2 " + f"10.0 --symprec {float(symprec)} " + f"--nbody 2" + ) + + pheasy_cmd_12 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -c --symprec " + f"{float(symprec)} --c2 10.0 -w 2" + ) + + pheasy_cmd_13 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -w 2 -d --symprec " + f"{float(symprec)} --c2 10.0 " + f"--ndata {int(num_har)} --disp_file" + ) + + phonon.generate_displacements(distance=displacement) + disps = phonon.displacements + num_judge = len(disps) + + if num_judge > 3: + pheasy_cmd_14 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -f --c2 10.0 " + f"--full_ifc -w 2 --symprec {float(symprec)} " + f"-l LASSO --std --rasr BHH --ndata {int(num_har)}" + ) + + else: + pheasy_cmd_14 = ( + f"pheasy --dim {int(supercell_matrix[0][0])} " + f"{int(supercell_matrix[1][1])} " + f"{int(supercell_matrix[2][2])} -f --full_ifc " + f"--c2 10.0 -w 2 --symprec {float(symprec)} " + f"--rasr BHH --ndata {int(num_har)}" + ) + + logger.info("Start running pheasy in cluster") + + subprocess.call(shlex.split(pheasy_cmd_11)) + subprocess.call(shlex.split(pheasy_cmd_12)) + subprocess.call(shlex.split(pheasy_cmd_13)) + subprocess.call(shlex.split(pheasy_cmd_14)) + + force_constants = parse_FORCE_CONSTANTS(filename="FORCE_CONSTANTS") + phonon.force_constants = force_constants + phonon.symmetrize_force_constants() + + # with phonon.load("phonopy.yaml") the phonopy API can be used + phonon.save("phonopy.yaml") + + # get phonon band structure + kpath_dict, kpath_concrete = cls.get_kpath( + structure=get_pmg_structure(phonon.primitive), + kpath_scheme=kpath_scheme, + symprec=symprec, + ) + + npoints_band = kwargs.get("npoints_band", 101) + qpoints, connections = get_band_qpoints_and_path_connections( + kpath_concrete, npoints=kwargs.get("npoints_band", 101) + ) + + # phonon band structures will always be cmouted + filename_band_yaml = "phonon_band_structure.yaml" + phonon.run_band_structure( + qpoints, path_connections=connections, with_eigenvectors=True + ) + phonon.write_yaml_band_structure(filename=filename_band_yaml) + bs_symm_line = get_ph_bs_symm_line( + filename_band_yaml, labels_dict=kpath_dict, has_nac=born is not None + ) + new_plotter = PhononBSPlotter(bs=bs_symm_line) + + new_plotter.save_plot( + filename=kwargs.get("filename_bs", "phonon_band_structure.pdf"), + units=kwargs.get("units", "THz"), + ) + + imaginary_modes_cutoff = bs_symm_line.has_imaginary_freq( + tol=kwargs.get("tol_imaginary_modes", 1e-5) + ) + imaginary_modes = imaginary_modes_cutoff + # new_plotter.save_plot( + # "phonon_band_structure.eps", + # img_format=kwargs.get("img_format", "eps"), + # units=kwargs.get("units", "THz"), + # ) + else: + pass + + # gets data for visualization on website - yaml is also enough + if kwargs.get("band_structure_eigenvectors"): + bs_symm_line.write_phononwebsite("phonon_website.json") + + # get phonon density of states + filename_dos_yaml = "phonon_dos.yaml" + + kpoint_density_dos = kwargs.get("kpoint_density_dos", 7_000) + kpoint = Kpoints.automatic_density( + structure=get_pmg_structure(phonon.primitive), + kppa=kpoint_density_dos, + force_gamma=True, + ) + phonon.run_mesh(kpoint.kpts[0]) + phonon.run_total_dos() + phonon.write_total_dos(filename=filename_dos_yaml) + dos = get_ph_dos(filename_dos_yaml) + new_plotter_dos = PhononDosPlotter() + new_plotter_dos.add_dos(label="total", dos=dos) + new_plotter_dos.save_plot( + filename=kwargs.get("filename_dos", "phonon_dos.pdf"), + units=kwargs.get("units", "THz"), + ) + + # compute vibrational part of free energies per formula unit + temperature_range = np.arange( + kwargs.get("tmin", 0), kwargs.get("tmax", 500), kwargs.get("tstep", 10) + ) + + free_energies = [ + dos.helmholtz_free_energy( + temp=temp, structure=get_pmg_structure(phonon.primitive) + ) + for temp in temperature_range + ] + + entropies = [ + dos.entropy(temp=temp, structure=get_pmg_structure(phonon.primitive)) + for temp in temperature_range + ] + + internal_energies = [ + dos.internal_energy( + temp=temp, structure=get_pmg_structure(phonon.primitive) + ) + for temp in temperature_range + ] + + heat_capacities = [ + dos.cv(temp=temp, structure=get_pmg_structure(phonon.primitive)) + for temp in temperature_range + ] + + # will compute thermal displacement matrices + # for the primitive cell (phonon.primitive!) + # only this is available in phonopy + if kwargs.get("create_thermal_displacements"): + phonon.run_mesh( + kpoint.kpts[0], with_eigenvectors=True, is_mesh_symmetry=False + ) + freq_min_thermal_displacements = kwargs.get( + "freq_min_thermal_displacements", 0.0 + ) + phonon.run_thermal_displacement_matrices( + t_min=kwargs.get("tmin_thermal_displacements", 0), + t_max=kwargs.get("tmax_thermal_displacements", 500), + t_step=kwargs.get("tstep_thermal_displacements", 100), + freq_min=freq_min_thermal_displacements, + ) + + temperature_range_thermal_displacements = np.arange( + kwargs.get("tmin_thermal_displacements", 0), + kwargs.get("tmax_thermal_displacements", 500), + kwargs.get("tstep_thermal_displacements", 100), + ) + for idx, temp in enumerate(temperature_range_thermal_displacements): + phonon.thermal_displacement_matrices.write_cif( + phonon.primitive, idx, filename=f"tdispmat_{temp}K.cif" + ) + _disp_mat = phonon._thermal_displacement_matrices # noqa: SLF001 + tdisp_mat = _disp_mat.thermal_displacement_matrices.tolist() + + tdisp_mat_cif = _disp_mat.thermal_displacement_matrices_cif.tolist() + + else: + tdisp_mat = None + tdisp_mat_cif = None + + formula_units = ( + structure.composition.num_atoms + / structure.composition.reduced_composition.num_atoms + ) + + total_dft_energy_per_formula_unit = ( + total_dft_energy / formula_units if total_dft_energy is not None else None + ) + + return cls.from_structure( + structure=structure, + meta_structure=structure, + num_displaced_supercells=num_displaced_supercells, + displacement_anhar=displacement_anhar, + num_disp_anhar=num_disp_anhar, + phonon_bandstructure=bs_symm_line, + phonon_dos=dos, + free_energies=free_energies, + internal_energies=internal_energies, + heat_capacities=heat_capacities, + entropies=entropies, + temperatures=temperature_range.tolist(), + total_dft_energy=total_dft_energy_per_formula_unit, + has_imaginary_modes=imaginary_modes, + force_constants={"force_constants": phonon.force_constants.tolist()} + if kwargs["store_force_constants"] + else None, + born=borns.tolist() if borns is not None else None, + epsilon_static=epsilon.tolist() if epsilon is not None else None, + supercell_matrix=phonon.supercell_matrix.tolist(), + primitive_matrix=phonon.primitive_matrix.tolist(), + code=code, + thermal_displacement_data={ + "temperatures_thermal_displacements": temperature_range_thermal_displacements.tolist(), # noqa: E501 + "thermal_displacement_matrix_cif": tdisp_mat_cif, + "thermal_displacement_matrix": tdisp_mat, + "freq_min_thermal_displacements": freq_min_thermal_displacements, + } + if kwargs.get("create_thermal_displacements") + else None, + jobdirs={ + "displacements_job_dirs": displacement_data["dirs"], + "static_run_job_dir": kwargs["static_run_job_dir"], + "born_run_job_dir": kwargs["born_run_job_dir"], + "optimization_run_job_dir": kwargs["optimization_run_job_dir"], + "taskdoc_run_job_dir": str(Path.cwd()), + }, + uuids={ + "displacements_uuids": displacement_data["uuids"], + "born_run_uuid": kwargs["born_run_uuid"], + "optimization_run_uuid": kwargs["optimization_run_uuid"], + "static_run_uuid": kwargs["static_run_uuid"], + }, + phonopy_settings={ + "npoints_band": npoints_band, + "kpath_scheme": kpath_scheme, + "kpoint_density_dos": kpoint_density_dos, + }, + ) + + @staticmethod + def get_kpath( + structure: Structure, kpath_scheme: str, symprec: float, **kpath_kwargs + ) -> tuple: + """Get high-symmetry points in k-space in phonopy format. + + Parameters + ---------- + structure: Structure Object + kpath_scheme: str + string describing kpath + symprec: float + precision for symmetry determination + **kpath_kwargs: + additional parameters that can be passed to this method as a dict + """ + if kpath_scheme in ("setyawan_curtarolo", "latimer_munro", "hinuma"): + high_symm_kpath = HighSymmKpath( + structure, path_type=kpath_scheme, symprec=symprec, **kpath_kwargs + ) + kpath = high_symm_kpath.kpath + elif kpath_scheme == "seekpath": + high_symm_kpath = KPathSeek(structure, symprec=symprec, **kpath_kwargs) + kpath = high_symm_kpath._kpath # noqa: SLF001 + else: + raise ValueError(f"Unexpected {kpath_scheme=}") + + path = copy.deepcopy(kpath["path"]) + + for set_idx, label_set in enumerate(kpath["path"]): + for lbl_idx, label in enumerate(label_set): + path[set_idx][lbl_idx] = kpath["kpoints"][label] + return kpath["kpoints"], path diff --git a/src/atomate2/forcefields/flows/eos.py b/src/atomate2/forcefields/flows/eos.py index 4567f357ab..fab91b3d2f 100644 --- a/src/atomate2/forcefields/flows/eos.py +++ b/src/atomate2/forcefields/flows/eos.py @@ -95,6 +95,7 @@ def from_force_field_name( force_field_name=force_field_name, relax_cell=False ), static_maker=None, + **kwargs, ) return cls( name=f"{force_field_name.split('MLFF.')[-1]} EOS Maker", diff --git a/src/atomate2/forcefields/flows/pheasy.py b/src/atomate2/forcefields/flows/pheasy.py new file mode 100644 index 0000000000..d9db491809 --- /dev/null +++ b/src/atomate2/forcefields/flows/pheasy.py @@ -0,0 +1,145 @@ +"""Flows for calculating phonons.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Literal + +from atomate2 import SETTINGS +from atomate2.common.flows.pheasy import BasePhononMaker +from atomate2.forcefields.jobs import ForceFieldRelaxMaker, ForceFieldStaticMaker + + +@dataclass +class PhononMaker(BasePhononMaker): + """ + Maker to calculate harmonic phonons with a force field. + + Calculate the harmonic phonons of a material. Initially, a tight structural + relaxation is performed to obtain a structure without forces on the atoms. + Subsequently, supercells with one displaced atom are generated and accurate + forces are computed for these structures. With the help of phonopy, these + forces are then converted into a dynamical matrix. To correct for polarization + effects, a correction of the dynamical matrix based on BORN charges can + be performed. The BORN charges can be supplied manually. + Finally, phonon densities of states, phonon band structures + and thermodynamic properties are computed. + + .. Note:: + It is heavily recommended to symmetrize the structure before passing it to + this flow. Otherwise, a different space group might be detected and too + many displacement calculations will be generated. + It is recommended to check the convergence parameters here and + adjust them if necessary. The default might not be strict enough + for your specific case. + + Parameters + ---------- + name : str + Name of the flows produced by this maker. + sym_reduce : bool + Whether to reduce the number of deformations using symmetry. + symprec : float + Symmetry precision to use in the + reduction of symmetry to find the primitive/conventional cell + (use_primitive_standard_structure, use_conventional_standard_structure) + and to handle all symmetry-related tasks in phonopy + displacement: float + displacement distance for phonons + min_length: float + min length of the supercell that will be built + prefer_90_degrees: bool + if set to True, supercell algorithm will first try to find a supercell + with 3 90 degree angles + get_supercell_size_kwargs: dict + kwargs that will be passed to get_supercell_size to determine supercell size + use_symmetrized_structure: str + allowed strings: "primitive", "conventional", None + + - "primitive" will enforce to start the phonon computation + from the primitive standard structure + according to Setyawan, W., & Curtarolo, S. (2010). + High-throughput electronic band structure calculations: + Challenges and tools. Computational Materials Science, + 49(2), 299-312. doi:10.1016/j.commatsci.2010.05.010. + This makes it possible to use certain k-path definitions + with this workflow. Otherwise, we must rely on seekpath + - "conventional" will enforce to start the phonon computation + from the conventional standard structure + according to Setyawan, W., & Curtarolo, S. (2010). + High-throughput electronic band structure calculations: + Challenges and tools. Computational Materials Science, + 49(2), 299-312. doi:10.1016/j.commatsci.2010.05.010. + We will however use seekpath and primitive structures + as determined by from phonopy to compute the phonon band structure + bulk_relax_maker : .ForceFieldRelaxMaker or None + A maker to perform a tight relaxation on the bulk. + Set to ``None`` to skip the + bulk relaxation + static_energy_maker : .ForceFieldRelaxMaker or None + A maker to perform the computation of the DFT energy on the bulk. + Set to ``None`` to skip the + static energy computation + born_maker: .ForceFieldStaticMaker or None + Maker to compute the BORN charges. + phonon_displacement_maker : .ForceFieldStaticMaker or None + Maker used to compute the forces for a supercell. + generate_frequencies_eigenvectors_kwargs : dict + Keyword arguments passed to :obj:`generate_frequencies_eigenvectors`. + create_thermal_displacements: bool + Bool that determines if thermal_displacement_matrices are computed + kpath_scheme: str + scheme to generate kpoints. Please be aware that + you can only use seekpath with any kind of cell + Otherwise, please use the standard primitive structure + Available schemes are: + "seekpath", "hinuma", "setyawan_curtarolo", "latimer_munro". + "seekpath" and "hinuma" are the same definition but + seekpath can be used with any kind of unit cell as + it relies on phonopy to handle the relationship + to the primitive cell and not pymatgen + code: str + determines the dft or force field code. + store_force_constants: bool + if True, force constants will be stored + socket: bool + If True, use the socket for the calculation + """ + + name: str = "phonon" + sym_reduce: bool = True + symprec: float = SETTINGS.PHONON_SYMPREC + displacement: float = 0.01 + min_length: float | None = 20.0 + prefer_90_degrees: bool = True + get_supercell_size_kwargs: dict = field(default_factory=dict) + use_symmetrized_structure: Literal["primitive", "conventional"] | None = None + bulk_relax_maker: ForceFieldRelaxMaker | None = field( + default_factory=lambda: ForceFieldRelaxMaker( + force_field_name="MACE", relax_kwargs={"fmax": 0.00001} + ) + ) + static_energy_maker: ForceFieldStaticMaker | None = field( + default_factory=lambda: ForceFieldStaticMaker(force_field_name="MACE") + ) + phonon_displacement_maker: ForceFieldStaticMaker = field( + default_factory=lambda: ForceFieldStaticMaker(force_field_name="MACE") + ) + create_thermal_displacements: bool = False + generate_frequencies_eigenvectors_kwargs: dict = field(default_factory=dict) + kpath_scheme: str = "seekpath" + store_force_constants: bool = True + code: str = "forcefields" + born_maker: ForceFieldStaticMaker | None = None + + @property + def prev_calc_dir_argname(self) -> None: + """Name of argument informing static maker of previous calculation directory. + + As this differs between different DFT codes (e.g., VASP, CP2K), it + has been left as a property to be implemented by the inheriting class. + + Note: this is only applicable if a relax_maker is specified; i.e., two + calculations are performed for each ordering (relax -> static) + """ + return diff --git a/src/atomate2/lobster/schemas.py b/src/atomate2/lobster/schemas.py index ecdd4629fc..445da1ecc6 100644 --- a/src/atomate2/lobster/schemas.py +++ b/src/atomate2/lobster/schemas.py @@ -1072,12 +1072,6 @@ def from_directory( data, allow_bson=True, strict=True, enum_values=True ) json.dump(monty_encoded_json_doc, file) - file.write(",") - data = {"builder_meta": doc.builder_meta} # add builder metadata - monty_encoded_json_doc = jsanitize( - data, allow_bson=False, strict=True, enum_values=True - ) - json.dump(monty_encoded_json_doc, file) del data, monty_encoded_json_doc file.write("]") diff --git a/src/atomate2/vasp/flows/pheasy.py b/src/atomate2/vasp/flows/pheasy.py new file mode 100644 index 0000000000..fca5039a00 --- /dev/null +++ b/src/atomate2/vasp/flows/pheasy.py @@ -0,0 +1,195 @@ +"""Define the VASP PhononMaker.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Literal + +from atomate2 import SETTINGS +from atomate2.common.flows.pheasy import BasePhononMaker +from atomate2.vasp.flows.core import DoubleRelaxMaker +from atomate2.vasp.jobs.core import DielectricMaker, StaticMaker, TightRelaxMaker +from atomate2.vasp.jobs.phonons import PhononDisplacementMaker +from atomate2.vasp.sets.core import StaticSetGenerator + +if TYPE_CHECKING: + from atomate2.vasp.jobs.base import BaseVaspMaker + + +@dataclass +class PhononMaker(BasePhononMaker): + """Maker to calculate harmonic phonons with LASSO-based ML code Pheasy. + + Calculate the zero-K harmonic phonons of a material and higher-order FCs. + Initially, a tight structural relaxation is performed to obtain a structure + without forces on the atoms. Subsequently, supercells with all atoms displaced + by a small amplitude (generally using 0.01 A) are generated and accurate forces + are computed for these structures for the second order force constants. With the + help of pheasy (LASSO technique), these forces are then converted into a dynamical + matrix. In this Workflow, we separate the harmonic phonon calculations and + anharmonic force constants calculations. To correct for polarization effects, a + correction of the dynamical matrix based on BORN charges can be performed. Finally, + phonon densities of states, phonon band structures and thermodynamic properties + are computed. For the anharmonic force constants, the supercells with all atoms + displaced by a larger amplitude (generally using 0.08 A) are generated and accurate + forces are computed for these structures. With the help of pheasy (LASSO technique), + the third- and fourth-order force constants are extracted at once. + + .. Note:: + It is heavily recommended to symmetrize the structure before passing it to + this flow. Otherwise, a different space group might be detected and too + many displacement calculations will be generated. + It is recommended to check the convergence parameters here and + adjust them if necessary. The default might not be strict enough + for your specific case. + + Parameters + ---------- + name : str + Name of the flow produced by this maker. + sym_reduce : bool + Whether to reduce the number of deformations using symmetry. + symprec : float + Symmetry precision to use in the + reduction of symmetry to find the primitive/conventional cell + (use_primitive_standard_structure, use_conventional_standard_structure) + and to handle all symmetry-related tasks in pheasy, we recommend to + use the value of 1e-3. + displacement: float + displacement distance for phonons, for most cases 0.01 A is a good choice, + but it can be increased to 0.02 A for heavier elements. + num_displaced_supercells: int + number of displacements to be generated using a random-displacement approach + for harmonic phonon calculations. The default value is 0 and the number of + displacements is automatically determined by the number of atoms in the + supercell and its space group. + cal_anhar_fcs: bool + if set to True, anharmonic force constants(FCs) up to fourth-order FCs will + be calculated. The default value is False, and only harmonic phonons will + be calculated. + displacement_anhar: float + displacement distance for anharmonic force constants(FCs) up to fourth-order + FCs, for most cases 0.08 A is a good choice, but it can be increased to 0.1 A. + num_disp_anhar: int + number of displacements to be generated using a random-displacement approach + for anharmonic phonon calculations. The default value is 0 and the number of + displacements is automatically determined by the number of atoms in the + supercell, cutoff distance for anharmonic FCs its space group. generally, + 50 large-distance displacements are enough for most cases. + fcs_cutoff_radius: list + cutoff distance for anharmonic force constants(FCs) up to fourth-order FCs. + The default value is [-1, 12, 10], which means that the cutoff distance for + second-order FCs is the Wigner-Seitz cell boundary and the cutoff distance + for third-order FCs is 12 Borh, and the cutoff distance for fourth-order FCs + is 10 Bohr. Generally, the default value is good enough. + min_length: float + minimum length of lattice constants will be used to create the supercell, + the default value is 14.0 A. In most cases, the default value is good + enough, but it can be increased for larger supercells. + prefer_90_degrees: bool + if set to True, supercell algorithm will first try to find a supercell + with 3 90 degree angles. + get_supercell_size_kwargs: dict + kwargs that will be passed to get_supercell_size to determine supercell size + use_symmetrized_structure: str + allowed strings: "primitive", "conventional", None + + - "primitive" will enforce to start the phonon computation + from the primitive standard structure + according to Setyawan, W., & Curtarolo, S. (2010). + High-throughput electronic band structure calculations: + Challenges and tools. Computational Materials Science, + 49(2), 299-312. doi:10.1016/j.commatsci.2010.05.010. + This makes it possible to use certain k-path definitions + with this workflow. Otherwise, we must rely on seekpath + - "conventional" will enforce to start the phonon computation + from the conventional standard structure + according to Setyawan, W., & Curtarolo, S. (2010). + High-throughput electronic band structure calculations: + Challenges and tools. Computational Materials Science, + 49(2), 299-312. doi:10.1016/j.commatsci.2010.05.010. + We will however use seekpath and primitive structures + as determined by from phonopy to compute the phonon band structure + bulk_relax_maker: .BaseVaspMaker, or None + A maker to perform a tight relaxation on the bulk. + Set to ``None`` to skip the + bulk relaxation + static_energy_maker: .BaseVaspMaker, or None + A maker to perform the computation of the DFT energy on the bulk. + Set to ``None`` to skip the + static energy computation + born_maker: .BaseVaspMaker, or None + Maker to compute the BORN charges. + phonon_displacement_maker: .BaseVaspMaker + Maker used to compute the forces for a supercell. + generate_frequencies_eigenvectors_kwargs : dict + Keyword arguments passed to :obj:`generate_frequencies_eigenvectors`. + create_thermal_displacements: bool + Bool that determines if thermal_displacement_matrices are computed + kpath_scheme: str + scheme to generate kpoints. Please be aware that + you can only use seekpath with any kind of cell + Otherwise, please use the standard primitive structure + Available schemes are: + "seekpath", "hinuma", "setyawan_curtarolo", "latimer_munro". + "seekpath" and "hinuma" are the same definition but + seekpath can be used with any kind of unit cell as + it relies on phonopy to handle the relationship + to the primitive cell and not pymatgen + code: str = "vasp" + determines the DFT code. currently only vasp is implemented. + This keyword might enable the implementation of other codes + in the future + store_force_constants: bool + if True, force constants will be stored + socket: bool + If True, use the socket for the calculation + """ + + name: str = "phonon" + sym_reduce: bool = True + symprec: float = SETTINGS.PHONON_SYMPREC + cal_anhar_fcs: bool = False + displacement: float = 0.01 + displacement_anhar: float = 0.08 + num_displaced_supercells: int = 0 + num_disp_anhar: int = 0 + fcs_cutoff_radius: list = field(default_factory=lambda: [-1, 12, 10]) + min_length: float | None = 8.0 + max_atoms: float | None = 200 + force_90_degrees: bool = True + force_diagonal: bool = True + allow_orthorhombic: bool = False + prefer_90_degrees: bool = True + get_supercell_size_kwargs: dict = field(default_factory=dict) + use_symmetrized_structure: Literal["primitive", "conventional"] | None = None + create_thermal_displacements: bool = False + generate_frequencies_eigenvectors_kwargs: dict = field(default_factory=dict) + kpath_scheme: str = "seekpath" + store_force_constants: bool = True + socket: bool = False + code: str = "vasp" + bulk_relax_maker: BaseVaspMaker | None = field( + default_factory=lambda: DoubleRelaxMaker.from_relax_maker(TightRelaxMaker()) + ) + static_energy_maker: BaseVaspMaker | None = field( + default_factory=lambda: StaticMaker( + input_set_generator=StaticSetGenerator(auto_ispin=True) + ) + ) + born_maker: BaseVaspMaker | None = field(default_factory=DielectricMaker) + phonon_displacement_maker: BaseVaspMaker = field( + default_factory=PhononDisplacementMaker + ) + + @property + def prev_calc_dir_argname(self) -> str: + """Name of argument informing static maker of previous calculation directory. + + As this differs between different DFT codes (e.g., VASP, CP2K), it + has been left as a property to be implemented by the inheriting class. + + Note: this is only applicable if a relax_maker is specified; i.e., two + calculations are performed for each ordering (relax -> static) + """ + return "prev_dir" diff --git a/tests/common/schemas/test_pheasy.py b/tests/common/schemas/test_pheasy.py new file mode 100644 index 0000000000..6b91914097 --- /dev/null +++ b/tests/common/schemas/test_pheasy.py @@ -0,0 +1,60 @@ +import json + +import numpy as np +import pytest +from monty.json import MontyEncoder +from pydantic import ValidationError + +from atomate2.common.schemas.pheasy import ( + PhononBSDOSDoc, + PhononComputationalSettings, + PhononJobDirs, + PhononUUIDs, + ThermalDisplacementData, +) + + +def test_thermal_displacement_data(): + doc = ThermalDisplacementData(freq_min_thermal_displacements=0.0) + validated = ThermalDisplacementData.model_validate_json( + json.dumps(doc, cls=MontyEncoder) + ) + assert isinstance(validated, ThermalDisplacementData) + + +def test_phonon_bs_dos_doc(): + kwargs = { + "total_dft_energy": None, + "supercell_matrix": np.eye(3), + "primitive_matrix": np.eye(3), + "code": "test", + "phonopy_settings": PhononComputationalSettings( + npoints_band=1, kpath_scheme="test", kpoint_density_dos=1 + ), + "thermal_displacement_data": None, + "jobdirs": None, + "uuids": None, + } + doc = PhononBSDOSDoc(**kwargs) + # check validation raises no errors + validated = PhononBSDOSDoc.model_validate_json(json.dumps(doc, cls=MontyEncoder)) + assert isinstance(validated, PhononBSDOSDoc) + + # test invalid supercell_matrix type fails + with pytest.raises(ValidationError): + doc = PhononBSDOSDoc(**kwargs | {"supercell_matrix": (1, 1, 1)}) + + # test optional material_id + doc = PhononBSDOSDoc(**kwargs | {"material_id": 1234}) + assert doc.material_id == 1234 + + # test extra="allow" option + doc = PhononBSDOSDoc(**kwargs | {"extra_field": "test"}) + assert doc.extra_field == "test" + + +# schemas where all fields have default values +@pytest.mark.parametrize("model_cls", [PhononJobDirs, PhononUUIDs]) +def test_model_validate(model_cls): + validated = model_cls.model_validate_json(json.dumps(model_cls(), cls=MontyEncoder)) + assert isinstance(validated, model_cls) diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/inputs/INCAR.gz b/tests/test_data/vasp/Si_pheasy/dielectric/inputs/INCAR.gz new file mode 100644 index 0000000000..df8dbd7760 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/inputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/inputs/INCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/dielectric/inputs/INCAR.orig.gz new file mode 100644 index 0000000000..9f1c1ad5e8 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/inputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/inputs/POSCAR.gz b/tests/test_data/vasp/Si_pheasy/dielectric/inputs/POSCAR.gz new file mode 100644 index 0000000000..a393c90ee2 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/inputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/inputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/dielectric/inputs/POSCAR.orig.gz new file mode 100644 index 0000000000..9b2f5f8175 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/inputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/inputs/POTCAR.spec.gz b/tests/test_data/vasp/Si_pheasy/dielectric/inputs/POTCAR.spec.gz new file mode 100644 index 0000000000..03d35c79c0 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/inputs/POTCAR.spec.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/outputs/CONTCAR.gz b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/CONTCAR.gz new file mode 100644 index 0000000000..c84d13a711 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/outputs/INCAR.gz b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/INCAR.gz new file mode 100644 index 0000000000..df8dbd7760 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/outputs/INCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..9f1c1ad5e8 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/outputs/OUTCAR.gz b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/OUTCAR.gz new file mode 100644 index 0000000000..8037e1d4fd Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/outputs/POSCAR.gz b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/POSCAR.gz new file mode 100644 index 0000000000..a393c90ee2 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/outputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..9b2f5f8175 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/outputs/POTCAR.spec.gz b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/POTCAR.spec.gz new file mode 100644 index 0000000000..03d35c79c0 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/POTCAR.spec.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/dielectric/outputs/vasprun.xml.gz b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..652ee17466 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/dielectric/outputs/vasprun.xml.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/INCAR.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/INCAR.gz new file mode 100644 index 0000000000..933bcccb96 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/INCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/INCAR.orig.gz new file mode 100644 index 0000000000..98ee85e2d8 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/KPOINTS.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/KPOINTS.gz new file mode 100644 index 0000000000..6024584b1d Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/KPOINTS.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/KPOINTS.orig.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/KPOINTS.orig.gz new file mode 100644 index 0000000000..2ef5ae4de7 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/KPOINTS.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/POSCAR.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/POSCAR.gz new file mode 100644 index 0000000000..bd6627e23d Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/POSCAR.orig.gz new file mode 100644 index 0000000000..0d45f1dab8 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/POTCAR.spec.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/POTCAR.spec.gz new file mode 100644 index 0000000000..03d35c79c0 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/inputs/POTCAR.spec.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/CONTCAR.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/CONTCAR.gz new file mode 100644 index 0000000000..3c05a8c698 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/INCAR.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/INCAR.gz new file mode 100644 index 0000000000..933bcccb96 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/INCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..98ee85e2d8 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/KPOINTS.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/KPOINTS.gz new file mode 100644 index 0000000000..6024584b1d Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/KPOINTS.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/KPOINTS.orig.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/KPOINTS.orig.gz new file mode 100644 index 0000000000..2ef5ae4de7 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/KPOINTS.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/OUTCAR.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/OUTCAR.gz new file mode 100644 index 0000000000..e030514420 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/POSCAR.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/POSCAR.gz new file mode 100644 index 0000000000..bd6627e23d Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..0d45f1dab8 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/POTCAR.spec.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/POTCAR.spec.gz new file mode 100644 index 0000000000..03d35c79c0 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/POTCAR.spec.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/vasprun.xml.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..2ce85dbb84 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_1_2/outputs/vasprun.xml.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/INCAR.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/INCAR.gz new file mode 100644 index 0000000000..16e6f2ca36 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/INCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/INCAR.orig.gz new file mode 100644 index 0000000000..24b6a95d02 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/KPOINTS.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/KPOINTS.gz new file mode 100644 index 0000000000..d6bf6427a4 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/KPOINTS.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/KPOINTS.orig.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/KPOINTS.orig.gz new file mode 100644 index 0000000000..dc147b5962 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/KPOINTS.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/POSCAR.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/POSCAR.gz new file mode 100644 index 0000000000..053d239170 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/POSCAR.orig.gz new file mode 100644 index 0000000000..4923c8cf94 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/POTCAR.spec.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/POTCAR.spec.gz new file mode 100644 index 0000000000..03d35c79c0 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/inputs/POTCAR.spec.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/CONTCAR.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/CONTCAR.gz new file mode 100644 index 0000000000..87d01fccb9 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/INCAR.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/INCAR.gz new file mode 100644 index 0000000000..16e6f2ca36 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/INCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..24b6a95d02 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/KPOINTS.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/KPOINTS.gz new file mode 100644 index 0000000000..d6bf6427a4 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/KPOINTS.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/KPOINTS.orig.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/KPOINTS.orig.gz new file mode 100644 index 0000000000..dc147b5962 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/KPOINTS.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/OUTCAR.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/OUTCAR.gz new file mode 100644 index 0000000000..07b205a9df Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/POSCAR.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/POSCAR.gz new file mode 100644 index 0000000000..053d239170 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..4923c8cf94 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/POTCAR.spec.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/POTCAR.spec.gz new file mode 100644 index 0000000000..03d35c79c0 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/POTCAR.spec.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/vasprun.xml.gz b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..f44a08d39b Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/phonon_static_2_2/outputs/vasprun.xml.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/inputs/INCAR.gz b/tests/test_data/vasp/Si_pheasy/static/inputs/INCAR.gz new file mode 100644 index 0000000000..de79338e0e Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/inputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/inputs/INCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/static/inputs/INCAR.orig.gz new file mode 100644 index 0000000000..18a00cc740 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/inputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/inputs/POSCAR.gz b/tests/test_data/vasp/Si_pheasy/static/inputs/POSCAR.gz new file mode 100644 index 0000000000..a5aea21035 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/inputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/inputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/static/inputs/POSCAR.orig.gz new file mode 100644 index 0000000000..8209f6e0aa Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/inputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/inputs/POTCAR.spec.gz b/tests/test_data/vasp/Si_pheasy/static/inputs/POTCAR.spec.gz new file mode 100644 index 0000000000..03d35c79c0 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/inputs/POTCAR.spec.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/outputs/CONTCAR.gz b/tests/test_data/vasp/Si_pheasy/static/outputs/CONTCAR.gz new file mode 100644 index 0000000000..23797aae36 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/outputs/INCAR.gz b/tests/test_data/vasp/Si_pheasy/static/outputs/INCAR.gz new file mode 100644 index 0000000000..de79338e0e Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/outputs/INCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/static/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..18a00cc740 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/outputs/OUTCAR.gz b/tests/test_data/vasp/Si_pheasy/static/outputs/OUTCAR.gz new file mode 100644 index 0000000000..c6e4600a7c Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/outputs/POSCAR.gz b/tests/test_data/vasp/Si_pheasy/static/outputs/POSCAR.gz new file mode 100644 index 0000000000..a5aea21035 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/outputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/static/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..8209f6e0aa Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/outputs/POTCAR.spec.gz b/tests/test_data/vasp/Si_pheasy/static/outputs/POTCAR.spec.gz new file mode 100644 index 0000000000..03d35c79c0 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/outputs/POTCAR.spec.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/static/outputs/vasprun.xml.gz b/tests/test_data/vasp/Si_pheasy/static/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..c4ddad2006 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/static/outputs/vasprun.xml.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/INCAR.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/INCAR.gz new file mode 100644 index 0000000000..2699cdc1e4 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/INCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/INCAR.orig.gz new file mode 100644 index 0000000000..504bf3725d Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/POSCAR.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/POSCAR.gz new file mode 100644 index 0000000000..a0d31abdfe Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/POSCAR.orig.gz new file mode 100644 index 0000000000..cf967cea39 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/POTCAR.spec.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/POTCAR.spec.gz new file mode 100644 index 0000000000..03d35c79c0 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/inputs/POTCAR.spec.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/CONTCAR.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/CONTCAR.gz new file mode 100644 index 0000000000..f9b41a941d Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/INCAR.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/INCAR.gz new file mode 100644 index 0000000000..2699cdc1e4 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/INCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..504bf3725d Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/OUTCAR.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/OUTCAR.gz new file mode 100644 index 0000000000..47230006c9 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/POSCAR.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/POSCAR.gz new file mode 100644 index 0000000000..a0d31abdfe Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..cf967cea39 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/POTCAR.spec.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/POTCAR.spec.gz new file mode 100644 index 0000000000..03d35c79c0 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/POTCAR.spec.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/vasprun.xml.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..4c181d8ee7 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_1/outputs/vasprun.xml.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/INCAR.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/INCAR.gz new file mode 100644 index 0000000000..48def910ba Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/INCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/INCAR.orig.gz new file mode 100644 index 0000000000..ac7a6dbe93 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/POSCAR.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/POSCAR.gz new file mode 100644 index 0000000000..8b9450aac9 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/POSCAR.orig.gz new file mode 100644 index 0000000000..0a4823de33 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/POTCAR.spec.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/POTCAR.spec.gz new file mode 100644 index 0000000000..03d35c79c0 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/inputs/POTCAR.spec.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/CONTCAR.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/CONTCAR.gz new file mode 100644 index 0000000000..fc60000106 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/CONTCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/INCAR.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/INCAR.gz new file mode 100644 index 0000000000..48def910ba Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/INCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/INCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/INCAR.orig.gz new file mode 100644 index 0000000000..ac7a6dbe93 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/INCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/OUTCAR.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/OUTCAR.gz new file mode 100644 index 0000000000..6877560f70 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/OUTCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/POSCAR.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/POSCAR.gz new file mode 100644 index 0000000000..8b9450aac9 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/POSCAR.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/POSCAR.orig.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/POSCAR.orig.gz new file mode 100644 index 0000000000..0a4823de33 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/POSCAR.orig.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/POTCAR.spec.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/POTCAR.spec.gz new file mode 100644 index 0000000000..03d35c79c0 Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/POTCAR.spec.gz differ diff --git a/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/vasprun.xml.gz b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/vasprun.xml.gz new file mode 100644 index 0000000000..b7a814eaac Binary files /dev/null and b/tests/test_data/vasp/Si_pheasy/tight_relax_2/outputs/vasprun.xml.gz differ diff --git a/tests/vasp/flows/test_pheasy.py b/tests/vasp/flows/test_pheasy.py new file mode 100644 index 0000000000..cb1865f62f --- /dev/null +++ b/tests/vasp/flows/test_pheasy.py @@ -0,0 +1,424 @@ +from jobflow import run_locally +from numpy.testing import assert_allclose +from pymatgen.core.structure import Structure +from pymatgen.phonon.bandstructure import PhononBandStructureSymmLine +from pymatgen.phonon.dos import PhononDos + +from atomate2.common.flows.pheasy import BasePhononMaker +from atomate2.common.powerups import add_metadata_to_flow +from atomate2.common.schemas.pheasy import ( + Forceconstants, + PhononBSDOSDoc, + PhononComputationalSettings, + PhononJobDirs, + PhononUUIDs, +) +from atomate2.vasp.flows.pheasy import PhononMaker +from atomate2.vasp.jobs.base import BaseVaspMaker +from atomate2.vasp.powerups import update_user_incar_settings + + +def test_pheasy_wf_vasp(mock_vasp, clean_dir, si_structure: Structure, test_dir): + # mapping from job name to directory containing test files + ref_paths = { + "tight relax 1": "Si_pheasy/tight_relax_1", + "tight relax 2": "Si_pheasy/tight_relax_2", + "phonon static 1/2": "Si_pheasy/phonon_static_1_2", + "phonon static 2/2": "Si_pheasy/phonon_static_2_2", + "static": "Si_pheasy/static", + "dielectric": "Si_pheasy/dielectric", + } + + # settings passed to fake_run_vasp; adjust these to check for certain INCAR settings + fake_run_vasp_kwargs = { + "tight relax 1": {"incar_settings": ["NSW", "ISMEAR", "KSPACING"]}, + "tight relax 2": {"incar_settings": ["NSW", "ISMEAR", "KSPACING"]}, + "phonon static 1/2": {"incar_settings": ["NSW", "ISMEAR"]}, + "phonon static 2/2": {"incar_settings": ["NSW", "ISMEAR"]}, + "static": {"incar_settings": ["NSW", "ISMEAR"]}, + "dielectric": {"incar_settings": ["NSW", "ISMEAR"]}, + } + + # automatically use fake VASP and write POTCAR.spec dulsring the test + mock_vasp(ref_paths, fake_run_vasp_kwargs) + + si_struct = Structure.from_file( + test_dir / "vasp/Si_pheasy/tight_relax_1/inputs/POSCAR.gz" + ) + + job = PhononMaker( + force_diagonal=True, + min_length=12, + cal_anhar_fcs=False, + # use_symmetrized_structure="primitive" + ).make(structure=si_struct) + + job = update_user_incar_settings( + job, + { + "ENCUT": 600, + "ISMEAR": 0, + "SIGMA": 0.05, + "KSPACING": 0.15, + "ISPIN": 1, + "EDIFFG": -1e-04, + "EDIFF": 1e-07, + }, + ) + job = add_metadata_to_flow( + flow=job, + additional_fields={"mp_id": "mp-149", "unit_testing": "yes"}, + class_filter=(BaseVaspMaker, BasePhononMaker, PhononMaker), + ) + + # run the flow or job and ensure that it finished running successfully + responses = run_locally(job, create_folders=True, ensure_success=True) + + # validate the outputs + assert isinstance(responses[job.jobs[-1].uuid][1].output, PhononBSDOSDoc) + assert_allclose( + responses[job.jobs[-1].uuid][1].output.free_energies, + [ + 5792.458116272716, + 5792.451271742757, + 5792.308574619996, + 5791.3893705576875, + 5788.26230090264, + 5781.251118319793, + 5768.997781886127, + 5750.565423632229, + 5725.332982293328, + 5692.876443331414, + 5652.890004107427, + 5605.143827748774, + 5549.4640389105325, + 5485.723569667365, + 5413.837265156259, + 5333.758132354205, + 5245.473570542215, + 5149.001346193473, + 5044.385428634828, + 4931.691884236595, + 4811.004999276292, + 4682.423743675511, + 4546.058632404462, + 4402.028999079057, + 4250.460668093644, + 4091.483995048405, + 3925.2322370663665, + 3751.840212042715, + 3571.4432067784546, + 3384.176096803778, + 3190.172644484756, + 2989.5649460944087, + 2782.483002538277, + 2569.054392149452, + 2349.4040273117657, + 2123.6539796025945, + 1891.9233606773441, + 1654.3282482754973, + 1410.9816485521276, + 1161.9934874706462, + 907.4706252726158, + 647.5168891063854, + 382.2331197810191, + 111.71722934513599, + -163.9357332035992, + -444.6335102719554, + -730.2865598681723, + -1020.8079770828365, + -1316.1134140135116, + -1616.1209990625455, + ], + rtol=1e-5, # relaxed relative tolerance + atol=1e-5, # add a small absolute tolerance + ) + + 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, + 10, + 20, + 30, + 40, + 50, + 60, + 70, + 80, + 90, + 100, + 110, + 120, + 130, + 140, + 150, + 160, + 170, + 180, + 190, + 200, + 210, + 220, + 230, + 240, + 250, + 260, + 270, + 280, + 290, + 300, + 310, + 320, + 330, + 340, + 350, + 360, + 370, + 380, + 390, + 400, + 410, + 420, + 430, + 440, + 450, + 460, + 470, + 480, + 490, + ], + ) + assert responses[job.jobs[-1].uuid][1].output.has_imaginary_modes is False + assert isinstance( + responses[job.jobs[-1].uuid][1].output.force_constants, Forceconstants + ) + assert isinstance(responses[job.jobs[-1].uuid][1].output.jobdirs, PhononJobDirs) + assert isinstance(responses[job.jobs[-1].uuid][1].output.uuids, PhononUUIDs) + assert_allclose(responses[job.jobs[-1].uuid][1].output.total_dft_energy, -5.7466748) + assert_allclose( + responses[job.jobs[-1].uuid][1].output.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.0, 0.0), (0.0, 0.0, 0.0)), + ], + ) + assert_allclose( + responses[job.jobs[-1].uuid][1].output.epsilon_static, + ( + (13.31020238, 0.0, -0.000000000000000000000000000000041086505480261033), + (0.000000000000000000000000000000032869204384208823, 13.31020238, 0.0), + ( + 0.00000000000000000000000000000003697785493223493, + -0.00000000000000000000000000000005310360021821649, + 13.31020238, + ), + ), + atol=1e-8, + ) + assert_allclose( + responses[job.jobs[-1].uuid][1].output.supercell_matrix, + [[4.0, 0.0, 0.0], [0.0, 4.0, 0.0], [0.0, 0.0, 4.0]], + ) + assert_allclose( + responses[job.jobs[-1].uuid][1].output.primitive_matrix, + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], + rtol=1e-5, + atol=1e-10, + ) + assert responses[job.jobs[-1].uuid][1].output.code == "vasp" + assert isinstance( + responses[job.jobs[-1].uuid][1].output.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 + == "seekpath" + ) + assert ( + responses[job.jobs[-1].uuid][1].output.phonopy_settings.kpoint_density_dos + == 7000 + ) + assert_allclose( + responses[job.jobs[-1].uuid][1].output.entropies, + [ + 0.0, + 0.0029053715285381693, + 0.03534406735813481, + 0.17371749757534496, + 0.4806355132850958, + 0.9443138235461701, + 1.5217487290037728, + 2.1748672785891556, + 2.8785588902287014, + 3.61777903114171, + 4.383308818024208, + 5.168810618964096, + 5.969257468971105, + 6.780249611796599, + 7.597788260092625, + 8.418241566262875, + 9.238367696860214, + 10.055337466350297, + 10.866738450845892, + 11.670559750796489, + 12.465162508699496, + 13.249242154996521, + 14.02178733336692, + 14.78203900188199, + 15.529451895610356, + 16.26365953641141, + 16.984443284189616, + 17.691705481526853, + 18.38544648297612, + 19.065745223657316, + 19.732742925263075, + 20.38662953003233, + 21.027632473643276, + 21.6560074426566, + 22.27203080258059, + 22.87599342375645, + 23.468195671240995, + 24.04894336028136, + 24.618544510282547, + 25.177306757323354, + 25.725535308514058, + 26.26353134118291, + 26.791590766448167, + 27.31000329059948, + 27.81905171927258, + 28.31901145900843, + 28.810150178756654, + 29.29272760048077, + 29.76699539348012, + 30.23319715155372, + ], + atol=1e-6, + ) + assert_allclose( + responses[job.jobs[-1].uuid][1].output.heat_capacities, + [ + 0.0, + 0.009726834044170778, + 0.13866101670569114, + 0.6571570545060572, + 1.5667423561930287, + 2.639591924628341, + 3.7231364670350806, + 4.771184569421581, + 5.786825421391361, + 6.782392815371432, + 7.764289299349057, + 8.731219108699392, + 9.676996516283827, + 10.593722262345167, + 11.473976773888314, + 12.311941879701372, + 13.103752616984227, + 13.847404360575291, + 14.542454296999962, + 15.189663439692012, + 15.790656197118794, + 16.347630811439014, + 16.86312946277534, + 17.33986465971092, + 17.780593548325292, + 18.188030602925156, + 18.564789807070603, + 18.913348785224382, + 19.23602883664659, + 19.53498619439316, + 19.812210987727507, + 20.069531311525665, + 20.30862052298697, + 20.531006428284563, + 20.738081424927493, + 20.931112960999076, + 21.111253886165763, + 21.279552422144512, + 21.436961588185195, + 21.584347992205277, + 21.722499949574136, + 21.852134925891033, + 21.97390632235039, + 22.088409636021186, + 22.19618803517433, + 22.29773739353601, + 22.393510828356888, + 22.483922786412425, + 22.56935272015279, + 22.65014839366568, + ], + rtol=1e-5, # relaxed relative tolerance + atol=1e-5, # add a small absolute tolerance + ) + + assert_allclose( + responses[job.jobs[-1].uuid][1].output.internal_energies, + [ + 5792.458116272716, + 5792.480324532992, + 5793.015455042023, + 5796.60089455924, + 5807.487720506598, + 5828.466808566305, + 5860.302704690473, + 5902.806132190803, + 5955.617692560522, + 6018.476555173027, + 6091.22088493709, + 6173.712994848893, + 6265.774934186432, + 6367.156018184099, + 6477.527620534776, + 6596.4943662401865, + 6723.612400966099, + 6858.408714377748, + 7000.398348669136, + 7149.098235746229, + 7304.03749984975, + 7464.76459503267, + 7630.851844526549, + 7801.897968265965, + 7977.529121766132, + 8157.3988778485345, + 8341.187489623593, + 8528.600690692962, + 8719.3682206193, + 8913.242210240971, + 9109.995520608829, + 9309.420098917732, + 9511.32539258518, + 9715.536846674577, + 9921.894498604659, + 10130.251676299567, + 10340.473800672748, + 10552.437289894393, + 10766.028560740182, + 10981.143121073083, + 11197.684746889987, + 11415.564737168335, + 11634.701239831205, + 11855.018642409685, + 12076.447021347745, + 12298.92164431772, + 12522.382520360086, + 12746.773993107487, + 12972.044372785333, + 13198.145603091058, + ], + rtol=1e-5, + atol=1e-5, + ) + assert responses[job.jobs[-1].uuid][1].output.chemsys == "Si" diff --git a/tests/vasp/lobster/schemas/test_lobster.py b/tests/vasp/lobster/schemas/test_lobster.py index ec72b82fd0..cee8058adc 100644 --- a/tests/vasp/lobster/schemas/test_lobster.py +++ b/tests/vasp/lobster/schemas/test_lobster.py @@ -202,7 +202,6 @@ def test_lobster_task_doc_saved_jsons(lobster_test_dir): "calc_quality_text", "dos", "lso_dos", - "builder_meta", ] for cba_key in expected_cba_keys_json: @@ -256,7 +255,6 @@ def test_lobster_task_doc_saved_jsons(lobster_test_dir): ) expected_computational_data_keys_json = [ - "builder_meta", "structure", "charges", "lobsterout",