class DeformStructureTransformation(AbstractTransformation): """ This transformation deforms a structure by a deformation gradient matrix Args: deformation (array): deformation gradient for the transformation """ def __init__(self, deformation): self.deformation = Deformation(deformation) def apply_transformation(self, structure): return self.deformation.apply_to_structure(structure) def __str__(self): return "DeformStructureTransformation : " + \ "Deformation = {}".format(str(self.deformation.tolist())) def __repr__(self): return self.__str__() @property def inverse(self): return DeformStructureTransformation(self.deformation.inv()) @property def is_one_to_many(self): return False def as_dict(self): return {"name": self.__class__.__name__, "version": __version__, "init_args": {"deformation": self.deformation.tolist()}, "@module": self.__class__.__module__, "@class": self.__class__.__name__}
class DeformStructureTransformation(AbstractTransformation): """ This transformation deforms a structure by a deformation gradient matrix Args: deformation (array): deformation gradient for the transformation """ def __init__(self, deformation=((1, 0, 0), (0, 1, 0), (0, 0, 1))): self._deform = Deformation(deformation) self.deformation = self._deform.tolist() def apply_transformation(self, structure): return self._deform.apply_to_structure(structure) def __str__(self): return "DeformStructureTransformation : " + \ "Deformation = {}".format(str(self.deformation)) def __repr__(self): return self.__str__() @property def inverse(self): return DeformStructureTransformation(self._deform.inv()) @property def is_one_to_many(self): return False
def __init__(self, deformation=((1, 0, 0), (0, 1, 0), (0, 0, 1))): """ Args: deformation (array): deformation gradient for the transformation """ self._deform = Deformation(deformation) self.deformation = self._deform.tolist()
class DeformationTest(PymatgenTest): def setUp(self): self.norm_defo = Deformation.from_index_amount((0, 0), 0.02) self.ind_defo = Deformation.from_index_amount((0, 1), 0.02) self.non_ind_defo = Deformation([[1.0, 0.02, 0.02], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) lattice = Lattice([[3.8401979337, 0.00, 0.00], [1.9200989668, 3.3257101909, 0.00], [0.00, -2.2171384943, 3.1355090603]]) self.structure = Structure(lattice, ["Si", "Si"], [[0, 0, 0], [0.75, 0.5, 0.75]]) def test_properties(self): # green_lagrange_strain self.assertArrayAlmostEqual( self.ind_defo.green_lagrange_strain, [[0., 0.01, 0.], [0.01, 0.0002, 0.], [0., 0., 0.]]) self.assertArrayAlmostEqual( self.non_ind_defo.green_lagrange_strain, [[0., 0.01, 0.01], [0.01, 0.0002, 0.0002], [0.01, 0.0002, 0.0002]]) def test_independence(self): self.assertFalse(self.non_ind_defo.is_independent()) self.assertEqual(self.ind_defo.get_perturbed_indices()[0], (0, 1)) def test_apply_to_structure(self): strained_norm = self.norm_defo.apply_to_structure(self.structure) strained_ind = self.ind_defo.apply_to_structure(self.structure) strained_non = self.non_ind_defo.apply_to_structure(self.structure) # Check lattices self.assertArrayAlmostEqual( strained_norm.lattice.matrix, [[3.9170018886, 0, 0], [1.958500946136, 3.32571019, 0], [0, -2.21713849, 3.13550906]]) self.assertArrayAlmostEqual( strained_ind.lattice.matrix, [[3.84019793, 0, 0], [1.9866132, 3.32571019, 0], [-0.04434277, -2.21713849, 3.13550906]]) self.assertArrayAlmostEqual( strained_non.lattice.matrix, [[3.84019793, 0, 0], [1.9866132, 3.3257102, 0], [0.0183674, -2.21713849, 3.13550906]]) # Check coordinates self.assertArrayAlmostEqual(strained_norm.sites[1].coords, [3.91700189, 1.224e-06, 2.3516318]) self.assertArrayAlmostEqual(strained_ind.sites[1].coords, [3.84019793, 1.224e-6, 2.3516318]) self.assertArrayAlmostEqual(strained_non.sites[1].coords, [3.8872306, 1.224e-6, 2.3516318]) # Check convention for applying transformation for vec, defo_vec in zip(self.structure.lattice.matrix, strained_non.lattice.matrix): new_vec = np.dot(self.non_ind_defo, np.transpose(vec)) self.assertArrayAlmostEqual(new_vec, defo_vec) for coord, defo_coord in zip(self.structure.cart_coords, strained_non.cart_coords): new_coord = np.dot(self.non_ind_defo, np.transpose(coord)) self.assertArrayAlmostEqual(new_coord, defo_coord)
def setUp(self): self.norm_defo = Deformation.from_index_amount((0, 0), 0.02) self.ind_defo = Deformation.from_index_amount((0, 1), 0.02) self.non_ind_defo = Deformation([[1.0, 0.02, 0.02], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) lattice = Lattice( [[3.8401979337, 0.00, 0.00], [1.9200989668, 3.3257101909, 0.00], [0.00, -2.2171384943, 3.1355090603]] ) self.structure = Structure(lattice, ["Si", "Si"], [[0, 0, 0], [0.75, 0.5, 0.75]])
def setUp(self): self.norm_defo = Deformation.from_index_amount((0, 0), 0.02) self.ind_defo = Deformation.from_index_amount((0, 1), 0.02) self.non_ind_defo = Deformation([[1.0, 0.02, 0.02], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) lattice = Lattice([[3.8401979337, 0.00, 0.00], [1.9200989668, 3.3257101909, 0.00], [0.00, -2.2171384943, 3.1355090603]]) self.structure = Structure(lattice, ["Si", "Si"], [[0, 0, 0], [0.75, 0.5, 0.75]])
class DeformationTest(PymatgenTest): def setUp(self): self.norm_defo = Deformation.from_index_amount((0, 0), 0.02) self.ind_defo = Deformation.from_index_amount((0, 1), 0.02) self.non_ind_defo = Deformation([[1.0, 0.02, 0.02], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) lattice = Lattice([[3.8401979337, 0.00, 0.00], [1.9200989668, 3.3257101909, 0.00], [0.00, -2.2171384943, 3.1355090603]]) self.structure = Structure(lattice, ["Si", "Si"], [[0, 0, 0], [0.75, 0.5, 0.75]]) def test_properties(self): # green_lagrange_strain self.assertArrayAlmostEqual(self.ind_defo.green_lagrange_strain, [[0., 0.01, 0.], [0.01, 0.0002, 0.], [0., 0., 0.]]) self.assertArrayAlmostEqual(self.non_ind_defo.green_lagrange_strain, [[0., 0.01, 0.01], [0.01, 0.0002, 0.0002], [0.01, 0.0002, 0.0002]]) def test_independence(self): self.assertFalse(self.non_ind_defo.is_independent()) self.assertEqual(self.ind_defo.get_perturbed_indices()[0], (0, 1)) def test_apply_to_structure(self): strained_norm = self.norm_defo.apply_to_structure(self.structure) strained_ind = self.ind_defo.apply_to_structure(self.structure) strained_non = self.non_ind_defo.apply_to_structure(self.structure) # Check lattices self.assertArrayAlmostEqual(strained_norm.lattice.matrix, [[3.9170018886, 0, 0], [1.958500946136, 3.32571019, 0], [0, -2.21713849, 3.13550906]]) self.assertArrayAlmostEqual(strained_ind.lattice.matrix, [[3.84019793, 0.07680396, 0], [1.92009897, 3.36411217, 0], [0, -2.21713849, 3.13550906]]) self.assertArrayAlmostEqual(strained_non.lattice.matrix, [[3.84019793, 0.07680396, 0.07680396], [1.92009897, 3.36411217, 0.0384019794], [0, -2.21713849, 3.13550906]]) # Check coordinates self.assertArrayAlmostEqual(strained_norm.sites[1].coords, [3.91700189, 1.224e-06, 2.3516318]) self.assertArrayAlmostEqual(strained_ind.sites[1].coords, [3.84019793, 0.07680518, 2.3516318]) self.assertArrayAlmostEqual(strained_non.sites[1].coords, [3.84019793, 0.07680518, 2.42843575])
def apply(self, structure, strength_multiplier=1.): deformation = Deformation( np.eye(3) + strength_multiplier * self.deformation_matrix) new_structure = deformation.apply_to_structure(structure) # move positions if self.pos_displacement_matrices is not None: for idx, mat in self.pos_displacement_matrices: new_structure.translate_sites( indices=[idx], # use original cartesian positions vector=strength_multiplier * np.dot(mat, structure.cart_coords[idx]), frac_coords=False) return new_structure
class DeformationTest(PymatgenTest): def setUp(self): self.norm_defo = Deformation.from_index_amount((0, 0), 0.02) self.ind_defo = Deformation.from_index_amount((0, 1), 0.02) self.non_ind_defo = Deformation([[1.0, 0.02, 0.02], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) lattice = Lattice([[3.8401979337, 0.00, 0.00], [1.9200989668, 3.3257101909, 0.00], [0.00, -2.2171384943, 3.1355090603]]) self.structure = Structure(lattice, ["Si", "Si"], [[0, 0, 0], [0.75, 0.5, 0.75]]) def test_properties(self): # green_lagrange_strain self.assertArrayAlmostEqual( self.ind_defo.green_lagrange_strain, [[0., 0.01, 0.], [0.01, 0.0002, 0.], [0., 0., 0.]]) self.assertArrayAlmostEqual( self.non_ind_defo.green_lagrange_strain, [[0., 0.01, 0.01], [0.01, 0.0002, 0.0002], [0.01, 0.0002, 0.0002]]) def test_independence(self): self.assertFalse(self.non_ind_defo.is_independent()) self.assertEqual(self.ind_defo.get_perturbed_indices()[0], (0, 1)) def test_apply_to_structure(self): strained_norm = self.norm_defo.apply_to_structure(self.structure) strained_ind = self.ind_defo.apply_to_structure(self.structure) strained_non = self.non_ind_defo.apply_to_structure(self.structure) # Check lattices self.assertArrayAlmostEqual( strained_norm.lattice.matrix, [[3.9170018886, 0, 0], [1.958500946136, 3.32571019, 0], [0, -2.21713849, 3.13550906]]) self.assertArrayAlmostEqual( strained_ind.lattice.matrix, [[3.84019793, 0.07680396, 0], [1.92009897, 3.36411217, 0], [0, -2.21713849, 3.13550906]]) self.assertArrayAlmostEqual(strained_non.lattice.matrix, [[3.84019793, 0.07680396, 0.07680396], [1.92009897, 3.36411217, 0.0384019794], [0, -2.21713849, 3.13550906]]) # Check coordinates self.assertArrayAlmostEqual(strained_norm.sites[1].coords, [3.91700189, 1.224e-06, 2.3516318]) self.assertArrayAlmostEqual(strained_ind.sites[1].coords, [3.84019793, 0.07680518, 2.3516318]) self.assertArrayAlmostEqual(strained_non.sites[1].coords, [3.84019793, 0.07680518, 2.42843575])
def test_energy_density(self): film_elac = ElasticTensor.from_voigt( [[324.32, 187.3, 170.92, 0., 0., 0.], [187.3, 324.32, 170.92, 0., 0., 0.], [170.92, 170.92, 408.41, 0., 0., 0.], [0., 0., 0., 150.73, 0., 0.], [0., 0., 0., 0., 150.73, 0.], [0., 0., 0., 0., 0., 238.74]]) dfm = Deformation([[-9.86004855e-01, 2.27539582e-01, -4.64426035e-17], [-2.47802121e-01, -9.91208483e-01, -7.58675185e-17], [-6.12323400e-17, -6.12323400e-17, 1.00000000e+00]]) self.assertAlmostEqual( film_elac.energy_density(dfm.green_lagrange_strain), 0.00125664672793) film_elac.energy_density( Strain.from_deformation([[0.99774738, 0.11520994, -0.], [-0.11520994, 0.99774738, 0.], [ -0., -0., 1., ]]))
def test_properties(self): # mean_stress self.assertEqual( self.rand_stress.mean_stress, 1. / 3. * (self.rand_stress[0, 0] + self.rand_stress[1, 1] + self.rand_stress[2, 2])) self.assertAlmostEqual(self.symm_stress.mean_stress, 3.66) # deviator_stress self.assertArrayAlmostEqual( self.symm_stress.deviator_stress, Stress([[-3.15, 2.29, 2.42], [2.29, 1.48, 5.07], [2.42, 5.07, 1.67]])) self.assertArrayAlmostEqual( self.non_symm.deviator_stress, [[-0.2666666667, 0.2, 0.3], [0.4, 0.133333333, 0.6], [0.2, 0.5, 0.133333333]]) # deviator_principal_invariants self.assertArrayAlmostEqual(self.symm_stress.dev_principal_invariants, [0, 44.2563, 111.953628]) # von_mises self.assertAlmostEqual(self.symm_stress.von_mises, 11.52253878275) # piola_kirchoff 1, 2 f = Deformation.from_index_amount((0, 1), 0.03) self.assertArrayAlmostEqual( self.symm_stress.piola_kirchoff_1(f), [[0.4413, 2.29, 2.42], [2.1358, 5.14, 5.07], [2.2679, 5.07, 5.33]]) self.assertArrayAlmostEqual( self.symm_stress.piola_kirchoff_2(f), [[0.377226, 2.1358, 2.2679], [2.1358, 5.14, 5.07], [2.2679, 5.07, 5.33]]) # voigt self.assertArrayEqual(self.symm_stress.voigt, [0.51, 5.14, 5.33, 5.07, 2.42, 2.29]) with self.assertRaises(ValueError): self.non_symm.voigt
def calculate_3D_elastic_energy(self, film, match, elasticity_tensor=None): """ Calculates the multi-plane elastic energy. Returns 999 if no elastic tensor was given on init """ if elasticity_tensor is None: return 9999 # Generate 3D lattice vectors for film super lattice film_matrix = list(match['film_sl_vecs']) film_matrix.append(np.cross(film_matrix[0], film_matrix[1])) # Generate 3D lattice vectors for substrate super lattice # Out of place substrate super lattice has to be same length as # Film out of plane vector to ensure no extra deformation in that # direction substrate_matrix = list(match['sub_sl_vecs']) temp_sub = np.cross(substrate_matrix[0], substrate_matrix[1]) temp_sub = temp_sub * fast_norm(film_matrix[2]) / fast_norm(temp_sub) substrate_matrix.append(temp_sub) transform_matrix = np.transpose( np.linalg.solve(film_matrix, substrate_matrix)) dfm = Deformation(transform_matrix) energy_density = elasticity_tensor.energy_density( dfm.green_lagrange_strain) return film.volume * energy_density / len(film.sites)
def test_properties(self): # mean_stress self.assertEqual(self.rand_stress.mean_stress, 1. / 3. * (self.rand_stress[0, 0] + self.rand_stress[1, 1] + self.rand_stress[2, 2])) self.assertAlmostEqual(self.symm_stress.mean_stress, 3.66) # deviator_stress self.assertArrayAlmostEqual(self.symm_stress.deviator_stress, Stress([[-3.15, 2.29, 2.42], [2.29, 1.48, 5.07], [2.42, 5.07, 1.67]])) self.assertArrayAlmostEqual(self.non_symm.deviator_stress, [[-0.2666666667, 0.2, 0.3], [0.4, 0.133333333, 0.6], [0.2, 0.5, 0.133333333]]) # deviator_principal_invariants self.assertArrayAlmostEqual(self.symm_stress.dev_principal_invariants, [0, 44.2563, 111.953628]) # von_mises self.assertAlmostEqual(self.symm_stress.von_mises, 11.52253878275) # piola_kirchoff 1, 2 f = Deformation.from_index_amount((0, 1), 0.03) self.assertArrayAlmostEqual(self.symm_stress.piola_kirchoff_1(f), [[0.4413, 2.29, 2.42], [2.1358, 5.14, 5.07], [2.2679, 5.07, 5.33]]) self.assertArrayAlmostEqual(self.symm_stress.piola_kirchoff_2(f), [[0.377226, 2.1358, 2.2679], [2.1358, 5.14, 5.07], [2.2679, 5.07, 5.33]]) # voigt self.assertArrayEqual(self.symm_stress.voigt, [0.51, 5.14, 5.33, 5.07, 2.42, 2.29])
def get_wf_bulk_modulus(structure, deformations, vasp_input_set=None, vasp_cmd="vasp", db_file=None, user_kpoints_settings=None, eos="vinet", tag=None, user_incar_settings=None): """ Returns the workflow that computes the bulk modulus by fitting to the given equation of state. Args: structure (Structure): input structure. deformations (list): list of deformation matrices(list of lists). vasp_input_set (VaspInputSet): for the static deformation calculations vasp_cmd (str): vasp command to run. db_file (str): path to the db file. user_kpoints_settings (dict): example: {"grid_density": 7000} eos (str): equation of state used for fitting the energies and the volumes. supported equation of states: "quadratic", "murnaghan", "birch", "birch_murnaghan", "pourier_tarantola", "vinet", "deltafactor". See pymatgen.analysis.eos.py tag (str): something unique to identify the tasks in this workflow. If None a random uuid will be assigned. user_incar_settings (dict): Returns: Workflow """ tag = tag or "bulk_modulus group: >>{}<<".format(str(uuid4())) deformations = [Deformation(defo_mat) for defo_mat in deformations] vis_static = vasp_input_set or MPStaticSet( structure=structure, force_gamma=True, lepsilon=False, user_kpoints_settings=user_kpoints_settings, user_incar_settings=user_incar_settings) wf_bulk_modulus = get_wf_deformations(structure, deformations, name="bulk_modulus deformation", vasp_input_set=vis_static, vasp_cmd=vasp_cmd, db_file=db_file, tag=tag) fw_analysis = Firework(FitEOSToDb(tag=tag, db_file=db_file, eos=eos), name="fit equation of state") wf_bulk_modulus.append_wf(Workflow.from_Firework(fw_analysis), wf_bulk_modulus.leaf_fw_ids) wf_bulk_modulus.name = "{}:{}".format( structure.composition.reduced_formula, "Bulk modulus") return wf_bulk_modulus
def get_strain_mapping(bulk_structure, deformation_calculations): strain_mapping = TensorMapping(tol=_mapping_tol) for i, calc in enumerate(deformation_calculations): deformed_structure = calc["bandstructure"].structure matrix = calculate_deformation(bulk_structure, deformed_structure) strain = Deformation(matrix).green_lagrange_strain strain_mapping[strain] = calc return strain_mapping
def get_wf_thermal_expansion(structure, deformations, vasp_input_set=None, vasp_cmd="vasp", db_file=None, user_kpoints_settings=None, t_step=10, t_min=0, t_max=1000, mesh=(20, 20, 20), eos="vinet", pressure=0.0, copy_vasp_outputs=False, tag=None): """ Returns quasi-harmonic thermal expansion workflow. Note: phonopy package is required for the final analysis step. Args: structure (Structure): input structure. deformations (list): list of deformation matrices(list of lists). vasp_input_set (VaspInputSet) vasp_cmd (str): vasp command to run. db_file (str): path to the db file. user_kpoints_settings (dict): example: {"grid_density": 7000} t_step (float): temperature step (in K) t_min (float): min temperature (in K) t_max (float): max temperature (in K) mesh (list/tuple): reciprocal space density eos (str): equation of state used for fitting the energies and the volumes. options supported by phonopy: "vinet", "murnaghan", "birch_murnaghan". Note: pymatgen supports more options than phonopy. see pymatgen.analysis.eos.py copy_vasp_outputs (bool): whether or not copy the outputs from the previous calc (usually structure optimization) before the deformations are performed. pressure (float): in GPa tag (str): something unique to identify the tasks in this workflow. If None a random uuid will be assigned. Returns: Workflow """ try: from phonopy import Phonopy except ImportError: logger.warning("'phonopy' package NOT installed. Required for the final analysis step.") tag = tag or "thermal_expansion group: >>{}<<".format(str(uuid4())) deformations = [Deformation(defo_mat) for defo_mat in deformations] vis_static = vasp_input_set or MPStaticSet(structure, force_gamma=True, lepsilon=True, user_kpoints_settings=user_kpoints_settings) wf_alpha = get_wf_deformations(structure, deformations, name="thermal_expansion deformation", vasp_cmd=vasp_cmd, db_file=db_file, tag=tag, copy_vasp_outputs=copy_vasp_outputs, vasp_input_set=vis_static) fw_analysis = Firework(ThermalExpansionCoeffToDb(tag=tag, db_file=db_file, t_step=t_step, t_min=t_min, t_max=t_max, mesh=mesh, eos=eos, pressure=pressure), name="Thermal expansion") wf_alpha.append_wf(Workflow.from_Firework(fw_analysis), wf_alpha.leaf_fw_ids) wf_alpha.name = "{}:{}".format(structure.composition.reduced_formula, "thermal expansion") return wf_alpha
class DeformStructureTransformation(AbstractTransformation): """ This transformation deforms a structure by a deformation gradient matrix """ def __init__(self, deformation=((1, 0, 0), (0, 1, 0), (0, 0, 1))): """ Args: deformation (array): deformation gradient for the transformation """ self._deform = Deformation(deformation) self.deformation = self._deform.tolist() def apply_transformation(self, structure): """ Apply the transformation. Args: structure (Structure): Input Structure Returns: Deformed Structure. """ return self._deform.apply_to_structure(structure) def __str__(self): return f"DeformStructureTransformation : Deformation = {self.deformation}" def __repr__(self): return self.__str__() @property def inverse(self): """ Returns: Inverse Transformation. """ return DeformStructureTransformation(self._deform.inv) @property def is_one_to_many(self): """ Returns: False """ return False
def calculate_3D_elastic_energy(self, film, match, elasticity_tensor=None, include_strain=False): """ Calculates the multi-plane elastic energy. Returns 999 if no elastic tensor was given on init Args: film(Structure): conventional standard structure for the film match(dictionary) : match dictionary from substrate analyzer elasticity_tensor(ElasticTensor): elasticity tensor for the film include_strain(bool): include strain in the output or not; changes return from just the energy to a tuple with the energy and strain in voigt notation """ if elasticity_tensor is None: return 9999 # Get the appropriate surface structure struc = SlabGenerator(self.film, match["film_miller"], 20, 15, primitive=False).get_slab().oriented_unit_cell # Generate 3D lattice vectors for film super lattice film_matrix = list(match["film_sl_vecs"]) film_matrix.append(np.cross(film_matrix[0], film_matrix[1])) # Generate 3D lattice vectors for substrate super lattice # Out of plane substrate super lattice has to be same length as # Film out of plane vector to ensure no extra deformation in that # direction substrate_matrix = list(match["sub_sl_vecs"]) temp_sub = np.cross(substrate_matrix[0], substrate_matrix[1]) temp_sub = temp_sub * fast_norm(film_matrix[2]) / fast_norm(temp_sub) substrate_matrix.append(temp_sub) transform_matrix = np.transpose( np.linalg.solve(film_matrix, substrate_matrix)) dfm = Deformation(transform_matrix) strain = dfm.green_lagrange_strain.convert_to_ieee(struc, initial_fit=False) energy_density = elasticity_tensor.energy_density(strain) if include_strain: return ( film.volume * energy_density / len(film.sites), strain.von_mises_strain, ) return film.volume * energy_density / len(film.sites)
def get_wf_bulk_modulus(structure, vasp_input_set=None, vasp_cmd="vasp", deformations=None, db_file=None, user_kpoints_settings=None, eos="vinet"): """ Returns the workflow that computes the bulk modulus by fitting to the given equation of state. Args: structure (Structure): input structure. vasp_input_set (VaspInputSet) vasp_cmd (str): vasp command to run. deformations (list): list of deformation matrices(list of lists). db_file (str): path to the db file. user_kpoints_settings (dict): example: {"grid_density": 7000} eos (str): equation of state used for fitting the energies and the volumes. supported equation of states: "quadratic", "murnaghan", "birch", "birch_murnaghan", "pourier_tarantola", "vinet", "deltafactor". See pymatgen.analysis.eos.py Returns: Workflow """ tag = datetime.utcnow().strftime('%Y-%m-%d-%H-%M-%S-%f') deformations = [Deformation(defo_mat) for defo_mat in deformations] wf_bulk_modulus = get_wf_deformations( structure, deformations, name="bulk_modulus deformation", vasp_input_set=vasp_input_set, lepsilon=False, vasp_cmd=vasp_cmd, db_file=db_file, user_kpoints_settings=user_kpoints_settings, tag=tag) fw_analysis = Firework(FitEquationOfStateTask(tag=tag, db_file=db_file, eos=eos), name="fit equation of state") append_fw_wf(wf_bulk_modulus, fw_analysis) wf_bulk_modulus.name = "{}:{}".format( structure.composition.reduced_formula, "Bulk modulus") return wf_bulk_modulus
def from_zsl( cls, match: ZSLMatch, film: Structure, film_miller, substrate_miller, elasticity_tensor=None, ground_state_energy=0, ): """Generate a substrate match from a ZSL match plus metadata""" # Get the appropriate surface structure struc = SlabGenerator(film, film_miller, 20, 15, primitive=False).get_slab().oriented_unit_cell dfm = Deformation(match.match_transformation) strain = dfm.green_lagrange_strain.convert_to_ieee(struc, initial_fit=False) von_mises_strain = strain.von_mises_strain if elasticity_tensor is not None: energy_density = elasticity_tensor.energy_density(strain) elastic_energy = film.volume * energy_density / len(film.sites) else: elastic_energy = 0 return cls( film_miller=film_miller, substrate_miller=substrate_miller, strain=strain, von_mises_strain=von_mises_strain, elastic_energy=elastic_energy, ground_state_energy=ground_state_energy, **{ k: getattr(match, k) for k in [ "film_sl_vectors", "substrate_sl_vectors", "film_vectors", "substrate_vectors", "film_transformation", "substrate_transformation", ] }, )
def get_wf_gibbs_free_energy(structure, deformations, vasp_input_set=None, vasp_cmd="vasp", db_file=None, user_kpoints_settings=None, t_step=10, t_min=0, t_max=1000, mesh=(20, 20, 20), eos="vinet", qha_type="debye_model", pressure=0.0, poisson=0.25, anharmonic_contribution=False, metadata=None, tag=None): """ Returns quasi-harmonic gibbs free energy workflow. Note: phonopy package is required for the final analysis step if qha_type="phonopy" Args: structure (Structure): input structure. deformations (list): list of deformation matrices(list of lists). vasp_input_set (VaspInputSet) vasp_cmd (str): vasp command to run. db_file (str): path to the db file. user_kpoints_settings (dict): example: {"grid_density": 7000} t_step (float): temperature step (in K) t_min (float): min temperature (in K) t_max (float): max temperature (in K) mesh (list/tuple): reciprocal space density eos (str): equation of state used for fitting the energies and the volumes. options supported by phonopy: "vinet", "murnaghan", "birch_murnaghan". Note: pymatgen supports more options than phonopy. see pymatgen.analysis.eos.py qha_type(str): quasi-harmonic approximation type: "debye_model" or "phonopy", default is "debye_model" pressure (float): in GPa poisson (float): poisson ratio anharmonic_contribution (bool): consider anharmonic contributions to Gibbs energy from the Debye model. Defaults to False. metadata (dict): meta data tag (str): something unique to identify the tasks in this workflow. If None a random uuid will be assigned. Returns: Workflow """ tag = tag or "gibbs group: >>{}<<".format(str(uuid4())) deformations = [Deformation(defo_mat) for defo_mat in deformations] # static input set for the transmuter fireworks vis_static = vasp_input_set if vis_static is None: lepsilon = False if qha_type not in ["debye_model"]: lepsilon = True try: from phonopy import Phonopy except ImportError: raise RuntimeError("'phonopy' package is NOT installed but is required for the final " "analysis step; you can alternatively switch to the qha_type to " "'debye_model' which does not require 'phonopy'.") vis_static = MPStaticSet(structure, force_gamma=True, lepsilon=lepsilon, user_kpoints_settings=user_kpoints_settings) wf_gibbs = get_wf_deformations(structure, deformations, name="gibbs deformation", vasp_cmd=vasp_cmd, db_file=db_file, tag=tag, metadata=metadata, vasp_input_set=vis_static) fw_analysis = Firework(GibbsAnalysisToDb(tag=tag, db_file=db_file, t_step=t_step, t_min=t_min, t_max=t_max, mesh=mesh, eos=eos, qha_type=qha_type, pressure=pressure, poisson=poisson, metadata=metadata, anharmonic_contribution=anharmonic_contribution,), name="Gibbs Free Energy") wf_gibbs.append_wf(Workflow.from_Firework(fw_analysis), wf_gibbs.leaf_fw_ids) wf_gibbs.name = "{}:{}".format(structure.composition.reduced_formula, "gibbs free energy") return wf_gibbs
def __init__(self, deformation=((1, 0, 0), (0, 1, 0), (0, 0, 1))): self._deform = Deformation(deformation) self.deformation = self._deform.tolist()
def get_interfaces( self, termination: Tuple[str, str], gap: float = 2.0, vacuum_over_film: float = 20.0, film_thickness: Union[float, int] = 1, substrate_thickness: Union[float, int] = 1, in_layers: bool = True, ) -> Iterator[Interface]: """ Generates interface structures given the film and substrate structure as well as the desired terminations Args: terminations: termination from self.termination list gap: gap between film and substrate vacuum_over_film: vacuum over the top of the film film_thickness: the film thickness substrate_thickness: substrate thickness in_layers: set the thickness in layer units """ film_sg = SlabGenerator( self.film_structure, self.film_miller, min_slab_size=film_thickness, min_vacuum_size=3, in_unit_planes=in_layers, center_slab=True, primitive=True, reorient_lattice= False, # This is necessary to not screw up the lattice ) sub_sg = SlabGenerator( self.substrate_structure, self.substrate_miller, min_slab_size=substrate_thickness, min_vacuum_size=3, in_unit_planes=in_layers, center_slab=True, primitive=True, reorient_lattice= False, # This is necessary to not screw up the lattice ) film_shift, sub_shift = self._terminations[termination] film_slab = film_sg.get_slab(shift=film_shift) sub_slab = sub_sg.get_slab(shift=sub_shift) for match in self.zsl_matches: # Build film superlattice super_film_transform = np.round( from_2d_to_3d( get_2d_transform(film_slab.lattice.matrix[:2], match.film_sl_vectors))).astype(int) film_sl_slab = film_slab.copy() film_sl_slab.make_supercell(super_film_transform) assert np.allclose( film_sl_slab.lattice.matrix[2], film_slab.lattice.matrix[2] ), "2D transformation affected C-axis for Film transformation" assert np.allclose( film_sl_slab.lattice.matrix[:2], match.film_sl_vectors ), "Transformation didn't make proper supercell for film" # Build substrate superlattice super_sub_transform = np.round( from_2d_to_3d( get_2d_transform(sub_slab.lattice.matrix[:2], match.substrate_sl_vectors))).astype(int) sub_sl_slab = sub_slab.copy() sub_sl_slab.make_supercell(super_sub_transform) assert np.allclose( sub_sl_slab.lattice.matrix[2], sub_slab.lattice.matrix[2] ), "2D transformation affected C-axis for Film transformation" assert np.allclose( sub_sl_slab.lattice.matrix[:2], match.substrate_sl_vectors ), "Transformation didn't make proper supercell for substrate" # Add extra info match_dict = match.as_dict() interface_properties = { k: match_dict[k] for k in match_dict.keys() if not k.startswith("@") } dfm = Deformation(match.match_transformation) strain = dfm.green_lagrange_strain interface_properties["strain"] = strain interface_properties["von_mises_strain"] = strain.von_mises_strain interface_properties["termination"] = termination interface_properties["film_thickness"] = film_thickness interface_properties["substrate_thickness"] = substrate_thickness yield (Interface.from_slabs( substrate_slab=sub_sl_slab, film_slab=film_sl_slab, gap=gap, vacuum_over_film=vacuum_over_film, interface_properties=interface_properties, ))
def get_wf_thermal_expansion(structure, vasp_input_set=None, vasp_cmd="vasp", deformations=None, db_file=None, user_kpoints_settings=None, t_step=10, t_min=0, t_max=1000, mesh=(20, 20, 20), eos="vinet", pressure=0.0): """ Returns quasi-harmonic thermal expansion workflow. Note: phonopy package is required for the final analysis step. Args: structure (Structure): input structure. vasp_input_set (VaspInputSet) vasp_cmd (str): vasp command to run. deformations (list): list of deformation matrices(list of lists). db_file (str): path to the db file. user_kpoints_settings (dict): example: {"grid_density": 7000} t_step (float): temperature step (in K) t_min (float): min temperature (in K) t_max (float): max temperature (in K) mesh (list/tuple): reciprocal space density eos (str): equation of state used for fitting the energies and the volumes. options supported by phonopy: "vinet", "murnaghan", "birch_murnaghan". Note: pymatgen supports more options than phonopy. see pymatgen.analysis.eos.py pressure (float): in GPa Returns: Workflow """ try: from phonopy import Phonopy except ImportError: logger.warn( "'phonopy' package NOT installed. Required for the final analysis step." ) tag = datetime.utcnow().strftime('%Y-%m-%d-%H-%M-%S-%f') deformations = [Deformation(defo_mat) for defo_mat in deformations] wf_alpha = get_wf_deformations(structure, deformations, name="thermal_expansion deformation", vasp_input_set=vasp_input_set, lepsilon=True, vasp_cmd=vasp_cmd, db_file=db_file, user_kpoints_settings=user_kpoints_settings, tag=tag) fw_analysis = Firework(ThermalExpansionCoeffTask(tag=tag, db_file=db_file, t_step=t_step, t_min=t_min, t_max=t_max, mesh=mesh, eos=eos, pressure=pressure), name="Thermal expansion") append_fw_wf(wf_alpha, fw_analysis) wf_alpha.name = "{}:{}".format(structure.composition.reduced_formula, "thermal expansion") return wf_alpha
def get_wf_elastic_constant(structure, metadata, strain_states=None, stencils=None, db_file=None, conventional=False, order=2, vasp_input_set=None, analysis=True, sym_reduce=False, tag='elastic', copy_vasp_outputs=False, **kwargs): """ Returns a workflow to calculate elastic constants. Firework 1 : write vasp input set for structural relaxation, run vasp, pass run location, database insertion. Firework 2 - number of total deformations: Static runs on the deformed structures last Firework : Analyze Stress/Strain data and fit the elastic tensor Args: structure (Structure): input structure to be optimized and run. strain_states (list of Voigt-notation strains): list of ratios of nonzero elements of Voigt-notation strain, e. g. [(1, 0, 0, 0, 0, 0), (0, 1, 0, 0, 0, 0), etc.]. stencils (list of floats, or list of list of floats): values of strain to multiply by for each strain state, i. e. stencil for the perturbation along the strain state direction, e. g. [-0.01, -0.005, 0.005, 0.01]. If a list of lists, stencils must correspond to each strain state provided. db_file (str): path to file containing the database credentials. conventional (bool): flag to convert input structure to conventional structure, defaults to False. order (int): order of the tensor expansion to be determined. Defaults to 2 and currently supports up to 3. vasp_input_set (VaspInputSet): vasp input set to be used. Defaults to static set with ionic relaxation parameters set. Take care if replacing this, default ensures that ionic relaxation is done and that stress is calculated for each vasp run. analysis (bool): flag to indicate whether analysis task should be added and stresses and strains passed to that task sym_reduce (bool): Whether or not to apply symmetry reductions tag (str): copy_vasp_outputs (bool): whether or not to copy previous vasp outputs. kwargs (keyword arguments): additional kwargs to be passed to get_wf_deformations Returns: Workflow """ # Convert to conventional if specified if conventional: structure = SpacegroupAnalyzer( structure).get_conventional_standard_structure() uis_elastic = { "IBRION": 2, "NSW": 99, "ISIF": 2, "ISTART": 1, "PREC": "High" } vis = vasp_input_set or MPStaticSet(structure, user_incar_settings=uis_elastic) strains = [] if strain_states is None: strain_states = get_default_strain_states(order) if stencils is None: stencils = [np.linspace(-0.01, 0.01, 5 + (order - 2) * 2)] * len(strain_states) if np.array(stencils).ndim == 1: stencils = [stencils] * len(strain_states) for state, stencil in zip(strain_states, stencils): strains.extend( [Strain.from_voigt(s * np.array(state)) for s in stencil]) # Remove zero strains strains = [strain for strain in strains if not (abs(strain) < 1e-10).all()] # Adding the zero strains for the purpose of calculating at finite pressure or thermal expansion _strains = [Strain.from_deformation([[1, 0, 0], [0, 1, 0], [0, 0, 1]])] strains.extend(_strains) """ """ vstrains = [strain.voigt for strain in strains] if np.linalg.matrix_rank(vstrains) < 6: # TODO: check for sufficiency of input for nth order raise ValueError( "Strain list is insufficient to fit an elastic tensor") deformations = [s.get_deformation_matrix() for s in strains] """ print(strains) print(deformations) """ if sym_reduce: # Note this casts deformations to a TensorMapping # with unique deformations as keys to symmops deformations = symmetry_reduce(deformations, structure) wf_elastic = get_wf_deformations(structure, deformations, tag=tag, db_file=db_file, vasp_input_set=vis, copy_vasp_outputs=copy_vasp_outputs, **kwargs) if analysis: defo_fws_and_tasks = get_fws_and_tasks( wf_elastic, fw_name_constraint="deformation", task_name_constraint="Transmuted") for idx_fw, idx_t in defo_fws_and_tasks: defo = \ wf_elastic.fws[idx_fw].tasks[idx_t]['transformation_params'][0][ 'deformation'] pass_dict = { 'strain': Deformation(defo).green_lagrange_strain.tolist(), 'stress': '>>output.ionic_steps.-1.stress', 'deformation_matrix': defo } if sym_reduce: pass_dict.update({'symmops': deformations[defo]}) mod_spec_key = "deformation_tasks->{}".format(idx_fw) pass_task = pass_vasp_result(pass_dict=pass_dict, mod_spec_key=mod_spec_key) wf_elastic.fws[idx_fw].tasks.append(pass_task) fw_analysis = Firework(ElasticTensorToDb(structure=structure, db_file=db_file, order=order, fw_spec_field='tags', metadata=metadata, vasp_input_set=vis), name="Analyze Elastic Data", spec={"_allow_fizzled_parents": True}) wf_elastic.append_wf(Workflow.from_Firework(fw_analysis), wf_elastic.leaf_fw_ids) wf_elastic.name = "{}:{}".format(structure.composition.reduced_formula, "elastic constants") return wf_elastic
def get_elastic_analysis(opt_task, defo_tasks): """ Performs the analysis of opt_tasks and defo_tasks necessary for an elastic analysis Args: opt_task: task doc corresponding to optimization defo_tasks: task_doc corresponding to deformations Returns: elastic document with fitted elastic tensor and analysis """ elastic_doc = {"warnings": []} opt_struct = Structure.from_dict(opt_task['output']['structure']) d_structs = [ Structure.from_dict(d['output']['structure']) for d in defo_tasks ] defos = [ calculate_deformation(opt_struct, def_structure) for def_structure in d_structs ] # Warning if deformation is not equivalent to stored deformation stored_defos = [d['transmuter']['transformation_params'][0]\ ['deformation'] for d in defo_tasks] if not np.allclose(defos, stored_defos, atol=1e-5): wmsg = "Inequivalent stored and calc. deformations." logger.warning(wmsg) elastic_doc["warnings"].append(wmsg) # Collect all fitting data and task ids defos = [Deformation(d) for d in defos] strains = [d.green_lagrange_strain for d in defos] vasp_stresses = [d['output']['stress'] for d in defo_tasks] cauchy_stresses = [-0.1 * Stress(s) for s in vasp_stresses] pk_stresses = [ Stress(s.piola_kirchoff_2(d)) for s, d in zip(cauchy_stresses, defos) ] defo_task_ids = [d['task_id'] for d in defo_tasks] # Determine whether data is sufficient to fit tensor # If raw data is insufficient but can be symmetrically transformed # to provide a sufficient set, use the expanded set with appropriate # symmetry transformations, fstresses/strains are "fitting # strains" below. vstrains = [s.voigt for s in strains] if np.linalg.matrix_rank(vstrains) < 6: symmops = SpacegroupAnalyzer(opt_struct).get_symmetry_operations() fstrains = [[s.transform(symmop) for symmop in symmops] for s in strains] fstrains = list(chain.from_iterable(fstrains)) vfstrains = [s.voigt for s in fstrains] if not np.linalg.matrix_rank(vfstrains) == 6: logger.warning("Insufficient data to form SOEC") elastic_doc['warnings'].append("insufficient strains") return None else: fstresses = [[s.transform(symmop) for symmop in symmops] for s in pk_stresses] fstresses = list(chain.from_iterable(fstresses)) else: fstrains = strains fstresses = pk_stresses with warnings.catch_warnings(): warnings.simplefilter('ignore') # TODO: more intelligently determine if independent # strain fitting can be done if len(cauchy_stresses) == 24: elastic_doc['legacy_fit'] = legacy_fit(strains, cauchy_stresses) et_raw = ElasticTensor.from_pseudoinverse(fstrains, fstresses) et = et_raw.voigt_symmetrized.convert_to_ieee(opt_struct) defo_tasks = sorted(defo_tasks, key=lambda x: x['completed_at']) vasp_input = opt_task['input'] vasp_input.pop('structure') elastic_doc.update({ "deformation_task_ids": defo_task_ids, "optimization_task_id": opt_task['task_id'], "pk_stresses": pk_stresses, "cauchy_stresses": cauchy_stresses, "strains": strains, "deformations": defos, "elastic_tensor": et.voigt, "elastic_tensor_raw": et_raw.voigt, "optimized_structure": opt_struct, "completed_at": defo_tasks[-1]['completed_at'], "optimization_input": vasp_input }) # Process input elastic_doc['warnings'] = get_warnings(et, opt_struct) or None #TODO: process MPWorks metadata? #TODO: higher order #TODO: add some of the relevant DFT params, kpoints elastic_doc['state'] = "filter_failed" if elastic_doc['warnings']\ else "successful" return elastic_doc
def run_task(self, fw_spec): ref_struct = self['structure'] d = {"analysis": {}, "initial_structure": self['structure'].as_dict()} # Get optimized structure calc_locs_opt = [ cl for cl in fw_spec.get('calc_locs', []) if 'optimiz' in cl['name'] ] if calc_locs_opt: optimize_loc = calc_locs_opt[-1]['path'] logger.info("Parsing initial optimization directory: {}".format( optimize_loc)) drone = VaspDrone() optimize_doc = drone.assimilate(optimize_loc) opt_struct = Structure.from_dict( optimize_doc["calcs_reversed"][0]["output"]["structure"]) d.update({"optimized_structure": opt_struct.as_dict()}) ref_struct = opt_struct eq_stress = -0.1 * Stress(optimize_doc["calcs_reversed"][0] ["output"]["ionic_steps"][-1]["stress"]) else: eq_stress = None if self.get("fw_spec_field"): d.update({ self.get("fw_spec_field"): fw_spec.get(self.get("fw_spec_field")) }) # Get the stresses, strains, deformations from deformation tasks defo_dicts = fw_spec["deformation_tasks"].values() stresses, strains, deformations = [], [], [] for defo_dict in defo_dicts: stresses.append(Stress(defo_dict["stress"])) strains.append(Strain(defo_dict["strain"])) deformations.append(Deformation(defo_dict["deformation_matrix"])) # Add derived stresses and strains if symmops is present for symmop in defo_dict.get("symmops", []): stresses.append(Stress(defo_dict["stress"]).transform(symmop)) strains.append(Strain(defo_dict["strain"]).transform(symmop)) deformations.append( Deformation( defo_dict["deformation_matrix"]).transform(symmop)) stresses = [-0.1 * s for s in stresses] pk_stresses = [ stress.piola_kirchoff_2(deformation) for stress, deformation in zip(stresses, deformations) ] d['fitting_data'] = { 'cauchy_stresses': stresses, 'eq_stress': eq_stress, 'strains': strains, 'pk_stresses': pk_stresses, 'deformations': deformations } logger.info("Analyzing stress/strain data") # TODO: @montoyjh: what if it's a cubic system? don't need 6. -computron # TODO: Can add population method but want to think about how it should # be done. -montoyjh order = self.get('order', 2) if order > 2: method = 'finite_difference' else: method = self.get('fitting_method', 'finite_difference') if method == 'finite_difference': result = ElasticTensorExpansion.from_diff_fit(strains, pk_stresses, eq_stress=eq_stress, order=order) if order == 2: result = ElasticTensor(result[0]) elif method == 'pseudoinverse': result = ElasticTensor.from_pseudoinverse(strains, pk_stresses) elif method == 'independent': result = ElasticTensor.from_independent_strains( strains, pk_stresses, eq_stress=eq_stress) else: raise ValueError( "Unsupported method, method must be finite_difference, " "pseudoinverse, or independent") ieee = result.convert_to_ieee(ref_struct) d.update({ "elastic_tensor": { "raw": result.voigt, "ieee_format": ieee.voigt } }) if order == 2: d.update({ "derived_properties": ieee.get_structure_property_dict(ref_struct) }) else: soec = ElasticTensor(ieee[0]) d.update({ "derived_properties": soec.get_structure_property_dict(ref_struct) }) d["formula_pretty"] = ref_struct.composition.reduced_formula d["fitting_method"] = method d["order"] = order d = jsanitize(d) # Save analysis results in json or db db_file = env_chk(self.get('db_file'), fw_spec) if not db_file: with open("elasticity.json", "w") as f: f.write(json.dumps(d, default=DATETIME_HANDLER)) else: db = VaspCalcDb.from_db_file(db_file, admin=True) db.collection = db.db["elasticity"] db.collection.insert_one(d) logger.info("Elastic analysis complete.") return FWAction()
def test_convert_strain_to_deformation(self): defo = Deformation.from_index_amount((1, 2), 0.01) pass
def __init__(self, deformation): self.deformation = Deformation(deformation)
def write_vasp_inputs(Str, VASPDir, functional='PBE', num_kpoints=25, additional_vasp_settings=None, strain=((1.01, 0, 0), (0, 1.05, 0), (0, 0, 1.03))): # This is a somewhat strange input set. Essentially the matgen input set (PBE+U), but with tigher # convergence. # This is also a somewhat outdated and convoluted way to generate VASP inputs but it should work fine. # These changes to the default input set give much better results. # Do not increaes the EDIFF to make it converge faster!!! # If convergence is too slow, reduce the K-points # This is still using PBE+U with matgen U values though. Need to use MITCompatibility (after the run) # to apply oxygen corrections and such. # In other expansions that rely on SCAN or HSE, the corrections are different - no O correction for example # In additional_vasp_settings, you can add to, or modify the default VASPsettings. VASPSettings = { "ALGO": 'VeryFast', "ISYM": 0, "ISMEAR": 0, "EDIFF": 1e-6, "NELM": 400, "NSW": 1000, "EDIFFG": -0.02, 'LVTOT': False, 'LWAVE': False, 'LCHARG': False, 'NELMDL': -6, 'NELMIN': 8, 'LSCALU': False, 'NPAR': 2, 'NSIM': 2, 'POTIM': 0.25, 'LDAU': True } if additional_vasp_settings: for key in additional_vasp_settings: VASPSettings[key] = additional_vasp_settings[key] print('Changed {} setting to {}.'.format( key, additional_vasp_settings[key])) if not os.path.isdir(VASPDir): os.mkdir(VASPDir) # Joggle the lattice to help symmetry broken relaxation. You may turn it off by setting strain=None if strain: deformation = Deformation(strain) defStr = deformation.apply_to_structure(Str) #Str=Structure(StrainedLatt,Species,FracCoords,to_unit_cell=False,coords_are_cartesian=False); VIO = MITRelaxSet(defStr, potcar_functional=functional) VIO.user_incar_settings = VASPSettings VIO.incar.write_file(os.path.join(VASPDir, 'INCAR')) VIO.poscar.write_file(os.path.join(VASPDir, 'POSCAR')) Kpoints.automatic(num_kpoints).write_file(os.path.join(VASPDir, 'KPOINTS')) # Use PAW_PBE pseudopotentials, cannot use PBE_52, this does not exist on ginar! # NOTE: For the POTCARs to work, you need to set up the VASP pseudopotential directory as per the # pymatgen instructions, and set the path to them in .pmgrc.yaml located in your home folder. # The pymatgen website has instuctrions for how to do this. POTSyms = VIO.potcar_symbols for i, Sym in enumerate(POTSyms): if Sym == 'Zr': POTSyms[i] = 'Zr_sv' Potcar(POTSyms, functional=functional).write_file(os.path.join(VASPDir, 'POTCAR'))
def get_wf_gibbs_free_energy(structure, vasp_input_set=None, vasp_cmd="vasp", deformations=None, db_file=None, user_kpoints_settings=None, t_step=10, t_min=0, t_max=1000, mesh=(20, 20, 20), eos="vinet", qha_type="debye_model", pressure=0.0): """ Returns quasi-harmonic gibbs free energy workflow. Note: phonopy package is required for the final analysis step if qha_type="phonopy" Args: structure (Structure): input structure. vasp_input_set (VaspInputSet) vasp_cmd (str): vasp command to run. deformations (list): list of deformation matrices(list of lists). db_file (str): path to the db file. user_kpoints_settings (dict): example: {"grid_density": 7000} t_step (float): temperature step (in K) t_min (float): min temperature (in K) t_max (float): max temperature (in K) mesh (list/tuple): reciprocal space density eos (str): equation of state used for fitting the energies and the volumes. options supported by phonopy: "vinet", "murnaghan", "birch_murnaghan". Note: pymatgen supports more options than phonopy. see pymatgen.analysis.eos.py qha_type(str): quasi-harmonic approximation type: "debye_model" or "phonopy", default is "debye_model" pressure (float): in GPa Returns: Workflow """ lepsilon = False if qha_type not in ["debye_model"]: lepsilon = True try: from phonopy import Phonopy except ImportError: logger.warn( "'phonopy' package NOT installed. Required for the final analysis step." "The debye model for the quasi harmonic approximation will be used." ) qha_type = "debye_model" lepsilon = False tag = datetime.utcnow().strftime('%Y-%m-%d-%H-%M-%S-%f') deformations = [Deformation(defo_mat) for defo_mat in deformations] wf_gibbs = get_wf_deformations(structure, deformations, name="gibbs deformation", vasp_input_set=vasp_input_set, lepsilon=lepsilon, vasp_cmd=vasp_cmd, db_file=db_file, user_kpoints_settings=user_kpoints_settings, tag=tag) fw_analysis = Firework(GibbsFreeEnergyTask(tag=tag, db_file=db_file, t_step=t_step, t_min=t_min, t_max=t_max, mesh=mesh, eos=eos, qha_type=qha_type, pressure=pressure), name="Gibbs Free Energy") append_fw_wf(wf_gibbs, fw_analysis) wf_gibbs.name = "{}:{}".format(structure.composition.reduced_formula, "gibbs free energy") return wf_gibbs
class DeformationTest(PymatgenTest): def setUp(self): self.norm_defo = Deformation.from_index_amount((0, 0), 0.02) self.ind_defo = Deformation.from_index_amount((0, 1), 0.02) self.non_ind_defo = Deformation([[1.0, 0.02, 0.02], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) lattice = Lattice([[3.8401979337, 0.00, 0.00], [1.9200989668, 3.3257101909, 0.00], [0.00, -2.2171384943, 3.1355090603]]) self.structure = Structure(lattice, ["Si", "Si"], [[0, 0, 0], [0.75, 0.5, 0.75]]) def test_properties(self): # green_lagrange_strain self.assertArrayAlmostEqual(self.ind_defo.green_lagrange_strain, [[0., 0.01, 0.], [0.01, 0.0002, 0.], [0., 0., 0.]]) self.assertArrayAlmostEqual(self.non_ind_defo.green_lagrange_strain, [[0., 0.01, 0.01], [0.01, 0.0002, 0.0002], [0.01, 0.0002, 0.0002]]) def test_independence(self): self.assertFalse(self.non_ind_defo.is_independent()) self.assertEqual(self.ind_defo.get_perturbed_indices()[0], (0, 1)) def test_apply_to_structure(self): strained_norm = self.norm_defo.apply_to_structure(self.structure) strained_ind = self.ind_defo.apply_to_structure(self.structure) strained_non = self.non_ind_defo.apply_to_structure(self.structure) # Check lattices self.assertArrayAlmostEqual(strained_norm.lattice.matrix, [[3.9170018886, 0, 0], [1.958500946136, 3.32571019, 0], [0, -2.21713849, 3.13550906]]) self.assertArrayAlmostEqual(strained_ind.lattice.matrix, [[3.84019793, 0, 0], [1.9866132, 3.32571019, 0], [-0.04434277, -2.21713849, 3.13550906]]) self.assertArrayAlmostEqual(strained_non.lattice.matrix, [[3.84019793, 0, 0], [1.9866132, 3.3257102, 0], [0.0183674, -2.21713849, 3.13550906]]) # Check coordinates self.assertArrayAlmostEqual(strained_norm.sites[1].coords, [3.91700189, 1.224e-06, 2.3516318]) self.assertArrayAlmostEqual(strained_ind.sites[1].coords, [3.84019793, 1.224e-6, 2.3516318]) self.assertArrayAlmostEqual(strained_non.sites[1].coords, [3.8872306, 1.224e-6, 2.3516318]) # Check convention for applying transformation for vec, defo_vec in zip(self.structure.lattice.matrix, strained_non.lattice.matrix): new_vec = np.dot(self.non_ind_defo, np.transpose(vec)) self.assertArrayAlmostEqual(new_vec, defo_vec) for coord, defo_coord in zip(self.structure.cart_coords, strained_non.cart_coords): new_coord = np.dot(self.non_ind_defo, np.transpose(coord)) self.assertArrayAlmostEqual(new_coord, defo_coord)
def process_elastic_calcs(opt_doc, defo_docs, add_derived=True, tol=0.002): """ Generates the list of calcs from deformation docs, along with 'derived stresses', i. e. stresses derived from symmop transformations of existing calcs from transformed strains resulting in an independent strain not in the input list Args: opt_doc (dict): document for optimization task defo_docs ([dict]) list of documents for deformation tasks add_derived (bool): flag for whether or not to add derived stress-strain pairs based on symmetry tol (float): tolerance for assigning equivalent stresses/strains Returns ([dict], [dict]): Two lists of summary documents corresponding to strains and stresses, one explicit and one derived """ structure = Structure.from_dict(opt_doc['output']['structure']) input_structure = Structure.from_dict(opt_doc['input']['structure']) # Process explicit calcs, store in dict keyed by strain explicit_calcs = TensorMapping() for doc in defo_docs: calc = { "type": "explicit", "input": doc["input"], "output": doc["output"], "task_id": doc["task_id"], "completed_at": doc["completed_at"] } deformed_structure = Structure.from_dict(doc['output']['structure']) defo = Deformation(calculate_deformation(structure, deformed_structure)) # Warning if deformation is not equivalent to stored deformation stored_defo = doc['transmuter']['transformation_params'][0]\ ['deformation'] if not np.allclose(defo, stored_defo, atol=1e-5): wmsg = "Inequivalent stored and calc. deformations." logger.debug(wmsg) calc["warnings"] = wmsg cauchy_stress = -0.1 * Stress(doc['output']['stress']) pk_stress = cauchy_stress.piola_kirchoff_2(defo) strain = defo.green_lagrange_strain calc.update({ "deformation": defo, "cauchy_stress": cauchy_stress, "strain": strain, "pk_stress": pk_stress }) if strain in explicit_calcs: existing_value = explicit_calcs[strain] if doc['completed_at'] > existing_value['completed_at']: explicit_calcs[strain] = calc else: explicit_calcs[strain] = calc if not add_derived: return explicit_calcs.values(), None # Determine all of the implicit calculations to include sga = SpacegroupAnalyzer(structure, symprec=0.1) symmops = sga.get_symmetry_operations(cartesian=True) derived_calcs_by_strain = TensorMapping(tol=0.002) for strain, calc in explicit_calcs.items(): # Generate all transformed strains task_id = calc['task_id'] tstrains = [(symmop, strain.transform(symmop)) for symmop in symmops] # Filter strains by those which are independent and new # For second order if len(explicit_calcs) < 30: tstrains = [(symmop, tstrain) for symmop, tstrain in tstrains if tstrain.get_deformation_matrix().is_independent(tol) and not tstrain in explicit_calcs] # For third order else: strain_states = get_default_strain_states(3) # Default stencil in atomate, this maybe shouldn't be hard-coded stencil = np.linspace(-0.075, 0.075, 7) valid_strains = [ Strain.from_voigt(s * np.array(strain_state)) for s, strain_state in product(stencil, strain_states) ] valid_strains = [v for v in valid_strains if not np.allclose(v, 0)] valid_strains = TensorMapping(valid_strains, [True] * len(valid_strains)) tstrains = [ (symmop, tstrain) for symmop, tstrain in tstrains if tstrain in valid_strains and not tstrain in explicit_calcs ] # Add surviving tensors to derived_strains dict for symmop, tstrain in tstrains: # curr_set = derived_calcs_by_strain[tstrain] if tstrain in derived_calcs_by_strain: curr_set = derived_calcs_by_strain[tstrain] curr_task_ids = [c[1] for c in curr_set] if task_id not in curr_task_ids: curr_set.append((symmop, calc['task_id'])) else: derived_calcs_by_strain[tstrain] = [(symmop, calc['task_id'])] # Process derived calcs explicit_calcs_by_id = {d['task_id']: d for d in explicit_calcs.values()} derived_calcs = [] for strain, calc_set in derived_calcs_by_strain.items(): symmops, task_ids = zip(*calc_set) task_strains = [ Strain(explicit_calcs_by_id[task_id]['strain']) for task_id in task_ids ] task_stresses = [ explicit_calcs_by_id[task_id]['cauchy_stress'] for task_id in task_ids ] derived_strains = [ tstrain.transform(symmop) for tstrain, symmop in zip(task_strains, symmops) ] for derived_strain in derived_strains: if not np.allclose(derived_strain, strain, atol=2e-3): logger.info("Issue with derived strains") raise ValueError("Issue with derived strains") derived_stresses = [ tstress.transform(sop) for sop, tstress in zip(symmops, task_stresses) ] input_docs = [{ "task_id": task_id, "strain": task_strain, "cauchy_stress": task_stress, "symmop": symmop } for task_id, task_strain, task_stress, symmop in zip( task_ids, task_strains, task_stresses, symmops)] calc = { "strain": strain, "cauchy_stress": Stress(np.average(derived_stresses, axis=0)), "deformation": strain.get_deformation_matrix(), "input_tasks": input_docs, "type": "derived" } calc['pk_stress'] = calc['cauchy_stress'].piola_kirchoff_2( calc['deformation']) derived_calcs.append(calc) return list(explicit_calcs.values()), derived_calcs
def _load_data(self): """ This function parses existing vasp calculations, does mapping check, assigns charges and writes into the calc_data file mentioned in previous functions. What we mean by mapping check here, is to see whether a deformed structure can be mapped into a supercell lattice and generates a set of correlation functions in clustersupercell.corr_from_structure. We plan to do modify corr_from_structure from using pymatgen.structurematcher to a grid matcher, which will ensure higher acceptance for DFT calculations, but does not necessarily improve CE hamitonian, since some highly dipoled and deformed structures might have poor DFT energy, and even SABOTAGE CE! """ # Every key in self.calcdata['compositions'] is a composition, and each composition contains a list of dict entrees. # relaxed_structure, input_structure, magmoms, total_energy. _is_vasp_calc = lambda fs: 'POSCAR' in fs and 'INCAR' in fs and 'KPOINTS' in fs and 'POTCAR' in fs # Load VASP runs from given directories n_matched = 0 n_inputs = 0 new_unassigned_strs = [] for root, dirs, files in os.walk(self.vaspdir): #A calculation directories has only 3 status: #accepted: calculation was successful, and already entered into calcdata.mson #falied: calculated but not successful, either aborted or can't be read into calcdata.mson #For these above two, we don't want to submit a calculation or post-process again. #not marked: calculation run not started or not finished yet. Since analyzer is always called #after runner, we don't need to worry that analyzer will find unmarked folders. if _is_vasp_calc(files) and (not 'accepted' in files) and (not 'failed' in files): print("Loading VASP run in {}".format(root)) parent_root = os.path.join(*root.split(os.sep)[0:-1]) parent_parent_root = os.path.join(*root.split(os.sep)[0:-2]) with open( os.path.join(parent_parent_root, 'composition_by_site')) as compfile: composition = json.load(compfile) compstring = json.dumps(composition) if compstring not in self.calcdata['compositions']: self.calcdata['compositions'][compstring] = [] if not os.path.isfile(os.path.join(parent_root, 'matrix')): print( 'Warning: matrix presave not found. Will autodetect supercell matrix using structure matcher,\ and will suffer from numerical errors!') matrix = None else: with open(os.path.join(parent_root, 'matrix')) as mat_file: matrix = json.load(mat_file) #Check existence of output structure try: relaxed_struct = Poscar.from_file( os.path.join(root, 'CONTCAR')).structure except: print('Entry {} CONTCAR can not be read. Skipping.'.format( root)) open(os.path.join(root, 'failed'), 'a').close() continue input_struct = Poscar.from_file( os.path.join(parent_root, 'POSCAR')).structure #Check uniqueness strict_sm = StructureMatcher(stol=0.1, ltol=0.1, angle_tol=1, comparator=ElementComparator()) _is_unique = True for entry in self.calcdata['compositions'][compstring]: entry_struct = Structure.from_dict( entry['relaxed_structure']) if strict_sm.fit(entry_struct, relaxed_struct): _is_unique = False break if not _is_unique: print('Entry {} alredy calculated before.'.format(root)) open(os.path.join(root, 'accepted'), 'a').close() continue n_inputs += 1 # Note: the input_struct here comes from the poscar in upper root, rather than fm.0, so # it is not deformed. # Rescale volume to that of unrelaxed structure, this will lead to a better mapping back. # I changed it to a rescaling tensor relaxed_lat_mat = np.matrix(relaxed_struct.lattice.matrix) input_lat_mat = np.matrix(input_struct.lattice.matrix) o2i_deformation = Deformation(input_lat_mat.T * relaxed_lat_mat.I.T) relaxed_deformed = o2i_deformation.apply_to_structure( relaxed_struct) #print(relaxed_deformed,input_struct) # Assign oxidation states to Mn based on magnetic moments in OUTCAR, first check existence of OUTCAR try: Out = Outcar(os.path.join(root, 'OUTCAR')) except: print('Entry {} OUTCAR can not be read. Skipping.'.format( root)) open(os.path.join(root, 'failed'), 'a').close() continue # Get final energy from OSZICAR or Vasprun. Vasprun is better but OSZICAR is much # faster and works fine is you separately check for convergence, sanity of # magnetic moments, structure geometry with open(os.path.join(root, 'OUTCAR')) as outfile: outcar_string = outfile.read() if 'reached required accuracy' not in outcar_string: print( 'Entry {} did not converge to required accuracy. Skipping.' .format(root)) open(os.path.join(root, 'failed'), 'a').close() continue TotE = Oszicar(os.path.join(root, 'OSZICAR')).final_energy # Checking convergence Mag = [] for SiteInd, Site in enumerate(relaxed_struct.sites): Mag.append(np.abs(Out.magnetization[SiteInd]['tot'])) new_entry = {} new_entry['input_structure'] = input_struct.as_dict() new_entry['relaxed_structure'] = relaxed_struct.as_dict() new_entry['relaxed_deformed'] = relaxed_deformed.as_dict() new_entry['total_energy'] = TotE new_entry['magmoms'] = Mag new_entry['matrix'] = matrix if os.path.isfile(os.path.join(parent_parent_root, 'axis')): with open(os.path.join(parent_parent_root, 'axis')) as axisfile: axis = json.load(axisfile) if 'axis' not in new_entry: new_entry['axis'] = axis new_unassigned_strs.append((compstring, root, new_entry)) if len(new_unassigned_strs) == 0: print('No new structures appeared. Calcdata will not be updated.') return #Charge assignment if self.is_charged_ce: relaxed_deformed_pool = [] relaxed_strs_pool = [] mags = [] roots = [] energies = [] comps = [] inputs = [] mats = [] if 'axis' in new_unassigned_strs[0][2]: axis = [] for compstring, root, new_entry in new_unassigned_strs: # Out=Outcar(os.path.join(root,'OUTCAR')) Mag = new_entry['magmoms'] relaxed_struct = Structure.from_dict( new_entry['relaxed_structure']) relaxed_deformed = Structure.from_dict( new_entry['relaxed_deformed']) # Throw out structures where oxidation states don't make charge balanced. mags.append(Mag) roots.append(root) relaxed_strs_pool.append(relaxed_struct) relaxed_deformed_pool.append(relaxed_deformed) comps.append(compstring) inputs.append(Structure.from_dict( new_entry['input_structure'])) energies.append(new_entry['total_energy']) mats.append(new_entry['matrix']) if 'axis' in new_entry: axis.append(new_entry['axis']) CA = ChargeAssign(relaxed_strs_pool, mags, algo=self.assign_algo) relaxed_strs_assigned = CA.assigned_structures relaxed_deformed_assigned = CA.extend_assignments( relaxed_deformed_pool, mags) for i in range(len(inputs)): if relaxed_strs_assigned[ i] is not None and relaxed_deformed_assigned[ i] is not None: # Checking whether structure can be mapped to corr function. # This is out deformation tolerance. try: if mats[i] is not None: cesup = self.ce.supercell_from_matrix(mats[i]) corr = cesup.corr_from_structure( relaxed_deformed_assigned[i]) else: corr = self.ce.corr_from_structure( relaxed_deformed_assigned[i]) except: print( "Entry {} too far from original lattice. Skipping." .format(roots[i])) open(os.path.join(roots[i], 'failed'), 'a').close() continue assigned_entry = {} assigned_entry['input_structure'] = inputs[i].as_dict() assigned_entry[ 'relaxed_structure'] = relaxed_strs_assigned[ i].as_dict() assigned_entry[ 'relaxed_deformed'] = relaxed_deformed_assigned[ i].as_dict() assigned_entry['matrix'] = mats[i] assigned_entry['total_energy'] = energies[i] assigned_entry['magmoms'] = mags[i] if 'axis' in new_unassigned_strs[0][2]: assigned_entry['axis'] = axis[i] self.calcdata['compositions'][comps[i]].append( assigned_entry) print('Entry {} accepted!'.format(roots[i])) open(os.path.join(roots[i], 'accepted'), 'a').close() n_matched += 1 else: print("Entry {} can not be assigned. Skipping.".format( roots[i])) open(os.path.join(roots[i], 'failed'), 'a').close() continue else: print('Doing non charged ce.') for compstring, root, new_entry in new_unassigned_strs: # Checking whether structure can be mapped to corr function. # This is out deformation tolerance. try: if new_entry['matrix'] is not None: cesup = self.ce.supercell_from_matrix( new_entry['matrix']) corr = cesup.corr_from_structure( Structure.from_dict(new_entry['relaxed_defromed'])) else: corr = self.ce.corr_from_structure( Structure.from_dict(new_entry['relaxed_defromed'])) except: print("Entry {} too far from original lattice. Skipping.". format(root)) open(os.path.join(root, 'failed'), 'a').close() continue self.calcdata['compositions'][compstring].append(new_entry) open(os.path.join(root, 'accepted'), 'a').close() n_matched += 1 # Data already deduplicated! print( '{}/{} structures matched in this run. Parsed vasp data will be saved into {}.' .format(n_matched, n_inputs, self.calc_data_file))
def get_wf_elastic_constant(structure, vasp_input_set=None, vasp_cmd="vasp", norm_deformations=[-0.01, -0.005, 0.005, 0.01], shear_deformations=[-0.06, -0.03, 0.03, 0.06], db_file=None, reciprocal_density=None): """ Returns a workflow to calculate elastic constants. Firework 1 : write vasp input set for structural relaxation, run vasp, pass run location, database insertion. Firework 2 - 25: Optimize Deformed Structure Firework 26: Analyze Stress/Strain data and fit the elastic tensor Args: structure (Structure): input structure to be optimized and run norm_deformations (list): list of values to for normal deformations shear_deformations (list): list of values to for shear deformations vasp_input_set (DictVaspInputSet): vasp input set. vasp_cmd (str): command to run db_file (str): path to file containing the database credentials. reciprocal_density (int): k-points per reciprocal atom by volume Returns: Workflow """ v = vasp_input_set or MPRelaxSet(structure, force_gamma=True) if reciprocal_density: v.config_dict["KPOINTS"].update( {"reciprocal_density":reciprocal_density}) v = DictSet(structure, v.config_dict) fws = [] fws.append(OptimizeFW(structure=structure, vasp_input_set=v, vasp_cmd=vasp_cmd, db_file=db_file)) deformations = [] # Generate deformations for ind in [(0, 0), (1, 1), (2, 2)]: for amount in norm_deformations: defo = Deformation.from_index_amount(ind, amount) deformations.append(defo) for ind in [(0, 1), (0, 2), (1, 2)]: for amount in shear_deformations: defo = Deformation.from_index_amount(ind, amount) deformations.append(defo) def_vasp_params = {"user_incar_settings":{"ISIF":2, "IBRION":2, "NSW":99, "LAECHG":False, "LHVAR":False, "ALGO":"Fast", "LWAVE":False}} if reciprocal_density: def_vasp_params.update( {"reciprocal_density":reciprocal_density}) for deformation in deformations: fw = TransmuterFW(name="elastic deformation", structure=structure, transformations=['DeformStructureTransformation'], transformation_params=[ {"deformation": deformation.tolist()}], copy_vasp_outputs=True, db_file=db_file, vasp_cmd=vasp_cmd, parents=fws[0], vasp_input_params = def_vasp_params ) fw.spec['_tasks'].append( PassStressStrainData(deformation=deformation.tolist()).to_dict()) fws.append(fw) fws.append(Firework(AnalyzeStressStrainData(structure=structure, db_file=db_file), name="Analyze Elastic Data", parents=fws[1:], spec = {"_allow_fizzled_parents":True})) wfname = "{}:{}".format(structure.composition.reduced_formula, "elastic constants") return Workflow(fws, name=wfname)
def get_wf_elastic_constant( structure, vasp_input_set=None, vasp_cmd="vasp", norm_deformations=None, shear_deformations=None, additional_deformations=None, db_file=None, user_kpoints_settings=None, add_analysis_task=True, conventional=True, ): """ Returns a workflow to calculate elastic constants. Firework 1 : write vasp input set for structural relaxation, run vasp, pass run location, database insertion. Firework 2 - number of total deformations: Static runs on the deformed structures last Firework : Analyze Stress/Strain data and fit the elastic tensor Args: structure (Structure): input structure to be optimized and run. norm_deformations (list): list of values to for normal deformations. shear_deformations (list): list of values to for shear deformations. additional_deformations (list of 3x3 array-likes): list of additional deformations. vasp_input_set (DictVaspInputSet): vasp input set. vasp_cmd (str): command to run. db_file (str): path to file containing the database credentials. user_kpoints_settings (dict): example: {"grid_density": 7000} add_analysis_task (bool): boolean indicating whether to add analysis Returns: Workflow """ # Convert to conventional if conventional: structure = SpacegroupAnalyzer(structure).get_conventional_standard_structure() # Generate deformations deformations = [] if norm_deformations is not None: deformations.extend( [ Deformation.from_index_amount(ind, amount) for ind in [(0, 0), (1, 1), (2, 2)] for amount in norm_deformations ] ) if shear_deformations is not None: deformations.extend( [ Deformation.from_index_amount(ind, amount) for ind in [(0, 1), (0, 2), (1, 2)] for amount in shear_deformations ] ) if additional_deformations: deformations.extend([Deformation(defo_mat) for defo_mat in additional_deformations]) if not deformations: raise ValueError("deformations list empty") wf_elastic = get_wf_deformations( structure, deformations, vasp_input_set=vasp_input_set, lepsilon=False, vasp_cmd=vasp_cmd, db_file=db_file, user_kpoints_settings=user_kpoints_settings, pass_stress_strain=True, name="deformation", relax_deformed=True, tag="elastic", ) if add_analysis_task: fw_analysis = Firework( ElasticTensorToDbTask(structure=structure, db_file=db_file), name="Analyze Elastic Data", spec={"_allow_fizzled_parents": True}, ) append_fw_wf(wf_elastic, fw_analysis) wf_elastic.name = "{}:{}".format(structure.composition.reduced_formula, "elastic constants") return wf_elastic