def test_get_representations(self): # tests to convert between storing magnetic moment information # on site_properties or on Species 'spin' property # test we store magnetic moments on site properties self.Fe.add_site_property("magmom", [5]) msa = CollinearMagneticStructureAnalyzer(self.Fe) self.assertEqual(msa.structure.site_properties["magmom"][0], 5) # and that we can retrieve a spin representation Fe_spin = msa.get_structure_with_spin() self.assertFalse("magmom" in Fe_spin.site_properties) self.assertEqual(Fe_spin[0].specie.spin, 5) # test we can remove magnetic moment information msa.get_nonmagnetic_structure() self.assertFalse("magmom" in Fe_spin.site_properties) # test with disorder on magnetic site self.Fe[0] = { Species("Fe", oxidation_state=0, properties={"spin": 5}): 0.5, "Ni": 0.5, } self.assertRaises(NotImplementedError, CollinearMagneticStructureAnalyzer, self.Fe)
def process_item(self, item): """ Process the tasks and materials into a magnetism collection Args: item dict: a dict of material_id, structure, and tasks Returns: dict: a magnetism dictionary """ struct = Structure.from_dict(item["structure"]) total_magnetization = item["magnetism"].get( "total_magnetization", 0) # not necessarily == sum(magmoms) msa = CollinearMagneticStructureAnalyzer(struct) sign = np.sign(total_magnetization) total_magnetization = abs(total_magnetization) magmoms = list(sign * np.array(msa.magmoms)) magnetism = { self.magnetism.key: item[self.materials.key], "magnetism": { 'ordering': msa.ordering.value, 'is_magnetic': msa.is_magnetic, 'exchange_symmetry': msa.get_exchange_group_info()[1], 'num_magnetic_sites': msa.number_of_magnetic_sites, 'num_unique_magnetic_sites': msa.number_of_unique_magnetic_sites(), 'types_of_magnetic_species': [str(t) for t in msa.types_of_magnetic_specie], 'magmoms': magmoms, 'total_magnetization_normalized_vol': total_magnetization / struct.volume, 'total_magnetization_normalized_formula_units': total_magnetization / (struct.composition.get_reduced_composition_and_factor()[1]) }, "pymatgen_version": pymatgen_version } return magnetism
def calc(self, item): """ Process the tasks and materials into a magnetism collection Args: item dict: a dict of material_id, structure, and tasks Returns: dict: a magnetism dictionary """ struct = Structure.from_dict(get(item, "output.structure")) struct_has_magmoms = 'magmom' in struct.site_properties total_magnetization = abs( get(item, "calcs_reversed.0.output.outcar.total_magnetization", 0)) msa = CollinearMagneticStructureAnalyzer(struct) magmoms = msa.magmoms magnetism = { "magnetism": { 'ordering': msa.ordering.value if struct_has_magmoms else "Unknown", 'is_magnetic': msa.is_magnetic, 'exchange_symmetry': msa.get_exchange_group_info()[1], 'num_magnetic_sites': msa.number_of_magnetic_sites, 'num_unique_magnetic_sites': msa.number_of_unique_magnetic_sites(), 'types_of_magnetic_species': [str(t) for t in msa.types_of_magnetic_specie], 'magmoms': magmoms, 'total_magnetization': total_magnetization, 'total_magnetization_normalized_vol': total_magnetization / struct.volume, 'total_magnetization_normalized_formula_units': total_magnetization / (struct.composition.get_reduced_composition_and_factor()[1]) }, "pymatgen_version": pymatgen_version } return magnetism
def _get_unique_sites(structure): """ Get dict that maps site indices to unique identifiers. Args: structure (Structure): ground state Structure object. Returns: unique_site_ids (dict): maps tuples of equivalent site indices to a unique int identifier wyckoff_ids (dict): maps tuples of equivalent site indices to their wyckoff symbols """ # Get a nonmagnetic representation of the supercell geometry s0 = CollinearMagneticStructureAnalyzer( structure, make_primitive=False, threshold=0.0).get_nonmagnetic_structure(make_primitive=False) # Get unique sites and wyckoff positions if "wyckoff" in s0.site_properties: s0.remove_site_property("wyckoff") symm_s0 = SpacegroupAnalyzer(s0).get_symmetrized_structure() wyckoff = ["n/a"] * len(symm_s0) equivalent_indices = symm_s0.equivalent_indices wyckoff_symbols = symm_s0.wyckoff_symbols # Construct dictionaries that map sites to numerical and wyckoff # identifiers unique_site_ids = {} wyckoff_ids = {} i = 0 for indices, symbol in zip(equivalent_indices, wyckoff_symbols): unique_site_ids[tuple(indices)] = i wyckoff_ids[i] = symbol i += 1 for index in indices: wyckoff[index] = symbol return unique_site_ids, wyckoff_ids
def add_magnetism(mat, mag=None): mag_types = { "NM": "Non-magnetic", "FiM": "Ferri", "AFM": "AFM", "FM": "FM" } struc = Structure.from_dict(mat["structure"]) msa = CollinearMagneticStructureAnalyzer(struc) mat["magnetic_type"] = mag_types[msa.ordering.value]
def test_round_magmoms(self): struct = self.NiO_AFM_001.copy() struct.add_site_property("magmom", [-5.0143, -5.02, 0.147, 0.146]) msa = CollinearMagneticStructureAnalyzer(struct, round_magmoms=0.001, make_primitive=False) self.assertTrue( np.allclose(msa.magmoms, [5.0171, 5.0171, -0.1465, -0.1465])) self.assertAlmostEqual(msa.magnetic_species_and_magmoms["Ni"], 5.0171) self.assertAlmostEqual(msa.magnetic_species_and_magmoms["O"], 0.1465) struct.add_site_property("magmom", [-5.0143, 4.5, 0.147, 0.146]) msa = CollinearMagneticStructureAnalyzer(struct, round_magmoms=0.001, make_primitive=False) self.assertTrue( np.allclose(msa.magmoms, [5.0143, -4.5, -0.1465, -0.1465])) self.assertAlmostEqual(msa.magnetic_species_and_magmoms["Ni"][0], 4.5) self.assertAlmostEqual(msa.magnetic_species_and_magmoms["Ni"][1], 5.0143) self.assertAlmostEqual(msa.magnetic_species_and_magmoms["O"], 0.1465)
def update_contents(self, new_store_contents): struct = self.from_data(new_store_contents) msa = CollinearMagneticStructureAnalyzer(struct, round_magmoms=1) if not msa.is_magnetic: # TODO: detect magnetic elements (?) return html.Div( "This structure is not magnetic or does not have " "magnetic information associated with it." ) mag_species_and_magmoms = msa.magnetic_species_and_magmoms for k, v in mag_species_and_magmoms.items(): if not isinstance(v, list): mag_species_and_magmoms[k] = [v] magnetic_atoms = "\n".join( [ f"{sp} ({', '.join([f'{magmom} µB' for magmom in magmoms])})" for sp, magmoms in mag_species_and_magmoms.items() ] ) magnetization_per_formula_unit = ( msa.total_magmoms / msa.structure.composition.get_reduced_composition_and_factor()[1] ) rows = [] rows.append( ( html.B("Total magnetization per formula unit"), html.Br(), f"{magnetization_per_formula_unit:.1f} µB", ) ) rows.append((html.B("Atoms with local magnetic moments"), html.Br(), magnetic_atoms)) data_block = html.Div([html.P([html.Span(cell) for cell in row]) for row in rows]) viewer = StructureMoleculeComponent( struct, id=self.id("structure"), color_scheme="magmom", static=True ) return Columns([ Column(html.Div([viewer.struct_layout], style={"height": "60vmin"})), Column(data_block) ])
def test_magnetic_properties(self): msa = CollinearMagneticStructureAnalyzer(self.GdB4) self.assertFalse(msa.is_collinear) msa = CollinearMagneticStructureAnalyzer(self.Fe) self.assertFalse(msa.is_magnetic) self.Fe.add_site_property("magmom", [5]) msa = CollinearMagneticStructureAnalyzer(self.Fe) self.assertTrue(msa.is_magnetic) self.assertTrue(msa.is_collinear) self.assertEqual(msa.ordering, Ordering.FM) msa = CollinearMagneticStructureAnalyzer( self.NiO, make_primitive=False, overwrite_magmom_mode="replace_all_if_undefined", ) self.assertEqual(msa.number_of_magnetic_sites, 4) self.assertEqual(msa.number_of_unique_magnetic_sites(), 1) self.assertEqual(msa.types_of_magnetic_species, (Element.Ni, )) self.assertEqual(msa.get_exchange_group_info(), ("Fm-3m", 225))
def test_get_ferromagnetic_structure(self): msa = CollinearMagneticStructureAnalyzer( self.NiO, overwrite_magmom_mode="replace_all_if_undefined") s1 = msa.get_ferromagnetic_structure() s1_magmoms = [float(m) for m in s1.site_properties["magmom"]] s1_magmoms_ref = [5.0, 0.0] self.assertListEqual(s1_magmoms, s1_magmoms_ref) _ = CollinearMagneticStructureAnalyzer( self.NiO_AFM_111, overwrite_magmom_mode="replace_all_if_undefined") s2 = msa.get_ferromagnetic_structure(make_primitive=False) s2_magmoms = [float(m) for m in s2.site_properties["magmom"]] s2_magmoms_ref = [5.0, 0.0] self.assertListEqual(s2_magmoms, s2_magmoms_ref) s2_prim = msa.get_ferromagnetic_structure(make_primitive=True) self.assertTrue( CollinearMagneticStructureAnalyzer(s1).matches_ordering(s2_prim))
def process_item(self, item): """ Process the tasks and materials into a dielectrics collection Args: item dict: a dict of material_id, structure, and tasks Returns: dict: a dieletrics dictionary """ struc = Structure.from_dict(item["structure"]) msa = CollinearMagneticStructureAnalyzer(struc) magnetism = { self.magnetism.key: item[self.materials.key], "magnetism": { 'ordering': msa.ordering.value } } return magnetism
def from_structure( # type: ignore[override] structure: Structure, material_id: str, fields: Optional[List[str]] = None, **kwargs) -> "MaterialsDoc": """ Builds a materials document using the minimal amount of information """ meta = StructureMetadata.from_structure(structure, fields=fields) ordering = CollinearMagneticStructureAnalyzer(structure).ordering kwargs.update(**meta.dict()) if "last_updated" not in kwargs: kwargs["last_updated"] = datetime.utcnow() if "created_at" not in kwargs: kwargs["created_at"] = datetime.utcnow() return MaterialsDoc(structure=structure, material_id=material_id, ordering=ordering, **kwargs)
def test_str(self): msa = CollinearMagneticStructureAnalyzer(self.NiO_AFM_001) ref_msa_str = """Structure Summary Lattice abc : 2.948635277547903 4.17 2.948635277547903 angles : 90.0 90.0 90.0 volume : 36.2558565 A : 2.085 2.085 0.0 B : 0.0 0.0 -4.17 C : -2.085 2.085 0.0 Magmoms Sites +5.00 PeriodicSite: Ni (0.0000, 0.0000, 0.0000) [0.0000, 0.0000, 0.0000] PeriodicSite: O (0.0000, 0.0000, -2.0850) [0.0000, 0.5000, 0.0000] PeriodicSite: O (0.0000, 2.0850, 0.0000) [0.5000, 0.0000, 0.5000] -5.00 PeriodicSite: Ni (0.0000, 2.0850, -2.0850) [0.5000, 0.5000, 0.5000]""" # just compare lines form 'Magmoms Sites', # since lattice param string can vary based on machine precision self.assertEqual( "\n".join(str(msa).split("\n")[-5:-1]), "\n".join(ref_msa_str.split("\n")[-5:-1]), )
def add_magnetism(mat, magnetism=None): # for historical consistency mag_types = { "NM": "Non-magnetic", "FiM": "Ferri", "AFM": "AFM", "FM": "FM" } struc = Structure.from_dict(mat["structure"]) msa = CollinearMagneticStructureAnalyzer(struc) mat["magnetic_type"] = mag_types[msa.ordering.value] # this should never happen for future mats, and should have been fixed # for older mats, but just in case if 'magmom' not in struc.site_properties and mat[ 'total_magnetization'] > 0.1: mat["magnetic_type"] = "Magnetic (unknown ordering)" # TODO: will deprecate the above from dedicated magnetism builder if magnetism: mat["magnetism"] = magnetism["magnetism"]
def test_modes(self): mode = "none" msa = CollinearMagneticStructureAnalyzer(self.NiO, overwrite_magmom_mode=mode) magmoms = msa.structure.site_properties["magmom"] self.assertEqual(magmoms, [0, 0]) mode = "respect_sign" msa = CollinearMagneticStructureAnalyzer(self.NiO_unphysical, overwrite_magmom_mode=mode) magmoms = msa.structure.site_properties["magmom"] self.assertEqual(magmoms, [-5, 0, 0, 0]) mode = "respect_zeros" msa = CollinearMagneticStructureAnalyzer(self.NiO_unphysical, overwrite_magmom_mode=mode) magmoms = msa.structure.site_properties["magmom"] self.assertEqual(magmoms, [5, 0, 0, 0]) mode = "replace_all" msa = CollinearMagneticStructureAnalyzer(self.NiO_unphysical, overwrite_magmom_mode=mode, make_primitive=False) magmoms = msa.structure.site_properties["magmom"] self.assertEqual(magmoms, [5, 5, 0, 0]) mode = "replace_all_if_undefined" msa = CollinearMagneticStructureAnalyzer(self.NiO, overwrite_magmom_mode=mode) magmoms = msa.structure.site_properties["magmom"] self.assertEqual(magmoms, [5, 0]) mode = "normalize" msa = CollinearMagneticStructureAnalyzer( msa.structure, overwrite_magmom_mode="normalize") magmoms = msa.structure.site_properties["magmom"] self.assertEqual(magmoms, [1, 0])
def test_net_positive(self): msa = CollinearMagneticStructureAnalyzer(self.NiO_unphysical) magmoms = msa.structure.site_properties["magmom"] self.assertEqual(magmoms, [3, 0, 0, 0])
def collate_mp_data(struct): mag_tasks = {} min_energy = 0 ordering_labels = { "Unknown": "Unknown", "FM": "Ferromagnetic", "AFM": "Antiferromagnetic", "FiM": "Ferrimagnetic", "NM": "Non-magnetic", } with MPRester(endpoint="https://zola.lbl.gov/rest/v2") as mpr: # find similar structures in Materials Project mpids = mpr.find_structure(struct) # and all tasks for those materials, specifically their structures # and total energy per atom for d in mpr.query({"task_id": {"$in": mpids}}, ["task_ids"]): for task_id in d["task_ids"]: add_task = True task_struct = mpr.get_task_data( task_id, prop="structure")[0]["structure"] have_magmoms = "magmom" in task_struct.site_properties task_energy = mpr.get_task_data( task_id, prop="energy_per_atom")[0]["energy_per_atom"] min_energy = min([min_energy, task_energy]) msa = CollinearMagneticStructureAnalyzer(task_struct) if not have_magmoms: ordering = "Unknown" else: ordering = msa.ordering.value # group together similar orderings, only display lowest # energy task for each ordering mag_tasks_to_remove = [] for mag_task_id, mag_task in mag_tasks.items(): struct_to_compare = mag_task["struct"] if msa.matches_ordering(struct_to_compare): # if existing task is lower in energy, keep that one if mag_task["energy"] < task_energy: add_task = False # else remove existing task and add this one else: mag_tasks_to_remove.append(mag_task_id) # remove higher energy duplicate tasks for mag_task_id in mag_tasks_to_remove: del mag_tasks[mag_task_id] if add_task: mag_tasks[task_id] = { "struct": task_struct, "ordering": ordering_labels[ordering], "energy": task_energy, } for k, v in mag_tasks.items(): v["energy"] = 1000 * (v["energy"] - min_energy) mag_tasks[k] = v return mag_tasks
def _do_cleanup(structures, energies): """Sanitize input structures and energies. Takes magnetic structures and performs the following operations - Erases nonmagnetic ions and gives all ions ['magmom'] site prop - Converts total energies -> energy / magnetic ion - Checks for duplicate/degenerate orderings - Sorts by energy Args: structures (list): Structure objects with magmoms. energies (list): Corresponding energies. Returns: ordered_structures (list): Sanitized structures. ordered_energies (list): Sorted energies. """ # Get only magnetic ions & give all structures site_properties['magmom'] # zero threshold so that magnetic ions with small moments # are preserved ordered_structures = [ CollinearMagneticStructureAnalyzer( s, make_primitive=False, threshold=0.0).get_structure_with_only_magnetic_atoms( make_primitive=False) for s in structures ] # Convert to energies / magnetic ion energies = [e / len(s) for (e, s) in zip(energies, ordered_structures)] # Check for duplicate / degenerate states (sometimes different initial # configs relax to the same state) remove_list = [] for i, e in enumerate(energies): e_tol = 6 # 10^-6 eV/atom tol on energies e = round(e, e_tol) if i not in remove_list: for i_check, e_check in enumerate(energies): e_check = round(e_check, e_tol) if i != i_check and i_check not in remove_list and e == e_check: remove_list.append(i_check) # Also discard structures with small |magmoms| < 0.1 uB # xx - get rid of these or just bury them in the list? # for i, s in enumerate(ordered_structures): # magmoms = s.site_properties['magmom'] # if i not in remove_list: # if any(abs(m) < 0.1 for m in magmoms): # remove_list.append(i) # Remove duplicates if len(remove_list): ordered_structures = [ s for i, s in enumerate(ordered_structures) if i not in remove_list ] energies = [ e for i, e in enumerate(energies) if i not in remove_list ] # Sort by energy if not already sorted ordered_structures = [ s for _, s in sorted(zip(energies, ordered_structures), reverse=False) ] ordered_energies = sorted(energies, reverse=False) return ordered_structures, ordered_energies
def get_low_energy_orderings(self): """ Find lowest energy FM and AFM orderings to compute E_AFM - E_FM. Returns: fm_struct (Structure): fm structure with 'magmom' site property afm_struct (Structure): afm structure with 'magmom' site property fm_e (float): fm energy afm_e (float): afm energy """ fm_struct, afm_struct = None, None mag_min = np.inf mag_max = 0.001 fm_e_min = 0 afm_e_min = 0 # epas = [e / len(s) for (e, s) in zip(self.energies, self.ordered_structures)] for s, e in zip(self.ordered_structures, self.energies): ordering = CollinearMagneticStructureAnalyzer( s, threshold=0.0, make_primitive=False).ordering magmoms = s.site_properties["magmom"] # Try to find matching orderings first if ordering == Ordering.FM and e < fm_e_min: fm_struct = s mag_max = abs(sum(magmoms)) fm_e = e fm_e_min = e if ordering == Ordering.AFM and e < afm_e_min: afm_struct = s afm_e = e mag_min = abs(sum(magmoms)) afm_e_min = e # Brute force search for closest thing to FM and AFM if not fm_struct or not afm_struct: for s, e in zip(self.ordered_structures, self.energies): magmoms = s.site_properties["magmom"] if abs(sum(magmoms)) > mag_max: # FM ground state fm_struct = s fm_e = e mag_max = abs(sum(magmoms)) # AFM ground state if abs(sum(magmoms)) < mag_min: afm_struct = s afm_e = e mag_min = abs(sum(magmoms)) afm_e_min = e elif abs(sum(magmoms)) == 0 and mag_min == 0: if e < afm_e_min: afm_struct = s afm_e = e afm_e_min = e # Convert to magnetic structures with 'magmom' site property fm_struct = CollinearMagneticStructureAnalyzer( fm_struct, make_primitive=False, threshold=0.0).get_structure_with_only_magnetic_atoms( make_primitive=False) afm_struct = CollinearMagneticStructureAnalyzer( afm_struct, make_primitive=False, threshold=0.0).get_structure_with_only_magnetic_atoms( make_primitive=False) return fm_struct, afm_struct, fm_e, afm_e
def procure_response_dict( struct_final, num_perturb_sites, incar_dict, outcar_dict, inv_block_dict, response_dict, perturb_dict, rkey, keys, ldaul_vals, analyzer_gs, ): """ Function to gather response data, in preparation for linear regression. This data is organized into `response_dict`. """ # perform magnetic ordering analysis analyzer_output = CollinearMagneticStructureAnalyzer(struct_final, threshold=0.61) magnet_order = analyzer_output.ordering.value if rkey == keys[0]: # store ground state ordering magnet_order_gs = magnet_order # check if ordering matches ground state configuration if analyzer_gs: if not analyzer_gs.matches_ordering(struct_final): use_calc = False calcs_skipped.append({ 'ICHARG': incar_dict.get("ICHARG", 0), 'ISPIN': incar_dict.get("ISPIN", 1), 'LDAUU': incar_dict['LDAUU'].copy(), 'LDAUJ': incar_dict['LDAUJ'].copy() }) for i in range(num_perturb_sites): specie = struct_final[i].specie ldaul = ldaul_vals[i] orbital = inv_block_dict[str(ldaul)] perturb_dict.update( {"site" + str(i): { "specie": str(specie), "orbital": orbital }}) # Obtain occupancy values n_tot = float(outcar_dict['charge'][i][orbital]) # FIXME: Adapt for noncollinear m_z = float((outcar_dict['magnetization'][i][orbital])) n_up = 0.5 * (n_tot + m_z) n_dn = 0.5 * (n_tot - m_z) v_up = float(incar_dict['LDAUU'][i]) v_dn = float(incar_dict['LDAUJ'][i]) response_dict[rkey]['site' + str(i)]['Nup'].append(n_up) response_dict[rkey]['site' + str(i)]['Ndn'].append(n_dn) response_dict[rkey]['site' + str(i)]['Ntot'].append(n_tot) response_dict[rkey]['site' + str(i)]['Mz'].append(m_z) response_dict[rkey]['site' + str(i)]['Vup'].append(v_up) response_dict[rkey]['site' + str(i)]['Vdn'].append(v_dn) response_dict[rkey]['magnetic order'].append(magnet_order)
def get_mag_ordering(struc): # helperd function to get a label of the magnetic ordering type return CollinearMagneticStructureAnalyzer(struc).ordering.value
def process_item(self, item): docs, material_dict = item grouped = group_by_material_id(material_dict, docs, 'input_structure') formula = docs[0]['pretty_formula'] if not grouped: formula = Structure.from_dict(list( material_dict.values())[0]).composition.reduced_formula logger.debug("No material match for {}".format(formula)) # For now just do the most recent one that's not failed # TODO: better sorting of docs all_docs = [] for task_id, elastic_docs in grouped.items(): elastic_docs = sorted(elastic_docs, key=lambda x: (x['order'], x['state'], x['completed_at'])) grouped_by_order = { k: list(v) for k, v in groupby(elastic_docs, key=lambda x: x['order']) } soec_docs = grouped_by_order.get(2) toec_docs = grouped_by_order.get(3) if soec_docs: final_doc = soec_docs[-1] else: final_doc = toec_docs[-1] structure = Structure.from_dict(final_doc['optimized_structure']) formula = structure.composition.reduced_formula elements = [s.symbol for s in structure.composition.elements] chemsys = '-'.join(elements) # Issue warning if relaxed structure differs warnings = final_doc.get('warnings') or [] opt = Structure.from_dict(final_doc['optimized_structure']) init = Structure.from_dict(final_doc['input_structure']) # TODO: are these the right params? if not StructureMatcher().fit(init, opt): warnings.append("Inequivalent optimization structure") material_mag = CollinearMagneticStructureAnalyzer( opt).ordering.value material_mag = mag_types[material_mag] if final_doc['magnetic_type'] != material_mag: warnings.append("Elastic magnetic phase is {}".format( final_doc['magnetic_type'])) warnings = warnings or None # Filter for failure and warnings k_vrh = final_doc['k_vrh'] if k_vrh < 0 or k_vrh > 600: state = 'failed' elif warnings is not None: state = 'warning' else: state = 'successful' final_doc.update({"warnings": warnings}) elastic_summary = { 'task_id': task_id, 'all_elastic_fits': elastic_docs, 'elasticity': final_doc, 'spacegroup': init.get_space_group_info()[0], 'magnetic_type': final_doc['magnetic_type'], 'pretty_formula': formula, 'chemsys': chemsys, 'elements': elements, 'last_updated': self.elasticity.lu_field, 'state': state } if toec_docs: # TODO: this should be a bit more refined final_toec_doc = deepcopy(toec_docs[-1]) et_exp = ElasticTensorExpansion.from_voigt( final_toec_doc['elastic_tensor_expansion']) symbol_dict = et_exp[1].zeroed(1e-2).get_symbol_dict() final_toec_doc.update({"symbol_dict": symbol_dict}) set_(elastic_summary, "elasticity.third_order", final_toec_doc) all_docs.append(jsanitize(elastic_summary)) # elastic_summary.update(final_doc) return all_docs
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']) input_struct = Structure.from_dict(opt_task['input']['structure']) # For now, discern order (i.e. TOEC) using parameters from optimization # TODO: figure this out more intelligently diff = get(opt_task, "input.incar.EDIFFG", 0) order = 3 if np.isclose(diff, -0.001) else 2 explicit, derived = process_elastic_calcs(opt_task, defo_tasks) all_calcs = explicit + derived stresses = [c.get("cauchy_stress") for c in all_calcs] pk_stresses = [c.get("pk_stress") for c in all_calcs] strains = [c.get("strain") for c in all_calcs] elastic_doc['calculations'] = all_calcs vstrains = [s.zeroed(0.002).voigt for s in strains] if np.linalg.matrix_rank(vstrains) == 6: if order == 2: et_fit = legacy_fit(strains, stresses) elif order == 3: # Test for TOEC if len(strains) < 70: logger.info("insufficient valid strains for {} TOEC".format( opt_task['formula_pretty'])) return None eq_stress = -0.1 * Stress(opt_task['output']['stress']) # strains = [s.zeroed(0.0001) for s in strains] # et_expansion = pdb_function(ElasticTensorExpansion.from_diff_fit, # strains, pk_stresses, eq_stress=eq_stress, tol=1e-5) et_exp_raw = ElasticTensorExpansion.from_diff_fit( strains, pk_stresses, eq_stress=eq_stress, tol=1e-6) et_exp = et_exp_raw.voigt_symmetrized.convert_to_ieee(opt_struct) et_exp = et_exp.round(1) et_fit = ElasticTensor(et_exp[0]) # Update elastic doc with TOEC stuff tec = et_exp.thermal_expansion_coeff(opt_struct, 300) elastic_doc.update({ "elastic_tensor_expansion": elastic_sanitize(et_exp), "elastic_tensor_expansion_original": elastic_sanitize(et_exp_raw), "thermal_expansion_tensor": tec, "average_linear_thermal_expansion": np.trace(tec) / 3 }) et = et_fit.voigt_symmetrized.convert_to_ieee(opt_struct) vasp_input = opt_task['input'] if 'structure' in vasp_input: vasp_input.pop('structure') completed_at = max([d['completed_at'] for d in defo_tasks]) elastic_doc.update({ "optimization_task_id": opt_task['task_id'], "optimization_dir_name": opt_task['dir_name'], "cauchy_stresses": stresses, "strains": strains, "elastic_tensor": elastic_sanitize(et.zeroed(0.01).round(0)), # Convert compliance to 10^-12 Pa "compliance_tensor": elastic_sanitize(et.compliance_tensor * 1000), "elastic_tensor_original": elastic_sanitize(et_fit), "optimized_structure": opt_struct, "spacegroup": input_struct.get_space_group_info()[0], "input_structure": input_struct, "completed_at": completed_at, "optimization_input": vasp_input, "order": order, "pretty_formula": opt_struct.composition.reduced_formula }) # Add magnetic type mag = CollinearMagneticStructureAnalyzer(opt_struct).ordering.value elastic_doc['magnetic_type'] = mag_types[mag] try: prop_dict = et.get_structure_property_dict(opt_struct) prop_dict.pop('structure') except ValueError: logger.debug("Negative K or G found, structure property " "dict not computed") prop_dict = et.property_dict for k, v in prop_dict.items(): if k in ['homogeneous_poisson', 'universal_anisotropy']: prop_dict[k] = np.round(v, 2) else: prop_dict[k] = np.round(v, 0) elastic_doc.update(prop_dict) # Update with state and warnings state, warnings = get_state_and_warnings(elastic_doc) elastic_doc.update({"state": state, "warnings": warnings}) # TODO: add kpoints params? return elastic_doc else: logger.info("insufficient valid strains for {}".format( opt_task['formula_pretty'])) return None
def group_by_material_id(materials_dict, docs, structure_key='structure', tol=1e-6, loosen=True, structure_matcher=None): """ Groups a collection of documents by material id as found in a materials collection Args: materials_dict (dict): dictionary of structures keyed by task_id docs ([dict]): list of documents tol (float): tolerance for lattice grouping loosen (bool): whether or not to loosen criteria if no matches are found structure_key (string): mongo-style key of documents where structures are contained (e. g. input.structure or output.structure) structure_matcher (StructureMatcher): structure matcher for finding equivalent structures Returns: documents grouped by task_id from the materials collection """ # Structify all input structures materials_dict = { mp_id: Structure.from_dict(struct) for mp_id, struct in materials_dict.items() } # Get magnetic phases mags = {} # TODO: refactor this with data from materials collection? for mp_id, structure in materials_dict.items(): mag = CollinearMagneticStructureAnalyzer(structure).ordering.value mags[mp_id] = mag_types[mag] docs_by_mp_id = {} for doc in docs: sm = structure_matcher or StructureMatcher( comparator=ElementComparator()) structure = Structure.from_dict(get(doc, structure_key)) input_sg_symbol = SpacegroupAnalyzer(structure, 0.1).get_space_group_symbol() # Iterate over all candidates until match is found matches = { c_id: candidate for c_id, candidate in materials_dict.items() if sm.fit(candidate, structure) } niter = 0 if not matches: # First try with conventional structure then loosen match criteria convs = { c_id: SpacegroupAnalyzer(candidate, 0.1).get_conventional_standard_structure() for c_id, candidate in materials_dict.items() } matches = { c_id: candidate for c_id, candidate in materials_dict.items() if sm.fit(convs[c_id], structure) } while len(matches) < 1 and niter < 4 and loosen: logger.debug("Loosening sm criteria") sm = StructureMatcher(sm.ltol * 2, sm.stol * 2, sm.angle_tol * 2, primitive_cell=False) matches = { c_id: candidate for c_id, candidate in materials_dict.items() if sm.fit(convs[c_id], structure) } niter += 1 if matches: # Get best match by spacegroup, then mag phase, then closest density mag = doc['magnetic_type'] def sort_criteria(m_id): dens_diff = abs(matches[m_id].density - structure.density) sg = matches[m_id].get_space_group_info(0.1)[0] mag_id = mags[m_id] # prefer explicit matches, allow non-mag materials match with FM tensors if mag_id == mag: mag_match = 0 elif mag_id == 'Non-magnetic' and mag == 'FM': mag_match = 1 else: mag_match = 2 return (sg != input_sg_symbol, mag_match, dens_diff) sorted_ids = sorted(list(matches.keys()), key=sort_criteria) mp_id = sorted_ids[0] if mp_id in docs_by_mp_id: docs_by_mp_id[mp_id].append(doc) else: docs_by_mp_id[mp_id] = [doc] else: logger.debug("No material match found for formula {}".format( structure.composition.reduced_formula)) return docs_by_mp_id
def test_matches(self): self.assertTrue(self.NiO.matches(self.NiO_AFM_111)) self.assertTrue(self.NiO.matches(self.NiO_AFM_001)) # MSA adds magmoms to Structure, so not equal msa = CollinearMagneticStructureAnalyzer( self.NiO, overwrite_magmom_mode="replace_all") self.assertFalse(msa.matches_ordering(self.NiO)) self.assertFalse(msa.matches_ordering(self.NiO_AFM_111)) self.assertFalse(msa.matches_ordering(self.NiO_AFM_001)) msa = CollinearMagneticStructureAnalyzer( self.NiO_AFM_001, overwrite_magmom_mode="respect_sign") self.assertFalse(msa.matches_ordering(self.NiO)) self.assertFalse(msa.matches_ordering(self.NiO_AFM_111)) self.assertTrue(msa.matches_ordering(self.NiO_AFM_001)) self.assertTrue(msa.matches_ordering(self.NiO_AFM_001_opposite)) msa = CollinearMagneticStructureAnalyzer( self.NiO_AFM_111, overwrite_magmom_mode="respect_sign") self.assertFalse(msa.matches_ordering(self.NiO)) self.assertTrue(msa.matches_ordering(self.NiO_AFM_111)) self.assertFalse(msa.matches_ordering(self.NiO_AFM_001)) self.assertFalse(msa.matches_ordering(self.NiO_AFM_001_opposite))