charge_distributions.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. from __future__ import annotations
  2. import copy
  3. import numpy as np
  4. from scipy.special import eval_legendre
  5. from charged_shells import expansion, functions as fn
  6. import quaternionic
  7. Array = np.ndarray
  8. Quaternion = quaternionic.array
  9. Expansion = expansion.Expansion
  10. def create_expansion24(sigma2: float | Array, sigma4: float | Array, sigma0: float | Array = 0.):
  11. l_array = np.array([0, 2, 4])
  12. try:
  13. broadcasted_arrs = np.broadcast_arrays(np.sqrt(4*np.pi) * sigma0, sigma2, sigma4)
  14. except ValueError:
  15. raise ValueError("Given sigma0, sigma2 and sigma4 arrays cannot be broadcast to a common shape.")
  16. coefs = rot_sym_expansion(l_array, np.stack(broadcasted_arrs, axis=-1))
  17. return Expansion(l_array, coefs)
  18. def create_mapped_rotsym_expansion(a_bar: Array | float,
  19. kappaR: Array | float,
  20. sigma_tilde: float,
  21. l_array: Array,
  22. sigma0: float | Array = 0) -> Expansion:
  23. """
  24. Create Expansion that matches the outside potential of an impermeable particle with
  25. a rotationally symmetric point charge distribution inside (specifically, either quadrupolar or dipolar).
  26. :param a_bar: distance between the center and off center charges
  27. :param kappaR: screening parameter
  28. :param sigma_tilde: magnitude of off-center charges / 4pi R^2
  29. :param l_max: maximal ell value for the expansion
  30. :param sigma0: total (mean) charge density
  31. """
  32. if not isinstance(sigma0, Array):
  33. sigma0 = np.array([sigma0])
  34. if sigma0.ndim > 1:
  35. raise ValueError(f'Sigma0 parameter cannot be an array of dimensions larger than 1, got dim={sigma0.ndim}')
  36. a_bar_bc, kappaR_bc = np.broadcast_arrays(a_bar, kappaR)
  37. a_bar_bc2, kappaR_bc2, l_array_expanded = np.broadcast_arrays(a_bar_bc[..., None],
  38. kappaR_bc[..., None],
  39. l_array[None, :])
  40. coefs = (2 * sigma_tilde * fn.coef_C_diff(l_array_expanded, kappaR_bc2)
  41. * np.sqrt(4 * np.pi * (2 * l_array_expanded + 1)) * np.power(a_bar_bc2, l_array_expanded))
  42. coefs = np.squeeze(rot_sym_expansion(l_array, coefs))
  43. kappaR_bc3, sigma0_bc3 = np.broadcast_arrays(kappaR_bc[..., None], sigma0[None, :])
  44. coefs = expansion_total_charge(coefs, net_charge_map(sigma0_bc3, kappaR_bc3), l_array)
  45. return Expansion(l_array, np.squeeze(coefs))
  46. def create_mapped_quad_expansion(a_bar: Array | float,
  47. kappaR: Array | float,
  48. sigma_tilde: float,
  49. l_max: int = 20,
  50. sigma0: float | Array = 0) -> Expansion:
  51. """Create Expansion mapped from a quadrupolar point charge distribution."""
  52. l_array = np.array([l for l in range(l_max + 1) if l % 2 == 0])
  53. return create_mapped_rotsym_expansion(a_bar, kappaR, sigma_tilde, l_array, sigma0)
  54. def create_mapped_dipolar_expansion(a_bar: Array | float,
  55. kappaR: Array | float,
  56. sigma_tilde: float,
  57. l_max: int = 20,
  58. sigma0: float | Array = 0) -> Expansion:
  59. """Create Expansion mapped from a dipolar point charge distribution."""
  60. l_array = np.array([l for l in range(l_max + 1) if l % 2 == 1 or l == 0])
  61. return create_mapped_rotsym_expansion(a_bar, kappaR, sigma_tilde, l_array, sigma0)
  62. def create_gaussian_charge_expansion(omega_k: Array,
  63. lambda_k: Array | float,
  64. sigma1: float,
  65. l_max: int,
  66. sigma0: float | Array = 0,
  67. equal_charges: bool = True) -> Expansion:
  68. """
  69. Create Expansion for a collection of smeared charges on the sphere.
  70. :param omega_k: array of positions (theta, phi) of all charges
  71. :param lambda_k: smear parameter for each charge or smear for different cases (if equal_charges = True)
  72. :param sigma1: scaling
  73. :param l_max: maximal ell value for the expansion
  74. :param sigma0: total (mean) charge density
  75. :param equal_charges: if this is False, length of lambda_k should be N. If True, theta0_k array will be treated
  76. as different expansion cases
  77. """
  78. omega_k = omega_k.reshape(-1, 2)
  79. if not isinstance(lambda_k, Array):
  80. lambda_k = np.array([lambda_k])
  81. if equal_charges:
  82. if lambda_k.ndim > 1:
  83. raise ValueError(f'If equal_charges=True, lambda_k should be a 1D array, got shape {lambda_k.shape}')
  84. lambda_k = np.full((omega_k.shape[0], lambda_k.shape[0]), lambda_k).T
  85. if lambda_k.shape[-1] != omega_k.shape[0]:
  86. raise ValueError("Number of charges (length of omega_k) should match the last dimension of lambda_k array.")
  87. lambda_k = lambda_k.reshape(-1, omega_k.shape[0])
  88. l_array = np.arange(l_max + 1)
  89. full_l_array, full_m_array = expansion.full_lm_arrays(l_array)
  90. theta_k = omega_k[:, 0]
  91. phi_k = omega_k[:, 1]
  92. summands = (lambda_k[:, None, :] / np.sinh(lambda_k[:, None, :])
  93. * fn.sph_bessel_i(full_l_array[None, :, None], lambda_k[:, None, :])
  94. * np.conj(fn.sph_harm(full_l_array[None, :, None], full_m_array[None, :, None],
  95. theta_k[None, None, :], phi_k[None, None, :])))
  96. coefs = np.squeeze(4 * np.pi * sigma1 * np.sum(summands, axis=-1))
  97. coefs = expansion_total_charge(coefs, sigma0, l_array)
  98. l_array, coefs = purge_unneeded_l(l_array, coefs)
  99. return Expansion(l_array, coefs)
  100. def create_spherical_cap_expansion(omega_k: Array,
  101. theta0_k: Array | float,
  102. sigma1: float,
  103. l_max: int,
  104. sigma0: float | Array = 0,
  105. equal_sizes: bool = True) -> Expansion:
  106. """
  107. Create Expansion for a collection of spherical caps.
  108. :param omega_k: array of positions (theta, phi) of all spherical caps
  109. :param theta0_k: sizes of each spherical caps or cap sizes for different cases (if equal_sizes = True)
  110. :param sigma1: charge magnitude for the single cap, currently assumes that this is equal for all caps
  111. :param l_max: maximal ell value for the expansion
  112. :param sigma0: total (mean) charge density
  113. :param equal_sizes: if this is False, length of theta0_k should be N. If True, theta0_k array will be treated as
  114. different expansion cases
  115. """
  116. omega_k = omega_k.reshape(-1, 2)
  117. if not isinstance(theta0_k, Array):
  118. theta0_k = np.array(theta0_k)
  119. if equal_sizes:
  120. if theta0_k.ndim == 0:
  121. theta0_k = np.full(omega_k.shape[0], theta0_k)
  122. elif theta0_k.ndim == 1:
  123. theta0_k = np.full((omega_k.shape[0], theta0_k.shape[0]), theta0_k)
  124. else:
  125. raise ValueError(f'If equal_charges=True, theta0_k should be a 1D array, got shape {theta0_k.shape}')
  126. if theta0_k.shape[0] != omega_k.shape[0]:
  127. raise ValueError("Number of charges (length of omega_k) should match the last dimension of theta0_k array.")
  128. rotations = Quaternion(np.stack((np.cos(omega_k[..., 0] / 2),
  129. np.sin(omega_k[..., 1]) * np.sin(omega_k[..., 0] / 2),
  130. np.cos(omega_k[..., 1]) * np.sin(omega_k[..., 0] / 2),
  131. np.zeros_like(omega_k[..., 0]))).T)
  132. l_array = np.arange(l_max + 1)
  133. coefs_l0 = -sigma1 * (np.sqrt(np.pi / (2 * l_array[None, :] + 1)) *
  134. (eval_legendre(l_array[None, :] + 1, np.cos(theta0_k[..., None]))
  135. - eval_legendre(l_array[None, :] - 1, np.cos(theta0_k[..., None]))))
  136. coefs = rot_sym_expansion(l_array, coefs_l0)
  137. coefs_all_single_caps = expansion.expansion_rotation(rotations, coefs, l_array)
  138. # Rotating is implemented in such a way that it rotates every patch to every position,
  139. # hence the redundancy of out of diagonal elements.
  140. coefs_all = np.sum(np.diagonal(coefs_all_single_caps), axis=-1)
  141. coefs_all = expansion_total_charge(coefs_all, sigma0, l_array)
  142. return Expansion(l_array, np.squeeze(coefs_all))
  143. def net_charge_map(sigma0: float | Array, kappaR: float | Array):
  144. return sigma0 * np.exp(kappaR) / np.sinh(kappaR) * kappaR / (1 + kappaR)
  145. def rot_sym_expansion(l_array: Array, coefs: Array) -> Array:
  146. """Create full expansion array for rotationally symmetric distributions with only m=0 terms different form 0."""
  147. full_coefs = np.zeros(coefs.shape[:-1] + (np.sum(2 * l_array + 1),))
  148. full_coefs[..., np.cumsum(2 * l_array + 1) - l_array - 1] = coefs
  149. return full_coefs
  150. def expansion_total_charge(coefs: Array, sigma0: float | Array | None, l_vals: Array):
  151. """Adds a new axis to the expansion coefficients that modifies expansion based on given net charge density."""
  152. if l_vals[0] != 0:
  153. raise NotImplementedError("To modify total charge, the first expansion term should be l=0. "
  154. "Adding l=0 term not yet implemented (TODO).")
  155. if sigma0 is None:
  156. return coefs
  157. if not isinstance(sigma0, Array):
  158. x = copy.deepcopy(coefs)
  159. x[..., 0] = sigma0 * np.sqrt(4 * np.pi)
  160. return x
  161. # insert new axis in the 2nd-to-last place and repeat the expansion data over this new axis
  162. x = np.repeat(np.expand_dims(coefs, -2), sigma0.shape[-1], axis=-2)
  163. x[..., 0] = sigma0 * np.sqrt(4 * np.pi)
  164. return x
  165. def purge_unneeded_l(l_array: Array, coefs: Array) -> (Array, Array):
  166. """Remove ell values from expansion for which all (ell, m) coefficients are zero."""
  167. def delete_zero_entries(l, l_arr, cfs):
  168. l_idx = np.where(l_arr == l)[0][0]
  169. m_indices = expansion.m_indices_at_l(l_arr, l_idx)
  170. if np.all(cfs[..., m_indices] == 0):
  171. return np.delete(l_arr, l_idx), np.delete(cfs, m_indices, axis=-1)
  172. return l_arr, cfs
  173. for l in l_array:
  174. l_array, coefs = delete_zero_entries(l, l_array, coefs)
  175. return l_array, coefs