jqnm 0.1.0__py3-none-any.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.
- jqnm/LICENSE +25 -0
- jqnm/__init__.py +113 -0
- jqnm/_version.py +1 -0
- jqnm/angular.py +432 -0
- jqnm/cached.py +660 -0
- jqnm/contfrac.py +166 -0
- jqnm/nearby.py +329 -0
- jqnm/radial.py +229 -0
- jqnm/schwarzschild/__init__.py +13 -0
- jqnm/schwarzschild/approx.py +151 -0
- jqnm/schwarzschild/data/Schw_dict.npz +0 -0
- jqnm/schwarzschild/overtonesequence.py +266 -0
- jqnm/schwarzschild/tabulated.py +266 -0
- jqnm/spinsequence.py +489 -0
- jqnm-0.1.0.dist-info/METADATA +218 -0
- jqnm-0.1.0.dist-info/RECORD +19 -0
- jqnm-0.1.0.dist-info/WHEEL +5 -0
- jqnm-0.1.0.dist-info/licenses/LICENSE +25 -0
- jqnm-0.1.0.dist-info/top_level.txt +1 -0
jqnm/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Cedric Ewen
|
|
4
|
+
Copyright (c) 2019 Leo C. Stein (original qnm package)
|
|
5
|
+
|
|
6
|
+
This package is derived from the qnm package (https://github.com/duetosymmetry/qnm)
|
|
7
|
+
by Leo C. Stein, which is also MIT licensed.
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
jqnm/__init__.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""jqnm - JAX-accelerated Kerr quasinormal modes.
|
|
2
|
+
|
|
3
|
+
A JAX-based rewrite of the qnm package for computing Kerr black hole
|
|
4
|
+
quasinormal mode frequencies, angular separation constants, and
|
|
5
|
+
spherical-spheroidal mixing coefficients.
|
|
6
|
+
|
|
7
|
+
This package provides GPU acceleration and automatic differentiation
|
|
8
|
+
capabilities via JAX.
|
|
9
|
+
|
|
10
|
+
The highest-level interface is via :class:`jqnm.cached.KerrSeqCache`,
|
|
11
|
+
which will fetch instances of :class:`jqnm.spinsequence.KerrSpinSeq`.
|
|
12
|
+
|
|
13
|
+
Examples
|
|
14
|
+
--------
|
|
15
|
+
|
|
16
|
+
>>> import jqnm
|
|
17
|
+
>>> # Modes are computed and cached automatically on first use
|
|
18
|
+
>>> grav_220 = jqnm.modes_cache(s=-2, l=2, m=2, n=0)
|
|
19
|
+
>>> omega, A, C = grav_220(a=0.68)
|
|
20
|
+
>>> print(omega)
|
|
21
|
+
(0.5239751042900845-0.08151262363119986j)
|
|
22
|
+
|
|
23
|
+
**Members**
|
|
24
|
+
|
|
25
|
+
.. autosummary::
|
|
26
|
+
|
|
27
|
+
jqnm.download_data
|
|
28
|
+
jqnm.modes_cache
|
|
29
|
+
|
|
30
|
+
.. autofunction:: download_data
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import print_function, division, absolute_import
|
|
35
|
+
|
|
36
|
+
# Enable 64-bit precision in JAX
|
|
37
|
+
import jax
|
|
38
|
+
jax.config.update("jax_enable_x64", True)
|
|
39
|
+
|
|
40
|
+
from ._version import __version__
|
|
41
|
+
|
|
42
|
+
__copyright__ = "Copyright (C) 2024 Cedric Ewen"
|
|
43
|
+
__email__ = "chessdric@gmail.com"
|
|
44
|
+
__status__ = "beta"
|
|
45
|
+
__author__ = "Cedric Ewen"
|
|
46
|
+
__license__ = """
|
|
47
|
+
MIT License
|
|
48
|
+
|
|
49
|
+
Copyright (c) 2024 Cedric Ewen
|
|
50
|
+
Copyright (c) 2019 Leo C. Stein (original qnm package)
|
|
51
|
+
|
|
52
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
53
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
54
|
+
in the Software without restriction, including without limitation the rights
|
|
55
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
56
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
57
|
+
furnished to do so, subject to the following conditions:
|
|
58
|
+
|
|
59
|
+
The above copyright notice and this permission notice shall be included in
|
|
60
|
+
all copies or substantial portions of the Software.
|
|
61
|
+
|
|
62
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
63
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
64
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
65
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
66
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
67
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
68
|
+
THE SOFTWARE.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
__credits__ = """
|
|
72
|
+
This package is a JAX-based rewrite of the qnm package by Leo C. Stein.
|
|
73
|
+
Original package: https://github.com/duetosymmetry/qnm
|
|
74
|
+
|
|
75
|
+
If you use this package in academic work, please cite:
|
|
76
|
+
Stein, Leo C. (2019). "qnm: A Python package for calculating Kerr
|
|
77
|
+
quasinormal modes, separation constants, and spherical-spheroidal
|
|
78
|
+
mixing coefficients." J. Open Source Softw., 4(42), 1683.
|
|
79
|
+
doi:10.21105/joss.01683, arXiv:1908.10377
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
from . import radial
|
|
83
|
+
from . import angular
|
|
84
|
+
from . import contfrac
|
|
85
|
+
from . import nearby
|
|
86
|
+
|
|
87
|
+
from . import spinsequence
|
|
88
|
+
|
|
89
|
+
from . import cached
|
|
90
|
+
from .cached import download_data
|
|
91
|
+
|
|
92
|
+
############################################################
|
|
93
|
+
## Package initialization
|
|
94
|
+
|
|
95
|
+
# Singleton for cache
|
|
96
|
+
modes_cache = cached.KerrSeqCache(init_schw=True)
|
|
97
|
+
"""Interface to the cache of QNMs. This is a singleton instance of
|
|
98
|
+
:class:`jqnm.cached.KerrSeqCache`. It can be called like a function
|
|
99
|
+
`jqnm.modes_cache(s,l,m,n)` to get a specific mode, the result being an
|
|
100
|
+
instance of :class:`jqnm.spinsequence.KerrSpinSeq`."""
|
|
101
|
+
|
|
102
|
+
# Ensure common versions of jitted functions are compiled
|
|
103
|
+
def _ensure_jitted():
|
|
104
|
+
finder = nearby.NearbyRootFinder(a=0.3, s=-2, m=2, A_closest_to=3.6+0.1j,
|
|
105
|
+
l_max=20, omega_guess=0.4-0.09j,
|
|
106
|
+
tol=1e-10, n_inv=0, Nr_max=4000)
|
|
107
|
+
finder.do_solve()
|
|
108
|
+
finder.set_params(Nr_max=float("inf"))
|
|
109
|
+
finder.do_solve()
|
|
110
|
+
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
_ensure_jitted()
|
jqnm/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
jqnm/angular.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Solve the angular Teukolsky equation via spectral decomposition.
|
|
3
|
+
|
|
4
|
+
For a given complex QNM frequency ω, the separation constant and
|
|
5
|
+
spherical-spheroidal decomposition are found as an eigenvalue and
|
|
6
|
+
eigenvector of an (infinite) matrix problem. The interface to solving
|
|
7
|
+
this problem is :meth:`C_and_sep_const_closest`, which returns a
|
|
8
|
+
certain eigenvalue A and eigenvector C. The eigenvector contains the
|
|
9
|
+
C coefficients in the equation
|
|
10
|
+
|
|
11
|
+
.. math:: {}_s Y_{\\ell m}(\\theta, \\phi; a\\omega) = {\\sum_{\\ell'=\\ell_{\\min} (s,m)}^{\\ell_\\max}} C_{\\ell' \\ell m}(a\\omega)\\ {}_s Y_{\\ell' m}(\\theta, \\phi) \\,.
|
|
12
|
+
|
|
13
|
+
Here ℓmin=max(\\|m\\|,\\|s\\|) (see :meth:`l_min`), and ℓmax can be chosen at
|
|
14
|
+
run time. The C coefficients are returned as a complex ndarray, with
|
|
15
|
+
the zeroth element corresponding to ℓmin. You can get the associated
|
|
16
|
+
ℓ values by calling :meth:`ells`.
|
|
17
|
+
|
|
18
|
+
This module uses JAX for GPU acceleration and automatic differentiation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import division, print_function, absolute_import
|
|
22
|
+
|
|
23
|
+
import jax
|
|
24
|
+
import jax.numpy as jnp
|
|
25
|
+
from jax import jit
|
|
26
|
+
from functools import partial
|
|
27
|
+
|
|
28
|
+
# TODO some documentation here, better documentation throughout
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _calF(s, l, m):
|
|
32
|
+
"""Eq. (52b)"""
|
|
33
|
+
# Handle the case when s=0 and l+1=0
|
|
34
|
+
l_float = jnp.asarray(l, dtype=jnp.float64)
|
|
35
|
+
m_float = jnp.asarray(m, dtype=jnp.float64)
|
|
36
|
+
s_float = jnp.asarray(s, dtype=jnp.float64)
|
|
37
|
+
|
|
38
|
+
denom1 = (2.0 * l_float + 3.0) * (2.0 * l_float + 1.0)
|
|
39
|
+
denom2 = (l_float + 1.0) ** 2
|
|
40
|
+
|
|
41
|
+
# Avoid division by zero
|
|
42
|
+
safe_denom1 = jnp.where(denom1 == 0.0, 1.0, denom1)
|
|
43
|
+
safe_denom2 = jnp.where(denom2 == 0.0, 1.0, denom2)
|
|
44
|
+
|
|
45
|
+
result = jnp.sqrt(
|
|
46
|
+
((l_float + 1.0) ** 2 - m_float * m_float) / safe_denom1
|
|
47
|
+
) * jnp.sqrt(((l_float + 1.0) ** 2 - s_float * s_float) / safe_denom2)
|
|
48
|
+
|
|
49
|
+
# Return 0 when s=0 and l+1=0
|
|
50
|
+
return jnp.where((s == 0) & (l + 1 == 0), 0.0, result)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _calG(s, l, m):
|
|
54
|
+
"""Eq. (52c)"""
|
|
55
|
+
l_float = jnp.asarray(l, dtype=jnp.float64)
|
|
56
|
+
m_float = jnp.asarray(m, dtype=jnp.float64)
|
|
57
|
+
s_float = jnp.asarray(s, dtype=jnp.float64)
|
|
58
|
+
|
|
59
|
+
denom1 = 4.0 * l_float * l_float - 1.0
|
|
60
|
+
denom2 = l_float * l_float
|
|
61
|
+
|
|
62
|
+
safe_denom1 = jnp.where(denom1 == 0.0, 1.0, denom1)
|
|
63
|
+
safe_denom2 = jnp.where(denom2 == 0.0, 1.0, denom2)
|
|
64
|
+
|
|
65
|
+
result = jnp.sqrt((l_float * l_float - m_float * m_float) / safe_denom1) * jnp.sqrt(
|
|
66
|
+
1.0 - s_float * s_float / safe_denom2
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return jnp.where(l == 0, 0.0, result)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _calH(s, l, m):
|
|
73
|
+
"""Eq. (52d)"""
|
|
74
|
+
l_float = jnp.asarray(l, dtype=jnp.float64)
|
|
75
|
+
m_float = jnp.asarray(m, dtype=jnp.float64)
|
|
76
|
+
s_float = jnp.asarray(s, dtype=jnp.float64)
|
|
77
|
+
|
|
78
|
+
denom = l_float * (l_float + 1.0)
|
|
79
|
+
safe_denom = jnp.where(denom == 0.0, 1.0, denom)
|
|
80
|
+
|
|
81
|
+
result = -m_float * s_float / safe_denom
|
|
82
|
+
|
|
83
|
+
return jnp.where((l == 0) | (s == 0), 0.0, result)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _calA(s, l, m):
|
|
87
|
+
"""Eq. (53a)"""
|
|
88
|
+
return _calF(s, l, m) * _calF(s, l + 1, m)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _calD(s, l, m):
|
|
92
|
+
"""Eq. (53b)"""
|
|
93
|
+
return _calF(s, l, m) * (_calH(s, l + 1, m) + _calH(s, l, m))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _calB(s, l, m):
|
|
97
|
+
"""Eq. (53c)"""
|
|
98
|
+
return (
|
|
99
|
+
_calF(s, l, m) * _calG(s, l + 1, m)
|
|
100
|
+
+ _calG(s, l, m) * _calF(s, l - 1, m)
|
|
101
|
+
+ _calH(s, l, m) ** 2
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _calE(s, l, m):
|
|
106
|
+
"""Eq. (53d)"""
|
|
107
|
+
return _calG(s, l, m) * (_calH(s, l - 1, m) + _calH(s, l, m))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _calC(s, l, m):
|
|
111
|
+
"""Eq. (53e)"""
|
|
112
|
+
return _calG(s, l, m) * _calG(s, l - 1, m)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def swsphericalh_A(s, l, m):
|
|
116
|
+
"""Angular separation constant at a=0.
|
|
117
|
+
|
|
118
|
+
Eq. (50). Has no dependence on m. The formula is
|
|
119
|
+
A_0 = l(l+1) - s(s+1)
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
s: int
|
|
124
|
+
Spin-weight of interest
|
|
125
|
+
|
|
126
|
+
l: int
|
|
127
|
+
Angular quantum number of interest
|
|
128
|
+
|
|
129
|
+
m: int
|
|
130
|
+
Magnetic quantum number, ignored
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
int
|
|
135
|
+
Value of A(a=0) = l(l+1) - s(s+1)
|
|
136
|
+
"""
|
|
137
|
+
return l * (l + 1) - s * (s + 1)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def M_matrix_elem(s, c, m, l, lprime):
|
|
141
|
+
"""The (l, lprime) matrix element from the spherical-spheroidal
|
|
142
|
+
decomposition matrix from Eq. (55).
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
s: int
|
|
147
|
+
Spin-weight of interest
|
|
148
|
+
|
|
149
|
+
c: complex
|
|
150
|
+
Oblateness of the spheroidal harmonic
|
|
151
|
+
|
|
152
|
+
m: int
|
|
153
|
+
Magnetic quantum number
|
|
154
|
+
|
|
155
|
+
l: int
|
|
156
|
+
Angular quantum number of interest
|
|
157
|
+
|
|
158
|
+
lprime: int
|
|
159
|
+
Primed quantum number of interest
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
complex
|
|
164
|
+
Matrix element M_{l, lprime}
|
|
165
|
+
"""
|
|
166
|
+
c = jnp.asarray(c, dtype=jnp.complex128)
|
|
167
|
+
|
|
168
|
+
# Compute all possible values
|
|
169
|
+
val_lm2 = -c * c * _calA(s, lprime, m)
|
|
170
|
+
val_lm1 = -c * c * _calD(s, lprime, m) + 2 * c * s * _calF(s, lprime, m)
|
|
171
|
+
val_l0 = (
|
|
172
|
+
swsphericalh_A(s, lprime, m)
|
|
173
|
+
- c * c * _calB(s, lprime, m)
|
|
174
|
+
+ 2 * c * s * _calH(s, lprime, m)
|
|
175
|
+
)
|
|
176
|
+
val_lp1 = -c * c * _calE(s, lprime, m) + 2 * c * s * _calG(s, lprime, m)
|
|
177
|
+
val_lp2 = -c * c * _calC(s, lprime, m)
|
|
178
|
+
|
|
179
|
+
# Select the appropriate value based on lprime - l
|
|
180
|
+
diff = lprime - l
|
|
181
|
+
result = jnp.where(
|
|
182
|
+
diff == -2,
|
|
183
|
+
val_lm2,
|
|
184
|
+
jnp.where(
|
|
185
|
+
diff == -1,
|
|
186
|
+
val_lm1,
|
|
187
|
+
jnp.where(
|
|
188
|
+
diff == 0,
|
|
189
|
+
val_l0,
|
|
190
|
+
jnp.where(
|
|
191
|
+
diff == 1, val_lp1, jnp.where(diff == 2, val_lp2, 0.0 + 0.0j)
|
|
192
|
+
),
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return result
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def l_min(s, m):
|
|
201
|
+
"""Minimum allowed value of l for a given s, m.
|
|
202
|
+
|
|
203
|
+
The formula is l_min = max(|m|,|s|).
|
|
204
|
+
|
|
205
|
+
Parameters
|
|
206
|
+
----------
|
|
207
|
+
s: int
|
|
208
|
+
Spin-weight of interest
|
|
209
|
+
|
|
210
|
+
m: int
|
|
211
|
+
Magnetic quantum number
|
|
212
|
+
|
|
213
|
+
Returns
|
|
214
|
+
-------
|
|
215
|
+
int
|
|
216
|
+
l_min
|
|
217
|
+
"""
|
|
218
|
+
return max(abs(s), abs(m))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def ells(s, m, l_max):
|
|
222
|
+
"""Vector of ℓ values in C vector and M matrix.
|
|
223
|
+
|
|
224
|
+
The format of the C vector and M matrix is that the 0th element
|
|
225
|
+
corresponds to l_min(s,m) (see :meth:`l_min`).
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
s: int
|
|
230
|
+
Spin-weight of interest
|
|
231
|
+
|
|
232
|
+
m: int
|
|
233
|
+
Magnetic quantum number
|
|
234
|
+
|
|
235
|
+
l_max: int
|
|
236
|
+
Maximum angular quantum number
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
int ndarray
|
|
241
|
+
Vector of ℓ values, starting from l_min
|
|
242
|
+
"""
|
|
243
|
+
return jnp.arange(l_min(s, m), l_max + 1)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def M_matrix(s, c, m, l_max):
|
|
247
|
+
"""Spherical-spheroidal decomposition matrix truncated at l_max.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
s: int
|
|
252
|
+
Spin-weight of interest
|
|
253
|
+
|
|
254
|
+
c: complex
|
|
255
|
+
Oblateness of the spheroidal harmonic
|
|
256
|
+
|
|
257
|
+
m: int
|
|
258
|
+
Magnetic quantum number
|
|
259
|
+
|
|
260
|
+
l_max: int
|
|
261
|
+
Maximum angular quantum number
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
complex ndarray
|
|
266
|
+
Decomposition matrix
|
|
267
|
+
"""
|
|
268
|
+
_ells = ells(s, m, l_max)
|
|
269
|
+
n = len(_ells)
|
|
270
|
+
c = jnp.asarray(c, dtype=jnp.complex128)
|
|
271
|
+
|
|
272
|
+
# Create index arrays
|
|
273
|
+
l_grid, lp_grid = jnp.meshgrid(_ells, _ells, indexing="ij")
|
|
274
|
+
|
|
275
|
+
# Compute all the coefficient arrays for each l value
|
|
276
|
+
# These are the building blocks for the matrix
|
|
277
|
+
|
|
278
|
+
# Diagonal (diff = 0)
|
|
279
|
+
A0 = swsphericalh_A(s, lp_grid, m).astype(jnp.complex128)
|
|
280
|
+
calB_vals = jax.vmap(lambda l: _calB(s, l, m))(_ells)
|
|
281
|
+
calH_vals = jax.vmap(lambda l: _calH(s, l, m))(_ells)
|
|
282
|
+
|
|
283
|
+
diag_vals = jnp.diag(A0[0] - c * c * calB_vals + 2 * c * s * calH_vals)
|
|
284
|
+
|
|
285
|
+
# Build each diagonal band
|
|
286
|
+
M = jnp.zeros((n, n), dtype=jnp.complex128)
|
|
287
|
+
|
|
288
|
+
# Diagonal (band 0)
|
|
289
|
+
for i in range(n):
|
|
290
|
+
l = _ells[i]
|
|
291
|
+
val = (
|
|
292
|
+
swsphericalh_A(s, l, m)
|
|
293
|
+
- c * c * _calB(s, l, m)
|
|
294
|
+
+ 2 * c * s * _calH(s, l, m)
|
|
295
|
+
)
|
|
296
|
+
M = M.at[i, i].set(val)
|
|
297
|
+
|
|
298
|
+
# Super-diagonal +1
|
|
299
|
+
for i in range(n - 1):
|
|
300
|
+
l = _ells[i]
|
|
301
|
+
lprime = _ells[i + 1]
|
|
302
|
+
val = -c * c * _calE(s, lprime, m) + 2 * c * s * _calG(s, lprime, m)
|
|
303
|
+
M = M.at[i, i + 1].set(val)
|
|
304
|
+
|
|
305
|
+
# Super-diagonal +2
|
|
306
|
+
for i in range(n - 2):
|
|
307
|
+
lprime = _ells[i + 2]
|
|
308
|
+
val = -c * c * _calC(s, lprime, m)
|
|
309
|
+
M = M.at[i, i + 2].set(val)
|
|
310
|
+
|
|
311
|
+
# Sub-diagonal -1
|
|
312
|
+
for i in range(1, n):
|
|
313
|
+
lprime = _ells[i - 1]
|
|
314
|
+
val = -c * c * _calD(s, lprime, m) + 2 * c * s * _calF(s, lprime, m)
|
|
315
|
+
M = M.at[i, i - 1].set(val)
|
|
316
|
+
|
|
317
|
+
# Sub-diagonal -2
|
|
318
|
+
for i in range(2, n):
|
|
319
|
+
lprime = _ells[i - 2]
|
|
320
|
+
val = -c * c * _calA(s, lprime, m)
|
|
321
|
+
M = M.at[i, i - 2].set(val)
|
|
322
|
+
|
|
323
|
+
return M
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@partial(jit, static_argnums=(0, 2, 3))
|
|
327
|
+
def sep_consts(s, c, m, l_max):
|
|
328
|
+
"""Finds eigenvalues of decomposition matrix, i.e. the separation
|
|
329
|
+
constants, As.
|
|
330
|
+
|
|
331
|
+
Parameters
|
|
332
|
+
----------
|
|
333
|
+
s: int
|
|
334
|
+
Spin-weight of interest
|
|
335
|
+
|
|
336
|
+
c: complex
|
|
337
|
+
Oblateness of spheroidal harmonic
|
|
338
|
+
|
|
339
|
+
m: int
|
|
340
|
+
Magnetic quantum number
|
|
341
|
+
|
|
342
|
+
l_max: int
|
|
343
|
+
Maximum angular quantum number
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
complex ndarray
|
|
348
|
+
Eigenvalues of spherical-spheroidal decomposition matrix
|
|
349
|
+
"""
|
|
350
|
+
return jnp.linalg.eigvals(M_matrix(s, c, m, l_max))
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def sep_const_closest(A0, s, c, m, l_max, temperature=1e-3):
|
|
354
|
+
"""Gives the separation constant that is closest to A0.
|
|
355
|
+
|
|
356
|
+
Uses a soft-min approach for differentiability.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
A0: complex
|
|
361
|
+
Value close to the desired separation constant.
|
|
362
|
+
|
|
363
|
+
s: int
|
|
364
|
+
Spin-weight of interest
|
|
365
|
+
|
|
366
|
+
c: complex
|
|
367
|
+
Oblateness of spheroidal harmonic
|
|
368
|
+
|
|
369
|
+
m: int
|
|
370
|
+
Magnetic quantum number
|
|
371
|
+
|
|
372
|
+
l_max: int
|
|
373
|
+
Maximum angular quantum number
|
|
374
|
+
|
|
375
|
+
temperature: float
|
|
376
|
+
Temperature for soft-min. Smaller = closer to hard argmin.
|
|
377
|
+
|
|
378
|
+
Returns
|
|
379
|
+
-------
|
|
380
|
+
complex
|
|
381
|
+
Separation constant that is the closest to A0.
|
|
382
|
+
"""
|
|
383
|
+
As = sep_consts(s, c, m, l_max)
|
|
384
|
+
distances = jnp.abs(As - A0)
|
|
385
|
+
|
|
386
|
+
# Use softmin weights for differentiable selection
|
|
387
|
+
# w_i = exp(-d_i / T) / sum(exp(-d_j / T))
|
|
388
|
+
log_weights = -distances / temperature
|
|
389
|
+
# Normalize for numerical stability
|
|
390
|
+
log_weights = log_weights - jnp.max(log_weights)
|
|
391
|
+
weights = jnp.exp(log_weights)
|
|
392
|
+
weights = weights / jnp.sum(weights)
|
|
393
|
+
|
|
394
|
+
# Weighted sum of eigenvalues
|
|
395
|
+
A_selected = jnp.sum(weights * As)
|
|
396
|
+
|
|
397
|
+
return A_selected
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def C_and_sep_const_closest(A0, s, c, m, l_max):
|
|
401
|
+
"""Get a single eigenvalue and eigenvector of decomposition
|
|
402
|
+
matrix, where the eigenvalue is closest to some guess A0.
|
|
403
|
+
|
|
404
|
+
Parameters
|
|
405
|
+
----------
|
|
406
|
+
A0: complex
|
|
407
|
+
Value close to the desired separation constant.
|
|
408
|
+
|
|
409
|
+
s: int
|
|
410
|
+
Spin-weight of interest
|
|
411
|
+
|
|
412
|
+
c: complex
|
|
413
|
+
Oblateness of spheroidal harmonic
|
|
414
|
+
|
|
415
|
+
m: int
|
|
416
|
+
Magnetic quantum number
|
|
417
|
+
|
|
418
|
+
l_max: int
|
|
419
|
+
Maximum angular quantum number
|
|
420
|
+
|
|
421
|
+
Returns
|
|
422
|
+
-------
|
|
423
|
+
complex, complex ndarray
|
|
424
|
+
The first element of the tuple is the eigenvalue that is closest
|
|
425
|
+
in value to A0. The second element of the tuple is the
|
|
426
|
+
corresponding eigenvector. The 0th element of this ndarray
|
|
427
|
+
corresponds to :meth:`l_min`.
|
|
428
|
+
"""
|
|
429
|
+
M = M_matrix(s, c, m, l_max)
|
|
430
|
+
As, Cs = jnp.linalg.eig(M)
|
|
431
|
+
i_closest = jnp.argmin(jnp.abs(As - A0))
|
|
432
|
+
return As[i_closest], Cs[:, i_closest]
|