|
@@ -8,10 +8,13 @@ import spherical
|
|
|
import time
|
|
|
import copy
|
|
|
import matplotlib.pyplot as plt
|
|
|
+from typing import Callable, TypeVar
|
|
|
|
|
|
|
|
|
Array = np.ndarray
|
|
|
Quaternion = quaternionic.array
|
|
|
+T = TypeVar('T')
|
|
|
+V = TypeVar('V')
|
|
|
|
|
|
|
|
|
class InvalidExpansion(Exception):
|
|
@@ -35,10 +38,27 @@ class Expansion:
|
|
|
self.coefs = self.coefs.astype(np.complex128)
|
|
|
self._starting_coefs = np.copy(self.coefs)
|
|
|
|
|
|
+ def __getitem__(self, item):
|
|
|
+ return Expansion(self.l_array, self.coefs[item])
|
|
|
+
|
|
|
@property
|
|
|
def max_l(self) -> int:
|
|
|
return max(self.l_array)
|
|
|
|
|
|
+ @property
|
|
|
+ def shape(self):
|
|
|
+ return self.coefs.shape[:-1]
|
|
|
+
|
|
|
+ def flatten(self) -> Expansion:
|
|
|
+ new_expansion = self.clone()
|
|
|
+ new_expansion.coefs = new_expansion.coefs.reshape(-1, new_expansion.coefs.shape[-1])
|
|
|
+ new_expansion._rotations = new_expansion._rotations.reshape(-1, 4)
|
|
|
+ return new_expansion
|
|
|
+
|
|
|
+ def reshape(self, shape):
|
|
|
+ self.coefs = self.coefs.reshape(shape + (self.coefs.shape[-1],))
|
|
|
+ self._rotations = self._rotations.reshape(shape + (4),)
|
|
|
+
|
|
|
@cached_property
|
|
|
def lm_arrays(self) -> (Array, Array):
|
|
|
"""Return l and m arrays containing all (l, m) pairs."""
|
|
@@ -50,6 +70,7 @@ class Expansion:
|
|
|
return np.repeat(arr, 2 * self.l_array + 1, axis=axis)
|
|
|
|
|
|
def rotate(self, rotations: Quaternion, rotate_existing=False):
|
|
|
+ # TODO: rotations are currently saved wrong if we start form existing coefficients not the og ones
|
|
|
self._rotations = rotations
|
|
|
coefs = self.coefs if rotate_existing else self._starting_coefs
|
|
|
self.coefs = expansion_rotation(rotations, coefs, self.l_array)
|
|
@@ -67,8 +88,9 @@ class Expansion:
|
|
|
phi = np.array([phi])
|
|
|
theta, phi = np.broadcast_arrays(theta, phi)
|
|
|
full_l_array, full_m_array = self.lm_arrays
|
|
|
- return np.sum(self.coefs[None, :] * fn.sph_harm(full_l_array[None, :], full_m_array[None, :],
|
|
|
- theta[:, None], phi[:, None]), axis=1)
|
|
|
+ return np.squeeze(np.real(np.sum(self.coefs[..., None] * fn.sph_harm(full_l_array[:, None],
|
|
|
+ full_m_array[:, None],
|
|
|
+ theta[None, :], phi[None, :]), axis=-2)))
|
|
|
|
|
|
def clone(self) -> Expansion:
|
|
|
return copy.deepcopy(self)
|
|
@@ -78,42 +100,70 @@ class Expansion24(Expansion):
|
|
|
|
|
|
def __init__(self, sigma2: float, sigma4: float, sigma0: float = 0.):
|
|
|
l_array = np.array([0, 2, 4])
|
|
|
- coeffs = rot_sym_expansion(l_array, np.array([sigma0, sigma2, sigma4]))
|
|
|
- super().__init__(l_array, coeffs)
|
|
|
+ coefs = rot_sym_expansion(l_array, np.array([sigma0, sigma2, sigma4]))
|
|
|
+ super().__init__(l_array, coefs)
|
|
|
|
|
|
|
|
|
class MappedExpansionQuad(Expansion):
|
|
|
|
|
|
- def __init__(self, a_bar: float, kappaR: float, sigma_m: float, max_l: int = 20, sigma0: float = 0):
|
|
|
- l_array = np.array([l for l in range(max_l + 1) if l % 2 == 0])
|
|
|
- coeffs = (2 * sigma_m * fn.coeff_C_diff(l_array, kappaR)
|
|
|
- * np.sqrt(4 * np.pi * (2 * l_array + 1)) * np.power(a_bar, l_array))
|
|
|
- coeffs[0] = sigma0
|
|
|
- coeffs = rot_sym_expansion(l_array, coeffs)
|
|
|
- super().__init__(l_array, coeffs)
|
|
|
+ def __init__(self, a_bar: Array | float, kappaR: Array | float, sigma_m: float, l_max: int = 20, sigma0: float = 0):
|
|
|
+ a_bar, kappaR = np.broadcast_arrays(a_bar, kappaR)
|
|
|
+
|
|
|
+ l_array = np.array([l for l in range(l_max + 1) if l % 2 == 0])
|
|
|
+ a_bar, kappaR, l_array_expanded = np.broadcast_arrays(a_bar[..., None], kappaR[..., None], l_array[None, :])
|
|
|
+
|
|
|
+ coefs = (2 * sigma_m * fn.coef_C_diff(l_array_expanded, kappaR)
|
|
|
+ * np.sqrt(4 * np.pi * (2 * l_array_expanded + 1)) * np.power(a_bar, l_array_expanded))
|
|
|
+ coefs[..., 0] = sigma0
|
|
|
+ coefs = rot_sym_expansion(l_array, coefs)
|
|
|
+ super().__init__(l_array, coefs)
|
|
|
|
|
|
|
|
|
-class SmearedCharges(Expansion):
|
|
|
+class GaussianCharges(Expansion):
|
|
|
|
|
|
- def __init__(self, omega_k: Array, lambda_k: Array | float, sigma1: float, l_max: int, sigma0: float = 0):
|
|
|
+ def __init__(self, omega_k: Array, lambda_k: Array | float, sigma1: float, l_max: int,
|
|
|
+ sigma0: float = 0, equal_charges=True):
|
|
|
omega_k = omega_k.reshape(-1, 2)
|
|
|
if not isinstance(lambda_k, Array):
|
|
|
- lambda_k = np.full((len(omega_k),), lambda_k)
|
|
|
+ lambda_k = np.array([lambda_k])
|
|
|
+ if equal_charges:
|
|
|
+ if lambda_k.ndim > 1:
|
|
|
+ raise ValueError(f'If equal_charges=True, lambda_k should be a 1D array, got shape {lambda_k.shape}')
|
|
|
+ lambda_k = np.full((omega_k.shape[0], lambda_k.shape[0]), lambda_k).T
|
|
|
if lambda_k.shape[-1] != omega_k.shape[0]:
|
|
|
- raise ValueError("Omega and lambda arrays should have the same length.")
|
|
|
+ raise ValueError("Number of charges (length of omega_k) should match the last dimension of lambda_k array.")
|
|
|
+ lambda_k = lambda_k.reshape(-1, omega_k.shape[0])
|
|
|
l_array = np.arange(l_max + 1)
|
|
|
full_l_array, full_m_array = full_fm_arrays(l_array)
|
|
|
theta_k = omega_k[:, 0]
|
|
|
phi_k = omega_k[:, 1]
|
|
|
- summands = (lambda_k[None, :] / np.sinh(lambda_k[None, :])
|
|
|
- * fn.sph_bessel_i(full_l_array[:, None], lambda_k[None, :])
|
|
|
- * np.conj(fn.sph_harm(full_l_array[:, None], full_m_array[:, None],
|
|
|
- theta_k[None, :], phi_k[None, :])))
|
|
|
- coefs = 4 * np.pi * sigma1 * np.sum(summands, axis=1)
|
|
|
- coefs[0] = sigma0
|
|
|
+ summands = (lambda_k[:, None, :] / np.sinh(lambda_k[:, None, :])
|
|
|
+ * fn.sph_bessel_i(full_l_array[None, :, None], lambda_k[:, None, :])
|
|
|
+ * np.conj(fn.sph_harm(full_l_array[None, :, None], full_m_array[None, :, None],
|
|
|
+ theta_k[None, None, :], phi_k[None, None, :])))
|
|
|
+ coefs = np.squeeze(4 * np.pi * sigma1 * np.sum(summands, axis=-1))
|
|
|
+ coefs[..., 0] = sigma0
|
|
|
+ l_array, coefs = purge_unneeded_l(l_array, coefs)
|
|
|
super().__init__(l_array, coefs)
|
|
|
|
|
|
|
|
|
+def map_over_expansion(f: Callable[[Expansion, T], V]) -> Callable[[Expansion, T], V]:
|
|
|
+ """Map a function f over the leading axes of an expansion."""
|
|
|
+
|
|
|
+ def mapped_f(ex: Expansion, *args, **kwargs):
|
|
|
+ og_shape = ex.shape
|
|
|
+ flat_ex = ex.flatten()
|
|
|
+ results = []
|
|
|
+ for i in range(np.prod(og_shape)):
|
|
|
+ results.append(f(flat_ex[i], *args, **kwargs))
|
|
|
+ try:
|
|
|
+ return np.array(results).reshape(og_shape + results[0].shape)
|
|
|
+ except AttributeError:
|
|
|
+ return np.array(results).reshape(og_shape)
|
|
|
+
|
|
|
+ return mapped_f
|
|
|
+
|
|
|
+
|
|
|
def full_fm_arrays(l_array: Array) -> (Array, Array):
|
|
|
all_m_list = []
|
|
|
for l in l_array:
|
|
@@ -122,22 +172,38 @@ def full_fm_arrays(l_array: Array) -> (Array, Array):
|
|
|
return np.repeat(l_array, 2 * l_array + 1), np.array(all_m_list)
|
|
|
|
|
|
|
|
|
-def rot_sym_expansion(l_array: Array, coeffs: Array) -> Array:
|
|
|
+def rot_sym_expansion(l_array: Array, coefs: Array) -> Array:
|
|
|
"""Create full expansion array for rotationally symmetric distributions with only m=0 terms different form 0."""
|
|
|
- full_coeffs = np.zeros(np.sum(2 * l_array + 1))
|
|
|
- full_coeffs[np.cumsum(2 * l_array + 1) - l_array - 1] = coeffs
|
|
|
- return full_coeffs
|
|
|
+ full_coefs = np.zeros(coefs.shape[:-1] + (np.sum(2 * l_array + 1),))
|
|
|
+ full_coefs[..., np.cumsum(2 * l_array + 1) - l_array - 1] = coefs
|
|
|
+ return full_coefs
|
|
|
|
|
|
|
|
|
-def coeffs_fill_missing_l(expansion: Expansion, target_l_array: Array) -> Expansion:
|
|
|
+def coefs_fill_missing_l(expansion: Expansion, target_l_array: Array) -> Expansion:
|
|
|
missing_l = np.setdiff1d(target_l_array, expansion.l_array, assume_unique=True)
|
|
|
fill = np.zeros(np.sum(2 * missing_l + 1))
|
|
|
full_l_array1, _ = expansion.lm_arrays
|
|
|
- # we search for where to place missing coeffs with the help of a boolean array and argmax function
|
|
|
+ # we search for where to place missing coefs with the help of a boolean array and argmax function
|
|
|
comparison_bool = (full_l_array1[:, None] - missing_l[None, :]) > 0
|
|
|
indices = np.where(np.any(comparison_bool, axis=0), np.argmax(comparison_bool, axis=0), full_l_array1.shape[0])
|
|
|
- new_coeffs = np.insert(expansion.coefs, np.repeat(indices, 2 * missing_l + 1), fill, axis=-1)
|
|
|
- return Expansion(target_l_array, new_coeffs)
|
|
|
+ new_coefs = np.insert(expansion.coefs, np.repeat(indices, 2 * missing_l + 1), fill, axis=-1)
|
|
|
+ return Expansion(target_l_array, new_coefs)
|
|
|
+
|
|
|
+
|
|
|
+def m_indices_at_l(l_arr: Array, l_idx: int):
|
|
|
+ return np.arange(np.sum(2 * l_arr[:l_idx] + 1), np.sum(2 * l_arr[:l_idx+1] + 1))
|
|
|
+
|
|
|
+
|
|
|
+def purge_unneeded_l(l_array: Array, coefs: Array) -> (Array, Array):
|
|
|
+ def delete_zero_entries(l, l_arr, cfs):
|
|
|
+ l_idx = np.where(l_arr == l)[0][0]
|
|
|
+ m_indices = m_indices_at_l(l_arr, l_idx)
|
|
|
+ if np.all(cfs[..., m_indices] == 0):
|
|
|
+ return np.delete(l_arr, l_idx), np.delete(cfs, m_indices, axis=-1)
|
|
|
+ return l_arr, cfs
|
|
|
+ for l in l_array:
|
|
|
+ l_array, coefs = delete_zero_entries(l, l_array, coefs)
|
|
|
+ return l_array, coefs
|
|
|
|
|
|
|
|
|
def plot_theta_profile(ex: Expansion, phi=0, num=100):
|
|
@@ -149,7 +215,7 @@ def plot_theta_profile(ex: Expansion, phi=0, num=100):
|
|
|
|
|
|
def expansions_to_common_l(ex1: Expansion, ex2: Expansion) -> (Expansion, Expansion):
|
|
|
common_l_array = np.union1d(ex1.l_array, ex2.l_array)
|
|
|
- return coeffs_fill_missing_l(ex1, common_l_array), coeffs_fill_missing_l(ex2, common_l_array)
|
|
|
+ return coefs_fill_missing_l(ex1, common_l_array), coefs_fill_missing_l(ex2, common_l_array)
|
|
|
|
|
|
|
|
|
def expansion_rotation(rotations: Quaternion, coefs: Array, l_array: Array):
|
|
@@ -167,7 +233,7 @@ def expansion_rotation(rotations: Quaternion, coefs: Array, l_array: Array):
|
|
|
new_coefs = np.zeros((rot_arrays.shape[0],) + coefs_reshaped.shape, dtype=np.complex128)
|
|
|
for i, l in enumerate(l_array):
|
|
|
Dlmn_slice = np.arange(l * (2 * l - 1) * (2 * l + 1) / 3, (l + 1) * (2 * l + 1) * (2 * l + 3) / 3).astype(int)
|
|
|
- all_m_indices = np.arange(np.sum(2 * l_array[:i] + 1), np.sum(2 * l_array[:i + 1] + 1))
|
|
|
+ all_m_indices = m_indices_at_l(l_array, i)
|
|
|
wm = wigner_matrices[:, Dlmn_slice].reshape((-1, 2*l+1, 2*l+1))
|
|
|
new_coefs[..., all_m_indices] = np.einsum('rnm, qm -> rqn',
|
|
|
wm, coefs_reshaped[:, all_m_indices])
|
|
@@ -178,7 +244,7 @@ if __name__ == '__main__':
|
|
|
|
|
|
# ex = MappedExpansionQuad(0.44, 3, 1, 10)
|
|
|
# ex = Expansion(np.arange(3), np.array([1, -1, 0, 1, 2, 0, 3, 0, 2]))
|
|
|
- ex = SmearedCharges(omega_k=np.array([[0, 0], [np.pi, 0]]), lambda_k=10, sigma1=0.001, l_max=10)
|
|
|
+ ex = GaussianCharges(omega_k=np.array([[0, 0], [np.pi, 0]]), lambda_k=10, sigma1=0.001, l_max=10)
|
|
|
# print(ex.coefs)
|
|
|
plot_theta_profile(ex)
|
|
|
|