expansion.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. from __future__ import annotations
  2. import numpy as np
  3. from dataclasses import dataclass
  4. import charged_shells.functions as fn
  5. import quaternionic
  6. import spherical
  7. import copy
  8. Array = np.ndarray
  9. Quaternion = quaternionic.array
  10. class InvalidExpansion(Exception):
  11. pass
  12. @dataclass
  13. class Expansion:
  14. """Generic class for storing surface charge expansion coefficients."""
  15. l_array: Array
  16. coefs: Array
  17. _starting_coefs: Array = None # initialized with the __post_init__ method
  18. _rotations: Quaternion = None
  19. def __post_init__(self):
  20. if self.coefs.shape[-1] != np.sum(2 * self.l_array + 1):
  21. raise InvalidExpansion('Number of expansion coefficients does not match the provided l_array.')
  22. if np.all(np.sort(self.l_array) != self.l_array) or np.all(np.unique(self.l_array) != self.l_array):
  23. raise InvalidExpansion('Array of l values should be unique and sorted.')
  24. self.coefs = self.coefs.astype(np.complex128)
  25. self._starting_coefs = np.copy(self.coefs)
  26. def __getitem__(self, item):
  27. return Expansion(self.l_array, self.coefs[item])
  28. @property
  29. def max_l(self) -> int:
  30. return max(self.l_array)
  31. @property
  32. def shape(self):
  33. return self.coefs.shape[:-1]
  34. def flatten(self) -> Expansion:
  35. new_expansion = self.clone() # np.ndarray.flatten() also copies the array
  36. new_expansion.coefs = new_expansion.coefs.reshape(-1, new_expansion.coefs.shape[-1])
  37. new_expansion._rotations = new_expansion._rotations.reshape(-1, 4)
  38. return new_expansion
  39. def reshape(self, shape: tuple):
  40. self.coefs = self.coefs.reshape(shape + (self.coefs.shape[-1],))
  41. self._rotations = self._rotations.reshape(shape + (4,))
  42. @property
  43. def lm_arrays(self) -> (Array, Array):
  44. """Return l and m arrays containing all (l, m) pairs."""
  45. return full_lm_arrays(self.l_array)
  46. def repeat_over_m(self, arr: Array, axis=0) -> Array:
  47. if not arr.shape[axis] == len(self.l_array):
  48. raise ValueError('Array length should be equal to the number of l in the expansion.')
  49. return np.repeat(arr, 2 * self.l_array + 1, axis=axis)
  50. def rotate(self, rotations: Quaternion, rotate_existing=False):
  51. if rotate_existing:
  52. raise NotImplementedError("Rotation of possibly already rotated coefficients is not yet supported.")
  53. self._rotations = rotations
  54. self.coefs = expansion_rotation(rotations, self._starting_coefs, self.l_array)
  55. def rotate_euler(self, alpha: Array = 0, beta: Array = 0, gamma: Array = 0, rotate_existing=False):
  56. R_euler = quaternionic.array.from_euler_angles(alpha, beta, gamma)
  57. self.rotate(R_euler, rotate_existing=rotate_existing)
  58. def inverse_sign(self, exclude_00: bool = False):
  59. if self.l_array[0] == 0 and exclude_00:
  60. self.coefs[..., 1:] = -self.coefs[..., 1:]
  61. self._starting_coefs[..., 1:] = -self._starting_coefs[..., 1:]
  62. return self
  63. self.coefs = -self.coefs
  64. self._starting_coefs = -self._starting_coefs
  65. return self
  66. def charge_value(self, theta: Array | float, phi: Array | float):
  67. if not isinstance(theta, Array):
  68. theta = np.array([theta])
  69. if not isinstance(phi, Array):
  70. phi = np.array([phi])
  71. theta, phi = np.broadcast_arrays(theta, phi)
  72. full_l_array, full_m_array = self.lm_arrays
  73. return np.squeeze(np.real(np.sum(self.coefs[..., None] * fn.sph_harm(full_l_array[:, None],
  74. full_m_array[:, None],
  75. theta[None, :], phi[None, :]), axis=-2)))
  76. def clone(self) -> Expansion:
  77. return copy.deepcopy(self)
  78. def m_indices_at_l(l_arr: Array, l_idx: int):
  79. """
  80. For a given l_array and index l_idx for some ell in this array, get indices of all (ell, m) coefficients
  81. in coefficients array.
  82. """
  83. return np.arange(np.sum(2 * l_arr[:l_idx] + 1), np.sum(2 * l_arr[:l_idx+1] + 1))
  84. def full_lm_arrays(l_array: Array) -> (Array, Array):
  85. """From an array of l_values get arrays of ell and m that give you all pairs (ell, m)."""
  86. all_m_list = []
  87. for l in l_array:
  88. for i in range(2 * l + 1):
  89. all_m_list.append(-l + i)
  90. return np.repeat(l_array, 2 * l_array + 1), np.array(all_m_list)
  91. def coefs_fill_missing_l(expansion: Expansion, target_l_array: Array) -> Expansion:
  92. """Explicitly add missing expansion coefficients so that expansion includes all ell values from the target array."""
  93. missing_l = np.setdiff1d(target_l_array, expansion.l_array, assume_unique=True)
  94. fill = np.zeros(np.sum(2 * missing_l + 1))
  95. full_l_array1, _ = expansion.lm_arrays
  96. # we search for where to place missing coefs with the help of a boolean array and argmax function
  97. comparison_bool = (full_l_array1[:, None] - missing_l[None, :]) > 0
  98. indices = np.where(np.any(comparison_bool, axis=0), np.argmax(comparison_bool, axis=0), full_l_array1.shape[0])
  99. new_coefs = np.insert(expansion.coefs, np.repeat(indices, 2 * missing_l + 1), fill, axis=-1)
  100. return Expansion(target_l_array, new_coefs)
  101. def expansions_to_common_l(ex1: Expansion, ex2: Expansion) -> (Expansion, Expansion):
  102. """Explicitly add zero expansion coefficients so that both expansions include coefficients for the same ell."""
  103. common_l_array = np.union1d(ex1.l_array, ex2.l_array)
  104. return coefs_fill_missing_l(ex1, common_l_array), coefs_fill_missing_l(ex2, common_l_array)
  105. def expansion_rotation(rotations: Quaternion, coefs: Array, l_array: Array):
  106. """
  107. General function for rotations of expansion coefficients using WignerD matrices. Combines all rotations
  108. with each expansion given in coefs array.
  109. :param rotations: Quaternion array, last dimension is 4
  110. :param coefs: array of expansion coefficients
  111. :param l_array: array of all ell values of the expansion
  112. :return rotated coefficients, output shape is rotations.shape[:-1] + coefs.shape
  113. """
  114. rot_arrays = rotations.ndarray.reshape((-1, 4))
  115. coefs_reshaped = coefs.reshape((-1, coefs.shape[-1]))
  116. wigner_matrices = spherical.Wigner(np.max(l_array)).D(rot_arrays)
  117. new_coefs = np.zeros((rot_arrays.shape[0],) + coefs_reshaped.shape, dtype=np.complex128)
  118. for i, l in enumerate(l_array):
  119. Dlmn_slice = np.arange(l * (2 * l - 1) * (2 * l + 1) / 3, (l + 1) * (2 * l + 1) * (2 * l + 3) / 3).astype(int)
  120. all_m_indices = m_indices_at_l(l_array, i)
  121. wm = wigner_matrices[:, Dlmn_slice].reshape((-1, 2*l+1, 2*l+1))
  122. new_coefs[..., all_m_indices] = np.einsum('rnm, qm -> rqn',
  123. wm, coefs_reshaped[:, all_m_indices])
  124. return new_coefs.reshape(rotations.ndarray.shape[:-1] + coefs.shape)