def __init__(self, mol, tol=0.1): """The default settings are usually sufficient. -- mol : Molecule to determine point group for. """ self.tol = tol self.etol = self.tol / 3.0 self.mol = mol # center molecule self.mol.positions -= self.mol.positions.mean(axis=0) # normalize norm_factor = numpy.amax( numpy.linalg.norm(self.mol.positions, axis=1, keepdims=True)) if norm_factor < 1e-6: norm_factor = 1.0 self.mol.positions /= norm_factor self.symmops = {"C": [], "S": [], "sigma": []} # identity self.symmops["I"] = numpy.eye(3) #inversion inversion = -numpy.eye(3) if is_valid_op(self.mol, inversion): self.symmops["-I"] = inversion logger.debug("Inversion center present") else: self.symmops["-I"] = None self.nrot = 0 self.nref = 0 self.analyze() if self.schoenflies in ["C1v", "C1h"]: self.schoenflies = "Cs"
def find_spherical_axes(self): """Looks for R5, R4, R3 and R2 axes in spherical 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 = {2: False, 3: False, 4: False, 5: False} test_set = self.find_possible_equivalent_positions() for c1, c2, c3 in itertools.combinations(test_set, 3): for cc1, cc2 in itertools.combinations([c1, c2, c3], 2): if not rot_present[2]: test_axis = cc1 + cc2 if numpy.linalg.norm(test_axis) > self.tol: op = rotation(axis=test_axis, order=2) if is_valid_op(self.mol, op): logger.debug("Found axis with order {0}".format(2)) rot_present[2] = True self.symmops["C"] += [ (2, test_axis, op), ] self.nrot += 1 test_axis = numpy.cross(c2 - c1, c3 - c1) if numpy.linalg.norm(test_axis) > self.tol: for order in (3, 4, 5): if not rot_present[order]: op = rotation(axis=test_axis, order=order) if is_valid_op(self.mol, op): logger.debug( "Found axis with order {0}".format(order)) rot_present[order] = True self.symmops["C"] += [ (order, test_axis, op), ] self.nrot += 1 break if (rot_present[2] & rot_present[3] & (rot_present[4] | rot_present[5])): break return
def find_reflection_plane(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. """ symmop = None # First test whether the axis itself is the normal to a mirror plane. op = reflection(axis) if is_valid_op(self.mol, op): symmop = ("h", axis, op) else: # Iterate through all pairs of atoms to find mirror for s1, s2 in itertools.combinations(self.mol, 2): if s1.symbol == s2.symbol: normal = s1.position - s2.position if normal.dot(axis) < self.tol: op = reflection(normal) if is_valid_op(self.mol, op): if self.nrot > 1: symmop = ("d", normal, op) for prev_order, prev_axis, prev_op in self.symmops[ "C"]: if not numpy.linalg.norm(prev_axis - axis) < self.tol: if numpy.dot(prev_axis, normal) < self.tol: symmop = ("v", normal, op) break else: symmop = ("v", normal, op) break if symmop is not None: self.symmops["sigma"] += [ symmop, ] return symmop[0] else: return symmop
def has_perpendicular_C2(self, axis): """Checks for R2 axes perpendicular to unique axis. For handling symmetric top molecules. """ min_set = self.find_possible_equivalent_positions(axis=axis) found = False for s1, s2 in itertools.combinations(min_set, 2): test_axis = numpy.cross(s1 - s2, axis) if numpy.linalg.norm(test_axis) > self.tol: op = rotation(axis=test_axis, order=2) if is_valid_op(self.mol, op): self.symmops["C"] += [(2, test_axis, op), ] self.nrot += 1 found = True return found
def analyze_cyclic_groups(self): """Handles cyclic group molecules.""" order, axis, op = max(self.symmops["C"], key=lambda v: v[0]) self.schoenflies = "C{0}".format(order) mirror_type = self.find_reflection_plane(axis) if mirror_type == "h": self.schoenflies += "h" elif mirror_type == "v": self.schoenflies += "v" elif mirror_type is None: rotoref = reflection(axis).dot( rotation(axis=axis, order=2 * order)) if is_valid_op(self.mol, rotoref): self.schoenflies = "S{0}".format(2 * order) self.symmops["S"] += [(order, axis, rotoref), ]
def detect_rotational_symmetry(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.find_possible_equivalent_positions(axis=axis) max_sym = len(min_set) for order in range(max_sym, 0, -1): if max_sym % order != 0: continue op = rotation(axis=axis, order=order) if is_valid_op(self.mol, op): logger.debug("Found axis with order {0}".format(order)) self.symmops["C"] += [(order, axis, op), ] self.nrot += 1 return order return 1
def analyze_asymmetric_top(self): """Handles assymetric top molecules, which cannot contain rotational symmetry larger than 2. """ for axis in self.eigvecs: op = rotation(axis=axis, order=2) if is_valid_op(self.mol, op): self.symmops["C"] += [(2, axis, op), ] self.nrot += 1 if self.nrot == 0: logger.debug("No rotation symmetries detected.") self.analyze_nonrotational_groups() elif self.nrot == 3: logger.debug("Dihedral group detected.") self.analyze_dihedral_groups() else: logger.debug("Cyclic group detected.") self.analyze_cyclic_groups()
def get_symmetry_elements(mol, max_order=8, epsilon=0.1): """Return an array counting the found symmetries of the object, up to axes of order max_order. Enables fast comparison of compatibility between objects: if array1 - array2 > 0, that means object1 fits the slot2 """ if len(mol) == 1: logger.debug("Point-symmetry detected.") symmetries = numpy.array([0, 0, 0, 0, 0, 1]) return symmetries if len(mol) == 2: logger.debug("Linear connectivity detected.") # simplest, linear case symmetries = numpy.array([1, 1, 0, 1, 1, 2]) return symmetries mol.center(about=0) # ensure there is a sufficient distance between connections dist = mol.get_all_distances().mean() if dist < 10.0: alpha = 10.0 / dist mol.positions = mol.positions.dot(numpy.eye(3) * alpha) logger.debug("DIST {}".format(dist)) axes = get_potential_axes(mol) # array for the operations: # size = (1inversion+rotationorders+rotinvorders+2planes+1multiplicity) symmetries = numpy.zeros(4 + 2 * max_order) # inversion inv = -1.0 * numpy.eye(3) has_inv = is_valid_op(mol, inv) symmetries[0] += int(has_inv) # rotations: principal_order = 1 principal_axes = [] for axis in axes: for order in range(2, max_order + 1): rot = rotation(axis, order) has_rot = is_valid_op(mol, rot) symmetries[order - 1] += int(has_rot) if has_rot: logger.debug("Detected: C{order}".format(order=order)) # bookkeeping for the planes if has_rot and order > principal_order: principal_order = order principal_axes = [axis, ] elif has_rot and order == principal_order: principal_axes.append(axis) # planes for axis in axes: ref = reflection(axis) has_ref = is_valid_op(mol, ref) if has_ref: dots = [abs(axis.dot(max_axis)) for max_axis in principal_axes] if not dots: continue mindot = numpy.amin(dots) maxdot = numpy.amax(dots) if maxdot > 1.0 - epsilon: # sigma h symmetries[-2] += 1 logger.debug("Detected: sigma h") elif mindot < epsilon: # sigma vd symmetries[-3] += 1 logger.debug("Detected: sigma vd") # rotoreflections for axis in axes: ref = reflection(axis) for order in range(2, max_order + 1): rot = rotation(axis, order) rr = rot.dot(ref) has_rr = is_valid_op(mol, rr) symmetries[order + max_order - 2] += int(has_rr) if has_rr: logger.debug("Detected: S{order}".format(order=order)) # multiplicity symmetries[-1] = len([x for x in mol if x.symbol == "X"]) return numpy.array(symmetries, dtype=int)