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 read_xyz(infile, units='ang', hasvec=False, hascom=False): """Reads input file in XYZ format. XYZ files are in the format: natm comment A X1 Y1 Z1 [Vx1 Vy1 Vz1] B X2 Y2 Z2 [Vx2 Vy2 Vz2] ... where natm is the number of atoms, comment is a comment line, A and B are atomic labels and X, Y and Z are cartesian coordinates (in Angstroms). The vectors Vx, Vy and Vz are optional and will only be read if hasvec = True. Due to the preceding number of atoms, multiple XYZ format geometries can easily be read from a single file. """ try: natm = int(infile.readline()) except ValueError: raise IOError('geometry not in XYZ format.') if hascom: comment = infile.readline().strip() else: infile.readline() comment = '' data = np.array([infile.readline().split() for i in range(natm)]) elem = data[:, 0] xyz = data[:, 1:4].astype(float) * con.conv(units, 'ang') if hasvec: vec = data[:, 4:7].astype(float) else: vec = None return elem, xyz, vec, comment
def translate(xyz, amp, axis, ind=None, units='ang'): """Translates a set of atoms along a given vector. Parameters ---------- xyz : (N, 3) array_like The atomic cartesian coordinates. amp : float The distance for translation. axis : array_like or str The axis of translation, parsed by :class:`VectorParser`. ind : array_like, optional List of atomic indices to specify which atoms are displaced. If ind is None (default) then all atoms are displaced. units : str, optional The units of length for displacement. Default is angstroms. Returns ------- (N, 3) ndarray The atomic cartesian coordinates of the displaced molecule. """ if ind is None: ind = range(len(xyz)) vp = VectorParser(xyz) u = vp(axis, unit=True) amp *= con.conv(units, 'ang') newxyz = np.copy(xyz) newxyz[ind] += amp * u return newxyz
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 read_gdat(infile, units='bohr', hasvec=False, hascom=False): """Reads input file in FMS90 Geometry.dat format. Geometry.dat files are in the format: comment natm A X1 Y1 Z1 B X2 Y2 Z2 ... Vx1 Vy1 Vz1 Vx2 Vy2 Vz2 ... where comment is a comment line, natm is the number of atoms, A and B are atomic labels, X, Y and Z are cartesian coordinates and Vq are vectors for cartesian coordinates q. The vectors are only read if hasvec = True. """ if hascom: comment = infile.readline().strip() else: infile.readline() comment = '' try: natm = int(infile.readline()) except ValueError: raise IOError('geometry not in Geometry.dat format') data = np.array([infile.readline().split() for i in range(natm)]) elem = data[:, 0] xyz = data[:, 1:].astype(float) * con.conv(units, 'ang') if hasvec: vec = np.array([infile.readline().split() for i in range(natm)], dtype=float) else: vec = None return elem, xyz, vec, comment
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 angax(rotmat, units='rad'): """Returns the angle, axis of rotation and determinant of a rotational matrix. Based on the form of R, it can be separated into symmetric and antisymmetric components with (r_ij + r_ji)/2 and (r_ij - r_ji)/2, respectively. Then, r_ii = cos(a) + u_i^2 (det(R) - cos(a)), cos(a) = (-det(R) + sum_j r_jj) / 2 = (tr(R) - det(R)) / 2. From the expression for r_ii, the magnitude of u_i can be found |u_i| = sqrt((1 + det(R) [2 r_ii - tr(R)]) / 2), which satisfies u.u = 1. Note that if det(R) tr(R) = 3, the axis is arbitrary (identity or inversion). Otherwise, the sign can be found from the antisymmetric component of R u_i sin(a) = (r_jk - r_kj) / 2, i != j != k, sign(u_i) = sign(r_jk - r_kj), since sin(a) is positive in the range 0 to pi. i, j and k obey the cyclic relation 3 -> 2 -> 1 -> 3 -> ... This fails when det(R) tr(R) = -1, in which case the symmetric component of R is used u_i u_j (det(R) - cos(a)) = (r_ij + r_ji) / 2, sign(u_i) sign(u_j) = det(R) sign(r_ij + r_ji). The signs can then be found by letting sign(u_3) = +1, since a rotation of pi or a reflection are equivalent for antiparallel axes. See http://scipp.ucsc.edu/~haber/ph251/rotreflect_17.pdf """ det = np.linalg.det(rotmat) if not np.isclose(np.abs(det), 1): raise ValueError('Determinant of a rotational matrix must be +/- 1') tr = np.trace(rotmat) ang = np.arccos((tr - det) / 2) * con.conv('rad', units) if np.isclose(det*tr, 3): u = np.array([0, 0, 1]) else: u = np.sqrt((1 + det*(2*np.diag(rotmat) - tr)) / (3 - det*tr)) if np.isclose(det*tr, -1): sgn = np.ones(3) sgn[1] = det * _nonzero_sign(rotmat[1,2] + rotmat[2,1]) sgn[0] = det * sgn[1] * _nonzero_sign(rotmat[0,1] + rotmat[1,0]) u *= sgn else: u[0] *= _nonzero_sign(rotmat[1,2] - rotmat[2,1]) u[1] *= _nonzero_sign(rotmat[2,0] - rotmat[0,2]) u[2] *= _nonzero_sign(rotmat[0,1] - rotmat[1,0]) return ang, u, det
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 write_xyz(outfile, elem, xyz, vec=None, comment='', units='ang'): """Writes geometry to an output file in XYZ format.""" natm = len(elem) write_xyz = xyz * con.conv('ang', units) outfile.write(' {}\n{}\n'.format(natm, comment)) if vec is None: for atm, xyzi in zip(elem, write_xyz): outfile.write('{:4s}{:12.6f}{:12.6f}{:12.6f}\n'.format(atm, *xyzi)) else: for atm, xyzi, pxyzi in zip(elem, write_xyz, vec): outfile.write('{:4s}{:12.6f}{:12.6f}{:12.6f}'.format(atm, *xyzi) + '{:12.6f}{:12.6f}{:12.6f}\n'.format(*pxyzi))
def write_gdat(outfile, elem, xyz, vec=None, comment='', units='bohr'): """Writes geometry to an output file in Geometry.dat format.""" natm = len(elem) write_xyz = xyz * con.conv('ang', units) outfile.write('{}\n{}\n'.format(comment, natm)) for atm, xyzi in zip(elem, write_xyz): outfile.write('{:<2s}{:18.8E}{:18.8E}{:18.8E}\n'.format(atm, *xyzi)) if vec is None: for line in range(natm): outfile.write(' {:18.8E}{:18.8E}{:18.8E}\n'.format(0, 0, 0)) else: for pxyzi in vec: outfile.write(' {:18.8E}{:18.8E}{:18.8E}\n'.format(*pxyzi))
def write_col(outfile, elem, xyz, vec=None, comment='', units='bohr'): """Writes geometry to an output file in COLUMBUS format. For the time being, vector output is not supported for the COLUMBUS file format. """ write_xyz = xyz * con.conv('ang', units) if comment != '': outfile.write(comment + '\n') for atm, (x, y, z) in zip(elem, write_xyz): outfile.write(' {:<2s}{:7.1f}{:14.8f}{:14.8f}{:14.8f}{:14.8f}' '\n'.format(atm, con.get_num(atm), x, y, z, con.get_mass(atm)))
def translate(xyz, amp, axis, ind=None, origin=np.zeros(3), units='ang'): """Translates a set of atoms along a given vector. If no indices are specified, all atoms are displaced. """ if ind is None: ind = range(len(xyz)) u = _parse_axis(axis) amp *= con.conv(units, 'ang') newxyz = np.copy(xyz) newxyz[ind] += amp * u return newxyz
def rotmat(ang, u, det=1, units='rad', xyz=None): r"""Returns the rotational matrix based on an angle and axis. A general rotational matrix in 3D can be formed given an angle and an axis by .. math:: \mathbf{R} = \cos(a) \mathbf{I} + (\det(\mathbf{R}) - \cos(a)) \mathbf{u} \otimes \mathbf{u} + \sin(a) [\mathbf{u}]_\times for identity matrix **I**, angle *a*, axis **u**, outer product :math:`\otimes` and cross-product matrix :math:`[\mathbf{u}]_\times`. Determinants of +1 and -1 give proper and improper rotation, respectively. Thus, :math:`\det(\mathbf{R}) = -1` and :math:`a = 0` is a reflection along the axis. Action of the rotational matrix occurs about the origin. See en.wikipedia.org/wiki/Rotation_matrix and http://scipp.ucsc.edu/~haber/ph251/rotreflect_17.pdf Parameters ---------- ang : float The angle of rotation. u : array_like or str The axis of rotation, converted to a unit vector. det : int, optional The determinant of the matrix (1 or -1) used to specify proper and improper rotations. Default is 1. units : str, optional The units of angle for the rotation. Default is radians. xyz : (N, 3) array_like, optional The cartesian coordinates used in axis specification. Returns ------- (3, 3) ndarray The rotational matrix of the given angle and axis. Raises ------ ValueError When the absolute value of the determinant is not equal to 1. """ if not np.isclose(np.abs(det), 1): raise ValueError('Determinant of a rotational matrix must be +/- 1') u /= np.linalg.norm(u) amp = ang * con.conv(units, 'rad') ucross = np.array([[0, u[2], -u[1]], [-u[2], 0, u[0]], [u[1], -u[0], 0]]) return (np.cos(amp) * np.eye(3) + np.sin(amp) * ucross + (det - np.cos(amp)) * np.outer(u, u))
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 write_traj(outfile, elem, xyz, vec=None, comment='', units='bohr', time=0., phase=0., ramp=0., iamp=0., state=0.): """Writes geometry to an output file in FMS/nomad trajectory format.""" natm = len(elem) write_xyz = xyz.flatten() * con.conv('ang', units) if comment != '': outfile.write(comment + '\n') if vec is None: write_vec = np.zeros_like(write_xyz) else: write_vec = vec.flatten() namp = ramp**2 + iamp**2 args = np.hstack((write_xyz, write_vec, phase, ramp, iamp, namp, state)) fmt = '{:10.2f}' + (6*natm + 5)*'{:10.4f}' + '\n' outfile.write(fmt.format(time, *args))
def read_traj(infile, units='bohr', hasvec=False, hascom=False, elem=None, time=None, autocom=False): """Reads input file in FMS/nomad trajectory format trajectory files are in the format: T1 X1 Y1 Z1 X2 Y2 ... Vx1 Vy1 Vz1 Vx2 Vy2 ... G Re(A) Im(A) |A| S T2 X1 Y1 Z1 X2 Y2 ... Vx1 Vy1 Vz1 Vx2 Vy2 ... G Re(A) Im(A) |A| S ... where T is the time, Vq are the vectors (momenta) for cartesian coordinates q, G is the phase, A is the amplitude and S is the state label. The vectors are only read if hasvec = True. Trajectory files do not contain atomic labels. If not provided, they are set to dummy atoms which may affect calculations involving atomic properties. A time should be provided, otherwise the first geometry in the file is used. """ if hascom: comment = infile.readline().strip() else: comment = '' if time is None: rawline = infile.readline().split() if rawline == []: raise IOError('empty line provided') elif 'Time' in rawline: line = np.array(infile.readline().split(), dtype=float) else: line = np.array(rawline, dtype=float) else: alldata = np.array([line.split() for line in infile.readlines() if 'Time' not in line], dtype=float) line = alldata[np.isclose(alldata[:,0], time)][0] natm = len(line) // 6 - 1 if natm < 1 or len(line) % 6 != 0: raise IOError('geometry not in trajectory format.') if elem is None: elem = np.array(['X'] * natm) xyz = line[1:3*natm+1].reshape(natm, 3) * con.conv(units,'ang') if hasvec: vec = line[3*natm+1:6*natm+1].reshape(natm, 3) else: vec = None if autocom: fmt = 't={:8.2f}, state={:4d}, a^2={:10.4f}' comment += fmt.format(line[0], int(line[-1]), line[-2]) return elem, xyz, vec, comment
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 read_col(infile, units='bohr', hasvec=False, hascom=False): """Reads input file in COLUMBUS format. COLUMBUS geometry files are in the format: A nA X1 Y1 Z1 mA B nB X2 Y2 Z2 mB ... where A and B are atomic labels, nA and nB are corresponding atomic numbers, mA and mB are corresponding atomic masses and X, Y and Z are cartesian coordinates (in Bohrs). COLUMBUS geometry files do not provide the number of atoms in each geometry. A comment line (or blank line) must be used to separate molecules. For the time being, vector input is not supported for the COLUMBUS file format. """ if hascom: comment = infile.readline().strip() else: comment = '' data = np.empty((0, 6), dtype=str) while True: pos = infile.tell() line = np.array(infile.readline().split()) try: # catch comment line or end-of-file line[1:].astype(float) data = np.vstack((data, line)) except (ValueError, IndexError): if len(data) < 1: raise IOError('geometry not in COLUMBUS format.') else: # roll back one line before break infile.seek(pos) break elem = data[:, 0] xyz = data[:, 2:5].astype(float) * con.conv(units, 'ang') if hasvec: vec = np.zeros_like(xyz) else: vec = None return elem, xyz, vec, comment
def stre(xyz, *inds, units='ang'): """Returns bond length based on index. Parameters ---------- xyz : (N, 3) array_like The atomic cartesian coordinates. inds : list The indices for which the bond distance is measured. units : str, optional The units of length for the output. Default is Angstroms. Returns ------- float The bond length. """ coord = np.linalg.norm(xyz[inds[0]] - xyz[inds[1]]) return coord * con.conv('ang', units)
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 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 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 rotmat(ang, ax, det=1, units='rad'): """Returns the rotational matrix based on an angle and axis. A general rotational matrix in 3D can be formed given an angle and an axis by R = cos(a) I + (det(R) - cos(a)) u (x) u + sin(a) [u]_x for identity matrix I, angle a, axis u, outer product (x) and cross-product matrix [u]_x. Determinants of +1 and -1 give proper and improper rotation, respectively. Thus, det(R) = -1 and a = 0 is a reflection along the axis. Action of the rotational matrix occurs about the origin. See en.wikipedia.org/wiki/Rotation_matrix and http://scipp.ucsc.edu/~haber/ph251/rotreflect_17.pdf """ if not np.isclose(np.abs(det), 1): raise ValueError('Determinant of a rotational matrix must be +/- 1') u = _parse_axis(ax) amp = ang * con.conv(units, 'rad') ucross = np.array([[0, u[2], -u[1]], [-u[2], 0, u[0]], [u[1], -u[0], 0]]) return (np.cos(amp) * np.eye(3) + np.sin(amp) * ucross + (det - np.cos(amp)) * np.outer(u, u))
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 test_conv_ang(): assert np.isclose(con.conv('deg', 'rad'), np.pi/180.)
def stre(xyz, *inds, units='ang', absv=False): """Returns bond length based on index.""" coord = np.linalg.norm(xyz[inds[0]] - xyz[inds[1]]) return coord * con.conv('ang', units)
def test_conv_mas(): assert np.isclose(con.conv('mp', 'me'), 1836.15267981)
def test_conv_ene(): assert np.isclose(con.conv('har', 'ev'), 27.21138505)
def test_conv_fails(): with pytest.raises(ValueError, match=r'.* not of same unit type'): con.conv('ev', 'fs')
def test_conv_unit(): assert np.isclose(con.conv('auto', 'auto'), 1.)
def angax(rotmat, units='rad'): r"""Returns the angle, axis of rotation and determinant of a rotational matrix. Based on the form of **R**, it can be separated into symmetric and antisymmetric components with :math:`(r_{ij} + r_{ji})/2` and :math:`(r_{ij} - r_{ji})/2`, respectively. Then, .. math:: r_{ii} = \cos(a) + u_i^2 (\det(\mathbf{R}) - \cos(a)), \cos(a) = (-\det(\mathbf{R}) + \sum_j r_{jj}) / 2 = (\mathrm{tr}(\mathbf{R}) - \det(\mathbf{R})) / 2. From the expression for :math:`r_{ii}`, the magnitude of :math:`u_i` can be found .. math:: |u_i| = \sqrt{\frac{1 + \det(\mathbf{R}) [2 r_{ii} - \mathrm{tr}(\mathbf{R})])}{3 - \det(\mathbf{R}) \mathrm{tr}(\mathbf{R})}}, which satisfies :math:`u \cdot u = 1`. Note that if :math:`\det(\mathbf{R}) \mathrm{tr}(\mathbf{R}) = 3`, the axis is arbitrary (identity or inversion). Otherwise, the sign can be found from the antisymmetric component of **R**. .. math:: u_i \sin(a) = (r_{jk} - r_{kj}) / 2, \quad i \neq j \neq k, \mathrm{sign}(u_i) = \mathrm{sign}(r_{jk} - r_{kj}), since :math:`\sin(a)` is positive in the range 0 to :math:`\pi`. :math:`i`, :math:`j` and :math:`k` obey the cyclic relation 3 -> 2 -> 1 -> 3 -> ... This fails when :math:`det(\mathbf{R}) \mathrm{tr}(\mathbf{R}) = -1`, in which case the symmetric component of **R** is used .. math:: u_i u_j (\det(\mathbf{R}) - \cos(a)) = (r_{ij} + r_{ji}) / 2, \mathrm{sign}(u_i) \mathrm{sign}(u_j) = \det(\mathbf{R}) \mathrm{sign}(r_{ij} + r_{ji}). The signs can then be found by letting :math:`\mathrm{sign}(u_3) = +1`, since a rotation of :math:`pi` or a reflection are equivalent for antiparallel axes. See http://scipp.ucsc.edu/~haber/ph251/rotreflect_17.pdf Parameters ---------- rotmat : (3, 3) array_like The rotational matrix. units : str, optional The output units for the angle. Default is radians. Returns ------- ang : float The angle of rotation. u : (3,) ndarray The axis of rotation as a 3D vector. det : int The determinant of the rotation matrix. Raises ------ ValueError When the absolute value of the determinant is not equal to 1. """ det = np.linalg.det(rotmat) if not np.isclose(np.abs(det), 1): raise ValueError('Determinant of a rotational matrix must be +/- 1') tr = np.trace(rotmat) ang = con.arccos((tr - det) / 2) * con.conv('rad', units) if np.isclose(det * tr, 3): u = np.array([0, 0, 1]) else: u = np.sqrt((1 + det * (2 * np.diag(rotmat) - tr)) / (3 - det * tr)) if np.isclose(det * tr, -1): sgn = np.ones(3) sgn[1] = det * _nonzero_sign(rotmat[1, 2] + rotmat[2, 1]) sgn[0] = det * sgn[1] * _nonzero_sign(rotmat[0, 1] + rotmat[1, 0]) u *= sgn else: u[0] *= _nonzero_sign(rotmat[1, 2] - rotmat[2, 1]) u[1] *= _nonzero_sign(rotmat[2, 0] - rotmat[0, 2]) u[2] *= _nonzero_sign(rotmat[0, 1] - rotmat[1, 0]) return ang, u, det
def test_conv_len(): assert np.isclose(con.conv('ang', 'pm'), 100.)
def read_zmt(infile, units='ang', hasvec=False, hascom=False): """Reads input file in Z-matrix format. Z-matrix files are in the format: A B 1 R1 C indR2 R2 indA2 A2 D indR3 R3 indA3 A3 indT3 T3 E indR4 R4 indA4 A4 indT4 T4 ... where A, B, C, D, E are atomic labels, indR, indA, indT are reference atom indices, R are bond lengths (in Angstroms), A are bond angles (in degrees) and T are dihedral angles (in degrees). For example, E is a distance R from atom indR with an E-indR-indA angle of A and an E-indR-indA-indT dihedral angle of T. Alternatively, values can be assigned to a list of variables after the Z-matrix (preceded by a blank line). Although the number of atoms is not provided, the unique format of the first atom allows multiple geometries to be read without separation by a comment line. For the time being, vector input is not supported for the Z-matrix file format. """ if hascom: comment = infile.readline().strip() else: comment = '' data = [] vlist = dict() while True: pos = infile.tell() line = infile.readline() split = line.split() if line == '': # end-of-file break elif len(split) == 1 and len(data) > 0: # roll back one line before break infile.seek(pos) break elif split == []: # blank line before variable assignment continue elif split[0] in con.sym: data.append(split) elif split[1] == '=' and len(split) == 3: vlist[split[0]] = float(split[2]) else: # assume it's a comment line and roll back infile.seek(pos) break natm = len(data) if natm < 1: raise IOError('geometry not in Z-matrix format.') elem = np.array([line[0] for line in data]) xyz = np.zeros((natm, 3)) for i in range(natm): if i == 0: # leave first molecule at origin continue elif i == 1: # move along z-axis by R xyz = displace.translate(xyz, _valvar(data[1][2], vlist), [0, 0, 1], ind=1) elif i == 2: indR = int(data[2][1]) - 1 indA = int(data[2][3]) - 1 xyz[2] = xyz[indR] # move from indR towards indA by R xyz = displace.translate(xyz, _valvar(data[2][2], vlist), xyz[indA]-xyz[indR], ind=2) # rotate into xz-plane by A xyz = displace.rotate(xyz, _valvar(data[2][4], vlist), [0, 1, 0], ind=2, origin=xyz[indR], units='deg') else: indR = int(data[i][1]) - 1 indA = int(data[i][3]) - 1 indT = int(data[i][5]) - 1 xyz[i] = xyz[indR] # move from indR towards indA by R xyz = displace.translate(xyz, _valvar(data[i][2], vlist), xyz[indA]-xyz[indR], ind=i) # rotate about (indT-indA)x(indR-indA) by A xyz = displace.rotate(xyz, _valvar(data[i][4], vlist), np.cross(xyz[indT]-xyz[indA], xyz[indR]-xyz[indA]), ind=i, origin=xyz[indR], units='deg') # rotate about indR-indA by T xyz = displace.rotate(xyz, _valvar(data[i][6], vlist), xyz[indR]-xyz[indA], ind=i, origin=xyz[indR], units='deg') xyz = displace.centre_mass(elem, xyz) * con.conv(units, 'ang') if hasvec: vec = np.zeros_like(xyz) else: vec = None return elem, xyz, vec, comment
def test_conv_tim(): assert np.isclose(con.conv('ps', 'fs'), 1e3)