pybhpt 0.9.10__cp313-cp313-musllinux_1_2_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pybhpt/swsh.py ADDED
@@ -0,0 +1,659 @@
1
+ import scipy.sparse
2
+ import scipy.sparse.linalg
3
+ # from scipy.special import sph_harm_y
4
+ # from scipy.special import binom
5
+ # from scipy.special import factorial
6
+ import numpy as np
7
+ from cybhpt_full import _YslmCy, _YslmCy_derivative, _YslmCy_derivative2, _clebschCy, _w3jCy
8
+
9
+ """
10
+ Wigner 3j-symbol and Clebsch-Gordon coefficients
11
+ """
12
+
13
+ def fac(n):
14
+ """
15
+ Computes the factorial of a non-negative integer n.
16
+
17
+ Parameters
18
+ ----------
19
+ n : int
20
+ A non-negative integer.
21
+
22
+ Returns
23
+ -------
24
+ float
25
+ The factorial of n.
26
+ """
27
+ if n < 0:
28
+ return 0
29
+ return float(np.math.factorial(n))
30
+
31
+ def Yslm(s, l, m, th, ph = None):
32
+ """
33
+ Evaluate the spin-weighted spherical harmonic $Y_{s}^{lm}$ at a given angle theta.
34
+
35
+ Parameters
36
+ ----------
37
+ s : int
38
+ The spin weight of the harmonic.
39
+ l : int
40
+ The angular number of the spherical harmonic.
41
+ m : int
42
+ The azimuthal number of the spherical harmonic.
43
+ th : array_like
44
+ The polar angle(s) at which to evaluate the spherical harmonic.
45
+
46
+ Returns
47
+ -------
48
+ array_like
49
+ The values of the spherical harmonic at the specified angles.
50
+ """
51
+ assert isinstance(s, int) and isinstance(l, int) and isinstance(m, int), "s, l, and m must be integers"
52
+ if ph is not None:
53
+ return Yslm(s, l, m, th)*np.exp(1.j*m*ph)
54
+ if np.abs(s) > l:
55
+ return 0.*th
56
+
57
+ return _YslmBase(s, l, m, th)
58
+
59
+ def Yslm_derivative(s, l, m, th, ph = None):
60
+ """
61
+ Evaluate the derivative of the spin-weighted spherical harmonic $Y_{s}^{lm}$ at a given angle theta.
62
+
63
+ Parameters
64
+ ----------
65
+ s : int
66
+ The spin weight of the harmonic.
67
+ l : int
68
+ The angular number of the spherical harmonic.
69
+ m : int
70
+ The azimuthal number of the spherical harmonic.
71
+ th : array_like
72
+ The polar angle(s) at which to evaluate the spherical harmonic.
73
+
74
+ Returns
75
+ -------
76
+ array_like
77
+ The derivatives of the spherical harmonic at the specified angles.
78
+ """
79
+ assert isinstance(s, int) and isinstance(l, int) and isinstance(m, int), "s, l, and m must be integers"
80
+ if ph is not None:
81
+ return Yslm_derivative(s, l, m, th)*np.exp(1.j*m*ph)
82
+ if np.abs(s) > l:
83
+ return 0.*th
84
+ return _YslmDerivBase(s, l, m, th)
85
+
86
+ def Yslm_derivative2(s, l, m, th, ph = None):
87
+ """
88
+ Evaluate the second derivative of the spin-weighted spherical harmonic $Y_{s}^{lm}$ at a given angle theta.
89
+
90
+ Parameters
91
+ ----------
92
+ s : int
93
+ The spin weight of the harmonic.
94
+ l : int
95
+ The angular number of the spherical harmonic.
96
+ m : int
97
+ The azimuthal number of the spherical harmonic.
98
+ th : array_like
99
+ The polar angle(s) at which to evaluate the spherical harmonic.
100
+
101
+ Returns
102
+ -------
103
+ array_like
104
+ The second derivatives of the spherical harmonic at the specified angles.
105
+ """
106
+ assert isinstance(s, int) and isinstance(l, int) and isinstance(m, int), "s, l, and m must be integers"
107
+ if ph is not None:
108
+ return Yslm_derivative2(s, l, m, th)*np.exp(1.j*m*ph)
109
+ if np.abs(s) > l:
110
+ return 0.*th
111
+ return _YslmDeriv2Base(s, l, m, th)
112
+
113
+ def _YslmBase(s, l, m, th):
114
+ assert isinstance(s, int) and isinstance(l, int) and isinstance(m, int), "s, l, and m must be integers"
115
+ if not isinstance(th, (int, float)):
116
+ b = np.broadcast(th)
117
+ out = np.empty(b.shape)
118
+ out.flat = [_YslmCy(s, l, m, thi) for (thi,) in b]
119
+ else:
120
+ out = _YslmCy(s, l, m, th)
121
+ return out
122
+
123
+ def _YslmDerivBase(s, l, m, th):
124
+ assert isinstance(s, int) and isinstance(l, int) and isinstance(m, int), "s, l, and m must be integers"
125
+ if not isinstance(th, (int, float)):
126
+ b = np.broadcast(th)
127
+ out = np.empty(b.shape)
128
+ out.flat = [_YslmCy_derivative(s, l, m, thi) for (thi,) in b]
129
+ else:
130
+ out = _YslmCy_derivative(s, l, m, th)
131
+ return out
132
+
133
+ def _YslmDeriv2Base(s, l, m, th):
134
+ assert isinstance(s, int) and isinstance(l, int) and isinstance(m, int), "s, l, and m must be integers"
135
+ if not isinstance(th, (int, float)):
136
+ b = np.broadcast(th)
137
+ out = np.empty(b.shape)
138
+ out.flat = [_YslmCy_derivative2(s, l, m, thi) for (thi,) in b]
139
+ else:
140
+ out = _YslmCy_derivative2(s, l, m, th)
141
+ return out
142
+
143
+ def Yslm_eigenvalue(s, l, *args):
144
+ return l*(l + 1.) - s*(s + 1.)
145
+
146
+ def clebsch(l1, l2, l3, m1, m2, m3):
147
+ """
148
+ Compute the Clebsch-Gordon coefficient <l1,m1,l2,m2|l3,m3>.
149
+
150
+ Parameters
151
+ ----------
152
+ l1 : int
153
+ The angular number of the first state.
154
+ l2 : int
155
+ The angular number of the second state.
156
+ l3 : int
157
+ The angular number of the combined state.
158
+ m1 : int
159
+ The azimuthal number of the first state.
160
+ m2 : int
161
+ The azimuthal number of the second state.
162
+ m3 : int
163
+ The azimuthal number of the combined state.
164
+
165
+ Returns
166
+ -------
167
+ float
168
+ The Clebsch-Gordon coefficient <l1,m1,l2,m2|l3,m3>.
169
+ """
170
+ assert isinstance(l1, int) and isinstance(l2, int) and isinstance(l3, int), "l1, l2, and l3 must be integers"
171
+ assert isinstance(m1, int) and isinstance(m2, int) and isinstance(m3, int), "m1, m2, and m3 must be integers"
172
+ return _clebschCy(l1, l2, l3, m1, m2, m3)
173
+
174
+ def w3j(l1, l2, l3, m1, m2, m3):
175
+ """
176
+ Compute the Wigner 3j-symbol
177
+ | l1 l2 l3 |
178
+ | m1 m2 m3 |
179
+
180
+ Parameters
181
+ ----------
182
+ l1 : int
183
+ The angular number of the first state.
184
+ l2 : int
185
+ The angular number of the second state.
186
+ l3 : int
187
+ The angular number of the combined state.
188
+ m1 : int
189
+ The azimuthal number of the first state.
190
+ m2 : int
191
+ The azimuthal number of the second state.
192
+ m3 : int
193
+ The azimuthal number of the combined state.
194
+
195
+ Returns
196
+ -------
197
+ float
198
+ The Wigner 3j-symbol $ \begin{pmatrix} l1 & l2 & l3 \\ m1 & m2 & m3 \end{pmatrix} $
199
+ """
200
+ assert isinstance(l1, int) and isinstance(l2, int) and isinstance(l3, int), "l1, l2, and l3 must be integers"
201
+ assert isinstance(m1, int) and isinstance(m2, int) and isinstance(m3, int), "m1, m2, and m3 must be integers"
202
+ return _w3jCy(l1, l2, l3, m1, m2, m3)
203
+
204
+ """
205
+ SWSH Eigenvalue Functions
206
+ """
207
+
208
+ def k1(s, l, j, m):
209
+ return np.sqrt((2*l + 1)/(2*j + 1))*clebsch(l, 1, j, m, 0, m)*clebsch(l, 1, j, -s, 0, -s);
210
+
211
+ def k2(s, l, j, m):
212
+ ktemp = 2./3.*np.sqrt((2*l + 1)/(2*j + 1))*clebsch(l, 2, j, m, 0, m)*clebsch(l, 2, j, -s, 0, -s);
213
+ if j == l:
214
+ ktemp += 1/3.
215
+ return ktemp
216
+
217
+ def k2m2(s, l, m):
218
+ temp = (l - m - 1.)/(l - 1.)
219
+ temp *= (l + m - 1.)/(l - 1.)
220
+ temp *= np.float64(l - m)/l
221
+ temp *= np.float64(l + m)/l
222
+ temp *= (l - s)/(2.*l - 1.)
223
+ temp *= (l + s)/(2.*l - 1.)
224
+ temp *= (l - s - 1.)/(2.*l + 1.)
225
+ temp *= (l + s - 1.)/(2.*l - 3.)
226
+ return np.sqrt(temp)
227
+
228
+ def k2m1(s, l, m):
229
+ temp = np.float64(l - m)*np.float64(l + m)/(2.*l - 1.)
230
+ temp *= np.float64(l - s)*np.float64(l + s)/(2.*l + 1.)
231
+ return -2.*m*s*np.sqrt(temp)/l/(l - 1.)/(l + 1.)
232
+
233
+ def k2p0(s, l, m):
234
+ temp = np.float64(l*(l + 1.) - 3.*m*m)/(2.*l - 1.)/l
235
+ temp *= np.float64(l*(l + 1.) - 3.*s*s)/(2.*l + 3.)/(l + 1.)
236
+ return 1./3.*(1. + 2.*temp)
237
+
238
+ def k2p1(s, l, m):
239
+ temp = np.float64(l - m + 1.)*np.float64(l + m + 1.)/(2.*l + 1.)
240
+ temp *= np.float64(l - s + 1.)*np.float64(l + s + 1.)/(2.*l + 3.)
241
+ return -2.*m*s*np.sqrt(temp)/l/(l + 1.)/(l + 2.)
242
+
243
+ def k2p2(s, l, m):
244
+ temp = (l - m + 1.)/(l + 1.)
245
+ temp *= (l + m + 1.)/(l + 1.)
246
+ temp *= (l - m + 2.)/(l + 2.)
247
+ temp *= (l + m + 2.)/(l + 2.)
248
+ temp *= (l - s + 2.)/(2.*l + 3.)
249
+ temp *= (l + s + 2.)/(2.*l + 3.)
250
+ temp *= (l - s + 1.)/(2.*l + 1.)
251
+ temp *= (l + s + 1.)/(2.*l + 5.)
252
+ return np.sqrt(temp)
253
+
254
+ def k1m1(s, l, m):
255
+ temp = np.float64(l - m)*np.float64(l + m)/(2.*l - 1.)
256
+ temp *= np.float64(l - s)*np.float64(l + s)/(2.*l + 1.)
257
+ return np.sqrt(temp)/l
258
+
259
+ def k1p0(s, l, m):
260
+ return -np.float64(m*s)/l/(l + 1.)
261
+
262
+ def k1p1(s, l, m):
263
+ temp = np.float64(l - m + 1.)*np.float64(l + m + 1.)/(2.*l + 3.)
264
+ temp *= np.float64(l - s + 1.)*np.float64(l + s + 1.)/(2.*l + 1.)
265
+ return np.sqrt(temp)/(l + 1.)
266
+
267
+ def akm2(s, l, m, g):
268
+ return -g*g*k2m2(s, l, m)
269
+
270
+ def akm1(s, l, m, g):
271
+ return -g*g*k2m1(s, l, m) + 2.*s*g*k1m1(s, l, m)
272
+
273
+ def akp0(s, l, m, g):
274
+ return -g*g*k2p0(s, l, m) + 2.*s*g*k1p0(s, l, m) + l*(l + 1.) - s*(s + 1.) - 2.*m*g + g*g
275
+
276
+ def akp1(s, l, m, g):
277
+ return -g*g*k2p1(s, l, m) + 2.*s*g*k1p1(s, l, m)
278
+
279
+ def akp2(s, l, m, g):
280
+ return -g*g*k2p2(s, l, m)
281
+
282
+ def spectral_sparse_matrix(s, m, g, nmax):
283
+ lmin = max(abs(s), abs(m))
284
+ larray = np.arange(lmin, lmin + nmax)
285
+ return scipy.sparse.diags([akm2(s, larray[2:], m, g), akm1(s, larray[1:], m, g), akp0(s, larray, m, g), akp1(s, larray[:-1], m, g), akp2(s, larray[:-2], m, g)], [-2, -1, 0, 1, 2])
286
+
287
+ def swsh_eigs(s, l, m, g, nmax=None, return_eigenvectors=True):
288
+ lmin = max(abs(s), abs(m))
289
+ kval = l - lmin
290
+
291
+ if nmax is None:
292
+ buffer = round(20 + 2*np.abs(g))
293
+ Nmax = kval + buffer + 2
294
+ else:
295
+ if nmax < kval:
296
+ Nmax = kval + 5
297
+ else:
298
+ Nmax = nmax
299
+
300
+ mat = spectral_sparse_matrix(s, m, g, Nmax)
301
+ out = scipy.sparse.linalg.eigs(mat, k=Nmax-2, which='SM', return_eigenvectors=return_eigenvectors)
302
+
303
+ return out
304
+
305
+ def swsh_coeffs(s, l, m, g, th):
306
+ if g == 0.:
307
+ return Yslm(s, l, m, th)
308
+
309
+ _, eig = swsh_eigs(s, l, m, g, nmax=None, return_eigenvectors=True)
310
+ if g.imag == 0.:
311
+ coeffs = np.real(eig[l - max(abs(s), abs(m))])
312
+ else:
313
+ coeffs = eig[l - max(abs(s), abs(m))]
314
+ return coeffs
315
+
316
+ def swsh_eigenvalue(s, l, m, g, nmax=None):
317
+ """
318
+ Compute the eigenvalue of the spin-weighted spheroidal harmonic.
319
+
320
+ Parameters
321
+ ----------
322
+ s : int
323
+ The spin weight of the harmonic.
324
+ l : int
325
+ The angular number of the harmonic.
326
+ m : int
327
+ The azimuthal number of the harmonic.
328
+ g : float or complex
329
+ The spheroidicity parameter.
330
+ nmax : int, optional
331
+ The maximum number of basis functions to use in the computation. If None, a default value is chosen.
332
+
333
+ Returns
334
+ -------
335
+ float or complex
336
+ The eigenvalue of the spin-weighted spheroidal harmonic.
337
+ """
338
+ if g == 0.:
339
+ return Yslm_eigenvalue(s, l)
340
+
341
+ las = swsh_eigs(s, l, m, g, nmax=nmax, return_eigenvectors=False)
342
+
343
+ idx = l - max(abs(s), abs(m))
344
+ sorted_indices = np.argsort(np.real(las))
345
+ if idx < 0 or idx >= len(sorted_indices):
346
+ raise IndexError(f"Index {idx} out of bounds for eigenvalue array of length {len(sorted_indices)}")
347
+ if g.imag == 0.:
348
+ eigen = np.real(las)[sorted_indices[idx]]
349
+ else:
350
+ eigen = las[sorted_indices[idx]]
351
+ return eigen
352
+ class SWSHBase:
353
+ def __init__(self, *args):
354
+ arg_num = np.array(args).shape[0]
355
+ if arg_num < 3:
356
+ print('Error. Not enough arguments to create class')
357
+ pass
358
+
359
+ self.s = args[0]
360
+ self.j = args[1]
361
+ self.m = args[2]
362
+ self.lmin = max(abs(self.s), abs(self.m))
363
+ self.jmin = max(abs(self.s), abs(self.m))
364
+
365
+ if arg_num > 3:
366
+ self.spheroidicity = args[3]
367
+ if self.spheroidicity.imag == 0:
368
+ self.spheroidicity = np.real(self.spheroidicity)
369
+
370
+ class SWSHSeriesBase(SWSHBase):
371
+ def __init__(self, s, j, m, g):
372
+ SWSHBase.__init__(self, s, j, m, g)
373
+
374
+ def sparse_matrix(self, nmax):
375
+ return spectral_sparse_matrix(self.s, self.m, self.spheroidicity, nmax)
376
+
377
+ def eigs(self, nmax = None, **kwargs):
378
+ kval = self.j - self.jmin
379
+
380
+ if nmax is None:
381
+ buffer = round(20 + np.abs(2*self.spheroidicity))
382
+ Nmax = kval + buffer + 2
383
+ else:
384
+ if nmax < kval:
385
+ Nmax = kval + 5
386
+ else:
387
+ Nmax = nmax
388
+
389
+ if "k" not in kwargs.keys():
390
+ kwargs["k"] = Nmax - 2
391
+
392
+ if "which" not in kwargs.keys():
393
+ kwargs["which"] = 'SM'
394
+
395
+ if "return_eigenvectors" not in kwargs.keys():
396
+ kwargs["return_eigenvectors"] = True
397
+
398
+ mat = self.sparse_matrix(Nmax)
399
+ return scipy.sparse.linalg.eigs(mat, **kwargs)
400
+
401
+ def generate_eigenvalue(self):
402
+ if self.spheroidicity.imag == 0.:
403
+ las = np.real(self.eigs(return_eigenvectors=False))
404
+ else:
405
+ las = self.eigs(return_eigenvectors=False)
406
+ pos = np.argsort(np.real(las))[self.j - self.jmin]
407
+ return las[pos]
408
+
409
+ def generate_eigs(self):
410
+ las, eigs = self.eigs()
411
+ pos_vec = np.argsort(np.real(las))
412
+ pos = pos_vec[self.j - self.jmin]
413
+ if self.spheroidicity.imag == 0.:
414
+ eigs_temp = np.real(eigs[:, pos])
415
+ eigs_return = np.sign(eigs_temp[self.j - self.jmin])*eigs_temp
416
+ eig = np.real(las[pos])
417
+ else:
418
+ eigs_temp = eigs[:, pos]
419
+ ref = eigs_temp[self.j - self.jmin]
420
+ eigs_temp = eigs_temp/ref
421
+ eigs_norm = np.linalg.norm(eigs_temp)
422
+ eigs_return = eigs_temp/eigs_norm
423
+ eig = las[pos]
424
+ return (eig, eigs_return)
425
+ class SpinWeightedSpheroidalHarmonic(SWSHSeriesBase):
426
+ """
427
+ A class for generating a spin-weighted spheroidal harmonic.
428
+
429
+ Parameters
430
+ ----------
431
+ s : int
432
+ The spin weight of the harmonic.
433
+ j : int
434
+ The angular number of the harmonic.
435
+ m : int
436
+ The azimuthal number of the harmonic.
437
+ g : float or complex
438
+ The spheroidicity parameter.
439
+
440
+ Attributes
441
+ ----------
442
+ couplingcoefficients : array_like
443
+ The coupling coefficients between the spin-weighted spheroidal harmonic and the spin-weighted spherical
444
+
445
+ """
446
+ def __init__(self, s, j, m, g):
447
+ SWSHSeriesBase.__init__(self, s, j, m, g)
448
+ if self.spheroidicity == 0.:
449
+ self.eval = self.Yslm
450
+ self.deriv = self.Yslm_derivative
451
+ self.deriv2 = self.Yslm_derivative2
452
+ self.eigenvalue = Yslm_eigenvalue(self.s, self.j)
453
+ self.coeffs = np.zeros(self.j - self.jmin + 1)
454
+ self.coeffs[-1] = 1.
455
+ self.mincouplingmode = self.j
456
+ self.maxcouplingmode = self.j
457
+ else:
458
+ self.eval = self.Sslm
459
+ self.deriv = self.Sslm_derivative
460
+ self.deriv2 = self.Sslm_derivative2
461
+ self.eigenvalue, self.coeffs = self.generate_eigs()
462
+ self.mincouplingmode = self.lmin
463
+ self.maxcouplingmode = self.lmin + self.coeffs.shape[0] - 1
464
+
465
+ @property
466
+ def couplingcoefficients(self):
467
+ return self.coeffs
468
+
469
+ def couplingcoefficient(self, l):
470
+ return self.coeffs[l - self.lmin]
471
+
472
+ def Yslm(self, l, th):
473
+ """
474
+ Evaluate the spin-weighted spherical harmonic $Y_{s}^{lm}(theta)$ at a given angle theta.
475
+
476
+ Parameters
477
+ ----------
478
+ l : int
479
+ The angular number of the spherical harmonic.
480
+ th : array_like
481
+ The polar angle(s) at which to evaluate the spherical harmonic.
482
+
483
+ Returns
484
+ -------
485
+ array_like
486
+ The values of the spherical harmonic at the specified angles.
487
+ """
488
+ return Yslm(self.s, l, self.m, th)
489
+
490
+ def Yslm_derivative(self, l, th):
491
+ """
492
+ Evaluate the derivative of the spin-weighted spherical harmonic $Y_{s}^{lm}(theta)$ at a given angle theta.
493
+
494
+ Parameters
495
+ ----------
496
+ l : int
497
+ The angular number of the spherical harmonic.
498
+ th : array_like
499
+ The polar angle(s) at which to evaluate the derivative of the spherical harmonic.
500
+
501
+ Returns
502
+ -------
503
+ array_like
504
+ The values of the derivative of the spherical harmonic at the specified angles.
505
+ """
506
+ return Yslm_derivative(self.s, l, self.m, th)
507
+
508
+ def Yslm_derivative2(self, l, th):
509
+ """
510
+ Evaluate the second derivative of the spin-weighted spherical harmonic $Y_{s}^{lm}(theta)$ at a given angle theta.
511
+
512
+ Parameters
513
+ ----------
514
+ l : int
515
+ The angular number of the spherical harmonic.
516
+ th : array_like
517
+ The polar angle(s) at which to evaluate the second derivative of the spherical harmonic.
518
+
519
+ Returns
520
+ -------
521
+ array_like
522
+ The values of the second derivative of the spherical harmonic at the specified angles.
523
+ """
524
+ return Yslm_derivative2(self.s, l, self.m, th)
525
+
526
+ def Sslm(self, *args):
527
+ """
528
+ Evaluate the spin-weighted spheroidal harmonic $S_{s}^{lm}(theta)$ at a given angle theta.
529
+
530
+ Parameters
531
+ ----------
532
+ th : array_like
533
+ The polar angle(s) at which to evaluate the spheroidal harmonic.
534
+
535
+ Returns
536
+ -------
537
+ array_like
538
+ The values of the spheroidal harmonic at the specified angles.
539
+ """
540
+ th = args[-1]
541
+ term_num = self.coeffs.shape[0]
542
+ if isinstance(th, (int, float)):
543
+ Yslm_array = np.empty(term_num)
544
+ else:
545
+ pts_num = th.shape[0]
546
+ Yslm_array = np.empty((term_num, pts_num))
547
+ for i in range(term_num):
548
+ Yslm_array[i] = self.Yslm(self.lmin + i, th)
549
+
550
+ return np.dot(self.coeffs, Yslm_array)
551
+
552
+ def Sslm_derivative(self, *args):
553
+ """
554
+ Evaluate the derivative of the spin-weighted spheroidal harmonic $S_{s}^{lm}(theta)$ at a given angle theta.
555
+
556
+ Parameters
557
+ ----------
558
+ th : array_like
559
+ The polar angle(s) at which to evaluate the spheroidal harmonic.
560
+
561
+ Returns
562
+ -------
563
+ array_like
564
+ The derivatives of the spheroidal harmonic at the specified angles.
565
+ """
566
+ th = args[-1]
567
+ term_num = self.coeffs.shape[0]
568
+ if isinstance(th, (int, float)):
569
+ dYslm_array = np.empty(term_num)
570
+ else:
571
+ pts_num = th.shape[0]
572
+ dYslm_array = np.empty((term_num, pts_num))
573
+ for i in range(term_num):
574
+ dYslm_array[i] = self.Yslm_derivative(self.lmin + i, th)
575
+
576
+ return np.dot(self.coeffs, dYslm_array)
577
+
578
+ def Sslm_derivative2(self, *args):
579
+ """
580
+ Evaluate the second derivative of the spin-weighted spheroidal harmonic $S_{s}^{lm}(theta)$ at a given angle theta.
581
+
582
+ Parameters
583
+ ----------
584
+ th : array_like
585
+ The polar angle(s) at which to evaluate the spheroidal harmonic.
586
+
587
+ Returns
588
+ -------
589
+ array_like
590
+ The second derivatives of the spheroidal harmonic at the specified angles.
591
+ """
592
+ th = args[-1]
593
+ term_num = self.coeffs.shape[0]
594
+ if isinstance(th, (int, float)):
595
+ dY2slm_array = np.empty(term_num)
596
+ else:
597
+ pts_num = th.shape[0]
598
+ dY2slm_array = np.empty((term_num, pts_num))
599
+ for i in range(term_num):
600
+ dY2slm_array[i] = self.Yslm_derivative2(self.lmin + i, th)
601
+
602
+ return np.dot(self.coeffs, dY2slm_array)
603
+
604
+ def __call__(self, th, ph = None, deriv = None):
605
+ if deriv is None or deriv == 0:
606
+ out = self.eval(self.j, th)
607
+ elif deriv == 1:
608
+ out = self.deriv(self.j, th)
609
+ elif deriv == 2:
610
+ out = self.deriv2(self.j, th)
611
+ else:
612
+ raise ValueError(f"Derivative order = {deriv} not supported")
613
+
614
+ if ph is not None:
615
+ out = out*np.exp(1.j*self.m*ph)
616
+ return out
617
+
618
+ def muCoupling(s, l):
619
+ """
620
+ Eigenvalue for the spin-weighted spherical harmonic lowering operator
621
+ Setting s -> -s gives the negative of the eigenvalue for the raising operator
622
+ """
623
+ if l + s < 0 or l - s + 1. < 0:
624
+ return 0
625
+ return np.sqrt((l - s + 1.)*(l + s))
626
+
627
+ def Asjlm(s, j, l, m):
628
+ """
629
+ Coupling coefficient between scalar and spin-weighted spherical harmonics
630
+
631
+ Parameters
632
+ ----------
633
+ s : int
634
+ The spin weight of the harmonic.
635
+ j : int
636
+ The angular number of the scalar harmonic.
637
+ l : int
638
+ The angular number of the spin-weighted harmonic.
639
+ m : int
640
+ The azimuthal number of the harmonics.
641
+
642
+ Returns
643
+ -------
644
+ float
645
+ The coupling coefficient $A_{s}^{jlm}$
646
+ """
647
+ if s >= 0:
648
+ return (-1.)**(m + s)*np.sqrt(4**s*fac(s)**2*(2*l + 1)*(2*j + 1)/fac(2*s))*w3j(s, l, j, 0, m, -m)*w3j(s, l, j, s, -s, 0)
649
+ else:
650
+ return (-1.)**(m)*np.sqrt(4**(-s)*fac(-s)**2*(2*l + 1)*(2*j + 1)/fac(-2*s))*w3j(-s, l, j, 0, m, -m)*w3j(-s, l, j, s, -s, 0)
651
+
652
+ def spin_operator_normalization(s, ns, l):
653
+ s_sgn = np.sign(s)
654
+ nmax1 = np.abs(s) + 1
655
+ Jterm = 1.
656
+ for ni in range(1, ns + 1):
657
+ Jterm *= -s_sgn*muCoupling((nmax1-ni), l)
658
+ return Jterm
659
+