def bend(xyz, *inds, units='rad', absv=False): """Returns bending angle for 3 atoms in a chain based on index.""" e1 = con.unit_vec(xyz[inds[0]] - xyz[inds[1]]) e2 = con.unit_vec(xyz[inds[2]] - xyz[inds[1]]) coord = np.arccos(np.dot(e1, e2)) return coord * con.conv('rad', units)
def tors(xyz, *inds, units='rad', absv=False): """Returns dihedral angle for 4 atoms in a chain based on index. Parameters ---------- xyz : (N, 3) array_like The atomic cartesian coordinates. inds : list The indices for which the dihedral angle is measured. units : str, optional The units of angle for the output. Default is radians. absv : bool, optional Specifies if the absolute value is returned. Returns ------- float The dihedral angle. """ e1 = xyz[inds[0]] - xyz[inds[1]] e2 = xyz[inds[2]] - xyz[inds[1]] e3 = xyz[inds[2]] - xyz[inds[3]] # get normals to 3-atom planes cp1 = con.unit_vec(np.cross(e1, e2)) cp2 = con.unit_vec(np.cross(e2, e3)) if absv: coord = con.arccos(np.dot(cp1, cp2)) else: # get cross product of plane normals for signed dihedral angle cp3 = np.cross(cp1, cp2) coord = np.sign(np.dot(cp3, e2)) * con.arccos(np.dot(cp1, cp2)) return coord * con.conv('rad', units)
def _parse_axis(inp): """Returns a numpy array based on a specified axis. Axis can be given as a string (e.g. 'x' or 'xy'), a vector or a set of 3 vectors. If the input defines a plane, the plane normal is returned. For instance, 'x', 'yz', [1, 0, 0] and [[0, 1, 0], [0, 0, 0], [0, 0, 1]] will all return [1, 0, 0]. """ if isinstance(inp, str): if inp in ['x', 'yz', 'zy']: return np.array([1., 0., 0.]) elif inp in ['y', 'xz', 'zx']: return np.array([0., 1., 0.]) elif inp in ['z', 'xy', 'yx']: return np.array([0., 0., 1.]) elif inp == '-x': return np.array([-1., 0., 0.]) elif inp == '-y': return np.array([0., -1., 0.]) elif inp == '-z': return np.array([0., 0., -1.]) elif len(inp) == 3: u = np.array(inp, dtype=float) if u.size == 9: unew = np.cross(u[0] - u[1], u[2] - u[1]) return con.unit_vec(unew) else: return con.unit_vec(u) else: raise ValueError('Axis specification not recognized')
def __call__(self, inp, unit=False): """Evaluates an expression based on a string. Parameters ---------- inp : str or array_like A string or array used to specify an axis. unit : bool, optional Specifies if the axis is converted to a unit vector. Returns ------- float or ndarray The result of the vector operation. Raises ------ ValueError If input is not a string, 3-vector or 3x3 array. """ if isinstance(inp, str): u = self.expr.parseString(inp, parseAll=True)[0] elif len(inp) == 3: u = np.array(inp, dtype=float) if u.size == 9: u = np.cross(u[0] - u[1], u[2] - u[1]) else: raise ValueError('Axis specification not recognized') if unit: return con.unit_vec(u) else: return u
def edgetors(xyz, *inds, units='rad', absv=False): """Returns the torsional angle based on the vector difference of the two external atoms (1-2 and 5-6) to the central 3-4 bond.""" e1 = con.unit_vec(xyz[inds[0]] - xyz[inds[2]]) e2 = con.unit_vec(xyz[inds[1]] - xyz[inds[2]]) e3 = con.unit_vec(xyz[inds[3]] - xyz[inds[2]]) e4 = con.unit_vec(xyz[inds[4]] - xyz[inds[3]]) e5 = con.unit_vec(xyz[inds[5]] - xyz[inds[3]]) # take the difference between unit vectors of external bonds e2 -= e1 e5 -= e4 # get cross products of difference vectors and the central bond cp1 = con.unit_vec(np.cross(e2, e3)) cp2 = con.unit_vec(np.cross(e3, e5)) if absv: coord = np.arccos(np.dot(cp1, cp2)) else: # get cross product of vectors for signed dihedral angle cp3 = np.cross(cp1, cp2) coord = np.sign(np.dot(cp3, e3)) * np.arccos(np.dot(cp1, cp2)) return coord * con.conv('rad', units)
def tors(xyz, *inds, units='rad', absv=False): """Returns dihedral angle for 4 atoms in a chain based on index.""" e1 = xyz[inds[0]] - xyz[inds[1]] e2 = xyz[inds[2]] - xyz[inds[1]] e3 = xyz[inds[2]] - xyz[inds[3]] # get normals to 3-atom planes cp1 = con.unit_vec(np.cross(e1, e2)) cp2 = con.unit_vec(np.cross(e2, e3)) if absv: coord = np.arccos(np.dot(cp1, cp2)) else: # get cross product of plane normals for signed dihedral angle cp3 = np.cross(cp1, cp2) coord = np.sign(np.dot(cp3, e2)) * np.arccos(np.dot(cp1, cp2)) return coord * con.conv('rad', units)
def planeang(xyz, *inds, units='rad', absv=False): """Returns the angle between the 1-2-3 and 4-5-6 planes.""" e1 = xyz[inds[0]] - xyz[inds[2]] e2 = xyz[inds[1]] - xyz[inds[2]] e3 = xyz[inds[3]] - xyz[inds[2]] e4 = xyz[inds[4]] - xyz[inds[3]] e5 = xyz[inds[5]] - xyz[inds[3]] # get normals to 3-atom planes cp1 = con.unit_vec(np.cross(e1, e2)) cp2 = con.unit_vec(np.cross(e4, e5)) if absv: coord = np.arccos(np.dot(cp1, cp2)) else: # get cross product of plane norms for signed dihedral angle cp3 = np.cross(cp1, cp2) coord = np.sign(np.dot(cp3, e3)) * np.arccos(np.dot(cp1, cp2)) return coord * con.conv('rad', units)
def planetors(xyz, *inds, units='rad', absv=False): """Returns the plane angle with the central bond projected out. Parameters ---------- xyz : (N, 3) array_like The atomic cartesian coordinates. inds : list The indices for which the plane dihedral angle is measured. units : str, optional The units of angle for the output. Default is radians. absv : bool, optional Specifies if the absolute value is returned. Returns ------- float The plane dihedral angle. """ e1 = xyz[inds[0]] - xyz[inds[2]] e2 = xyz[inds[1]] - xyz[inds[2]] e3 = con.unit_vec(xyz[inds[3]] - xyz[inds[2]]) e4 = xyz[inds[4]] - xyz[inds[3]] e5 = xyz[inds[5]] - xyz[inds[3]] # get normals to 3-atom planes cp1 = np.cross(e1, e2) cp2 = np.cross(e4, e5) # project out component along central bond pj1 = con.unit_vec(cp1 - np.dot(cp1, e3) * e3) pj2 = con.unit_vec(cp2 - np.dot(cp2, e3) * e3) if absv: coord = con.arccos(np.dot(pj1, pj2)) else: # get cross product of vectors for signed dihedral angle cp3 = np.cross(pj1, pj2) coord = np.sign(np.dot(cp3, e3)) * con.arccos(np.dot(pj1, pj2)) return coord * con.conv('rad', units)
def oop(xyz, *inds, units='rad', absv=False): """Returns out-of-plane angle of atom 1 connected to atom 4 in the 2-3-4 plane. Contains an additional sign convention such that rotation of the out-of-plane atom over (under) the central plane atom gives an angle greater than pi/2 (less than -pi/2). """ e1 = con.unit_vec(xyz[inds[0]] - xyz[inds[3]]) e2 = con.unit_vec(xyz[inds[1]] - xyz[inds[3]]) e3 = con.unit_vec(xyz[inds[2]] - xyz[inds[3]]) sintau = np.dot(np.cross(e2, e3) / np.sqrt(1 - np.dot(e2, e3) ** 2), e1) coord = np.sign(np.dot(e2+e3, e1)) * np.arccos(sintau) + np.pi/2 # sign convention to keep |oop| < pi if coord > np.pi: coord -= 2 * np.pi if absv: return abs(coord) * con.conv('rad', units) else: return coord * con.conv('rad', units)
def bend(xyz, *inds, units='rad'): """Returns bending angle for 3 atoms in a chain based on index. Parameters ---------- xyz : (N, 3) array_like The atomic cartesian coordinates. inds : list The indices for which the bond angle is measured. units : str, optional The units of angle for the output. Default is radians. Returns ------- float The bond angle. """ e1 = con.unit_vec(xyz[inds[0]] - xyz[inds[1]]) e2 = con.unit_vec(xyz[inds[2]] - xyz[inds[1]]) coord = con.arccos(np.dot(e1, e2)) return coord * con.conv('rad', units)
def oop(xyz, *inds, units='rad', absv=False): """Returns out-of-plane angle of atom 1 connected to atom 4 in the 2-3-4 plane. Contains an additional sign convention such that rotation of the out-of-plane atom over (under) the central plane atom gives an angle greater than :math:`\pi/2` (less than :math:`-\pi/2`). Parameters ---------- xyz : (N, 3) array_like The atomic cartesian coordinates. inds : list The indices for which the out-of-plane angle is measured. units : str, optional The units of angle for the output. Default is radians. absv : bool, optional Specifies if the absolute value is returned. Returns ------- float The out-of-plane angle. """ e1 = con.unit_vec(xyz[inds[0]] - xyz[inds[3]]) e2 = con.unit_vec(xyz[inds[1]] - xyz[inds[3]]) e3 = con.unit_vec(xyz[inds[2]] - xyz[inds[3]]) sintau = np.dot(np.cross(e2, e3) / np.sqrt(1 - np.dot(e2, e3)**2), e1) coord = np.sign(np.dot(e2 + e3, e1)) * con.arccos(sintau) + np.pi / 2 # sign convention to keep |oop| < pi if coord > np.pi: coord -= 2 * np.pi if absv: return abs(coord) * con.conv('rad', units) else: return coord * con.conv('rad', units)
def planetors(xyz, *inds, units='rad', absv=False): """Returns the plane angle with the central bond projected out.""" e1 = xyz[inds[0]] - xyz[inds[2]] e2 = xyz[inds[1]] - xyz[inds[2]] e3 = con.unit_vec(xyz[inds[3]] - xyz[inds[2]]) e4 = xyz[inds[4]] - xyz[inds[3]] e5 = xyz[inds[5]] - xyz[inds[3]] # get normals to 3-atom planes cp1 = np.cross(e1, e2) cp2 = np.cross(e4, e5) # project out component along central bond pj1 = con.unit_vec(cp1 - np.dot(cp1, e3) * e3) pj2 = con.unit_vec(cp2 - np.dot(cp2, e3) * e3) if absv: coord = np.arccos(np.dot(pj1, pj2)) else: # get cross product of vectors for signed dihedral angle cp3 = np.cross(pj1, pj2) coord = np.sign(np.dot(cp3, e3)) * np.arccos(np.dot(pj1, pj2)) return coord * con.conv('rad', units)
def add_subs(self, *lbls, inds=-1): """Returns the element list and cartesian geometry from a combination of substituents. Parameters ---------- lbls : list A list of substituent labels to be combined. inds : int or array_like, optional The indices for substitution between substituents. Setting inds=-1 (default) makes the last atom the subtituted atom. Otherwise a list of indices can be given for the first of each pair of substituents. Returns ------- elem : (N,) ndarray The atomic symbols of the combined substituent. xyz : (N, 3) ndarray The atomic cartesian coordinates of the combined substituent. """ if isinstance(inds, int): inds = (len(lbls) - 1) * [inds] elif len(inds) != len(lbls) - 1: raise ValueError('Number of inds != number of labels - 1') rot = 0 lbl0 = self.syn[lbls[0].lower()] elem = self.elem[lbl0] xyz = self.xyz[lbl0] for i, label in zip(inds, lbls[1:]): dist = np.linalg.norm(xyz - xyz[i], axis=1) dist[i] += np.max(dist) ibond = np.argmin(dist) rot = (rot + 1) % 2 ax = con.unit_vec(xyz[i] - xyz[ibond]) lbl = self.syn[label.lower()] new_elem = self.elem[lbl] new_xyz = displace.rotate(self.xyz[lbl], rot * np.pi, 'Z') new_xyz = displace.align_axis(new_xyz, 'Z', ax) blen = con.get_covrad(elem[ibond]) + con.get_covrad(new_elem[0]) new_xyz += xyz[ibond] + blen * ax elem = np.hstack((np.delete(elem, i), new_elem)) xyz = np.vstack((np.delete(xyz, i, axis=0), new_xyz)) return elem, xyz
def edgetors(xyz, *inds, units='rad', absv=False): """Returns the torsional angle based on the vector difference of the two external atoms (1-2 and 5-6) to the central 3-4 bond. Parameters ---------- xyz : (N, 3) array_like The atomic cartesian coordinates. inds : list The indices for which the edge dihedral angle is measured. units : str, optional The units of angle for the output. Default is radians. absv : bool, optional Specifies if the absolute value is returned. Returns ------- float The edge dihedral angle. """ e1 = con.unit_vec(xyz[inds[0]] - xyz[inds[2]]) e2 = con.unit_vec(xyz[inds[1]] - xyz[inds[2]]) e3 = con.unit_vec(xyz[inds[3]] - xyz[inds[2]]) e4 = con.unit_vec(xyz[inds[4]] - xyz[inds[3]]) e5 = con.unit_vec(xyz[inds[5]] - xyz[inds[3]]) # take the difference between unit vectors of external bonds e2 -= e1 e5 -= e4 # get cross products of difference vectors and the central bond cp1 = con.unit_vec(np.cross(e2, e3)) cp2 = con.unit_vec(np.cross(e3, e5)) if absv: coord = con.arccos(np.dot(cp1, cp2)) else: # get cross product of vectors for signed dihedral angle cp3 = np.cross(cp1, cp2) coord = np.sign(np.dot(cp3, e3)) * con.arccos(np.dot(cp1, cp2)) return coord * con.conv('rad', units)
def subst(elem, xyz, sublbl, isub, ibond=None, pl=None, vec=None): """Returns a molecular geometry with an specified atom replaced by substituent. Labels are case-insensitive. The index isub gives the position to be substituted. If specified, ibond gives the atom bonded to the substituent. Otherwise, the nearest atom to isub is used. The orientation of the substituent can be given as a vector (the plane normal) or an index (the plane containing isub, ibond and pl). If isub is given as a list, the entire list of atoms is removed and the first index is treated as the position of the substituent. Parameters ---------- elem : (N,) array_like The atomic symbols of the unsubstituted molecule. xyz : (N, 3) array_like The atomic cartesian coordinates of the unsubstituted molecule. sublbl : str The substituent label. isub : int or list The atomic index (or indices) to be replaced by the substituent. ibond : int, optional The atomic index of the atom bonded to position isub. If None (default), the nearest atom is chosen. pl : int or array_like, optional The atomic index or vector defining the xz-plane of the substituent. If an index is given, the plane normal to the isub-ibond-pl plane is used. If None (default), the plane is arbitrarily set to [1, 1, 1] and the bond axis is projected out. vec : (N, 3) array_like, optional The atomic cartesian vectors of the unsubstitued molecule. Default is None. Returns ------- new_elem : (N,) ndarray The atomic symbols of the substituted molecule. new_xyz : (N, 3) ndarray The atomic cartesian coordinates of the substituted molecule. new_vec : (N, 3) ndarray The atomic cartesian vectors of the substituted molecule. Substituent atoms are all set of zero. If vec is None, new_vec is all zeros. """ elem = np.array(elem) xyz = np.atleast_2d(xyz) if not isinstance(isub, int): ipos = isub[0] else: isub = [isub] ipos = isub[0] if ibond is None: dist = np.linalg.norm(xyz - xyz[ipos], axis=1) dist[ipos] += np.max(dist) ibond = np.argmin(dist) elif ibond == ipos: raise ValueError('sub and bond indices cannot be the same') ax = con.unit_vec(xyz[ipos] - xyz[ibond]) if pl is None: # choose an arbitrary axis and project out the bond axis pl = np.ones(3) pl -= np.dot(pl, ax) * ax elif isinstance(pl, int): if pl == ipos: raise ValueError('plane and sub indices cannot be the same') elif pl == ibond: raise ValueError('plane and bond indices cannot be the same') pl = np.cross(xyz[ipos] - xyz[ibond], xyz[pl] - xyz[ibond]) sub_el, sub_xyz = import_sub(sublbl) if elem[ipos] == sub_el[0]: blen = np.linalg.norm(xyz[ipos] - xyz[ibond]) else: blen = con.get_covrad(elem[ibond]) + con.get_covrad(sub_el[0]) # rotate to correct orientation and displace to correct position sub_xyz = displace.align_axis(sub_xyz, 'Z', ax) sub_pl = displace.align_axis([0., 1., 0.], 'Z', ax) sub_xyz = displace.align_axis(sub_xyz, sub_pl, pl) sub_xyz += xyz[ibond] + blen * ax # build the final geometry ind1 = [i for i in range(ipos) if i not in isub[1:]] ind2 = [i for i in range(ipos + 1, len(elem)) if i not in isub[1:]] new_elem = np.hstack((elem[ind1], sub_el, elem[ind2])) new_xyz = np.vstack((xyz[ind1], sub_xyz, xyz[ind2])) if vec is None: return new_elem, new_xyz, None else: new_vec = np.vstack((vec[ind1], np.zeros((len(sub_el), 3)), vec[ind2])) return new_elem, new_xyz, new_vec
def test_unit_vec_fails(): with pytest.raises(ValueError, match=r'Cannot make unit vector from .*'): uvec = con.unit_vec(np.zeros(3))
def test_unit_vec(): vec_len = np.linalg.norm(con.unit_vec([1., -1., 2.])) assert np.isclose(vec_len, 1.)