Esempio n. 1
0
    def cf2(v, distorted_grid_rotors):
        from math import sin, cos

        # κ = 1 / [γ(1-v⋅r)]
        # ð(κ⁻¹) = - κ⁻² ðκ
        # ðκ = - κ² ð(κ⁻¹)
        theta_phi = sf.theta_phi(n_theta, n_phi)
        kinv = γ * np.array(
            [
                [1 - v[0] * sin(θ) * cos(ϕ) - v[1] * math.sin(θ) * math.sin(ϕ) - v[2] * math.cos(θ) for θ, ϕ in row]
                for row in theta_phi
            ]
        )
        k = 1 / kinv
        κinv = spinsfast.map2salm(kinv, 0, ell_max)
        κ = spinsfast.map2salm(k, 0, ell_max)
        SWSHs = sf.SWSH_grid(distorted_grid_rotors, 0, ell_max)
        one_over_k2 = np.tensordot(κinv, SWSHs, axes=([-1], [-1]))
        one_over_k_cubed2 = one_over_k2 ** 3
        k2 = 1 / one_over_k2
        ðκ = sf.eth_GHP(κ, 0)
        SWSHs = sf.SWSH_grid(distorted_grid_rotors, 1, ell_max)
        ðk = np.tensordot(ðκ, SWSHs, axes=([-1], [-1]))
        ðk_over_k2 = ðk / k2
        return (
            k2[np.newaxis, :, :],
            ðk_over_k2[np.newaxis, :, :],
            one_over_k2[np.newaxis, :, :],
            one_over_k_cubed2[np.newaxis, :, :],
        )
Esempio n. 2
0
def silly_angular_momentum_flux(h, hdot=None):
    """Compute angular momentum flux from waveform explicitly

    This function uses very explicit (and slow) methods, but different as far as possible from the
    methods used in the main code.

    """
    import spinsfast
    hdot = hdot or h.data_dot
    zeros = np.zeros((hdot.shape[0], 4), dtype=hdot.dtype)
    data = np.concatenate((zeros, hdot), axis=1)  # Pad with zeros for spinsfast
    ell_min = 0
    ell_max = 2*h.ell_max  # Maximum ell value required to fully resolve Ji{h} * hdot
    n_theta = 2*ell_max + 1
    n_phi = n_theta
    hdot_map = spinsfast.salm2map(data, h.spin_weight, h.ell_max, n_theta, n_phi)

    jdot = np.zeros((h.n_times, 3), dtype=float)

    # Compute J_+ h
    for ell in range(h.ell_min, h.ell_max+1):
        i = h.index(ell, -ell) + 4
        data[:, i] = 0.0j
        for m in range(-ell, ell):
            i = h.index(ell, m+1) + 4
            j = h.index(ell, m)
            data[:, i] = 1.j * np.sqrt((ell-m)*(ell+m+1)) * h.data[:, j]
    jplus_h_map = spinsfast.salm2map(data, h.spin_weight, h.ell_max, n_theta, n_phi)

    # Compute J_- h
    for ell in range(h.ell_min, h.ell_max+1):
        for m in range(-ell+1, ell+1):
            i = h.index(ell, m-1) + 4
            j = h.index(ell, m)
            data[:, i] = 1.j * np.sqrt((ell+m)*(ell-m+1)) * h.data[:, j]
        i = h.index(ell, ell) + 4
        data[:, i] = 0.0j
    jminus_h_map = spinsfast.salm2map(data, h.spin_weight, h.ell_max, n_theta, n_phi)

    # Combine jplus and jminus to compute x-y components, then conjugate, multiply by hdot, and integrate
    jx_h_map = 0.5 * (jplus_h_map + jminus_h_map)
    jy_h_map = -0.5j * (jplus_h_map - jminus_h_map)
    jdot[:, 0] = -spinsfast.map2salm(jx_h_map.conjugate() * hdot_map, 0, 0)[..., 0].real * (2*np.sqrt(np.pi)) / (16*np.pi)
    jdot[:, 1] = -spinsfast.map2salm(jy_h_map.conjugate() * hdot_map, 0, 0)[..., 0].real * (2*np.sqrt(np.pi)) / (16*np.pi)

    # Compute J_z h, then conjugate, multiply by hdot, and integrate
    for ell in range(h.ell_min, h.ell_max+1):
        for m in range(-ell, ell+1):
            i = h.index(ell, m)
            data[:, i+4] = 1.j * m * h.data[:, i]
    jz_h_map = spinsfast.salm2map(data, h.spin_weight, h.ell_max, n_theta, n_phi)
    jdot[:, 2] = -spinsfast.map2salm(jz_h_map.conjugate() * hdot_map, 0, 0)[..., 0].real * (2*np.sqrt(np.pi)) / (16*np.pi)

    return jdot
Esempio n. 3
0
    def speciality_index(self, **kwargs):
        """Computes the Baker-Campanelli speciality index (arXiv:gr-qc/0003031). NOTE: This quantity can only 
        determine algebraic speciality but can not determine the type! The rule of thumb given by Baker and
        Campanelli is that for an algebraically special spacetime the speciality index should differ from unity 
        by no more than a factor of two.

        """

        import spinsfast
        import spherical_functions as sf
        from spherical_functions import LM_index

        output_ell_max = kwargs.pop("output_ell_max") if "output_ell_max" in kwargs else self.ell_max
        working_ell_max = kwargs.pop("working_ell_max") if "working_ell_max" in kwargs else 2 * self.ell_max
        n_theta = n_phi = 2 * working_ell_max + 1

        # Transform to grid representation
        psi4 = np.empty((self.n_times, n_theta, n_phi), dtype=complex)
        psi3 = np.empty((self.n_times, n_theta, n_phi), dtype=complex)
        psi2 = np.empty((self.n_times, n_theta, n_phi), dtype=complex)
        psi1 = np.empty((self.n_times, n_theta, n_phi), dtype=complex)
        psi0 = np.empty((self.n_times, n_theta, n_phi), dtype=complex)

        for t_i in range(self.n_times):
            psi4[t_i, :, :] = spinsfast.salm2map(
                self.psi4.ndarray[t_i, :], self.psi4.spin_weight, lmax=self.ell_max, Ntheta=n_theta, Nphi=n_phi
            )
            psi3[t_i, :, :] = spinsfast.salm2map(
                self.psi3.ndarray[t_i, :], self.psi3.spin_weight, lmax=self.ell_max, Ntheta=n_theta, Nphi=n_phi
            )
            psi2[t_i, :, :] = spinsfast.salm2map(
                self.psi2.ndarray[t_i, :], self.psi2.spin_weight, lmax=self.ell_max, Ntheta=n_theta, Nphi=n_phi
            )
            psi1[t_i, :, :] = spinsfast.salm2map(
                self.psi1.ndarray[t_i, :], self.psi1.spin_weight, lmax=self.ell_max, Ntheta=n_theta, Nphi=n_phi
            )
            psi0[t_i, :, :] = spinsfast.salm2map(
                self.psi0.ndarray[t_i, :], self.psi0.spin_weight, lmax=self.ell_max, Ntheta=n_theta, Nphi=n_phi
            )

        curvature_invariant_I = psi4 * psi0 - 4 * psi3 * psi1 + 3 * psi2 ** 2
        curvature_invariant_J = (
            psi4 * (psi2 * psi0 - psi1 ** 2) - psi3 * (psi3 * psi0 - psi1 * psi2) + psi2 * (psi3 * psi1 - psi2 ** 2)
        )
        speciality_index = 27 * curvature_invariant_J ** 2 / curvature_invariant_I ** 3

        # Transform back to mode representation
        speciality_index_modes = np.empty((self.n_times, (working_ell_max) ** 2), dtype=complex)
        for t_i in range(self.n_times):
            speciality_index_modes[t_i, :] = spinsfast.map2salm(speciality_index[t_i, :], 0, lmax=working_ell_max - 1)

        # Convert product ndarray to a ModesTimeSeries object
        speciality_index_modes = speciality_index_modes[:, : LM_index(output_ell_max, output_ell_max, 0) + 1]
        speciality_index_modes = ModesTimeSeries(
            sf.SWSH_modes.Modes(
                speciality_index_modes, spin_weight=0, ell_min=0, ell_max=output_ell_max, multiplication_truncator=max
            ),
            time=self.t,
        )
        return speciality_index_modes
Esempio n. 4
0
def modes(self, ell_max=None, **kwargs):
    """Return mode weights of function decomposed into SWSHs

    This method uses `spinsfast` to convert values on an equiangular grid to mode weights.

    The output array has one less dimension than this object; rather than the last two axes giving
    the values on the two-dimensional grid, the last axis gives the mode weights.

    Parameters
    ==========
    ell_max: None or int [defaults to None]
        Maximum ell value in the output.  If None, the result will have enough ell values to express
        the data on the grid without aliasing: (max(n_phi, n_theta) - 1) // 2.
    **kwargs: any types
        Additional keyword arguments are passed through to the Modes constructor on output

    """
    import copy
    import numpy as np
    import spinsfast
    from .. import Modes
    ell_max = ell_max or (max(n_phi, n_theta) - 1) // 2
    metadata = copy.copy
    return Modes(spinsfast.map2salm(self.view(np.ndarray), self.s, ell_max),
                 spin_weight=self.s,
                 ell_min=0,
                 ell_max=ell_max,
                 **metadata)
Esempio n. 5
0
    def to_modes(self, ell_max=None, ell_min=None):
        """Transform to modes of a spin-weighted spherical harmonic expansion

        Parameters
        ----------
        self : WaveformGrid object
            This is the object to be transformed to SWSH modes
        ell_max : int, optional
            The largest ell value to include in the output data.  Default value
            is deduced from n_theta and n_phi.
        ell_min : int, optional
            The smallest ell value to include in the output data.  Default value
            is abs(spin_weight).

        """
        s = SpinWeights[self.dataType]
        if ell_max is None:
            ell_max = int((max(self.n_theta, self.n_phi) - 1) // 2)
        if ell_min is None:
            ell_min = abs(s)
        if not isinstance(ell_max, numbers.Integral) or ell_max < 0:
            raise ValueError(f"Input `ell_max` should be a nonnegative integer; got `{ell_max}`.")
        if not isinstance(ell_min, numbers.Integral) or ell_min < 0 or ell_min > ell_max:
            raise ValueError(f"Input `ell_min` should be an integer between 0 and {ell_max}; got `{ell_min}`.")

        final_dim = int(np.prod(self.data.shape[2:]))
        old_data = self.data.reshape((self.n_times, self.n_theta, self.n_phi, final_dim))
        new_data = np.empty((self.n_times, sf.LM_total_size(ell_min, ell_max), final_dim), dtype=complex)
        # Note that spinsfast returns all modes, including ell<abs(s).  So we just chop those off
        for i_time in range(self.n_times):
            for i_final in range(final_dim):
                new_data[i_time, :, i_final] = spinsfast.map2salm(old_data[i_time, :, :, i_final], s, ell_max)[
                    sf.LM_index(ell_min, -ell_min, 0) :
                ]
        new_data = new_data.reshape((self.n_times, sf.LM_total_size(ell_min, ell_max)) + self.data.shape[2:])

        # old_data = self.data.reshape((self.n_times, self.n_theta, self.n_phi)+self.data.shape[2:])
        # new_data = np.empty((self.n_times, sf.LM_total_size(ell_min, ell_max))+self.data.shape[2:], dtype=complex)
        # # Note that spinsfast returns all modes, including ell<abs(s).  So we just chop those off
        # for i_time in range(self.n_times):
        #     new_data[i_time, :] = spinsfast.map2salm(old_data[i_time, :, :], s, ell_max)\
        #         [sf.LM_index(ell_min, -ell_min, 0):]

        m = WaveformModes(
            t=self.t,
            data=new_data,
            history=self.history,
            ell_min=ell_min,
            ell_max=ell_max,
            frameType=self.frameType,
            dataType=self.dataType,
            r_is_scaled_out=self.r_is_scaled_out,
            m_is_scaled_out=self.m_is_scaled_out,
            constructor_statement=f"{self}.to_modes({ell_max})",
        )
        return m
def test_eth_derivation(eth, spin_weight_of_eth):
    """Ensure that the various `eth` operators are derivations -- i.e., they obey the Leibniz product law

    Given two spin-weighted functions `f` and `g`, we need to test that

        eth(f * g) = eth(f) * g + f * eth(g)

    This test generates a set of random modes with equal power for `f` and `g`
    (though more realistic functions can be expected to have exponentially decaying
    mode amplitudes).  Because of the large power in high-ell modes, we need to
    double the number of modes in the representation of their product, which is why
    we use

        n_theta = n_phi = 4 * ell_max + 1

    These `f` and `g` functions must be transformed to the physical-space
    representation, multiplied there, the product transformed back to spectral
    space, the eth operator evaluated, and then transformed back again to physical
    space for comparison.

    We test both the Newman-Penrose and Geroch-Held-Penrose versions of eth, as
    well as their conjugates.

    """
    import spinsfast
    ell_max = 16
    n_modes = sf.LM_total_size(0, ell_max)
    n_theta = n_phi = 4 * ell_max + 1
    for s1 in range(-2, 2 + 1):
        for s2 in range(-s1, s1 + 1):
            np.random.seed(1234)
            ell_min1 = abs(s1)
            ell_min2 = abs(s2)
            f = np.random.rand(n_modes) + 1j * np.random.rand(n_modes)
            f[:sf.LM_total_size(0, ell_min1 - 1)] = 0j
            f_j_k = spinsfast.salm2map(f, s1, ell_max, n_theta, n_phi)
            g = np.random.rand(n_modes) + 1j * np.random.rand(n_modes)
            g[:sf.LM_total_size(0, ell_min2 - 1)] = 0j
            g_j_k = spinsfast.salm2map(g, s2, ell_max, n_theta, n_phi)
            fg_j_k = f_j_k * g_j_k
            fg = spinsfast.map2salm(fg_j_k, s1 + s2, 2 * ell_max)
            ethf = eth(f, s1, ell_min=0)
            ethg = eth(g, s2, ell_min=0)
            ethfg = eth(fg, s1 + s2, ell_min=0)
            ethf_j_k = spinsfast.salm2map(ethf, s1 + spin_weight_of_eth,
                                          ell_max, n_theta, n_phi)
            ethg_j_k = spinsfast.salm2map(ethg, s2 + spin_weight_of_eth,
                                          ell_max, n_theta, n_phi)
            ethfg_j_k = spinsfast.salm2map(ethfg, s1 + s2 + spin_weight_of_eth,
                                           2 * ell_max, n_theta, n_phi)
            assert np.allclose(ethfg_j_k,
                               ethf_j_k * g_j_k + f_j_k * ethg_j_k,
                               rtol=1e-10,
                               atol=1e-10)
Esempio n. 7
0
def silly_momentum_flux(h):
    """Compute momentum flux from waveform with a silly but simple method

    This function evaluates the momentum-flux formula quite literally.  The formula is

         dp       R**2  | dh |**2 ^
    ---------- = ------ | -- |    n
    dOmega  dt   16  pi | dt |

    Here, p and nhat are vectors and R is the distance to the source.  The input h is differentiated
    numerically to find modes of dh/dt, which are then used to construct the values of dh/dt on a
    grid.  At each point of that grid, the value of |dh/dt|**2 nhat is computed, which is then
    integrated over the sphere to arrive at dp/dt.  Note that this integration is accomplished by
    way of a spherical-harmonic decomposition; the ell=m=0 mode is multiplied by 2*sqrt(pi) to
    arrive at the result that would be achieved by integrating over the sphere.

    """
    import spinsfast

    hdot = h.data_dot
    zeros = np.zeros((hdot.shape[0], 4), dtype=hdot.dtype)
    data = np.concatenate((zeros, hdot),
                          axis=1)  # Pad with zeros for spinsfast
    ell_min = 0
    ell_max = 2 * h.ell_max + 1  # Maximum ell value required for nhat*|hdot|^2
    n_theta = 2 * ell_max + 1
    n_phi = n_theta
    hdot_map = spinsfast.salm2map(data, h.spin_weight, h.ell_max, n_theta,
                                  n_phi)
    hdot_mag_squared_map = hdot_map * hdot_map.conjugate()

    theta = np.linspace(0.0, np.pi, num=n_theta, endpoint=True)
    phi = np.linspace(0.0, 2 * np.pi, num=n_phi, endpoint=False)
    x = np.outer(np.sin(theta), np.cos(phi))
    y = np.outer(np.sin(theta), np.sin(phi))
    z = np.outer(np.cos(theta), np.ones_like(phi))

    pdot = np.array([
        spinsfast.map2salm(hdot_mag_squared_map * n /
                           (16 * np.pi), 0, 0)[..., 0].real *
        (2 * np.sqrt(np.pi)) for n in [x, y, z]
    ]).T

    return pdot
def test_eth_derivation(eth, spin_weight_of_eth):
    """Ensure that the various `eth` operators are derivations -- i.e., they obey the Leibniz product law

    Given two spin-weighted functions `f` and `g`, we need to test that

        eth(f * g) = eth(f) * g + f * eth(g)

    This test generates a set of random modes with equal power for `f` and `g` (though more realistic functions can
    be expected to have exponentially decaying mode amplitudes).  Because of the large power in high-ell modes,
    we need to double the number of modes in the representation of their product, which is why we use

        n_theta = n_phi = 4 * ell_max + 1

    These `f` and `g` functions must be transformed to the physical-space representation, multiplied there,
    the product transformed back to spectral space, the eth operator evaluated, and then transformed back again to
    physical space for comparison.

    We test both the Newman-Penrose and Geroch-Held-Penrose versions of eth, as well as their conjugates.

    """
    import spinsfast
    ell_max = 16
    n_modes = sf.LM_total_size(0, ell_max)
    n_theta = n_phi = 4 * ell_max + 1
    for s1 in range(-2, 2 + 1):
        for s2 in range(-s1, s1 + 1):
            np.random.seed(1234)
            ell_min1 = abs(s1)
            ell_min2 = abs(s2)
            f = np.random.rand(n_modes) + 1j * np.random.rand(n_modes)
            f[:sf.LM_total_size(0, ell_min1-1)] = 0j
            f_j_k = spinsfast.salm2map(f, s1, ell_max, n_theta, n_phi)
            g = np.random.rand(n_modes) + 1j * np.random.rand(n_modes)
            g[:sf.LM_total_size(0, ell_min2-1)] = 0j
            g_j_k = spinsfast.salm2map(g, s2, ell_max, n_theta, n_phi)
            fg_j_k = f_j_k * g_j_k
            fg = spinsfast.map2salm(fg_j_k, s1+s2, 2*ell_max)
            ethf = eth(f, s1, ell_min=0)
            ethg = eth(g, s2, ell_min=0)
            ethfg = eth(fg, s1+s2, ell_min=0)
            ethf_j_k = spinsfast.salm2map(ethf, s1+spin_weight_of_eth, ell_max, n_theta, n_phi)
            ethg_j_k = spinsfast.salm2map(ethg, s2+spin_weight_of_eth, ell_max, n_theta, n_phi)
            ethfg_j_k = spinsfast.salm2map(ethfg, s1+s2+spin_weight_of_eth, 2*ell_max, n_theta, n_phi)
            assert np.allclose(ethfg_j_k, ethf_j_k * g_j_k + f_j_k * ethg_j_k, rtol=1e-10, atol=1e-10)
Esempio n. 9
0
    def spinsfast_multiply(f, ellmin_f, ellmax_f, s_f, g, ellmin_g, ellmax_g, s_g):
        """Multiply functions pointwise

        This function takes the same arguments as the spherical_functions.multiply function and
        returns the same quantities.  However, this performs the multiplication simply by
        transforming the modes to function values on a (theta, phi) grid, multiplying those values
        at each point, and then transforming back to modes.

        """
        s_fg = s_f + s_g
        ellmin_fg = 0
        ellmax_fg = ellmax_f + ellmax_g
        n_theta = 2*ellmax_fg + 1
        n_phi = n_theta
        f_map = spinsfast.salm2map(f, s_f, ellmax_f, n_theta, n_phi)
        g_map = spinsfast.salm2map(g, s_g, ellmax_g, n_theta, n_phi)
        fg_map = f_map * g_map
        fg = spinsfast.map2salm(fg_map, s_fg, ellmax_fg)
        return fg, ellmin_fg, ellmax_fg, s_fg
Esempio n. 10
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
Esempio n. 11
0
# Fill the alm with white noise
seed(3124432)
for l in range(abs(s),lmax+1) :
    for m in range(-l,l+1) :
        i = spinsfast.lm_ind(l,m,lmax)
        if (m==0) :
            alm[i] = normal()
        else :
            alm[i] = normal()/2 + 1j*normal()/2


f =  spinsfast.salm2map(alm,s,lmax,Ntheta,Nphi)
# In this pixelization, access the map with f[itheta,iphi]
# where 0 <= itheta <= Ntheta-1 and 0<= iphi <= Nphi-1
# and theta = pi*itheta/(Ntheta-1) phi = 2*pi*iphi/Nphi


alm2 = spinsfast.map2salm(f,s,lmax)

diff_max = max((alm-alm2))
print("max(alm2-alm) = "+str(diff_max))

figure()
imshow(f.real,interpolation='nearest')
colorbar()
title("Real part of f")
xlabel("iphi")
ylabel("itheta")
show()
Esempio n. 12
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
Esempio n. 13
0
    def grid_multiply(self, mts, **kwargs):
        """Compute mode weights of the product of two functions

        This will compute the values of `self` and `mts` on a grid, multiply the grid
        values together, and then return the mode coefficients of the product.  This
        takes less time and memory compared to the `SWSH_modes.Modes.multiply()`
        function, at the risk of introducing aliasing effects if `working_ell_max` is
        too small.

        Parameters
        ----------
        self: ModesTimeSeries
            One of the quantities to multiply.
        mts: ModesTimeSeries
            The quantity to multiply with 'self'.
        working_ell_max: int, optional
            The value of ell_max to be used to define the computation grid. The
            number of theta points and the number of phi points are set to
            2*working_ell_max+1. Defaults to (self.ell_max + mts.ell_max).
        output_ell_max: int, optional
            The value of ell_max in the output mts object. Defaults to self.ell_max.

        """
        import spinsfast
        from spherical_functions import LM_index

        output_ell_max = kwargs.pop("output_ell_max", self.ell_max)
        working_ell_max = kwargs.pop("working_ell_max",
                                     self.ell_max + mts.ell_max)
        n_theta = n_phi = 2 * working_ell_max + 1

        if self.n_times != mts.n_times or not np.equal(self.t, mts.t).all():
            raise ValueError(
                "The time series of objects to be multiplied must be the same."
            )

        # Transform to grid representation
        self_grid = spinsfast.salm2map(self.ndarray,
                                       self.spin_weight,
                                       lmax=self.ell_max,
                                       Ntheta=n_theta,
                                       Nphi=n_phi)
        mts_grid = spinsfast.salm2map(mts.ndarray,
                                      mts.spin_weight,
                                      lmax=mts.ell_max,
                                      Ntheta=n_theta,
                                      Nphi=n_phi)

        product_grid = self_grid * mts_grid
        product_spin_weight = self.spin_weight + mts.spin_weight

        # Transform back to mode representation
        product = spinsfast.map2salm(product_grid,
                                     product_spin_weight,
                                     lmax=working_ell_max)

        # Convert product ndarray to a ModesTimeSeries object
        product = product[:, :LM_index(output_ell_max, output_ell_max, 0) + 1]
        product = ModesTimeSeries(
            spherical_functions.SWSH_modes.Modes(
                product,
                spin_weight=product_spin_weight,
                ell_min=0,
                ell_max=output_ell_max,
                multiplication_truncator=max,
            ),
            time=self.t,
        )
        return product
Esempio n. 14
0
Nlm = spinsfast.N_lm(lmax)
alm = zeros(Nlm, dtype=complex)

# Fill the alm with white noise
seed(3124432)
for l in range(abs(s), lmax + 1):
    for m in range(-l, l + 1):
        i = spinsfast.lm_ind(l, m, lmax)
        if (m == 0):
            alm[i] = normal()
        else:
            alm[i] = normal() / 2 + 1j * normal() / 2

f = spinsfast.salm2map(alm, s, lmax, Ntheta, Nphi)
# In this pixelization, access the map with f[itheta,iphi]
# where 0 <= itheta <= Ntheta-1 and 0<= iphi <= Nphi-1
# and theta = pi*itheta/(Ntheta-1) phi = 2*pi*iphi/Nphi

alm2 = spinsfast.map2salm(f, s, lmax)

diff_max = max((alm - alm2))
print("max(alm2-alm) = " + str(diff_max))

figure()
imshow(f.real, interpolation='nearest')
colorbar()
title("Real part of f")
xlabel("iphi")
ylabel("itheta")
show()