示例#1
0
def test_vector_as_ell_1_modes(special_angles):
    indices = np.array([[1, -1], [1, 0], [1, 1]])

    def nhat(theta, phi):
        return np.array([
            math.sin(theta) * math.cos(phi),
            math.sin(theta) * math.sin(phi),
            math.cos(theta)
        ])

    np.random.seed(123)
    for rep in range(1000):
        vector = np.random.uniform(-1, 1, size=(3, ))
        vec_ell_m = sf.vector_as_ell_1_modes(vector)
        assert np.allclose(vector,
                           sf.vector_from_ell_1_modes(vec_ell_m),
                           atol=1.0e-16,
                           rtol=1.0e-15)
        for theta in special_angles:
            for phi in special_angles:
                dot1 = np.dot(vector, nhat(theta, phi))
                dot2 = np.dot(
                    vec_ell_m,
                    sf.SWSH(quaternion.from_spherical_coords(theta, phi), 0,
                            indices)).real
                assert abs(dot1 - dot2) < 1e-15
示例#2
0
def test_from_spherical_coords():
    np.random.seed(1843)
    random_angles = [[np.random.uniform(-np.pi, np.pi), np.random.uniform(-np.pi, np.pi)]
                     for i in range(5000)]
    for vartheta, varphi in random_angles:
        assert abs((np.quaternion(0, 0, 0, varphi / 2.).exp() * np.quaternion(0, 0, vartheta / 2., 0).exp())
                   - quaternion.from_spherical_coords(vartheta, varphi)) < 1.e-15
示例#3
0
 def swsh(self, s, l, m, theta, phi):
     # get the rotor related for Wigner matrix
     # Note: We need to specify the (-theta,-phi direction to match the convenction used in HAD)
     R_tp = quaternion.from_spherical_coords(-theta, -phi)
     W = sf.Wigner_D_element(R_tp, l, m, -s)
     #print(W)
     return ((-1)**(s)) * math.sqrt((2 * l + 1) / (4 * math.pi)) * W
示例#4
0
    def swsh(self,s,l,m,theta,phi):

        # get the rotor related for Wigner matrix
        R_tp = quaternion.from_spherical_coords(theta, phi)
        W=sf.Wigner_D_element(R_tp,l,m,-s)
        #print(W)
        return ((-1)**s ) *math.sqrt((2*l+1)/(4*math.pi)) * W     
示例#5
0
def boosted_grid(frame_rotation, boost_velocity, n_theta, n_phi):
    beta = np.linalg.norm(boost_velocity)
    gamma = 1 / math.sqrt(1 - beta**2)
    rapidity = math.atanh(beta)

    # Construct the function that modifies our rotor grid to account for the boost
    if beta > 3e-14:  # Tolerance for beta; any smaller and numerical errors will have greater effect
        vhat = boost_velocity / beta

        def Bprm_j_k(thetaprm, phiprm):
            """Construct rotor taking r' to r

            I derived this result in a different way, but I've also found it described in
            Penrose-Rindler Vol. 1, around Eq. (1.3.5).  Note, however, that their discussion is for
            the past celestial sphere, so there's a sign difference.

            """
            # Note: It doesn't matter which we use -- r' or r; all we need is the direction of the
            # bivector spanned by v and r', which is the same as the direction of the bivector
            # spanned by v and r, since either will be normalized, and one cross product is zero iff
            # the other is zero.
            rprm = np.array([
                math.cos(phiprm) * math.sin(thetaprm),
                math.sin(phiprm) * math.sin(thetaprm),
                math.cos(thetaprm)
            ])
            Thetaprm = math.acos(np.dot(vhat, rprm))
            Theta = 2 * math.atan(
                math.exp(-rapidity) * math.tan(Thetaprm / 2.0))
            rprm_cross_vhat = np.quaternion(0.0, *np.cross(rprm, vhat))
            if rprm_cross_vhat.abs() > 1e-200:
                return (rprm_cross_vhat.normalized() * (Thetaprm - Theta) /
                        2).exp()
            else:
                return quaternion.one

    else:

        def Bprm_j_k(thetaprm, phiprm):
            return quaternion.one

    # These are the angles in the transformed system at which we need to know the function values
    thetaprm_phiprm = sf.theta_phi(n_theta, n_phi)

    # Set up rotors that we can use to evaluate the SWSHs in the original frame
    R_j_k = np.empty((n_theta, n_phi), dtype=np.quaternion)
    for j in range(n_theta):
        for k in range(n_phi):
            thetaprm_j, phiprm_k = thetaprm_phiprm[j, k]
            R_j_k[j,
                  k] = (Bprm_j_k(thetaprm_j, phiprm_k) * frame_rotation *
                        quaternion.from_spherical_coords(thetaprm_j, phiprm_k))

    return R_j_k
示例#6
0
def test_SWSH_conjugation(special_angles, ell_max):
    # {s}Y{l,m}.conjugate() = (-1.)**(s+m) {-s}Y{l,-m}
    indices1 = sf.LM_range(0, ell_max)
    indices2 = np.array([[ell, -m] for ell, m in indices1])
    neg1_to_m = np.array([(-1.)**m for ell, m in indices1])
    for iota in special_angles:
        for phi in special_angles:
            R = quaternion.from_spherical_coords(iota, phi)
            for s in range(1-ell_max, ell_max):
                assert np.allclose(sf.SWSH(R, s, indices1),
                                   (-1.)**s * neg1_to_m * np.conjugate(sf.SWSH(R, -s, indices2)),
                                   atol=1e-15, rtol=1e-15)
def test_constant_as_ell_0_mode(special_angles):
    indices = np.array([[0, 0]])
    np.random.seed(123)
    for imaginary_part in [0.0, 1.0j]:  # Test both real and imaginary constants
        for rep in range(1000):
            constant = np.random.uniform(-1, 1) + imaginary_part * np.random.uniform(-1, 1)
            const_ell_m = sf.constant_as_ell_0_mode(constant)
            assert abs(constant - sf.constant_from_ell_0_mode(const_ell_m)) < 1e-15
            for theta in special_angles:
                for phi in special_angles:
                    dot = np.dot(const_ell_m, sf.SWSH(quaternion.from_spherical_coords(theta, phi), 0, indices))
                    assert abs(constant - dot) < 1e-15
示例#8
0
def swsh(s, modes, theta, phi, psi=0):
    """
    Return a value of a spin-weighted spherical harmonic of spin-weight s. 
    If passed a list of several modes, then a numpy array is returned with 
    SWSH values of each mode for the given point.
    
    For one mode:       swsh(s,[(l,m)],theta,phi,psi=0)
    For several modes:  swsh(s,[(l1,m1),(l2,m2),(l3,m3),...],theta,phi,psi=0)
    """
    import spherical_functions as sf
    import quaternion as qt

    return sf.SWSH(qt.from_spherical_coords(theta, phi), s, modes) * np.exp(
        1j * s * psi
    )
示例#9
0
def euler_to_rot_quat(phi, th):
    """Convert Euler angles to rotation quaternion.

    By project convention, the Euler angles are a composition of
    a rotation about the original x axis (roll),
    a rotation about the original y axis (pitch), and
    a rotation about the original z axis (yaw).
    This is also known as x-y-z, consisting of extrinsic rotations.

    Args:
        phi (float): First rotation about y axis (pitch).
        th (float): Second rotation about unrotated z axis (yaw).

    Returns:
        np.quaternion: Rotation quaterion.
    """
    return quaternion.from_spherical_coords(th, phi)
示例#10
0
def quat_from_spherical(theta, phi):
    """
    Return the quaternion from the spherical coordinates.

    Parameters
    ----------
    theta: float
        Poloidal angle.
    phi: float
        Toroidal angle.

    Returns
    -------
    quat: quaternion
         Rotational quaternion.
    """
    return quaternion.from_spherical_coords(theta, phi)
示例#11
0
def test_constant_as_ell_0_mode(special_angles):
    indices = np.array([[0, 0]])
    np.random.seed(123)
    for imaginary_part in [0.0,
                           1.0j]:  # Test both real and imaginary constants
        for rep in range(1000):
            constant = np.random.uniform(
                -1, 1) + imaginary_part * np.random.uniform(-1, 1)
            const_ell_m = sf.constant_as_ell_0_mode(constant)
            assert abs(constant -
                       sf.constant_from_ell_0_mode(const_ell_m)) < 1e-15
            for theta in special_angles:
                for phi in special_angles:
                    dot = np.dot(
                        const_ell_m,
                        sf.SWSH(quaternion.from_spherical_coords(theta, phi),
                                0, indices))
                    assert abs(constant - dot) < 1e-15
def test_vector_as_ell_1_modes(special_angles):
    indices = np.array([[1, -1], [1, 0], [1, 1]])

    def nhat(theta, phi):
        return np.array([math.sin(theta) * math.cos(phi),
                         math.sin(theta) * math.sin(phi),
                         math.cos(theta)])

    np.random.seed(123)
    for rep in range(1000):
        vector = np.random.uniform(-1, 1, size=(3,))
        vec_ell_m = sf.vector_as_ell_1_modes(vector)
        assert np.allclose(vector, sf.vector_from_ell_1_modes(vec_ell_m), atol=1.0e-16, rtol=1.0e-15)
        for theta in special_angles:
            for phi in special_angles:
                dot1 = np.dot(vector, nhat(theta, phi))
                dot2 = np.dot(vec_ell_m, sf.SWSH(quaternion.from_spherical_coords(theta, phi), 0, indices)).real
                assert abs(dot1 - dot2) < 1e-15
示例#13
0
    def rotations_in_quaternions_region(self,
                                        rotations='All',
                                        rotations_distribution='healpix',
                                        nside=8,
                                        with_network=True):

        if rotations == 'All':
            if rotations_distribution == 'healpix':  #nside debe ser potencia de 2 para que sea regular y tengan 8 vecinos cada uno
                num_rotations = hp.nside2npix(nside)
                sphere_coors = hp.pix2ang(nside,
                                          np.arange(num_rotations),
                                          nest=False)
                rotations = quaternion.from_spherical_coords(
                    sphere_coors[0], sphere_coors[1])
                self.rotations = rotations
                self.nside = nside
                self.num_rotations = num_rotations
                del (rotations)
        pass
示例#14
0
    def from_spherical(theta, phi):
        """
        Return the quaternion from the spherical coordinates.

        Parameters
        ----------
        theta: float
            Poloidal angle.
        phi: float
            Toroidal angle.

        Returns
        -------
        quat: quaternion
             Rotational quaternion.
        """
        q = quaternion.from_spherical_coords(theta, phi)
        a = quaternion.as_float_array(q)
        return Quaternion(a[0], a[1], a[2], a[3])
示例#15
0
def arbRot(v, o, rotV):
    """
    Arbitraty rotation of v at o about rotV.
    
        Inputs:
            v -- (3, 1) vector, list or array
            o -- center point of rotation
            rotV -- (2,1) vector, passed to quaternion.from_spherical_coords
        Returns
            rotated -- returns rotated vector
    """
    q = qt.from_spherical_coords(rotV)
    rotM = R.from_quat([q.x, q.y, q.z, q.w])
    tranM = np.eye(4)
    tranM[:-1, -1] = -np.array(o)

    vec = np.hstack((v, np.ones((v.shape[0], 1))))
    vec = rotM.apply(vec.dot(tranM.T)[:, :3])
    vec = (np.hstack((vec, np.ones((vec.shape[0], 1))))).dot(-tranM.T)

    return vec[:, :3]
示例#16
0
def process_transformation_kwargs(ell_max, **kwargs):
    # Build the supertranslation and spacetime_translation arrays
    supertranslation = np.zeros((4, ),
                                dtype=complex)  # For now; may be resized below
    ell_max_supertranslation = 1  # For now; may be increased below
    if "supertranslation" in kwargs:
        supertranslation = np.array(kwargs.pop("supertranslation"),
                                    dtype=complex)
        if supertranslation.dtype != "complex" and supertranslation.size > 0:
            # I don't actually think this can ever happen...
            raise TypeError(
                "\nInput argument `supertranslation` should be a complex array with size>0.\n"
                "Got a {} array of shape {}.".format(supertranslation.dtype,
                                                     supertranslation.shape))
        # Make sure the array has size at least 4, by padding with zeros
        if supertranslation.size <= 4:
            supertranslation = np.lib.pad(supertranslation,
                                          (0, 4 - supertranslation.size),
                                          "constant",
                                          constant_values=(0.0, ))
        # Check that the shape is a possible array of scalar modes with complete (ell,m) data
        ell_max_supertranslation = int(np.sqrt(len(supertranslation))) - 1
        if (ell_max_supertranslation + 1)**2 != len(supertranslation):
            raise ValueError(
                "\nInput supertranslation parameter must contain modes from ell=0 up to some ell_max, "
                "including\nall relevant m modes in standard order (see `spherical_functions` "
                "documentation for details).\nThus, it must be an array with length given by a "
                "perfect square; its length is {}".format(
                    len(supertranslation)))
        # Check that the resulting supertranslation will be real
        for ell in range(ell_max_supertranslation + 1):
            for m in range(ell + 1):
                i_pos = sf.LM_index(ell, m, 0)
                i_neg = sf.LM_index(ell, -m, 0)
                a = supertranslation[i_pos]
                b = supertranslation[i_neg]
                if abs(a - (-1.0)**m * b.conjugate()) > 3e-16 + 1e-15 * abs(b):
                    raise ValueError(
                        f"\nsupertranslation[{i_pos}]={a}  # (ell,m)=({ell},{m})\n"
                        +
                        "supertranslation[{}]={}  # (ell,m)=({},{})\n".format(
                            i_neg, b, ell, -m) +
                        "Will result in an imaginary supertranslation.")
    spacetime_translation = np.zeros((4, ), dtype=float)
    spacetime_translation[0] = sf.constant_from_ell_0_mode(
        supertranslation[0]).real
    spacetime_translation[1:4] = -sf.vector_from_ell_1_modes(
        supertranslation[1:4]).real
    if "spacetime_translation" in kwargs:
        st_trans = np.array(kwargs.pop("spacetime_translation"), dtype=float)
        if st_trans.shape != (4, ) or st_trans.dtype != "float":
            raise TypeError(
                "\nInput argument `spacetime_translation` should be a float array of shape (4,).\n"
                "Got a {} array of shape {}.".format(st_trans.dtype,
                                                     st_trans.shape))
        spacetime_translation = st_trans[:]
        supertranslation[0] = sf.constant_as_ell_0_mode(
            spacetime_translation[0])
        supertranslation[1:4] = sf.vector_as_ell_1_modes(
            -spacetime_translation[1:4])
    if "space_translation" in kwargs:
        s_trans = np.array(kwargs.pop("space_translation"), dtype=float)
        if s_trans.shape != (3, ) or s_trans.dtype != "float":
            raise TypeError(
                "\nInput argument `space_translation` should be an array of floats of shape (3,).\n"
                "Got a {} array of shape {}.".format(s_trans.dtype,
                                                     s_trans.shape))
        spacetime_translation[1:4] = s_trans[:]
        supertranslation[1:4] = sf.vector_as_ell_1_modes(
            -spacetime_translation[1:4])
    if "time_translation" in kwargs:
        t_trans = kwargs.pop("time_translation")
        if not isinstance(t_trans, float):
            raise TypeError(
                "\nInput argument `time_translation` should be a single float.\n"
                "Got {}.".format(t_trans))
        spacetime_translation[0] = t_trans
        supertranslation[0] = sf.constant_as_ell_0_mode(
            spacetime_translation[0])

    # Decide on the number of points to use in each direction.  A nontrivial supertranslation will introduce
    # power in higher modes, so for best accuracy, we need to account for that.  But we'll make it a firm
    # requirement to have enough points to capture the original waveform, at least
    w_ell_max = ell_max
    ell_max = w_ell_max + ell_max_supertranslation
    n_theta = kwargs.pop("n_theta", 2 * ell_max + 1)
    n_phi = kwargs.pop("n_phi", 2 * ell_max + 1)
    if n_theta < 2 * ell_max + 1 and abs(supertranslation[1:]).max() > 0.0:
        warning = (
            f"n_theta={n_theta} is small; because of the supertranslation, " +
            f"it will lose accuracy for anything less than 2*ell+1={ell_max}")
        warnings.warn(warning)
    if n_theta < 2 * w_ell_max + 1:
        raise ValueError(f"n_theta={n_theta} is too small; " +
                         "must be at least 2*ell+1={}".format(2 * w_ell_max +
                                                              1))
    if n_phi < 2 * ell_max + 1 and abs(supertranslation[1:]).max() > 0.0:
        warning = (
            f"n_phi={n_phi} is small; because of the supertranslation, " +
            f"it will lose accuracy for anything less than 2*ell+1={ell_max}")
        warnings.warn(warning)
    if n_phi < 2 * w_ell_max + 1:
        raise ValueError(f"n_phi={n_phi} is too small; " +
                         "must be at least 2*ell+1={}".format(2 * w_ell_max +
                                                              1))

    # Get the rotor for the frame rotation
    frame_rotation = np.quaternion(
        *np.array(kwargs.pop("frame_rotation", [1, 0, 0, 0]), dtype=float))
    if frame_rotation.abs() < 3e-16:
        raise ValueError(
            f"frame_rotation={frame_rotation} should be a unit quaternion")
    frame_rotation = frame_rotation.normalized()

    # Get the boost velocity vector
    boost_velocity = np.array(kwargs.pop("boost_velocity", [0.0] * 3),
                              dtype=float)
    beta = np.linalg.norm(boost_velocity)
    if boost_velocity.shape != (3, ) or beta >= 1.0:
        raise ValueError(
            "Input boost_velocity=`{}` should be a 3-vector with "
            "magnitude strictly less than 1.0.".format(boost_velocity))
    gamma = 1 / math.sqrt(1 - beta**2)
    varphi = math.atanh(beta)

    # These are the angles in the transformed system at which we need to know the function values
    thetaprm_j_phiprm_k = np.array([[
        [thetaprm_j, phiprm_k]
        for phiprm_k in np.linspace(0.0, 2 * np.pi, num=n_phi, endpoint=False)
    ] for thetaprm_j in np.linspace(0.0, np.pi, num=n_theta, endpoint=True)])

    # Construct the function that modifies our rotor grid to account for the boost
    if beta > 3e-14:  # Tolerance for beta; any smaller and numerical errors will have greater effect
        vhat = boost_velocity / beta

        def Bprm_j_k(thetaprm, phiprm):
            """Construct rotor taking r' to r

            I derived this result in a different way, but I've also found it described in Penrose-Rindler Vol. 1,
            around Eq. (1.3.5).  Note, however, that their discussion is for the past celestial sphere,
            so there's a sign difference.

            """
            # Note: It doesn't matter which we use -- r' or r; all we need is the direction of the bivector
            # spanned by v and r', which is the same as the direction of the bivector spanned by v and r,
            # since either will be normalized, and one cross product is zero iff the other is zero.
            rprm = np.array([
                math.cos(phiprm) * math.sin(thetaprm),
                math.sin(phiprm) * math.sin(thetaprm),
                math.cos(thetaprm)
            ])
            Thetaprm = math.acos(np.dot(vhat, rprm))
            Theta = 2 * math.atan(math.exp(-varphi) * math.tan(Thetaprm / 2.0))
            rprm_cross_vhat = np.quaternion(0.0, *np.cross(rprm, vhat))
            if rprm_cross_vhat.abs() > 1e-200:
                return (rprm_cross_vhat.normalized() * (Thetaprm - Theta) /
                        2).exp()
            else:
                return quaternion.one

    else:

        def Bprm_j_k(thetaprm, phiprm):
            return quaternion.one

    # Set up rotors that we can use to evaluate the SWSHs in the original frame
    R_j_k = np.empty(thetaprm_j_phiprm_k.shape[:2], dtype=np.quaternion)
    for j in range(thetaprm_j_phiprm_k.shape[0]):
        for k in range(thetaprm_j_phiprm_k.shape[1]):
            thetaprm_j, phiprm_k = thetaprm_j_phiprm_k[j, k]
            R_j_k[j,
                  k] = (Bprm_j_k(thetaprm_j, phiprm_k) * frame_rotation *
                        quaternion.from_spherical_coords(thetaprm_j, phiprm_k))

    return (
        supertranslation,
        ell_max_supertranslation,
        ell_max,
        n_theta,
        n_phi,
        boost_velocity,
        beta,
        gamma,
        varphi,
        R_j_k,
        Bprm_j_k,
        thetaprm_j_phiprm_k,
        kwargs,
    )
def add_noise_poke_vector(vec, std_dev_deg=1):
    theta_phi = np.radians(np.random.normal(0, std_dev_deg, size=(1, 2)))
    q = quaternion.from_spherical_coords(theta_phi)
    vec_rot = quaternion.rotate_vectors(q, vec)
    return vec_rot[0]
示例#18
0
 def from_spherical_coords(theta_phi, phi=None):
     return quaternion.fromQuaternion(qt.from_spherical_coords(theta_phi, phi))
示例#19
0
def talker():
    pub = rospy.Publisher('ee_data', end_effector, queue_size=10)
    rospy.init_node('talker', anonymous=True)
    rate = rospy.Rate(100)  # 100hz

    #Quaternion for example orientation
    q0 = quaternion.from_spherical_coords(0, 0)
    q1 = quaternion.from_spherical_coords(pi, 0)
    q2 = quaternion.from_spherical_coords(pi / 2, 0)

    while not rospy.is_shutdown():
        data = end_effector()
        time = rospy.get_time()
        data.time = time
        dt = 0.1
        # #Modify next value will change end-effector position and velocity
        # data.position = [0,0,0,0,0,0.7]
        # data.velocity = [1,1,1,1,1,1]
        # pub.publish(data)
        # rospy.loginfo(data)
        # rate.sleep()

        #Some test for trajectory execution

        if time < 10:
            q_sl = quaternion.slerp(q0, q1, 0, 10, time)
            q_sl_old = quaternion.slerp(q0, q1, 0, 10, time - 1)
            q_sl_d = (q_sl - q_sl_old) / dt

            w_sl = -2 * q_sl.conjugate() * q_sl_d
            w_sl = quaternion.as_float_array(w_sl)

            q_sl = quaternion.as_euler_angles(q_sl)

            data.position = [0, 0, 0, 0.5, 0.5, 0.5]
            data.velocity = [10, 10, 10, 10, 10, 10]

            pub.publish(data)
            rate.sleep()

        elif time >= 10 and time < 20:
            q_sl = quaternion.slerp(q1, q2, 10, 20, time)
            q_sl_old = quaternion.slerp(q1, q2, 10, 20, time - 1)

            q_sl_d = (q_sl - q_sl_old) / dt

            w_sl = -2 * q_sl.conjugate() * q_sl_d
            w_sl = quaternion.as_float_array(w_sl)

            q_sl = quaternion.as_euler_angles(q_sl)

            data.position = [0, 0, 0, 0, 0, 1]
            data.velocity = [10, 10, 10, 10, 10, 10]

            pub.publish(data)
            rate.sleep()

        else:
            q_sl = quaternion.as_euler_angles(q0)

            data.position = [0, 0, 0, 0, 0, 1.2]
            data.velocity = [0, 0, 0, 0, 0, 0]

            pub.publish(data)
            rate.sleep()
示例#20
0
    def from_modes(cls, w_modes, **kwargs):
        """Construct grid object from modes, with optional BMS transformation

        This "constructor" is designed with the goal of transforming the frame in which the modes are measured.  If
        this is not desired, it can be called without those parameters.

        It is important to note that the input transformation parameters are applied in the order listed in the
        parameter list below:
          1. (Super)Translations
          2. Rotation (about the origin of the translated system)
          3. Boost
        All input parameters refer to the transformation required to take the mode's inertial frame onto the inertial
        frame of the grid's inertial observers.  In what follows, the inertial frame of the modes will be unprimed,
        while the inertial frame of the grid will be primed.

        The translations (space, time, spacetime, or super) can be given in various ways, which may override each
        other.  Ultimately, however, they are essentially combined into a single function `alpha`, representing the
        supertranslation, which transforms the asymptotic time variable `u` as
          u'(theta, phi) = u - alpha(theta, phi)
        A simple time translation would correspond to
          alpha(theta, phi) = time_translation
        A pure spatial translation would correspond to
          alpha(theta, phi) = np.dot(space_translation, -nhat(theta, phi))
        where `np.dot` is the usual dot product, and `nhat` is the unit vector in the given direction.


        Parameters
        ----------
        w_modes : WaveformModes
            The object storing the modes of the original waveform, which will be converted to values on a grid in
            this function.  This is the only required argument to this function.
        n_theta : int, optional
        n_phi : int, optional
            Number of points in the equi-angular grid in the colatitude (theta) and azimuth (phi) directions. Each
            defaults to 2*ell_max+1, which is optimal for accuracy and speed.  However, this ell_max will typically
            be greater than the input waveform's ell_max by at least one, or the ell_max of the input
            supertranslation (whichever is greater).  This is to minimally account for the power at higher orders
            that such a supertranslation introduces.  You may wish to increase this further if the spatial size of
            your supertranslation is large compared to the smallest wavelength you wish to capture in your data
            [e.g., ell_max*Omega_orbital_max/speed_of_light], or if your boost speed is close to the speed of light.
        time_translation : float, optional
            Defaults to zero.  Nonzero overrides spacetime_translation and supertranslation.
        space_translation : float array of length 3, optional
            Defaults to empty (no translation).  Non-empty overrides spacetime_translation and supertranslation.
        spacetime_translation : float array of length 4, optional
            Defaults to empty (no translation).  Non-empty overrides supertranslation.
        supertranslation : complex array, optional
            This gives the complex components of the spherical-harmonic expansion of the supertranslation in standard
            form, starting from ell=0 up to some ell_max, which may be different from the ell_max of the input
            WaveformModes object.  Supertranslations must be real, so these values must obey the condition
              alpha^{ell,m} = (-1)^m \bar{alpha}^{ell,-m}
            Defaults to empty, which causes no supertranslation.
        frame_rotation : quaternion, optional
            Transformation applied to (x,y,z) basis of the mode's inertial frame.  For example, the basis z vector of
            the new grid frame may be written as
              z' = frame_rotation * z * frame_rotation.inverse()
            Defaults to 1, corresponding to the identity transformation (no frame_rotation).
        boost_velocity : float array of length 3, optional
            This is the three-velocity vector of the grid frame relative to the mode frame.  The norm of this vector
            is checked to ensure that it is smaller than 1.  Defaults to [], corresponding to no boost.

        Returns
        -------
        WaveformGrid

        """
        # Check input object type and frame type
        #
        # The data in `w_modes` is given in the original frame.  We need to get the value of the field on a grid of
        # points corresponding to the points in the new grid frame.  But we must also remember that this is a
        # spin-weighted and boost-weighted field, which means that we need to account for the frame_rotation due to
        # `frame_rotation` and `boost_velocity`.  The way to do this is to use rotors to transform the points as needed,
        # and evaluate the SWSHs.  But for now, let's just reject any waveforms in a non-inertial frame
        if not isinstance(w_modes, WaveformModes):
            raise TypeError("\nInput waveform object must be an instance of `WaveformModes`; "
                            "this is of type `{0}`".format(type(w_modes).__name__))
        if w_modes.frameType != Inertial:
            raise ValueError("\nInput waveform object must be in an inertial frame; "
                             "this is in a frame of type `{0}`".format(w_modes.frame_type_string))

        # The first task is to establish a set of constant u' slices on which the new grid should be evaluated.  This
        # is done simply by translating the original set of slices by the time translation (the lowest moment of the
        # supertranslation).  But some of these slices (at the beginning and end) will not have complete data,
        # because of the direction-dependence of the rest of the supertranslation.  That is, in some directions,
        # the data for the complete slice (data in all directions on the sphere) of constant u' will actually refer to
        # spacetime events that were not in the original set of time slices; we would have to extrapolate the original
        # data.  So, for nontrivial supertranslations, the output data will actually represent a proper subset of the
        # input data.
        #
        # We can invert the equation for u' to obtain u as a function of angle assuming constant u'
        #   u'(theta, phi) = u + alpha(theta, phi) + u * np.dot(boost_velocity, nhat(theta, phi))
        #   u(theta, phi) = (u' - alpha(theta, phi)) / (1 + np.dot(boost_velocity, nhat(theta, phi)))
        # But really, we want u'(theta', phi') for given values
        #
        # Note that `space_translation` (and the spatial part of `spacetime_translation`) get reversed signs when
        # transformed into supertranslation modes, because these pieces enter the time transformation with opposite
        # sign compared to the time translation, as can be seen by looking at the retarded time: `t-r`.

        original_kwargs = kwargs.copy()

        # Build the supertranslation and spacetime_translation arrays
        supertranslation = np.zeros((4,), dtype=complex)  # For now; may be resized below
        ell_max_supertranslation = 1  # For now; may be increased below
        if 'supertranslation' in kwargs:
            supertranslation = np.array(kwargs.pop('supertranslation'), dtype=complex)
            if supertranslation.dtype != 'complex' and supertranslation.size > 0:
                # I don't actually think this can ever happen...
                raise TypeError("\nInput argument `supertranslation` should be a complex array with size>0.\n"
                                "Got a {0} array of shape {1}.".format(supertranslation.dtype,
                                                                       supertranslation.shape))
            # Make sure the array has size at least 4, by padding with zeros
            if supertranslation.size <= 4:
                supertranslation = np.lib.pad(supertranslation, (0, 4-supertranslation.size),
                                              'constant', constant_values=(0.0,))
            # Check that the shape is a possible array of scalar modes with complete (ell,m) data
            ell_max_supertranslation = int(np.sqrt(len(supertranslation))) - 1
            if (ell_max_supertranslation + 1)**2 != len(supertranslation):
                raise ValueError('\nInput supertranslation parameter must contain modes from ell=0 up to some ell_max, '
                                 'including\nall relevant m modes in standard order (see `spherical_functions` '
                                 'documentation for details).\nThus, it must be an array with length given by a '
                                 'perfect square; its length is {0}'.format(len(supertranslation)))
            # Check that the resulting supertranslation will be real
            for ell in range(ell_max_supertranslation+1):
                for m in range(ell+1):
                    i_pos = sf.LM_index(ell, m, 0)
                    i_neg = sf.LM_index(ell, -m, 0)
                    a = supertranslation[i_pos]
                    b = supertranslation[i_neg]
                    if abs(a - (-1.)**m * b.conjugate()) > 3e-16 + 1e-15 * abs(b):
                        raise ValueError("\nsupertranslation[{0}]={1}  # (ell,m)=({2},{3})\n".format(i_pos, a, ell, m)
                                         + "supertranslation[{0}]={1}  # (ell,m)=({2},{3})\n".format(i_neg, b, ell, -m)
                                         + "Will result in an imaginary supertranslation.")
        spacetime_translation = np.zeros((4,), dtype=float)
        spacetime_translation[0] = sf.constant_from_ell_0_mode(supertranslation[0]).real
        spacetime_translation[1:4] = -sf.vector_from_ell_1_modes(supertranslation[1:4]).real
        if 'spacetime_translation' in kwargs:
            st_trans = np.array(kwargs.pop('spacetime_translation'), dtype=float)
            if st_trans.shape != (4,) or st_trans.dtype != 'float':
                raise TypeError("\nInput argument `spacetime_translation` should be a float array of shape (4,).\n"
                                "Got a {0} array of shape {1}.".format(st_trans.dtype, st_trans.shape))
            spacetime_translation = st_trans[:]
            supertranslation[0] = sf.constant_as_ell_0_mode(spacetime_translation[0])
            supertranslation[1:4] = sf.vector_as_ell_1_modes(-spacetime_translation[1:4])
        if 'space_translation' in kwargs:
            s_trans = np.array(kwargs.pop('space_translation'), dtype=float)
            if s_trans.shape != (3,) or s_trans.dtype != 'float':
                raise TypeError("\nInput argument `space_translation` should be an array of floats of shape (3,).\n"
                                "Got a {0} array of shape {1}.".format(s_trans.dtype, s_trans.shape))
            spacetime_translation[1:4] = s_trans[:]
            supertranslation[1:4] = sf.vector_as_ell_1_modes(-spacetime_translation[1:4])
        if 'time_translation' in kwargs:
            t_trans = kwargs.pop('time_translation')
            if not isinstance(t_trans, float):
                raise TypeError("\nInput argument `time_translation` should be a single float.\n"
                                "Got {0}.".format(t_trans))
            spacetime_translation[0] = t_trans
            supertranslation[0] = sf.constant_as_ell_0_mode(spacetime_translation[0])

        # Decide on the number of points to use in each direction.  A nontrivial supertranslation will introduce
        # power in higher modes, so for best accuracy, we need to account for that.  But we'll make it a firm
        # requirement to have enough points to capture the original waveform, at least
        ell_max = w_modes.ell_max + ell_max_supertranslation
        n_theta = kwargs.pop('n_theta', 2*ell_max+1)
        n_phi = kwargs.pop('n_phi', 2*ell_max+1)
        if n_theta < 2*ell_max+1 and abs(supertranslation[1:]).max() > 0.0:
            warning = ("n_theta={0} is small; because of the supertranslation, ".format(n_theta)
                       + "it will lose accuracy for anything less than 2*ell+1={1}".format(ell_max))
            warnings.warn(warning)
        if n_theta < 2*w_modes.ell_max+1:
            raise ValueError('n_theta={0} is too small; '.format(n_theta)
                             + 'must be at least 2*ell+1={1}'.format(2*w_modes.ell_max+1))
        if n_phi < 2*ell_max+1 and abs(supertranslation[1:]).max() > 0.0:
            warning = ("n_phi={0} is small; because of the supertranslation, ".format(n_phi)
                       + "it will lose accuracy for anything less than 2*ell+1={1}".format(ell_max))
            warnings.warn(warning)
        if n_phi < 2*w_modes.ell_max+1:
            raise ValueError('n_phi={0} is too small; '.format(n_phi)
                             + 'must be at least 2*ell+1={1}'.format(2*w_modes.ell_max+1))

        # Get the rotor for the frame rotation
        frame_rotation = np.quaternion(*np.array(kwargs.pop('frame_rotation', [1, 0, 0, 0]), dtype=float))
        if frame_rotation.abs() < 3e-16:
            raise ValueError('frame_rotation={0} should be a unit quaternion'.format(frame_rotation))
        frame_rotation = frame_rotation.normalized()

        # Get the boost velocity vector
        boost_velocity = np.array(kwargs.pop('boost_velocity', [0.0]*3), dtype=float)
        beta = np.linalg.norm(boost_velocity)
        if boost_velocity.shape != (3,) or beta >= 1.0:
            raise ValueError('Input boost_velocity=`{0}` should be a 3-vector with '
                             'magnitude strictly less than 1.0.'.format(boost_velocity))
        gamma = 1 / math.sqrt(1 - beta**2)
        varphi = math.atanh(beta)

        if kwargs:
            import pprint
            warnings.warn("\nUnused kwargs passed to this function:\n{0}".format(pprint.pformat(kwargs, width=1)))

        # These are the angles in the transformed system at which we need to know the function values
        thetaprm_j_phiprm_k = np.array([[[thetaprm_j, phiprm_k]
                                         for phiprm_k in np.linspace(0.0, 2*np.pi, num=n_phi, endpoint=False)]
                                        for thetaprm_j in np.linspace(0.0, np.pi, num=n_theta, endpoint=True)])

        # Construct the function that modifies our rotor grid to account for the boost
        if beta > 3e-14:  # Tolerance for beta; any smaller and numerical errors will have greater effect
            vhat = boost_velocity / beta

            def Bprm_j_k(thetaprm, phiprm):
                """Construct rotor taking r' to r

                I derived this result in a different way, but I've also found it described in Penrose-Rindler Vol. 1,
                around Eq. (1.3.5).  Note, however, that their discussion is for the past celestial sphere,
                so there's a sign difference.

                """
                # Note: It doesn't matter which we use -- r' or r; all we need is the direction of the bivector
                # spanned by v and r', which is the same as the direction of the bivector spanned by v and r,
                # since either will be normalized, and one cross product is zero iff the other is zero.
                rprm = np.array([math.cos(phiprm)*math.sin(thetaprm),
                                 math.sin(phiprm)*math.sin(thetaprm),
                                 math.cos(thetaprm)])
                Thetaprm = math.acos(np.dot(vhat, rprm))
                Theta = 2 * math.atan(math.exp(-varphi) * math.tan(Thetaprm/2.0))
                rprm_cross_vhat = np.quaternion(0.0, *np.cross(rprm, vhat))
                if rprm_cross_vhat.abs() > 1e-200:
                    return (rprm_cross_vhat.normalized() * (Thetaprm - Theta) / 2).exp()
                else:
                    return quaternion.one
        else:
            def Bprm_j_k(thetaprm, phiprm):
                return quaternion.one

        # Set up rotors that we can use to evaluate the SWSHs in the original frame
        R_j_k = np.empty(thetaprm_j_phiprm_k.shape[:2], dtype=np.quaternion)
        for j in range(thetaprm_j_phiprm_k.shape[0]):
            for k in range(thetaprm_j_phiprm_k.shape[1]):
                thetaprm_j, phiprm_k = thetaprm_j_phiprm_k[j, k]
                R_j_k[j, k] = (Bprm_j_k(thetaprm_j, phiprm_k)
                               * frame_rotation
                               * quaternion.from_spherical_coords(thetaprm_j, phiprm_k))

        # TODO: Incorporate the w_modes.frame information into rotors, which will require time dependence throughout
        # It would be best to leave the waveform in its frame.  But we'll have to apply the frame_rotation to the BMS
        # elements, which will be a little tricky.  Still probably not as tricky as applying to the waveform...

        # We need values of (1) waveform, (2) conformal factor, and (3) supertranslation, at each point of the
        # transformed grid, at each instant of time.
        SWSH_j_k = sf.SWSH_grid(R_j_k, w_modes.spin_weight, ell_max)
        SH_j_k = sf.SWSH_grid(R_j_k, 0, ell_max_supertranslation)  # standard (spin-zero) spherical harmonics
        r_j_k = np.array([(R*quaternion.z*R.inverse()).vec for R in R_j_k.flat]).T
        kconformal_j_k = 1. / (gamma*(1-np.dot(boost_velocity, r_j_k).reshape(R_j_k.shape)))
        alphasupertranslation_j_k = np.tensordot(supertranslation, SH_j_k, axes=([0], [2])).real
        fprm_i_j_k = np.tensordot(
            w_modes.data, SWSH_j_k[:, :, sf.LM_index(w_modes.ell_min, -w_modes.ell_min, 0)
                                         :sf.LM_index(w_modes.ell_max, w_modes.ell_max, 0)+1],
            axes=([1], [2]))
        if w_modes.dataType == h:
            # Note that SWSH_j_k will use s=-2 in this case, so it can be used in the tensordot correctly
            supertranslation_deriv = sf.ethbar_GHP(sf.ethbar_GHP(supertranslation, 0, 0), -1, 0)
            supertranslation_deriv_values = np.tensordot(
                supertranslation_deriv,
                SWSH_j_k[:, :, :sf.LM_index(ell_max_supertranslation, ell_max_supertranslation, 0)+1],
                axes=([0], [2]))
            fprm_i_j_k -= supertranslation_deriv_values[np.newaxis, :, :]
        elif w_modes.dataType == sigma:
            # Note that SWSH_j_k will use s=+2 in this case, so it can be used in the tensordot correctly
            supertranslation_deriv = sf.eth_GHP(sf.eth_GHP(supertranslation, 0, 0), 1, 0)
            supertranslation_deriv_values = np.tensordot(
                supertranslation_deriv,
                SWSH_j_k[:, :, :sf.LM_index(ell_max_supertranslation, ell_max_supertranslation, 0)+1],
                axes=([0], [2]))
            fprm_i_j_k -= supertranslation_deriv_values[np.newaxis, :, :]
        elif w_modes.dataType in [psi0, psi1, psi2, psi3]:
            warning = ("\nTechnically, waveforms of dataType `{0}` ".format(w_modes.data_type_string)
                       + "do not transform among themselves;\n there is mixing from psi_n, for each n greater than "
                       + "this waveform's.\nProceeding on the assumption other contributions are small.  However,\n"
                       + "note that it is possible to construct a `psin` data type containing all necessary modes.")
            warnings.warn(warning)

        fprm_i_j_k *= (gamma**w_modes.gamma_weight
                       * kconformal_j_k**w_modes.conformal_weight)[np.newaxis, :, :]

        # Determine the new time slices.  The set u' is chosen so that on each slice of constant u'_i, the average value
        # of u is precisely u_i.  But then, we have to narrow that set down, so that every physical point on all the
        # u'_i' slices correspond to data in the range of input data.
        uprm_i = (1/gamma) * (w_modes.t - spacetime_translation[0])
        uprm_min = (kconformal_j_k * (w_modes.t[0] - alphasupertranslation_j_k)).max()
        uprm_max = (kconformal_j_k * (w_modes.t[-1] - alphasupertranslation_j_k)).min()
        uprm_iprm = uprm_i[(uprm_i >= uprm_min) & (uprm_i <= uprm_max)]

        # Interpolate along each grid line to the new time in that direction.  Note that if there are additional
        # dimensions in the waveform data, InterpolatedUnivariateSpline will not be able to handle them automatically,
        # so we have to loop over them explicitly; an Ellipsis can't handle them.  Also, we are doing all time steps in
        # one go, for each j,k,... value, which means that we can overwrite the original data
        final_dim = int(np.prod(fprm_i_j_k.shape[3:]))
        fprm_i_j_k = fprm_i_j_k.reshape(fprm_i_j_k.shape[:3] + (final_dim,))
        for j in range(n_theta):
            for k in range(n_phi):
                uprm_i_j_k = kconformal_j_k[j, k] * (w_modes.t - alphasupertranslation_j_k[j, k])
                for final_indices in range(final_dim):
                    re_fprm_iprm_j_k = interpolate.InterpolatedUnivariateSpline(uprm_i_j_k,
                        fprm_i_j_k[:, j, k, final_indices].real)
                    im_fprm_iprm_j_k = interpolate.InterpolatedUnivariateSpline(uprm_i_j_k,
                        fprm_i_j_k[:, j, k, final_indices].imag)
                    fprm_i_j_k[:len(uprm_iprm), j, k, final_indices] = (
                        re_fprm_iprm_j_k(uprm_iprm) + 1j * im_fprm_iprm_j_k(uprm_iprm))

        # Delete the extra rows from fprm_i_j_k, corresponding to values of u' outside of [u'min, u'max]
        fprm_iprm_j_k = np.delete(fprm_i_j_k, np.s_[len(uprm_iprm):], 0)

        # Reshape, to have correct final dimensions
        fprm_iprm_j_k = fprm_iprm_j_k.reshape((fprm_iprm_j_k.shape[0], n_theta*n_phi)+w_modes.data.shape[2:])

        # Encapsulate into a new grid waveform
        g = cls(t=uprm_iprm, data=fprm_iprm_j_k, history=w_modes.history,
                n_theta=n_theta, n_phi=n_phi,
                frameType=w_modes.frameType, dataType=w_modes.dataType,
                r_is_scaled_out=w_modes.r_is_scaled_out, m_is_scaled_out=w_modes.m_is_scaled_out,
                constructor_statement="{0}.from_modes({1}, **{2})".format(cls.__name__, w_modes, original_kwargs))
        return g