Spanning angle among three atoms.

        The indices `at_1` and `at_3` can be the same (yielding a
        trivial zero angle), but `at_2` must be different from
        both `at_1` and `at_3`.

        Parameters
        ----------
        g_num
            |int| --
            Index of the desired geometry

        at_1
            |int| --
            Index of the first atom

        at_2
            |int| --
            Index of the second atom

        at_3
            |int| --
            Index of the third atom

        Returns
        -------
        angle
            |npfloat_| --
            Spanning angle in degrees between `at_1`-`at_2`-`at_3`, from
            geometry `g_num`

        Raises
        ------
        ~exceptions.IndexError
            If an invalid (out-of-range) `g_num` or `at_#` is provided

        ~exceptions.ValueError
            If `at_2` is equal to either `at_1` or `at_3`

def angle_single(self, g_num, at_1, at_2, at_3):
        """ Spanning angle among three atoms.

        The indices `at_1` and `at_3` can be the same (yielding a
        trivial zero angle), but `at_2` must be different from
        both `at_1` and `at_3`.

        Parameters
        ----------
        g_num
            |int| --
            Index of the desired geometry

        at_1
            |int| --
            Index of the first atom

        at_2
            |int| --
            Index of the second atom

        at_3
            |int| --
            Index of the third atom

        Returns
        -------
        angle
            |npfloat_| --
            Spanning angle in degrees between `at_1`-`at_2`-`at_3`, from
            geometry `g_num`

        Raises
        ------
        ~exceptions.IndexError
            If an invalid (out-of-range) `g_num` or `at_#` is provided

        ~exceptions.ValueError
            If `at_2` is equal to either `at_1` or `at_3`

        """

        # Imports
        import numpy as np
        from .utils import safe_cast as scast
        from .utils.vector import vec_angle

        # The below errors are explicitly thrown since they are multiplied by
        #  three when they are used as an index and thus give non-intuitive
        #  errors in later code.
        # Complain if at_1 is invalid
        if not(-self.num_atoms <= at_1 < self.num_atoms):
            raise IndexError("Invalid index for 'at_1' ({0})".format(at_1))

        # Complain if at_2 is invalid
        if not(-self.num_atoms <= at_2 < self.num_atoms):
            raise IndexError("Invalid index for 'at_2' ({0})".format(at_2))

        # Complain if at_3 is invalid
        if not(-self.num_atoms <= at_3 < self.num_atoms):
            raise IndexError("Invalid index for 'at_3' ({0})".format(at_3))

        # Should never be necessary (save for badly erroneous calling code),
        #  but coerce the at_x to their floor() values.  This is again
        #  needed since they are multiplied by three in the index expresssions
        #  below, and can cause funny behavior when truncated by the indexing
        at_1 = scast(np.floor(at_1), np.int_)
        at_2 = scast(np.floor(at_2), np.int_)
        at_3 = scast(np.floor(at_3), np.int_)

        # Complain if at_2 is equal to either at_1 or at_3.  Must factor in
        #  the possibility of negative indexing via modulo arithmetic.
        if (at_2 % self.num_atoms) == (at_1 % self.num_atoms):
            raise ValueError("'at_1' and 'at_2' must be different")
        if (at_2 % self.num_atoms) == (at_3 % self.num_atoms):
            raise ValueError("'at_2' and 'at_3' must be different")

        # Trivial return if at_1 and at_3 are the same
        if (at_1 % self.num_atoms) == (at_3 % self.num_atoms):
            # Angle is identically zero in this case
            return 0.0
        ## end if

        # Store the displacement vectors from at_2 to at_1 and to at_3
        # The np.float64 type should be retained through the displ_single call.
        vec_2_1 = self.displ_single(g_num, at_2, at_1)
        vec_2_3 = self.displ_single(g_num, at_2, at_3)

        # Compute and return the calculated angle, in degrees
        # v1 {dot} v2 == |v1||v2| * cos(theta)
        angle = vec_angle(vec_2_1, vec_2_3)
        return angle