123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- from __future__ import annotations
- import copy
- import numpy as np
- from scipy.special import eval_legendre
- from charged_shells import expansion, functions as fn
- import quaternionic
- Array = np.ndarray
- Quaternion = quaternionic.array
- Expansion = expansion.Expansion
- def create_expansion24(sigma2: float | Array, sigma4: float | Array, sigma0: float | Array = 0.):
- l_array = np.array([0, 2, 4])
- try:
- broadcasted_arrs = np.broadcast_arrays(np.sqrt(4*np.pi) * sigma0, sigma2, sigma4)
- except ValueError:
- raise ValueError("Given sigma0, sigma2 and sigma4 arrays cannot be broadcast to a common shape.")
- coefs = rot_sym_expansion(l_array, np.stack(broadcasted_arrs, axis=-1))
- return Expansion(l_array, coefs)
- def create_mapped_rotsym_expansion(a_bar: Array | float,
- kappaR: Array | float,
- sigma_tilde: float,
- l_array: Array,
- sigma0: float | Array = 0) -> Expansion:
- """
- Create Expansion that matches the outside potential of an impermeable particle with
- a rotationally symmetric point charge distribution inside (specifically, either quadrupolar or dipolar).
- :param a_bar: distance between the center and off center charges
- :param kappaR: screening parameter
- :param sigma_tilde: magnitude of off-center charges / 4pi R^2
- :param l_max: maximal ell value for the expansion
- :param sigma0: total (mean) charge density
- """
- if not isinstance(sigma0, Array):
- sigma0 = np.array([sigma0])
- if sigma0.ndim > 1:
- raise ValueError(f'Sigma0 parameter cannot be an array of dimensions larger than 1, got dim={sigma0.ndim}')
- a_bar_bc, kappaR_bc = np.broadcast_arrays(a_bar, kappaR)
- a_bar_bc2, kappaR_bc2, l_array_expanded = np.broadcast_arrays(a_bar_bc[..., None],
- kappaR_bc[..., None],
- l_array[None, :])
- coefs = (2 * sigma_tilde * fn.coef_C_diff(l_array_expanded, kappaR_bc2)
- * np.sqrt(4 * np.pi * (2 * l_array_expanded + 1)) * np.power(a_bar_bc2, l_array_expanded))
- coefs = np.squeeze(rot_sym_expansion(l_array, coefs))
- kappaR_bc3, sigma0_bc3 = np.broadcast_arrays(kappaR_bc[..., None], sigma0[None, :])
- coefs = expansion_total_charge(coefs, net_charge_map(sigma0_bc3, kappaR_bc3), l_array)
- return Expansion(l_array, np.squeeze(coefs))
- def create_mapped_quad_expansion(a_bar: Array | float,
- kappaR: Array | float,
- sigma_tilde: float,
- l_max: int = 20,
- sigma0: float | Array = 0) -> Expansion:
- """Create Expansion mapped from a quadrupolar point charge distribution."""
- l_array = np.array([l for l in range(l_max + 1) if l % 2 == 0])
- return create_mapped_rotsym_expansion(a_bar, kappaR, sigma_tilde, l_array, sigma0)
- def create_mapped_dipolar_expansion(a_bar: Array | float,
- kappaR: Array | float,
- sigma_tilde: float,
- l_max: int = 20,
- sigma0: float | Array = 0) -> Expansion:
- """Create Expansion mapped from a dipolar point charge distribution."""
- l_array = np.array([l for l in range(l_max + 1) if l % 2 == 1 or l == 0])
- return create_mapped_rotsym_expansion(a_bar, kappaR, sigma_tilde, l_array, sigma0)
- def create_gaussian_charge_expansion(omega_k: Array,
- lambda_k: Array | float,
- sigma1: float,
- l_max: int,
- sigma0: float | Array = 0,
- equal_charges: bool = True) -> Expansion:
- """
- Create Expansion for a collection of smeared charges on the sphere.
- :param omega_k: array of positions (theta, phi) of all charges
- :param lambda_k: smear parameter for each charge or smear for different cases (if equal_charges = True)
- :param sigma1: scaling
- :param l_max: maximal ell value for the expansion
- :param sigma0: total (mean) charge density
- :param equal_charges: if this is False, length of lambda_k should be N. If True, theta0_k array will be treated
- as different expansion cases
- """
- omega_k = omega_k.reshape(-1, 2)
- if not isinstance(lambda_k, Array):
- 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("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 = expansion.full_lm_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, :, 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 = expansion_total_charge(coefs, sigma0, l_array)
- l_array, coefs = purge_unneeded_l(l_array, coefs)
- return Expansion(l_array, coefs)
- def create_spherical_cap_expansion(omega_k: Array,
- theta0_k: Array | float,
- sigma1: float,
- l_max: int,
- sigma0: float | Array = 0,
- equal_sizes: bool = True) -> Expansion:
- """
- Create Expansion for a collection of spherical caps.
- :param omega_k: array of positions (theta, phi) of all spherical caps
- :param theta0_k: sizes of each spherical caps or cap sizes for different cases (if equal_sizes = True)
- :param sigma1: charge magnitude for the single cap, currently assumes that this is equal for all caps
- :param l_max: maximal ell value for the expansion
- :param sigma0: total (mean) charge density
- :param equal_sizes: if this is False, length of theta0_k should be N. If True, theta0_k array will be treated as
- different expansion cases
- """
- omega_k = omega_k.reshape(-1, 2)
- if not isinstance(theta0_k, Array):
- theta0_k = np.array(theta0_k)
- if equal_sizes:
- if theta0_k.ndim == 0:
- theta0_k = np.full(omega_k.shape[0], theta0_k)
- elif theta0_k.ndim == 1:
- theta0_k = np.full((omega_k.shape[0], theta0_k.shape[0]), theta0_k)
- else:
- raise ValueError(f'If equal_charges=True, theta0_k should be a 1D array, got shape {theta0_k.shape}')
- if theta0_k.shape[0] != omega_k.shape[0]:
- raise ValueError("Number of charges (length of omega_k) should match the last dimension of theta0_k array.")
- rotations = Quaternion(np.stack((np.cos(omega_k[..., 0] / 2),
- np.sin(omega_k[..., 1]) * np.sin(omega_k[..., 0] / 2),
- np.cos(omega_k[..., 1]) * np.sin(omega_k[..., 0] / 2),
- np.zeros_like(omega_k[..., 0]))).T)
- l_array = np.arange(l_max + 1)
- coefs_l0 = -sigma1 * (np.sqrt(np.pi / (2 * l_array[None, :] + 1)) *
- (eval_legendre(l_array[None, :] + 1, np.cos(theta0_k[..., None]))
- - eval_legendre(l_array[None, :] - 1, np.cos(theta0_k[..., None]))))
- coefs = rot_sym_expansion(l_array, coefs_l0)
- coefs_all_single_caps = expansion.expansion_rotation(rotations, coefs, l_array)
- # Rotating is implemented in such a way that it rotates every patch to every position,
- # hence the redundancy of out of diagonal elements.
- coefs_all = np.sum(np.diagonal(coefs_all_single_caps), axis=-1)
- coefs_all = expansion_total_charge(coefs_all, sigma0, l_array)
- return Expansion(l_array, np.squeeze(coefs_all))
- def net_charge_map(sigma0: float | Array, kappaR: float | Array):
- return sigma0 * np.exp(kappaR) / np.sinh(kappaR) * kappaR / (1 + kappaR)
- 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_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 expansion_total_charge(coefs: Array, sigma0: float | Array | None, l_vals: Array):
- """Adds a new axis to the expansion coefficients that modifies expansion based on given net charge density."""
- if l_vals[0] != 0:
- raise NotImplementedError("To modify total charge, the first expansion term should be l=0. "
- "Adding l=0 term not yet implemented (TODO).")
- if sigma0 is None:
- return coefs
- if not isinstance(sigma0, Array):
- x = copy.deepcopy(coefs)
- x[..., 0] = sigma0 * np.sqrt(4 * np.pi)
- return x
- # insert new axis in the 2nd-to-last place and repeat the expansion data over this new axis
- x = np.repeat(np.expand_dims(coefs, -2), sigma0.shape[-1], axis=-2)
- x[..., 0] = sigma0 * np.sqrt(4 * np.pi)
- return x
- def purge_unneeded_l(l_array: Array, coefs: Array) -> (Array, Array):
- """Remove ell values from expansion for which all (ell, m) coefficients are zero."""
- def delete_zero_entries(l, l_arr, cfs):
- l_idx = np.where(l_arr == l)[0][0]
- m_indices = expansion.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
|