From 07757857fa192c0ccce27e788bff8f220ed73717 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Mon, 9 Sep 2024 14:02:47 -0400 Subject: [PATCH 01/29] Add pheasy branch for phonon calculation using LASSO --- src/atomate2/common/flows/pheasy.py | 0 src/atomate2/common/jobs/pheasy.py | 0 src/atomate2/common/schemas/pheasy.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/atomate2/common/flows/pheasy.py create mode 100644 src/atomate2/common/jobs/pheasy.py create mode 100644 src/atomate2/common/schemas/pheasy.py diff --git a/src/atomate2/common/flows/pheasy.py b/src/atomate2/common/flows/pheasy.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/atomate2/common/jobs/pheasy.py b/src/atomate2/common/jobs/pheasy.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py new file mode 100644 index 0000000000..e69de29bb2 From 789155456d2bc26736b158335c317681c249248a Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Mon, 9 Sep 2024 16:17:39 -0400 Subject: [PATCH 02/29] pheasy_phonons --- src/atomate2/common/flows/pheasy.py | 374 ++++++++++++ src/atomate2/common/jobs/pheasy.py | 420 +++++++++++++ src/atomate2/common/schemas/pheasy.py | 812 ++++++++++++++++++++++++++ 3 files changed, 1606 insertions(+) diff --git a/src/atomate2/common/flows/pheasy.py b/src/atomate2/common/flows/pheasy.py index e69de29bb2..6f5a4a1e22 100644 --- a/src/atomate2/common/flows/pheasy.py +++ b/src/atomate2/common/flows/pheasy.py @@ -0,0 +1,374 @@ +"""Flows for calculating phonons.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from jobflow import Flow, Maker + +from atomate2.common.jobs.pheasy import ( + generate_frequencies_eigenvectors, + generate_phonon_displacements, + get_supercell_size, + get_total_energy_per_cell, + run_phonon_displacements, +) +from atomate2.common.jobs.utils import structure_to_conventional, structure_to_primitive + +if TYPE_CHECKING: + from pathlib import Path + + from emmet.core.math import Matrix3D + 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(Maker, ABC): + """ + Maker to calculate harmonic phonons with a DFT/force field code and Phonopy. + + 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. 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, .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-4 + 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: str | 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 + + # add mpid to the phonon output + mp_id: str = None + store_force_constants: bool = True + socket: bool = False + + def make( + self, + structure: Structure, + prev_dir: str | Path | None = None, + born: list[Matrix3D] | None = None, + epsilon_static: Matrix3D | None = None, + total_dft_energy_per_formula_unit: float | None = None, + supercell_matrix: Matrix3D | None = None, + ) -> Flow: + """Make flow to calculate the phonon properties. + + Parameters + ---------- + structure : Structure + A pymatgen structure object. Please start with a structure + that is nearly fully optimized as the internal optimizers + have very strict settings! + prev_dir : str or Path or None + A previous calculation directory to use for copying outputs. + born: Matrix3D + Instead of recomputing born charges and epsilon, these values can also be + provided manually. If born and epsilon_static are provided, the born run + will be skipped it can be provided in the VASP convention with information + for every atom in unit cell. Please be careful when converting structures + within in this workflow as this could lead to errors + epsilon_static: Matrix3D + The high-frequency dielectric constant to use instead of recomputing born + charges and epsilon. If born, epsilon_static are provided, the born run + will be skipped + total_dft_energy_per_formula_unit: float + It has to be given per formula unit (as a result in corresponding Doc). + Instead of recomputing the energy of the bulk structure every time, this + value can also be provided in eV. If it is provided, the static run will be + skipped. This energy is the typical output dft energy of the dft workflow. + No conversion needed. + supercell_matrix: list + Instead of min_length, also a supercell_matrix can be given, e.g. + [[1.0,0.0,0.0],[0.0,1.0,0.0],[0.0,0.0,1.0] + """ + use_symmetrized_structure = self.use_symmetrized_structure + kpath_scheme = self.kpath_scheme + valid_structs = (None, "primitive", "conventional") + if use_symmetrized_structure not in valid_structs: + raise ValueError( + f"Invalid {use_symmetrized_structure=}, use one of {valid_structs}" + ) + + if use_symmetrized_structure != "primitive" and kpath_scheme != "seekpath": + raise ValueError( + f"You can't use {kpath_scheme=} with the primitive standard " + "structure, please use seekpath" + ) + + valid_schemes = ("seekpath", "hinuma", "setyawan_curtarolo", "latimer_munro") + if kpath_scheme not in valid_schemes: + raise ValueError( + f"{kpath_scheme=} is not implemented, use one of {valid_schemes}" + ) + + if self.code is None or self.code not in SUPPORTED_CODES: + raise ValueError( + "The code variable must be passed and it must be a supported code." + f" Supported codes are: {SUPPORTED_CODES}" + ) + + jobs = [] + + # TODO: should this be after or before structural optimization as the + # optimization could change the symmetry we could add a tutorial and point out + # that the structure should be nearly optimized before the phonon workflow + if self.use_symmetrized_structure == "primitive": + # These structures are compatible with many + # of the kpath algorithms that are used for Materials Project + prim_job = structure_to_primitive(structure, self.symprec) + jobs.append(prim_job) + structure = prim_job.output + elif self.use_symmetrized_structure == "conventional": + # it could be beneficial to use conventional standard structures to arrive + # faster at supercells with right angles + conv_job = structure_to_conventional(structure, self.symprec) + jobs.append(conv_job) + structure = conv_job.output + + optimization_run_job_dir = None + optimization_run_uuid = None + + if self.bulk_relax_maker is not None: + # optionally relax the structure + bulk_kwargs = {} + if self.prev_calc_dir_argname is not None: + bulk_kwargs[self.prev_calc_dir_argname] = prev_dir + bulk = self.bulk_relax_maker.make(structure, **bulk_kwargs) + jobs.append(bulk) + structure = bulk.output.structure + prev_dir = bulk.output.dir_name + optimization_run_job_dir = bulk.output.dir_name + optimization_run_uuid = bulk.output.uuid + + # 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, + self.min_length, + self.prefer_90_degrees, + **self.get_supercell_size_kwargs, + ) + jobs.append(supercell_job) + supercell_matrix = supercell_job.output + + # Computation of static energy + total_dft_energy = None + static_run_job_dir = None + static_run_uuid = None + if (self.static_energy_maker is not None) and ( + total_dft_energy_per_formula_unit is None + ): + static_job_kwargs = {} + if self.prev_calc_dir_argname is not None: + static_job_kwargs[self.prev_calc_dir_argname] = prev_dir + static_job = self.static_energy_maker.make( + structure=structure, **static_job_kwargs + ) + jobs.append(static_job) + total_dft_energy = static_job.output.output.energy + static_run_job_dir = static_job.output.dir_name + static_run_uuid = static_job.output.uuid + prev_dir = static_job.output.dir_name + elif total_dft_energy_per_formula_unit is not None: + # to make sure that one can reuse results from Doc + compute_total_energy_job = get_total_energy_per_cell( + total_dft_energy_per_formula_unit, structure + ) + jobs.append(compute_total_energy_job) + 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, + ) + 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, + ) + jobs.append(displacement_calcs) + + # Computation of BORN charges + born_run_job_dir = None + born_run_uuid = None + if self.born_maker is not None and (born is None or epsilon_static is None): + born_kwargs = {} + if self.prev_calc_dir_argname is not None: + born_kwargs[self.prev_calc_dir_argname] = prev_dir + born_job = self.born_maker.make(structure, **born_kwargs) + jobs.append(born_job) + + # I am not happy how we currently access "born" charges + # This is very vasp specific code aims and forcefields + # do not support this at the moment, if this changes we have + # to update this section + epsilon_static = born_job.output.calcs_reversed[0].output.epsilon_static + born = born_job.output.calcs_reversed[0].output.outcar["born"] + born_run_job_dir = born_job.output.dir_name + born_run_uuid = born_job.output.uuid + + phonon_collect = generate_frequencies_eigenvectors( + 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, + mp_id=self.mp_id, + 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, + ) + + jobs.append(phonon_collect) + + # create a flow including all jobs for a phonon computation + return Flow(jobs, phonon_collect.output) + + @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/jobs/pheasy.py b/src/atomate2/common/jobs/pheasy.py index e69de29bb2..0d975a48f0 100644 --- a/src/atomate2/common/jobs/pheasy.py +++ b/src/atomate2/common/jobs/pheasy.py @@ -0,0 +1,420 @@ +"""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.pheasy import Forceconstants, PhononBSDOSDoc, 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 + + +logger = logging.getLogger(__name__) + + +@job +def get_total_energy_per_cell( + total_dft_energy_per_formula_unit: float, structure: Structure +) -> float: + """ + Job that computes total DFT energy of the cell. + + Parameters + ---------- + total_dft_energy_per_formula_unit: float + Total DFT energy in eV per formula unit. + structure: Structure object + Corresponding structure object. + """ + formula_units = ( + structure.composition.num_atoms + / structure.composition.reduced_composition.num_atoms + ) + + return total_dft_energy_per_formula_unit * formula_units + + +@job +def get_supercell_size( + structure: Structure, min_length: float, prefer_90_degrees: bool, **kwargs +) -> list[list[float]]: + """ + Determine supercell size with given min_length. + + Parameters + ---------- + structure: Structure Object + Input structure that will be used to determine supercell + min_length: float + minimum length of cell in Angstrom + prefer_90_degrees: bool + if True, the algorithm will try to find a cell with 90 degree angles first + **kwargs: + Additional parameters that can be set. + """ + kwargs.setdefault("force_diagonal", False) + common_kwds = dict( + min_length=min_length, + min_atoms=kwargs.get("min_atoms"), + force_diagonal=kwargs["force_diagonal"], + ) + + if not prefer_90_degrees: + transformation = CubicSupercellTransformation( + **common_kwds, max_atoms=kwargs.get("max_atoms"), force_90_degrees=False + ) + transformation.apply_transformation(structure=structure) + else: + try: + transformation = CubicSupercellTransformation( + **common_kwds, + max_atoms=kwargs.get("max_atoms", 1200), + force_90_degrees=True, + angle_tolerance=kwargs.get("angle_tolerance", 1e-2), + ) + transformation.apply_transformation(structure=structure) + + except AttributeError: + transformation = CubicSupercellTransformation( + **common_kwds, max_atoms=kwargs.get("max_atoms"), force_90_degrees=False + ) + transformation.apply_transformation(structure=structure) + + # matrix from pymatgen has to be transposed + return transformation.transformation_matrix.transpose().tolist() + + +@job(data=[Structure]) +def generate_phonon_displacements( + structure: Structure, + supercell_matrix: np.array, + displacement: float, + sym_reduce: bool, + symprec: float, + use_symmetrized_structure: str | None, + kpath_scheme: str, + code: str, +) -> list[Structure]: + """ + Generate displaced structures with phonopy. + + 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 + 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 + + phonon = Phonopy( + cell, + supercell_matrix, + primitive_matrix=primitive_matrix, + factor=factor, + symprec=symprec, + is_symmetry=sym_reduce, + ) + + + # following is modified by jiongzhi Zheng + # I will use the ALM to determine how many displaced supercells we need to use for extract second order force constants here + from alm import ALM + supercell_z = phonon.supercell + lattice = supercell_z.cell + positions = supercell_z.scaled_positions + numbers = supercell_z.numbers + natom = len(numbers) + with ALM(lattice, positions, numbers) as alm: + alm.define(1) + alm.suggest() + n_fp = alm._get_number_of_irred_fc_elements(1) + + """Determine how many displaced supercells we need to use for extract second order force constants here + if we want to calculate the lattice thermal conductivty here, I highly suggest you to use the finite diplacement method + to calculate the zero-K second order force constants which garantee you get the completely converged results""" + + num = int(np.ceil(n_fp / (3.0 * natom))) + + + displacement_t = 0.01 + phonon.generate_displacements(displacement_t) + num_disp_t = len(phonon.displacements) + if num_disp_t > 3: + num_d = int(np.ceil(num * 1.8)) + else: + pass + + + + #previous version + #if num_disp_t > 3: + # num_d = int(np.ceil(num_disp_t / 3.0)) + # if num_d < num: + # num_d = int(num + 1) + # else: + # pass + #else: + # num_d = int(num+1) + + print ("The number of free parameters of Second Order Force Constants is ", n_fp) + print () + print ("The Number of Equations Used to Obtain the 2ND FCs is ", 3 * natom * num) + print () + print ("Be Careful!!! Full Rank of Matrix can not always guarantee the correct result sometimes"\ + "\n if the total atoms in supercell is less than 100 and"\ + "\n lattice constants are less than 10 angstrom,"\ + "\n I highly suggest you to displace more random configurations"\ + "\n At least use one or two more configurations basd on the suggested number of displacements") + + displacement_f = 0.01 + phonon.generate_displacements(distance=displacement_f) + + disps = phonon.displacements + + finite_disp = False + f_disp_n = len(disps) + if f_disp_n > 3: + phonon.generate_displacements(distance=displacement, number_of_snapshots=num_d, random_seed=103) + else: + finite_disp = True + + supercells = phonon.supercells_with_displacements + + displacements = [] + for cell in supercells: + displacements.append(get_pmg_structure(cell)) + + displacements.append(get_pmg_structure(phonon.supercell)) + return displacements + + + #phonon.generate_displacements(distance=displacement) + + #supercells = phonon.supercells_with_displacements + + #return [get_pmg_structure(cell) for cell in supercells] + + +@job( + output_schema=PhononBSDOSDoc, + data=[PhononDos, PhononBandStructureSymmLine, Forceconstants], +) +def generate_frequencies_eigenvectors( + structure: Structure, + supercell_matrix: np.array, + displacement: float, + sym_reduce: bool, + symprec: float, + use_symmetrized_structure: str | None, + kpath_scheme: str, + code: str, + mp_id: 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, + sym_reduce=sym_reduce, + symprec=symprec, + use_symmetrized_structure=use_symmetrized_structure, + kpath_scheme=kpath_scheme, + code=code, + mp_id=mp_id, + displacement_data=displacement_data, + total_dft_energy=total_dft_energy, + epsilon_static=epsilon_static, + born=born, + **kwargs, + ) + + +@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)}") + + # 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 index e69de29bb2..07c1ebca9c 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -0,0 +1,812 @@ +"""Schemas for phonon documents.""" + +import copy +import logging +from pathlib import Path +from typing import Optional, Union + +import numpy as np +from emmet.core.math import Matrix3D +from emmet.core.structure import StructureMetadata +from monty.json import MSONable +from phonopy import Phonopy +from phonopy.phonon.band_structure import get_band_qpoints_and_path_connections +from phonopy.structure.symmetry import symmetrize_borns_and_epsilon +from phonopy.units import VaspToTHz +from pydantic import BaseModel, 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 lib by jiongzhi zheng +from ase.io import read +from hiphive import ClusterSpace, ForceConstantPotential, ForceConstants, enforce_rotational_sum_rules +from hiphive.cutoffs import estimate_maximum_cutoff +from hiphive.utilities import extract_parameters +import subprocess +from phonopy.file_IO import write_FORCE_CONSTANTS, parse_FORCE_CONSTANTS +from phonopy.interface.vasp import write_vasp +from phonopy.interface.vasp import read_vasp + + +from atomate2.aims.utils.units import omegaToTHz + +logger = logging.getLogger(__name__) + + +def get_factor(code: str) -> float: + """ + Get the frequency conversion factor to THz for each code. + + Parameters + ---------- + code: str + The code to get the conversion factor for + + Returns + ------- + float + The correct conversion factor + + Raises + ------ + ValueError + If code is not defined + """ + if code in ["forcefields", "vasp"]: + return VaspToTHz + if code == "aims": + return omegaToTHz # Based on CODATA 2002 + raise ValueError(f"Frequency conversion factor for code ({code}) not defined.") + + +class PhononComputationalSettings(BaseModel): + """Collection to store computational settings for the phonon computation.""" + + # could be optional and implemented at a later stage? + npoints_band: int = Field("number of points for band structure computation") + kpath_scheme: str = Field("indicates the kpath scheme") + kpoint_density_dos: int = Field( + "number of points for computation of free energies and densities of states", + ) + + +class ThermalDisplacementData(BaseModel): + """Collection to store information on the thermal displacement matrices.""" + + freq_min_thermal_displacements: float = Field( + "cutoff frequency in THz to avoid numerical issues in the " + "computation of the thermal displacement parameters" + ) + thermal_displacement_matrix_cif: Optional[list[list[Matrix3D]]] = Field( + None, description="field including thermal displacement matrices in CIF format" + ) + thermal_displacement_matrix: Optional[list[list[Matrix3D]]] = Field( + None, + description="field including thermal displacement matrices in Cartesian " + "coordinate system", + ) + temperatures_thermal_displacements: Optional[list[int]] = Field( + None, + description="temperatures at which the thermal displacement matrices" + "have been computed", + ) + + +class PhononUUIDs(BaseModel): + """Collection to save all uuids connected to the phonon run.""" + + optimization_run_uuid: Optional[str] = Field( + None, description="optimization run uuid" + ) + displacements_uuids: Optional[list[str]] = Field( + None, description="The uuids of the displacement jobs." + ) + static_run_uuid: Optional[str] = Field(None, description="static run uuid") + born_run_uuid: Optional[str] = Field(None, description="born run uuid") + + +class Forceconstants(MSONable): + """A force constants class.""" + + def __init__(self, force_constants: list[list[Matrix3D]]) -> None: + self.force_constants = force_constants + + +class PhononJobDirs(BaseModel): + """Collection to save all job directories relevant for the phonon run.""" + + displacements_job_dirs: Optional[list[Optional[str]]] = Field( + None, description="The directories where the displacement jobs were run." + ) + static_run_job_dir: Optional[Optional[str]] = Field( + None, description="Directory where static run was performed." + ) + born_run_job_dir: Optional[str] = Field( + None, description="Directory where born run was performed." + ) + optimization_run_job_dir: Optional[str] = Field( + None, description="Directory where optimization run was performed." + ) + taskdoc_run_job_dir: Optional[str] = Field( + None, description="Directory where task doc was generated." + ) + + +class PhononBSDOSDoc(StructureMetadata, extra="allow"): # type: ignore[call-arg] + """Collection of all data produced by the phonon workflow.""" + + structure: Optional[Structure] = Field( + None, description="Structure of Materials Project." + ) + + phonon_bandstructure: Optional[PhononBandStructureSymmLine] = Field( + None, + description="Phonon band structure object.", + ) + + phonon_dos: Optional[PhononDos] = Field( + None, + description="Phonon density of states object.", + ) + + free_energies: Optional[list[float]] = Field( + None, + description="vibrational part of the free energies in J/mol per " + "formula unit for temperatures in temperature_list", + ) + + heat_capacities: Optional[list[float]] = Field( + None, + description="heat capacities in J/K/mol per " + "formula unit for temperatures in temperature_list", + ) + + internal_energies: Optional[list[float]] = Field( + None, + description="internal energies in J/mol per " + "formula unit for temperatures in temperature_list", + ) + entropies: Optional[list[float]] = Field( + None, + description="entropies in J/(K*mol) per formula unit" + "for temperatures in temperature_list ", + ) + + temperatures: Optional[list[int]] = Field( + None, + description="temperatures at which the vibrational" + " part of the free energies" + " and other properties have been computed", + ) + + total_dft_energy: Optional[float] = Field("total DFT energy per formula unit in eV") + + has_imaginary_modes: Optional[bool] = Field( + None, description="if true, structure has imaginary modes" + ) + + # needed, e.g. to compute Grueneisen parameter etc + force_constants: Optional[Forceconstants] = Field( + None, description="Force constants between every pair of atoms in the structure" + ) + + born: Optional[list[Matrix3D]] = Field( + None, + description="born charges as computed from phonopy. Only for symmetrically " + "different atoms", + ) + + epsilon_static: Optional[Matrix3D] = 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: Optional[ThermalDisplacementData] = Field( + "Includes all data of the computation of the thermal displacements" + ) + + jobdirs: Optional[PhononJobDirs] = Field( + "Field including all relevant job directories" + ) + + uuids: Optional[PhononUUIDs] = Field("Field including all relevant uuids") + + @classmethod + def from_forces_born( + cls, + structure: Structure, + supercell_matrix: np.array, + displacement: float, + sym_reduce: bool, + symprec: float, + use_symmetrized_structure: Union[str, None], + kpath_scheme: str, + code: str, + mp_id: 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 + 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 + + phonon = Phonopy( + cell, + supercell_matrix, + primitive_matrix=primitive_matrix, + factor=factor, + symprec=symprec, + is_symmetry=sym_reduce, + ) + + # modified by jiongzhi zheng + supercell = phonon.get_supercell() + write_vasp("POSCAR", cell) + write_vasp("SPOSCAR", supercell) + + phonon.generate_displacements(distance=displacement) + disps_j = phonon.displacements + + f_disp_n1 = len(disps_j) + + # jiongzhi zheng + # Print keys and their corresponding values + for key, value in displacement_data.items(): + print(f"{key}: {value}") + + + if f_disp_n1 < 1: + set_of_forces = [np.array(forces) for forces in displacement_data["forces"]] + set_of_forces_a = np.array(set_of_forces) + set_of_disps = [np.array(disps.cart_coords) for disps in displacement_data["displaced_structures"]] + set_of_disps_m = np.array(set_of_disps) + print(set_of_disps_m) + + print(np.shape(set_of_disps_m)) + n_shape = set_of_disps_m.shape[0] + # set_of_disps_d = {} + # set_of_disps_d['displacements'] = set_of_disps_m + set_of_disps_d = {'displacements': set_of_disps_m, 'dtype': 'double', 'order': 'C'} + else: + set_of_forces = [np.array(forces) for forces in displacement_data["forces"]] + set_of_forces_a_o = np.array(set_of_forces) + set_of_forces_a_t = set_of_forces_a_o - set_of_forces_a_o[-1, :, :] + set_of_forces_a = set_of_forces_a_t[:-1, :, :] + set_of_disps = [np.array(disps.cart_coords) for disps in displacement_data["displaced_structures"]] + set_of_disps_m_o = np.round((set_of_disps - supercell.get_positions()), decimals=16).astype('double') + set_of_disps_m = set_of_disps_m_o[:-1, :, :] + print(set_of_disps_m) + + print(np.shape(set_of_disps_m)) + n_shape = set_of_disps_m.shape[0] + # set_of_disps_d = {} + # set_of_disps_d['displacements'] = set_of_disps_m + set_of_disps_d = {'displacements': set_of_disps_m, 'dtype': 'double', 'order': 'C'} + # phonon.set_displacement_dataset(set_of_disps_d) + # print (set_of_disps) + + import pickle + with open("disp_matrix.pkl","wb") as file: + pickle.dump(set_of_disps_m,file) + with open("force_matrix.pkl","wb") as file: + pickle.dump(set_of_forces_a,file) + + """"put a if else condition here to determine whether we need to calculate the higher order force constants """ + Calc_anharmonic_FCs = False + if Calc_anharmonic_FCs: + from alm import ALM + supercell_z = phonon.supercell + lattice = supercell_z.cell + positions = supercell_z.scaled_positions + numbers = supercell_z.numbers + natom = len(numbers) + with ALM(lattice, positions, numbers) as alm: + alm.define(1) + alm.suggest() + n_fp = alm._get_number_of_irred_fc_elements(1) + + num = int(np.ceil(n_fp / (3.0 * natom))) + num_round = int(np.round((n_fp / (3.0 * natom)))) + + if num > num_round: + num_d = num + displacement_t = 0.01 + phonon.generate_displacements(displacement_t) + num_disp_t = len(phonon.displacements) + int_num = int(num_disp_t / num_d) + if int_num > 3: + num_d = int(np.ceil(int_num / 3.0)) + else: + num_d = int(np.ceil(int_num / 3.0) + 1) + else: + num_d = num + displacement_t = 0.01 + phonon.generate_displacements(displacement_t) + num_disp_t = len(phonon.displacements) + int_num = int(num_disp_t / num_d) + if int_num >= 3: + num_d = int(np.ceil(int_num / 3.0)) + else: + num_d = int(num + 1) + + displacement_f = 0.01 + phonon.generate_displacements(distance=displacement_f) + disps = phonon.displacements + + f_disp_n = int(len(disps)) + if f_disp_n > 2: + num_har = num_d + else: + num_har = f_disp_n + else: + num_har = n_shape + + + #set_of_forces = [np.array(forces) for forces in displacement_data["forces"]] + + 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 + + # Produces all force constants + #phonon.produce_force_constants(forces=set_of_forces) + #phonon.produce_force_constants(forces=set_of_forces, fc_calculator="alm") + #phonon.symmetrize_force_constants() + #fcs = phonon.force_constants + #write_FORCE_CONSTANTS(fcs) + prim = read('POSCAR') + supercell = read('SPOSCAR') + + + pheasy_cmd_1 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --symprec 1e-3 --nbody 2'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2])) + pheasy_cmd_2 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 -w 2'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2])) + pheasy_cmd_3 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --ndata "{3}" --disp_file'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + + displacement_f = 0.01 + phonon.generate_displacements(distance=displacement_f) + disps = phonon.displacements + num_judge = len(disps) + + if num_judge > 3: + pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + else: + pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + #pheasy_cmd_4_1 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --hdf5 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + + print ("Start running pheasy in andes8: Dartmouth College Clusters or NERSC Perlmutter") + subprocess.call(pheasy_cmd_1, shell=True) + subprocess.call(pheasy_cmd_2, shell=True) + subprocess.call(pheasy_cmd_3, shell=True) + subprocess.call(pheasy_cmd_4, shell=True) + #subprocess.call(pheasy_cmd_4_1, shell=True) + + # Produces all force constants + #phonon.produce_force_constants(forces=set_of_forces) + + force_constants = parse_FORCE_CONSTANTS(filename="FORCE_CONSTANTS") + phonon.force_constants = force_constants + 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" + + # 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) + 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) + ) + + # jiongzhi zheng + 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 + + if imaginary_modes_hiphive: + pheasy_cmd_11 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --c2 10.0 --symprec 1e-3 --nbody 2'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2])) + pheasy_cmd_12 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 --c2 10.0 -w 2'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2])) + pheasy_cmd_13 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --c2 10.0 --ndata "{3}" --disp_file'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + + displacement_f = 0.01 + phonon.generate_displacements(distance=displacement_f) + disps = phonon.displacements + num_judge = len(disps) + + if num_judge > 3: + pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --c2 10.0 --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + else: + pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc --c2 10.0 -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + #pheasy_cmd_4_1 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --hdf5 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + + print ("Start running pheasy in discovery") + subprocess.call(pheasy_cmd_11, shell=True) + subprocess.call(pheasy_cmd_12, shell=True) + subprocess.call(pheasy_cmd_13, shell=True) + subprocess.call(pheasy_cmd_14, shell=True) + + force_constants = parse_FORCE_CONSTANTS(filename="FORCE_CONSTANTS") + phonon.force_constants = force_constants + phonon.symmetrize_force_constants() + + + #phonon.force_constants = fcs + + # 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, + 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, + mp_id=mp_id, + 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 From d643fc1c81b170a5e76b7276ec058a8eb2ecaaeb Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Tue, 10 Sep 2024 16:04:22 -0400 Subject: [PATCH 03/29] add_pheasy_to_vasp_folder --- src/atomate2/vasp/flows/pheasy.py | 154 ++++++++++++++++++++++++++++++ src/atomate2/vasp/jobs/pheasy.py | 0 2 files changed, 154 insertions(+) create mode 100644 src/atomate2/vasp/flows/pheasy.py create mode 100644 src/atomate2/vasp/jobs/pheasy.py diff --git a/src/atomate2/vasp/flows/pheasy.py b/src/atomate2/vasp/flows/pheasy.py new file mode 100644 index 0000000000..a2b5a90097 --- /dev/null +++ b/src/atomate2/vasp/flows/pheasy.py @@ -0,0 +1,154 @@ +"""Define the VASP PhononMaker.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +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.pheasy 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 VASP and Phonopy. + + 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. 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 = "phonon" + Name of the flows produced by this maker. + sym_reduce : bool = True + Whether to reduce the number of deformations using symmetry. + symprec : float = 1e-4 + 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 = 0.01 + displacement distance for phonons + min_length: float = 20.0 + min length of the supercell that will be built + prefer_90_degrees: bool = True + 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 or None = None + 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 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 = "seekpath" + 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 + 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: str | None = None + create_thermal_displacements: bool = True + 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/src/atomate2/vasp/jobs/pheasy.py b/src/atomate2/vasp/jobs/pheasy.py new file mode 100644 index 0000000000..e69de29bb2 From 93219ce504cf5b69cc4b4cbc40606619947adc44 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Tue, 10 Sep 2024 16:06:06 -0400 Subject: [PATCH 04/29] Add_pheasy_to_vasp_file --- src/atomate2/vasp/jobs/pheasy.py | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/atomate2/vasp/jobs/pheasy.py b/src/atomate2/vasp/jobs/pheasy.py index e69de29bb2..20fb339c57 100644 --- a/src/atomate2/vasp/jobs/pheasy.py +++ b/src/atomate2/vasp/jobs/pheasy.py @@ -0,0 +1,61 @@ +"""Define the PhononDisplacementMaker for VASP.""" + +from dataclasses import dataclass, field + +from atomate2.vasp.jobs.base import BaseVaspMaker +from atomate2.vasp.sets.base import VaspInputGenerator +from atomate2.vasp.sets.core import StaticSetGenerator + + +@dataclass +class PhononDisplacementMaker(BaseVaspMaker): + """ + Maker to perform a static calculation as a part of the finite displacement method. + + The input set is for a static run with tighter convergence parameters. + Both the k-point mesh density and convergence parameters + are stricter than a normal relaxation. + + Parameters + ---------- + name : str + The job name. + input_set_generator : .VaspInputGenerator + A generator used to make the input set. + write_input_set_kwargs : dict + Keyword arguments that will get passed to :obj:`.write_vasp_input_set`. + copy_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.copy_vasp_outputs`. + run_vasp_kwargs : dict + Keyword arguments that will get passed to :obj:`.run_vasp`. + task_document_kwargs : dict + Keyword arguments that will get passed to :obj:`.TaskDoc.from_directory`. + stop_children_kwargs : dict + Keyword arguments that will get passed to :obj:`.should_stop_children`. + write_additional_data : dict + Additional data to write to the current directory. Given as a dict of + {filename: data}. Note that if using FireWorks, dictionary keys cannot contain + the "." character which is typically used to denote file extensions. To avoid + this, use the ":" character, which will automatically be converted to ".". E.g. + ``{"my_file:txt": "contents of the file"}``. + """ + + name: str = "phonon static" + input_set_generator: VaspInputGenerator = field( + default_factory=lambda: StaticSetGenerator( + user_kpoints_settings={"reciprocal_density": 100}, + user_incar_settings={ + "IBRION": 2, + "ISIF": 3, + "ENCUT": 700, + "EDIFF": 1e-7, + "LAECHG": False, + "LREAL": False, + "ALGO": "Normal", + "NSW": 0, + "LCHARG": False, + }, + auto_ispin=True, + ) + ) + From 11d546b0daa26542df3530823a8c5b179e8b8720 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Mon, 23 Sep 2024 13:57:13 -0400 Subject: [PATCH 05/29] Modified some parts based on Janine's comments. Modified some parts based on Janine's comments. --- src/atomate2/common/flows/pheasy.py | 20 ++++----- src/atomate2/common/jobs/pheasy.py | 63 ++++++++++++----------------- 2 files changed, 36 insertions(+), 47 deletions(-) diff --git a/src/atomate2/common/flows/pheasy.py b/src/atomate2/common/flows/pheasy.py index 6f5a4a1e22..06fb033aea 100644 --- a/src/atomate2/common/flows/pheasy.py +++ b/src/atomate2/common/flows/pheasy.py @@ -1,4 +1,4 @@ -"""Flows for calculating phonons.""" +"""Flows for calculating phonons with the pheasy code (LASSO and random-displacement method).""" from __future__ import annotations @@ -33,16 +33,16 @@ @dataclass class BasePhononMaker(Maker, ABC): """ - Maker to calculate harmonic phonons with a DFT/force field code and Phonopy. + Maker to calculate harmonic phonons with a DFT/force field code and the code Pheasy. 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. Finally, phonon densities of states, phonon band structures - and thermodynamic properties are computed. + Subsequently, supercells with all atoms displaced by a small amount (generally 0.01 A) + are 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. 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 @@ -129,7 +129,7 @@ class BasePhononMaker(Maker, ABC): sym_reduce: bool = True symprec: float = 1e-4 displacement: float = 0.01 - min_length: float | None = 20.0 + min_length: float | None = 14.0 prefer_90_degrees: bool = True get_supercell_size_kwargs: dict = field(default_factory=dict) use_symmetrized_structure: str | None = None @@ -287,7 +287,7 @@ def make( jobs.append(compute_total_energy_job) total_dft_energy = compute_total_energy_job.output - # get a phonon object from phonopy + # get a phonon object from pheasy code displacements = generate_phonon_displacements( structure=structure, supercell_matrix=supercell_matrix, diff --git a/src/atomate2/common/jobs/pheasy.py b/src/atomate2/common/jobs/pheasy.py index 0d975a48f0..af59470155 100644 --- a/src/atomate2/common/jobs/pheasy.py +++ b/src/atomate2/common/jobs/pheasy.py @@ -117,7 +117,10 @@ def generate_phonon_displacements( code: str, ) -> list[Structure]: """ - Generate displaced structures with phonopy. + Generate displaced 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 when the displacement number is less than 2. + 2. random-displacement method when the displacement number is more than 2. Parameters ---------- @@ -175,9 +178,15 @@ def generate_phonon_displacements( is_symmetry=sym_reduce, ) + # 1. the ALM module is used to dertermine how many free parameters of second order + # force constants within the supercell. 2. Baed 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 correct result sometimes, you may need to displace more + # random configurations. At least use one or two more configurations basd on the suggested + # number of displacements. - # following is modified by jiongzhi Zheng - # I will use the ALM to determine how many displaced supercells we need to use for extract second order force constants here from alm import ALM supercell_z = phonon.supercell lattice = supercell_z.cell @@ -189,10 +198,6 @@ def generate_phonon_displacements( alm.suggest() n_fp = alm._get_number_of_irred_fc_elements(1) - """Determine how many displaced supercells we need to use for extract second order force constants here - if we want to calculate the lattice thermal conductivty here, I highly suggest you to use the finite diplacement method - to calculate the zero-K second order force constants which garantee you get the completely converged results""" - num = int(np.ceil(n_fp / (3.0 * natom))) @@ -203,28 +208,20 @@ def generate_phonon_displacements( num_d = int(np.ceil(num * 1.8)) else: pass - - - - #previous version - #if num_disp_t > 3: - # num_d = int(np.ceil(num_disp_t / 3.0)) - # if num_d < num: - # num_d = int(num + 1) - # else: - # pass - #else: - # num_d = int(num+1) - - print ("The number of free parameters of Second Order Force Constants is ", n_fp) - print () - print ("The Number of Equations Used to Obtain the 2ND FCs is ", 3 * natom * num) - print () - print ("Be Careful!!! Full Rank of Matrix can not always guarantee the correct result sometimes"\ - "\n if the total atoms in supercell is less than 100 and"\ - "\n lattice constants are less than 10 angstrom,"\ - "\n I highly suggest you to displace more random configurations"\ - "\n At least use one or two more configurations basd on the suggested number of displacements") + + 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." + ) displacement_f = 0.01 phonon.generate_displacements(distance=displacement_f) @@ -247,14 +244,6 @@ def generate_phonon_displacements( displacements.append(get_pmg_structure(phonon.supercell)) return displacements - - #phonon.generate_displacements(distance=displacement) - - #supercells = phonon.supercells_with_displacements - - #return [get_pmg_structure(cell) for cell in supercells] - - @job( output_schema=PhononBSDOSDoc, data=[PhononDos, PhononBandStructureSymmLine, Forceconstants], From 57ab59fab0259e437c299fc3c0c9744aa85ce596 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Mon, 23 Sep 2024 14:32:22 -0400 Subject: [PATCH 06/29] directly import the class methods from phonons --- src/atomate2/common/schemas/pheasy.py | 106 +++----------------------- 1 file changed, 9 insertions(+), 97 deletions(-) diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index 07c1ebca9c..ee0151788b 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -42,79 +42,15 @@ from atomate2.aims.utils.units import omegaToTHz -logger = logging.getLogger(__name__) - - -def get_factor(code: str) -> float: - """ - Get the frequency conversion factor to THz for each code. - - Parameters - ---------- - code: str - The code to get the conversion factor for - - Returns - ------- - float - The correct conversion factor - - Raises - ------ - ValueError - If code is not defined - """ - if code in ["forcefields", "vasp"]: - return VaspToTHz - if code == "aims": - return omegaToTHz # Based on CODATA 2002 - raise ValueError(f"Frequency conversion factor for code ({code}) not defined.") - - -class PhononComputationalSettings(BaseModel): - """Collection to store computational settings for the phonon computation.""" - - # could be optional and implemented at a later stage? - npoints_band: int = Field("number of points for band structure computation") - kpath_scheme: str = Field("indicates the kpath scheme") - kpoint_density_dos: int = Field( - "number of points for computation of free energies and densities of states", - ) - - -class ThermalDisplacementData(BaseModel): - """Collection to store information on the thermal displacement matrices.""" - - freq_min_thermal_displacements: float = Field( - "cutoff frequency in THz to avoid numerical issues in the " - "computation of the thermal displacement parameters" - ) - thermal_displacement_matrix_cif: Optional[list[list[Matrix3D]]] = Field( - None, description="field including thermal displacement matrices in CIF format" - ) - thermal_displacement_matrix: Optional[list[list[Matrix3D]]] = Field( - None, - description="field including thermal displacement matrices in Cartesian " - "coordinate system", - ) - temperatures_thermal_displacements: Optional[list[int]] = Field( - None, - description="temperatures at which the thermal displacement matrices" - "have been computed", - ) - +# import some modules directly from phonons +from atomate2.common.schemas.phonons import get_factor +from atomate2.common.schemas.phonons import ThermalDisplacementData +from atomate2.common.schemas.phonons import PhononComputationalSettings +from atomate2.common.schemas.phonons import PhononUUIDs +from atomate2.common.schemas.phonons import PhononJobDirs -class PhononUUIDs(BaseModel): - """Collection to save all uuids connected to the phonon run.""" +logger = logging.getLogger(__name__) - optimization_run_uuid: Optional[str] = Field( - None, description="optimization run uuid" - ) - displacements_uuids: Optional[list[str]] = Field( - None, description="The uuids of the displacement jobs." - ) - static_run_uuid: Optional[str] = Field(None, description="static run uuid") - born_run_uuid: Optional[str] = Field(None, description="born run uuid") class Forceconstants(MSONable): @@ -123,27 +59,6 @@ class Forceconstants(MSONable): def __init__(self, force_constants: list[list[Matrix3D]]) -> None: self.force_constants = force_constants - -class PhononJobDirs(BaseModel): - """Collection to save all job directories relevant for the phonon run.""" - - displacements_job_dirs: Optional[list[Optional[str]]] = Field( - None, description="The directories where the displacement jobs were run." - ) - static_run_job_dir: Optional[Optional[str]] = Field( - None, description="Directory where static run was performed." - ) - born_run_job_dir: Optional[str] = Field( - None, description="Directory where born run was performed." - ) - optimization_run_job_dir: Optional[str] = Field( - None, description="Directory where optimization run was performed." - ) - taskdoc_run_job_dir: Optional[str] = Field( - None, description="Directory where task doc was generated." - ) - - class PhononBSDOSDoc(StructureMetadata, extra="allow"): # type: ignore[call-arg] """Collection of all data produced by the phonon workflow.""" @@ -552,7 +467,7 @@ def from_forces_born( 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"),) + # 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) @@ -588,9 +503,6 @@ def from_forces_born( phonon.force_constants = force_constants phonon.symmetrize_force_constants() - - #phonon.force_constants = fcs - # with phonon.load("phonopy.yaml") the phonopy API can be used phonon.save("phonopy.yaml") @@ -622,7 +534,7 @@ def from_forces_born( 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( + # new_plotter.save_plot( # "phonon_band_structure.eps", # img_format=kwargs.get("img_format", "eps"), # units=kwargs.get("units", "THz"), From 647fe49a7669875749fb9f2092176c2abc63f3b4 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Mon, 23 Sep 2024 14:42:48 -0400 Subject: [PATCH 07/29] clean up the code --- src/atomate2/common/schemas/pheasy.py | 38 +++------------------------ 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index ee0151788b..0925bbb2f3 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -225,22 +225,15 @@ def from_forces_born( is_symmetry=sym_reduce, ) - # modified by jiongzhi zheng + # Start from here, we use the pheasy code to extract the force constants. supercell = phonon.get_supercell() write_vasp("POSCAR", cell) write_vasp("SPOSCAR", supercell) phonon.generate_displacements(distance=displacement) disps_j = phonon.displacements - f_disp_n1 = len(disps_j) - # jiongzhi zheng - # Print keys and their corresponding values - for key, value in displacement_data.items(): - print(f"{key}: {value}") - - if f_disp_n1 < 1: set_of_forces = [np.array(forces) for forces in displacement_data["forces"]] set_of_forces_a = np.array(set_of_forces) @@ -250,9 +243,8 @@ def from_forces_born( print(np.shape(set_of_disps_m)) n_shape = set_of_disps_m.shape[0] - # set_of_disps_d = {} - # set_of_disps_d['displacements'] = set_of_disps_m set_of_disps_d = {'displacements': set_of_disps_m, 'dtype': 'double', 'order': 'C'} + else: set_of_forces = [np.array(forces) for forces in displacement_data["forces"]] set_of_forces_a_o = np.array(set_of_forces) @@ -261,15 +253,9 @@ def from_forces_born( set_of_disps = [np.array(disps.cart_coords) for disps in displacement_data["displaced_structures"]] set_of_disps_m_o = np.round((set_of_disps - supercell.get_positions()), decimals=16).astype('double') set_of_disps_m = set_of_disps_m_o[:-1, :, :] - print(set_of_disps_m) - print(np.shape(set_of_disps_m)) n_shape = set_of_disps_m.shape[0] - # set_of_disps_d = {} - # set_of_disps_d['displacements'] = set_of_disps_m set_of_disps_d = {'displacements': set_of_disps_m, 'dtype': 'double', 'order': 'C'} - # phonon.set_displacement_dataset(set_of_disps_d) - # print (set_of_disps) import pickle with open("disp_matrix.pkl","wb") as file: @@ -327,9 +313,6 @@ def from_forces_born( else: num_har = n_shape - - #set_of_forces = [np.array(forces) for forces in displacement_data["forces"]] - if born is not None and epsilon_static is not None: if len(structure) == len(born): borns, epsilon = symmetrize_borns_and_epsilon( @@ -355,13 +338,7 @@ def from_forces_born( else: borns = None epsilon = None - - # Produces all force constants - #phonon.produce_force_constants(forces=set_of_forces) - #phonon.produce_force_constants(forces=set_of_forces, fc_calculator="alm") - #phonon.symmetrize_force_constants() - #fcs = phonon.force_constants - #write_FORCE_CONSTANTS(fcs) + prim = read('POSCAR') supercell = read('SPOSCAR') @@ -379,17 +356,12 @@ def from_forces_born( pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) else: pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) - #pheasy_cmd_4_1 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --hdf5 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) print ("Start running pheasy in andes8: Dartmouth College Clusters or NERSC Perlmutter") subprocess.call(pheasy_cmd_1, shell=True) subprocess.call(pheasy_cmd_2, shell=True) subprocess.call(pheasy_cmd_3, shell=True) subprocess.call(pheasy_cmd_4, shell=True) - #subprocess.call(pheasy_cmd_4_1, shell=True) - - # Produces all force constants - #phonon.produce_force_constants(forces=set_of_forces) force_constants = parse_FORCE_CONSTANTS(filename="FORCE_CONSTANTS") phonon.force_constants = force_constants @@ -491,7 +463,6 @@ def from_forces_born( pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --c2 10.0 --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) else: pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc --c2 10.0 -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) - #pheasy_cmd_4_1 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --hdf5 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) print ("Start running pheasy in discovery") subprocess.call(pheasy_cmd_11, shell=True) @@ -541,8 +512,7 @@ def from_forces_born( #) 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") From 94a1acc1f25e3bd33b60ac4f3e89520ffa7373fb Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Mon, 23 Sep 2024 15:09:54 -0400 Subject: [PATCH 08/29] remove some files. --- src/atomate2/common/schemas/pheasy.py | 4 +- src/atomate2/vasp/flows/pheasy.py | 2 +- src/atomate2/vasp/jobs/pheasy.py | 61 --------------------------- 3 files changed, 4 insertions(+), 63 deletions(-) delete mode 100644 src/atomate2/vasp/jobs/pheasy.py diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index 0925bbb2f3..6da0f2fb06 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -49,6 +49,7 @@ from atomate2.common.schemas.phonons import PhononUUIDs from atomate2.common.schemas.phonons import PhononJobDirs + logger = logging.getLogger(__name__) @@ -148,6 +149,7 @@ class PhononBSDOSDoc(StructureMetadata, extra="allow"): # type: ignore[call-arg uuids: Optional[PhononUUIDs] = Field("Field including all relevant uuids") + @classmethod def from_forces_born( cls, @@ -512,7 +514,7 @@ def from_forces_born( #) 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") diff --git a/src/atomate2/vasp/flows/pheasy.py b/src/atomate2/vasp/flows/pheasy.py index a2b5a90097..c4e63fa4b3 100644 --- a/src/atomate2/vasp/flows/pheasy.py +++ b/src/atomate2/vasp/flows/pheasy.py @@ -9,7 +9,7 @@ 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.pheasy import PhononDisplacementMaker +from atomate2.vasp.jobs.phonons import PhononDisplacementMaker from atomate2.vasp.sets.core import StaticSetGenerator if TYPE_CHECKING: diff --git a/src/atomate2/vasp/jobs/pheasy.py b/src/atomate2/vasp/jobs/pheasy.py deleted file mode 100644 index 20fb339c57..0000000000 --- a/src/atomate2/vasp/jobs/pheasy.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Define the PhononDisplacementMaker for VASP.""" - -from dataclasses import dataclass, field - -from atomate2.vasp.jobs.base import BaseVaspMaker -from atomate2.vasp.sets.base import VaspInputGenerator -from atomate2.vasp.sets.core import StaticSetGenerator - - -@dataclass -class PhononDisplacementMaker(BaseVaspMaker): - """ - Maker to perform a static calculation as a part of the finite displacement method. - - The input set is for a static run with tighter convergence parameters. - Both the k-point mesh density and convergence parameters - are stricter than a normal relaxation. - - Parameters - ---------- - name : str - The job name. - input_set_generator : .VaspInputGenerator - A generator used to make the input set. - write_input_set_kwargs : dict - Keyword arguments that will get passed to :obj:`.write_vasp_input_set`. - copy_vasp_kwargs : dict - Keyword arguments that will get passed to :obj:`.copy_vasp_outputs`. - run_vasp_kwargs : dict - Keyword arguments that will get passed to :obj:`.run_vasp`. - task_document_kwargs : dict - Keyword arguments that will get passed to :obj:`.TaskDoc.from_directory`. - stop_children_kwargs : dict - Keyword arguments that will get passed to :obj:`.should_stop_children`. - write_additional_data : dict - Additional data to write to the current directory. Given as a dict of - {filename: data}. Note that if using FireWorks, dictionary keys cannot contain - the "." character which is typically used to denote file extensions. To avoid - this, use the ":" character, which will automatically be converted to ".". E.g. - ``{"my_file:txt": "contents of the file"}``. - """ - - name: str = "phonon static" - input_set_generator: VaspInputGenerator = field( - default_factory=lambda: StaticSetGenerator( - user_kpoints_settings={"reciprocal_density": 100}, - user_incar_settings={ - "IBRION": 2, - "ISIF": 3, - "ENCUT": 700, - "EDIFF": 1e-7, - "LAECHG": False, - "LREAL": False, - "ALGO": "Normal", - "NSW": 0, - "LCHARG": False, - }, - auto_ispin=True, - ) - ) - From b92e34e167e03ad9f6a0db27d4cc411bfa7f0152 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Mon, 23 Sep 2024 16:13:00 -0400 Subject: [PATCH 09/29] minor change --- src/atomate2/common/schemas/pheasy.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index 6da0f2fb06..b5038380c3 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -52,8 +52,6 @@ logger = logging.getLogger(__name__) - - class Forceconstants(MSONable): """A force constants class.""" From 05650d08b334a08932f46fa1bda93c44f111b6ca Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Mon, 23 Sep 2024 16:55:15 -0400 Subject: [PATCH 10/29] allow the users to define the number of displacements for random-displacement method. --- src/atomate2/common/flows/pheasy.py | 17 ++++++++++++----- src/atomate2/common/jobs/pheasy.py | 20 +++++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/atomate2/common/flows/pheasy.py b/src/atomate2/common/flows/pheasy.py index 06fb033aea..c27b85a982 100644 --- a/src/atomate2/common/flows/pheasy.py +++ b/src/atomate2/common/flows/pheasy.py @@ -33,13 +33,14 @@ @dataclass class BasePhononMaker(Maker, ABC): """ - Maker to calculate harmonic phonons with a DFT/force field code and the code Pheasy. + Maker to calculate harmonic phonons with a DFT/force field code and the LASSO-based machine + learning code Pheasy. 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 all atoms displaced by a small amount (generally 0.01 A) + Subsequently, supercells with all atoms displaced by a small amount (generally using 0.01 A) are are generated and accurate forces are computed for these structures. - With the help of phonopy, these forces are then converted into a dynamical matrix. + With the help of pheasy (LASSO), 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. Finally, phonon densities of states, phonon band structures and thermodynamic properties are computed. @@ -47,7 +48,7 @@ class BasePhononMaker(Maker, ABC): .. 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. + 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. @@ -65,6 +66,8 @@ class BasePhononMaker(Maker, ABC): and to handle all symmetry-related tasks in phonopy displacement: float displacement distance for phonons + num_displaced_supercells: int + number of displacements to be generated using a random displacement method min_length: float min length of the supercell that will be built prefer_90_degrees: bool @@ -119,6 +122,8 @@ class BasePhononMaker(Maker, ABC): to the primitive cell and not pymatgen code: str determines the dft or force field code. + mp_id: str + The mp_id of the material in the Materials Project database. store_force_constants: bool if True, force constants will be stored socket: bool @@ -129,6 +134,7 @@ class BasePhononMaker(Maker, ABC): sym_reduce: bool = True symprec: float = 1e-4 displacement: float = 0.01 + num_displaced_supercells: int = 0 min_length: float | None = 14.0 prefer_90_degrees: bool = True get_supercell_size_kwargs: dict = field(default_factory=dict) @@ -146,7 +152,7 @@ class BasePhononMaker(Maker, ABC): kpath_scheme: str = "seekpath" code: str = None - # add mpid to the phonon output + # add mp_id to the phonon output mp_id: str = None store_force_constants: bool = True socket: bool = False @@ -292,6 +298,7 @@ def make( structure=structure, supercell_matrix=supercell_matrix, displacement=self.displacement, + num_displaced_supercells=self.num_displaced_supercells, sym_reduce=self.sym_reduce, symprec=self.symprec, use_symmetrized_structure=self.use_symmetrized_structure, diff --git a/src/atomate2/common/jobs/pheasy.py b/src/atomate2/common/jobs/pheasy.py index af59470155..79487821e7 100644 --- a/src/atomate2/common/jobs/pheasy.py +++ b/src/atomate2/common/jobs/pheasy.py @@ -109,6 +109,7 @@ def get_supercell_size( def generate_phonon_displacements( structure: Structure, supercell_matrix: np.array, + num_displaced_supercells: int, displacement: float, sym_reduce: bool, symprec: float, @@ -130,6 +131,8 @@ def generate_phonon_displacements( array to describe supercell matrix displacement: float displacement in Angstrom + num_displaced_supercells: int + number of displaced supercells sym_reduce: bool if True, symmetry will be used to generate displacements symprec: float @@ -200,9 +203,7 @@ def generate_phonon_displacements( num = int(np.ceil(n_fp / (3.0 * natom))) - - displacement_t = 0.01 - phonon.generate_displacements(displacement_t) + phonon.generate_displacements(distance=displacement) num_disp_t = len(phonon.displacements) if num_disp_t > 3: num_d = int(np.ceil(num * 1.8)) @@ -223,20 +224,25 @@ def generate_phonon_displacements( "At least use one or two more configurations based on the suggested number of displacements." ) - displacement_f = 0.01 - phonon.generate_displacements(distance=displacement_f) + phonon.generate_displacements(distance=displacement) disps = phonon.displacements finite_disp = False f_disp_n = len(disps) if f_disp_n > 3: - phonon.generate_displacements(distance=displacement, number_of_snapshots=num_d, random_seed=103) + 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: finite_disp = True supercells = phonon.supercells_with_displacements - displacements = [] for cell in supercells: displacements.append(get_pmg_structure(cell)) From 0bf1bf896039326b396174083c6e30bdb4a93248 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Mon, 23 Sep 2024 19:24:16 -0400 Subject: [PATCH 11/29] format_adjustment --- src/atomate2/common/flows/pheasy.py | 6 ++- src/atomate2/common/jobs/pheasy.py | 3 +- src/atomate2/common/schemas/pheasy.py | 71 ++++++++++++++++++++------- src/atomate2/vasp/flows/pheasy.py | 3 +- 4 files changed, 61 insertions(+), 22 deletions(-) diff --git a/src/atomate2/common/flows/pheasy.py b/src/atomate2/common/flows/pheasy.py index c27b85a982..7dfe750bd4 100644 --- a/src/atomate2/common/flows/pheasy.py +++ b/src/atomate2/common/flows/pheasy.py @@ -1,4 +1,6 @@ -"""Flows for calculating phonons with the pheasy code (LASSO and random-displacement method).""" +"""Flows for calculating phonons, higher-order force constants, +and phonon renormalization with the pheasy code (LASSO and +random-displacement method).""" from __future__ import annotations @@ -134,7 +136,9 @@ class BasePhononMaker(Maker, ABC): sym_reduce: bool = True symprec: float = 1e-4 displacement: float = 0.01 + displacement_anharmonic: float = 0.08 num_displaced_supercells: int = 0 + num_displaced_supercells_anharmonic: int = 0 min_length: float | None = 14.0 prefer_90_degrees: bool = True get_supercell_size_kwargs: dict = field(default_factory=dict) diff --git a/src/atomate2/common/jobs/pheasy.py b/src/atomate2/common/jobs/pheasy.py index 79487821e7..e960308169 100644 --- a/src/atomate2/common/jobs/pheasy.py +++ b/src/atomate2/common/jobs/pheasy.py @@ -18,7 +18,8 @@ CubicSupercellTransformation, ) -from atomate2.common.schemas.pheasy import Forceconstants, PhononBSDOSDoc, get_factor +from atomate2.common.schemas.pheasy import Forceconstants, PhononBSDOSDoc +from atomate2.common.schemas.phonons import get_factor if TYPE_CHECKING: from pathlib import Path diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index b5038380c3..d6e7a4fed9 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -342,10 +342,16 @@ def from_forces_born( prim = read('POSCAR') supercell = read('SPOSCAR') - - pheasy_cmd_1 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --symprec 1e-3 --nbody 2'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2])) - pheasy_cmd_2 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 -w 2'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2])) - pheasy_cmd_3 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --ndata "{3}" --disp_file'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + pheasy_cmd_1 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --symprec 1e-3 --nbody 2'.format(int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2])) + pheasy_cmd_2 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 -w 2'.format(int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2])) + pheasy_cmd_3 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --ndata "{3}" --disp_file'.format(int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2]), + int(num_har)) displacement_f = 0.01 phonon.generate_displacements(distance=displacement_f) @@ -353,11 +359,16 @@ def from_forces_born( num_judge = len(disps) if num_judge > 3: - pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2]), + int(num_har)) else: - pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) - - print ("Start running pheasy in andes8: Dartmouth College Clusters or NERSC Perlmutter") + pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2]), + int(num_har)) + logger.info("Start running pheasy in andes8: Dartmouth College Clusters or NERSC Perlmutter") subprocess.call(pheasy_cmd_1, shell=True) subprocess.call(pheasy_cmd_2, shell=True) subprocess.call(pheasy_cmd_3, shell=True) @@ -367,7 +378,6 @@ def from_forces_born( phonon.force_constants = force_constants phonon.symmetrize_force_constants() - # with phonopy.load("phonopy.yaml") the phonopy API can be used phonon.save("phonopy.yaml") @@ -417,12 +427,17 @@ def from_forces_born( # 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) + 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') @@ -433,11 +448,14 @@ def from_forces_born( 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) + 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(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"),) @@ -450,9 +468,16 @@ def from_forces_born( imaginary_modes = False if imaginary_modes_hiphive: - pheasy_cmd_11 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --c2 10.0 --symprec 1e-3 --nbody 2'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2])) - pheasy_cmd_12 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 --c2 10.0 -w 2'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2])) - pheasy_cmd_13 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --c2 10.0 --ndata "{3}" --disp_file'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + pheasy_cmd_11 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --c2 10.0 --symprec 1e-3 --nbody 2'.format(int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2])) + pheasy_cmd_12 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 --c2 10.0 -w 2'.format(int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2])) + pheasy_cmd_13 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --c2 10.0 --ndata "{3}" --disp_file'.format(int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2]), + int(num_har)) displacement_f = 0.01 phonon.generate_displacements(distance=displacement_f) @@ -460,9 +485,15 @@ def from_forces_born( num_judge = len(disps) if num_judge > 3: - pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --c2 10.0 --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --c2 10.0 --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2]), + int(num_har)) else: - pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc --c2 10.0 -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) + pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc --c2 10.0 -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2]), + int(num_har)) print ("Start running pheasy in discovery") subprocess.call(pheasy_cmd_11, shell=True) @@ -500,7 +531,9 @@ def from_forces_born( ) 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(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)) diff --git a/src/atomate2/vasp/flows/pheasy.py b/src/atomate2/vasp/flows/pheasy.py index c4e63fa4b3..692b11b1d2 100644 --- a/src/atomate2/vasp/flows/pheasy.py +++ b/src/atomate2/vasp/flows/pheasy.py @@ -117,7 +117,8 @@ class PhononMaker(BasePhononMaker): sym_reduce: bool = True symprec: float = SETTINGS.PHONON_SYMPREC displacement: float = 0.01 - min_length: float | None = 20.0 + num_displaced_supercells: int = 0 + min_length: float | None = 14.0 prefer_90_degrees: bool = True get_supercell_size_kwargs: dict = field(default_factory=dict) use_symmetrized_structure: str | None = None From e9cd8a629863cce922996ad5850a3106be891f7f Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Mon, 23 Sep 2024 19:36:54 -0400 Subject: [PATCH 12/29] format_adjustment --- src/atomate2/common/schemas/pheasy.py | 90 +++++++++++++++------------ 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index d6e7a4fed9..bbf86d99ce 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -49,7 +49,6 @@ from atomate2.common.schemas.phonons import PhononUUIDs from atomate2.common.schemas.phonons import PhononJobDirs - logger = logging.getLogger(__name__) class Forceconstants(MSONable): @@ -342,16 +341,19 @@ def from_forces_born( prim = read('POSCAR') supercell = read('SPOSCAR') - pheasy_cmd_1 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --symprec 1e-3 --nbody 2'.format(int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2])) - pheasy_cmd_2 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 -w 2'.format(int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2])) - pheasy_cmd_3 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --ndata "{3}" --disp_file'.format(int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - int(num_har)) + pheasy_cmd_1 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --symprec 1e-3 --nbody 2'.format( + int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2])) + pheasy_cmd_2 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 -w 2'.format( + int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2])) + pheasy_cmd_3 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --ndata "{3}" --disp_file'.format( + int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2]), + int(num_har)) displacement_f = 0.01 phonon.generate_displacements(distance=displacement_f) @@ -359,15 +361,18 @@ def from_forces_born( num_judge = len(disps) if num_judge > 3: - pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - int(num_har)) + pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format( + int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2]), + int(num_har)) else: - pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - int(num_har)) + pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format( + int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2]), + int(num_har)) + logger.info("Start running pheasy in andes8: Dartmouth College Clusters or NERSC Perlmutter") subprocess.call(pheasy_cmd_1, shell=True) subprocess.call(pheasy_cmd_2, shell=True) @@ -427,7 +432,7 @@ def from_forces_born( # 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) @@ -468,34 +473,39 @@ def from_forces_born( imaginary_modes = False if imaginary_modes_hiphive: - pheasy_cmd_11 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --c2 10.0 --symprec 1e-3 --nbody 2'.format(int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2])) - pheasy_cmd_12 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 --c2 10.0 -w 2'.format(int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2])) - pheasy_cmd_13 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --c2 10.0 --ndata "{3}" --disp_file'.format(int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - int(num_har)) - + pheasy_cmd_11 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --c2 10.0 --symprec 1e-3 --nbody 2'.format( + int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2])) + pheasy_cmd_12 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 --c2 10.0 -w 2'.format( + int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2])) + pheasy_cmd_13 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --c2 10.0 --ndata "{3}" --disp_file'.format( + int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2]), + int(num_har)) + displacement_f = 0.01 phonon.generate_displacements(distance=displacement_f) disps = phonon.displacements num_judge = len(disps) if num_judge > 3: - pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --c2 10.0 --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - int(num_har)) + pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --c2 10.0 --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format( + int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2]), + int(num_har)) else: - pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc --c2 10.0 -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format(int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - int(num_har)) + pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc --c2 10.0 -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format( + int(supercell_matrix[0][0]), + int(supercell_matrix[1][1]), + int(supercell_matrix[2][2]), + int(num_har)) - print ("Start running pheasy in discovery") + logger.info("Start running pheasy in cluster") subprocess.call(pheasy_cmd_11, shell=True) subprocess.call(pheasy_cmd_12, shell=True) subprocess.call(pheasy_cmd_13, shell=True) From 2bacc860b97aeea5607c669fd879fd3fd677f5f1 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Mon, 23 Sep 2024 23:38:13 -0400 Subject: [PATCH 13/29] small update --- src/atomate2/common/schemas/pheasy.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index bbf86d99ce..67e545b32e 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -42,13 +42,15 @@ from atomate2.aims.utils.units import omegaToTHz -# import some modules directly from phonons +# import some classmethod directly from phonons from atomate2.common.schemas.phonons import get_factor from atomate2.common.schemas.phonons import ThermalDisplacementData from atomate2.common.schemas.phonons import PhononComputationalSettings from atomate2.common.schemas.phonons import PhononUUIDs from atomate2.common.schemas.phonons import PhononJobDirs +import pickle + logger = logging.getLogger(__name__) class Forceconstants(MSONable): @@ -256,7 +258,7 @@ def from_forces_born( n_shape = set_of_disps_m.shape[0] set_of_disps_d = {'displacements': set_of_disps_m, 'dtype': 'double', 'order': 'C'} - import pickle + with open("disp_matrix.pkl","wb") as file: pickle.dump(set_of_disps_m,file) with open("force_matrix.pkl","wb") as file: From 45354975e2a282cd6e06f584aa016843c166ab3f Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Tue, 24 Sep 2024 00:32:07 -0400 Subject: [PATCH 14/29] clean up the code and add more comments --- src/atomate2/common/schemas/pheasy.py | 67 +++++++++++++++------------ 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index 67e545b32e..06c131029b 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -28,6 +28,7 @@ from pymatgen.symmetry.bandstructure import HighSymmKpath from pymatgen.symmetry.kpath import KPathSeek from typing_extensions import Self +import pickle # import lib by jiongzhi zheng from ase.io import read @@ -49,7 +50,7 @@ from atomate2.common.schemas.phonons import PhononUUIDs from atomate2.common.schemas.phonons import PhononJobDirs -import pickle + logger = logging.getLogger(__name__) @@ -231,40 +232,31 @@ def from_forces_born( write_vasp("POSCAR", cell) write_vasp("SPOSCAR", supercell) - phonon.generate_displacements(distance=displacement) - disps_j = phonon.displacements - f_disp_n1 = len(disps_j) - - if f_disp_n1 < 1: - set_of_forces = [np.array(forces) for forces in displacement_data["forces"]] - set_of_forces_a = np.array(set_of_forces) - set_of_disps = [np.array(disps.cart_coords) for disps in displacement_data["displaced_structures"]] - set_of_disps_m = np.array(set_of_disps) - print(set_of_disps_m) + # get the force-displacement dataset from previous calculations + set_of_forces = [np.array(forces) for forces in displacement_data["forces"]] + set_of_forces_a_o = np.array(set_of_forces) - print(np.shape(set_of_disps_m)) - n_shape = set_of_disps_m.shape[0] - set_of_disps_d = {'displacements': set_of_disps_m, 'dtype': 'double', 'order': 'C'} + # deduct the residual forces on the equilibrium structure + set_of_forces_a_t = set_of_forces_a_o - set_of_forces_a_o[-1, :, :] + set_of_forces_a = set_of_forces_a_t[:-1, :, :] + set_of_disps = [np.array(disps.cart_coords) for disps in displacement_data["displaced_structures"]] - else: - set_of_forces = [np.array(forces) for forces in displacement_data["forces"]] - set_of_forces_a_o = np.array(set_of_forces) - set_of_forces_a_t = set_of_forces_a_o - set_of_forces_a_o[-1, :, :] - set_of_forces_a = set_of_forces_a_t[:-1, :, :] - set_of_disps = [np.array(disps.cart_coords) for disps in displacement_data["displaced_structures"]] - set_of_disps_m_o = np.round((set_of_disps - supercell.get_positions()), decimals=16).astype('double') - set_of_disps_m = set_of_disps_m_o[:-1, :, :] + # get the displacement dataset + set_of_disps_m_o = np.round((set_of_disps - supercell.get_positions()), + decimals=16).astype('double') + set_of_disps_m = set_of_disps_m_o[:-1, :, :] - n_shape = set_of_disps_m.shape[0] - set_of_disps_d = {'displacements': set_of_disps_m, 'dtype': 'double', 'order': 'C'} + # get the number of displacements + n_shape = set_of_disps_m.shape[0] - + # save the displacement and force matrix in the current directory + # for the future use by pheasy code with open("disp_matrix.pkl","wb") as file: pickle.dump(set_of_disps_m,file) with open("force_matrix.pkl","wb") as file: pickle.dump(set_of_forces_a,file) - """"put a if else condition here to determine whether we need to calculate the higher order force constants """ + # TODO: extract the anharmonic force constants Calc_anharmonic_FCs = False if Calc_anharmonic_FCs: from alm import ALM @@ -314,6 +306,7 @@ def from_forces_born( else: num_har = n_shape + if born is not None and epsilon_static is not None: if len(structure) == len(born): borns, epsilon = symmetrize_borns_and_epsilon( @@ -343,33 +336,47 @@ def from_forces_born( prim = read('POSCAR') supercell = read('SPOSCAR') + # To generate the culsters and orbots for second order force constants pheasy_cmd_1 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --symprec 1e-3 --nbody 2'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2])) + + # Create the null space to further reduce the free parameters for + # specific force constants and make them physically correct. pheasy_cmd_2 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 -w 2'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2])) + + # Generate the sensing matrix for the input of machine leaning method.i.e., LASSO, pheasy_cmd_3 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --ndata "{3}" --disp_file'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) - displacement_f = 0.01 - phonon.generate_displacements(distance=displacement_f) + + # 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: - pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format( + # Generate the force constants using the LASSO method + pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 \ + -l LASSO --std --rasr BHH --ndata "{3}"'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), int(num_har)) else: - pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format( + # Generate the force constants using the least-squred method + pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 \ + --rasr BHH --ndata "{3}"'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), From eab1fe645bb2559f25ad4e30f165e8dd6bb4d6f5 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Tue, 24 Sep 2024 00:52:56 -0400 Subject: [PATCH 15/29] minor update --- src/atomate2/common/schemas/pheasy.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index 06c131029b..49fec91c6a 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -336,7 +336,7 @@ def from_forces_born( prim = read('POSCAR') supercell = read('SPOSCAR') - # To generate the culsters and orbots for second order force constants + # Create the clusters and orbitals for second order force constants pheasy_cmd_1 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --symprec 1e-3 --nbody 2'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), @@ -349,8 +349,10 @@ def from_forces_born( int(supercell_matrix[1][1]), int(supercell_matrix[2][2])) - # Generate the sensing matrix for the input of machine leaning method.i.e., LASSO, - pheasy_cmd_3 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --ndata "{3}" --disp_file'.format( + # Generate the Compressive Sensing matrix,i.e., displacment matrix + # for the input of machine leaning method.i.e., LASSO, + pheasy_cmd_3 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 \ + --ndata "{3}" --disp_file'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), @@ -366,7 +368,7 @@ def from_forces_born( num_judge = len(disps) if num_judge > 3: - # Generate the force constants using the LASSO method + # Calculate the force constants using the LASSO method pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 \ -l LASSO --std --rasr BHH --ndata "{3}"'.format( int(supercell_matrix[0][0]), @@ -374,7 +376,7 @@ def from_forces_born( int(supercell_matrix[2][2]), int(num_har)) else: - # Generate the force constants using the least-squred method + # Calculate the force constants using the least-squred method pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 \ --rasr BHH --ndata "{3}"'.format( int(supercell_matrix[0][0]), @@ -382,7 +384,8 @@ def from_forces_born( int(supercell_matrix[2][2]), int(num_har)) - logger.info("Start running pheasy in andes8: Dartmouth College Clusters or NERSC Perlmutter") + logger.info("Start running pheasy in cluster") + subprocess.call(pheasy_cmd_1, shell=True) subprocess.call(pheasy_cmd_2, shell=True) subprocess.call(pheasy_cmd_3, shell=True) From 36aa5b75c334c1c2e62e5d12f5b2ffda848ff40b Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Tue, 24 Sep 2024 12:17:59 -0400 Subject: [PATCH 16/29] allow the users to control the symmetry precision --- src/atomate2/common/schemas/pheasy.py | 52 ++++++++++++++++----------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index 49fec91c6a..9c73f8bc98 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -337,25 +337,28 @@ def from_forces_born( supercell = read('SPOSCAR') # Create the clusters and orbitals for second order force constants - pheasy_cmd_1 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --symprec 1e-3 --nbody 2'.format( + pheasy_cmd_1 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --symprec "{3}" --nbody 2'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), - int(supercell_matrix[2][2])) + int(supercell_matrix[2][2]), + float(symprec)) # Create the null space to further reduce the free parameters for # specific force constants and make them physically correct. - pheasy_cmd_2 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 -w 2'.format( + pheasy_cmd_2 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec "{3}" -w 2'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), - int(supercell_matrix[2][2])) + int(supercell_matrix[2][2]), + float(symprec)) # Generate the Compressive Sensing matrix,i.e., displacment matrix # for the input of machine leaning method.i.e., LASSO, - pheasy_cmd_3 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 \ - --ndata "{3}" --disp_file'.format( + pheasy_cmd_3 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec "{3}" \ + --ndata "{4}" --disp_file'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), + float(symprec), int(num_har)) @@ -369,23 +372,25 @@ def from_forces_born( if num_judge > 3: # Calculate the force constants using the LASSO method - pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 \ - -l LASSO --std --rasr BHH --ndata "{3}"'.format( + pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec "{3}" \ + -l LASSO --std --rasr BHH --ndata "{4}"'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), + float(symprec), int(num_har)) else: # Calculate the force constants using the least-squred method - pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec 1e-3 \ - --rasr BHH --ndata "{3}"'.format( + pheasy_cmd_4 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec "{3}" \ + --rasr BHH --ndata "{4}"'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), + int(supercell_matrix[2][2]), + float(symprec), int(num_har)) logger.info("Start running pheasy in cluster") - + subprocess.call(pheasy_cmd_1, shell=True) subprocess.call(pheasy_cmd_2, shell=True) subprocess.call(pheasy_cmd_3, shell=True) @@ -485,18 +490,21 @@ def from_forces_born( imaginary_modes = False if imaginary_modes_hiphive: - pheasy_cmd_11 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --c2 10.0 --symprec 1e-3 --nbody 2'.format( + pheasy_cmd_11 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --c2 10.0 --symprec "{3}" --nbody 2'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), - int(supercell_matrix[2][2])) - pheasy_cmd_12 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec 1e-3 --c2 10.0 -w 2'.format( + int(supercell_matrix[2][2]), + float(symprec)) + pheasy_cmd_12 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec "{3}" --c2 10.0 -w 2'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), - int(supercell_matrix[2][2])) - pheasy_cmd_13 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec 1e-3 --c2 10.0 --ndata "{3}" --disp_file'.format( + int(supercell_matrix[2][2]), + float(symprec)) + pheasy_cmd_13 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec "{3}" --c2 10.0 --ndata "{4}" --disp_file'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), + float(symprec), int(num_har)) displacement_f = 0.01 @@ -505,16 +513,18 @@ def from_forces_born( num_judge = len(disps) if num_judge > 3: - pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --c2 10.0 --full_ifc -w 2 --symprec 1e-3 -l LASSO --std --rasr BHH --ndata "{3}"'.format( + pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --c2 10.0 --full_ifc -w 2 --symprec "{3}" -l LASSO --std --rasr BHH --ndata "{4}"'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), + int(supercell_matrix[2][2]), + float(symprec), int(num_har)) else: - pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc --c2 10.0 -w 2 --symprec 1e-3 --rasr BHH --ndata "{3}"'.format( + pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc --c2 10.0 -w 2 --symprec "{3}" --rasr BHH --ndata "{4}"'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), + int(supercell_matrix[2][2]), + float(symprec), int(num_har)) logger.info("Start running pheasy in cluster") From c69df934183f317d246f64c994cadaaad48fdc6c Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Tue, 24 Sep 2024 12:26:04 -0400 Subject: [PATCH 17/29] Lower the symmetry precision to allow pheasy to find a correct space group for crystals. --- src/atomate2/common/flows/pheasy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/atomate2/common/flows/pheasy.py b/src/atomate2/common/flows/pheasy.py index 7dfe750bd4..a8c49facdd 100644 --- a/src/atomate2/common/flows/pheasy.py +++ b/src/atomate2/common/flows/pheasy.py @@ -134,7 +134,7 @@ class BasePhononMaker(Maker, ABC): name: str = "phonon" sym_reduce: bool = True - symprec: float = 1e-4 + symprec: float = 1e-3 displacement: float = 0.01 displacement_anharmonic: float = 0.08 num_displaced_supercells: int = 0 From 92ea25a2e4f8592ffd079f079fc6c468568e581d Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Tue, 24 Sep 2024 13:34:18 -0400 Subject: [PATCH 18/29] Minor update --- src/atomate2/common/schemas/pheasy.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index 9c73f8bc98..3a3fb6e3fe 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -479,7 +479,8 @@ def from_forces_born( 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"),) + # 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) @@ -500,7 +501,8 @@ def from_forces_born( int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), float(symprec)) - pheasy_cmd_13 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec "{3}" --c2 10.0 --ndata "{4}" --disp_file'.format( + pheasy_cmd_13 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec "{3}" --c2 10.0 --ndata "{4}" \ + --disp_file'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), @@ -513,14 +515,16 @@ def from_forces_born( num_judge = len(disps) if num_judge > 3: - pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --c2 10.0 --full_ifc -w 2 --symprec "{3}" -l LASSO --std --rasr BHH --ndata "{4}"'.format( + pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --c2 10.0 --full_ifc -w 2 --symprec "{3}" \ + -l LASSO --std --rasr BHH --ndata "{4}"'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), float(symprec), int(num_har)) else: - pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc --c2 10.0 -w 2 --symprec "{3}" --rasr BHH --ndata "{4}"'.format( + pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc --c2 10.0 -w 2 --symprec "{3}" \ + --rasr BHH --ndata "{4}"'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), From 5d6f122a0d099b08bef91a8f7e872e47f40e42f0 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Tue, 24 Sep 2024 13:37:15 -0400 Subject: [PATCH 19/29] minor adjustment --- src/atomate2/common/schemas/pheasy.py | 54 +-------------------------- 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index 3a3fb6e3fe..14a8d3d5d1 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -247,7 +247,7 @@ def from_forces_born( set_of_disps_m = set_of_disps_m_o[:-1, :, :] # get the number of displacements - n_shape = set_of_disps_m.shape[0] + num_har = set_of_disps_m.shape[0] # save the displacement and force matrix in the current directory # for the future use by pheasy code @@ -256,57 +256,7 @@ def from_forces_born( with open("force_matrix.pkl","wb") as file: pickle.dump(set_of_forces_a,file) - # TODO: extract the anharmonic force constants - Calc_anharmonic_FCs = False - if Calc_anharmonic_FCs: - from alm import ALM - supercell_z = phonon.supercell - lattice = supercell_z.cell - positions = supercell_z.scaled_positions - numbers = supercell_z.numbers - natom = len(numbers) - with ALM(lattice, positions, numbers) as alm: - alm.define(1) - alm.suggest() - n_fp = alm._get_number_of_irred_fc_elements(1) - - num = int(np.ceil(n_fp / (3.0 * natom))) - num_round = int(np.round((n_fp / (3.0 * natom)))) - - if num > num_round: - num_d = num - displacement_t = 0.01 - phonon.generate_displacements(displacement_t) - num_disp_t = len(phonon.displacements) - int_num = int(num_disp_t / num_d) - if int_num > 3: - num_d = int(np.ceil(int_num / 3.0)) - else: - num_d = int(np.ceil(int_num / 3.0) + 1) - else: - num_d = num - displacement_t = 0.01 - phonon.generate_displacements(displacement_t) - num_disp_t = len(phonon.displacements) - int_num = int(num_disp_t / num_d) - if int_num >= 3: - num_d = int(np.ceil(int_num / 3.0)) - else: - num_d = int(num + 1) - - displacement_f = 0.01 - phonon.generate_displacements(distance=displacement_f) - disps = phonon.displacements - - f_disp_n = int(len(disps)) - if f_disp_n > 2: - num_har = num_d - else: - num_har = f_disp_n - else: - num_har = n_shape - - + # 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( From e17a3160df44520bbf03e3950989ad62ea571286 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Tue, 24 Sep 2024 17:22:55 -0400 Subject: [PATCH 20/29] minor update for flow/pheasy.py --- src/atomate2/common/flows/pheasy.py | 63 +++++++++++++++++------------ 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/src/atomate2/common/flows/pheasy.py b/src/atomate2/common/flows/pheasy.py index a8c49facdd..944212a4bf 100644 --- a/src/atomate2/common/flows/pheasy.py +++ b/src/atomate2/common/flows/pheasy.py @@ -1,6 +1,5 @@ -"""Flows for calculating phonons, higher-order force constants, -and phonon renormalization with the pheasy code (LASSO and -random-displacement method).""" +"""Flows for calculating phonons, an-harmonic force constants, +and phonon energy renormalization with the pheasy.""" from __future__ import annotations @@ -38,43 +37,59 @@ class BasePhononMaker(Maker, ABC): Maker to calculate harmonic phonons with a DFT/force field code and the LASSO-based machine learning code Pheasy. - 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 all atoms displaced by a small amount (generally using 0.01 A) - are are generated and accurate forces are computed for these structures. - With the help of pheasy (LASSO), 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. Finally, phonon densities of states, phonon band - structures and thermodynamic properties are computed. + Calculate the zero-K harmonic phonons of a material. 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 are generated and accurate + forces are computed for these structures. With the help of pheasy (LASSO technique), 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. 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 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. + 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 flows produced by this maker. + 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 phonopy + and to handle all symmetry-related tasks in pheasy, we recommend to + use the value of 1e-3. displacement: float - displacement distance for phonons + 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. + displacement_anharmonic: float + displacement distance for anharmonic force constants(FCs) up to sixth-order + FCs, for most cases 0.08 A is a good choice, but it can be increased to 0.1 A. num_displaced_supercells: int - number of displacements to be generated using a random displacement method + 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. + num_displaced_supercells_anharmonic: 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. min_length: float - min length of the supercell that will be built + 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 + 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 @@ -155,8 +170,6 @@ class BasePhononMaker(Maker, ABC): generate_frequencies_eigenvectors_kwargs: dict = field(default_factory=dict) kpath_scheme: str = "seekpath" code: str = None - - # add mp_id to the phonon output mp_id: str = None store_force_constants: bool = True socket: bool = False From f46c2fd9bdd9c0ee0d6870f73af9290d3e2e27a6 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Wed, 25 Sep 2024 15:09:00 -0400 Subject: [PATCH 21/29] resuse some jobs from phonons --- src/atomate2/common/flows/pheasy.py | 13 ++++-- src/atomate2/common/jobs/pheasy.py | 70 ----------------------------- 2 files changed, 10 insertions(+), 73 deletions(-) diff --git a/src/atomate2/common/flows/pheasy.py b/src/atomate2/common/flows/pheasy.py index 944212a4bf..f30652309e 100644 --- a/src/atomate2/common/flows/pheasy.py +++ b/src/atomate2/common/flows/pheasy.py @@ -12,10 +12,14 @@ from atomate2.common.jobs.pheasy import ( generate_frequencies_eigenvectors, generate_phonon_displacements, + run_phonon_displacements, +) + +from atomate2.common.jobs.phonons import ( get_supercell_size, get_total_energy_per_cell, - run_phonon_displacements, ) + from atomate2.common.jobs.utils import structure_to_conventional, structure_to_primitive if TYPE_CHECKING: @@ -273,7 +277,10 @@ def make( optimization_run_uuid = bulk.output.uuid # if supercell_matrix is None, supercell size will be determined after relax - # maker to ensure that cell lengths are really larger than threshold + # maker to ensure that cell lengths are really larger than threshold. + # Note that If one wants to calculate the lattice thermal conductivity, + # the supercell dimensions should be forced to be diagonal, e.g., + # supercell_matrix = [[2, 0, 0], [0, 2, 0], [0, 0, 2]] if supercell_matrix is None: supercell_job = get_supercell_size( structure, @@ -310,7 +317,7 @@ def make( jobs.append(compute_total_energy_job) total_dft_energy = compute_total_energy_job.output - # get a phonon object from pheasy code + # get a phonon object from pheasy code using the random-displacement approach displacements = generate_phonon_displacements( structure=structure, supercell_matrix=supercell_matrix, diff --git a/src/atomate2/common/jobs/pheasy.py b/src/atomate2/common/jobs/pheasy.py index e960308169..76e985aea5 100644 --- a/src/atomate2/common/jobs/pheasy.py +++ b/src/atomate2/common/jobs/pheasy.py @@ -34,76 +34,6 @@ logger = logging.getLogger(__name__) -@job -def get_total_energy_per_cell( - total_dft_energy_per_formula_unit: float, structure: Structure -) -> float: - """ - Job that computes total DFT energy of the cell. - - Parameters - ---------- - total_dft_energy_per_formula_unit: float - Total DFT energy in eV per formula unit. - structure: Structure object - Corresponding structure object. - """ - formula_units = ( - structure.composition.num_atoms - / structure.composition.reduced_composition.num_atoms - ) - - return total_dft_energy_per_formula_unit * formula_units - - -@job -def get_supercell_size( - structure: Structure, min_length: float, prefer_90_degrees: bool, **kwargs -) -> list[list[float]]: - """ - Determine supercell size with given min_length. - - Parameters - ---------- - structure: Structure Object - Input structure that will be used to determine supercell - min_length: float - minimum length of cell in Angstrom - prefer_90_degrees: bool - if True, the algorithm will try to find a cell with 90 degree angles first - **kwargs: - Additional parameters that can be set. - """ - kwargs.setdefault("force_diagonal", False) - common_kwds = dict( - min_length=min_length, - min_atoms=kwargs.get("min_atoms"), - force_diagonal=kwargs["force_diagonal"], - ) - - if not prefer_90_degrees: - transformation = CubicSupercellTransformation( - **common_kwds, max_atoms=kwargs.get("max_atoms"), force_90_degrees=False - ) - transformation.apply_transformation(structure=structure) - else: - try: - transformation = CubicSupercellTransformation( - **common_kwds, - max_atoms=kwargs.get("max_atoms", 1200), - force_90_degrees=True, - angle_tolerance=kwargs.get("angle_tolerance", 1e-2), - ) - transformation.apply_transformation(structure=structure) - - except AttributeError: - transformation = CubicSupercellTransformation( - **common_kwds, max_atoms=kwargs.get("max_atoms"), force_90_degrees=False - ) - transformation.apply_transformation(structure=structure) - - # matrix from pymatgen has to be transposed - return transformation.transformation_matrix.transpose().tolist() @job(data=[Structure]) From 34a197ea3d1b36ed6b5eedbe648c4b3837e541f2 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Wed, 25 Sep 2024 15:43:12 -0400 Subject: [PATCH 22/29] clean up the job(generate_phonon_displacements) --- src/atomate2/common/jobs/pheasy.py | 66 ++++++++++++++++-------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/atomate2/common/jobs/pheasy.py b/src/atomate2/common/jobs/pheasy.py index 76e985aea5..3f14d1a3e1 100644 --- a/src/atomate2/common/jobs/pheasy.py +++ b/src/atomate2/common/jobs/pheasy.py @@ -34,8 +34,6 @@ logger = logging.getLogger(__name__) - - @job(data=[Structure]) def generate_phonon_displacements( structure: Structure, @@ -49,10 +47,11 @@ def generate_phonon_displacements( code: str, ) -> list[Structure]: """ - Generate displaced structures with phonopy based on two ways: + 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 when the displacement number is less than 2. - 2. random-displacement method when the displacement number is more than 2. + 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 ---------- @@ -61,9 +60,9 @@ def generate_phonon_displacements( supercell_matrix: np.array array to describe supercell matrix displacement: float - displacement in Angstrom + displacement in Angstrom (default: 0.01) num_displaced_supercells: int - number of displaced supercells + number of displaced supercells defined by users sym_reduce: bool if True, symmetry will be used to generate displacements symprec: float @@ -102,7 +101,8 @@ def generate_phonon_displacements( "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, @@ -112,31 +112,38 @@ def generate_phonon_displacements( is_symmetry=sym_reduce, ) - # 1. the ALM module is used to dertermine how many free parameters of second order - # force constants within the supercell. 2. Baed 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 correct result sometimes, you may need to displace more - # random configurations. At least use one or two more configurations basd on the suggested - # number of displacements. + # 1. the ALM module is used to dertermine how many free parameters (irreducible force + # constants) of second order force constants (FCs) within the supercell. 2. Baed 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 basd on the suggested number of displacements. from alm import ALM - supercell_z = phonon.supercell - lattice = supercell_z.cell - positions = supercell_z.scaled_positions - numbers = supercell_z.numbers + 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) + # 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_t = len(phonon.displacements) - if num_disp_t > 3: + num_disp_f = len(phonon.displacements) + if num_disp_f > 3: num_d = int(np.ceil(num * 1.8)) else: pass @@ -154,14 +161,9 @@ def generate_phonon_displacements( "I highly suggest displacing more random configurations.\n" "At least use one or two more configurations based on the suggested number of displacements." ) - - phonon.generate_displacements(distance=displacement) - - disps = phonon.displacements + logger.info("") - finite_disp = False - f_disp_n = len(disps) - if f_disp_n > 3: + if num_disp_f > 3: if num_displaced_supercells != 0: phonon.generate_displacements(distance=displacement, number_of_snapshots=num_displaced_supercells, @@ -171,13 +173,15 @@ def generate_phonon_displacements( number_of_snapshots=num_d, random_seed=103) else: - finite_disp = True - + pass + supercells = phonon.supercells_with_displacements displacements = [] for cell in supercells: displacements.append(get_pmg_structure(cell)) + # add the equilibrium structure to the list for calculating + # the residual forces. displacements.append(get_pmg_structure(phonon.supercell)) return displacements From be89bced408bb2a354e20d998cac8da920d471d8 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Wed, 25 Sep 2024 15:46:27 -0400 Subject: [PATCH 23/29] finished cleaning up the pheasy jobs module --- src/atomate2/common/jobs/pheasy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/atomate2/common/jobs/pheasy.py b/src/atomate2/common/jobs/pheasy.py index 3f14d1a3e1..21cdc51238 100644 --- a/src/atomate2/common/jobs/pheasy.py +++ b/src/atomate2/common/jobs/pheasy.py @@ -256,7 +256,9 @@ def generate_frequencies_eigenvectors( **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], From 1316b1d963b4e2ee6f443101516c3c1f2d3c92a0 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Wed, 25 Sep 2024 16:52:24 -0400 Subject: [PATCH 24/29] finished cleaning up the shemas/pheasy --- src/atomate2/common/schemas/pheasy.py | 64 ++++++++++++++++++--------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index 14a8d3d5d1..283225789d 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -28,7 +28,7 @@ from pymatgen.symmetry.bandstructure import HighSymmKpath from pymatgen.symmetry.kpath import KPathSeek from typing_extensions import Self -import pickle + # import lib by jiongzhi zheng from ase.io import read @@ -39,7 +39,7 @@ from phonopy.file_IO import write_FORCE_CONSTANTS, parse_FORCE_CONSTANTS from phonopy.interface.vasp import write_vasp from phonopy.interface.vasp import read_vasp - +import pickle from atomate2.aims.utils.units import omegaToTHz @@ -217,7 +217,9 @@ def from_forces_born( "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, @@ -227,34 +229,36 @@ def from_forces_born( is_symmetry=sym_reduce, ) - # Start from here, we use the pheasy code to extract the force constants. + # Write the POSCAR and SPOSCAR files for the input of pheasy code supercell = phonon.get_supercell() write_vasp("POSCAR", cell) write_vasp("SPOSCAR", supercell) # get the force-displacement dataset from previous calculations - set_of_forces = [np.array(forces) for forces in displacement_data["forces"]] - set_of_forces_a_o = np.array(set_of_forces) + dataset_forces = [np.array(forces) for forces in displacement_data["forces"]] + dataset_forces_array = np.array(dataset_forces) + + # 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, :, :] - # deduct the residual forces on the equilibrium structure - set_of_forces_a_t = set_of_forces_a_o - set_of_forces_a_o[-1, :, :] - set_of_forces_a = set_of_forces_a_t[:-1, :, :] - set_of_disps = [np.array(disps.cart_coords) for disps in displacement_data["displaced_structures"]] + # 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 - set_of_disps_m_o = np.round((set_of_disps - supercell.get_positions()), + dataset_disps_array_rr = np.round((dataset_disps - supercell.get_positions()), decimals=16).astype('double') - set_of_disps_m = set_of_disps_m_o[:-1, :, :] + dataset_disps_array_use = dataset_disps_array_rr[:-1, :, :] - # get the number of displacements - num_har = set_of_disps_m.shape[0] + # get the number of displacements for harmonic phonon calculation + num_har = dataset_disps_array_use.shape[0] # save the displacement and force matrix in the current directory # for the future use by pheasy code with open("disp_matrix.pkl","wb") as file: - pickle.dump(set_of_disps_m,file) + pickle.dump(dataset_disps_array_use,file) with open("force_matrix.pkl","wb") as file: - pickle.dump(set_of_forces_a,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: @@ -287,6 +291,9 @@ def from_forces_born( 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 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --symprec "{3}" --nbody 2'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), @@ -321,7 +328,9 @@ def from_forces_born( num_judge = len(disps) if num_judge > 3: - # Calculate the force constants using the LASSO method + # 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 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec "{3}" \ -l LASSO --std --rasr BHH --ndata "{4}"'.format( int(supercell_matrix[0][0]), @@ -346,8 +355,11 @@ def from_forces_born( subprocess.call(pheasy_cmd_3, shell=True) subprocess.call(pheasy_cmd_4, shell=True) + # Read the force constants from the output file of pheasy code force_constants = parse_FORCE_CONSTANTS(filename="FORCE_CONSTANTS") 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 @@ -390,7 +402,12 @@ def from_forces_born( tol=kwargs.get("tol_imaginary_modes", 1e-5) ) - # jiongzhi zheng + # If imaginary modes are present, we first use the hiphive code to enforce some symmetry constraints + # to eleminate the imaginary modes (gernerally 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 eleminate 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 @@ -439,18 +456,22 @@ def from_forces_born( else: imaginary_modes_hiphive = False imaginary_modes = False - + + # Using a shorter cutoff (10 A) to generate the force constants to eleminate the imaginary modes near Gamma point + # in phesay code if imaginary_modes_hiphive: pheasy_cmd_11 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --c2 10.0 --symprec "{3}" --nbody 2'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), float(symprec)) + pheasy_cmd_12 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec "{3}" --c2 10.0 -w 2'.format( int(supercell_matrix[0][0]), int(supercell_matrix[1][1]), int(supercell_matrix[2][2]), float(symprec)) + pheasy_cmd_13 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec "{3}" --c2 10.0 --ndata "{4}" \ --disp_file'.format( int(supercell_matrix[0][0]), @@ -459,8 +480,7 @@ def from_forces_born( float(symprec), int(num_har)) - displacement_f = 0.01 - phonon.generate_displacements(distance=displacement_f) + phonon.generate_displacements(distance=displacement) disps = phonon.displacements num_judge = len(disps) @@ -482,6 +502,7 @@ def from_forces_born( int(num_har)) logger.info("Start running pheasy in cluster") + subprocess.call(pheasy_cmd_11, shell=True) subprocess.call(pheasy_cmd_12, shell=True) subprocess.call(pheasy_cmd_13, shell=True) @@ -711,3 +732,4 @@ def get_kpath( for lbl_idx, label in enumerate(label_set): path[set_idx][lbl_idx] = kpath["kpoints"][label] return kpath["kpoints"], path + \ No newline at end of file From 833205a4485f2359c6108d81b8f80580f70818ec Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Sat, 28 Sep 2024 14:23:25 -0400 Subject: [PATCH 25/29] small adjustment to pass the lint --- src/atomate2/common/flows/pheasy.py | 59 ++--- src/atomate2/common/jobs/pheasy.py | 76 ++++--- src/atomate2/common/schemas/pheasy.py | 307 +++++++++++++------------- src/atomate2/vasp/flows/pheasy.py | 3 +- 4 files changed, 234 insertions(+), 211 deletions(-) diff --git a/src/atomate2/common/flows/pheasy.py b/src/atomate2/common/flows/pheasy.py index f30652309e..3c24dca8b4 100644 --- a/src/atomate2/common/flows/pheasy.py +++ b/src/atomate2/common/flows/pheasy.py @@ -1,5 +1,6 @@ """Flows for calculating phonons, an-harmonic force constants, -and phonon energy renormalization with the pheasy.""" +and phonon energy renormalization with the pheasy. +""" from __future__ import annotations @@ -15,11 +16,7 @@ run_phonon_displacements, ) -from atomate2.common.jobs.phonons import ( - get_supercell_size, - get_total_energy_per_cell, -) - +from atomate2.common.jobs.phonons import get_supercell_size, get_total_energy_per_cell from atomate2.common.jobs.utils import structure_to_conventional, structure_to_primitive if TYPE_CHECKING: @@ -38,25 +35,28 @@ @dataclass class BasePhononMaker(Maker, ABC): """ - Maker to calculate harmonic phonons with a DFT/force field code and the LASSO-based machine - learning code Pheasy. - Calculate the zero-K harmonic phonons of a material. 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 are generated and accurate - forces are computed for these structures. With the help of pheasy (LASSO technique), 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. Finally, phonon - densities of states, phonon band structures and thermodynamic properties are computed. + Maker to calculate harmonic phonons with a DFT/force field code and the LASSO-based + machine learning code Pheasy. + + Calculate the zero-K harmonic phonons of a material. 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. With the help of + pheasy (LASSO technique), 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. 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 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. + 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 ---------- @@ -68,19 +68,19 @@ class BasePhononMaker(Maker, ABC): 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 + 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. displacement_anharmonic: float - displacement distance for anharmonic force constants(FCs) up to sixth-order + displacement distance for anharmonic force constants(FCs) up to sixth-order FCs, for most cases 0.08 A is a good choice, but it can be increased to 0.1 A. 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. + displacements is automatically determined by the number of atoms in the + supercell and its space group. num_displaced_supercells_anharmonic: 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 @@ -88,9 +88,9 @@ class BasePhononMaker(Maker, ABC): cutoff distance for anharmonic FCs its space group. generally, 50 large-distance displacements are enough for most cases. 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. + 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. @@ -149,6 +149,7 @@ class BasePhononMaker(Maker, ABC): if True, force constants will be stored socket: bool If True, use the socket for the calculation + """ name: str = "phonon" diff --git a/src/atomate2/common/jobs/pheasy.py b/src/atomate2/common/jobs/pheasy.py index 21cdc51238..150532498a 100644 --- a/src/atomate2/common/jobs/pheasy.py +++ b/src/atomate2/common/jobs/pheasy.py @@ -14,9 +14,6 @@ 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.pheasy import Forceconstants, PhononBSDOSDoc from atomate2.common.schemas.phonons import get_factor @@ -47,10 +44,11 @@ def generate_phonon_displacements( code: str, ) -> list[Structure]: """ - Generate small-distance perturbed structures with phonopy based on two ways: + + 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 + is less than 3. 2. random-displacement method (all-displaced atoms) when the displacement number is more than 3. Parameters @@ -73,7 +71,9 @@ def generate_phonon_displacements( 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.", @@ -112,16 +112,19 @@ def generate_phonon_displacements( is_symmetry=sym_reduce, ) - # 1. the ALM module is used to dertermine how many free parameters (irreducible force - # constants) of second order force constants (FCs) within the supercell. 2. Baed 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 basd on the suggested number of displacements. + # 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. from alm import ALM + supercell_ph = phonon.supercell lattice = supercell_ph.cell positions = supercell_ph.scaled_positions @@ -148,43 +151,54 @@ def generate_phonon_displacements( else: pass - logger.info("The number of free parameters of Second Order Force Constants is %s", n_fp) + 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( + "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" + "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." - ) + "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) + 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) + phonon.generate_displacements( + distance=displacement, + number_of_snapshots=num_d, + random_seed=103, + ) else: pass supercells = phonon.supercells_with_displacements - displacements = [] - for cell in supercells: - displacements.append(get_pmg_structure(cell)) + displacements = [get_pmg_structure(cell) for cell in supercells] - # add the equilibrium structure to the list for calculating + # 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], @@ -256,9 +270,10 @@ def generate_frequencies_eigenvectors( **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. +# to the output. @job(data=["forces", "displaced_structures"]) def run_phonon_displacements( displacements: list[Structure], @@ -319,7 +334,8 @@ def run_phonon_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 + # 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): diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index 283225789d..8528d055ce 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -2,18 +2,32 @@ import copy import logging +import pickle +import subprocess from pathlib import Path from typing import Optional, 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 +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 phonopy.units import VaspToTHz -from pydantic import BaseModel, Field +from pydantic import Field from pymatgen.core import Structure from pymatgen.io.phonopy import ( get_ph_bs_symm_line, @@ -29,37 +43,25 @@ from pymatgen.symmetry.kpath import KPathSeek from typing_extensions import Self - -# import lib by jiongzhi zheng -from ase.io import read -from hiphive import ClusterSpace, ForceConstantPotential, ForceConstants, enforce_rotational_sum_rules -from hiphive.cutoffs import estimate_maximum_cutoff -from hiphive.utilities import extract_parameters -import subprocess -from phonopy.file_IO import write_FORCE_CONSTANTS, parse_FORCE_CONSTANTS -from phonopy.interface.vasp import write_vasp -from phonopy.interface.vasp import read_vasp -import pickle - -from atomate2.aims.utils.units import omegaToTHz - # import some classmethod directly from phonons -from atomate2.common.schemas.phonons import get_factor -from atomate2.common.schemas.phonons import ThermalDisplacementData -from atomate2.common.schemas.phonons import PhononComputationalSettings -from atomate2.common.schemas.phonons import PhononUUIDs -from atomate2.common.schemas.phonons import PhononJobDirs - - +from atomate2.common.schemas.phonons import ( + get_factor, + ThermalDisplacementData, + PhononComputationalSettings, + PhononUUIDs, + PhononJobDirs, +) 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.""" @@ -149,7 +151,6 @@ class PhononBSDOSDoc(StructureMetadata, extra="allow"): # type: ignore[call-arg uuids: Optional[PhononUUIDs] = Field("Field including all relevant uuids") - @classmethod def from_forces_born( cls, @@ -217,9 +218,9 @@ def from_forces_born( "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. + + # 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, @@ -238,27 +239,33 @@ def from_forces_born( dataset_forces = [np.array(forces) for forces in displacement_data["forces"]] dataset_forces_array = np.array(dataset_forces) - # To deduct the residual forces on an equilibrium structure to eliminate the fitting error + # 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"]] + 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_rr = np.round( + (dataset_disps - supercell.get_positions()), + decimals=16 + ).astype('double') dataset_disps_array_use = dataset_disps_array_rr[:-1, :, :] # get the number of displacements for harmonic phonon calculation num_har = dataset_disps_array_use.shape[0] # save the displacement and force matrix in the current directory - # for the future use by pheasy code - 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) + # for the future use by pheasy code + 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: @@ -287,66 +294,60 @@ def from_forces_born( borns = None epsilon = None - prim = read('POSCAR') - supercell = read('SPOSCAR') + 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 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --symprec "{3}" --nbody 2'.format( - int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - float(symprec)) + # 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])}" "{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" -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 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec "{3}" -w 2'.format( - int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - float(symprec)) + pheasy_cmd_2 = ( + f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" -c --symprec "{float(symprec)}" -w 2' + ) - # Generate the Compressive Sensing matrix,i.e., displacment matrix + # Generate the Compressive Sensing matrix,i.e., displacement matrix # for the input of machine leaning method.i.e., LASSO, - pheasy_cmd_3 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec "{3}" \ - --ndata "{4}" --disp_file'.format( - int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - float(symprec), - int(num_har)) - - - # 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. + pheasy_cmd_3 = ( + f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" -w 2 -d --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 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec "{3}" \ - -l LASSO --std --rasr BHH --ndata "{4}"'.format( - int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - float(symprec), - int(num_har)) + # 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])}" "{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" -f --full_ifc -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 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc -w 2 --symprec "{3}" \ - --rasr BHH --ndata "{4}"'.format( - int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - float(symprec), - int(num_har)) + pheasy_cmd_4 = ( + f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" -f --full_ifc -w 2 --symprec "{float(symprec)}" ' + f'--rasr BHH --ndata "{int(num_har)}"' + ) logger.info("Start running pheasy in cluster") @@ -358,8 +359,8 @@ def from_forces_born( # Read the force constants from the output file of pheasy code force_constants = parse_FORCE_CONSTANTS(filename="FORCE_CONSTANTS") phonon.force_constants = force_constants - # symmetrize the force constants to make them physically correct based on the space group - # symmetry of the crystal structure. + # 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 @@ -402,11 +403,13 @@ def from_forces_born( 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 eleminate the imaginary modes (gernerally 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 eleminate 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 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 @@ -414,92 +417,93 @@ def from_forces_born( 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') + # 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 + # 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) + 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 + # 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') + 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.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) + 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( + 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"), + # 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) - ) + 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 eleminate the imaginary modes near Gamma point - # in phesay code + 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 = 'pheasy --dim "{0}" "{1}" "{2}" -s -w 2 --c2 10.0 --symprec "{3}" --nbody 2'.format( - int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - float(symprec)) - - pheasy_cmd_12 = 'pheasy --dim "{0}" "{1}" "{2}" -c --symprec "{3}" --c2 10.0 -w 2'.format( - int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - float(symprec)) - - pheasy_cmd_13 = 'pheasy --dim "{0}" "{1}" "{2}" -w 2 -d --symprec "{3}" --c2 10.0 --ndata "{4}" \ - --disp_file'.format( - int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - float(symprec), - int(num_har)) + pheasy_cmd_11 = ( + f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" -s -w 2 --c2 10.0 --symprec "{float(symprec)}" ' + f'--nbody 2' + ) + + pheasy_cmd_12 = ( + f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" -c --symprec "{float(symprec)}" --c2 10.0 -w 2' + ) + + pheasy_cmd_13 = ( + f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" -w 2 -d --symprec "{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 = 'pheasy --dim "{0}" "{1}" "{2}" -f --c2 10.0 --full_ifc -w 2 --symprec "{3}" \ - -l LASSO --std --rasr BHH --ndata "{4}"'.format( - int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - float(symprec), - int(num_har)) + pheasy_cmd_14 = ( + f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" -f --c2 10.0 --full_ifc -w 2 --symprec "{float(symprec)}" ' + f'-l LASSO --std --rasr BHH --ndata "{int(num_har)}"' + ) + else: - pheasy_cmd_14 = 'pheasy --dim "{0}" "{1}" "{2}" -f --full_ifc --c2 10.0 -w 2 --symprec "{3}" \ - --rasr BHH --ndata "{4}"'.format( - int(supercell_matrix[0][0]), - int(supercell_matrix[1][1]), - int(supercell_matrix[2][2]), - float(symprec), - int(num_har)) + pheasy_cmd_14 = ( + f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" -f --full_ifc --c2 10.0 -w 2 --symprec "{float(symprec)}" ' + f'--rasr BHH --ndata "{int(num_har)}"' + ) logger.info("Start running pheasy in cluster") @@ -538,18 +542,21 @@ def from_forces_born( ) 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( + 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)) + 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 diff --git a/src/atomate2/vasp/flows/pheasy.py b/src/atomate2/vasp/flows/pheasy.py index 692b11b1d2..97251f56c8 100644 --- a/src/atomate2/vasp/flows/pheasy.py +++ b/src/atomate2/vasp/flows/pheasy.py @@ -151,5 +151,4 @@ def prev_calc_dir_argname(self) -> str: 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" - + return "prev_dir" \ No newline at end of file From f741a8c53e60a5e320887fa5079af61b616b16e1 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Sat, 28 Sep 2024 18:08:39 -0400 Subject: [PATCH 26/29] raise a error if ALM is not installed. --- src/atomate2/common/jobs/pheasy.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/atomate2/common/jobs/pheasy.py b/src/atomate2/common/jobs/pheasy.py index 150532498a..d9bd298e43 100644 --- a/src/atomate2/common/jobs/pheasy.py +++ b/src/atomate2/common/jobs/pheasy.py @@ -123,7 +123,13 @@ def generate_phonon_displacements( # may need to displace more random configurations. At least use one or # two more configurations based on the suggested number of displacements. - from alm import ALM + try: + from alm import ALM + except ImportError as e: + logging.error( + f"Error importing ALM: {e}. Please ensure the 'alm'" + "library is installed." + ) supercell_ph = phonon.supercell lattice = supercell_ph.cell From 30de21a4daa1a82313cbb179e59261252bd9a454 Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Sat, 28 Sep 2024 18:12:32 -0400 Subject: [PATCH 27/29] minor update --- src/atomate2/common/flows/pheasy.py | 2 +- src/atomate2/common/schemas/pheasy.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/atomate2/common/flows/pheasy.py b/src/atomate2/common/flows/pheasy.py index 3c24dca8b4..c106efb0b0 100644 --- a/src/atomate2/common/flows/pheasy.py +++ b/src/atomate2/common/flows/pheasy.py @@ -1,5 +1,5 @@ """Flows for calculating phonons, an-harmonic force constants, -and phonon energy renormalization with the pheasy. + and phonon energy renormalization with the pheasy. """ from __future__ import annotations diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index 8528d055ce..c51e28c009 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -738,5 +738,4 @@ def get_kpath( 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 - \ No newline at end of file + return kpath["kpoints"], path \ No newline at end of file From 5e2660ae0e50cb82461de72cc639320ff51d496c Mon Sep 17 00:00:00 2001 From: Jiongzhi ZHENG Date: Tue, 1 Oct 2024 17:25:33 -0400 Subject: [PATCH 28/29] minor update to pass the lint check --- src/atomate2/common/flows/pheasy.py | 46 +++++++------- src/atomate2/common/jobs/pheasy.py | 31 +++++----- src/atomate2/common/schemas/pheasy.py | 86 ++++++++++++++++----------- src/atomate2/vasp/flows/pheasy.py | 3 +- 4 files changed, 90 insertions(+), 76 deletions(-) diff --git a/src/atomate2/common/flows/pheasy.py b/src/atomate2/common/flows/pheasy.py index c106efb0b0..94844ad1cd 100644 --- a/src/atomate2/common/flows/pheasy.py +++ b/src/atomate2/common/flows/pheasy.py @@ -1,5 +1,5 @@ -"""Flows for calculating phonons, an-harmonic force constants, - and phonon energy renormalization with the pheasy. +"""Flows for calculating phonons, an-harmonic force constants, +and phonon energy renormalization with the pheasy. """ from __future__ import annotations @@ -15,7 +15,6 @@ generate_phonon_displacements, run_phonon_displacements, ) - from atomate2.common.jobs.phonons import get_supercell_size, get_total_energy_per_cell from atomate2.common.jobs.utils import structure_to_conventional, structure_to_primitive @@ -40,23 +39,24 @@ class BasePhononMaker(Maker, ABC): machine learning code Pheasy. Calculate the zero-K harmonic phonons of a material. 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. With the help of - pheasy (LASSO technique), 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. Finally, phonon densities of states, phonon band - structures and thermodynamic properties are computed. + 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. + With the help of pheasy (LASSO technique), 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. 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 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. + 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 ---------- @@ -84,9 +84,9 @@ class BasePhononMaker(Maker, ABC): num_displaced_supercells_anharmonic: 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. + 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. 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 @@ -278,7 +278,7 @@ def make( optimization_run_uuid = bulk.output.uuid # if supercell_matrix is None, supercell size will be determined after relax - # maker to ensure that cell lengths are really larger than threshold. + # maker to ensure that cell lengths are really larger than threshold. # Note that If one wants to calculate the lattice thermal conductivity, # the supercell dimensions should be forced to be diagonal, e.g., # supercell_matrix = [[2, 0, 0], [0, 2, 0], [0, 0, 2]] @@ -304,7 +304,7 @@ def make( static_job_kwargs[self.prev_calc_dir_argname] = prev_dir static_job = self.static_energy_maker.make( structure=structure, **static_job_kwargs - ) + ) jobs.append(static_job) total_dft_energy = static_job.output.output.energy static_run_job_dir = static_job.output.dir_name diff --git a/src/atomate2/common/jobs/pheasy.py b/src/atomate2/common/jobs/pheasy.py index d9bd298e43..5c47d21105 100644 --- a/src/atomate2/common/jobs/pheasy.py +++ b/src/atomate2/common/jobs/pheasy.py @@ -73,7 +73,6 @@ def generate_phonon_displacements( 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.", @@ -112,7 +111,7 @@ def generate_phonon_displacements( is_symmetry=sym_reduce, ) - # 1. the ALM module is used to determine how many free parameters + # 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 @@ -120,13 +119,13 @@ def generate_phonon_displacements( # 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 + # 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 as e: - logging.error( + logging.exception( f"Error importing ALM: {e}. Please ensure the 'alm'" "library is installed." ) @@ -149,25 +148,23 @@ def generate_phonon_displacements( # 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. + # 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)) else: pass - + logger.info( - "The number of free parameters of Second Order Force Constants is %s", - n_fp + "The number of free parameters of Second Order Force Constants is %s", n_fp ) - logger.info("") + logger.info("") logger.info( - "The Number of Equations Used to Obtain the 2ND FCs is %s", - 3 * natom * num + "The Number of Equations Used to Obtain the 2ND FCs is %s", 3 * natom * num ) - logger.info("") + logger.info("") logger.warning( "Be Careful!!! Full Rank of Matrix cannot always guarantee the correct result\ @@ -183,19 +180,19 @@ def generate_phonon_displacements( if num_disp_f > 3: if num_displaced_supercells != 0: phonon.generate_displacements( - distance=displacement, - number_of_snapshots=num_displaced_supercells, + distance=displacement, + number_of_snapshots=num_displaced_supercells, random_seed=103, ) else: phonon.generate_displacements( - distance=displacement, - number_of_snapshots=num_d, + 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] diff --git a/src/atomate2/common/schemas/pheasy.py b/src/atomate2/common/schemas/pheasy.py index c51e28c009..ecc6f396f6 100644 --- a/src/atomate2/common/schemas/pheasy.py +++ b/src/atomate2/common/schemas/pheasy.py @@ -9,15 +9,16 @@ import numpy as np -# import lib by jiongzhi zheng +# 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) + ClusterSpace, + ForceConstantPotential, + ForceConstants, + enforce_rotational_sum_rules +) from hiphive.cutoffs import estimate_maximum_cutoff from hiphive.utilities import extract_parameters @@ -45,11 +46,11 @@ # import some classmethod directly from phonons from atomate2.common.schemas.phonons import ( - get_factor, - ThermalDisplacementData, PhononComputationalSettings, - PhononUUIDs, PhononJobDirs, + PhononUUIDs, + ThermalDisplacementData, + get_factor, ) logger = logging.getLogger(__name__) @@ -246,14 +247,13 @@ def from_forces_born( # force matrix on the displaced structures dataset_forces_array_disp = dataset_forces_array_rr[:-1, :, :] dataset_disps = [ - np.array(disps.cart_coords) + 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 + (dataset_disps - supercell.get_positions()), decimals=16 ).astype('double') dataset_disps_array_use = dataset_disps_array_rr[:-1, :, :] @@ -306,14 +306,14 @@ def from_forces_born( f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' f'"{int(supercell_matrix[2][2])}" -s -w 2 --symprec "{float(symprec)}" --nbody 2' ) - - # Create the null space to further reduce the free parameters for + + # 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])}" "{int(supercell_matrix[1][1])}" ' f'"{int(supercell_matrix[2][2])}" -c --symprec "{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 = ( @@ -333,7 +333,7 @@ def from_forces_born( 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 + # constraint, i.e., tag: --rasr BHH, is enforced during the # fitting process. pheasy_cmd_4 = ( f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' @@ -348,7 +348,7 @@ def from_forces_born( f'"{int(supercell_matrix[2][2])}" -f --full_ifc -w 2 --symprec "{float(symprec)}" ' f'--rasr BHH --ndata "{int(num_har)}"' ) - + logger.info("Start running pheasy in cluster") subprocess.call(pheasy_cmd_1, shell=True) @@ -427,7 +427,7 @@ def from_forces_born( # Enforce the rotational sum rules parameters_rot = enforce_rotational_sum_rules( - cs, parameters, ['Huang','Born-Huang'], alpha=1e-6 + cs, parameters, ["Huang","Born-Huang"], alpha=1e-6 ) # use the new parameters to make a fcp and then create the force @@ -441,7 +441,7 @@ def from_forces_born( phonon.symmetrize_force_constants() phonon.run_band_structure( - qpoints, path_connections=connections,with_eigenvectors=True + qpoints, path_connections=connections, with_eigenvectors=True ) phonon.write_yaml_band_structure(filename=filename_band_yaml) bs_symm_line = get_ph_bs_symm_line( @@ -471,42 +471,58 @@ def from_forces_born( # 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])}" "{int(supercell_matrix[1][1])}" ' - f'"{int(supercell_matrix[2][2])}" -s -w 2 --c2 10.0 --symprec "{float(symprec)}" ' + f'pheasy --dim "{int(supercell_matrix[0][0])}" ' + f'"{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" ' + f'-s -w 2 --c2 10.0 ' + f'--symprec "{float(symprec)}" ' f'--nbody 2' ) pheasy_cmd_12 = ( - f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' - f'"{int(supercell_matrix[2][2])}" -c --symprec "{float(symprec)}" --c2 10.0 -w 2' + f'pheasy --dim "{int(supercell_matrix[0][0])}" ' + f'"{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" ' + f'-c --symprec "{float(symprec)}" ' + f'--c2 10.0 -w 2' ) pheasy_cmd_13 = ( - f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' - f'"{int(supercell_matrix[2][2])}" -w 2 -d --symprec "{float(symprec)}" --c2 10.0 ' - f'--ndata "{int(num_har)}" --disp_file' + f'pheasy --dim "{int(supercell_matrix[0][0])}" ' + f'"{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" ' + f'-w 2 -d --symprec "{float(symprec)}" ' + f'--c2 10.0 --ndata "{int(num_har)}" ' + f'--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])}" "{int(supercell_matrix[1][1])}" ' - f'"{int(supercell_matrix[2][2])}" -f --c2 10.0 --full_ifc -w 2 --symprec "{float(symprec)}" ' - f'-l LASSO --std --rasr BHH --ndata "{int(num_har)}"' + f'pheasy --dim "{int(supercell_matrix[0][0])}" ' + f'"{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" ' + f'-f --c2 10.0 --full_ifc -w 2 ' + f'--symprec "{float(symprec)}" ' + f'-l LASSO --std --rasr BHH ' + f'--ndata "{int(num_har)}"' ) else: pheasy_cmd_14 = ( - f'pheasy --dim "{int(supercell_matrix[0][0])}" "{int(supercell_matrix[1][1])}" ' - f'"{int(supercell_matrix[2][2])}" -f --full_ifc --c2 10.0 -w 2 --symprec "{float(symprec)}" ' + f'pheasy --dim "{int(supercell_matrix[0][0])}" ' + f'"{int(supercell_matrix[1][1])}" ' + f'"{int(supercell_matrix[2][2])}" ' + f'-f --full_ifc --c2 10.0 -w 2 ' + f'--symprec "{float(symprec)}" ' f'--rasr BHH --ndata "{int(num_har)}"' ) logger.info("Start running pheasy in cluster") - + subprocess.call(pheasy_cmd_11, shell=True) subprocess.call(pheasy_cmd_12, shell=True) subprocess.call(pheasy_cmd_13, shell=True) @@ -543,8 +559,7 @@ def from_forces_born( new_plotter = PhononBSPlotter(bs=bs_symm_line) new_plotter.save_plot( - filename=kwargs.get("filename_bs", - "phonon_band_structure.pdf"), + filename=kwargs.get("filename_bs", "phonon_band_structure.pdf"), units=kwargs.get("units", "THz") ) @@ -738,4 +753,5 @@ def get_kpath( 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 \ No newline at end of file + return kpath["kpoints"], path + \ No newline at end of file diff --git a/src/atomate2/vasp/flows/pheasy.py b/src/atomate2/vasp/flows/pheasy.py index 97251f56c8..df777d50ea 100644 --- a/src/atomate2/vasp/flows/pheasy.py +++ b/src/atomate2/vasp/flows/pheasy.py @@ -151,4 +151,5 @@ def prev_calc_dir_argname(self) -> str: 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" \ No newline at end of file + return "prev_dir" + \ No newline at end of file From cabc93a801cbdf9a88db4dd4c6fc53428604b2a9 Mon Sep 17 00:00:00 2001 From: Hrushikesh Sahasrabuddhe Date: Mon, 14 Oct 2024 15:03:53 -0700 Subject: [PATCH 29/29] added support for MLFFs --- src/atomate2/forcefields/flows/pheasy.py | 145 +++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/atomate2/forcefields/flows/pheasy.py 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