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 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]