Coverage for local_installation_linux/mumott/core/wigner_d_utilities.py: 91%
83 statements
« prev ^ index » next coverage.py v7.3.2, created at 2025-05-05 21:21 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2025-05-05 21:21 +0000
1import numpy as np
2import importlib.resources
3import h5py
4import logging
5from typing import List
6from numpy.typing import NDArray
8logger = logging.getLogger(__name__)
11def load_d_matrices(ell_max: int) -> List[NDArray[float]]:
12 """ Load Wigner (small) d-matrices corresponding to a rotation by 90 degrees
13 around the x-axis up to order :attr:`ell_max`.
14 The sign and normalization conventions are those adopted throughout :program:`mumott`.
16 Parameters
17 ----------
18 ell_max
19 maximum order of the Wigner (small) d-matrices to return
21 Returns
22 ---------
23 list of Wigner (small) d-matrices as numpy arrays with shape ``(l*2+1)x(l*2+1)``
24 """
25 d_matrices = []
26 # Open h5 file
27 with importlib.resources.path(__package__, 'wigner_d_matrices.h5') as p:
28 path = p
30 with h5py.File(path, 'r') as file:
31 for ell in range(ell_max + 1):
32 d_matrices.append(file[str(ell)][...].T)
33 return d_matrices
36def calculate_sph_coefficients_rotated_by_90_degrees_around_positive_x(
37 input_array: NDArray[float],
38 ell_list: List[int],
39 d_matrices: List[NDArray[float]],
40 output_array: NDArray[float] = None,
41 ) -> NDArray[float]:
42 r""" Calculate the spherical harmonic coefficients after a rotation by 90 degrees around +x.
44 Parameters
45 ----------
46 input_array
47 spherical harmonic coefficients
48 ell_list
49 list of :math:`\ell` values to use. If ``None`` (default) we assume that all even orders are inluded
50 until some :math:`\ell_\mathrm{max}`, which will be calculated from the shape of the coefficients
51 array.
52 d_matrices
53 list of Wigner (small) d-matrices corresponding to a 90 degree rotation around x.
54 If ``None`` (default) pre-calculated matrix elements are used.
55 output_array
56 numpy array with same shape as :attr:`input_array`.
57 If ``None`` (default) a new array is initialized.
58 If :attr:`output_array` is given, the calculations are carried out in place.
60 Returns
61 ---------
62 numpy array with the same shape as :attr:`input_array`.
63 """
64 if output_array is None: 64 ↛ 65line 64 didn't jump to line 65, because the condition on line 64 was never true
65 output_array = np.zeros(input_array.shape, dtype=float)
67 start_index = 0
68 for ell in ell_list:
69 output_array[..., start_index:start_index+(2*ell+1)]\
70 = np.einsum('...m,nm->...n', input_array[..., start_index:start_index+(2*ell+1)], d_matrices[ell])
71 start_index += 2 * ell + 1
72 return output_array
75def calculate_sph_coefficients_rotated_by_90_degrees_around_negative_x(
76 input_array: NDArray[float],
77 ell_list: List[int],
78 d_matrices: List[NDArray[float]],
79 output_array: NDArray[float] = None,
80 ) -> NDArray[float]:
81 r""" Calculate the spherical harmonic coefficients after a rotation by 90 degrees around -x.
83 Parameters
84 ----------
85 input_array
86 spherical harmonic coefficients
87 ell_list
88 list of :math:`\ell` values to use. If ``None`` (default) we assume that all even orders are inluded
89 until some :math:`\ell_\mathrm{max}`, which will be calculated from the shape of the coefficients
90 array.
91 d_matrices
92 list of Wigner (small) d-matrices corresponding to a 90 degree rotation around x.
93 If ``None`` (default) pre-calculated matrix elements are used.
94 output_array
95 numpy array with same shape as :attr:`input_array`.
96 If ``None`` (default) a new array is initialized.
97 If :attr:`output_array` is given, the calculations are carried out in place.
99 Returns
100 ---------
101 numpy array with same shape as :attr:`input_array`.
102 """
103 if output_array is None: 103 ↛ 104line 103 didn't jump to line 104, because the condition on line 103 was never true
104 output_array = np.zeros(input_array.shape, dtype=float)
106 start_index = 0
107 for ell in ell_list:
108 output_array[..., start_index:start_index+(2*ell+1)]\
109 = np.einsum('...m,mn->...n', input_array[..., start_index:start_index+(2*ell+1)], d_matrices[ell])
110 start_index += 2 * ell + 1
111 return output_array
114def calculate_sph_coefficients_rotated_around_z(
115 input_array: NDArray[float],
116 angle: NDArray[float],
117 ell_list: List[int],
118 output_array: NDArray[float] = None,
119 ) -> NDArray[float]:
120 r""" Calculate the spherical harmonic coefficients after a rotation around +z.
122 Parameters
123 ----------
124 input_array
125 spherical harmonic coefficients
126 angle
127 either an array with the same shape as ``input_array.shape[:-1]`` or a scalar.
128 If a scalar is given all coefficient lists are rotated by the same angle.
129 ell_list
130 list of :math:`\ell` values to use. If ``None`` (default) we assume that all even orders are inluded
131 until some :math:`\ell_\mathrm{max}`, which will be calculated from the shape of the coefficients
132 array.
133 d_matrices
134 list of Wigner (small) d-matrices corresponding to a 90 degree rotation around x.
135 If ``None`` (default) pre-calculated matrix elements are used.
136 output_array
137 numpy array with same shape as :attr:`input_array`.
138 If ``None`` (default) a new array is initialized.
139 If :attr:`output_array` is given, the calculations are carried out in place.
141 Returns
142 ---------
143 numpy array with same shape as :attr:`input_array`.
144 """
145 if output_array is None:
146 output_array = np.zeros(input_array.shape, dtype=float)
148 start_index = 0
150 for ell in ell_list:
152 if ell == 0:
153 output_array[..., 0] = input_array[..., 0]
154 else:
155 m = np.arange(-ell, ell+1) # using negative ms is a comp. trick
156 ma = np.einsum('m,...->...m', m, angle)
157 output_array[..., start_index:start_index+(2*ell+1)]\
158 = input_array[..., start_index:start_index+(2*ell+1)] * np.cos(ma)\
159 - input_array[..., start_index+(2*ell+1)-1:start_index-1:-1] * np.sin(ma)
161 start_index += 2 * ell + 1
163 return output_array
166def calculate_sph_coefficients_rotated_around_z_derived_wrt_the_angle(
167 input_array: NDArray[float],
168 angle: List[float],
169 ell_list: List[int],
170 output_array: NDArray[float] = None,
171 ) -> NDArray[float]:
172 r""" Calculate the angular derivatives of spherical harmonic coefficients with respect
173 at the specified angle, where the angle refers to the rotation around +z.
175 Parameters
176 ----------
177 input_array
178 spherical harmonic coefficients
179 angle
180 either an array with the same shape as :attr:`input_array.shape[:-1]` or a scalar.
181 If a scalar is given all coefficient lists are rotated by the same angle.
182 ell_list
183 list of :math:`\ell` values to use. If ``None`` (default) we assume that all even orders are inluded
184 until some :math:`\ell_\mathrm{max}`, which will be calculated from the shape of the coefficients
185 array.
186 d_matrices
187 list of Wigner (small) d-matrices corresponding to a 90 degree rotation about x.
188 If ``None`` (default) pre-calculated matrix elements are used.
189 output_array
190 numpy array with same shape as :attr:`input_array`.
191 If ``None`` (default) a new array is initialized.
192 If :attr:`output_array` is given, the calculations are carried out in place.
194 Returns
195 ---------
196 numpy array with same shape as :attr:`input_array`.
197 """
198 if output_array is None:
199 output_array = np.zeros(input_array.shape, dtype=float)
201 start_index = 0
203 for ell in ell_list:
205 if ell == 0:
206 output_array[..., 0] = 0
207 else:
208 m = np.arange(-ell, ell+1) # using negative ms is a comp. trick
209 ma = np.einsum('m,...->...m', m, angle)
210 m = np.einsum('m,...->...m', m, np.ones(angle.shape)) # Hacky indexing trick
212 output_array[..., start_index:start_index+(2*ell+1)]\
213 = input_array[..., start_index:start_index+(2*ell+1)] * (-m) * np.sin(ma)\
214 - input_array[..., start_index+(2*ell+1)-1:start_index-1:-1] * m * np.cos(ma)
216 start_index += 2 * ell + 1
218 return output_array
221def calculate_sph_coefficients_rotated_by_euler_angles(
222 input_array: NDArray[float],
223 Psi: NDArray[float],
224 Theta: NDArray[float],
225 Phi: NDArray[float],
226 ell_list: List[int] = None,
227 d_matrices: List[NDArray] = None,
228 output_array: NDArray[float] = None,
229 ) -> NDArray[float]:
230 r"""Calculate the spherical harmonics coefficients after a rotation specified by a set of Euler angles.
231 The Euler angles need to be given in 'zyz' format.
232 A rotation with ``(0, Theta, Phi)`` will move the z-axis into the coordinates ``(Theta, Phi)``.
234 Parameters
235 ----------
236 input_array
237 array, the last dimension of which runs over the spherical harmonics coefficients.
238 Psi
239 First Euler angle. Initial rotation about the z-axis. Can be either a scalar
240 or a numpy array with shape :attr:`input_array.shape[:-1]`.
241 If ``None`` the rotation is skipped.
242 Theta
243 Second Euler angle. Rotation about the y-axis. Can be either a scalar
244 or a numpy array with shape ``input_array.shape[:-1]``.
245 If ``None`` the rotation is skipped.
246 Phi
247 Third Euler angle. Final rotation about the z-axis. Can be either a scalar
248 or a numpy array with shape ``input_array.shape[:-1]``.
249 If ``None`` the rotation is skipped.
250 ell_list
251 list of :math:`\ell` values to use. If ``None`` (default) we assume that all even orders are inluded
252 until some :math:`\ell_\mathrm{max}`, which will be calculated from the shape of the coefficients
253 array.
254 d_matrices
255 list of Wigner (small) d-matrices corresponding to a 90 degree rotation around x.
256 If ``None`` (default) pre-calculated matrix elements are used.
257 output_array
258 numpy array with same shape as :attr:`input_array`.
259 If ``None`` (default) a new array is initialized.
260 If :attr:`output_array` is given, the calculations are carried out in place.
262 Returns
263 ---------
264 numpy array with same shape as :attr:`input_array`.
265 """
267 if output_array is None: 267 ↛ 270line 267 didn't jump to line 270, because the condition on line 267 was never false
268 output_array = np.zeros(input_array.shape, dtype=float)
270 if isinstance(ell_list, int): 270 ↛ 271line 270 didn't jump to line 271, because the condition on line 270 was never true
271 ell_list = np.arange(0, ell_list+1, 2)
272 elif ell_list is None: 272 ↛ 290line 272 didn't jump to line 290, because the condition on line 272 was never false
273 # figure out what ell_max is
274 num_coeffs = input_array.shape[-1]
275 ell = 0
276 cum_sum = 1
277 while cum_sum < num_coeffs:
278 ell += 2
279 cum_sum += 2 * ell + 1
281 if cum_sum == num_coeffs: 281 ↛ 284line 281 didn't jump to line 284, because the condition on line 281 was never false
282 ell_list = np.arange(0, ell+1, 2)
283 else:
284 raise ValueError('ell_max cannot be derived from input')
286 if d_matrices is None:
287 d_matrices = load_d_matrices(np.max(ell_list))
289 # Do z rotation of Psi
290 if Psi is not None:
291 output_array = calculate_sph_coefficients_rotated_around_z(input_array,
292 Psi,
293 ell_list,
294 output_array=output_array)
295 else:
296 output_array = np.array(input_array, dtype=float)
297 # Do y rotation about Theta
298 if Theta is not None: 298 ↛ 315line 298 didn't jump to line 315, because the condition on line 298 was never false
299 # Do 90 degree rotation around x
300 calculate_sph_coefficients_rotated_by_90_degrees_around_positive_x(output_array,
301 ell_list,
302 d_matrices,
303 output_array=output_array)
304 # Do z rotation of Theta
305 calculate_sph_coefficients_rotated_around_z(output_array,
306 Theta,
307 ell_list,
308 output_array=output_array)
309 # Do -90 degree rotation around x
310 calculate_sph_coefficients_rotated_by_90_degrees_around_negative_x(output_array,
311 ell_list,
312 d_matrices,
313 output_array=output_array)
314 # Do z rotation of Phi
315 if Phi is not None:
316 # Do z rotation of Phi
317 calculate_sph_coefficients_rotated_around_z(output_array,
318 Phi,
319 ell_list,
320 output_array=output_array)
322 return output_array