Example #1
0
def test_modes_conjugate():
    tolerance = 1e-15
    np.random.seed(1234)
    for inplace in [False, True]:
        for s in range(-2, 2 + 1):
            ell_min = abs(s)
            ell_max = 8
            a = np.random.rand(3, 7,
                               sf.LM_total_size(ell_min, ell_max) *
                               2).view(complex)
            m = sf.Modes(a, spin_weight=s, ell_min=ell_min, ell_max=ell_max)
            g = m.grid()
            s = m.s
            ell_min = m.ell_min
            ell_max = m.ell_max
            shape = m.shape
            mbar = m.conjugate(inplace)
            gbar = mbar.grid()
            assert s == -mbar.s
            assert ell_min == mbar.ell_min
            assert ell_max == mbar.ell_max
            assert shape == mbar.shape
            assert np.allclose(g,
                               np.conjugate(gbar),
                               rtol=tolerance,
                               atol=tolerance)
Example #2
0
 def __new__(cls, input_array, *args, **kwargs):
     if len(args) > 2:
         raise ValueError("Only one positional argument may be passed")
     if len(args) == 1:
         kwargs["time"] = args[0]
     metadata = copy.copy(getattr(input_array, "_metadata", {}))
     metadata.update(**kwargs)
     input_array = np.asanyarray(input_array).view(complex)
     time = metadata.get("time", None)
     if time is None:
         raise ValueError("Time data must be specified as part of input array or as constructor parameter")
     time = np.asarray(time).view(float)
     if time.ndim != 1:
         raise ValueError(f"Input time array must have exactly 1 dimension; it has {time.ndim}.")
     if input_array.ndim == 0:
         input_array = input_array[np.newaxis, np.newaxis]
     elif input_array.ndim == 1:
         input_array = input_array[np.newaxis, :]
     elif input_array.shape[-2] != time.shape[0] and input_array.shape[-2] != 1:
         raise ValueError(
             f"Second-to-last axis of input array must have size 1 or same size as time array.\n            Their shapes are {input_array.shape} and {time.shape}, respectively."
         )
     obj = spherical_functions.Modes(input_array, **kwargs).view(cls)
     obj._metadata["time"] = time
     return obj
Example #3
0
def test_modes_squared_angular_momenta():
    tolerance = 1e-13
    np.random.seed(1234)
    L2 = sf.Modes.Lsquared
    Lz = sf.Modes.Lz
    Lp = sf.Modes.Lplus
    Lm = sf.Modes.Lminus
    R2 = sf.Modes.Rsquared
    Rz = sf.Modes.Rz
    Rp = sf.Modes.Rplus
    Rm = sf.Modes.Rminus
    for s in range(-2, 2 + 1):
        ell_min = abs(s)
        ell_max = 8
        a = np.random.rand(3, 7,
                           sf.LM_total_size(ell_min, ell_max) *
                           2).view(complex)
        m = sf.Modes(a, spin_weight=s, ell_min=ell_min, ell_max=ell_max)

        # Test L^2 = 0.5(L+L- + L-L+) + LzLz
        m1 = L2(m)
        m2 = 0.5 * (Lp(Lm(m)) + Lm(Lp(m))) + Lz(Lz(m))
        assert np.allclose(m1, m2, rtol=tolerance, atol=tolerance)

        # Test R^2 = 0.5(R+R- + R-R+) + RzRz
        m1 = R2(m)
        m2 = 0.5 * (Rp(Rm(m)) + Rm(Rp(m))) + Rz(Rz(m))
        assert np.allclose(m1, m2, rtol=tolerance, atol=tolerance)

        # Test L^2 = R^2
        m1 = L2(m)
        m2 = R2(m)
        assert np.allclose(m1, m2, rtol=tolerance, atol=tolerance)
Example #4
0
def test_modes_real():
    tolerance = 1e-14
    np.random.seed(1234)
    for inplace in [False, True]:
        s = 0
        ell_min = abs(s)
        ell_max = 8
        a = np.random.rand(3, 7,
                           sf.LM_total_size(ell_min, ell_max) *
                           2).view(complex)
        # Test success with spin_weight==0
        m = sf.Modes(a, spin_weight=s, ell_min=ell_min, ell_max=ell_max)
        g = m.grid()
        s = m.s
        ell_min = m.ell_min
        ell_max = m.ell_max
        shape = m.shape
        mreal = m._real_func(inplace)
        greal = mreal.grid()
        assert s == mreal.s
        assert ell_min == mreal.ell_min
        assert ell_max == mreal.ell_max
        assert shape == mreal.shape
        assert np.allclose(greal,
                           np.real(greal) + 0.0j,
                           rtol=tolerance,
                           atol=tolerance)
        assert np.allclose(np.real(g),
                           np.real(greal),
                           rtol=tolerance,
                           atol=tolerance)
        assert np.allclose(np.zeros_like(g, dtype=float),
                           np.imag(greal),
                           rtol=tolerance,
                           atol=tolerance)
        # Test failure with s!=0
        for s in [-3, -2, -1, 1, 2, 3]:
            m = sf.Modes(a, spin_weight=s, ell_min=ell_min, ell_max=ell_max)
            with pytest.raises(ValueError):
                mreal = m._real_func(inplace)
Example #5
0
def test_modes_imag():
    tolerance = 1e-14
    np.random.seed(1234)
    for inplace in [False, True]:
        s = 0
        ell_min = abs(s)
        ell_max = 8
        a = np.random.rand(3, 7,
                           sf.LM_total_size(ell_min, ell_max) *
                           2).view(complex)
        # Test success with spin_weight==0
        m = sf.Modes(a, spin_weight=s, ell_min=ell_min, ell_max=ell_max)
        g = m.grid()
        s = m.s
        ell_min = m.ell_min
        ell_max = m.ell_max
        shape = m.shape
        mimag = m._imag_func(inplace)
        gimag = mimag.grid()
        assert s == mimag.s
        assert ell_min == mimag.ell_min
        assert ell_max == mimag.ell_max
        assert shape == mimag.shape
        assert np.allclose(gimag,
                           np.real(gimag),
                           rtol=tolerance,
                           atol=tolerance)  # gimag is purely real
        assert np.allclose(np.array(np.imag(g.ndarray), dtype=complex),
                           gimag.ndarray,
                           rtol=tolerance,
                           atol=tolerance)  # imag(g) == gimag
        assert np.allclose(np.imag(gimag.ndarray),
                           np.zeros_like(g.ndarray, dtype=float),
                           rtol=tolerance,
                           atol=tolerance)  # imag(gimag) == 0
        # Test failure with s!=0
        for s in [-3, -2, -1, 1, 2, 3]:
            m = sf.Modes(a, spin_weight=s, ell_min=ell_min, ell_max=ell_max)
            with pytest.raises(ValueError):
                mimag = m._imag_func(inplace)
Example #6
0
def test_modes_ufuncs():
    for s1 in range(-2, 2 + 1):
        ell_min1 = abs(s1)
        ell_max1 = 8
        a1 = np.random.rand(11,
                            sf.LM_total_size(ell_min1, ell_max1) *
                            2).view(complex)
        m1 = sf.Modes(a1, spin_weight=s1, ell_min=ell_min1, ell_max=ell_max1)
        positivem1 = +m1
        assert np.array_equal(m1.view(np.ndarray), positivem1.view(np.ndarray))
        negativem1 = -m1
        assert np.array_equal(-(m1.view(np.ndarray)),
                              negativem1.view(np.ndarray))
Example #7
0
def test_modes_norm():
    tolerance = 1e-15
    np.random.seed(1234)
    for s in range(-2, 2 + 1):
        ell_min = abs(s)
        ell_max = 8
        a = np.random.rand(3, 7,
                           sf.LM_total_size(ell_min, ell_max) *
                           2).view(complex)
        m = sf.Modes(a, spin_weight=s, ell_min=ell_min, ell_max=ell_max)
        mmbar = m.multiply(m.conjugate())
        norm = np.sqrt(2 * math.sqrt(np.pi) *
                       mmbar[..., 0].view(np.ndarray).real)
        assert np.allclose(norm, m.norm(), rtol=tolerance, atol=tolerance)
Example #8
0
def test_modes_copying_and_pickling(copier):
    for s in range(-2, 2 + 1):
        ell_min = abs(s)
        ell_max = 8
        a = np.random.rand(3, 7,
                           sf.LM_total_size(ell_min, ell_max) *
                           2).view(complex)
        m = sf.Modes(a, spin_weight=s, ell_min=ell_min, ell_max=ell_max)
        c = copier(m)
        assert m is not c
        assert np.array_equal(c, m)
        assert isinstance(c, type(m))
        assert c.s == m.s
        assert c.ell_min == m.ell_min
        assert c.ell_max == m.ell_max
Example #9
0
def conformal_factors(boost_velocity, distorted_grid_rotors):
    """Compute various combinations of the conformal factor

    This is primarily a utility function for use in the `transform` function, pulled out so that it
    can be tested separately.

    Parameters
    ==========
    boost_velocity: array of 3 floats
        Three-velocity of the new frame relative to the old frame
    distorted_grid_rotors: 2-d array of quaternions
        Unit quaternions giving the rotation of the (x, y, z) basis onto the basis vectors with
        respect to which the output spin-weighted fields are evaluated

    Returns
    =======
    k: spherical_functions.Grid
    ðk_over_k: spherical_functions.Grid
    one_over_k: spherical_functions.Grid
    one_over_k_cubed: spherical_functions.Grid
        These all have the same shape as `distorted_grid_rotors` except for an additional dimension
        of size 1 at the beginning, so that they can broadcast against the time dimension.

    """
    from quaternion import rotate_vectors

    β = np.linalg.norm(boost_velocity)
    γ = 1 / math.sqrt(1 - β**2)

    # Note that ðk / k = ð(v·r) / (1 - v·r), but evaluating ð(v·r) is slightly delicate.  As modes
    # in the undistorted frame, we have ð(v·r) ~ (v·r), but the right hand side is now an s=1 field,
    # so it has to be evaluated as such.
    v_dot_r = sf.Grid(np.dot(
        rotate_vectors(distorted_grid_rotors, quaternion.z.vec),
        boost_velocity),
                      spin_weight=0)[np.newaxis, :, :]
    ðv_dot_r = sf.Grid(
        sf.Modes(np.insert(sf.vector_as_ell_1_modes(boost_velocity), 0, 0.0),
                 spin_weight=1).evaluate(distorted_grid_rotors),
        spin_weight=1,
    )[np.newaxis, :, :]
    one_over_k = γ * (1 - v_dot_r)
    k = 1.0 / one_over_k
    ðk_over_k = ðv_dot_r / (1 - v_dot_r)
    one_over_k_cubed = one_over_k**3
    return k, ðk_over_k, one_over_k, one_over_k_cubed
Example #10
0
def test_modes_grid():
    for s in range(-2, 2 + 1):
        ell_min = abs(s)
        ell_max = 8
        a = np.random.rand(3, 7,
                           sf.LM_total_size(ell_min, ell_max) *
                           2).view(complex)
        m = sf.Modes(a, spin_weight=s, ell_min=ell_min, ell_max=ell_max)
        n = 2 * ell_max + 1
        for n_theta, n_phi in [[None, None], [n, None], [None, n], [n, n],
                               [n + 1, n], [n, n + 1], [n + 1, n + 1]]:
            g = m.grid(n_theta=n_theta, n_phi=n_phi)
            assert g.dtype == np.complex
            assert g.shape[:-2] == a.shape[:-1]
            if n_theta is None:
                n_theta = n
            if n_phi is None:
                n_phi = n
            assert g.shape[-2:] == (n_theta, n_phi)
Example #11
0
def transform(self, **kwargs):
    """Apply BMS transformation to AsymptoticBondiData object

    It is important to note that the input transformation parameters are applied in this order:

      1. (Super)Translations
      2. Rotation (about the origin)
      3. Boost (about the origin)

    All input parameters refer to the transformation required to take the input data's inertial
    frame onto the inertial frame of the output data's inertial observers.  In what follows, the
    coordinates of and functions in the input inertial frame will be unprimed, while corresponding
    values of the output inertial frame 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
    `α`, representing the supertranslation, which transforms the asymptotic time variable `u` as

        u'(u, θ, ϕ) = u(u, θ, ϕ) - α(θ, ϕ)

    A simple time translation by δt would correspond to

        α(θ, ϕ) = δt  # Independent of (θ, ϕ)

    A pure spatial translation δx would correspond to

        α(θ, ϕ) = -δx · n̂(θ, ϕ)

    where `·` is the usual dot product, and `n̂` is the unit vector in the given direction.


    Parameters
    ==========
    abd: AsymptoticBondiData
        The object storing the modes of the original data, which will be transformed in this
        function.  This is the only required argument to this function.
    time_translation: float, optional
        Defaults to zero.  Nonzero overrides corresponding components of `spacetime_translation` and
        `supertranslation` parameters.  Note that this is the actual change in the coordinate value,
        rather than the corresponding mode weight (which is what `supertranslation` represents).
    space_translation : float array of length 3, optional
        Defaults to empty (no translation).  Non-empty overrides corresponding components of
        `spacetime_translation` and `supertranslation` parameters.  Note that this is the actual
        change in the coordinate value, rather than the corresponding mode weight (which is what
        `supertranslation` represents).
    spacetime_translation : float array of length 4, optional
        Defaults to empty (no translation).  Non-empty overrides corresponding components of
        `supertranslation`.  Note that this is the actual change in the coordinate value, rather
        than the corresponding mode weight (which is what `supertranslation` represents).
    supertranslation : complex array [defaults to 0]
        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 `abd` object.  Supertranslations must be real, so
        these values should obey the condition
            α^{ℓ,m} = (-1)^m ᾱ^{ℓ,-m}
        This condition is actually imposed on the input data, so imaginary parts of α(θ, ϕ) will
        essentially be discarded.  Defaults to empty, which causes no supertranslation.  Note that
        some components may be overridden by the parameters above.
    frame_rotation : quaternion [defaults to 1]
        Transformation applied to (x,y,z) basis of the input mode's inertial frame.  For example,
        the basis z vector of the new frame may be written as
           z' = frame_rotation * z * frame_rotation.inverse()
        Defaults to 1, corresponding to the identity transformation (no rotation).
    boost_velocity : float array of length 3 [defaults to (0, 0, 0)]
        This is the three-velocity vector of the new frame relative to the input frame.  The norm of
        this vector is required to be smaller than 1.
    output_ell_max: int [defaults to abd.ell_max]
        Maximum ell value in the output data.
    working_ell_max: int [defaults to 2 * abd.ell_max]
        Maximum ell value to use during the intermediate calculations.  Rotations and time
        translations do not require this to be any larger than abd.ell_max, but other
        transformations will require more values of ell for accurate results.  In particular, boosts
        are multiplied by time, meaning that a large boost of data with large values of time will
        lead to very large power in higher modes.  Similarly, large (super)translations will couple
        power through a lot of modes.  To avoid aliasing, this value should be large, to accomodate
        power in higher modes.

    Returns
    -------
    abdprime: AsymptoticBondiData
        Object representing the transformed data.

    """
    from quaternion import rotate_vectors
    from scipy.interpolate import CubicSpline

    # Parse the input arguments, and define the basic parameters for this function
    (
        frame_rotation,
        boost_velocity,
        supertranslation,
        working_ell_max,
        output_ell_max,
    ) = _process_transformation_kwargs(self.ell_max, **kwargs)
    n_theta = 2 * working_ell_max + 1
    n_phi = n_theta
    β = np.linalg.norm(boost_velocity)
    γ = 1 / math.sqrt(1 - β**2)

    # Make this into a Modes object, so it can keep track of its spin weight, etc., through the
    # various operations needed below.
    supertranslation = sf.Modes(supertranslation, spin_weight=0).real

    # This is a 2-d array of unit quaternions, which are what the spin-weighted functions should be
    # evaluated on (even for spin 0 functions, for simplicity).  That will be equivalent to
    # evaluating the spin-weighted functions with respect to the transformed grid -- although on the
    # original time slices.
    distorted_grid_rotors = boosted_grid(frame_rotation, boost_velocity,
                                         n_theta, n_phi)

    # Compute u, α, ðα, ððα, k, ðk/k, 1/k, and 1/k³ on the distorted grid, including new axes to
    # enable broadcasting with time-dependent functions.  Note that the first axis should represent
    # variation in u, the second axis variation in θ', and the third axis variation in ϕ'.
    u = self.u
    α = sf.Grid(supertranslation.evaluate(distorted_grid_rotors),
                spin_weight=0).real[np.newaxis, :, :]
    # The factors of 1/sqrt(2) and 1/2 come from using the GHP eth instead of the NP eth.
    ðα = sf.Grid(supertranslation.eth.evaluate(distorted_grid_rotors) /
                 np.sqrt(2),
                 spin_weight=α.s + 1)[np.newaxis, :, :]
    ððα = sf.Grid(0.5 *
                  supertranslation.eth.eth.evaluate(distorted_grid_rotors),
                  spin_weight=α.s + 2)[np.newaxis, :, :]
    k, ðk_over_k, one_over_k, one_over_k_cubed = conformal_factors(
        boost_velocity, distorted_grid_rotors)

    # ðu'(u, θ', ϕ') exp(iλ) / k(θ', ϕ')
    ðuprime_over_k = ðk_over_k * (u - α) - ðα

    # ψ0(u, θ', ϕ') exp(2iλ)
    ψ0 = sf.Grid(self.psi0.evaluate(distorted_grid_rotors), spin_weight=2)
    # ψ1(u, θ', ϕ') exp(iλ)
    ψ1 = sf.Grid(self.psi1.evaluate(distorted_grid_rotors), spin_weight=1)
    # ψ2(u, θ', ϕ')
    ψ2 = sf.Grid(self.psi2.evaluate(distorted_grid_rotors), spin_weight=0)
    # ψ3(u, θ', ϕ') exp(-1iλ)
    ψ3 = sf.Grid(self.psi3.evaluate(distorted_grid_rotors), spin_weight=-1)
    # ψ4(u, θ', ϕ') exp(-2iλ)
    ψ4 = sf.Grid(self.psi4.evaluate(distorted_grid_rotors), spin_weight=-2)
    # σ(u, θ', ϕ') exp(2iλ)
    σ = sf.Grid(self.sigma.evaluate(distorted_grid_rotors), spin_weight=2)

    ### The following calculations are done using in-place Horner form.  I suspect this will be the
    ### most efficient form of this calculation, within reason.  Note that the factors of exp(isλ)
    ### were computed automatically by evaluating in terms of quaternions.
    #
    fprime_of_timenaught_directionprime = np.empty(
        (6, self.n_times, n_theta, n_phi), dtype=complex)
    # ψ0'(u, θ', ϕ')
    fprime_temp = ψ4.copy()
    fprime_temp *= ðuprime_over_k
    fprime_temp += -4 * ψ3
    fprime_temp *= ðuprime_over_k
    fprime_temp += 6 * ψ2
    fprime_temp *= ðuprime_over_k
    fprime_temp += -4 * ψ1
    fprime_temp *= ðuprime_over_k
    fprime_temp += ψ0
    fprime_temp *= one_over_k_cubed
    fprime_of_timenaught_directionprime[0] = fprime_temp
    # ψ1'(u, θ', ϕ')
    fprime_temp = -ψ4
    fprime_temp *= ðuprime_over_k
    fprime_temp += 3 * ψ3
    fprime_temp *= ðuprime_over_k
    fprime_temp += -3 * ψ2
    fprime_temp *= ðuprime_over_k
    fprime_temp += ψ1
    fprime_temp *= one_over_k_cubed
    fprime_of_timenaught_directionprime[1] = fprime_temp
    # ψ2'(u, θ', ϕ')
    fprime_temp = ψ4.copy()
    fprime_temp *= ðuprime_over_k
    fprime_temp += -2 * ψ3
    fprime_temp *= ðuprime_over_k
    fprime_temp += ψ2
    fprime_temp *= one_over_k_cubed
    fprime_of_timenaught_directionprime[2] = fprime_temp
    # ψ3'(u, θ', ϕ')
    fprime_temp = -ψ4
    fprime_temp *= ðuprime_over_k
    fprime_temp += ψ3
    fprime_temp *= one_over_k_cubed
    fprime_of_timenaught_directionprime[3] = fprime_temp
    # ψ4'(u, θ', ϕ')
    fprime_temp = ψ4.copy()
    fprime_temp *= one_over_k_cubed
    fprime_of_timenaught_directionprime[4] = fprime_temp
    # σ'(u, θ', ϕ')
    fprime_temp = σ.copy()
    fprime_temp -= ððα
    fprime_temp *= one_over_k
    fprime_of_timenaught_directionprime[5] = fprime_temp

    # Determine the new time slices.  The set timeprime is chosen so that on each slice of constant
    # u'_i, the average value of u=(u'/k)+α is precisely <u>=u'γ+<α>=u_i.  But then, we have to
    # narrow that set down, so that every grid point on all the u'_i' slices correspond to data in
    # the range of input data.
    timeprime = (u - sf.constant_from_ell_0_mode(supertranslation[0]).real) / γ
    timeprime_of_initialtime_directionprime = k * (u[0] - α)
    timeprime_of_finaltime_directionprime = k * (u[-1] - α)
    earliest_complete_timeprime = np.max(
        timeprime_of_initialtime_directionprime.view(np.ndarray))
    latest_complete_timeprime = np.min(
        timeprime_of_finaltime_directionprime.view(np.ndarray))
    timeprime = timeprime[(timeprime >= earliest_complete_timeprime)
                          & (timeprime <= latest_complete_timeprime)]

    # This will store the values of f'(u', θ', ϕ') for the various functions `f`
    fprime_of_timeprime_directionprime = np.zeros(
        (6, timeprime.size, n_theta, n_phi), dtype=complex)

    # Interpolate the various transformed function values on the transformed grid from the original
    # time coordinate to the new set of time coordinates, independently for each direction.
    for i in range(n_theta):
        for j in range(n_phi):
            k_i_j = k[0, i, j]
            α_i_j = α[0, i, j]
            # u'(u, θ', ϕ')
            timeprime_of_timenaught_directionprime_i_j = k_i_j * (u - α_i_j)
            # f'(u', θ', ϕ')
            fprime_of_timeprime_directionprime[:, :, i, j] = CubicSpline(
                timeprime_of_timenaught_directionprime_i_j,
                fprime_of_timenaught_directionprime[:, :, i, j],
                axis=1)(timeprime)

    # Finally, transform back from the distorted grid to the SWSH mode weights as measured in that
    # grid.  I'll abuse notation slightly here by indicating those "distorted" mode weights with
    # primes, so that f'(u')_{ℓ', m'} = ∫ f'(u', θ', ϕ') sȲ_{ℓ', m'}(θ', ϕ') sin(θ') dθ' dϕ'
    abdprime = type(self)(timeprime, output_ell_max)
    # ψ0'(u')_{ℓ', m'}
    abdprime.psi0 = spinsfast.map2salm(fprime_of_timeprime_directionprime[0],
                                       2, output_ell_max)
    # ψ1'(u')_{ℓ', m'}
    abdprime.psi1 = spinsfast.map2salm(fprime_of_timeprime_directionprime[1],
                                       1, output_ell_max)
    # ψ2'(u')_{ℓ', m'}
    abdprime.psi2 = spinsfast.map2salm(fprime_of_timeprime_directionprime[2],
                                       0, output_ell_max)
    # ψ3'(u')_{ℓ', m'}
    abdprime.psi3 = spinsfast.map2salm(fprime_of_timeprime_directionprime[3],
                                       -1, output_ell_max)
    # ψ4'(u')_{ℓ', m'}
    abdprime.psi4 = spinsfast.map2salm(fprime_of_timeprime_directionprime[4],
                                       -2, output_ell_max)
    # σ'(u')_{ℓ', m'}
    abdprime.sigma = spinsfast.map2salm(fprime_of_timeprime_directionprime[5],
                                        2, output_ell_max)

    return abdprime
Example #12
0
def test_modes_creation():
    for s in range(-2, 2 + 1):
        ell_min = abs(s)
        ell_max = 8

        # Test successful creation with real data of the right shape
        a = np.random.rand(3, 7, sf.LM_total_size(ell_min, ell_max) * 2)
        m = sf.Modes(a, spin_weight=s, ell_min=ell_min, ell_max=ell_max)
        assert m.s == s
        assert m.ell_min == 0  # NOTE: This is hard coded!!!
        assert m.ell_max == ell_max
        assert np.array_equal(a.view(complex),
                              m[..., sf.LM_total_size(0, ell_min - 1):])
        assert np.all(m[..., :sf.LM_total_size(0, abs(s) - 1)] == 0.0)
        m = sf.Modes(a, spin_weight=s, ell_min=ell_min)  # ell_max is deduced!
        assert m.s == s
        assert m.ell_min == 0  # NOTE: This is hard coded!!!
        assert m.ell_max == ell_max
        assert np.array_equal(a.view(complex),
                              m[..., sf.LM_total_size(0, ell_min - 1):])
        assert np.all(m[..., :sf.LM_total_size(0, abs(s) - 1)] == 0.0)

        # Test successful creation with complex data of the right shape
        a = a.view(complex)
        m = sf.Modes(a, spin_weight=s, ell_min=ell_min, ell_max=ell_max)
        assert m.s == s
        assert m.ell_min == 0  # NOTE: This is hard coded!!!
        assert m.ell_max == ell_max
        assert np.array_equal(a, m[..., sf.LM_total_size(0, ell_min - 1):])
        assert np.all(m[..., :sf.LM_total_size(0, abs(s) - 1)] == 0.0)
        m = sf.Modes(a, spin_weight=s, ell_min=ell_min)  # ell_max is deduced!
        assert m.s == s
        assert m.ell_min == 0  # NOTE: This is hard coded!!!
        assert m.ell_max == ell_max
        assert np.array_equal(a, m[..., sf.LM_total_size(0, ell_min - 1):])
        assert np.all(m[..., :sf.LM_total_size(0, abs(s) - 1)] == 0.0)

        # Test failed creation with complex data of inconsistent shape
        if ell_min != 0:
            with pytest.raises(ValueError):
                m = sf.Modes(a, spin_weight=s)
        with pytest.raises(ValueError):
            m = sf.Modes(a,
                         spin_weight=s,
                         ell_min=ell_min - 1,
                         ell_max=ell_max)
        with pytest.raises(ValueError):
            m = sf.Modes(a,
                         spin_weight=s,
                         ell_min=ell_min + 1,
                         ell_max=ell_max)
        with pytest.raises(ValueError):
            m = sf.Modes(a,
                         spin_weight=s,
                         ell_min=ell_min,
                         ell_max=ell_max - 1)
        with pytest.raises(ValueError):
            m = sf.Modes(a,
                         spin_weight=s,
                         ell_min=ell_min,
                         ell_max=ell_max + 1)

        # Test failed creation with complex data of impossible shape
        with pytest.raises(ValueError):
            m = sf.Modes(a[..., 1:], spin_weight=s, ell_min=ell_min)

        # Test successful creation with complex data containing extraneous data at ell<abs(s)
        a = np.random.rand(3, 7, sf.LM_total_size(0, ell_max) * 2)
        a = a.view(complex)
        m = sf.Modes(a, spin_weight=s)
        assert m.s == s
        assert m.ell_min == 0  # NOTE: This is hard coded!!!
        assert m.ell_max == ell_max
        assert np.all(m[..., :sf.LM_total_size(0, abs(s) - 1)] == 0.0)
Example #13
0
def transform_moreschi_supermomentum(supermomentum, **kwargs):
    """Apply a BMS transformation to the Moreschi supermomentum using the Moreschi formula,
    Eq. (9) of arXiv:gr-qc/0203075. NOTE: This transformation only holds for the Moreschi
    supermomentum!

    It is important to note that the input transformation parameters are applied in this order:

      1. (Super)Translations
      2. Rotation (about the origin)
      3. Boost (about the origin)

    All input parameters refer to the transformation required to take the input data's inertial
    frame onto the inertial frame of the output data's inertial observers.  In what follows, the
    coordinates of and functions in the input inertial frame will be unprimed, while corresponding
    values of the output inertial frame 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
    `α`, representing the supertranslation, which transforms the asymptotic time variable `u` as

        u'(u, θ, ϕ) = u(u, θ, ϕ) - α(θ, ϕ)

    A simple time translation by δt would correspond to

        α(θ, ϕ) = δt  # Independent of (θ, ϕ)

    A pure spatial translation δx would correspond to

        α(θ, ϕ) = -δx · n̂(θ, ϕ)

    where `·` is the usual dot product, and `n̂` is the unit vector in the given direction.


    Parameters
    ----------
    supermomentum: ModesTimeSeries
        The object storing the modes of the original data, which will be transformed in this
        function.  This is the only required argument to this function.
    time_translation: float, optional
        Defaults to zero.  Nonzero overrides corresponding components of `spacetime_translation` and
        `supertranslation` parameters.  Note that this is the actual change in the coordinate value,
        rather than the corresponding mode weight (which is what `supertranslation` represents).
    space_translation : float array of length 3, optional
        Defaults to empty (no translation).  Non-empty overrides corresponding components of
        `spacetime_translation` and `supertranslation` parameters.  Note that this is the actual
        change in the coordinate value, rather than the corresponding mode weight (which is what
        `supertranslation` represents).
    spacetime_translation : float array of length 4, optional
        Defaults to empty (no translation).  Non-empty overrides corresponding components of
        `supertranslation`.  Note that this is the actual change in the coordinate value, rather
        than the corresponding mode weight (which is what `supertranslation` represents).
    supertranslation : complex array [defaults to 0]
        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 `supermomentum`. Supertranslations must be real, so
        these values should obey the condition
            α^{ℓ,m} = (-1)^m ᾱ^{ℓ,-m}
        This condition is actually imposed on the input data, so imaginary parts of α(θ, ϕ) will
        essentially be discarded.  Defaults to empty, which causes no supertranslation.  Note that
        some components may be overridden by the parameters above.
    frame_rotation : quaternion [defaults to 1]
        Transformation applied to (x,y,z) basis of the input mode's inertial frame.  For example,
        the basis z vector of the new frame may be written as
           z' = frame_rotation * z * frame_rotation.inverse()
        Defaults to 1, corresponding to the identity transformation (no rotation).
    boost_velocity : float array of length 3 [defaults to (0, 0, 0)]
        This is the three-velocity vector of the new frame relative to the input frame.  The norm of
        this vector is required to be smaller than 1.
    output_ell_max: int [defaults to supermomentum.ell_max]
        Maximum ell value in the output data.
    working_ell_max: int [defaults to 2 * supermomentum.ell_max]
        Maximum ell value to use during the intermediate calculations.  Rotations and time
        translations do not require this to be any larger than supermomentum.ell_max, but other
        transformations will require more values of ell for accurate results.  In particular, boosts
        are multiplied by time, meaning that a large boost of data with large values of time will
        lead to very large power in higher modes.  Similarly, large (super)translations will couple
        power through a lot of modes.  To avoid aliasing, this value should be large, to accomodate
        power in higher modes.

    Returns
    -------
    ModesTimeSeries

    """
    from quaternion import rotate_vectors
    from scipy.interpolate import CubicSpline
    import spherical_functions as sf
    import spinsfast
    import math
    from .transformations import _process_transformation_kwargs, boosted_grid, conformal_factors
    from ..modes_time_series import ModesTimeSeries

    # Parse the input arguments, and define the basic parameters for this function
    frame_rotation, boost_velocity, supertranslation, working_ell_max, output_ell_max, = _process_transformation_kwargs(
        supermomentum.ell_max, **kwargs
    )
    n_theta = 2 * working_ell_max + 1
    n_phi = n_theta
    β = np.linalg.norm(boost_velocity)
    γ = 1 / math.sqrt(1 - β ** 2)

    # Make this into a Modes object, so it can keep track of its spin weight, etc., through the
    # various operations needed below.
    supertranslation = sf.Modes(supertranslation, spin_weight=0).real

    # This is a 2-d array of unit quaternions, which are what the spin-weighted functions should be
    # evaluated on (even for spin 0 functions, for simplicity).  That will be equivalent to
    # evaluating the spin-weighted functions with respect to the transformed grid -- although on the
    # original time slices.
    distorted_grid_rotors = boosted_grid(frame_rotation, boost_velocity, n_theta, n_phi)

    # Compute u, α, Δα, k, ðk/k, 1/k, and 1/k³ on the distorted grid, including new axes to
    # enable broadcasting with time-dependent functions.  Note that the first axis should represent
    # variation in u, the second axis variation in θ', and the third axis variation in ϕ'.
    u = supermomentum.u
    α = sf.Grid(supertranslation.evaluate(distorted_grid_rotors), spin_weight=0).real[np.newaxis, :, :]
    # The factor of 0.25 comes from using the GHP eth instead of the NP eth.
    Δα = sf.Grid(0.25 * supertranslation.ethbar.ethbar.eth.eth.evaluate(distorted_grid_rotors), spin_weight=α.s)[
        np.newaxis, :, :
    ]
    k, ðk_over_k, one_over_k, one_over_k_cubed = conformal_factors(boost_velocity, distorted_grid_rotors)

    # Ψ(u, θ', ϕ') exp(2iλ)
    Ψ = sf.Grid(supermomentum.evaluate(distorted_grid_rotors), spin_weight=0)

    ### The following calculations are done using in-place Horner form.  I suspect this will be the
    ### most efficient form of this calculation, within reason.  Note that the factors of exp(isλ)
    ### were computed automatically by evaluating in terms of quaternions.
    #
    # Ψ'(u, θ', ϕ') = k⁻³ (Ψ - ð²ðbar²α)
    Ψprime_of_timenaught_directionprime = Ψ.copy() - Δα
    Ψprime_of_timenaught_directionprime *= one_over_k_cubed

    # Determine the new time slices.  The set timeprime is chosen so that on each slice of constant
    # u'_i, the average value of u=(u'/k)+α is precisely <u>=u'γ+<α>=u_i.  But then, we have to
    # narrow that set down, so that every grid point on all the u'_i' slices correspond to data in
    # the range of input data.
    timeprime = (u - sf.constant_from_ell_0_mode(supertranslation[0]).real) / γ
    timeprime_of_initialtime_directionprime = k * (u[0] - α)
    timeprime_of_finaltime_directionprime = k * (u[-1] - α)
    earliest_complete_timeprime = np.max(timeprime_of_initialtime_directionprime.view(np.ndarray))
    latest_complete_timeprime = np.min(timeprime_of_finaltime_directionprime.view(np.ndarray))
    timeprime = timeprime[(timeprime >= earliest_complete_timeprime) & (timeprime <= latest_complete_timeprime)]

    # This will store the values of Ψ'(u', θ', ϕ')
    Ψprime_of_timeprime_directionprime = np.zeros((timeprime.size, n_theta, n_phi), dtype=complex)

    # Interpolate the various transformed function values on the transformed grid from the original
    # time coordinate to the new set of time coordinates, independently for each direction.
    for i in range(n_theta):
        for j in range(n_phi):
            k_i_j = k[0, i, j]
            α_i_j = α[0, i, j]
            # u'(u, θ', ϕ')
            timeprime_of_timenaught_directionprime_i_j = k_i_j * (u - α_i_j)
            # Ψ'(u', θ', ϕ')
            Ψprime_of_timeprime_directionprime[:, i, j] = CubicSpline(
                timeprime_of_timenaught_directionprime_i_j, Ψprime_of_timenaught_directionprime[:, i, j], axis=0
            )(timeprime)

    # Finally, transform back from the distorted grid to the SWSH mode weights as measured in that
    # grid.  I'll abuse notation slightly here by indicating those "distorted" mode weights with
    # primes, so that Ψ'(u')_{ℓ', m'} = ∫ Ψ'(u', θ', ϕ') sȲ_{ℓ', m'}(θ', ϕ') sin(θ') dθ' dϕ'
    supermomentum_prime = spinsfast.map2salm(Ψprime_of_timeprime_directionprime, 0, output_ell_max)
    supermomentum_prime = ModesTimeSeries(
        sf.SWSH_modes.Modes(
            supermomentum_prime, spin_weight=0, ell_min=0, ell_max=output_ell_max, multiplication_truncator=max
        ),
        time=timeprime,
    )

    return supermomentum_prime
Example #14
0
def test_modes_subtraction():
    tolerance = 1e-14
    np.random.seed(1234)
    for s1 in range(-2, 2 + 1):
        ell_min1 = abs(s1)
        ell_max1 = 8
        a1 = np.random.rand(3, 7, sf.LM_total_size(ell_min1, ell_max1) * 2)
        a2 = np.random.rand(*a1.shape)
        a1 = a1.view(complex)
        a2 = a2.view(complex)
        m1 = sf.Modes(a1, spin_weight=s1, ell_min=ell_min1, ell_max=ell_max1)
        m2 = sf.Modes(a2, spin_weight=s1, ell_min=ell_min1, ell_max=ell_max1)
        m1m2 = m1 - m2
        assert m1m2.s == s1
        assert m1m2.ell_max == m1.ell_max
        assert np.array_equal(m1m2, m1.subtract(m2))
        assert np.array_equal(m1m2, m1.view(np.ndarray) - m2.view(np.ndarray))
        for s2 in range(-s1, s1 + 1):
            ell_min2 = ell_min1 + 1
            ell_max2 = ell_max1 - 1
            a2 = np.random.rand(3, 7,
                                sf.LM_total_size(ell_min2, ell_max2) *
                                2).view(complex)
            m2 = sf.Modes(a2,
                          spin_weight=s2,
                          ell_min=ell_min2,
                          ell_max=ell_max2)
            if s1 != s2:
                # Don't allow addition of non-zero data
                with pytest.raises(ValueError):
                    m1m2 = m1.subtract(m2)
                # Do allow subtraction with various forms of 0, for convenience
                for m3 in [
                        m1.subtract(0),
                        m1.subtract(np.zeros(1)),
                        m1.subtract(np.zeros((1, ))),
                        m1.subtract(np.zeros((7, ))),
                        m1.subtract(np.zeros((3, 7))),
                        m1 - 0,
                        m1 - np.zeros(1),
                        m1 - np.zeros((1, )),
                        m1 - np.zeros((7, )),
                        m1 - np.zeros((3, 7)),
                ]:
                    assert m3.s == s1
                    assert m3.ell_min == m1.ell_min
                    assert m3.ell_max == m1.ell_max
                    assert np.array_equal(m1, m3)
                for m3 in [
                        0 - m1,
                        np.zeros(1) - m1,
                        np.zeros((1, )) - m1,
                        np.zeros((7, )) - m1,
                        np.zeros((3, 7)) - m1,
                ]:
                    assert m3.s == s1
                    assert m3.ell_min == m1.ell_min
                    assert m3.ell_max == m1.ell_max
                    assert np.array_equal(-m1, m3)
            else:
                m1m2 = m1.subtract(m2)
                assert m1m2.s == s1
                assert m1m2.ell_max == m1.ell_max
                i1 = sf.LM_total_size(0, min(ell_min1, ell_min2) - 1)
                i2 = sf.LM_total_size(0, max(ell_min1, ell_min2) - 1)
                i3 = sf.LM_total_size(0, min(ell_max1, ell_max2))
                i4 = sf.LM_total_size(0, max(ell_max1, ell_max2))
                assert np.array_equiv(m1m2[..., :i1], 0.0)
                assert np.array_equal(m1m2[..., i1:i2], a1[..., :i2 - i1])
                assert np.array_equal(m1m2[..., i1:i2],
                                      m1.view(np.ndarray)[..., i1:i2])
                assert np.array_equal(
                    m1m2[..., i2:i3],
                    m1.view(np.ndarray)[..., i2:i3] -
                    m2.view(np.ndarray)[..., i2:i3])
                assert np.array_equal(m1m2[..., i3:i4],
                                      m1.view(np.ndarray)[..., i3:i4])
                g12 = m1m2.grid()
                n_theta, n_phi = g12.shape[-2:]
                g1 = m1.grid(n_theta, n_phi)
                g2 = m2.grid(n_theta, n_phi)
                assert np.allclose(g1 - g2,
                                   g12,
                                   rtol=tolerance,
                                   atol=tolerance)
Example #15
0
def test_modes_multiplication():
    tolerance = 1e-13
    np.random.seed(1234)
    # Test without truncation
    for i_mul, mul in enumerate([
            np.multiply, lambda a, b: a.multiply(b),
            lambda a, b: a.multiply(b, truncator=max)
    ]):
        for s1 in range(-2, 2 + 1):
            ell_min1 = abs(s1)
            ell_max1 = 8
            a1 = np.random.rand(3, 7,
                                sf.LM_total_size(ell_min1, ell_max1) *
                                2).view(complex)
            m1 = sf.Modes(a1,
                          spin_weight=s1,
                          ell_min=ell_min1,
                          ell_max=ell_max1)
            # Check scalar multiplications
            s = np.random.rand()
            m1s = mul(m1, s)
            assert m1.s == s1
            assert m1s.ell_max == m1.ell_max
            g1s = m1s.grid()
            n_theta, n_phi = g1s.shape[-2:]
            g1 = m1.grid(n_theta, n_phi)
            assert np.allclose(g1 * s, g1s, rtol=tolerance, atol=tolerance)
            if mul is np.multiply:
                sm1 = mul(s, m1)
                assert sm1.s == s1
                assert sm1.ell_max == m1.ell_max
                sg1 = sm1.grid()
                n_theta, n_phi = sg1.shape[-2:]
                g1 = m1.grid(n_theta, n_phi)
                assert np.allclose(s * g1, sg1, rtol=tolerance, atol=tolerance)
            # Check scalar-array multiplications
            s = np.random.rand(3, 7)
            m1s = mul(m1, s)
            assert m1.s == s1
            assert m1s.ell_max == m1.ell_max
            g1s = m1s.grid()
            n_theta, n_phi = g1s.shape[-2:]
            g1 = m1.grid(n_theta, n_phi)
            assert np.allclose(g1 * s, g1s, rtol=tolerance, atol=tolerance)
            if mul is np.multiply:
                sm1 = mul(s, m1)
                assert sm1.s == s1
                assert sm1.ell_max == m1.ell_max
                sg1 = sm1.grid()
                n_theta, n_phi = sg1.shape[-2:]
                g1 = m1.grid(n_theta, n_phi)
                assert np.allclose(s * g1, sg1, rtol=tolerance, atol=tolerance)
            # Check spin-weighted multiplications
            for s2 in range(-s1, s1 + 1):
                ell_min2 = ell_min1 + 1
                ell_max2 = ell_max1 - 1
                a2 = np.random.rand(3, 7,
                                    sf.LM_total_size(ell_min2, ell_max2) *
                                    2).view(complex)
                m2 = sf.Modes(a2,
                              spin_weight=s2,
                              ell_min=ell_min2,
                              ell_max=ell_max2)
                m1m2 = mul(m1, m2)
                assert m1m2.s == s1 + s2
                if i_mul == 2:
                    assert m1m2.ell_max == max(m1.ell_max, m2.ell_max)
                else:
                    assert m1m2.ell_max == m1.ell_max + m2.ell_max
                    g12 = m1m2.grid()
                    n_theta, n_phi = g12.shape[-2:]
                    g1 = m1.grid(n_theta, n_phi)
                    g2 = m2.grid(n_theta, n_phi)
                    assert np.allclose(g1 * g2,
                                       g12,
                                       rtol=tolerance,
                                       atol=tolerance)
Example #16
0
def test_modes_derivatives_on_grids():
    # Test various SWSH-derivative expressions on grids
    tolerance = 2e-14
    np.random.seed(1234)
    for s in range(-2, 2 + 1):
        ell_min = 0
        ell_max = abs(s) + 5
        zeros = lambda: np.zeros(sf.LM_total_size(ell_min, ell_max),
                                 dtype=complex)
        for ell in range(abs(s), ell_max + 1):
            for m in range(-ell, ell + 1):
                sYlm = sf.Modes(zeros(),
                                spin_weight=s,
                                ell_min=ell_min,
                                ell_max=ell_max)
                sYlm[sYlm.index(ell, m)] = 1.0
                g_sYlm = sYlm.grid()
                n_theta, n_phi = g_sYlm.shape[-2:]

                # Test Lsquared {s}Y{l,m} = l * (l+1) * {s}Y{l,m}
                L2_sYlm = sYlm.Lsquared()
                g_L2_sYlm = L2_sYlm.grid(n_theta, n_phi)
                factor = ell * (ell + 1)
                assert np.allclose(g_L2_sYlm,
                                   factor * g_sYlm,
                                   rtol=tolerance,
                                   atol=tolerance)

                # Test Lz {s}Y{l,m} = m * {s}Y{l,m}
                Lz_sYlm = sYlm.Lz()
                g_Lz_sYlm = Lz_sYlm.grid(n_theta, n_phi)
                factor = m
                assert np.allclose(g_Lz_sYlm,
                                   factor * g_sYlm,
                                   rtol=tolerance,
                                   atol=tolerance)

                # Test Lplus {s}Y{l,m} = sqrt((l-m)*(l+m+1)) {s}Y{l,m+1}
                invalid = abs(m + 1) > ell
                sYlmp1 = sf.Modes(zeros(),
                                  spin_weight=s,
                                  ell_min=ell_min,
                                  ell_max=ell_max)
                if invalid:
                    with pytest.raises(ValueError):
                        sYlmp1.index(ell, m + 1)
                else:
                    sYlmp1[sYlmp1.index(ell, m + 1)] = 1.0
                g_sYlmp1 = sYlmp1.grid(n_theta, n_phi)
                Lp_sYlm = sYlm.Lplus()
                g_Lp_sYlm = Lp_sYlm.grid(n_theta, n_phi)
                factor = 0.0 if invalid else math.sqrt(
                    (ell - m) * (ell + m + 1))
                assert np.allclose(g_Lp_sYlm,
                                   factor * g_sYlmp1,
                                   rtol=tolerance,
                                   atol=tolerance)

                # Test Lminus {s}Y{l,m} = sqrt((l+m)*(l-m+1)) * {s}Y{l,m-1}
                invalid = abs(m - 1) > ell
                sYlmm1 = sf.Modes(zeros(),
                                  spin_weight=s,
                                  ell_min=ell_min,
                                  ell_max=ell_max)
                if invalid:
                    with pytest.raises(ValueError):
                        sYlmm1.index(ell, m - 1)
                else:
                    sYlmm1[sYlmm1.index(ell, m - 1)] = 1.0
                g_sYlmm1 = sYlmm1.grid(n_theta, n_phi)
                Lm_sYlm = sYlm.Lminus()
                g_Lm_sYlm = Lm_sYlm.grid(n_theta, n_phi)
                factor = 0.0 if invalid else math.sqrt(
                    (ell + m) * (ell - m + 1))
                assert np.allclose(g_Lm_sYlm,
                                   factor * g_sYlmm1,
                                   rtol=tolerance,
                                   atol=tolerance)

                # Test Rsquared {s}Y{l,m} = l * (l+1) * {s}Y{l,m}
                R2_sYlm = sYlm.Rsquared()
                g_R2_sYlm = R2_sYlm.grid(n_theta, n_phi)
                factor = ell * (ell + 1)
                assert np.allclose(g_R2_sYlm,
                                   factor * g_sYlm,
                                   rtol=tolerance,
                                   atol=tolerance)

                # Test Rz {s}Y{l,m} = -s * {s}Y{l,m}
                Rz_sYlm = sYlm.Rz()
                g_Rz_sYlm = Rz_sYlm.grid(n_theta, n_phi)
                factor = -s
                assert np.allclose(g_Rz_sYlm,
                                   factor * g_sYlm,
                                   rtol=tolerance,
                                   atol=tolerance)

                # Test Rplus {s}Y{l,m} = sqrt((l+s)(l-s+1)) {s-1}Y{l,m}
                invalid = abs(s - 1) > ell
                sm1Ylm = sf.Modes(zeros(),
                                  spin_weight=s - 1,
                                  ell_min=ell_min,
                                  ell_max=ell_max)
                if invalid:
                    with pytest.raises(ValueError):
                        sm1Ylm.index(ell, m)
                else:
                    sm1Ylm[sm1Ylm.index(ell, m)] = 1.0
                g_sm1Ylm = sm1Ylm.grid(n_theta, n_phi)
                Rp_sYlm = sYlm.Rplus()
                g_Rp_sYlm = Rp_sYlm.grid(n_theta, n_phi)
                factor = 0.0 if invalid else math.sqrt(
                    (ell + s) * (ell - s + 1))
                assert np.allclose(g_Rp_sYlm,
                                   factor * g_sm1Ylm,
                                   rtol=tolerance,
                                   atol=tolerance)

                # Test Rminus {s}Y{l,m} = sqrt((l-s)(l+s+1)) {s+1}Y{l,m}
                invalid = abs(s + 1) > ell
                sp1Ylm = sf.Modes(zeros(),
                                  spin_weight=s + 1,
                                  ell_min=ell_min,
                                  ell_max=ell_max)
                if invalid:
                    with pytest.raises(ValueError):
                        sp1Ylm.index(ell, m)
                else:
                    sp1Ylm[sp1Ylm.index(ell, m)] = 1.0
                Rm_sYlm = sYlm.Rminus()
                g_sp1Ylm = sp1Ylm.grid(n_theta, n_phi)
                g_Rm_sYlm = Rm_sYlm.grid(n_theta, n_phi)
                factor = 0.0 if invalid else math.sqrt(
                    (ell - s) * (ell + s + 1))
                assert np.allclose(g_Rm_sYlm,
                                   factor * g_sp1Ylm,
                                   rtol=tolerance,
                                   atol=tolerance)

                # Test eth {s}Y{l,m} = sqrt((l-s)(l+s+1)) {s+1}Y{l,m}
                invalid = abs(s + 1) > ell
                sp1Ylm = sf.Modes(zeros(),
                                  spin_weight=s + 1,
                                  ell_min=ell_min,
                                  ell_max=ell_max)
                if invalid:
                    with pytest.raises(ValueError):
                        sp1Ylm.index(ell, m)
                else:
                    sp1Ylm[sp1Ylm.index(ell, m)] = 1.0
                eth_sYlm = sYlm.eth
                g_sp1Ylm = sp1Ylm.grid(n_theta, n_phi)
                g_eth_sYlm = eth_sYlm.grid(n_theta, n_phi)
                factor = 0.0 if invalid else math.sqrt(
                    (ell - s) * (ell + s + 1))
                assert np.allclose(g_eth_sYlm,
                                   factor * g_sp1Ylm,
                                   rtol=tolerance,
                                   atol=tolerance)

                # Test ethbar {s}Y{l,m} = -sqrt((l+s)(l-s+1)) {s-1}Y{l,m}
                invalid = abs(s - 1) > ell
                sm1Ylm = sf.Modes(zeros(),
                                  spin_weight=s - 1,
                                  ell_min=ell_min,
                                  ell_max=ell_max)
                if invalid:
                    with pytest.raises(ValueError):
                        sm1Ylm.index(ell, m)
                else:
                    sm1Ylm[sm1Ylm.index(ell, m)] = 1.0
                g_sm1Ylm = sm1Ylm.grid(n_theta, n_phi)
                ethbar_sYlm = sYlm.ethbar
                g_ethbar_sYlm = ethbar_sYlm.grid(n_theta, n_phi)
                factor = 0.0 if invalid else -math.sqrt(
                    (ell + s) * (ell - s + 1))
                assert np.allclose(g_ethbar_sYlm,
                                   factor * g_sm1Ylm,
                                   rtol=tolerance,
                                   atol=tolerance)

                # Test ethbar eth sYlm = -(l-s)(l+s+1) sYlm
                ethbar_eth_sYlm = sYlm.eth.ethbar
                g_ethbar_eth_sYlm = ethbar_eth_sYlm.grid(n_theta, n_phi)
                factor = 0.0 if (abs(s + 1) > ell or
                                 abs(s) > ell) else -(ell - s) * (ell + s + 1)
                assert np.allclose(g_ethbar_eth_sYlm,
                                   factor * g_sYlm,
                                   rtol=tolerance,
                                   atol=tolerance)
Example #17
0
def test_modes_derivative_commutators():
    tolerance = 1e-13
    np.random.seed(1234)
    # Note that post-fix operators are in the opposite order compared
    # to prefixed commutators, so we pull the post-fix operators out
    # as functions to make things look right.
    np.random.seed(1234)
    L2 = sf.Modes.Lsquared
    Lz = sf.Modes.Lz
    Lp = sf.Modes.Lplus
    Lm = sf.Modes.Lminus
    R2 = sf.Modes.Rsquared
    Rz = sf.Modes.Rz
    Rp = sf.Modes.Rplus
    Rm = sf.Modes.Rminus
    eth = lambda modes: modes.eth
    ethbar = lambda modes: modes.ethbar
    for s in range(-2, 2 + 1):
        ell_min = abs(s)
        ell_max = 8
        a = np.random.rand(3, 7,
                           sf.LM_total_size(ell_min, ell_max) *
                           2).view(complex)
        m = sf.Modes(a, spin_weight=s, ell_min=ell_min, ell_max=ell_max)
        # Test [Ri, Lj] = 0
        for R in [Rz, Rp, Rm]:
            for L in [Lz, Lp, Lm]:
                assert np.max(np.abs(L(R(m)) - R(L(m)))) < tolerance
        # Test [L2, Lj] = 0
        for L in [Lz, Lp, Lm]:
            assert np.max(np.abs(L2(L(m)) - L(L2(m)))) < 5 * tolerance
        # Test [R2, Rj] = 0
        for R in [Rz, Rp, Rm]:
            assert np.max(np.abs(R2(R(m)) - R(R2(m)))) < 5 * tolerance
        # Test [Lz, Lp] = Lp
        assert np.allclose(Lz(Lp(m)) - Lp(Lz(m)),
                           Lp(m),
                           rtol=tolerance,
                           atol=tolerance)
        # Test [Lz, Lm] = -Lm
        assert np.allclose(Lz(Lm(m)) - Lm(Lz(m)),
                           -Lm(m),
                           rtol=tolerance,
                           atol=tolerance)
        # Test [Lp, Lm] = 2Lz
        assert np.allclose(Lp(Lm(m)) - Lm(Lp(m)),
                           2 * Lz(m),
                           rtol=tolerance,
                           atol=tolerance)
        # Test [Rz, Rp] = Rp
        assert np.allclose(Rz(Rp(m)) - Rp(Rz(m)),
                           Rp(m),
                           rtol=tolerance,
                           atol=tolerance)
        # Test [Rz, Rm] = -Rm
        assert np.allclose(Rz(Rm(m)) - Rm(Rz(m)),
                           -Rm(m),
                           rtol=tolerance,
                           atol=tolerance)
        # Test [Rp, Rm] = 2Rz
        assert np.allclose(Rp(Rm(m)) - Rm(Rp(m)),
                           2 * Rz(m),
                           rtol=tolerance,
                           atol=tolerance)
        # Test [ethbar, eth] = 2s
        assert np.allclose(ethbar(eth(m)) - eth(ethbar(m)),
                           2 * m.s * m,
                           rtol=tolerance,
                           atol=tolerance)