def __init__(self, sch_symbol, operations, tolerance=0.1): from pymatgen.symmetry.analyzer import generate_full_symmops self.sch_symbol = sch_symbol super(PointGroupOperations, self).__init__([ op.rotation_matrix for op in generate_full_symmops(operations, tolerance) ])
def get_symmetry(mol, already_oriented=False): ''' Return a list of SymmOps for a molecule's point symmetry already_oriented: whether or not the principle axes of mol are already reoriented ''' pga = PointGroupAnalyzer(mol) #Handle linear molecules if '*' in pga.sch_symbol: if already_oriented == False: #Reorient the molecule oriented_mol, P = reoriented_molecule(mol) pga = PointGroupAnalyzer(oriented_mol) pg = pga.get_pointgroup() symm_m = [] for op in pg: symm_m.append(op) #Add 12-fold and reflections in place of ininitesimal rotation for axis in [[1, 0, 0], [0, 1, 0], [0, 0, 1]]: op = SymmOp.from_rotation_and_translation(aa2matrix(axis, pi / 6), [0, 0, 0]) if pga.is_valid_op(op): symm_m.append(op) #Any molecule with infinitesimal symmetry is linear; #Thus, it possess mirror symmetry for any axis perpendicular #To the rotational axis. pymatgen does not add this symmetry #for all linear molecules - for example, hydrogen if axis == [1, 0, 0]: symm_m.append(SymmOp.from_xyz_string('x,-y,z')) symm_m.append(SymmOp.from_xyz_string('x,y,-z')) elif axis == [0, 1, 0]: symm_m.append(SymmOp.from_xyz_string('-x,y,z')) symm_m.append(SymmOp.from_xyz_string('x,y,-z')) elif axis == [0, 0, 1]: symm_m.append(SymmOp.from_xyz_string('-x,y,z')) symm_m.append(SymmOp.from_xyz_string('x,-y,z')) #Generate a full list of SymmOps for the molecule's pointgroup symm_m = generate_full_symmops(symm_m, 1e-3) break #Reorient the SymmOps into mol's original frame if not already_oriented: new = [] for op in symm_m: new.append(P.inverse * op * P) return new elif already_oriented: return symm_m #Handle nonlinear molecules else: pg = pga.get_pointgroup() symm_m = [] for op in pg: symm_m.append(op) return symm_m
def mol_get_rot_list0(mol, tol=1.e-5, log=sys.stdout): from pymatgen.symmetry.analyzer import PointGroupAnalyzer,\ generate_full_symmops from scipy.linalg import det analyzer = PointGroupAnalyzer(mol) print(" sch_symbol = {}".format(analyzer.sch_symbol), file=log) # Pick rotations only. symmops = [ o for o in analyzer.symmops if abs(det(o.rotation_matrix) - 1) < 1.e-5 ] symmops = generate_full_symmops(symmops, tol, max_recursion_depth=50) rot_list = [o.rotation_matrix for o in symmops] return rot_list
def get_symmetry(mol, already_oriented=False): """ Return a molecule's point symmetry. Note: for linear molecules, infinitessimal rotations are treated as 6-fold rotations, which works for 3d and 2d point groups. Args: mol: a Molecule object already_oriented: whether or not the principle axes of mol are already reoriented. Can save time if True, but is not required. Returns: a list of SymmOp objects which leave the molecule unchanged when applied """ # For single atoms, we cannot represent the point group using a list of operations if len(mol) == 1: return [] pga = PointGroupAnalyzer(mol) # Handle linear molecules if "*" in pga.sch_symbol: if not already_oriented: # Reorient the molecule oriented_mol, P = reoriented_molecule(mol) pga = PointGroupAnalyzer(oriented_mol) pg = pga.get_pointgroup() symm_m = [] for op in pg: symm_m.append(op) # Add 12-fold and reflections in place of ininitesimal rotation for axis in [[1, 0, 0], [0, 1, 0], [0, 0, 1]]: # op = SymmOp.from_rotation_and_translation(aa2matrix(axis, np.pi/6), [0,0,0]) m1 = Rotation.from_rotvec(np.pi / 6 * axis).as_matrix() op = SymmOp.from_rotation_and_translation(m1, [0, 0, 0]) if pga.is_valid_op(op): symm_m.append(op) # Any molecule with infinitesimal symmetry is linear; # Thus, it possess mirror symmetry for any axis perpendicular # To the rotational axis. pymatgen does not add this symmetry # for all linear molecules - for example, hydrogen if axis == [1, 0, 0]: symm_m.append(SymmOp.from_xyz_string("x,-y,z")) symm_m.append(SymmOp.from_xyz_string("x,y,-z")) #r = SymmOp.from_xyz_string("-x,y,-z") elif axis == [0, 1, 0]: symm_m.append(SymmOp.from_xyz_string("-x,y,z")) symm_m.append(SymmOp.from_xyz_string("x,y,-z")) #r = SymmOp.from_xyz_string("-x,-y,z") elif axis == [0, 0, 1]: symm_m.append(SymmOp.from_xyz_string("-x,y,z")) symm_m.append(SymmOp.from_xyz_string("x,-y,z")) #r = SymmOp.from_xyz_string("x,-y,-z") # Generate a full list of SymmOps for the molecule's pointgroup symm_m = generate_full_symmops(symm_m, 1e-3) break # Reorient the SymmOps into mol's original frame if not already_oriented: new = [] for op in symm_m: new.append(P.inverse * op * P) return new elif already_oriented: return symm_m # Handle nonlinear molecules else: pg = pga.get_pointgroup() symm_m = [] for op in pg: symm_m.append(op) return symm_m
Returns whether or not two operations are conjugate (the same operation in a different reference frame). Rotations with the same order will not always return True. For example, a 5/12 and 1/12 rotation will not be considered conjugate. Args: op1: a SymmOp or OperationAnalyzer object op2: a SymmOp or OperationAnalyzer object to compare with op1 Returns: True if op2 is conjugate to op1, and False otherwise """ if type(op1) != OperationAnalyzer: opa1 = OperationAnalyzer(op1) return opa1.is_conjugate(op2) # Test Functionality if __name__ == "__main__": # ---------------------------------------------------- op = SymmOp.from_rotation_and_translation( Rotation.from_rotvec(np.pi / 6 * np.array([1, 0, 0])).as_matrix(), [0, 0, 0]) ops = [op] from pymatgen.symmetry.analyzer import generate_full_symmops symm_m = generate_full_symmops(ops, 1e-3) for op in symm_m: opa = OperationAnalyzer(op) print(opa.order)
def ss_string_from_ops(ops, sg, complete=True): ''' Print the Hermann-Mauguin symbol for a site symmetry group, using a list of SymmOps as input. Note that the symbol does not necessarily refer to the x,y,z axes. For information on reading these symbols, see: http://en.wikipedia.org/wiki/Hermann-Mauguin_notation#Point_groups args: ops: a list of SymmOp objects representing the site symmetry sg: International number of the spacegroup. Used to determine which axes to show. For example, a 3-fold rotation in a cubic system is written as ".3.", whereas a 3-fold rotation in a trigonal system is written as "3.." complete: whether or not all symmetry operations in the group are present. If False, we generate the rest ''' #Return the symbol for a single axis #Will be called later in the function def get_symbol(opas, order, has_reflection): #ops: a list of Symmetry operations about the axis #order: highest order of any symmetry operation about the axis #has_reflection: whether or not the axis has mirror symmetry if has_reflection is True: #rotations have priority for opa in opas: if opa.order == order and opa.type == "rotation": return str(opa.rotation_order) + "/m" for opa in opas: if (opa.order == order and opa.type == "rotoinversion" and opa.order != 2): return "-" + str(opa.rotation_order) return "m" elif has_reflection is False: #rotoinversion has priority for opa in opas: if opa.order == order and opa.type == "rotoinversion": return "-" + str(opa.rotation_order) for opa in opas: if opa.order == order and opa.type == "rotation": return str(opa.rotation_order) return "." #Given a list of single-axis symbols, return the one with highest symmetry #Will be called later in the function def get_highest_symbol(symbols): symbol_list = [ '.', '2', 'm', '-2', '2/m', '3', '4', '-4', '4/m', '-3', '6', '-6', '6/m' ] max_index = 0 for symbol in symbols: i = symbol_list.index(symbol) if i > max_index: max_index = i return symbol_list[max_index] #Return whether or not two axes are symmetrically equivalent #It is assumed that both axes possess the same symbol #Will be called within combine_axes def are_symmetrically_equivalent(index1, index2): axis1 = axes[index1] axis2 = axes[index2] condition1 = False condition2 = False #Check for an operation mapping one axis onto the other for op in ops: if condition1 is False or condition2 is False: new1 = op.operate(axis1) new2 = op.operate(axis2) if np.isclose(abs(np.dot(new1, axis2)), 1): condition1 = True if np.isclose(abs(np.dot(new2, axis1)), 1): condition2 = True if condition1 is True and condition2 is True: return True else: return False #Given a list of axis indices, return the combined symbol #Axes may or may not be symmetrically equivalent, but must be of the same #type (x/y/z, face-diagonal, body-diagonal) #Will be called for mid- and high-symmetry crystallographic point groups def combine_axes(indices): symbols = {} for index in deepcopy(indices): symbol = get_symbol(params[index], orders[index], reflections[index]) if symbol == ".": indices.remove(index) else: symbols[index] = symbol if indices == []: return "." #Remove redundant axes for i in deepcopy(indices): for j in deepcopy(indices): if j > i: if symbols[i] == symbols[j]: if are_symmetrically_equivalent(i, j): if j in indices: indices.remove(j) #Combine symbols for non-equivalent axes new_symbols = [] for i in indices: new_symbols.append(symbols[i]) symbol = "" while new_symbols != []: highest = get_highest_symbol(new_symbols) symbol += highest new_symbols.remove(highest) if symbol == "": print("Error: could not combine site symmetry axes.") return else: return symbol #Generate needed ops if complete is False: ops = generate_full_symmops(ops, 1e-3) #Get OperationAnalyzer object for all ops opas = [] for op in ops: opas.append(OperationAnalyzer(op)) #Store the symmetry of each axis params = [[], [], [], [], [], [], [], [], [], [], [], [], []] has_inversion = False #Store possible symmetry axes for crystallographic point groups axes = [[1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 1, 0], [0, 1, 1], [1, 0, 1], [1, -1, 0], [0, 1, -1], [1, 0, -1], [1, 1, 1], [-1, 1, 1], [1, -1, 1], [1, 1, -1]] for i, axis in enumerate(axes): axes[i] = axis / np.linalg.norm(axis) for opa in opas: if opa.type != "identity" and opa.type != "inversion": found = False for i, axis in enumerate(axes): if np.isclose(abs(np.dot(opa.axis, axis)), 1): found = True params[i].append(opa) #Store uncommon axes for trigonal and hexagonal lattices if found is False: axes.append(opa.axis) #Check that new axis is not symmetrically equivalent to others unique = True for i, axis in enumerate(axes): if i != len(axes) - 1: if are_symmetrically_equivalent(i, len(axes) - 1): unique = False if unique is True: params.append([opa]) elif unique is False: axes.pop() elif opa.type == "inversion": has_inversion = True #Determine how many high-symmetry axes are present n_axes = 0 #Store the order of each axis orders = [] #Store whether or not each axis has reflection symmetry reflections = [] for axis in params: order = 1 high_symm = False has_reflection = False for opa in axis: if opa.order >= 3: high_symm = True if opa.order > order: order = opa.order if opa.order == 2 and opa.type == "rotoinversion": has_reflection = True orders.append(order) if high_symm == True: n_axes += 1 reflections.append(has_reflection) #Triclinic, monoclinic, orthorhombic #Positions in symbol refer to x,y,z axes respectively if sg >= 1 and sg <= 74: symbol = (get_symbol(params[0], orders[0], reflections[0]) + get_symbol(params[1], orders[1], reflections[1]) + get_symbol(params[2], orders[2], reflections[2])) if symbol != "...": return symbol elif symbol == "...": if has_inversion is True: return "-1" else: return "1" #Trigonal, Hexagonal, Tetragonal elif sg >= 75 and sg <= 194: #1st symbol: z axis s1 = get_symbol(params[2], orders[2], reflections[2]) #2nd symbol: x or y axes (whichever have higher symmetry) s2 = combine_axes([0, 1]) #3rd symbol: face-diagonal axes (whichever have highest symmetry) s3 = combine_axes(list(range(3, len(axes)))) symbol = s1 + " " + s2 + " " + s3 if symbol != ". . .": return symbol elif symbol == ". . .": if has_inversion is True: return "-1" else: return "1" #Cubic elif sg >= 195 and sg <= 230: pass #1st symbol: x, y, and/or z axes (whichever have highest symmetry) s1 = combine_axes([0, 1, 2]) #2nd symbol: body-diagonal axes (whichever has highest symmetry) s2 = combine_axes([9, 10, 11, 12]) #3rd symbol: face-diagonal axes (whichever have highest symmetry) s3 = combine_axes([3, 4, 5, 6, 7, 8]) symbol = s1 + " " + s2 + " " + s3 if symbol != ". . .": return symbol elif symbol == ". . .": if has_inversion is True: return "-1" else: return "1" else: print("Error: invalid spacegroup number") return
from __future__ import print_function import unittest import pickle with open('mol.pickle', 'rb') as f: mol = pickle.load(f) from pymatgen.symmetry.analyzer import PointGroupAnalyzer analyzer = PointGroupAnalyzer(mol) print(" sch_symbol = ", analyzer.sch_symbol) from pymatgen.symmetry.analyzer import generate_full_symmops symmops = generate_full_symmops(analyzer.symmops, tol=1.e-7) print(" num symmops = ", len(symmops)) class KnowValues(unittest.TestCase): def test_PointGroupAnalyzer(self): self.assertTrue(analyzer.sch_symbol == 'D4h') self.assertTrue(len(symmops) == 16) if __name__ == "__main__": print("Tests for pymatgen.") unittest.main()
def orientation_in_wyckoff_position(mol, sg, index, randomize=True, exact_orientation=False, already_oriented=False): ''' Tests if a molecule meets the symmetry requirements of a Wyckoff position. If it does, return the rotation matrix needed. Otherwise, returns False. args: mol: pymatgen.core.structure.Molecule object. Orientation is arbitrary sg: the spacegroup to check index: the index of the Wyckoff position within the sg to check randomize: whether or not to apply a random rotation consistent with the symmetry requirements. exact_orientation: whether to only check compatibility for the provided orientation of the molecule. Used within general case for checking. If True, this function only returns True or False ''' #Obtain the Wyckoff symmetry symm_w_unedited = get_wyckoff_symmetry(sg)[index][0] found_params = [] symm_w_partial = [] symm_w = [] orthogonal = True #Check if operations are orthogonal; 3-fold and 6-fold operations are not for op in symm_w_unedited: m1 = np.dot(op.rotation_matrix, np.transpose(op.rotation_matrix)) m2 = np.dot(np.transpose(op.rotation_matrix), op.rotation_matrix) if (not allclose(m1, np.identity(3))) or (not allclose( m2, np.identity(3))): rotation_order = 0 d = np.linalg.det(op.rotation_matrix) if isclose(d, 1): op_type = "rotation" elif isclose(d, -1): op_type = "improper rotation" if op_type == "rotation": newop = deepcopy(op) for n in range(1, 7): newop = (newop * op) if allclose(newop.rotation_matrix, np.identity(3), rtol=1e-2): rotation_order = n + 1 break elif op_type == "improper rotation": #We only want the order of the rotational part of op, #So we multiply op by -1 for rotoinversions op_1 = SymmOp.from_rotation_and_translation( op.rotation_matrix * -1, [0, 0, 0]) new_op = deepcopy(op_1) for n in range(1, 7): newop = (newop * op_1) if allclose(newop.rotation_matrix, np.identity(3)): rotation_order = n + 1 break if rotation_order == 0: print("Error: could not convert to orthogonal operation:") print(op) symm_w_partial.append(op) else: params = [rotation_order, op_type] if params not in found_params: found_params.append(params) else: symm_w_partial.append(op) #Add 3-fold and 6-fold symmetry back in using absolute coordinates for params in found_params: order = params[0] op_type = params[1] if op_type == "rotation" and (order == 3 or order == 6): m = aa2matrix([0, 0, 1], (2 * pi) / order) elif op_type == "improper rotation" and (order == 3 or order == 6): m = aa2matrix([0, 0, 1], 2 * pi / order) m[2] *= -1 symm_w_partial.append( SymmOp.from_rotation_and_translation(m, [0, 0, 0])) symm_w = generate_full_symmops(symm_w_partial, 1e-3) #Check exact orientation if exact_orientation is True: mo = deepcopy(mol) pga = PointGroupAnalyzer(mo) valid = True for op in symm_w: op_w = SymmOp.from_rotation_and_translation( op.rotation_matrix, [0, 0, 0]) if not pga.is_valid_op(op_w): valid = False if valid is True: return True elif valid is False: return False #Obtain molecular symmetry, exact_orientation==False symm_m = get_symmetry(mol, already_oriented=already_oriented) #Store OperationAnalyzer objects for each SymmOp opa_w = [] for op_w in symm_w: opa_w.append(OperationAnalyzer(op_w)) opa_m = [] for op_m in symm_m: opa_m.append(OperationAnalyzer(op_m)) #Check for constraints from the Wyckoff symmetry... #If we find ANY two constraints (SymmOps with unique axes), the molecule's #point group MUST contain SymmOps which can be aligned to these particular #constraints. However, there may be multiple compatible orientations of the #molecule consistent with these constraints constraint1 = None constraint2 = None for i, op_w in enumerate(symm_w): if opa_w[i].axis is not None: constraint1 = opa_w[i] for j, op_w in enumerate(symm_w): if opa_w[j].axis is not None: dot = np.dot(opa_w[i].axis, opa_w[j].axis) if (not isclose(dot, 1)) and (not isclose(dot, -1)): constraint2 = opa_w[j] break break #Indirectly store the angle between the constraint axes if (constraint1 is not None and constraint2 is not None): dot_w = np.dot(constraint1.axis, constraint2.axis) #Store corresponding symmetry elements of the molecule constraints_m = [] if constraint1 is not None: for i, op_m in enumerate(symm_m): if opa_m[i].is_conjugate(constraint1): constraints_m.append([opa_m[i], []]) if constraint2 is not None: for j, op_m2 in enumerate(symm_m): if opa_m[j].is_conjugate(constraint2): dot_m = np.dot(opa_m[i].axis, opa_m[j].axis) #Ensure that the angles are equal if isclose(dot_m, dot_w): constraints_m[-1][1].append(opa_m[j]) #Eliminate redundancy for 1st constraint list_i = list(range(len(constraints_m))) list_j = list(range(len(constraints_m))) for i, c1 in enumerate(constraints_m): if i in list_i: for j, c2 in enumerate(constraints_m): if i > j and j in list_j and j in list_i: #check if c1 and c2 are symmetrically equivalent #calculate moments of inertia about both axes m1_a = get_moment_of_inertia(mol, c1[0].axis) m2_a = get_moment_of_inertia(mol, c2[0].axis) m1_b = get_moment_of_inertia(mol, c1[0].axis, scale=2.0) m2_b = get_moment_of_inertia(mol, c2[0].axis, scale=2.0) if isclose(m1_a, m2_a, rtol=1e-2) and isclose( m1_b, m2_b, rtol=1e-2): list_i.remove(j) list_j.remove(j) c_m = deepcopy(constraints_m) constraints_m = [] for i in list_i: constraints_m.append(c_m[i]) #Eliminate redundancy for second constraint for k, c in enumerate(constraints_m): if c[1] != []: list_i = list(range(len(c[1]))) list_j = list(range(len(c[1]))) for i, c1 in enumerate(c[1]): if i in list_i: for j, c2 in enumerate(c[1]): if j > i and j in list_j and j in list_i: #check if c1 and c2 are symmetrically equivalent #calculate moments of inertia about both axes m1_a = get_moment_of_inertia(mol, c1.axis) m2_a = get_moment_of_inertia(mol, c2.axis) m1_b = get_moment_of_inertia(mol, c1.axis, scale=2.0) m2_b = get_moment_of_inertia(mol, c2.axis, scale=2.0) if isclose(m1_a, m2_a, rtol=1e-2) and isclose( m1_b, m2_b, rtol=1e-2): list_i.remove(j) list_j.remove(j) c_m = deepcopy(c[1]) constraints_m[k][1] = [] for i in list_i: constraints_m[k][1].append(c_m[i]) #Generate orientations consistent with the possible constraints orientations = [] #Loop over molecular constraint sets for c1 in constraints_m: v1 = c1[0].axis v2 = constraint1.axis T = rotate_vector(v1, v2) #Loop over second molecular constraints for opa in c1[1]: v1 = np.dot(T, opa.axis) v2 = constraint2.axis R = rotate_vector(v1, v2) T2 = np.dot(T, R) orientations.append(T2) if c1[1] == []: if randomize is True: angle = rand() * 2 * pi R = aa2matrix(v1, angle) T2 = np.dot(T, R) orientations.append(T2) else: orientations.append(T) #Ensure the identity orientation is checked if no constraints are found if constraints_m == []: if randomize is True: R = aa2matrix(1, 1, random=True) orientations.append(R) else: orientations.append(np.identity(3)) #Check each of the found orientations for consistency with the Wyckoff pos. #If consistent, put into an array of valid orientations allowed = [] for o in orientations: o = SymmOp.from_rotation_and_translation(o, [0, 0, 0]) mo = deepcopy(mol) mo.apply_operation(o) if orientation_in_wyckoff_position( mo, sg, index, exact_orientation=True, already_oriented=already_oriented) is True: allowed.append(o) #Return the array of allowed orientations. If there are none, return False if allowed == []: return False else: return allowed
def __init__(self, sch_symbol, operations, tolerance=0.1): self.sch_symbol = sch_symbol super(PointGroupOperations, self).__init__([ op.rotation_matrix for op in generate_full_symmops(operations, tolerance) ])