def speciality_index(self, **kwargs): """Computes the Baker-Campanelli speciality index (arXiv:gr-qc/0003031). NOTE: This quantity can only determine algebraic speciality but can not determine the type! The rule of thumb given by Baker and Campanelli is that for an algebraically special spacetime the speciality index should differ from unity by no more than a factor of two. """ import spinsfast import spherical_functions as sf from spherical_functions import LM_index output_ell_max = kwargs.pop("output_ell_max") if "output_ell_max" in kwargs else self.ell_max working_ell_max = kwargs.pop("working_ell_max") if "working_ell_max" in kwargs else 2 * self.ell_max n_theta = n_phi = 2 * working_ell_max + 1 # Transform to grid representation psi4 = np.empty((self.n_times, n_theta, n_phi), dtype=complex) psi3 = np.empty((self.n_times, n_theta, n_phi), dtype=complex) psi2 = np.empty((self.n_times, n_theta, n_phi), dtype=complex) psi1 = np.empty((self.n_times, n_theta, n_phi), dtype=complex) psi0 = np.empty((self.n_times, n_theta, n_phi), dtype=complex) for t_i in range(self.n_times): psi4[t_i, :, :] = spinsfast.salm2map( self.psi4.ndarray[t_i, :], self.psi4.spin_weight, lmax=self.ell_max, Ntheta=n_theta, Nphi=n_phi ) psi3[t_i, :, :] = spinsfast.salm2map( self.psi3.ndarray[t_i, :], self.psi3.spin_weight, lmax=self.ell_max, Ntheta=n_theta, Nphi=n_phi ) psi2[t_i, :, :] = spinsfast.salm2map( self.psi2.ndarray[t_i, :], self.psi2.spin_weight, lmax=self.ell_max, Ntheta=n_theta, Nphi=n_phi ) psi1[t_i, :, :] = spinsfast.salm2map( self.psi1.ndarray[t_i, :], self.psi1.spin_weight, lmax=self.ell_max, Ntheta=n_theta, Nphi=n_phi ) psi0[t_i, :, :] = spinsfast.salm2map( self.psi0.ndarray[t_i, :], self.psi0.spin_weight, lmax=self.ell_max, Ntheta=n_theta, Nphi=n_phi ) curvature_invariant_I = psi4 * psi0 - 4 * psi3 * psi1 + 3 * psi2 ** 2 curvature_invariant_J = ( psi4 * (psi2 * psi0 - psi1 ** 2) - psi3 * (psi3 * psi0 - psi1 * psi2) + psi2 * (psi3 * psi1 - psi2 ** 2) ) speciality_index = 27 * curvature_invariant_J ** 2 / curvature_invariant_I ** 3 # Transform back to mode representation speciality_index_modes = np.empty((self.n_times, (working_ell_max) ** 2), dtype=complex) for t_i in range(self.n_times): speciality_index_modes[t_i, :] = spinsfast.map2salm(speciality_index[t_i, :], 0, lmax=working_ell_max - 1) # Convert product ndarray to a ModesTimeSeries object speciality_index_modes = speciality_index_modes[:, : LM_index(output_ell_max, output_ell_max, 0) + 1] speciality_index_modes = ModesTimeSeries( sf.SWSH_modes.Modes( speciality_index_modes, spin_weight=0, ell_min=0, ell_max=output_ell_max, multiplication_truncator=max ), time=self.t, ) return speciality_index_modes
def silly_angular_momentum_flux(h, hdot=None): """Compute angular momentum flux from waveform explicitly This function uses very explicit (and slow) methods, but different as far as possible from the methods used in the main code. """ import spinsfast hdot = hdot or h.data_dot zeros = np.zeros((hdot.shape[0], 4), dtype=hdot.dtype) data = np.concatenate((zeros, hdot), axis=1) # Pad with zeros for spinsfast ell_min = 0 ell_max = 2*h.ell_max # Maximum ell value required to fully resolve Ji{h} * hdot n_theta = 2*ell_max + 1 n_phi = n_theta hdot_map = spinsfast.salm2map(data, h.spin_weight, h.ell_max, n_theta, n_phi) jdot = np.zeros((h.n_times, 3), dtype=float) # Compute J_+ h for ell in range(h.ell_min, h.ell_max+1): i = h.index(ell, -ell) + 4 data[:, i] = 0.0j for m in range(-ell, ell): i = h.index(ell, m+1) + 4 j = h.index(ell, m) data[:, i] = 1.j * np.sqrt((ell-m)*(ell+m+1)) * h.data[:, j] jplus_h_map = spinsfast.salm2map(data, h.spin_weight, h.ell_max, n_theta, n_phi) # Compute J_- h for ell in range(h.ell_min, h.ell_max+1): for m in range(-ell+1, ell+1): i = h.index(ell, m-1) + 4 j = h.index(ell, m) data[:, i] = 1.j * np.sqrt((ell+m)*(ell-m+1)) * h.data[:, j] i = h.index(ell, ell) + 4 data[:, i] = 0.0j jminus_h_map = spinsfast.salm2map(data, h.spin_weight, h.ell_max, n_theta, n_phi) # Combine jplus and jminus to compute x-y components, then conjugate, multiply by hdot, and integrate jx_h_map = 0.5 * (jplus_h_map + jminus_h_map) jy_h_map = -0.5j * (jplus_h_map - jminus_h_map) jdot[:, 0] = -spinsfast.map2salm(jx_h_map.conjugate() * hdot_map, 0, 0)[..., 0].real * (2*np.sqrt(np.pi)) / (16*np.pi) jdot[:, 1] = -spinsfast.map2salm(jy_h_map.conjugate() * hdot_map, 0, 0)[..., 0].real * (2*np.sqrt(np.pi)) / (16*np.pi) # Compute J_z h, then conjugate, multiply by hdot, and integrate for ell in range(h.ell_min, h.ell_max+1): for m in range(-ell, ell+1): i = h.index(ell, m) data[:, i+4] = 1.j * m * h.data[:, i] jz_h_map = spinsfast.salm2map(data, h.spin_weight, h.ell_max, n_theta, n_phi) jdot[:, 2] = -spinsfast.map2salm(jz_h_map.conjugate() * hdot_map, 0, 0)[..., 0].real * (2*np.sqrt(np.pi)) / (16*np.pi) return jdot
def test_eth_derivation(eth, spin_weight_of_eth): """Ensure that the various `eth` operators are derivations -- i.e., they obey the Leibniz product law Given two spin-weighted functions `f` and `g`, we need to test that eth(f * g) = eth(f) * g + f * eth(g) This test generates a set of random modes with equal power for `f` and `g` (though more realistic functions can be expected to have exponentially decaying mode amplitudes). Because of the large power in high-ell modes, we need to double the number of modes in the representation of their product, which is why we use n_theta = n_phi = 4 * ell_max + 1 These `f` and `g` functions must be transformed to the physical-space representation, multiplied there, the product transformed back to spectral space, the eth operator evaluated, and then transformed back again to physical space for comparison. We test both the Newman-Penrose and Geroch-Held-Penrose versions of eth, as well as their conjugates. """ import spinsfast ell_max = 16 n_modes = sf.LM_total_size(0, ell_max) n_theta = n_phi = 4 * ell_max + 1 for s1 in range(-2, 2 + 1): for s2 in range(-s1, s1 + 1): np.random.seed(1234) ell_min1 = abs(s1) ell_min2 = abs(s2) f = np.random.rand(n_modes) + 1j * np.random.rand(n_modes) f[:sf.LM_total_size(0, ell_min1 - 1)] = 0j f_j_k = spinsfast.salm2map(f, s1, ell_max, n_theta, n_phi) g = np.random.rand(n_modes) + 1j * np.random.rand(n_modes) g[:sf.LM_total_size(0, ell_min2 - 1)] = 0j g_j_k = spinsfast.salm2map(g, s2, ell_max, n_theta, n_phi) fg_j_k = f_j_k * g_j_k fg = spinsfast.map2salm(fg_j_k, s1 + s2, 2 * ell_max) ethf = eth(f, s1, ell_min=0) ethg = eth(g, s2, ell_min=0) ethfg = eth(fg, s1 + s2, ell_min=0) ethf_j_k = spinsfast.salm2map(ethf, s1 + spin_weight_of_eth, ell_max, n_theta, n_phi) ethg_j_k = spinsfast.salm2map(ethg, s2 + spin_weight_of_eth, ell_max, n_theta, n_phi) ethfg_j_k = spinsfast.salm2map(ethfg, s1 + s2 + spin_weight_of_eth, 2 * ell_max, n_theta, n_phi) assert np.allclose(ethfg_j_k, ethf_j_k * g_j_k + f_j_k * ethg_j_k, rtol=1e-10, atol=1e-10)
def backward(salm, Ntheta, Nphi): """ Returns a function on S^2 given it's spin coefficients in the form of an salm object. Parameters ---------- salm : swsh.salm.salm a subclass of numpy.ndarray that stores the spin coefficients Ntheta : int an int giving the number of points discritising the theta variable of S^2 Nphi : an int giving the number of points discritising the phi variable of S^2 Returns ------- numpy.ndarray : a numpy.ndarray of shape (salm.spins.shape[[0], Ntheta, Nphi) containing the values of the function on S^2, parameterised via the ecp discretisation, see section 2.3 """ spins = np.atleast_1d(salm.spins) Ntransform = spins.shape[0] lmax = salm.lmax if len(spins.shape) != 1: raise ValueError('spins must be an int or a one dimensional array of ints') data = np.asarray(salm) f = np.array([ spinsfast.salm2map(data[i], spins[i], lmax, Ntheta, Nphi) for i in range(Ntransform) ]) return f
def test_eth_derivation(eth, spin_weight_of_eth): """Ensure that the various `eth` operators are derivations -- i.e., they obey the Leibniz product law Given two spin-weighted functions `f` and `g`, we need to test that eth(f * g) = eth(f) * g + f * eth(g) This test generates a set of random modes with equal power for `f` and `g` (though more realistic functions can be expected to have exponentially decaying mode amplitudes). Because of the large power in high-ell modes, we need to double the number of modes in the representation of their product, which is why we use n_theta = n_phi = 4 * ell_max + 1 These `f` and `g` functions must be transformed to the physical-space representation, multiplied there, the product transformed back to spectral space, the eth operator evaluated, and then transformed back again to physical space for comparison. We test both the Newman-Penrose and Geroch-Held-Penrose versions of eth, as well as their conjugates. """ import spinsfast ell_max = 16 n_modes = sf.LM_total_size(0, ell_max) n_theta = n_phi = 4 * ell_max + 1 for s1 in range(-2, 2 + 1): for s2 in range(-s1, s1 + 1): np.random.seed(1234) ell_min1 = abs(s1) ell_min2 = abs(s2) f = np.random.rand(n_modes) + 1j * np.random.rand(n_modes) f[:sf.LM_total_size(0, ell_min1-1)] = 0j f_j_k = spinsfast.salm2map(f, s1, ell_max, n_theta, n_phi) g = np.random.rand(n_modes) + 1j * np.random.rand(n_modes) g[:sf.LM_total_size(0, ell_min2-1)] = 0j g_j_k = spinsfast.salm2map(g, s2, ell_max, n_theta, n_phi) fg_j_k = f_j_k * g_j_k fg = spinsfast.map2salm(fg_j_k, s1+s2, 2*ell_max) ethf = eth(f, s1, ell_min=0) ethg = eth(g, s2, ell_min=0) ethfg = eth(fg, s1+s2, ell_min=0) ethf_j_k = spinsfast.salm2map(ethf, s1+spin_weight_of_eth, ell_max, n_theta, n_phi) ethg_j_k = spinsfast.salm2map(ethg, s2+spin_weight_of_eth, ell_max, n_theta, n_phi) ethfg_j_k = spinsfast.salm2map(ethfg, s1+s2+spin_weight_of_eth, 2*ell_max, n_theta, n_phi) assert np.allclose(ethfg_j_k, ethf_j_k * g_j_k + f_j_k * ethg_j_k, rtol=1e-10, atol=1e-10)
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
def grid(self, n_theta=None, n_phi=None, use_spinsfast=True, **kwargs): """Return values of function on an equi-angular grid This method converts mode weights of spin-weighted function to values on a grid. The grid has `n_theta` evenly spaced points along the usual polar (colatitude) angle theta, and `n_phi` evenly spaced points along the usual azimuthal angle phi. This grid corresponds to the one produced by `spherical.theta_phi`; see that function for specifics. The output array has one more dimension than this object; rather than the last axis giving the mode weights, the last two axes give the values on the two-dimensional grid. Parameters ---------- n_theta : {None, int}, optional Number of points to use in theta direction. None is equivalent to 2*self.ell_max+1, which is the minimum number that can capture behavior up to and including ell_max. If you need to multiply the result with some `other` spin-weighted function, you should use an n_theta value of 2 * (self.ell_max + other.ell_max) + 1 to avoid aliasing. n_phi : {None, int}, optional Number of points to use in the phi direction. Here, None is equivalent to n_phi=n_theta, after calculation of the default value for n_theta. Note that the same comments apply about avoiding aliasing. use_spinsfast : bool, optional If True, and `spinsfast` is accessible, use it to evaluate the function values; otherwise, use this module's `Wigner.evaluate` method. **kwargs : Any Additional keyword arguments are passed through to the Grid constructor on output """ import copy import numpy as np import quaternionic from .. import Grid, theta_phi n_theta = n_theta or 2*self.ell_max+1 n_phi = n_phi or n_theta metadata = copy.copy(self._metadata) metadata.pop('ell_max', None) metadata.update(**kwargs) try: import spinsfast except ImportError: use_spinsfast = False if use_spinsfast: return Grid( spinsfast.salm2map(self.view(np.ndarray), self.spin_weight, self.ell_max, n_theta, n_phi), **metadata ) else: return Grid( self.evaluate(quaternionic.array.from_spherical_coordinates(theta_phi(n_theta, n_phi))), **metadata )
def spinsfast_multiply(f, ellmin_f, ellmax_f, s_f, g, ellmin_g, ellmax_g, s_g): """Multiply functions pointwise This function takes the same arguments as the spherical_functions.multiply function and returns the same quantities. However, this performs the multiplication simply by transforming the modes to function values on a (theta, phi) grid, multiplying those values at each point, and then transforming back to modes. """ s_fg = s_f + s_g ellmin_fg = 0 ellmax_fg = ellmax_f + ellmax_g n_theta = 2*ellmax_fg + 1 n_phi = n_theta f_map = spinsfast.salm2map(f, s_f, ellmax_f, n_theta, n_phi) g_map = spinsfast.salm2map(g, s_g, ellmax_g, n_theta, n_phi) fg_map = f_map * g_map fg = spinsfast.map2salm(fg_map, s_fg, ellmax_fg) return fg, ellmin_fg, ellmax_fg, s_fg
def silly_momentum_flux(h): """Compute momentum flux from waveform with a silly but simple method This function evaluates the momentum-flux formula quite literally. The formula is dp R**2 | dh |**2 ^ ---------- = ------ | -- | n dOmega dt 16 pi | dt | Here, p and nhat are vectors and R is the distance to the source. The input h is differentiated numerically to find modes of dh/dt, which are then used to construct the values of dh/dt on a grid. At each point of that grid, the value of |dh/dt|**2 nhat is computed, which is then integrated over the sphere to arrive at dp/dt. Note that this integration is accomplished by way of a spherical-harmonic decomposition; the ell=m=0 mode is multiplied by 2*sqrt(pi) to arrive at the result that would be achieved by integrating over the sphere. """ import spinsfast hdot = h.data_dot zeros = np.zeros((hdot.shape[0], 4), dtype=hdot.dtype) data = np.concatenate((zeros, hdot), axis=1) # Pad with zeros for spinsfast ell_min = 0 ell_max = 2 * h.ell_max + 1 # Maximum ell value required for nhat*|hdot|^2 n_theta = 2 * ell_max + 1 n_phi = n_theta hdot_map = spinsfast.salm2map(data, h.spin_weight, h.ell_max, n_theta, n_phi) hdot_mag_squared_map = hdot_map * hdot_map.conjugate() theta = np.linspace(0.0, np.pi, num=n_theta, endpoint=True) phi = np.linspace(0.0, 2 * np.pi, num=n_phi, endpoint=False) x = np.outer(np.sin(theta), np.cos(phi)) y = np.outer(np.sin(theta), np.sin(phi)) z = np.outer(np.cos(theta), np.ones_like(phi)) pdot = np.array([ spinsfast.map2salm(hdot_mag_squared_map * n / (16 * np.pi), 0, 0)[..., 0].real * (2 * np.sqrt(np.pi)) for n in [x, y, z] ]).T return pdot
def test_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]])])
def grid(self, n_theta=None, n_phi=None, **kwargs): """Return values of function on an equi-angular grid This method uses `spinsfast` to convert mode weights of spin-weighted function to values on a grid. The grid has `n_theta` evenly spaced points along the usual polar (colatitude) angle theta, and `n_phi` evenly spaced points along the usual azimuthal angle phi. This grid corresponds to the one produced by `spherical_functions.theta_phi`; see that function for specifics. The output array has one more dimension than this object; rather than the last axis giving the mode weights, the last two axes give the values on the two-dimensional grid. Parameters ========== n_theta: None or int [defaults to None] Number of points to use in theta direction. None is equivalent to 2*self.ell_max+1, which is the minimum number that can capture behavior up to and including ell_max. If you need to multiply the result with some `other` spin-weighted function, you should use an n_theta value of 2 * (self.ell_max + other.ell_max) + 1 to avoid aliasing. n_phi: None or int [defaults to None] Number of points to use in the phi direction. Here, None is equivalent to n_phi=n_theta, after calculation of the default value for n_theta. Note that the same comments apply about avoiding aliasing. **kwargs: any types Additional keyword arguments are passed through to the Grid constructor on output """ import copy import numpy as np import spinsfast from .. import Grid n_theta = n_theta or 2 * self.ell_max + 1 n_phi = n_phi or n_theta metadata = copy.copy(self._metadata) metadata.pop('ell_max', None) metadata.update(**kwargs) return Grid( spinsfast.salm2map(self.view(np.ndarray), self.s, self.ell_max, n_theta, n_phi), **metadata)
Nlm = spinsfast.N_lm(lmax); alm = zeros(Nlm,dtype=complex) # Fill the alm with white noise seed(3124432) for l in range(abs(s),lmax+1) : for m in range(-l,l+1) : i = spinsfast.lm_ind(l,m,lmax) if (m==0) : alm[i] = normal() else : alm[i] = normal()/2 + 1j*normal()/2 f = spinsfast.salm2map(alm,s,lmax,Ntheta,Nphi) # In this pixelization, access the map with f[itheta,iphi] # where 0 <= itheta <= Ntheta-1 and 0<= iphi <= Nphi-1 # and theta = pi*itheta/(Ntheta-1) phi = 2*pi*iphi/Nphi alm2 = spinsfast.map2salm(f,s,lmax) diff_max = max((alm-alm2)) print("max(alm2-alm) = "+str(diff_max)) figure() imshow(f.real,interpolation='nearest') colorbar() title("Real part of f") xlabel("iphi")
# Set up the parameters of the transformation. These Ntheta and Nphi # are usually overkill for plotting purposes, but don't slow things # down noticeably. s = 0 # Spin weight lmax = 7 Ntheta = 2**9 + 1 Nphi = 2**9 Nlm = spinsfast.N_lm(lmax) # These are the physical coordinates to be used below. theta = linspace(0, pi, num=Ntheta) phi = linspace(0, 2 * pi, num=Nphi) # Here, we transform the data, take its absolute value, and then # construct a colormap from its normalized values. f = spinsfast.salm2map(alm, s, lmax, Ntheta, Nphi) absf = abs(f) # This is for 2-d plots. pcolormesh(phi, theta, absf) xlim((0, 2 * pi)) ylim((0, pi)) xlabel(r'$\phi$') ylabel(r'$\theta$') tight_layout() #savefig('Waveform.png', dpi=200, transparent=True) #show() # This is for 3-d plots. This is by far the slowest part of the # process. Increase rstride and cstride to make this go faster at the # cost of resolution.
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_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 grid_multiply(self, mts, **kwargs): """Compute mode weights of the product of two functions This will compute the values of `self` and `mts` on a grid, multiply the grid values together, and then return the mode coefficients of the product. This takes less time and memory compared to the `SWSH_modes.Modes.multiply()` function, at the risk of introducing aliasing effects if `working_ell_max` is too small. Parameters ---------- self: ModesTimeSeries One of the quantities to multiply. mts: ModesTimeSeries The quantity to multiply with 'self'. working_ell_max: int, optional The value of ell_max to be used to define the computation grid. The number of theta points and the number of phi points are set to 2*working_ell_max+1. Defaults to (self.ell_max + mts.ell_max). output_ell_max: int, optional The value of ell_max in the output mts object. Defaults to self.ell_max. """ import spinsfast from spherical_functions import LM_index output_ell_max = kwargs.pop("output_ell_max", self.ell_max) working_ell_max = kwargs.pop("working_ell_max", self.ell_max + mts.ell_max) n_theta = n_phi = 2 * working_ell_max + 1 if self.n_times != mts.n_times or not np.equal(self.t, mts.t).all(): raise ValueError( "The time series of objects to be multiplied must be the same." ) # Transform to grid representation self_grid = spinsfast.salm2map(self.ndarray, self.spin_weight, lmax=self.ell_max, Ntheta=n_theta, Nphi=n_phi) mts_grid = spinsfast.salm2map(mts.ndarray, mts.spin_weight, lmax=mts.ell_max, Ntheta=n_theta, Nphi=n_phi) product_grid = self_grid * mts_grid product_spin_weight = self.spin_weight + mts.spin_weight # Transform back to mode representation product = spinsfast.map2salm(product_grid, product_spin_weight, lmax=working_ell_max) # Convert product ndarray to a ModesTimeSeries object product = product[:, :LM_index(output_ell_max, output_ell_max, 0) + 1] product = ModesTimeSeries( spherical_functions.SWSH_modes.Modes( product, spin_weight=product_spin_weight, ell_min=0, ell_max=output_ell_max, multiplication_truncator=max, ), time=self.t, ) return product