Exemple #1
0
    def z_parity_conjugate(self):
        """Reflect modes across x-y plane (along z axis)

        See "Gravitational-wave modes from precessing black-hole binaries" by
        Boyle et al. (2014) for more details.

        """
        if self.dataType == UnknownDataType:
            raise ValueError("Cannot compute parity type for {0}.".format(
                self.data_type_string))
        W = self[:, :
                 0]  # W without `data`, and `ells`=(0,-1); different from `self.copy_without_data()`
        W.data = np.empty_like(self.data)
        W.ells = self.ells
        W.frame = np.z_parity_conjugate(self.frame)
        s = self.spin_weight
        for ell in range(W.ell_min, W.ell_max + 1):
            if ((ell + s) % 2) == 0:
                lm_indices = [
                    sf.LM_index(ell, m, W.ell_min)
                    for m in range(-ell, ell + 1)
                ]
                W.data[:, lm_indices] = np.conjugate(
                    self.data[:, list(reversed(lm_indices))])
            else:
                lm_indices = [
                    sf.LM_index(ell, m, W.ell_min)
                    for m in range(-ell, ell + 1)
                ]
                W.data[:, lm_indices] = -np.conjugate(
                    self.data[:, list(reversed(lm_indices))])
        W.__history_depth__ -= 1
        W._append_history('{0} = {1}.z_parity_conjugate'.format(W, self))
        return W
Exemple #2
0
    def parity_conjugate(self):
        """Reflect modes along all axes

        See "Gravitational-wave modes from precessing black-hole binaries" by
        Boyle et al. (2014) for more details.

        """
        if self.dataType == UnknownDataType:
            raise ValueError(
                f"Cannot compute parity type for {self.data_type_string}.")
        W = self[:, :
                 0]  # W without `data`, and `ells`=(0,-1); different from `self.copy_without_data()`
        W.data = np.empty_like(self.data)
        W.ells = self.ells
        W.frame = np.parity_conjugate(self.frame)
        s = self.spin_weight
        for ell in range(W.ell_min, W.ell_max + 1):
            lm_indices = [
                sf.LM_index(ell, m, W.ell_min) for m in range(-ell, ell + 1)
                if ((ell + s + m) % 2) == 0
            ]
            W.data[:, lm_indices] = np.conjugate(
                self.data[:, list(reversed(lm_indices))])
            lm_indices = [
                sf.LM_index(ell, m, W.ell_min) for m in range(-ell, ell + 1)
                if ((ell + s + m) % 2) != 0
            ]
            W.data[:, lm_indices] = -np.conjugate(
                self.data[:, list(reversed(lm_indices))])
        W.__history_depth__ -= 1
        W._append_history(f"{W} = {self}.parity_conjugate")
        return W
Exemple #3
0
 def modifier(sigma, sigmadot, sigmaddot, psi2, psi1, psi0):
     # Nonsensical values that should have no effect
     psi0[:sf.LM_index(1, 1, 0)] = 1.234
     psi1[0] = 0.123
     sigma[:sf.LM_index(1, 1, 0)] = 0.567
     sigmadot[:sf.LM_index(1, 1, 0)] = 0.678
     sigmaddot[:sf.LM_index(1, 1, 0)] = 0.789
     # Actual values that should carry through
     psi2[sf.LM_index(0, 0, 0)] = 0.234
Exemple #4
0
def test_supertranslation_inverses():
    w1 = samples.random_waveform_proportional_to_time(rotating=False)
    ell_max = 4
    for ellpp, mpp in sf.LM_range(0, ell_max):
        supertranslation = np.zeros((sf.LM_total_size(0, ell_max), ),
                                    dtype=complex)
        if mpp == 0:
            supertranslation[sf.LM_index(ellpp, mpp, 0)] = 1.0
        elif mpp < 0:
            supertranslation[sf.LM_index(ellpp, mpp, 0)] = 1.0
            supertranslation[sf.LM_index(ellpp, -mpp, 0)] = (-1.0)**mpp
        elif mpp > 0:
            supertranslation[sf.LM_index(ellpp, mpp, 0)] = 1.0j
            supertranslation[sf.LM_index(ellpp, -mpp, 0)] = (-1.0)**mpp * -1.0j
        max_displacement = abs(
            spinsfast.salm2map(supertranslation, 0, ell_max, 4 * ell_max + 1,
                               4 * ell_max + 1)).max()
        w2 = copy.deepcopy(w1)
        w2 = w2.transform(supertranslation=supertranslation)
        w2 = w2.transform(supertranslation=-supertranslation)

        i1A = np.argmin(abs(w1.t - (w1.t[0] + 3 * max_displacement)))
        i1B = np.argmin(abs(w1.t - (w1.t[-1] - 3 * max_displacement)))
        i2A = np.argmin(abs(w2.t - w1.t[i1A]))
        i2B = np.argmin(abs(w2.t - w1.t[i1B]))
        try:
            assert np.allclose(w1.t[i1A:i1B + 1],
                               w2.t[i2A:i2B + 1],
                               rtol=0.0,
                               atol=1e-15), (
                                   w1.t[i1A],
                                   w2.t[i2A],
                                   w1.t[i1B],
                                   w2.t[i2B],
                                   w1.t[i1A:i1B + 1].shape,
                                   w2.t[i2A:i2B + 1].shape,
                               )
        except ValueError:
            print("Indices:\n\t", i1A, i1B, i2A, i2B)
            print("Times:\n\t", w1.t[i1A], w1.t[i1B], w2.t[i2A], w2.t[i2B])
            raise
        data1 = w1.data[i1A:i1B + 1]
        data2 = w2.data[i2A:i2B + 1]
        try:
            assert np.allclose(data1, data2, rtol=5e-10, atol=5e-14), [
                abs(data1 - data2).max(),
                data1.ravel()[np.argmax(abs(data1 - data2))],
                data2.ravel()[np.argmax(abs(data1 - data2))],
                np.unravel_index(np.argmax(abs(data1 - data2)), data1.shape),
            ]
            # list(sf.LM_range(0, ell_max)[np.unravel_index(np.argmax(abs(data1-data2)),
            #                                               data1.shape)[1]])])
        except:
            print("Indices:\n\t", i1A, i1B, i2A, i2B)
            print("Times:\n\t", w1.t[i1A], w1.t[i1B], w2.t[i2A], w2.t[i2B])
            raise
Exemple #5
0
 def modifier(sigma, sigmadot, sigmaddot, psi2, psi1, psi0):
     # Nonsensical values that should have no effect
     psi0[:sf.LM_index(1, 1, 0)] = 1.234
     psi1[0] = 0.123
     sigma[:sf.LM_index(1, 1, 0)] = 0.567
     sigmadot[:sf.LM_index(1, 1, 0)] = 0.678
     sigmaddot[:sf.LM_index(1, 1, 0)] = 0.789
     # Actual values that should carry through
     sigmaddot[sf.LM_index(2, 2,
                           0)] = 0.1 / 10_000**2  # 10_000 = max(time)**2
def evaluate(self, rotors, **kwargs):
    """Return values of function on input rotors"""
    import numpy as np
    import spherical_functions as sf
    SWSHs = sf.SWSH_grid(rotors, self.spin_weight, self.ell_max)
    return np.tensordot(
        self.view(np.ndarray),
        SWSHs[...,
              sf.LM_index(self.ell_min, -self.ell_min, 0
                          ):sf.LM_index(self.ell_max, self.ell_max, 0) + 1],
        axes=([-1], [-1]))
Exemple #7
0
def test_LM_index(ell_max):
    for ell_min in range(ell_max + 1):
        LM = sf.LM_range(ell_min, ell_max)
        for ell in range(ell_min, ell_max + 1):
            for m in range(-ell, ell + 1):
                assert np.array_equal(np.array([ell, m]),
                                      LM[sf.LM_index(ell, m, ell_min)])
Exemple #8
0
def single_mode_proportional_to_time(**kwargs):
    """Return WaveformModes object a single nonzero mode, proportional to time

    The waveform output by this function will have just one nonzero mode.  The behavior of that mode will be
    particularly simple; it will just be proportional to time.

    Parameters
    ----------
    s : int, optional
        Spin weight of the waveform field.  Default is -2.
    ell, m : int, optional
        The (ell, m) values of the nonzero mode in the returned waveform.  Default value is (abs(s), -abs(s)).
    ell_min, ell_max : int, optional
        Smallest and largest ell values present in the output.  Default values are abs(s) and 8.
    data_type : int, optional
        Default value is whichever psi_n corresponds to the input spin.  It is important to choose these, rather than
        `h` or `sigma` for the analytical solution to translations, which doesn't account for the direct contribution
        of supertranslations (as opposed to the indirect contribution, which involves moving points around).
    t_0, t_1 : float, optional
        Beginning and end of time.  Default values are -20. and 20.
    dt : float, optional
        Time step.  Default value is 0.1.
    beta : complex, optional
        Constant of proportionality such that nonzero mode is beta*t.  Default is 1.

    """
    s = kwargs.pop("s", -2)
    ell = kwargs.pop("ell", abs(s))
    m = kwargs.pop("m", -ell)
    ell_min = kwargs.pop("ell_min", abs(s))
    ell_max = kwargs.pop("ell_max", 8)
    data_type = kwargs.pop("data_type",
                           scri.DataType[scri.SpinWeights.index(s)])
    t_0 = kwargs.pop("t_0", -20.0)
    t_1 = kwargs.pop("t_1", 20.0)
    dt = kwargs.pop("dt", 1.0 / 10.0)
    t = np.arange(t_0, t_1 + dt, dt)
    n_times = t.size
    beta = kwargs.pop("beta", 1.0)
    data = np.zeros((n_times, sf.LM_total_size(ell_min, ell_max)),
                    dtype=complex)
    data[:, sf.LM_index(ell, m, ell_min)] = beta * t

    if kwargs:
        import pprint

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

    return scri.WaveformModes(
        t=t,
        data=data,
        ell_min=ell_min,
        ell_max=ell_max,
        frameType=scri.Inertial,
        dataType=data_type,
        r_is_scaled_out=True,
        m_is_scaled_out=True,
    )
Exemple #9
0
def test_trivial_multiplication(multiplication_function):
    """Test 1*g and g*1

    We test trivial multiplication by 1.  The modes of the `1` function are just sqrt(4*pi) for the
    (ell,m)=(0,0) mode, and 0 for everything else.  Even though this can be described by just the
    first element, we test this for each ellmax_f up to 8 just to make sure the multiplication
    function works as expected.  This is multiplied by a function with random values input for its
    modes.

    """
    np.random.seed(1234)
    atol = 2e-15
    rtol = 2e-15
    ellmax_g = 8
    for ellmax_f in range(1, ellmax_g):
        f = np.zeros(sf.LM_total_size(0, ellmax_f), dtype=np.complex)
        f[0] = np.sqrt(4*np.pi)
        ellmin_f = 0
        s_f = 0
        ellmin_g = 0
        for s_g in range(1-ellmax_g, ellmax_g):
            i_max = sf.LM_index(ellmax_g, ellmax_g, ellmin_g)
            g = np.random.rand(sf.LM_total_size(0, ellmax_g)) + 1j*np.random.rand(sf.LM_total_size(0, ellmax_g))
            g[:sf.LM_total_size(0, abs(s_g)-1)+1] = 0.0
            fg, ellmin_fg, ellmax_fg, s_fg = multiplication_function(f, ellmin_f, ellmax_f, s_f, g, ellmin_g, ellmax_g, s_g)
            assert s_fg == s_f + s_g
            assert ellmin_fg == 0
            assert ellmax_fg == ellmax_f + ellmax_g
            assert np.allclose(fg[:i_max+1], g[:i_max+1], atol=atol, rtol=rtol)
            gf, ellmin_gf, ellmax_gf, s_gf = multiplication_function(g, ellmin_g, ellmax_g, s_g, f, ellmin_f, ellmax_f, s_f)
            assert s_gf == s_g + s_f
            assert ellmin_gf == 0
            assert ellmax_gf == ellmax_g + ellmax_f
            assert np.allclose(gf[:i_max+1], g[:i_max+1], atol=atol, rtol=rtol)
Exemple #10
0
def read_from_h5(file_name, **kwargs):
    """Read data from an H5 file in LVC format"""
    import re
    import h5py
    from scipy.interpolate import InterpolatedUnivariateSpline as Spline

    phase_re = re.compile('phase_l(?P<ell>.*)_m(?P<m>.*)')
    amp_re = re.compile('amp_l(?P<ell>.*)_m(?P<m>.*)')
    
    with h5py.File(file_name) as f:
        t = f['NRtimes'][:]
        ell_m = np.array([[int(match['ell']), int(match['m'])] for key in f for match in [phase_re.match(key)] if match])
        ell_min = np.min(ell_m[:, 0])
        ell_max = np.max(ell_m[:, 0])
        data = np.empty((t.size, sf.LM_total_size(ell_min, ell_max)), dtype=complex)
        for ell in range(ell_min, ell_max+1):
            for m in range(-ell, ell+1):
                amp = Spline(f['amp_l{0}_m{1}/X'.format(ell, m)][:],
                             f['amp_l{0}_m{1}/Y'.format(ell, m)][:],
                             k=int(f['amp_l{0}_m{1}/deg'.format(ell, m)][()]))(t)
                phase = Spline(f['phase_l{0}_m{1}/X'.format(ell, m)][:],
                               f['phase_l{0}_m{1}/Y'.format(ell, m)][:],
                               k=int(f['phase_l{0}_m{1}/deg'.format(ell, m)][()]))(t)
                data[:, sf.LM_index(ell, m, ell_min)] = amp * np.exp(1j * phase)
        if 'auxiliary-info' in f and 'history.txt' in f['auxiliary-info']:
            history = ("### " + f['auxiliary-info/history.txt'][()].decode().replace('\n', '\n### ')).split('\n')
        else:
            history = [""]
        constructor_statement = "scri.LVC.read_from_h5('{0}')".format(file_name)
        w = WaveformModes(t=t, data=data, ell_min=ell_min, ell_max=ell_max,
                          frameType=Inertial, dataType=h,
                          history=history, constructor_statement=constructor_statement,
                          r_is_scaled_out=True, m_is_scaled_out=True)

    return w
def single_mode_constant_rotation(**kwargs):
    """Return WaveformModes object a single nonzero mode, with phase proportional to time

    The waveform output by this function will have just one nonzero mode.  The behavior of that mode will be fairly
    simple; it will be given by exp(i*omega*t).  Note that omega can be complex, which gives damping.

    Parameters
    ----------
    s : int, optional
        Spin weight of the waveform field.  Default is -2.
    ell, m : int, optional
        The (ell, m) values of the nonzero mode in the returned waveform.  Default value is (abs(s), -abs(s)).
    ell_min, ell_max : int, optional
        Smallest and largest ell values present in the output.  Default values are abs(s) and 8.
    data_type : int, optional
        Default value is whichever psi_n corresponds to the input spin.  It is important to choose these, rather than
        `h` or `sigma` for the analytical solution to translations, which doesn't account for the direct contribution
        of supertranslations (as opposed to the indirect contribution, which involves moving points around).
    t_0, t_1 : float, optional
        Beginning and end of time.  Default values are -20. and 20.
    dt : float, optional
        Time step.  Default value is 0.1.
    omega : complex, optional
        Constant of proportionality such that nonzero mode is exp(i*omega*t).  Note that this can be complex, which
        implies damping.  Default is 0.5.

    """
    s = kwargs.pop('s', -2)
    ell = kwargs.pop('ell', abs(s))
    m = kwargs.pop('m', -ell)
    ell_min = kwargs.pop('ell_min', abs(s))
    ell_max = kwargs.pop('ell_max', 8)
    data_type = kwargs.pop('data_type',
                           scri.DataType[scri.SpinWeights.index(s)])
    t_0 = kwargs.pop('t_0', -20.0)
    t_1 = kwargs.pop('t_1', 20.0)
    dt = kwargs.pop('dt', 1. / 10.)
    t = np.arange(t_0, t_1 + dt, dt)
    n_times = t.size
    omega = complex(kwargs.pop('omega', 0.5))
    data = np.zeros((n_times, sf.LM_total_size(ell_min, ell_max)),
                    dtype=complex)
    data[:, sf.LM_index(ell, m, ell_min)] = np.exp(1j * omega * t)

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

    return scri.WaveformModes(t=t,
                              data=data,
                              ell_min=ell_min,
                              ell_max=ell_max,
                              frameType=scri.Inertial,
                              dataType=data_type,
                              r_is_scaled_out=True,
                              m_is_scaled_out=True)
Exemple #12
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
Exemple #13
0
def test_hyper_translation():
    """Compare code-transformed waveform to analytically transformed waveform"""
    print("")
    ell_max = 4
    for s in range(-2, 2+1):
        for ell in range(abs(s), ell_max+1):
            for m in range(-ell, ell+1):
                print("\tWorking on spin s =", s, ", ell =", ell, ", m =", m)
                for ellpp, mpp in sf.LM_range(2, ell_max):
                    supertranslation = np.zeros((sf.LM_total_size(0, ell_max),), dtype=complex)
                    if mpp == 0:
                        supertranslation[sf.LM_index(ellpp, mpp, 0)] = 1.0
                    elif mpp < 0:
                        supertranslation[sf.LM_index(ellpp, mpp, 0)] = 1.0
                        supertranslation[sf.LM_index(ellpp, -mpp, 0)] = (-1.0)**mpp
                    elif mpp > 0:
                        supertranslation[sf.LM_index(ellpp, mpp, 0)] = 1.0j
                        supertranslation[sf.LM_index(ellpp, -mpp, 0)] = (-1.0)**mpp * -1.0j
                    max_displacement = abs(spinsfast.salm2map(supertranslation, 0,
                                                              ell_max, 4*ell_max+1, 4*ell_max+1)).max()
                    w_m1 = (samples.single_mode_proportional_to_time(s=s, ell=ell, m=m)
                            .transform(supertranslation=supertranslation))
                    w_m2 = samples.single_mode_proportional_to_time_supertranslated(s=s, ell=ell, m=m,
                                                                                    supertranslation=supertranslation)
                    i1A = np.argmin(abs(w_m1.t-(w_m1.t[0]+2*max_displacement)))
                    i1B = np.argmin(abs(w_m1.t-(w_m1.t[-1]-2*max_displacement)))
                    i2A = np.argmin(abs(w_m2.t-w_m1.t[i1A]))
                    i2B = np.argmin(abs(w_m2.t-w_m1.t[i1B]))
                    assert np.allclose(w_m1.t[i1A:i1B+1], w_m2.t[i2A:i2B+1], rtol=0.0, atol=1e-16), \
                        (w_m1.t[i1A], w_m2.t[i2A], w_m1.t[i1B], w_m2.t[i2B],
                         w_m1.t[i1A:i1B+1].shape, w_m2.t[i2A:i2B+1].shape)
                    data1 = w_m1.data[i1A:i1B+1]
                    data2 = w_m2.data[i2A:i2B+1]
                    assert np.allclose(data1, data2, rtol=0.0, atol=5e-14), \
                        ([s, ell, m],
                         supertranslation,
                         [abs(data1-data2).max(),
                          data1.ravel()[np.argmax(abs(data1-data2))], data2.ravel()[np.argmax(abs(data1-data2))]],
                         [np.unravel_index(np.argmax(abs(data1-data2)), data1.shape)[0],
                          list(sf.LM_range(abs(s), ell_max)[np.unravel_index(np.argmax(abs(data1-data2)),
                                                                             data1.shape)[1]])])
Exemple #14
0
def delta_waveform(ell, m, begin=-10., end=100., n_times=1000, ell_min=2, ell_max=8):
    """WaveformModes with 1 in selected slot and 0 elsewhere"""
    n_modes = (ell_max * (ell_max + 2) - ell_min ** 2 + 1)
    t = np.linspace(begin, end, num=n_times)
    data = np.zeros((n_times, n_modes), dtype=complex)
    data[:, sf.LM_index(ell, m, ell_min)] = 1.0 + 0.0j
    W = scri.WaveformModes(t=t, data=data,  # frame=frame,
                           ell_min=ell_min, ell_max=ell_max,
                           history=['# Called from delta_waveform'],
                           frameType=scri.Inertial, dataType=scri.psi4,
                           r_is_scaled_out=False, m_is_scaled_out=True)
    return W
Exemple #15
0
def read_from_h5(file_name, **kwargs):
    """Read data from an H5 file in LVC format"""
    import re
    import h5py
    from scipy.interpolate import InterpolatedUnivariateSpline as Spline

    phase_re = re.compile("phase_l(?P<ell>.*)_m(?P<m>.*)")
    amp_re = re.compile("amp_l(?P<ell>.*)_m(?P<m>.*)")

    with h5py.File(file_name, "r") as f:
        t = f["NRtimes"][:]
        ell_m = np.array([[int(match["ell"]),
                           int(match["m"])] for key in f
                          for match in [phase_re.match(key)] if match])
        ell_min = np.min(ell_m[:, 0])
        ell_max = np.max(ell_m[:, 0])
        data = np.empty((t.size, sf.LM_total_size(ell_min, ell_max)),
                        dtype=complex)
        for ell in range(ell_min, ell_max + 1):
            for m in range(-ell, ell + 1):
                amp = Spline(f[f"amp_l{ell}_m{m}/X"][:],
                             f[f"amp_l{ell}_m{m}/Y"][:],
                             k=int(f[f"amp_l{ell}_m{m}/deg"][()]))(t)
                phase = Spline(f[f"phase_l{ell}_m{m}/X"][:],
                               f[f"phase_l{ell}_m{m}/Y"][:],
                               k=int(f[f"phase_l{ell}_m{m}/deg"][()]))(t)
                data[:,
                     sf.LM_index(ell, m, ell_min)] = amp * np.exp(1j * phase)
        if "auxiliary-info" in f and "history.txt" in f["auxiliary-info"]:
            history = ("### " +
                       f["auxiliary-info/history.txt"][()].decode().replace(
                           "\n", "\n### ")).split("\n")
        else:
            history = [""]
        constructor_statement = f"scri.LVC.read_from_h5('{file_name}')"
        w = WaveformModes(
            t=t,
            data=data,
            ell_min=ell_min,
            ell_max=ell_max,
            frameType=Inertial,
            dataType=h,
            history=history,
            constructor_statement=constructor_statement,
            r_is_scaled_out=True,
            m_is_scaled_out=True,
        )

    return w
Exemple #16
0
    def index(self, ell, m):
        """Index of given (ell,m) mode in the data

        Parameters
        ----------
        ell: int
        m: int

        Returns
        -------
        idx: int
            Index such that self.LM[idx] is [ell, m]

        """
        return sf.LM_index(ell, m, self.ell_min)
Exemple #17
0
    def apply_eth(self, operations, eth_convention="NP"):
        """Apply spin raising/lowering operators to waveform mode data
        in a specified order.  This does not modify the original
        waveform object.

        Parameters
        ----------
        operations: list or str composed of +1, -1, '+', '-', 'ð', or 'ð̅'
            The order of eth (+1, '+', or 'ð') and
            ethbar (-1, '-', or 'ð̅') operations to perform, applied from right to
            left. Example, operations='--+' will perform on WaveformModes data f the operation
            ethbar(ethbar(eth(f))).  Note that the order of operations is right-to-left.
        eth_convention: either 'NP' or 'GHP' [default: 'NP']
            Choose between Newman-Penrose or Geroch-Held-Penrose convention

        Returns
        -------
        mode_data: array of complex
            Note that the returned data has the same shape as this object's `data` attribute, and
            the modes correspond to the same (ell, m) values.  In particular, if the spin weight
            changes, the output data will no longer satisfy the expectation that ell_min == abs(s).
        """

        s = self.spin_weight
        mode_data = self.data.copy()

        for ell in range(self.ell_min, self.ell_max + 1):
            ladder_factor = self.ladder_factor(operations,
                                               s,
                                               ell,
                                               eth_convention=eth_convention)
            lm_indices = [
                sf.LM_index(ell, m, self.ell_min)
                for m in range(-ell, ell + 1)
            ]
            mode_data[:, lm_indices] *= ladder_factor

        return mode_data
Exemple #18
0
def _process_transformation_kwargs(input_ell_max, **kwargs):
    original_kwargs = kwargs.copy()

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

    # Decide on the number of points to use in each direction.  A nontrivial supertranslation will
    # introduce power in higher modes, so for best accuracy, we need to account for that.  But we'll
    # make it a firm requirement to have enough points to capture the original waveform, at least
    output_ell_max = kwargs.pop("output_ell_max", input_ell_max)
    working_ell_max = kwargs.pop("working_ell_max",
                                 2 * input_ell_max + ell_max_supertranslation)
    if working_ell_max < input_ell_max:
        raise ValueError(
            f"working_ell_max={working_ell_max} is too small; it must be at least ell_max={input_ell_max}"
        )

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

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

    return frame_rotation, boost_velocity, supertranslation, working_ell_max, output_ell_max
def test_first_nontrivial_multiplication(multiplication_function):
    """Test f*g where f is an ell=1, s=0 function

    We can write out the expression for f*g in the case where f is a pure ell=1 function, so we can
    check the multiplication function against that formula.  As in `test_trivial_multiplication`, we
    test with multiple values of ellmax_f, and construct `g` from random values.

    """
    def specialized_multiplication(f, ellmin_f, ellmax_f, s_f, g, ellmin_g,
                                   ellmax_g, s_g):
        ellmin_fg = 0
        ellmax_fg = ellmax_f + ellmax_g
        s_fg = s_f + s_g
        fg = np.zeros(sf.LM_total_size(0, ellmax_fg), dtype=np.complex)
        # for ell in range(ellmin_g, ellmax_g+1):
        #     for m in range(-ell, ell+1):
        #         i_g = sf.LM_index(ell, m, ellmin_g)
        #         for ellprime in [ell-1, ell, ell+1]:
        #             if ellprime < ellmin_g or ellprime > ellmax_g or ellprime < abs(s_g):
        #                 continue
        #             for mprime in [m-1, m, m+1]:
        #                 if mprime < -ellprime or mprime > ellprime:
        #                     continue
        #                 i_fg = sf.LM_index(ellprime, mprime, ellmin_fg)
        #                 m1 = mprime - m
        #                 fg[i_fg] += (
        #                     # (-1)**(s_g+m+1)
        #                     # * np.sqrt(3*(2*ell+1)/(4*np.pi))
        #                     # * (-1)**(m1)
        #                     # * np.sqrt(2*ellprime+1)
        #                     (-1)**(1 + ell + ellprime + s_g + m + m1)
        #                     * np.sqrt(3*(2*ell+1)*(2*ellprime+1)/(4*np.pi))
        #                     * sf.Wigner3j(1, ell, ellprime, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ellprime, m1, m, -mprime)
        #                     * f[sf.LM_index(1, m1, 0)]
        #                     * g[i_g]
        #                 )
        ####################################################################################
        # for ell in range(ellmin_g, ellmax_g+1):
        #     for m in range(-ell, ell+1):
        #         i_g = sf.LM_index(ell, m, ellmin_g)
        #         coefficient = (-1)**(s_g + m) * np.sqrt(3*(2*ell+1)/(4*np.pi))
        #         for ellprime in [ell-1, ell, ell+1]:
        #             if ellprime < ellmin_g or ellprime > ellmax_g or ellprime < abs(s_g):
        #                 continue
        #             if m-1 >= -ellprime and m-1 <= ellprime:
        #                 i_fg = sf.LM_index(ellprime, m-1, ellmin_fg)
        #                 fg[i_fg] += (
        #                     coefficient
        #                     * (-1)**(ell + ellprime)
        #                     * np.sqrt((2*ellprime+1))
        #                     * sf.Wigner3j(1, ell, ellprime, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ellprime, -1, m, -m+1)
        #                     * f[sf.LM_index(1, -1, 0)]
        #                     * g[i_g]
        #                 )
        #             if m >= -ellprime and m <= ellprime:
        #                 i_fg = sf.LM_index(ellprime, m, ellmin_fg)
        #                 fg[i_fg] += (
        #                     coefficient
        #                     * (-1)**(ell + ellprime)
        #                     * -np.sqrt(2*ellprime+1)
        #                     * sf.Wigner3j(1, ell, ellprime, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ellprime, 0, m, -m)
        #                     * f[sf.LM_index(1, 0, 0)]
        #                     * g[i_g]
        #                 )
        #             if m+1 >= -ellprime and m+1 <= ellprime:
        #                 i_fg = sf.LM_index(ellprime, m+1, ellmin_fg)
        #                 fg[i_fg] += (
        #                     coefficient
        #                     * (-1)**(ell + ellprime)
        #                     * np.sqrt(2*ellprime+1)
        #                     * sf.Wigner3j(1, ell, ellprime, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ellprime, 1, m, -m-1)
        #                     * f[sf.LM_index(1, 1, 0)]
        #                     * g[i_g]
        #                 )
        ####################################################################################
        # for ell in range(ellmin_g, ellmax_g+1):
        #     for m in range(-ell, ell+1):
        #         i_g = sf.LM_index(ell, m, ellmin_g)
        #         coefficient = (-1)**(s_g + m + 1) * np.sqrt(3*(2*ell+1)/(4*np.pi))
        #         if ell-1 >= ellmin_g and ell-1 <= ellmax_g and ell-1 >= abs(s_g):
        #             if m-1 >= -ell+1 and m-1 <= ell-1:
        #                 i_fg = sf.LM_index(ell-1, m-1, ellmin_fg)
        #                 fg[i_fg] += (
        #                     coefficient
        #                     * np.sqrt(2*ell-1)
        #                     * sf.Wigner3j(1, ell, ell-1, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ell-1, -1, m, -m+1)
        #                     * f[sf.LM_index(1, -1, 0)]
        #                     * g[i_g]
        #                 )
        #             if m >= -ell+1 and m <= ell-1:
        #                 i_fg = sf.LM_index(ell-1, m, ellmin_fg)
        #                 fg[i_fg] += (
        #                     -coefficient
        #                     * np.sqrt(2*ell-1)
        #                     * sf.Wigner3j(1, ell, ell-1, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ell-1, 0, m, -m)
        #                     * f[sf.LM_index(1, 0, 0)]
        #                     * g[i_g]
        #                 )
        #             if m+1 >= -ell+1 and m+1 <= ell-1:
        #                 i_fg = sf.LM_index(ell-1, m+1, ellmin_fg)
        #                 fg[i_fg] += (
        #                     coefficient
        #                     * np.sqrt(2*ell-1)
        #                     * sf.Wigner3j(1, ell, ell-1, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ell-1, 1, m, -m-1)
        #                     * f[sf.LM_index(1, 1, 0)]
        #                     * g[i_g]
        #                 )
        #         if ell >= ellmin_g and ell <= ellmax_g and ell >= abs(s_g):
        #             if m-1 >= -ell and m-1 <= ell:
        #                 i_fg = sf.LM_index(ell, m-1, ellmin_fg)
        #                 fg[i_fg] += (
        #                     -coefficient
        #                     * np.sqrt(2*ell+1)
        #                     * sf.Wigner3j(1, ell, ell, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ell, -1, m, -m+1)
        #                     * f[sf.LM_index(1, -1, 0)]
        #                     * g[i_g]
        #                 )
        #             if m >= -ell and m <= ell:
        #                 i_fg = sf.LM_index(ell, m, ellmin_fg)
        #                 fg[i_fg] += (
        #                     coefficient
        #                     * np.sqrt(2*ell+1)
        #                     * sf.Wigner3j(1, ell, ell, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ell, 0, m, -m)
        #                     * f[sf.LM_index(1, 0, 0)]
        #                     * g[i_g]
        #                 )
        #             if m+1 >= -ell and m+1 <= ell:
        #                 i_fg = sf.LM_index(ell, m+1, ellmin_fg)
        #                 fg[i_fg] += (
        #                     -coefficient
        #                     * np.sqrt(2*ell+1)
        #                     * sf.Wigner3j(1, ell, ell, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ell, 1, m, -m-1)
        #                     * f[sf.LM_index(1, 1, 0)]
        #                     * g[i_g]
        #                 )
        #         if ell+1 >= ellmin_g and ell+1 <= ellmax_g and ell+1 >= abs(s_g):
        #             if m-1 >= -ell-1 and m-1 <= ell+1:
        #                 i_fg = sf.LM_index(ell+1, m-1, ellmin_fg)
        #                 fg[i_fg] += (
        #                     coefficient
        #                     * np.sqrt(2*ell+3)
        #                     * sf.Wigner3j(1, ell, ell+1, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ell+1, -1, m, -m+1)
        #                     * f[sf.LM_index(1, -1, 0)]
        #                     * g[i_g]
        #                 )
        #             if m >= -ell-1 and m <= ell+1:
        #                 i_fg = sf.LM_index(ell+1, m, ellmin_fg)
        #                 fg[i_fg] += (
        #                     -coefficient
        #                     * np.sqrt(2*ell+3)
        #                     * sf.Wigner3j(1, ell, ell+1, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ell+1, 0, m, -m)
        #                     * f[sf.LM_index(1, 0, 0)]
        #                     * g[i_g]
        #                 )
        #             if m+1 >= -ell-1 and m+1 <= ell+1:
        #                 i_fg = sf.LM_index(ell+1, m+1, ellmin_fg)
        #                 fg[i_fg] += (
        #                     coefficient
        #                     * np.sqrt(2*ell+3)
        #                     * sf.Wigner3j(1, ell, ell+1, 0, s_g, -s_g)
        #                     * sf.Wigner3j(1, ell, ell+1, 1, m, -m-1)
        #                     * f[sf.LM_index(1, 1, 0)]
        #                     * g[i_g]
        #                 )
        ####################################################################################
        for ell in range(ellmin_g, ellmax_g + 1):
            for m in range(-ell, ell + 1):
                i_fg = sf.LM_index(ell, m, ellmin_fg)
                coefficient = (-1)**(s_g + m) * np.sqrt(3 * (2 * ell + 1) /
                                                        (4 * np.pi))
                if ell + 1 >= ellmin_g and ell + 1 <= ellmax_g and ell + 1 >= abs(
                        s_g):
                    if m + 1 >= -ell - 1 and m + 1 <= ell + 1:
                        fg[i_fg] += (
                            coefficient * np.sqrt(2 * ell + 3) *
                            sf.Wigner3j(1, ell + 1, ell, 0, s_g, -s_g) *
                            sf.Wigner3j(1, ell + 1, ell, -1, m + 1, -m) *
                            f[sf.LM_index(1, -1, 0)] *
                            g[sf.LM_index(ell + 1, m + 1, ellmin_g)])
                    if m >= -ell - 1 and m <= ell + 1:
                        fg[i_fg] += (
                            coefficient * np.sqrt(2 * ell + 3) *
                            sf.Wigner3j(1, ell + 1, ell, 0, s_g, -s_g) *
                            sf.Wigner3j(1, ell + 1, ell, 0, m, -m) *
                            f[sf.LM_index(1, 0, 0)] *
                            g[sf.LM_index(ell + 1, m, ellmin_g)])
                    if m - 1 >= -ell - 1 and m - 1 <= ell + 1:
                        fg[i_fg] += (
                            coefficient * np.sqrt(2 * ell + 3) *
                            sf.Wigner3j(1, ell + 1, ell, 0, s_g, -s_g) *
                            sf.Wigner3j(1, ell + 1, ell, 1, m - 1, -m) *
                            f[sf.LM_index(1, 1, 0)] *
                            g[sf.LM_index(ell + 1, m - 1, ellmin_g)])
                if ell >= ellmin_g and ell <= ellmax_g and ell >= abs(s_g):
                    if m + 1 >= -ell and m + 1 <= ell:
                        fg[i_fg] += (-coefficient * np.sqrt(2 * ell + 1) *
                                     sf.Wigner3j(1, ell, ell, 0, s_g, -s_g) *
                                     sf.Wigner3j(1, ell, ell, -1, m + 1, -m) *
                                     f[sf.LM_index(1, -1, 0)] *
                                     g[sf.LM_index(ell, m + 1, ellmin_g)])
                    if m >= -ell and m <= ell:
                        fg[i_fg] += (-coefficient * np.sqrt(2 * ell + 1) *
                                     sf.Wigner3j(1, ell, ell, 0, s_g, -s_g) *
                                     sf.Wigner3j(1, ell, ell, 0, m, -m) *
                                     f[sf.LM_index(1, 0, 0)] *
                                     g[sf.LM_index(ell, m, ellmin_g)])
                    if m - 1 >= -ell and m - 1 <= ell:
                        fg[i_fg] += (-coefficient * np.sqrt(2 * ell + 1) *
                                     sf.Wigner3j(1, ell, ell, 0, s_g, -s_g) *
                                     sf.Wigner3j(1, ell, ell, 1, m - 1, -m) *
                                     f[sf.LM_index(1, 1, 0)] *
                                     g[sf.LM_index(ell, m - 1, ellmin_g)])
                if ell - 1 >= ellmin_g and ell - 1 <= ellmax_g and ell - 1 >= abs(
                        s_g):
                    if m + 1 >= -ell + 1 and m + 1 <= ell - 1:
                        fg[i_fg] += (
                            coefficient * np.sqrt(2 * ell - 1) *
                            sf.Wigner3j(1, ell - 1, ell, 0, s_g, -s_g) *
                            sf.Wigner3j(1, ell - 1, ell, -1, m + 1, -m) *
                            f[sf.LM_index(1, -1, 0)] *
                            g[sf.LM_index(ell - 1, m + 1, ellmin_g)])
                    if m >= -ell + 1 and m <= ell - 1:
                        fg[i_fg] += (
                            coefficient * np.sqrt(2 * ell - 1) *
                            sf.Wigner3j(1, ell - 1, ell, 0, s_g, -s_g) *
                            sf.Wigner3j(1, ell - 1, ell, 0, m, -m) *
                            f[sf.LM_index(1, 0, 0)] *
                            g[sf.LM_index(ell - 1, m, ellmin_g)])
                    if m - 1 >= -ell + 1 and m - 1 <= ell - 1:
                        fg[i_fg] += (
                            coefficient * np.sqrt(2 * ell - 1) *
                            sf.Wigner3j(1, ell - 1, ell, 0, s_g, -s_g) *
                            sf.Wigner3j(1, ell - 1, ell, 1, m - 1, -m) *
                            f[sf.LM_index(1, 1, 0)] *
                            g[sf.LM_index(ell - 1, m - 1, ellmin_g)])
        return fg, ellmin_fg, ellmax_fg, s_fg

    np.random.seed(1234)
    atol = 2e-15
    rtol = 2e-15
    ellmax_g = 8
    print()
    for ellmax_f in range(1, ellmax_g + 1):
        # print(ellmax_f)
        f = np.zeros(sf.LM_total_size(0, ellmax_f), dtype=np.complex)
        f[1:4] = np.random.rand(3) + 1j * np.random.rand(3)
        ellmin_f = 0
        s_f = 0
        ellmin_g = 0
        for s_g in range(1 - ellmax_g, ellmax_g):
            # print('\t', s_g)
            i_max = sf.LM_index(ellmax_g, ellmax_g, ellmin_g)
            g = np.random.rand(sf.LM_total_size(
                0,
                ellmax_g)) + 1j * np.random.rand(sf.LM_total_size(0, ellmax_g))
            g[:sf.LM_total_size(0, abs(s_g) - 1) + 1] = 0.0
            fg, ellmin_fg, ellmax_fg, s_fg = multiplication_function(
                f, ellmin_f, ellmax_f, s_f, g, ellmin_g, ellmax_g, s_g)
            fg2, ellmin_fg2, ellmax_fg2, s_fg2 = specialized_multiplication(
                f, ellmin_f, ellmax_f, s_f, g, ellmin_g, ellmax_g, s_g)
            assert s_fg == s_f + s_g
            assert ellmin_fg == 0
            assert ellmax_fg == ellmax_f + ellmax_g
            import pprint
            assert np.allclose(
                fg[:i_max + 1], fg2[:i_max + 1], atol=atol,
                rtol=rtol), pprint.pformat(list(fg)) + '\n\n' + pprint.pformat(
                    list(fg2))
            gf, ellmin_gf, ellmax_gf, s_gf = multiplication_function(
                g, ellmin_g, ellmax_g, s_g, f, ellmin_f, ellmax_f, s_f)
            assert s_gf == s_g + s_f
            assert ellmin_gf == 0
            assert ellmax_gf == ellmax_g + ellmax_f
            assert np.allclose(gf[:i_max + 1],
                               fg2[:i_max + 1],
                               atol=atol,
                               rtol=rtol)
Exemple #20
0
def create_fake_finite_radius_strain_h5file(
    output_file_path="./rh_FiniteRadii_CodeUnits.h5",
    n_subleading=3,
    amp=1.0,
    t_0=0.0,
    t_1=3000.0,
    dt=0.1,
    r_min=100.0,
    r_max=600.0,
    n_radii=24,
    ell_max=8,
    initial_adm_energy=0.99,
    avg_lapse=0.99,
    avg_areal_radius_diff=1.0,
    mass_ratio=1.0,
    precession_opening_angle=0.0,
    **kwargs,
):
    """
    Create an HDF5 file with fake finite-radius GW strain data in the NRAR format, as
    used by the Spectral Einstein Code (SpEC). The asymptotic waveform is created by
    the function scri.sample_waveforms.fake_precessing_waveform and then radius-dependent
    terms are added to it. These subleading artificial "near-zone" effects are simple
    sinusoids. The finite-radius waveform, scaled by a factor of the radius, is then,

        r*h(r) = h_0 + h_1*r**-1 + h_2*r**-2 + ... + h_n*r**-n

    where h_0 is the waveform from scri.sample_waveforms.fake_precessing_waveform() and n is
    chosen by the user.

    Finally, the finite-radius waveforms as output by SpEC uses simulation coordinates for
    radius and time, which do not perfectly parametrize outgoing null rays. This function
    approximates these simulation coordinates so that the data in the resulting HDF5 file
    most nearly mimics that of a SpEC HDF5 output file.

    Parameters
    ==========
    output_file_path: str
        The name and path of the output file. The filename must be "rh_FiniteRadius_CodeUnits.h5"
        if you wish to be able to run scri.extrapolation.extrapolate on it.
    n_subleading: int [defaults to 3]
        The number of subleading, radius-dependent terms to add to the asymptotic waveform.
    amp: float [defaults to 1.0]
        The amplitude of the subleading, radius-dependent terms.
    t_0: float [defaults to 0.0]
    t_1: float [defaults to 3000.0]
        The initial and final times in the output waveform. Note that the merger is placed
        100.0 time units before `t_1`, and several transitions are made before this, so `t_0`
        must be that far ahead of `t_1`.
    dt: float [defaults to 0.1]
        Spacing of output time series.
    r_min: float [defaults to 100.0]
    r_max: float [defaults to 600.0]
    n_radii: float [defaults to 24]
        The minimum and maximum radius, and the number of radii between these values, at which
        to produce a finite-radius waveform. These will be equally spaced in inverse radius.
    ell_max: int [defaults to 8]
        Largest ell value in the output modes.
    initial_adm_energy: float [defaults to 0.99]
        The intial ADM energy as would be computed by the SpEC intitial data solver.
    avg_lapse: float [defaults to 0.99]
        The value of the lapse averaged over a sphere at
    avg_areal_radius_diff: float [defaults to 1.0]
        How much on average the areal radius is larger than the coord radius, may be negative.
    mass_ratio: float [defaults to 1.0]
        Ratio of BH masses to use as input to rough approximations for orbital evolution
        and mode amplitudes.
    precession_opening_angle: float [defaults to 0.0]
        Opening angle of the precession cone. The default results in no precession. If
        this value is non-zero, then the following options from fake_precessing_waveform
        may be supplied as kwagrs:
            * precession_opening_angle_dot
            * precession_relative_rate
            * precession_nutation_angle
        See the help text of fake_precessing_waveform for documentation.

    """
    import h5py
    from scipy.interpolate import CubicSpline

    with h5py.File(output_file_path, "x") as h5file:

        # Set up an H5 group for each radius
        coord_radii = (1 /
                       np.linspace(1 / r_min, 1 / r_max, n_radii)).astype(int)
        groups = [f"R{R:04}.dir" for R in coord_radii]
        for group in groups:
            h5file.create_group(group)

        # Create version history dataset
        # We mimic how SpEC encodes the strings in VersionHist.ver to ensure we
        # don't hit any errors due to the encoding.
        version_hist = [[
            "ef51849550a1d8a5bbdd810c7debf0dd839e86dd",
            "Overall sign change in complex strain h."
        ]]
        reencoded_version_hist = [[
            entry[0].encode("ascii", "ignore"),
            entry[1].encode("ascii", "ignore")
        ] for entry in version_hist]
        dset = h5file.create_dataset(
            "VersionHist.ver",
            (len(version_hist), 2),
            maxshape=(None, 2),
            data=reencoded_version_hist,
            dtype=h5py.special_dtype(vlen=bytes),
        )
        dset.attrs.create("Version",
                          len(version_hist),
                          shape=(1, ),
                          dtype=np.uint64)

        # Generate the asymptotic waveform
        h0 = scri.sample_waveforms.fake_precessing_waveform(
            t_0=t_0,
            t_1=t_1,
            dt=dt,
            ell_max=ell_max,
            precession_opening_angle=precession_opening_angle,
            **kwargs)
        n_times = int(h0.t.shape[0] + r_max / dt)
        t_1 += r_max
        all_times = np.linspace(0, t_1, n_times)

        # Set auxiliary datasetsss
        for i in range(len(groups)):
            # Set coordinate radius dataset
            coord_radius = np.vstack((all_times, [coord_radii[i]] * n_times)).T
            dset = h5file.create_dataset(f"{groups[i]}/CoordRadius.dat",
                                         data=coord_radius)
            dset.attrs.create("Legend", ["time", "CoordRadius"])

            # Set areal radius dataset
            areal_radius = coord_radius
            areal_radius[:, 1] += avg_areal_radius_diff
            dset = h5file.create_dataset(f"{groups[i]}/ArealRadius.dat",
                                         data=areal_radius)
            dset.attrs.create("Legend", ["time", "ArealRadius"])

            # Set initial ADM energy dataset
            seg_start_times = h0.t[::int(n_times / 20)]
            adm_energy_dset = np.vstack(
                (seg_start_times,
                 [initial_adm_energy] * len(seg_start_times))).T
            dset = h5file.create_dataset(f"{groups[i]}/InitialAdmEnergy.dat",
                                         data=adm_energy_dset)
            dset.attrs.create("Legend", ["time", "InitialAdmEnergy"])

            # Set average lapse dataset
            avg_lapse_dset = np.vstack((all_times, [avg_lapse] * n_times)).T
            dset = h5file.create_dataset(f"{groups[i]}/AverageLapse.dat",
                                         data=avg_lapse_dset)
            dset.attrs.create("Legend", ["time", "AverageLapse"])

            # Set finite radius data
            R = areal_radius[0, 1]
            tortoise_coord = R + 2 * initial_adm_energy * np.log(
                R / (2 * initial_adm_energy) - 1)
            # Compute the approximate time in SpEC simulation coordinates
            simulation_time = (h0.t + tortoise_coord) * np.sqrt(
                1 - 2 * initial_adm_energy / R) / avg_lapse
            for l in range(2, ell_max + 1):
                for m in range(-l, l + 1):
                    index = sf.LM_index(l, m, 2)
                    new_data = h0.data[:, index]
                    for n in range(1, n_subleading + 1):
                        new_data += amp * R**-n * np.exp(
                            (1j * n * 50 * np.pi / h0.n_times) *
                            h0.t) * h0.abs[:, index]
                    new_data = CubicSpline(simulation_time,
                                           new_data)(all_times)
                    new_data[(all_times < simulation_time[0]) |
                             (all_times > simulation_time[-1])] = 0.0
                    new_data += (1 + 1j) * 1e-14 * all_times
                    new_dset = np.vstack(
                        (all_times, new_data.real, new_data.imag)).T
                    dset = h5file.create_dataset(
                        f"{groups[i]}/Y_l{l}_m{m}.dat", data=new_dset)
                    dset.attrs.create(
                        "Legend",
                        [
                            "time",
                            f"Re[rh]_l{l}_m{m}(R={coord_radii[i]}.00)",
                            f"Im[rh]_l{l}_m{m}(R={coord_radii[i]}.00)",
                        ],
                    )
Exemple #21
0
def single_mode_proportional_to_time_supertranslated(**kwargs):
    """Return WaveformModes as in single_mode_proportional_to_time, with analytical supertranslation

    This function constructs the same basic object as the `single_mode_proportional_to_time`, but then applies an
    analytical supertranslation.  The arguments to this function are the same as to the other, with two additions:

    Additional parameters
    ---------------------
    supertranslation : complex array, optional
        Spherical-harmonic modes of the supertranslation to apply to the waveform.  This is overwritten by
         `space_translation` if present.  Default value is `None`.
    space_translation : float array of length 3, optional
        This is just the 3-vector representing the displacement to apply to the waveform.  Note that if
        `supertranslation`, this parameter overwrites it.  Default value is [1.0, 0.0, 0.0].

    """
    s = kwargs.pop("s", -2)
    ell = kwargs.pop("ell", abs(s))
    m = kwargs.pop("m", -ell)
    ell_min = kwargs.pop("ell_min", abs(s))
    ell_max = kwargs.pop("ell_max", 8)
    data_type = kwargs.pop("data_type",
                           scri.DataType[scri.SpinWeights.index(s)])
    t_0 = kwargs.pop("t_0", -20.0)
    t_1 = kwargs.pop("t_1", 20.0)
    dt = kwargs.pop("dt", 1.0 / 10.0)
    t = np.arange(t_0, t_1 + dt, dt)
    n_times = t.size
    beta = kwargs.pop("beta", 1.0)
    data = np.zeros((n_times, sf.LM_total_size(ell_min, ell_max)),
                    dtype=complex)
    data[:, sf.LM_index(ell, m, ell_min)] = beta * t
    supertranslation = np.array(kwargs.pop("supertranslation",
                                           np.array([], dtype=complex)),
                                dtype=complex)
    if "space_translation" in kwargs:
        if supertranslation.size < 4:
            supertranslation.resize((4, ))
        supertranslation[1:4] = -sf.vector_as_ell_1_modes(
            kwargs.pop("space_translation"))
    supertranslation_ell_max = int(math.sqrt(supertranslation.size) - 1)
    if supertranslation_ell_max * (supertranslation_ell_max +
                                   2) + 1 != supertranslation.size:
        raise ValueError(
            f"Bad number of elements in supertranslation: {supertranslation.size}"
        )
    for i, (ellpp, mpp) in enumerate(sf.LM_range(0, supertranslation_ell_max)):
        if supertranslation[i] != 0.0:
            mp = m + mpp
            for ellp in range(ell_min, min(ell_max, (ell + ellpp)) + 1):
                if ellp >= abs(mp):
                    addition = (beta * supertranslation[i] * math.sqrt(
                        ((2 * ellpp + 1) * (2 * ell + 1) *
                         (2 * ellp + 1)) / (4 * math.pi)) *
                                sf.Wigner3j(ellpp, ell, ellp, 0, -s, s) *
                                sf.Wigner3j(ellpp, ell, ellp, mpp, m, -mp))
                    if (s + mp) % 2 == 1:
                        addition *= -1
                    data[:, sf.LM_index(ellp, mp, ell_min)] += addition

    if kwargs:
        import pprint

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

    return scri.WaveformModes(
        t=t,
        data=data,
        ell_min=ell_min,
        ell_max=ell_max,
        frameType=scri.Inertial,
        dataType=data_type,
        r_is_scaled_out=True,
        m_is_scaled_out=True,
    )
 def specialized_multiplication(f, ellmin_f, ellmax_f, s_f, g, ellmin_g,
                                ellmax_g, s_g):
     ellmin_fg = 0
     ellmax_fg = ellmax_f + ellmax_g
     s_fg = s_f + s_g
     fg = np.zeros(sf.LM_total_size(0, ellmax_fg), dtype=np.complex)
     # for ell in range(ellmin_g, ellmax_g+1):
     #     for m in range(-ell, ell+1):
     #         i_g = sf.LM_index(ell, m, ellmin_g)
     #         for ellprime in [ell-1, ell, ell+1]:
     #             if ellprime < ellmin_g or ellprime > ellmax_g or ellprime < abs(s_g):
     #                 continue
     #             for mprime in [m-1, m, m+1]:
     #                 if mprime < -ellprime or mprime > ellprime:
     #                     continue
     #                 i_fg = sf.LM_index(ellprime, mprime, ellmin_fg)
     #                 m1 = mprime - m
     #                 fg[i_fg] += (
     #                     # (-1)**(s_g+m+1)
     #                     # * np.sqrt(3*(2*ell+1)/(4*np.pi))
     #                     # * (-1)**(m1)
     #                     # * np.sqrt(2*ellprime+1)
     #                     (-1)**(1 + ell + ellprime + s_g + m + m1)
     #                     * np.sqrt(3*(2*ell+1)*(2*ellprime+1)/(4*np.pi))
     #                     * sf.Wigner3j(1, ell, ellprime, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ellprime, m1, m, -mprime)
     #                     * f[sf.LM_index(1, m1, 0)]
     #                     * g[i_g]
     #                 )
     ####################################################################################
     # for ell in range(ellmin_g, ellmax_g+1):
     #     for m in range(-ell, ell+1):
     #         i_g = sf.LM_index(ell, m, ellmin_g)
     #         coefficient = (-1)**(s_g + m) * np.sqrt(3*(2*ell+1)/(4*np.pi))
     #         for ellprime in [ell-1, ell, ell+1]:
     #             if ellprime < ellmin_g or ellprime > ellmax_g or ellprime < abs(s_g):
     #                 continue
     #             if m-1 >= -ellprime and m-1 <= ellprime:
     #                 i_fg = sf.LM_index(ellprime, m-1, ellmin_fg)
     #                 fg[i_fg] += (
     #                     coefficient
     #                     * (-1)**(ell + ellprime)
     #                     * np.sqrt((2*ellprime+1))
     #                     * sf.Wigner3j(1, ell, ellprime, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ellprime, -1, m, -m+1)
     #                     * f[sf.LM_index(1, -1, 0)]
     #                     * g[i_g]
     #                 )
     #             if m >= -ellprime and m <= ellprime:
     #                 i_fg = sf.LM_index(ellprime, m, ellmin_fg)
     #                 fg[i_fg] += (
     #                     coefficient
     #                     * (-1)**(ell + ellprime)
     #                     * -np.sqrt(2*ellprime+1)
     #                     * sf.Wigner3j(1, ell, ellprime, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ellprime, 0, m, -m)
     #                     * f[sf.LM_index(1, 0, 0)]
     #                     * g[i_g]
     #                 )
     #             if m+1 >= -ellprime and m+1 <= ellprime:
     #                 i_fg = sf.LM_index(ellprime, m+1, ellmin_fg)
     #                 fg[i_fg] += (
     #                     coefficient
     #                     * (-1)**(ell + ellprime)
     #                     * np.sqrt(2*ellprime+1)
     #                     * sf.Wigner3j(1, ell, ellprime, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ellprime, 1, m, -m-1)
     #                     * f[sf.LM_index(1, 1, 0)]
     #                     * g[i_g]
     #                 )
     ####################################################################################
     # for ell in range(ellmin_g, ellmax_g+1):
     #     for m in range(-ell, ell+1):
     #         i_g = sf.LM_index(ell, m, ellmin_g)
     #         coefficient = (-1)**(s_g + m + 1) * np.sqrt(3*(2*ell+1)/(4*np.pi))
     #         if ell-1 >= ellmin_g and ell-1 <= ellmax_g and ell-1 >= abs(s_g):
     #             if m-1 >= -ell+1 and m-1 <= ell-1:
     #                 i_fg = sf.LM_index(ell-1, m-1, ellmin_fg)
     #                 fg[i_fg] += (
     #                     coefficient
     #                     * np.sqrt(2*ell-1)
     #                     * sf.Wigner3j(1, ell, ell-1, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ell-1, -1, m, -m+1)
     #                     * f[sf.LM_index(1, -1, 0)]
     #                     * g[i_g]
     #                 )
     #             if m >= -ell+1 and m <= ell-1:
     #                 i_fg = sf.LM_index(ell-1, m, ellmin_fg)
     #                 fg[i_fg] += (
     #                     -coefficient
     #                     * np.sqrt(2*ell-1)
     #                     * sf.Wigner3j(1, ell, ell-1, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ell-1, 0, m, -m)
     #                     * f[sf.LM_index(1, 0, 0)]
     #                     * g[i_g]
     #                 )
     #             if m+1 >= -ell+1 and m+1 <= ell-1:
     #                 i_fg = sf.LM_index(ell-1, m+1, ellmin_fg)
     #                 fg[i_fg] += (
     #                     coefficient
     #                     * np.sqrt(2*ell-1)
     #                     * sf.Wigner3j(1, ell, ell-1, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ell-1, 1, m, -m-1)
     #                     * f[sf.LM_index(1, 1, 0)]
     #                     * g[i_g]
     #                 )
     #         if ell >= ellmin_g and ell <= ellmax_g and ell >= abs(s_g):
     #             if m-1 >= -ell and m-1 <= ell:
     #                 i_fg = sf.LM_index(ell, m-1, ellmin_fg)
     #                 fg[i_fg] += (
     #                     -coefficient
     #                     * np.sqrt(2*ell+1)
     #                     * sf.Wigner3j(1, ell, ell, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ell, -1, m, -m+1)
     #                     * f[sf.LM_index(1, -1, 0)]
     #                     * g[i_g]
     #                 )
     #             if m >= -ell and m <= ell:
     #                 i_fg = sf.LM_index(ell, m, ellmin_fg)
     #                 fg[i_fg] += (
     #                     coefficient
     #                     * np.sqrt(2*ell+1)
     #                     * sf.Wigner3j(1, ell, ell, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ell, 0, m, -m)
     #                     * f[sf.LM_index(1, 0, 0)]
     #                     * g[i_g]
     #                 )
     #             if m+1 >= -ell and m+1 <= ell:
     #                 i_fg = sf.LM_index(ell, m+1, ellmin_fg)
     #                 fg[i_fg] += (
     #                     -coefficient
     #                     * np.sqrt(2*ell+1)
     #                     * sf.Wigner3j(1, ell, ell, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ell, 1, m, -m-1)
     #                     * f[sf.LM_index(1, 1, 0)]
     #                     * g[i_g]
     #                 )
     #         if ell+1 >= ellmin_g and ell+1 <= ellmax_g and ell+1 >= abs(s_g):
     #             if m-1 >= -ell-1 and m-1 <= ell+1:
     #                 i_fg = sf.LM_index(ell+1, m-1, ellmin_fg)
     #                 fg[i_fg] += (
     #                     coefficient
     #                     * np.sqrt(2*ell+3)
     #                     * sf.Wigner3j(1, ell, ell+1, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ell+1, -1, m, -m+1)
     #                     * f[sf.LM_index(1, -1, 0)]
     #                     * g[i_g]
     #                 )
     #             if m >= -ell-1 and m <= ell+1:
     #                 i_fg = sf.LM_index(ell+1, m, ellmin_fg)
     #                 fg[i_fg] += (
     #                     -coefficient
     #                     * np.sqrt(2*ell+3)
     #                     * sf.Wigner3j(1, ell, ell+1, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ell+1, 0, m, -m)
     #                     * f[sf.LM_index(1, 0, 0)]
     #                     * g[i_g]
     #                 )
     #             if m+1 >= -ell-1 and m+1 <= ell+1:
     #                 i_fg = sf.LM_index(ell+1, m+1, ellmin_fg)
     #                 fg[i_fg] += (
     #                     coefficient
     #                     * np.sqrt(2*ell+3)
     #                     * sf.Wigner3j(1, ell, ell+1, 0, s_g, -s_g)
     #                     * sf.Wigner3j(1, ell, ell+1, 1, m, -m-1)
     #                     * f[sf.LM_index(1, 1, 0)]
     #                     * g[i_g]
     #                 )
     ####################################################################################
     for ell in range(ellmin_g, ellmax_g + 1):
         for m in range(-ell, ell + 1):
             i_fg = sf.LM_index(ell, m, ellmin_fg)
             coefficient = (-1)**(s_g + m) * np.sqrt(3 * (2 * ell + 1) /
                                                     (4 * np.pi))
             if ell + 1 >= ellmin_g and ell + 1 <= ellmax_g and ell + 1 >= abs(
                     s_g):
                 if m + 1 >= -ell - 1 and m + 1 <= ell + 1:
                     fg[i_fg] += (
                         coefficient * np.sqrt(2 * ell + 3) *
                         sf.Wigner3j(1, ell + 1, ell, 0, s_g, -s_g) *
                         sf.Wigner3j(1, ell + 1, ell, -1, m + 1, -m) *
                         f[sf.LM_index(1, -1, 0)] *
                         g[sf.LM_index(ell + 1, m + 1, ellmin_g)])
                 if m >= -ell - 1 and m <= ell + 1:
                     fg[i_fg] += (
                         coefficient * np.sqrt(2 * ell + 3) *
                         sf.Wigner3j(1, ell + 1, ell, 0, s_g, -s_g) *
                         sf.Wigner3j(1, ell + 1, ell, 0, m, -m) *
                         f[sf.LM_index(1, 0, 0)] *
                         g[sf.LM_index(ell + 1, m, ellmin_g)])
                 if m - 1 >= -ell - 1 and m - 1 <= ell + 1:
                     fg[i_fg] += (
                         coefficient * np.sqrt(2 * ell + 3) *
                         sf.Wigner3j(1, ell + 1, ell, 0, s_g, -s_g) *
                         sf.Wigner3j(1, ell + 1, ell, 1, m - 1, -m) *
                         f[sf.LM_index(1, 1, 0)] *
                         g[sf.LM_index(ell + 1, m - 1, ellmin_g)])
             if ell >= ellmin_g and ell <= ellmax_g and ell >= abs(s_g):
                 if m + 1 >= -ell and m + 1 <= ell:
                     fg[i_fg] += (-coefficient * np.sqrt(2 * ell + 1) *
                                  sf.Wigner3j(1, ell, ell, 0, s_g, -s_g) *
                                  sf.Wigner3j(1, ell, ell, -1, m + 1, -m) *
                                  f[sf.LM_index(1, -1, 0)] *
                                  g[sf.LM_index(ell, m + 1, ellmin_g)])
                 if m >= -ell and m <= ell:
                     fg[i_fg] += (-coefficient * np.sqrt(2 * ell + 1) *
                                  sf.Wigner3j(1, ell, ell, 0, s_g, -s_g) *
                                  sf.Wigner3j(1, ell, ell, 0, m, -m) *
                                  f[sf.LM_index(1, 0, 0)] *
                                  g[sf.LM_index(ell, m, ellmin_g)])
                 if m - 1 >= -ell and m - 1 <= ell:
                     fg[i_fg] += (-coefficient * np.sqrt(2 * ell + 1) *
                                  sf.Wigner3j(1, ell, ell, 0, s_g, -s_g) *
                                  sf.Wigner3j(1, ell, ell, 1, m - 1, -m) *
                                  f[sf.LM_index(1, 1, 0)] *
                                  g[sf.LM_index(ell, m - 1, ellmin_g)])
             if ell - 1 >= ellmin_g and ell - 1 <= ellmax_g and ell - 1 >= abs(
                     s_g):
                 if m + 1 >= -ell + 1 and m + 1 <= ell - 1:
                     fg[i_fg] += (
                         coefficient * np.sqrt(2 * ell - 1) *
                         sf.Wigner3j(1, ell - 1, ell, 0, s_g, -s_g) *
                         sf.Wigner3j(1, ell - 1, ell, -1, m + 1, -m) *
                         f[sf.LM_index(1, -1, 0)] *
                         g[sf.LM_index(ell - 1, m + 1, ellmin_g)])
                 if m >= -ell + 1 and m <= ell - 1:
                     fg[i_fg] += (
                         coefficient * np.sqrt(2 * ell - 1) *
                         sf.Wigner3j(1, ell - 1, ell, 0, s_g, -s_g) *
                         sf.Wigner3j(1, ell - 1, ell, 0, m, -m) *
                         f[sf.LM_index(1, 0, 0)] *
                         g[sf.LM_index(ell - 1, m, ellmin_g)])
                 if m - 1 >= -ell + 1 and m - 1 <= ell - 1:
                     fg[i_fg] += (
                         coefficient * np.sqrt(2 * ell - 1) *
                         sf.Wigner3j(1, ell - 1, ell, 0, s_g, -s_g) *
                         sf.Wigner3j(1, ell - 1, ell, 1, m - 1, -m) *
                         f[sf.LM_index(1, 1, 0)] *
                         g[sf.LM_index(ell - 1, m - 1, ellmin_g)])
     return fg, ellmin_fg, ellmax_fg, s_fg
Exemple #23
0
 def data_functor(t, LM):
     data = np.empty((t.shape[0], LM.shape[0]), dtype=complex)
     data[:, sf.LM_index(ell, m, min(LM[:, 0]))] = 1.0 + 0.0j
     return data
Exemple #24
0
        transition_to_constant(phi / precession_relative_rate, t, t0, t1) *
        quaternion.z / 2)
    R_nutation = np.exp(precession_nutation_angle * transition * quaternion.x /
                        2)
    frame = (R_orbital * R_nutation * R_orbital.conjugate() * R_precession *
             R_opening * R_precession.conjugate() * R_orbital)
    frame = frame[0].sqrt().conjugate(
    ) * frame  # Just give the initial angle a weird little tweak to screw things up

    # Construct the modes
    x = omega**(2 / 3)
    modulation = transition_function(t, t[i0], t[i1], 1,
                                     0) * np.cos(phi) / 40.0
    for ell in range(ell_min, ell_max + 1):
        for m in range(-ell, ell + 1):
            data[:, sf.LM_index(ell, m, ell_min)] = pn_leading_order_amplitude(
                ell, m, x,
                mass_ratio=mass_ratio) * (1 + np.sign(m) * modulation)

    # Apply ringdown (mode amplitudes are constant after t_merger)
    data *= ringdown[:, np.newaxis]

    h_corot = scri.WaveformModes(
        t=t,
        frame=frame,
        data=data,
        ell_min=ell_min,
        ell_max=ell_max,
        frameType=scri.Corotating,
        dataType=data_type,
        r_is_scaled_out=True,
def test_SWSH_multiplication_formula(multiplication_function):
    """Test formula for multiplication of SWSHs

    Much of the analysis is based on the formula

    s1Yl1m1 * s2Yl2m2 = sum([
        s3Yl3m3 * (-1)**(l1+l2+l3+s3+m3) * sqrt((2*l1+1)*(2*l2+1)*(2*l3+1)/(4*pi))
            * Wigner3j(l1, l2, l3, s1, s2, -s3) * Wigner3j(l1, l2, l3, m1, m2, -m3)
        for s3 in [s1+s2]
        for l3 in range(abs(l1-l2), l1+l2+1)
        for m3 in [m1+m2]
    ])

    This test evaluates each side of this formula, and compares the values at all collocation points
    on the sphere.  This is tested for each possible value of (s1, l1, m1, s2, l2, m2) up to l1=4
    and l2=4 [the number of items to test scales very rapidly with ell], and tested again for each
    (0, 1, m1, s2, l2, m2) up to l2=8.

    """
    atol = 2e-15
    rtol = 2e-15
    ell_max = 4
    for ell1 in range(ell_max + 1):
        for s1 in range(-ell1, ell1 + 1):
            for m1 in range(-ell1, ell1 + 1):
                for ell2 in range(ell_max + 1):
                    for s2 in range(-ell2, ell2 + 1):
                        for m2 in range(-ell2, ell2 + 1):
                            swsh1 = np.zeros(sf.LM_total_size(0, ell_max),
                                             dtype=np.complex)
                            swsh1[sf.LM_index(ell1, m1, 0)] = 1.0
                            swsh2 = np.zeros(sf.LM_total_size(0, ell_max),
                                             dtype=np.complex)
                            swsh2[sf.LM_index(ell2, m2, 0)] = 1.0
                            swsh3, ell_min_3, ell_max_3, s3 = multiplication_function(
                                swsh1, 0, ell_max, s1, swsh2, 0, ell_max, s2)
                            assert s3 == s1 + s2
                            assert ell_min_3 == 0
                            assert ell_max_3 == 2 * ell_max
                            n_theta = 2 * ell_max_3 + 1
                            n_phi = n_theta
                            swsh3_map = spinsfast.salm2map(
                                swsh3, s3, ell_max_3, n_theta, n_phi)
                            swsh4_map = np.zeros_like(swsh3_map)
                            for ell4 in range(abs(ell1 - ell2),
                                              ell1 + ell2 + 1):
                                for s4 in [s1 + s2]:
                                    for m4 in [m1 + m2]:
                                        swsh4_k = np.zeros_like(swsh3)
                                        swsh4_k[sf.LM_index(ell4, m4, 0)] = (
                                            (-1)
                                            **(ell1 + ell2 + ell4 + s4 + m4) *
                                            np.sqrt(
                                                (2 * ell1 + 1) *
                                                (2 * ell2 + 1) *
                                                (2 * ell4 + 1) / (4 * np.pi)) *
                                            sf.Wigner3j(
                                                ell1, ell2, ell4, s1, s2, -s4)
                                            * sf.Wigner3j(
                                                ell1, ell2, ell4, m1, m2, -m4))
                                        swsh4_map[:] += spinsfast.salm2map(
                                            swsh4_k, s4, ell_max_3, n_theta,
                                            n_phi)
                            assert np.allclose(swsh3_map,
                                               swsh4_map,
                                               atol=atol,
                                               rtol=rtol)

    atol = 8e-15
    rtol = 8e-15
    ell_max = 8
    for ell1 in [1]:
        for s1 in [0]:
            for m1 in [-1, 0, 1]:
                for ell2 in range(ell_max + 1):
                    for s2 in range(-ell2, ell2 + 1):
                        for m2 in range(-ell2, ell2 + 1):
                            swsh1 = np.zeros(sf.LM_total_size(0, ell_max),
                                             dtype=np.complex)
                            swsh1[sf.LM_index(ell1, m1, 0)] = 1.0
                            swsh2 = np.zeros(sf.LM_total_size(0, ell_max),
                                             dtype=np.complex)
                            swsh2[sf.LM_index(ell2, m2, 0)] = 1.0
                            swsh3, ell_min_3, ell_max_3, s3 = multiplication_function(
                                swsh1, 0, ell_max, s1, swsh2, 0, ell_max, s2)
                            assert s3 == s1 + s2
                            assert ell_min_3 == 0
                            assert ell_max_3 == 2 * ell_max
                            n_theta = 2 * ell_max_3 + 1
                            n_phi = n_theta
                            swsh3_map = spinsfast.salm2map(
                                swsh3, s3, ell_max_3, n_theta, n_phi)
                            swsh4_map = np.zeros_like(swsh3_map)
                            for ell4 in [ell2 - 1, ell2, ell2 + 1]:
                                if ell4 < 0:
                                    continue
                                swsh4_k = np.zeros_like(swsh3)
                                # swsh4_k[sf.LM_index(ell4, m1+m2, 0)] = (
                                #     (-1)**(1+ell2+ell4+s2+m1+m2)
                                #     * np.sqrt(3*(2*ell2+1)*(2*ell4+1)/(4*np.pi))
                                #     * sf.Wigner3j(1, ell2, ell4, 0, s2, -s2)
                                #     * sf.Wigner3j(1, ell2, ell4, m1, m2, -m1-m2)
                                # )
                                # swsh4_map[:] += (
                                #     spinsfast.salm2map(swsh4_k, s2, ell_max_3, n_theta, n_phi)
                                # )
                                swsh4_k[sf.LM_index(ell4, m1 + m2, 0)] = (
                                    (-1)**(ell2 + ell4 + m1) * np.sqrt(
                                        (2 * ell4 + 1)) *
                                    sf.Wigner3j(1, ell2, ell4, 0, s2, -s2) *
                                    sf.Wigner3j(1, ell2, ell4, m1, m2,
                                                -m1 - m2))
                                swsh4_map[:] += (
                                    (-1)**(s2 + m2 + 1) *
                                    np.sqrt(3 * (2 * ell2 + 1) / (4 * np.pi)) *
                                    spinsfast.salm2map(swsh4_k, s2, ell_max_3,
                                                       n_theta, n_phi))
                            assert np.allclose(swsh3_map,
                                               swsh4_map,
                                               atol=atol,
                                               rtol=rtol), np.max(
                                                   np.abs(swsh3_map -
                                                          swsh4_map))
def test_ethbar_inverse_NP():
    for s in range(-5, 5 + 1):
        f = np.random.random(81) + 1j * np.random.random(81)
        f[:sf.LM_index(abs(s), -abs(s), 0)] = 0.0

        # Test ethbar_inverse_NP(ethbar_NP(x)) == x
        ethbarf = sf.ethbar_NP(f, s, 0)
        ethbarf_prime = sf.ethbar_inverse_NP(ethbarf, s - 1, 0)
        assert np.allclose(f[sf.LM_index(abs(s - 1), -abs(s - 1), 0):],
                           ethbarf_prime[sf.LM_index(abs(s -
                                                         1), -abs(s - 1), 0):],
                           atol=0,
                           rtol=1e-15)
        if sf.LM_index(abs(s), -abs(s), 0) < sf.LM_index(
                abs(s - 1), -abs(s - 1), 0):
            assert abs(
                ethbarf_prime[sf.LM_index(abs(s), -abs(s), 0):sf.
                              LM_index(abs(s -
                                           1), -abs(s - 1), 0)]).max() == 0.0

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

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

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

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

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

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

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

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

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

    else:

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

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

    return (
        supertranslation,
        ell_max_supertranslation,
        ell_max,
        n_theta,
        n_phi,
        boost_velocity,
        beta,
        gamma,
        varphi,
        R_j_k,
        Bprm_j_k,
        thetaprm_j_phiprm_k,
        kwargs,
    )
Exemple #29
0
 def modifier(sigma, sigmadot, sigmaddot, psi2, psi1, psi0):
     psi0[:sf.LM_index(1, 1, 0)] = 1.234
     psi1[0] = 0.123
     sigma[:sf.LM_index(1, 1, 0)] = 0.567
     sigmadot[:sf.LM_index(1, 1, 0)] = 0.678
     sigmaddot[:sf.LM_index(1, 1, 0)] = 0.789
Exemple #30
0
    def from_modes(cls, w_modes, **kwargs):
        """Construct grid object from modes, with optional BMS transformation

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

        It is important to note that the input transformation parameters are applied in the order listed in the
        parameter list below:
          1. (Super)Translations
          2. Rotation (about the origin of the translated system)
          3. Boost
        All input parameters refer to the transformation required to take the mode's inertial frame onto the inertial
        frame of the grid's inertial observers.  In what follows, the inertial frame of the modes will be unprimed,
        while the inertial frame of the grid will be primed.  NOTE: These are passive transformations, e.g. supplying
        the option space_translation=[0, 0, 5] to a Schwarzschild spacetime will move the coordinates to z'=z+5 and so
        the center of mass will be shifted in the negative z direction by 5 in the new coordinates.

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


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

        Returns
        -------
        WaveformGrid

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

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

        original_kwargs = kwargs.copy()

        (
            supertranslation,
            ell_max_supertranslation,
            ell_max,
            n_theta,
            n_phi,
            boost_velocity,
            beta,
            gamma,
            varphi,
            R_j_k,
            Bprm_j_k,
            thetaprm_j_phiprm_k,
            kwargs,
        ) = process_transformation_kwargs(w_modes.ell_max, **kwargs)

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

        # We need values of (1) waveform, (2) conformal factor, and (3) supertranslation, at each point of the
        # transformed grid, at each instant of time.
        SWSH_j_k = sf.SWSH_grid(R_j_k, w_modes.spin_weight, ell_max)
        SH_j_k = sf.SWSH_grid(R_j_k, 0, ell_max_supertranslation
                              )  # standard (spin-zero) spherical harmonics
        r_j_k = np.array([(R * quaternion.z * R.inverse()).vec
                          for R in R_j_k.flat]).T
        kconformal_j_k = 1.0 / (
            gamma * (1 - np.dot(boost_velocity, r_j_k).reshape(R_j_k.shape)))
        alphasupertranslation_j_k = np.tensordot(supertranslation,
                                                 SH_j_k,
                                                 axes=([0], [2])).real
        fprm_i_j_k = np.tensordot(
            w_modes.data,
            SWSH_j_k[:, :,
                     sf.LM_index(w_modes.ell_min, -w_modes.ell_min, 0):sf.
                     LM_index(w_modes.ell_max, w_modes.ell_max, 0) + 1, ],
            axes=([1], [2]),
        )
        if beta != 0 or (supertranslation[1:] != 0).any():
            if w_modes.dataType == h:
                # Note that SWSH_j_k will use s=-2 in this case, so it can be used in the tensordot correctly
                supertranslation_deriv = 0.5 * sf.ethbar_GHP(
                    sf.ethbar_GHP(supertranslation, 0, 0), -1, 0)
                supertranslation_deriv_values = np.tensordot(
                    supertranslation_deriv,
                    SWSH_j_k[:, :, :sf.LM_index(ell_max_supertranslation,
                                                ell_max_supertranslation, 0) +
                             1],
                    axes=([0], [2]),
                )
                fprm_i_j_k -= supertranslation_deriv_values[np.newaxis, :, :]
            elif w_modes.dataType == sigma:
                # Note that SWSH_j_k will use s=+2 in this case, so it can be used in the tensordot correctly
                supertranslation_deriv = 0.5 * sf.eth_GHP(
                    sf.eth_GHP(supertranslation, 0, 0), 1, 0)
                supertranslation_deriv_values = np.tensordot(
                    supertranslation_deriv,
                    SWSH_j_k[:, :, :sf.LM_index(ell_max_supertranslation,
                                                ell_max_supertranslation, 0) +
                             1],
                    axes=([0], [2]),
                )
                fprm_i_j_k -= supertranslation_deriv_values[np.newaxis, :, :]
            elif w_modes.dataType in [psi0, psi1, psi2, psi3]:
                from scipy.special import comb

                eth_alphasupertranslation_j_k = np.tensordot(
                    1 / np.sqrt(2) *
                    sf.eth_GHP(supertranslation, spin_weight=0),
                    sf.SWSH_grid(R_j_k, 1, ell_max_supertranslation),
                    axes=([0], [2]),
                )
                v_dot_rhat = np.insert(
                    sf.vector_as_ell_1_modes(boost_velocity), 0, 0.0)
                eth_v_dot_rhat_j_k = np.tensordot(1 / np.sqrt(2) * v_dot_rhat,
                                                  sf.SWSH_grid(R_j_k, 1, 1),
                                                  axes=([0], [2]))
                eth_uprm_over_kconformal_i_j_k = (
                    (w_modes.t[:, np.newaxis, np.newaxis] -
                     alphasupertranslation_j_k[np.newaxis, :, :]) * gamma *
                    kconformal_j_k[np.newaxis, :, :] *
                    eth_v_dot_rhat_j_k[np.newaxis, :, :] -
                    eth_alphasupertranslation_j_k[np.newaxis, :, :])
                # Loop over the Weyl scalars of higher index than w_modes, and sum
                # them with appropriate prefactors.
                for DT in range(w_modes.dataType + 1, psi4 + 1):
                    try:
                        w_modes_temp = kwargs.pop("psi{}_modes".format(
                            DataNames[DT][-1]))
                    except KeyError:
                        raise ValueError(
                            "\nA BMS transformation of {} requires information from {}, which "
                            "has not been supplied.".format(
                                w_modes.data_type_string, DataNames[DT]))
                    SWSH_temp_j_k = sf.SWSH_grid(R_j_k,
                                                 w_modes_temp.spin_weight,
                                                 w_modes_temp.ell_max)
                    f_i_j_k = np.tensordot(
                        w_modes_temp.data,
                        SWSH_temp_j_k[:, :,
                                      sf.LM_index(w_modes_temp.ell_min,
                                                  -w_modes_temp.ell_min, 0):sf.
                                      LM_index(w_modes_temp.ell_max,
                                               w_modes_temp.ell_max, 0) + 1, ],
                        axes=([1], [2]),
                    )
                    fprm_i_j_k += (comb(5 - w_modes.dataType, 5 - DT) *
                                   f_i_j_k * eth_uprm_over_kconformal_i_j_k
                                   **(DT - w_modes.dataType))
            elif w_modes.dataType not in [psi4, hdot, news]:
                warning = (
                    "\nNo BMS transformation is implemented for waveform objects "
                    "of dataType '{}'. Proceeding with the transformation as if it "
                    "were dataType 'Psi4'.".format(w_modes.data_type_string))
                warnings.warn(warning)

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

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

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

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

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

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

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

        return g