def dihedral(a1, a2=None, a3=None, a4=None): """Twist angle of bonds a1-a2 and a4-a3 around around the central bond a2-a3 Can be called as ``dihedral(a1, a2, a3, a4)`` OR ``dihedral(a2, a2)`` OR ``dihedral(bond)`` Args: a1 (mdt.Bond): the central bond in the dihedral. OR a1,a2 (mdt.Atom): the atoms describing the dihedral a3,a4 (mdt.Atom): (optional) if not passed, ``a1`` and ``a2`` will be treated as the central atoms in this bond, and a3 and a4 will be inferred. Returns: (units.Scalar[angle]): angle - [0, 2 pi) radians """ if a3 is a4 is None: # infer the first and last atoms a1, a2, a3, a4 = _infer_dihedral(a1, a2) r21 = (a1.position - a2.position).defunits_value() # remove units immediately to improve speed r34 = (a4.position - a3.position).defunits_value() dim = len(r21.shape) center_bond = (a2.position - a3.position).defunits_value() plane_normal = normalized(center_bond) if dim == 1: r21_proj = r21 - plane_normal * r21.dot(plane_normal) r34_proj = r34 - plane_normal * r34.dot(plane_normal) else: assert dim == 2 r21_proj = r21 - plane_normal * (r21*plane_normal).sum(axis=1)[:, None] r34_proj = r34 - plane_normal * (r34*plane_normal).sum(axis=1)[:, None] va = normalized(r21_proj) vb = normalized(r34_proj) if dim == 1: costheta = np.dot(va, vb) if np.allclose(costheta, 1.0): return 0.0 * u.radians elif np.allclose(costheta, -1.0): return u.pi * u.radians else: costheta = (va*vb).sum(axis=1) abstheta = safe_arccos(costheta) cross = np.cross(va, vb) if dim == 1: theta = abstheta * np.sign(np.dot(cross, plane_normal)) else: theta = abstheta * np.sign((cross*plane_normal).sum(axis=1)) return (theta * u.radians) % (2.0 * u.pi * u.radians)
def test_bond_alignment_on_axis(benzene): mol = benzene.copy() directions = ['x', 'y', 'z', [1,2,3.0], [0,1,0], [0.1, 0.1, 0.1] * u.angstrom] for i, dir in enumerate(directions): bond = mdt.Bond(*random.sample(mol.atoms, 2)) center = (i % 2) == 0.0 bond.align(dir, centered=center) if center: np.testing.assert_allclose(bond.midpoint, np.zeros(3), atol=1.e-12) np.testing.assert_allclose(bond.a1.position.defunits_value(), -bond.a2.position.defunits_value(), atol=1e-10) if isinstance(dir, str): if dir == 'x': d = np.array([1.0, 0, 0]) elif dir == 'y': d = np.array([0.0, 1.0, 0.0]) elif dir == 'z': d = np.array([0.0, 0.0, 1.0]) else: raise WtfError() else: d = normalized(u.array(dir)) newvec = (bond.a2.position - bond.a1.position).normalized() assert abs(1.0 - d.dot(newvec)) < 1e-10
def test_perpendicular(setupfn): arr, expected_norm = setupfn() vectorized = len(arr.shape) > 1 normvecs = mathutils.normalized(arr) perpvecs = mathutils.perpendicular(arr) assert isinstance(perpvecs, np.ndarray) assert not hasattr(perpvecs, 'units') # test that vectors are indeed perpendicular if vectorized: assert (np.abs((normvecs * perpvecs).sum(axis=1)) < 1e-12).all() else: assert abs(perpvecs.dot(normvecs)) < 1e-12 # test that they are unit vectors (or 0 if the input vector is zero) if not vectorized: arr = [arr] perpvecs = [perpvecs] expected_norm = [expected_norm] for i, (vec, perpvec) in enumerate(zip(arr, perpvecs)): if expected_norm[i] == 0.0: assert mathutils.norm(perpvec) < 1e-12 else: assert np.abs(1.0 - mathutils.norm(perpvec)) < 1e-12
def test_bond_alignment_on_axis(benzene): mol = benzene.copy() directions = [ 'x', 'y', 'z', [1, 2, 3.0], [0, 1, 0], [0.1, 0.1, 0.1] * u.angstrom ] for i, dir in enumerate(directions): bond = mdt.Bond(*random.sample(mol.atoms, 2)) center = (i % 2) == 0.0 bond.align(dir, centered=center) if center: np.testing.assert_allclose(bond.midpoint, np.zeros(3), atol=1.e-12) np.testing.assert_allclose(bond.a1.position.defunits_value(), -bond.a2.position.defunits_value(), atol=1e-10) if isinstance(dir, str): if dir == 'x': d = np.array([1.0, 0, 0]) elif dir == 'y': d = np.array([0.0, 1.0, 0.0]) elif dir == 'z': d = np.array([0.0, 0.0, 1.0]) else: raise WtfError() else: d = normalized(u.array(dir)) newvec = (bond.a2.position - bond.a1.position).normalized() assert abs(1.0 - d.dot(newvec)) < 1e-10
def test_normalized(setupfn): arr, expected_norm = setupfn() vectorized = len(arr.shape) > 1 normed = mathutils.normalized(arr) if not vectorized: arr, expected_norm, normed = [arr], [expected_norm], [normed] for v, n, unitvec in zip(arr, expected_norm, normed): if n != 0: helpers.assert_almost_equal(unitvec, v / n)
def test_normalized(setupfn): arr, expected_norm = setupfn() vectorized = len(arr.shape) > 1 normed = mathutils.normalized(arr) if not vectorized: arr, expected_norm, normed = [arr], [expected_norm], [normed] for v, n, unitvec in zip(arr, expected_norm, normed): if n != 0: helpers.assert_almost_equal(unitvec, v/n)
def test_align_two_bonds(benzene, h2): h2 = h2.copy() alignbond = list(h2.bonds)[0] one_was_different = False for targetbond in benzene.bonds: # sanity checks before we start: assert not np.allclose(alignbond.midpoint.defunits_value(), targetbond.midpoint.defunits_value()) vec1, vec2 = (normalized(b.a2.position - b.a1.position) for b in (alignbond, targetbond)) one_was_different = (one_was_different or vec1.dot(vec2) < 0.8) alignbond.align(targetbond) np.testing.assert_allclose(alignbond.midpoint, targetbond.midpoint) vec1, vec2 = (normalized(b.a2.position - b.a1.position) for b in (alignbond, targetbond)) assert (1.0 - vec1.dot(vec2)) < 1e-8
def angle(a1, a2, a3): """ The angle between bonds a2-a1 and a2-a3 Args: a1,a2,a3 (mdt.Atom): the atoms describing the angle Returns: u.Scalar[length]: the distance """ r21 = (a1.position - a2.position).defunits_value() # remove units immediately to improve speed r23 = (a3.position - a2.position).defunits_value() e12 = normalized(r21) e23 = normalized(r23) dim = len(e12.shape) if dim == 2: costheta = (e12*e23).sum(axis=1) else: assert dim == 1 costheta = np.dot(e12, e23) theta = safe_arccos(costheta) return theta * u.radians
def distance_gradient(a1, a2): r""" Gradient of the distance between two atoms, .. math:: \frac{\partial \mathbf{R}_1}{\partial \mathbf{r}} ||\mathbf{R}_1 - \mathbf{R}_2|| = \frac{\mathbf{R}_1 - \mathbf{R}_2}{||\mathbf{R}_1 - \mathbf{R}_2||} Args: a1,a2 (mdt.Atom): the two atoms Returns: Tuple[u.Vector[length], u.Vector[length]]: (gradient w.r.t. first atom, gradient w.r.t. second atom) """ d = normalized(a1.position-a2.position) return d, -d