def test_ndarray_args(): def f1(a, b, c): d = np.asarray(a).copy() assert isinstance( a, np.ndarray) and not isinstance(a, quaternionic.array) assert isinstance( b, np.ndarray) and not isinstance(b, quaternionic.array) assert isinstance( c, np.ndarray) and not isinstance(c, quaternionic.array) assert isinstance( d, np.ndarray) and not isinstance(d, quaternionic.array) return d a = quaternionic.array(np.random.rand(17, 3, 4)) b = quaternionic.array(np.random.rand(13, 3, 4)) c = quaternionic.array(np.random.rand(11, 3, 4)) f2 = quaternionic.utilities.ndarray_args(f1) d2 = f2(a, b, c) assert isinstance(d2, np.ndarray) and not isinstance(d2, quaternionic.array) f1.nin = 3 f3 = quaternionic.utilities.ndarray_args(f1) d3 = f3(a, b, c) assert isinstance(d3, np.ndarray) and not isinstance(d3, quaternionic.array)
def test_Wigner_D_underflow(Rs, ell_max, eps): # NOTE: This is a delicate test, which depends on the result underflowing exactly when expected. # In particular, it should underflow to 0.0 when |mp+m|>32, but should never underflow to 0.0 # when |mp+m|<32. So it's not the end of the world if this test fails, but it does mean that # the underflow properties have changed, so it might be worth a look. epsilon = 1.e-10 ϵ = 5 * ell_max * eps D = np.zeros(sf.WignerDsize(0, ell_max), dtype=complex) wigner = sf.Wigner(ell_max) ell_mp_m = sf.WignerDrange(0, ell_max) # Test |Ra|=1e-10 R = quaternionic.array(epsilon, 1, 0, 0).normalized wigner.D(R, out=D) # print(R.to_euler_angles.tolist()) # print(D.tolist()) non_underflowing_indices = np.abs(ell_mp_m[:, 1] + ell_mp_m[:, 2]) < 32 assert np.all(D[non_underflowing_indices] != 0j) underflowing_indices = np.abs(ell_mp_m[:, 1] + ell_mp_m[:, 2]) > 32 assert np.all(D[underflowing_indices] == 0j) # Test |Rb|=1e-10 R = quaternionic.array(1, epsilon, 0, 0).normalized wigner.D(R, out=D) non_underflowing_indices = np.abs(ell_mp_m[:, 1] - ell_mp_m[:, 2]) < 32 assert np.all(D[non_underflowing_indices] != 0j) underflowing_indices = np.abs(ell_mp_m[:, 1] - ell_mp_m[:, 2]) > 32 assert np.all(D[underflowing_indices] == 0j)
def test_pyguvectorize(): _quaternion_resolution = 10 * np.finfo(float).resolution np.random.seed(1234) one = quaternionic.array(1, 0, 0, 0) x = quaternionic.array(np.random.rand(7, 13, 4)) y = quaternionic.array(np.random.rand(13, 4)) z = np.random.rand(13) arg0s = [one, -(1 + 2 * _quaternion_resolution) * one, -one, x] for k in dir(quaternionic.algebra_ufuncs): if not k.startswith('__'): f1 = getattr(quaternionic.algebra_ufuncs, k) f2 = getattr(quaternionic.algebra, k) sig = f2.signature inputs = sig.split('->')[0].split(',') for arg0 in arg0s: args = [arg0.ndarray] if inputs[0] == '(n)' else [ z, ] if len(inputs) > 1: args.append(y.ndarray if inputs[1] == '(n)' else z) assert np.allclose(f1(*args), quaternionic.utilities.pyguvectorize( f2.types, f2.signature)(f2)(*args), atol=0.0, rtol=_quaternion_resolution)
def test_Wigner_D_non_overflow(ell_max): D = np.zeros(sf.WignerDsize(0, ell_max), dtype=complex) wigner = sf.Wigner(ell_max) # Test |Ra|=1e-10 R = quaternionic.array(1.e-10, 1, 0, 0).normalized assert np.all(np.isfinite(wigner.D(R, out=D))) # Test |Rb|=1e-10 R = quaternionic.array(1, 1.e-10, 0, 0).normalized assert np.all(np.isfinite(wigner.D(R, out=D)))
def Rs(): np.random.seed(1842) ones = [0, -1., 1.] rs = [[w, x, y, z] for w in ones for x in ones for y in ones for z in ones][1:] rs = rs + [ r for r in [ quaternionic.array(np.random.uniform(-1, 1, size=4)) for _ in range(20) ] ] return quaternionic.array(rs).normalized
def test_basis_multiplication(): # Basis components one, i, j, k = tuple(quaternionic.array(np.eye(4))) # Full multiplication table assert one * one == one assert one * i == i assert one * j == j assert one * k == k assert i * one == i assert i * i == np.negative(one) assert i * j == k assert i * k == -j assert j * one == j assert j * i == -k assert j * j == -one assert j * k == i assert k * one == k assert k * i == j assert k * j == -i assert k * k == -one # Standard expressions assert one * one == one assert i * i == -one assert j * j == -one assert k * k == -one assert i * j * k == -one
def __init__(self , xyz = (0,0,0)): self.position = np.asarray(xyz[0:3], dtype=np.float32) self.quaternion = quat.array((1,0,0,0)) #quaternion identity self.pitch = 0 self.yaw = 0 self.scale = np.ones((1,3) , dtype = np.float32) #scale identity self.forward = np.asarray([0,0,-1], dtype=np.float32)
def test_wigner_evaluate(horner, ell_max_slow, eps): import time ell_max = max(3, ell_max_slow) np.random.seed(1234) ϵ = 10 * (2 * ell_max + 1) * eps n_theta = n_phi = 2 * ell_max + 1 max_s = 2 wigner = sf.Wigner(ell_max, mp_max=max_s) max_error = 0.0 total_time = 0.0 for rotors in [ quaternionic.array.from_spherical_coordinates( sf.theta_phi(n_theta, n_phi)), quaternionic.array(np.random.rand(n_theta, n_phi, 4)).normalized ]: for s in range(-max_s, max_s + 1): ell_min = abs(s) a1 = np.random.rand(7, sf.Ysize(ell_min, ell_max) * 2).view(complex) a1[:, sf.Yindex(ell_min, -ell_min, ell_min):sf. Yindex(abs(s), -abs(s), ell_min)] = 0.0 m1 = sf.Modes(a1, spin_weight=s, ell_min=ell_min, ell_max=ell_max) t1 = time.perf_counter() f1 = wigner.evaluate(m1, rotors, horner=horner) t2 = time.perf_counter() assert f1.shape == m1.shape[:-1] + rotors.shape[:-1] # print(f"Evaluation for s={s} took {t2-t1:.4f} seconds") # print(f1.shape) sYlm = np.zeros((sf.Ysize(0, ell_max), ) + rotors.shape[:-1], dtype=complex) for i, Rs in enumerate(rotors): for j, R in enumerate(Rs): wigner.sYlm(s, R, out=sYlm[:, i, j]) f2 = np.tensordot(m1.view(np.ndarray), sYlm, axes=([-1], [0])) assert f2.shape == m1.shape[:-1] + rotors.shape[:-1] assert np.allclose(f1, f2, rtol=ϵ, atol=ϵ), ( f"max|f1-f2|={np.max(np.abs(f1-f2))} > ϵ={ϵ}\n\n" f"s = {s}\n\nrotors = {rotors.tolist()}\n\n" # f"f1 = {f1.tolist()}\n\nf2 = {f2.tolist()}" ) max_error = max(np.max(np.abs(f1 - f2)), max_error) total_time += t2 - t1 print() print(f"\tmax_error[{horner}] = {max_error}") print(f"\ttotal_time[{horner}] = {total_time}")
def test_sYlm_spin_behavior(Rs, special_angles, ell_max_slow, eps): # We expect that the SWSHs behave according to # sYlm( R * exp(gamma*z/2) ) = sYlm(R) * exp(-1j*s*gamma) # See http://moble.github.io/spherical/SWSHs.html#fn:2 # for a more detailed explanation # print("") ϵ = 2 * ell_max_slow * eps wigner = sf.Wigner(ell_max_slow) for i, R in enumerate(Rs): # print("\t{0} of {1}: R = {2}".format(i, len(Rs), R)) for gamma in special_angles: Rgamma = R * quaternionic.array(math.cos(gamma / 2.), 0, 0, math.sin(gamma / 2.)) for s in range(-ell_max_slow, ell_max_slow + 1): Y1 = wigner.sYlm(s, Rgamma) Y2 = wigner.sYlm(s, R) * cmath.exp(-1j * s * gamma) assert np.allclose(Y1, Y2, atol=ϵ, rtol=ϵ)
def test_getting_components(): q = quaternionic.array([1, 2, 3, 4]) # Note the integer input assert q.w == 1.0 assert q.x == 2.0 assert q.y == 3.0 assert q.z == 4.0 assert q.scalar == 1.0 assert np.array_equal(q.vector, [2.0, 3.0, 4.0]) assert q.i == 2.0 assert q.j == 3.0 assert q.k == 4.0 assert q.real == 1.0 assert np.array_equal(q.imag, [2.0, 3.0, 4.0])
def quaternion_sampler(): Qs_array = quaternionic.array([ [np.nan, 0., 0., 0.], [np.inf, 0., 0., 0.], [-np.inf, 0., 0., 0.], [0., 0., 0., 0.], [1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.], [1.1, 2.2, 3.3, 4.4], [-1.1, -2.2, -3.3, -4.4], [1.1, -2.2, -3.3, -4.4], [ 0.18257418583505537115232326093360, 0.36514837167011074230464652186720, 0.54772255750516611345696978280080, 0.73029674334022148460929304373440 ], [ 1.7959088706354, 0.515190292664085, 0.772785438996128, 1.03038058532817 ], [ 2.81211398529184, -0.392521193481878, -0.588781790222817, -0.785042386963756 ], ]) names = type("QNames", (object, ), dict())() names.q_nan1 = 0 names.q_inf1 = 1 names.q_minf1 = 2 names.q_0 = 3 names.q_1 = 4 names.x = 5 names.y = 6 names.z = 7 names.Q = 8 names.Qneg = 9 names.Qbar = 10 names.Qnormalized = 11 names.Qlog = 12 names.Qexp = 13 return Qs_array, names
def test_setting_components(): q = quaternionic.array([1, 2, 3, 4]) # Note the integer input q.w = 5 q.x = 6 q.y = 7 q.z = 8 assert np.array_equal(q.ndarray, [5.0, 6.0, 7.0, 8.0]) q.scalar = 1 q.vector = [2, 3, 4] assert np.array_equal(q.ndarray, [1.0, 2.0, 3.0, 4.0]) q.w = 5 q.i = 6 q.j = 7 q.k = 8 assert np.array_equal(q.ndarray, [5.0, 6.0, 7.0, 8.0]) q.real = 1 q.imag = [2, 3, 4] assert np.array_equal(q.ndarray, [1.0, 2.0, 3.0, 4.0])
def to_sxs(self): """Convert this object to an `sxs.WaveformModes` object Note that the resulting object will likely contain references to the same underlying data contained in the original object; modifying one will modify the other. You can make a copy of this object *before* calling this function — using code like `w.copy().to_sxs` — to obtain separate data. """ import sxs import quaternionic import quaternion from .extrapolation import extrapolate # All of these will be stored in the `_metadata` member of the resulting WaveformModes # object; most of these will also be accessible directly as attributes. kwargs = dict( time=self.t, time_axis=0, modes_axis=1, #frame=, # see below spin_weight=self.spin_weight, data_type=self.data_type_string.lower(), frame_type=self.frame_type_string.lower(), history=self.history, version_hist=self.version_hist, r_is_scaled_out=self.r_is_scaled_out, m_is_scaled_out=self.m_is_scaled_out, ell_min=self.ell_min, ell_max=self.ell_max, ) # If self.frame.size==0, we just don't pass any argument if self.frame.size == 1: kwargs["frame"] = quaternionic.array( [quaternion.as_float_array(self.frame)]) elif self.frame.size == self.n_times: kwargs["frame"] = quaternionic.array( quaternion.as_float_array(self.frame)) elif self.frame.size > 0: raise ValueError( f"Frame size ({self.frame.size}) should be 0, 1, or " f"equal to the number of time steps ({self.n_times})") w = sxs.WaveformModes(self.data, **kwargs) # Special cases for extrapolate_coord_radii and translation/boost if hasattr(self, "extrapolate_coord_radii"): w.register_modification( extrapolate, CoordRadii=list(self.extrapolate_coord_radii), ) if hasattr(self, "space_translation") or hasattr( self, "boost_velocity"): w.register_modification( self.transform, space_translation=list( getattr(self, "space_translation", [0., 0., 0.])), boost_velocity=list( getattr(self, "boost_velocity", [0., 0., 0.])), ) return w
def rotate(modes, R): """Rotate Modes object by rotor(s) Compute fₗₘ = Σₙ fₗₙ 𝔇ˡₙₘ(R), where f is a (possibly spin-weighted) function, fₗₙ are its mode weights in the current frame, and fₗₘ are its mode weights in the rotated frame. fₗₘ = Σₙ fₗₙ 𝔇ˡₙₘ(R) = Σₙ fₗₙ dˡₙₘ(R) exp[iϕₐ(m-n)+iϕₛ(m+n)] = Σₙ fₗₙ dˡₙₘ(R) exp[i(ϕₛ+ϕₐ)m+i(ϕₛ-ϕₐ)n] = exp[i(ϕₛ+ϕₐ)m] Σₙ fₗₙ dˡₙₘ(R) exp[i(ϕₛ-ϕₐ)n] = zₚᵐ Σₙ fₗₙ dˡₙₘ(R) zₘⁿ = zₚᵐ {fₗ₀ dˡ₀ₘ(R) + Σₚₙ [fₗₙ dˡₙₘ(R) zₘⁿ + fₗ₋ₙ dˡ₋ₙₘ(R) / zₘⁿ]} = zₚᵐ {fₗ₀ ϵ₋ₘ Hˡ₀ₘ(R) + Σₚₙ [fₗₙ ϵₙ ϵ₋ₘ Hˡₙₘ(R) zₘⁿ + fₗ₋ₙ ϵ₋ₙ ϵ₋ₘ Hˡ₋ₙₘ(R) / zₘⁿ]} = ϵ₋ₘ zₚᵐ {fₗ₀ Hˡ₀ₘ(R) + Σₚₙ [fₗₙ (-1)ⁿ Hˡₙₘ(R) zₘⁿ + fₗ₋ₙ Hˡ₋ₙₘ(R) / zₘⁿ]} Here, n ranges over [-l, l] and pn ranges over [1, l]. Parameters ========== modes: Modes SWSH modes to rotate R: quaternionic.array Its shape must satifsy R.shape[:-1] == modes.shape[:-1] """ import quaternionic fₗₙ = modes.reshape((np.prod(modes.shape[:-1], dtype=int), modes.shape[-1])) fₗₘ = np.zeros_like(modes) ell_min = modes.ell_min ell_max = modes.ell_max if not isinstance(R, quaternionic.array) and R.shape != modes.shape[:-1]: raise ValueError( f"Input rotor must be either a single quaternion or an array with\n" f"shape R.shape={R.shape} == modes.shape[:-1]={modes.shape[:-1]}" ) rotors = quaternionic.array(R).ndarray.reshape((-1, 4)) #unfinished: raise NotImplementedError() #first_slice = slice(None) if rotors.shape[0]==1 else ? for iᵣ in range(rotors.shape[0]): zᵦ, zₚ, zₘ = quaternion_angles(rotors[i]) # Compute H elements (basically Wigner's d functions) Hˡₙₘ = H(zᵦ.real, zᵦ.imag, workspace) # Pre-compute zₚᵐ=exp[i(ϕₛ+ϕₐ)m] for all values of m zₚᵐ = complex_powers(zₚ, ell_max) for ell in range(ell_min, ell_max+1): for m in range(-ell_max, ell_max+1): # fₗₘ = ϵ₋ₘ zₚᵐ {fₗ₀ Hˡ₀ₘ(R) + Σₚₙ [fₗ₋ₙ Hˡ₋ₙₘ(R) / zₘⁿ + fₗₙ (-1)ⁿ Hˡₙₘ(R) zₘⁿ]} iₘ = fₗₘ.index(ell, m) # Initialize with n=0 term fₗₘ[first_slice, iₘ] = fₗₙ[fₗₙ.index(ell, 0)] * Hˡₙₘ.element(ell, 0, m) # Compute dˡₙₘ terms recursively for 0<n<l, using symmetries for negative n, and # simultaneously add the mode weights times zₘⁿ=exp[i(ϕₛ-ϕₐ)n] to the result using # Horner form negative_terms[first_slice] = fₗₙ[first_slice, fₗₙ.index(ell, -ell)] * Hˡₙₘ.element(ell, -ell, m) positive_terms[first_slice] = fₗₙ[first_slice, fₗₙ.index(ell, ell)] * Hˡₙₘ.element(ell, ell, m) * (-1)**ell for n in range(ell-1, 0, -1): negative_terms /= zₘ negative_terms += fₗₙ[first_slice, fₗₙ.index(ell, -n)] * Hˡₙₘ.element(ell, -n, m) positive_terms *= zₘ positive_terms += fₗₙ[first_slice, fₗₙ.index(ell, n)] * Hˡₙₘ.element(ell, n, m) * (-1)**n fₗₘ[first_slice, iₘ] += negative_terms / zₘ fₗₘ[first_slice, iₘ] += positive_terms * zₘ # Finish calculation of fₗₘ by multiplying by zₚᵐ=exp[i(ϕₛ+ϕₐ)m] fₗₘ[first_slice, iₘ] *= ϵ(-m) * zₚᵐ[m] fₗₘ = fₗₘ.reshape(modes.shape) return fₗₘ
def prepare_unpickle(self): self.player.transform.quaternion = quat.array(self.player.transform.quaternion) for e in self.entities: e.transform.quaternion = quat.array(e.transform.quaternion)
def D(self, R, out=None, workspace=None): """Compute Wigner's 𝔇 matrix Parameters ---------- R : array_like Array to be interpreted as a quaternionic array (thus its final dimension must have size 4), representing the rotations on which the 𝔇 matrix will be evaluated. out : array_like, optional Array into which the 𝔇 values should be written. It should be an array of complex, with size `self.Dsize`. If not present, the array will be created. In either case, the array will also be returned. workspace : array_like, optional A working array like the one returned by Wigner.new_workspace(). If not present, this object's default workspace will be used. Note that it is not safe to use the same workspace on multiple threads. Returns ------- D : array This is a 1-dimensional array of complex; see below. See Also -------- H : Compute a portion of the H matrix d : Compute the full Wigner d matrix rotate : Avoid computing the full 𝔇 matrix and rotate modes directly evaluate : Avoid computing the full 𝔇 matrix and evaluate modes directly Notes ----- This function is the preferred method of computing the 𝔇 matrix for large ell values. In particular, above ell≈32 standard formulas become completely unusable because of numerical instabilities and overflow. This function uses stable recursion methods instead, and should be usable beyond ell≈1000. This function computes 𝔇ˡₘₚ,ₘ(R). The result is returned in a 1-dimensional array ordered as [ 𝔇(ell, mp, m, R) for ell in range(ell_max+1) for mp in range(-min(ℓ, mp_max), min(ℓ, mp_max)+1) for m in range(-ell, ell+1) ] """ if self.mp_max < self.ell_max: raise ValueError( f"Cannot compute full 𝔇 matrix up to ell_max={self.ell_max} if mp_max is only {self.mp_max}" ) if out is not None and out.size != (self.Dsize * R.size // 4): raise ValueError( f"Given output array has size {out.size}; it should be {self.Dsize * R.size // 4}" ) if out is not None and out.dtype != complex: raise ValueError(f"Given output array has dtype {out.dtype}; it should be complex") if workspace is not None: Hwedge, Hv, Hextra, zₐpowers, zᵧpowers, z = self._split_workspace(workspace) else: Hwedge, Hv, Hextra, zₐpowers, zᵧpowers, z = ( self.Hwedge, self.Hv, self.Hextra, self.zₐpowers, self.zᵧpowers, self.z ) quaternions = quaternionic.array(R).ndarray.reshape((-1, 4)) function_values = ( out.reshape(quaternions.shape[0], self.Dsize) if out is not None else np.zeros(quaternions.shape[:-1] + (self.Dsize,), dtype=complex) ) # Loop over all input quaternions for i_R in range(quaternions.shape[0]): to_euler_phases(quaternions[i_R], z) Hwedge = self.H(z[1], Hwedge, Hv, Hextra) 𝔇 = function_values[i_R] _complex_powers(z[0:1], self.ell_max, zₐpowers) _complex_powers(z[2:3], self.ell_max, zᵧpowers) _fill_wigner_D(self.ell_min, self.ell_max, self.mp_max, 𝔇, Hwedge, zₐpowers[0], zᵧpowers[0]) return function_values.reshape(R.shape[:-1] + (self.Dsize,))
def test_rotate_vectors(Rs): one, x, y, z = tuple(quaternionic.array(np.eye(4))) zero = 0.0 * one with pytest.raises(ValueError): one.rotate(np.array(3.14)) with pytest.raises(ValueError): one.rotate(np.random.rand(17, 9, 4)) with pytest.raises(ValueError): one.rotate(np.random.rand(17, 9, 3), axis=1) np.random.seed(1234) # Test (1)*(1) vecs = np.random.rand(3) quats = z vecsprime = quats.rotate(vecs) assert np.allclose(vecsprime, (quats * quaternionic.array(0, *vecs) * quats.inverse).vector, rtol=0.0, atol=0.0) assert quats.shape[:-1] + vecs.shape == vecsprime.shape, ("Out of shape!", quats.shape, vecs.shape, vecsprime.shape) # Test (1)*(5) vecs = np.random.rand(5, 3) quats = z vecsprime = quats.rotate(vecs) for i, vec in enumerate(vecs): assert np.allclose(vecsprime[i], (quats * quaternionic.array(0, *vec) * quats.inverse).vector, rtol=0.0, atol=0.0) assert quats.shape[:-1] + vecs.shape == vecsprime.shape, ("Out of shape!", quats.shape, vecs.shape, vecsprime.shape) # Test (1)*(5) inner axis vecs = np.random.rand(3, 5) quats = z vecsprime = quats.rotate(vecs, axis=-2) for i, vec in enumerate(vecs.T): assert np.allclose(vecsprime[:, i], (quats * quaternionic.array(0, *vec) * quats.inverse).vector, rtol=0.0, atol=0.0) assert quats.shape[:-1] + vecs.shape == vecsprime.shape, ("Out of shape!", quats.shape, vecs.shape, vecsprime.shape) # Test (N)*(1) vecs = np.random.rand(3) quats = Rs vecsprime = quats.rotate(vecs) assert np.allclose(vecsprime, [ vprime.vector for vprime in quats * quaternionic.array(0, *vecs) * ~quats ], rtol=1e-15, atol=1e-15) assert quats.shape[:-1] + vecs.shape == vecsprime.shape, ("Out of shape!", quats.shape, vecs.shape, vecsprime.shape) # Test (N)*(5) vecs = np.random.rand(5, 3) quats = Rs vecsprime = quats.rotate(vecs) for i, vec in enumerate(vecs): assert np.allclose(vecsprime[:, i], [ vprime.vector for vprime in quats * quaternionic.array(0, *vec) * ~quats ], rtol=1e-15, atol=1e-15) assert quats.shape[:-1] + vecs.shape == vecsprime.shape, ("Out of shape!", quats.shape, vecs.shape, vecsprime.shape) # Test (N)*(5) inner axis vecs = np.random.rand(3, 5) quats = Rs vecsprime = quats.rotate(vecs, axis=-2) for i, vec in enumerate(vecs.T): assert np.allclose(vecsprime[:, :, i], [ vprime.vector for vprime in quats * quaternionic.array(0, *vec) * ~quats ], rtol=1e-15, atol=1e-15) assert quats.shape[:-1] + vecs.shape == vecsprime.shape, ("Out of shape!", quats.shape, vecs.shape, vecsprime.shape)
def test_iterator(): a = np.arange(17 * 3 * 4).reshape((17, 3, 4)) q = quaternionic.array(a) for i, qi in enumerate(q.iterator): assert np.array_equal(qi, np.arange(4) + 4.0 * i)
def sYlm(self, s, R, out=None, workspace=None): """Evaluate (possibly spin-weighted) spherical harmonic Parameters ---------- R : array_like Array to be interpreted as a quaternionic array (thus its final dimension must have size 4), representing the rotations on which the sYlm will be evaluated. out : array_like, optional Array into which the d values should be written. It should be an array of complex, with size `self.Ysize`. If not present, the array will be created. In either case, the array will also be returned. workspace : array_like, optional A working array like the one returned by Wigner.new_workspace(). If not present, this object's default workspace will be used. Note that it is not safe to use the same workspace on multiple threads. Returns ------- Y : array This is a 1-dimensional array of complex; see below. See Also -------- H : Compute a portion of the H matrix d : Compute the full Wigner d matrix D : Compute the full Wigner 𝔇 matrix rotate : Avoid computing the full 𝔇 matrix and rotate modes directly evaluate : Avoid computing the full 𝔇 matrix and evaluate modes directly Notes ----- The spherical harmonics of spin weight s are related to the 𝔇 matrix as ₛYₗₘ(R) = (-1)ˢ √((2ℓ+1)/(4π)) 𝔇ˡₘ₋ₛ(R) = (-1)ˢ √((2ℓ+1)/(4π)) 𝔇̄ˡ₋ₛₘ(R̄) This function is the preferred method of computing the sYlm for large ell values. In particular, above ell≈32 standard formulas become completely unusable because of numerical instabilities and overflow. This function uses stable recursion methods instead, and should be usable beyond ell≈1000. This function computes ₛYₗₘ(R). The result is returned in a 1-dimensional array ordered as [ Y(s, ell, m, R) for ell in range(ell_max+1) for m in range(-ell, ell+1) ] """ if abs(s) > self.mp_max: raise ValueError( f"This object has mp_max={self.mp_max}, which is not " f"sufficient to compute sYlm values for spin weight s={s}" ) if out is not None and out.size != (self.Ysize * R.size // 4): raise ValueError( f"Given output array has size {out.size}; it should be {self.Ysize * R.size // 4}" ) if out is not None and out.dtype != complex: raise ValueError(f"Given output array has dtype {out.dtype}; it should be complex") if workspace is not None: Hwedge, Hv, Hextra, zₐpowers, zᵧpowers, z = self._split_workspace(workspace) else: Hwedge, Hv, Hextra, zₐpowers, zᵧpowers, z = ( self.Hwedge, self.Hv, self.Hextra, self.zₐpowers, self.zᵧpowers, self.z ) quaternions = quaternionic.array(R).ndarray.reshape((-1, 4)) function_values = ( out.reshape(quaternions.shape[0], self.Ysize) if out is not None else np.zeros(quaternions.shape[:-1] + (self.Ysize,), dtype=complex) ) # Loop over all input quaternions for i_R in range(quaternions.shape[0]): to_euler_phases(quaternions[i_R], z) Hwedge = self.H(z[1], Hwedge, Hv, Hextra) Y = function_values[i_R] _complex_powers(z[0:1], self.ell_max, zₐpowers) zᵧpower = z[2]**abs(s) _fill_sYlm(self.ell_min, self.ell_max, self.mp_max, s, Y, Hwedge, zₐpowers[0], zᵧpower) return function_values.reshape(R.shape[:-1] + (self.Ysize,))
def rotate(self, modes, R, out=None, workspace=None, horner=False): """Rotate Modes object Parameters ---------- modes : Modes object R : quaternionic.array Unit quaternion representing the rotation of the frame in which the mode weights are measured. out : array_like, optional Array into which the rotated mode weights should be written. It should be an array of complex with the same shape as `modes`. If not present, the array will be created. In either case, the array will also be returned. workspace : array_like, optional A working array like the one returned by Wigner.new_workspace(). If not present, this object's default workspace will be used. Note that it is not safe to use the same workspace on multiple threads. horner : bool, optional If False (the default), rotation will be done using matrix multiplication with Wigner's 𝔇 — which will typically use BLAS, and thus be as fast as possible. If True, the result will be built up using Horner form, which should be more accurate, but may be significantly slower. Returns ------- rotated_modes : array_like This array holds the complex function values. Its shape is modes.shape[:-1]+R.shape[:-1]. """ if self.mp_max < self.ell_max: raise ValueError( f"Cannot rotate modes up to ell_max={self.ell_max} if mp_max is only {self.mp_max}" ) ell_min = modes.ell_min ell_max = modes.ell_max spin_weight = modes.spin_weight if ell_max > self.ell_max: raise ValueError( f"This object has ell_max={self.ell_max}, which is not " f"sufficient for the input modes object with ell_max={ell_max}" ) # Reinterpret inputs as 2-d np.arrays mode_weights = modes.ndarray.reshape((-1, modes.shape[-1])) R = quaternionic.array(R) # Construct storage space rotated_mode_weights = ( out if out is not None else np.zeros_like(mode_weights) ) if horner: if workspace is not None: Hwedge, Hv, Hextra, _, _, z = self._split_workspace(workspace) else: Hwedge, Hv, Hextra, z = self.Hwedge, self.Hv, self.Hextra, self.z to_euler_phases(R, z) Hwedge = self.H(z[1], Hwedge, Hv, Hextra) _rotate_Horner( mode_weights, rotated_mode_weights, self.ell_min, self.ell_max, self.mp_max, ell_min, ell_max, spin_weight, Hwedge, z[0], z[2] ) else: D = self.D(R, workspace) _rotate( mode_weights, rotated_mode_weights, self.ell_min, self.ell_max, self.mp_max, ell_min, ell_max, spin_weight, D ) return type(modes)( rotated_mode_weights.reshape(modes.shape), **modes._metadata )
def evaluate(self, modes, R, out=None, workspace=None, horner=False): """Evaluate Modes object as function of rotations Parameters ---------- modes : Modes object R : quaternionic.array Arbitrarily shaped array of quaternions. All modes in the input will be evaluated on each of these quaternions. Note that it is fairly standard to construct these quaternions from spherical coordinates, as with the function `quaternionic.array.from_spherical_coordinates`. out : array_like, optional Array into which the function values should be written. It should be an array of complex, with shape `modes.shape[:-1]+R.shape[:-1]`. If not present, the array will be created. In either case, the array will also be returned. workspace : array_like, optional A working array like the one returned by Wigner.new_workspace(). If not present, this object's default workspace will be used. Note that it is not safe to use the same workspace on multiple threads. horner : bool, optional If False (the default), evaluation will be done using vector multiplication with sYlm — which will typically use BLAS, and thus be as fast as possible. If True, the result will be built up using Horner form, which should be more accurate, but may be significantly slower. Returns ------- f : array_like This array holds the complex function values. Its shape is modes.shape[:-1]+R.shape[:-1]. """ spin_weight = modes.spin_weight ell_min = modes.ell_min ell_max = modes.ell_max if abs(spin_weight) > self.mp_max: raise ValueError( f"This object has mp_max={self.mp_max}, which is not " f"sufficient to compute sYlm values for spin weight s={spin_weight}" ) if max(abs(spin_weight), ell_min) < self.ell_min: raise ValueError( f"This object has ell_min={self.ell_min}, which is not " f"sufficient for the requested spin weight s={spin_weight} and ell_min={ell_min}" ) if ell_max > self.ell_max: raise ValueError( f"This object has ell_max={self.ell_max}, which is not " f"sufficient for the input modes object with ell_max={ell_max}" ) # Reinterpret inputs as 2-d np.arrays mode_weights = modes.ndarray.reshape((-1, modes.shape[-1])) quaternions = quaternionic.array(R).ndarray.reshape((-1, 4)) # Construct storage space function_values = ( out if out is not None else np.zeros(mode_weights.shape[:-1] + quaternions.shape[:-1], dtype=complex) ) if horner: if workspace is not None: Hwedge, Hv, Hextra, _, _, z = self._split_workspace(workspace) else: Hwedge, Hv, Hextra, z = self.Hwedge, self.Hv, self.Hextra, self.z # Loop over all input quaternions for i_R in range(quaternions.shape[0]): to_euler_phases(quaternions[i_R], z) Hwedge = self.H(z[1], Hwedge, Hv, Hextra) _evaluate_Horner( mode_weights, function_values[..., i_R], self.ell_min, self.ell_max, self.mp_max, ell_min, ell_max, spin_weight, Hwedge, z[0], z[2] ) else: Y = np.zeros(self.Ysize, dtype=complex) # Loop over all input quaternions for i_R in range(quaternions.shape[0]): self.sYlm(spin_weight, quaternions[i_R], out=Y, workspace=workspace) np.matmul(mode_weights, Y, out=function_values[..., i_R]) return function_values.reshape(modes.shape[:-1] + R.shape[:-1])