def cf2(v, distorted_grid_rotors): from math import sin, cos # κ = 1 / [γ(1-v⋅r)] # ð(κ⁻¹) = - κ⁻² ðκ # ðκ = - κ² ð(κ⁻¹) theta_phi = sf.theta_phi(n_theta, n_phi) kinv = γ * np.array( [ [1 - v[0] * sin(θ) * cos(ϕ) - v[1] * math.sin(θ) * math.sin(ϕ) - v[2] * math.cos(θ) for θ, ϕ in row] for row in theta_phi ] ) k = 1 / kinv κinv = spinsfast.map2salm(kinv, 0, ell_max) κ = spinsfast.map2salm(k, 0, ell_max) SWSHs = sf.SWSH_grid(distorted_grid_rotors, 0, ell_max) one_over_k2 = np.tensordot(κinv, SWSHs, axes=([-1], [-1])) one_over_k_cubed2 = one_over_k2 ** 3 k2 = 1 / one_over_k2 ðκ = sf.eth_GHP(κ, 0) SWSHs = sf.SWSH_grid(distorted_grid_rotors, 1, ell_max) ðk = np.tensordot(ðκ, SWSHs, axes=([-1], [-1])) ðk_over_k2 = ðk / k2 return ( k2[np.newaxis, :, :], ðk_over_k2[np.newaxis, :, :], one_over_k2[np.newaxis, :, :], one_over_k_cubed2[np.newaxis, :, :], )
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 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 modes(self, ell_max=None, **kwargs): """Return mode weights of function decomposed into SWSHs This method uses `spinsfast` to convert values on an equiangular grid to mode weights. The output array has one less dimension than this object; rather than the last two axes giving the values on the two-dimensional grid, the last axis gives the mode weights. Parameters ========== ell_max: None or int [defaults to None] Maximum ell value in the output. If None, the result will have enough ell values to express the data on the grid without aliasing: (max(n_phi, n_theta) - 1) // 2. **kwargs: any types Additional keyword arguments are passed through to the Modes constructor on output """ import copy import numpy as np import spinsfast from .. import Modes ell_max = ell_max or (max(n_phi, n_theta) - 1) // 2 metadata = copy.copy return Modes(spinsfast.map2salm(self.view(np.ndarray), self.s, ell_max), spin_weight=self.s, ell_min=0, ell_max=ell_max, **metadata)
def to_modes(self, ell_max=None, ell_min=None): """Transform to modes of a spin-weighted spherical harmonic expansion Parameters ---------- self : WaveformGrid object This is the object to be transformed to SWSH modes ell_max : int, optional The largest ell value to include in the output data. Default value is deduced from n_theta and n_phi. ell_min : int, optional The smallest ell value to include in the output data. Default value is abs(spin_weight). """ s = SpinWeights[self.dataType] if ell_max is None: ell_max = int((max(self.n_theta, self.n_phi) - 1) // 2) if ell_min is None: ell_min = abs(s) if not isinstance(ell_max, numbers.Integral) or ell_max < 0: raise ValueError(f"Input `ell_max` should be a nonnegative integer; got `{ell_max}`.") if not isinstance(ell_min, numbers.Integral) or ell_min < 0 or ell_min > ell_max: raise ValueError(f"Input `ell_min` should be an integer between 0 and {ell_max}; got `{ell_min}`.") final_dim = int(np.prod(self.data.shape[2:])) old_data = self.data.reshape((self.n_times, self.n_theta, self.n_phi, final_dim)) new_data = np.empty((self.n_times, sf.LM_total_size(ell_min, ell_max), final_dim), dtype=complex) # Note that spinsfast returns all modes, including ell<abs(s). So we just chop those off for i_time in range(self.n_times): for i_final in range(final_dim): new_data[i_time, :, i_final] = spinsfast.map2salm(old_data[i_time, :, :, i_final], s, ell_max)[ sf.LM_index(ell_min, -ell_min, 0) : ] new_data = new_data.reshape((self.n_times, sf.LM_total_size(ell_min, ell_max)) + self.data.shape[2:]) # old_data = self.data.reshape((self.n_times, self.n_theta, self.n_phi)+self.data.shape[2:]) # new_data = np.empty((self.n_times, sf.LM_total_size(ell_min, ell_max))+self.data.shape[2:], dtype=complex) # # Note that spinsfast returns all modes, including ell<abs(s). So we just chop those off # for i_time in range(self.n_times): # new_data[i_time, :] = spinsfast.map2salm(old_data[i_time, :, :], s, ell_max)\ # [sf.LM_index(ell_min, -ell_min, 0):] m = WaveformModes( t=self.t, data=new_data, history=self.history, ell_min=ell_min, ell_max=ell_max, frameType=self.frameType, dataType=self.dataType, r_is_scaled_out=self.r_is_scaled_out, m_is_scaled_out=self.m_is_scaled_out, constructor_statement=f"{self}.to_modes({ell_max})", ) return m
def test_eth_derivation(eth, spin_weight_of_eth): """Ensure that the various `eth` operators are derivations -- i.e., they obey the Leibniz product law Given two spin-weighted functions `f` and `g`, we need to test that eth(f * g) = eth(f) * g + f * eth(g) This test generates a set of random modes with equal power for `f` and `g` (though more realistic functions can be expected to have exponentially decaying mode amplitudes). Because of the large power in high-ell modes, we need to double the number of modes in the representation of their product, which is why we use n_theta = n_phi = 4 * ell_max + 1 These `f` and `g` functions must be transformed to the physical-space representation, multiplied there, the product transformed back to spectral space, the eth operator evaluated, and then transformed back again to physical space for comparison. We test both the Newman-Penrose and Geroch-Held-Penrose versions of eth, as well as their conjugates. """ import spinsfast ell_max = 16 n_modes = sf.LM_total_size(0, ell_max) n_theta = n_phi = 4 * ell_max + 1 for s1 in range(-2, 2 + 1): for s2 in range(-s1, s1 + 1): np.random.seed(1234) ell_min1 = abs(s1) ell_min2 = abs(s2) f = np.random.rand(n_modes) + 1j * np.random.rand(n_modes) f[:sf.LM_total_size(0, ell_min1 - 1)] = 0j f_j_k = spinsfast.salm2map(f, s1, ell_max, n_theta, n_phi) g = np.random.rand(n_modes) + 1j * np.random.rand(n_modes) g[:sf.LM_total_size(0, ell_min2 - 1)] = 0j g_j_k = spinsfast.salm2map(g, s2, ell_max, n_theta, n_phi) fg_j_k = f_j_k * g_j_k fg = spinsfast.map2salm(fg_j_k, s1 + s2, 2 * ell_max) ethf = eth(f, s1, ell_min=0) ethg = eth(g, s2, ell_min=0) ethfg = eth(fg, s1 + s2, ell_min=0) ethf_j_k = spinsfast.salm2map(ethf, s1 + spin_weight_of_eth, ell_max, n_theta, n_phi) ethg_j_k = spinsfast.salm2map(ethg, s2 + spin_weight_of_eth, ell_max, n_theta, n_phi) ethfg_j_k = spinsfast.salm2map(ethfg, s1 + s2 + spin_weight_of_eth, 2 * ell_max, n_theta, n_phi) assert np.allclose(ethfg_j_k, ethf_j_k * g_j_k + f_j_k * ethg_j_k, rtol=1e-10, atol=1e-10)
def silly_momentum_flux(h): """Compute momentum flux from waveform with a silly but simple method This function evaluates the momentum-flux formula quite literally. The formula is dp R**2 | dh |**2 ^ ---------- = ------ | -- | n dOmega dt 16 pi | dt | Here, p and nhat are vectors and R is the distance to the source. The input h is differentiated numerically to find modes of dh/dt, which are then used to construct the values of dh/dt on a grid. At each point of that grid, the value of |dh/dt|**2 nhat is computed, which is then integrated over the sphere to arrive at dp/dt. Note that this integration is accomplished by way of a spherical-harmonic decomposition; the ell=m=0 mode is multiplied by 2*sqrt(pi) to arrive at the result that would be achieved by integrating over the sphere. """ import spinsfast hdot = h.data_dot zeros = np.zeros((hdot.shape[0], 4), dtype=hdot.dtype) data = np.concatenate((zeros, hdot), axis=1) # Pad with zeros for spinsfast ell_min = 0 ell_max = 2 * h.ell_max + 1 # Maximum ell value required for nhat*|hdot|^2 n_theta = 2 * ell_max + 1 n_phi = n_theta hdot_map = spinsfast.salm2map(data, h.spin_weight, h.ell_max, n_theta, n_phi) hdot_mag_squared_map = hdot_map * hdot_map.conjugate() theta = np.linspace(0.0, np.pi, num=n_theta, endpoint=True) phi = np.linspace(0.0, 2 * np.pi, num=n_phi, endpoint=False) x = np.outer(np.sin(theta), np.cos(phi)) y = np.outer(np.sin(theta), np.sin(phi)) z = np.outer(np.cos(theta), np.ones_like(phi)) pdot = np.array([ spinsfast.map2salm(hdot_mag_squared_map * n / (16 * np.pi), 0, 0)[..., 0].real * (2 * np.sqrt(np.pi)) for n in [x, y, z] ]).T return pdot
def test_eth_derivation(eth, spin_weight_of_eth): """Ensure that the various `eth` operators are derivations -- i.e., they obey the Leibniz product law Given two spin-weighted functions `f` and `g`, we need to test that eth(f * g) = eth(f) * g + f * eth(g) This test generates a set of random modes with equal power for `f` and `g` (though more realistic functions can be expected to have exponentially decaying mode amplitudes). Because of the large power in high-ell modes, we need to double the number of modes in the representation of their product, which is why we use n_theta = n_phi = 4 * ell_max + 1 These `f` and `g` functions must be transformed to the physical-space representation, multiplied there, the product transformed back to spectral space, the eth operator evaluated, and then transformed back again to physical space for comparison. We test both the Newman-Penrose and Geroch-Held-Penrose versions of eth, as well as their conjugates. """ import spinsfast ell_max = 16 n_modes = sf.LM_total_size(0, ell_max) n_theta = n_phi = 4 * ell_max + 1 for s1 in range(-2, 2 + 1): for s2 in range(-s1, s1 + 1): np.random.seed(1234) ell_min1 = abs(s1) ell_min2 = abs(s2) f = np.random.rand(n_modes) + 1j * np.random.rand(n_modes) f[:sf.LM_total_size(0, ell_min1-1)] = 0j f_j_k = spinsfast.salm2map(f, s1, ell_max, n_theta, n_phi) g = np.random.rand(n_modes) + 1j * np.random.rand(n_modes) g[:sf.LM_total_size(0, ell_min2-1)] = 0j g_j_k = spinsfast.salm2map(g, s2, ell_max, n_theta, n_phi) fg_j_k = f_j_k * g_j_k fg = spinsfast.map2salm(fg_j_k, s1+s2, 2*ell_max) ethf = eth(f, s1, ell_min=0) ethg = eth(g, s2, ell_min=0) ethfg = eth(fg, s1+s2, ell_min=0) ethf_j_k = spinsfast.salm2map(ethf, s1+spin_weight_of_eth, ell_max, n_theta, n_phi) ethg_j_k = spinsfast.salm2map(ethg, s2+spin_weight_of_eth, ell_max, n_theta, n_phi) ethfg_j_k = spinsfast.salm2map(ethfg, s1+s2+spin_weight_of_eth, 2*ell_max, n_theta, n_phi) assert np.allclose(ethfg_j_k, ethf_j_k * g_j_k + f_j_k * ethg_j_k, rtol=1e-10, atol=1e-10)
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 transform(self, **kwargs): """Apply BMS transformation to AsymptoticBondiData object It is important to note that the input transformation parameters are applied in this order: 1. (Super)Translations 2. Rotation (about the origin) 3. Boost (about the origin) All input parameters refer to the transformation required to take the input data's inertial frame onto the inertial frame of the output data's inertial observers. In what follows, the coordinates of and functions in the input inertial frame will be unprimed, while corresponding values of the output inertial frame will be primed. The translations (space, time, spacetime, or super) can be given in various ways, which may override each other. Ultimately, however, they are essentially combined into a single function `α`, representing the supertranslation, which transforms the asymptotic time variable `u` as u'(u, θ, ϕ) = u(u, θ, ϕ) - α(θ, ϕ) A simple time translation by δt would correspond to α(θ, ϕ) = δt # Independent of (θ, ϕ) A pure spatial translation δx would correspond to α(θ, ϕ) = -δx · n̂(θ, ϕ) where `·` is the usual dot product, and `n̂` is the unit vector in the given direction. Parameters ========== abd: AsymptoticBondiData The object storing the modes of the original data, which will be transformed in this function. This is the only required argument to this function. time_translation: float, optional Defaults to zero. Nonzero overrides corresponding components of `spacetime_translation` and `supertranslation` parameters. Note that this is the actual change in the coordinate value, rather than the corresponding mode weight (which is what `supertranslation` represents). space_translation : float array of length 3, optional Defaults to empty (no translation). Non-empty overrides corresponding components of `spacetime_translation` and `supertranslation` parameters. Note that this is the actual change in the coordinate value, rather than the corresponding mode weight (which is what `supertranslation` represents). spacetime_translation : float array of length 4, optional Defaults to empty (no translation). Non-empty overrides corresponding components of `supertranslation`. Note that this is the actual change in the coordinate value, rather than the corresponding mode weight (which is what `supertranslation` represents). supertranslation : complex array [defaults to 0] This gives the complex components of the spherical-harmonic expansion of the supertranslation in standard form, starting from ell=0 up to some ell_max, which may be different from the ell_max of the input `abd` object. Supertranslations must be real, so these values should obey the condition α^{ℓ,m} = (-1)^m ᾱ^{ℓ,-m} This condition is actually imposed on the input data, so imaginary parts of α(θ, ϕ) will essentially be discarded. Defaults to empty, which causes no supertranslation. Note that some components may be overridden by the parameters above. frame_rotation : quaternion [defaults to 1] Transformation applied to (x,y,z) basis of the input mode's inertial frame. For example, the basis z vector of the new frame may be written as z' = frame_rotation * z * frame_rotation.inverse() Defaults to 1, corresponding to the identity transformation (no rotation). boost_velocity : float array of length 3 [defaults to (0, 0, 0)] This is the three-velocity vector of the new frame relative to the input frame. The norm of this vector is required to be smaller than 1. output_ell_max: int [defaults to abd.ell_max] Maximum ell value in the output data. working_ell_max: int [defaults to 2 * abd.ell_max] Maximum ell value to use during the intermediate calculations. Rotations and time translations do not require this to be any larger than abd.ell_max, but other transformations will require more values of ell for accurate results. In particular, boosts are multiplied by time, meaning that a large boost of data with large values of time will lead to very large power in higher modes. Similarly, large (super)translations will couple power through a lot of modes. To avoid aliasing, this value should be large, to accomodate power in higher modes. Returns ------- abdprime: AsymptoticBondiData Object representing the transformed data. """ from quaternion import rotate_vectors from scipy.interpolate import CubicSpline # Parse the input arguments, and define the basic parameters for this function ( frame_rotation, boost_velocity, supertranslation, working_ell_max, output_ell_max, ) = _process_transformation_kwargs(self.ell_max, **kwargs) n_theta = 2 * working_ell_max + 1 n_phi = n_theta β = np.linalg.norm(boost_velocity) γ = 1 / math.sqrt(1 - β**2) # Make this into a Modes object, so it can keep track of its spin weight, etc., through the # various operations needed below. supertranslation = sf.Modes(supertranslation, spin_weight=0).real # This is a 2-d array of unit quaternions, which are what the spin-weighted functions should be # evaluated on (even for spin 0 functions, for simplicity). That will be equivalent to # evaluating the spin-weighted functions with respect to the transformed grid -- although on the # original time slices. distorted_grid_rotors = boosted_grid(frame_rotation, boost_velocity, n_theta, n_phi) # Compute u, α, ðα, ððα, k, ðk/k, 1/k, and 1/k³ on the distorted grid, including new axes to # enable broadcasting with time-dependent functions. Note that the first axis should represent # variation in u, the second axis variation in θ', and the third axis variation in ϕ'. u = self.u α = sf.Grid(supertranslation.evaluate(distorted_grid_rotors), spin_weight=0).real[np.newaxis, :, :] # The factors of 1/sqrt(2) and 1/2 come from using the GHP eth instead of the NP eth. ðα = sf.Grid(supertranslation.eth.evaluate(distorted_grid_rotors) / np.sqrt(2), spin_weight=α.s + 1)[np.newaxis, :, :] ððα = sf.Grid(0.5 * supertranslation.eth.eth.evaluate(distorted_grid_rotors), spin_weight=α.s + 2)[np.newaxis, :, :] k, ðk_over_k, one_over_k, one_over_k_cubed = conformal_factors( boost_velocity, distorted_grid_rotors) # ðu'(u, θ', ϕ') exp(iλ) / k(θ', ϕ') ðuprime_over_k = ðk_over_k * (u - α) - ðα # ψ0(u, θ', ϕ') exp(2iλ) ψ0 = sf.Grid(self.psi0.evaluate(distorted_grid_rotors), spin_weight=2) # ψ1(u, θ', ϕ') exp(iλ) ψ1 = sf.Grid(self.psi1.evaluate(distorted_grid_rotors), spin_weight=1) # ψ2(u, θ', ϕ') ψ2 = sf.Grid(self.psi2.evaluate(distorted_grid_rotors), spin_weight=0) # ψ3(u, θ', ϕ') exp(-1iλ) ψ3 = sf.Grid(self.psi3.evaluate(distorted_grid_rotors), spin_weight=-1) # ψ4(u, θ', ϕ') exp(-2iλ) ψ4 = sf.Grid(self.psi4.evaluate(distorted_grid_rotors), spin_weight=-2) # σ(u, θ', ϕ') exp(2iλ) σ = sf.Grid(self.sigma.evaluate(distorted_grid_rotors), spin_weight=2) ### The following calculations are done using in-place Horner form. I suspect this will be the ### most efficient form of this calculation, within reason. Note that the factors of exp(isλ) ### were computed automatically by evaluating in terms of quaternions. # fprime_of_timenaught_directionprime = np.empty( (6, self.n_times, n_theta, n_phi), dtype=complex) # ψ0'(u, θ', ϕ') fprime_temp = ψ4.copy() fprime_temp *= ðuprime_over_k fprime_temp += -4 * ψ3 fprime_temp *= ðuprime_over_k fprime_temp += 6 * ψ2 fprime_temp *= ðuprime_over_k fprime_temp += -4 * ψ1 fprime_temp *= ðuprime_over_k fprime_temp += ψ0 fprime_temp *= one_over_k_cubed fprime_of_timenaught_directionprime[0] = fprime_temp # ψ1'(u, θ', ϕ') fprime_temp = -ψ4 fprime_temp *= ðuprime_over_k fprime_temp += 3 * ψ3 fprime_temp *= ðuprime_over_k fprime_temp += -3 * ψ2 fprime_temp *= ðuprime_over_k fprime_temp += ψ1 fprime_temp *= one_over_k_cubed fprime_of_timenaught_directionprime[1] = fprime_temp # ψ2'(u, θ', ϕ') fprime_temp = ψ4.copy() fprime_temp *= ðuprime_over_k fprime_temp += -2 * ψ3 fprime_temp *= ðuprime_over_k fprime_temp += ψ2 fprime_temp *= one_over_k_cubed fprime_of_timenaught_directionprime[2] = fprime_temp # ψ3'(u, θ', ϕ') fprime_temp = -ψ4 fprime_temp *= ðuprime_over_k fprime_temp += ψ3 fprime_temp *= one_over_k_cubed fprime_of_timenaught_directionprime[3] = fprime_temp # ψ4'(u, θ', ϕ') fprime_temp = ψ4.copy() fprime_temp *= one_over_k_cubed fprime_of_timenaught_directionprime[4] = fprime_temp # σ'(u, θ', ϕ') fprime_temp = σ.copy() fprime_temp -= ððα fprime_temp *= one_over_k fprime_of_timenaught_directionprime[5] = fprime_temp # Determine the new time slices. The set timeprime is chosen so that on each slice of constant # u'_i, the average value of u=(u'/k)+α is precisely <u>=u'γ+<α>=u_i. But then, we have to # narrow that set down, so that every grid point on all the u'_i' slices correspond to data in # the range of input data. timeprime = (u - sf.constant_from_ell_0_mode(supertranslation[0]).real) / γ timeprime_of_initialtime_directionprime = k * (u[0] - α) timeprime_of_finaltime_directionprime = k * (u[-1] - α) earliest_complete_timeprime = np.max( timeprime_of_initialtime_directionprime.view(np.ndarray)) latest_complete_timeprime = np.min( timeprime_of_finaltime_directionprime.view(np.ndarray)) timeprime = timeprime[(timeprime >= earliest_complete_timeprime) & (timeprime <= latest_complete_timeprime)] # This will store the values of f'(u', θ', ϕ') for the various functions `f` fprime_of_timeprime_directionprime = np.zeros( (6, timeprime.size, n_theta, n_phi), dtype=complex) # Interpolate the various transformed function values on the transformed grid from the original # time coordinate to the new set of time coordinates, independently for each direction. for i in range(n_theta): for j in range(n_phi): k_i_j = k[0, i, j] α_i_j = α[0, i, j] # u'(u, θ', ϕ') timeprime_of_timenaught_directionprime_i_j = k_i_j * (u - α_i_j) # f'(u', θ', ϕ') fprime_of_timeprime_directionprime[:, :, i, j] = CubicSpline( timeprime_of_timenaught_directionprime_i_j, fprime_of_timenaught_directionprime[:, :, i, j], axis=1)(timeprime) # Finally, transform back from the distorted grid to the SWSH mode weights as measured in that # grid. I'll abuse notation slightly here by indicating those "distorted" mode weights with # primes, so that f'(u')_{ℓ', m'} = ∫ f'(u', θ', ϕ') sȲ_{ℓ', m'}(θ', ϕ') sin(θ') dθ' dϕ' abdprime = type(self)(timeprime, output_ell_max) # ψ0'(u')_{ℓ', m'} abdprime.psi0 = spinsfast.map2salm(fprime_of_timeprime_directionprime[0], 2, output_ell_max) # ψ1'(u')_{ℓ', m'} abdprime.psi1 = spinsfast.map2salm(fprime_of_timeprime_directionprime[1], 1, output_ell_max) # ψ2'(u')_{ℓ', m'} abdprime.psi2 = spinsfast.map2salm(fprime_of_timeprime_directionprime[2], 0, output_ell_max) # ψ3'(u')_{ℓ', m'} abdprime.psi3 = spinsfast.map2salm(fprime_of_timeprime_directionprime[3], -1, output_ell_max) # ψ4'(u')_{ℓ', m'} abdprime.psi4 = spinsfast.map2salm(fprime_of_timeprime_directionprime[4], -2, output_ell_max) # σ'(u')_{ℓ', m'} abdprime.sigma = spinsfast.map2salm(fprime_of_timeprime_directionprime[5], 2, output_ell_max) return abdprime
# Fill the alm with white noise seed(3124432) for l in range(abs(s),lmax+1) : for m in range(-l,l+1) : i = spinsfast.lm_ind(l,m,lmax) if (m==0) : alm[i] = normal() else : alm[i] = normal()/2 + 1j*normal()/2 f = spinsfast.salm2map(alm,s,lmax,Ntheta,Nphi) # In this pixelization, access the map with f[itheta,iphi] # where 0 <= itheta <= Ntheta-1 and 0<= iphi <= Nphi-1 # and theta = pi*itheta/(Ntheta-1) phi = 2*pi*iphi/Nphi alm2 = spinsfast.map2salm(f,s,lmax) diff_max = max((alm-alm2)) print("max(alm2-alm) = "+str(diff_max)) figure() imshow(f.real,interpolation='nearest') colorbar() title("Real part of f") xlabel("iphi") ylabel("itheta") show()
def transform_moreschi_supermomentum(supermomentum, **kwargs): """Apply a BMS transformation to the Moreschi supermomentum using the Moreschi formula, Eq. (9) of arXiv:gr-qc/0203075. NOTE: This transformation only holds for the Moreschi supermomentum! It is important to note that the input transformation parameters are applied in this order: 1. (Super)Translations 2. Rotation (about the origin) 3. Boost (about the origin) All input parameters refer to the transformation required to take the input data's inertial frame onto the inertial frame of the output data's inertial observers. In what follows, the coordinates of and functions in the input inertial frame will be unprimed, while corresponding values of the output inertial frame will be primed. The translations (space, time, spacetime, or super) can be given in various ways, which may override each other. Ultimately, however, they are essentially combined into a single function `α`, representing the supertranslation, which transforms the asymptotic time variable `u` as u'(u, θ, ϕ) = u(u, θ, ϕ) - α(θ, ϕ) A simple time translation by δt would correspond to α(θ, ϕ) = δt # Independent of (θ, ϕ) A pure spatial translation δx would correspond to α(θ, ϕ) = -δx · n̂(θ, ϕ) where `·` is the usual dot product, and `n̂` is the unit vector in the given direction. Parameters ---------- supermomentum: ModesTimeSeries The object storing the modes of the original data, which will be transformed in this function. This is the only required argument to this function. time_translation: float, optional Defaults to zero. Nonzero overrides corresponding components of `spacetime_translation` and `supertranslation` parameters. Note that this is the actual change in the coordinate value, rather than the corresponding mode weight (which is what `supertranslation` represents). space_translation : float array of length 3, optional Defaults to empty (no translation). Non-empty overrides corresponding components of `spacetime_translation` and `supertranslation` parameters. Note that this is the actual change in the coordinate value, rather than the corresponding mode weight (which is what `supertranslation` represents). spacetime_translation : float array of length 4, optional Defaults to empty (no translation). Non-empty overrides corresponding components of `supertranslation`. Note that this is the actual change in the coordinate value, rather than the corresponding mode weight (which is what `supertranslation` represents). supertranslation : complex array [defaults to 0] This gives the complex components of the spherical-harmonic expansion of the supertranslation in standard form, starting from ell=0 up to some ell_max, which may be different from the ell_max of the input `supermomentum`. Supertranslations must be real, so these values should obey the condition α^{ℓ,m} = (-1)^m ᾱ^{ℓ,-m} This condition is actually imposed on the input data, so imaginary parts of α(θ, ϕ) will essentially be discarded. Defaults to empty, which causes no supertranslation. Note that some components may be overridden by the parameters above. frame_rotation : quaternion [defaults to 1] Transformation applied to (x,y,z) basis of the input mode's inertial frame. For example, the basis z vector of the new frame may be written as z' = frame_rotation * z * frame_rotation.inverse() Defaults to 1, corresponding to the identity transformation (no rotation). boost_velocity : float array of length 3 [defaults to (0, 0, 0)] This is the three-velocity vector of the new frame relative to the input frame. The norm of this vector is required to be smaller than 1. output_ell_max: int [defaults to supermomentum.ell_max] Maximum ell value in the output data. working_ell_max: int [defaults to 2 * supermomentum.ell_max] Maximum ell value to use during the intermediate calculations. Rotations and time translations do not require this to be any larger than supermomentum.ell_max, but other transformations will require more values of ell for accurate results. In particular, boosts are multiplied by time, meaning that a large boost of data with large values of time will lead to very large power in higher modes. Similarly, large (super)translations will couple power through a lot of modes. To avoid aliasing, this value should be large, to accomodate power in higher modes. Returns ------- ModesTimeSeries """ from quaternion import rotate_vectors from scipy.interpolate import CubicSpline import spherical_functions as sf import spinsfast import math from .transformations import _process_transformation_kwargs, boosted_grid, conformal_factors from ..modes_time_series import ModesTimeSeries # Parse the input arguments, and define the basic parameters for this function frame_rotation, boost_velocity, supertranslation, working_ell_max, output_ell_max, = _process_transformation_kwargs( supermomentum.ell_max, **kwargs ) n_theta = 2 * working_ell_max + 1 n_phi = n_theta β = np.linalg.norm(boost_velocity) γ = 1 / math.sqrt(1 - β ** 2) # Make this into a Modes object, so it can keep track of its spin weight, etc., through the # various operations needed below. supertranslation = sf.Modes(supertranslation, spin_weight=0).real # This is a 2-d array of unit quaternions, which are what the spin-weighted functions should be # evaluated on (even for spin 0 functions, for simplicity). That will be equivalent to # evaluating the spin-weighted functions with respect to the transformed grid -- although on the # original time slices. distorted_grid_rotors = boosted_grid(frame_rotation, boost_velocity, n_theta, n_phi) # Compute u, α, Δα, k, ðk/k, 1/k, and 1/k³ on the distorted grid, including new axes to # enable broadcasting with time-dependent functions. Note that the first axis should represent # variation in u, the second axis variation in θ', and the third axis variation in ϕ'. u = supermomentum.u α = sf.Grid(supertranslation.evaluate(distorted_grid_rotors), spin_weight=0).real[np.newaxis, :, :] # The factor of 0.25 comes from using the GHP eth instead of the NP eth. Δα = sf.Grid(0.25 * supertranslation.ethbar.ethbar.eth.eth.evaluate(distorted_grid_rotors), spin_weight=α.s)[ np.newaxis, :, : ] k, ðk_over_k, one_over_k, one_over_k_cubed = conformal_factors(boost_velocity, distorted_grid_rotors) # Ψ(u, θ', ϕ') exp(2iλ) Ψ = sf.Grid(supermomentum.evaluate(distorted_grid_rotors), spin_weight=0) ### The following calculations are done using in-place Horner form. I suspect this will be the ### most efficient form of this calculation, within reason. Note that the factors of exp(isλ) ### were computed automatically by evaluating in terms of quaternions. # # Ψ'(u, θ', ϕ') = k⁻³ (Ψ - ð²ðbar²α) Ψprime_of_timenaught_directionprime = Ψ.copy() - Δα Ψprime_of_timenaught_directionprime *= one_over_k_cubed # Determine the new time slices. The set timeprime is chosen so that on each slice of constant # u'_i, the average value of u=(u'/k)+α is precisely <u>=u'γ+<α>=u_i. But then, we have to # narrow that set down, so that every grid point on all the u'_i' slices correspond to data in # the range of input data. timeprime = (u - sf.constant_from_ell_0_mode(supertranslation[0]).real) / γ timeprime_of_initialtime_directionprime = k * (u[0] - α) timeprime_of_finaltime_directionprime = k * (u[-1] - α) earliest_complete_timeprime = np.max(timeprime_of_initialtime_directionprime.view(np.ndarray)) latest_complete_timeprime = np.min(timeprime_of_finaltime_directionprime.view(np.ndarray)) timeprime = timeprime[(timeprime >= earliest_complete_timeprime) & (timeprime <= latest_complete_timeprime)] # This will store the values of Ψ'(u', θ', ϕ') Ψprime_of_timeprime_directionprime = np.zeros((timeprime.size, n_theta, n_phi), dtype=complex) # Interpolate the various transformed function values on the transformed grid from the original # time coordinate to the new set of time coordinates, independently for each direction. for i in range(n_theta): for j in range(n_phi): k_i_j = k[0, i, j] α_i_j = α[0, i, j] # u'(u, θ', ϕ') timeprime_of_timenaught_directionprime_i_j = k_i_j * (u - α_i_j) # Ψ'(u', θ', ϕ') Ψprime_of_timeprime_directionprime[:, i, j] = CubicSpline( timeprime_of_timenaught_directionprime_i_j, Ψprime_of_timenaught_directionprime[:, i, j], axis=0 )(timeprime) # Finally, transform back from the distorted grid to the SWSH mode weights as measured in that # grid. I'll abuse notation slightly here by indicating those "distorted" mode weights with # primes, so that Ψ'(u')_{ℓ', m'} = ∫ Ψ'(u', θ', ϕ') sȲ_{ℓ', m'}(θ', ϕ') sin(θ') dθ' dϕ' supermomentum_prime = spinsfast.map2salm(Ψprime_of_timeprime_directionprime, 0, output_ell_max) supermomentum_prime = ModesTimeSeries( sf.SWSH_modes.Modes( supermomentum_prime, spin_weight=0, ell_min=0, ell_max=output_ell_max, multiplication_truncator=max ), time=timeprime, ) return supermomentum_prime
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
Nlm = spinsfast.N_lm(lmax) alm = zeros(Nlm, dtype=complex) # Fill the alm with white noise seed(3124432) for l in range(abs(s), lmax + 1): for m in range(-l, l + 1): i = spinsfast.lm_ind(l, m, lmax) if (m == 0): alm[i] = normal() else: alm[i] = normal() / 2 + 1j * normal() / 2 f = spinsfast.salm2map(alm, s, lmax, Ntheta, Nphi) # In this pixelization, access the map with f[itheta,iphi] # where 0 <= itheta <= Ntheta-1 and 0<= iphi <= Nphi-1 # and theta = pi*itheta/(Ntheta-1) phi = 2*pi*iphi/Nphi alm2 = spinsfast.map2salm(f, s, lmax) diff_max = max((alm - alm2)) print("max(alm2-alm) = " + str(diff_max)) figure() imshow(f.real, interpolation='nearest') colorbar() title("Real part of f") xlabel("iphi") ylabel("itheta") show()