def generate_doc(self, dir_name, vasprun_files, outcar_files): """ Adapted from matgendb.creator.generate_doc """ try: # basic properties, incl. calcs_reversed and run_stats fullpath = os.path.abspath(dir_name) d = {k: v for k, v in self.additional_fields.items()} d["schema"] = {"code": "atomate", "version": VaspDrone.__version__} d["dir_name"] = fullpath d["calcs_reversed"] = [self.process_vasprun(dir_name, taskname, filename) for taskname, filename in vasprun_files.items()] outcar_data = [Outcar(os.path.join(dir_name, filename)).as_dict() for taskname, filename in outcar_files.items()] run_stats = {} for i, d_calc in enumerate(d["calcs_reversed"]): run_stats[d_calc["task"]["name"]] = outcar_data[i].pop("run_stats") if d_calc.get("output"): d_calc["output"].update({"outcar": outcar_data[i]}) else: d_calc["output"] = {"outcar": outcar_data[i]} try: overall_run_stats = {} for key in ["Total CPU time used (sec)", "User time (sec)", "System time (sec)", "Elapsed time (sec)"]: overall_run_stats[key] = sum([v[key] for v in run_stats.values()]) run_stats["overall"] = overall_run_stats except: logger.error("Bad run stats for {}.".format(fullpath)) d["run_stats"] = run_stats # reverse the calculations data order so newest calc is first d["calcs_reversed"].reverse() # set root formula/composition keys based on initial and final calcs d_calc_init = d["calcs_reversed"][-1] d_calc_final = d["calcs_reversed"][0] d["chemsys"] = "-".join(sorted(d_calc_final["elements"])) comp = Composition(d_calc_final["composition_unit_cell"]) d["formula_anonymous"] = comp.anonymized_formula d["formula_reduced_abc"] = comp.reduced_composition.alphabetical_formula for root_key in ["completed_at", "nsites", "composition_unit_cell", "composition_reduced", "formula_pretty", "elements", "nelements"]: d[root_key] = d_calc_final[root_key] # store the input key based on initial calc # store any overrides to the exchange correlation functional xc = d_calc_init["input"]["incar"].get("GGA") if xc: xc = xc.upper() p = d_calc_init["input"]["potcar_type"][0].split("_") pot_type = p[0] functional = "lda" if len(pot_type) == 1 else "_".join(p[1:]) d["input"] = {"structure": d_calc_init["input"]["structure"], "is_hubbard": d_calc_init.pop("is_hubbard"), "hubbards": d_calc_init.pop("hubbards"), "is_lasph": d_calc_init["input"]["incar"].get("LASPH", False), "potcar_spec": d_calc_init["input"].get("potcar_spec"), "xc_override": xc, "pseudo_potential": {"functional": functional.lower(), "pot_type": pot_type.lower(), "labels": d_calc_init["input"]["potcar"]}, "parameters": d_calc_init["input"]["parameters"], "incar": d_calc_init["input"]["incar"] } # store the output key based on final calc d["output"] = { "structure": d_calc_final["output"]["structure"], "density": d_calc_final.pop("density"), "energy": d_calc_final["output"]["energy"], "energy_per_atom": d_calc_final["output"]["energy_per_atom"], "forces": d_calc_final["output"]["ionic_steps"][-1].get("forces"), "stress": d_calc_final["output"]["ionic_steps"][-1].get("stress")} # patch calculated magnetic moments into final structure if len(d_calc_final["output"]["outcar"]["magnetization"]) != 0: magmoms = [m["tot"] for m in d_calc_final["output"]["outcar"]["magnetization"]] s = Structure.from_dict(d["output"]["structure"]) s.add_site_property('magmom', magmoms) d["output"]["structure"] = s.as_dict() calc = d["calcs_reversed"][0] try: d["output"].update({"bandgap": calc["output"]["bandgap"], "cbm": calc["output"]["cbm"], "vbm": calc["output"]["vbm"], "is_gap_direct": calc["output"]["is_gap_direct"], "is_metal": calc["output"]["is_metal"]}) if not calc["output"]["is_gap_direct"]: d["output"]["direct_gap"] = calc["output"]["direct_gap"] if "transition" in calc["output"]: d["output"]["transition"] = calc["output"]["transition"] except Exception: if self.bandstructure_mode is True: import traceback logger.error(traceback.format_exc()) logger.error("Error in " + os.path.abspath(dir_name) + ".\n" + traceback.format_exc()) raise sg = SpacegroupAnalyzer(Structure.from_dict(d_calc_final["output"]["structure"]), 0.1) if not sg.get_symmetry_dataset(): sg = SpacegroupAnalyzer(Structure.from_dict(d_calc_final["output"]["structure"]), 1e-3, 1) d["output"]["spacegroup"] = { "source": "spglib", "symbol": sg.get_space_group_symbol(), "number": sg.get_space_group_number(), "point_group": sg.get_point_group_symbol(), "crystal_system": sg.get_crystal_system(), "hall": sg.get_hall()} if d["input"]["parameters"].get("LEPSILON"): for k in ['epsilon_static', 'epsilon_static_wolfe', 'epsilon_ionic']: d["output"][k] = d_calc_final["output"][k] if SymmOp.inversion() not in sg.get_symmetry_operations(): for k in ["piezo_ionic_tensor", "piezo_tensor"]: d["output"][k] = d_calc_final["output"]["outcar"][k] d["state"] = "successful" if d_calc["has_vasp_completed"] else "unsuccessful" self.set_analysis(d) d["last_updated"] = datetime.datetime.today() return d except Exception: import traceback logger.error(traceback.format_exc()) logger.error("Error in " + os.path.abspath(dir_name) + ".\n" + traceback.format_exc()) raise
class PointGroupAnalyzer(object): """ A class to analyze the point group of a molecule. The general outline of the algorithm is as follows: 1. Center the molecule around its center of mass. 2. Compute the inertia tensor and the eigenvalues and eigenvectors. 3. Handle the symmetry detection based on eigenvalues. a. Linear molecules have one zero eigenvalue. Possible symmetry operations are C*v or D*v b. Asymetric top molecules have all different eigenvalues. The maximum rotational symmetry in such molecules is 2 c. Symmetric top molecules have 1 unique eigenvalue, which gives a unique rotation axis. All axial point groups are possible except the cubic groups (T & O) and I. d. Spherical top molecules have all three eigenvalues equal. They have the rare T, O or I point groups. .. attribute:: sch_symbol Schoenflies symbol of the detected point group. """ inversion_op = SymmOp.inversion() def __init__(self, mol, tolerance=0.3, eigen_tolerance=0.01, matrix_tol=0.1): """ The default settings are usually sufficient. Args: mol (Molecule): Molecule to determine point group for. tolerance (float): Distance tolerance to consider sites as symmetrically equivalent. Defaults to 0.3 Angstrom. eigen_tolerance (float): Tolerance to compare eigen values of the inertia tensor. Defaults to 0.01. matrix_tol (float): Tolerance used to generate the full set of symmetry operations of the point group. """ self.mol = mol self.centered_mol = mol.get_centered_molecule() self.tol = tolerance self.eig_tol = eigen_tolerance self.mat_tol = matrix_tol self._analyze() def _analyze(self): if len(self.centered_mol) == 1: self.sch_symbol = "Kh" else: inertia_tensor = np.zeros((3, 3)) total_inertia = 0 for site in self.mol: c = site.coords wt = site.species_and_occu.weight for i in range(3): inertia_tensor[i, i] += wt * (c[(i + 1) % 3]**2 + c[(i + 2) % 3]**2) for i, j in itertools.combinations(list(range(3)), 2): inertia_tensor[i, j] += -wt * c[i] * c[j] inertia_tensor[j, i] += -wt * c[j] * c[i] total_inertia += wt * np.dot(c, c) # Normalize the inertia tensor so that it does not scale with size # of the system. This mitigates the problem of choosing a proper # comparison tolerance for the eigenvalues. inertia_tensor /= total_inertia eigvals, eigvecs = np.linalg.eig(inertia_tensor) self.principal_axes = eigvecs.T self.eigvals = eigvals v1, v2, v3 = eigvals eig_zero = abs(v1 * v2 * v3) < self.eig_tol**3 eig_all_same = abs(v1 - v2) < self.eig_tol and abs( v1 - v3) < self.eig_tol eig_all_diff = abs(v1 - v2) > self.eig_tol and abs( v1 - v3) > self.eig_tol and abs(v2 - v3) > self.eig_tol self.rot_sym = [] self.symmops = [SymmOp(np.eye(4))] if eig_zero: logger.debug("Linear molecule detected") self._proc_linear() elif eig_all_same: logger.debug("Spherical top molecule detected") self._proc_sph_top() elif eig_all_diff: logger.debug("Asymmetric top molecule detected") self._proc_asym_top() else: logger.debug("Symmetric top molecule detected") self._proc_sym_top() def _proc_linear(self): if self.is_valid_op(PointGroupAnalyzer.inversion_op): self.sch_symbol = "D*h" self.symmops.append(PointGroupAnalyzer.inversion_op) else: self.sch_symbol = "C*v" def _proc_asym_top(self): """ Handles assymetric top molecules, which cannot contain rotational symmetry larger than 2. """ self._check_R2_axes_asym() if len(self.rot_sym) == 0: logger.debug("No rotation symmetries detected.") self._proc_no_rot_sym() elif len(self.rot_sym) == 3: logger.debug("Dihedral group detected.") self._proc_dihedral() else: logger.debug("Cyclic group detected.") self._proc_cyclic() def _proc_sym_top(self): """ Handles symetric top molecules which has one unique eigenvalue whose corresponding principal axis is a unique rotational axis. More complex handling required to look for R2 axes perpendicular to this unique axis. """ if abs(self.eigvals[0] - self.eigvals[1]) < self.eig_tol: ind = 2 elif abs(self.eigvals[1] - self.eigvals[2]) < self.eig_tol: ind = 0 else: ind = 1 unique_axis = self.principal_axes[ind] self._check_rot_sym(unique_axis) if len(self.rot_sym) > 0: self._check_perpendicular_r2_axis(unique_axis) if len(self.rot_sym) >= 2: self._proc_dihedral() elif len(self.rot_sym) == 1: self._proc_cyclic() else: self._proc_no_rot_sym() def _proc_no_rot_sym(self): """ Handles molecules with no rotational symmetry. Only possible point groups are C1, Cs and Ci. """ self.sch_symbol = "C1" if self.is_valid_op(PointGroupAnalyzer.inversion_op): self.sch_symbol = "Ci" self.symmops.append(PointGroupAnalyzer.inversion_op) else: for v in self.principal_axes: mirror_type = self._find_mirror(v) if not mirror_type == "": self.sch_symbol = "Cs" break def _proc_cyclic(self): """ Handles cyclic group molecules. """ main_axis, rot = max(self.rot_sym, key=lambda v: v[1]) self.sch_symbol = "C{}".format(rot) mirror_type = self._find_mirror(main_axis) if mirror_type == "h": self.sch_symbol += "h" elif mirror_type == "v": self.sch_symbol += "v" elif mirror_type == "": if self.is_valid_op( SymmOp.rotoreflection(main_axis, angle=180 / rot)): self.sch_symbol = "S{}".format(2 * rot) def _proc_dihedral(self): """ Handles dihedral group molecules, i.e those with intersecting R2 axes and a main axis. """ main_axis, rot = max(self.rot_sym, key=lambda v: v[1]) self.sch_symbol = "D{}".format(rot) mirror_type = self._find_mirror(main_axis) if mirror_type == "h": self.sch_symbol += "h" elif not mirror_type == "": self.sch_symbol += "d" def _check_R2_axes_asym(self): """ Test for 2-fold rotation along the principal axes. Used to handle asymetric top molecules. """ for v in self.principal_axes: op = SymmOp.from_axis_angle_and_translation(v, 180) if self.is_valid_op(op): self.symmops.append(op) self.rot_sym.append((v, 2)) def _find_mirror(self, axis): """ Looks for mirror symmetry of specified type about axis. Possible types are "h" or "vd". Horizontal (h) mirrors are perpendicular to the axis while vertical (v) or diagonal (d) mirrors are parallel. v mirrors has atoms lying on the mirror plane while d mirrors do not. """ mirror_type = "" # First test whether the axis itself is the normal to a mirror plane. if self.is_valid_op(SymmOp.reflection(axis)): self.symmops.append(SymmOp.reflection(axis)) mirror_type = "h" else: # Iterate through all pairs of atoms to find mirror for s1, s2 in itertools.combinations(self.centered_mol, 2): if s1.species_and_occu == s2.species_and_occu: normal = s1.coords - s2.coords if np.dot(normal, axis) < self.tol: op = SymmOp.reflection(normal) if self.is_valid_op(op): self.symmops.append(op) if len(self.rot_sym) > 1: mirror_type = "d" for v, r in self.rot_sym: if not np.linalg.norm(v - axis) < self.tol: if np.dot(v, normal) < self.tol: mirror_type = "v" break else: mirror_type = "v" break return mirror_type def _get_smallest_set_not_on_axis(self, axis): """ Returns the smallest list of atoms with the same species and distance from origin AND does not lie on the specified axis. This maximal set limits the possible rotational symmetry operations, since atoms lying on a test axis is irrelevant in testing rotational symmetryOperations. """ def not_on_axis(site): v = np.cross(site.coords, axis) return np.linalg.norm(v) > self.tol valid_sets = [] origin_site, dist_el_sites = cluster_sites(self.centered_mol, self.tol) for test_set in dist_el_sites.values(): valid_set = list(filter(not_on_axis, test_set)) if len(valid_set) > 0: valid_sets.append(valid_set) return min(valid_sets, key=lambda s: len(s)) def _check_rot_sym(self, axis): """ Determines the rotational symmetry about supplied axis. Used only for symmetric top molecules which has possible rotational symmetry operations > 2. """ min_set = self._get_smallest_set_not_on_axis(axis) max_sym = len(min_set) for i in range(max_sym, 0, -1): if max_sym % i != 0: continue op = SymmOp.from_axis_angle_and_translation(axis, 360 / i) rotvalid = self.is_valid_op(op) if rotvalid: self.symmops.append(op) self.rot_sym.append((axis, i)) return i return 1 def _check_perpendicular_r2_axis(self, axis): """ Checks for R2 axes perpendicular to unique axis. For handling symmetric top molecules. """ min_set = self._get_smallest_set_not_on_axis(axis) for s1, s2 in itertools.combinations(min_set, 2): test_axis = np.cross(s1.coords - s2.coords, axis) if np.linalg.norm(test_axis) > self.tol: op = SymmOp.from_axis_angle_and_translation(test_axis, 180) r2present = self.is_valid_op(op) if r2present: self.symmops.append(op) self.rot_sym.append((test_axis, 2)) return True def _proc_sph_top(self): """ Handles Sperhical Top Molecules, which belongs to the T, O or I point groups. """ self._find_spherical_axes() if len(self.rot_sym) == 0: logger.debug("Accidental speherical top!") self._proc_sym_top() main_axis, rot = max(self.rot_sym, key=lambda v: v[1]) if rot < 3: logger.debug("Accidental speherical top!") self._proc_sym_top() elif rot == 3: mirror_type = self._find_mirror(main_axis) if mirror_type != "": if self.is_valid_op(PointGroupAnalyzer.inversion_op): self.symmops.append(PointGroupAnalyzer.inversion_op) self.sch_symbol = "Th" else: self.sch_symbol = "Td" else: self.sch_symbol = "T" elif rot == 4: if self.is_valid_op(PointGroupAnalyzer.inversion_op): self.symmops.append(PointGroupAnalyzer.inversion_op) self.sch_symbol = "Oh" else: self.sch_symbol = "O" elif rot == 5: if self.is_valid_op(PointGroupAnalyzer.inversion_op): self.symmops.append(PointGroupAnalyzer.inversion_op) self.sch_symbol = "Ih" else: self.sch_symbol = "I" def _find_spherical_axes(self): """ Looks for R5, R4, R3 and R2 axes in speherical top molecules. Point group T molecules have only one unique 3-fold and one unique 2-fold axis. O molecules have one unique 4, 3 and 2-fold axes. I molecules have a unique 5-fold axis. """ rot_present = defaultdict(bool) origin_site, dist_el_sites = cluster_sites(self.centered_mol, self.tol) test_set = min(dist_el_sites.values(), key=lambda s: len(s)) coords = [s.coords for s in test_set] for c1, c2, c3 in itertools.combinations(coords, 3): for cc1, cc2 in itertools.combinations([c1, c2, c3], 2): if not rot_present[2]: test_axis = cc1 + cc2 if np.linalg.norm(test_axis) > self.tol: op = SymmOp.from_axis_angle_and_translation( test_axis, 180) rot_present[2] = self.is_valid_op(op) if rot_present[2]: self.symmops.append(op) self.rot_sym.append((test_axis, 2)) test_axis = np.cross(c2 - c1, c3 - c1) if np.linalg.norm(test_axis) > self.tol: for r in (3, 4, 5): if not rot_present[r]: op = SymmOp.from_axis_angle_and_translation( test_axis, 360 / r) rot_present[r] = self.is_valid_op(op) if rot_present[r]: self.symmops.append(op) self.rot_sym.append((test_axis, r)) break if rot_present[2] and rot_present[3] and (rot_present[4] or rot_present[5]): break def get_pointgroup(self): """ Returns a PointGroup object for the molecule. """ return PointGroupOperations(self.sch_symbol, self.symmops, self.mat_tol) def is_valid_op(self, symmop): """ Check if a particular symmetry operation is a valid symmetry operation for a molecule, i.e., the operation maps all atoms to another equivalent atom. Args: symmop (SymmOp): Symmetry operation to test. Returns: (bool): Whether SymmOp is valid for Molecule. """ coords = self.centered_mol.cart_coords for site in self.centered_mol: coord = symmop.operate(site.coords) ind = find_in_coord_list(coords, coord, self.tol) if not (len(ind) == 1 and self.centered_mol[ind[0]].species_and_occu == site.species_and_occu): return False return True
def test_inversion(self): origin=np.random.rand(3) op=SymmOp.inversion(origin) pt=np.random.rand(3) inv_pt=op.operate(pt) self.assertArrayAlmostEqual(pt - origin, origin - inv_pt)
def test_inversion(self): origin = np.random.rand(3) op = SymmOp.inversion(origin) pt = np.random.rand(3) inv_pt = op.operate(pt) self.assertArrayAlmostEqual(pt - origin, origin - inv_pt)
def is_centro(structure): sga = SpacegroupAnalyzer(structure) return SymmOp.inversion() in sga.get_symmetry_operations()