pybhpt 0.9.4__tar.gz → 0.9.8__tar.gz

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.

Potentially problematic release.


This version of pybhpt might be problematic. Click here for more details.

Files changed (101) hide show
  1. pybhpt-0.9.8/.readthedocs.yml +13 -0
  2. {pybhpt-0.9.4 → pybhpt-0.9.8}/PKG-INFO +14 -4
  3. {pybhpt-0.9.4 → pybhpt-0.9.8}/README.md +13 -3
  4. {pybhpt-0.9.4 → pybhpt-0.9.8}/build_gsl.sh +7 -4
  5. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/geo.hpp +6 -0
  6. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/swsh.hpp +0 -1
  7. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/geo.cpp +22 -0
  8. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/metriccoeffs.cpp +2 -2
  9. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/swsh.cpp +4 -1
  10. {pybhpt-0.9.4 → pybhpt-0.9.8}/cython/geo_wrap.pyx +153 -16
  11. pybhpt-0.9.8/cython/swsh_wrap.pyx +70 -0
  12. {pybhpt-0.9.4 → pybhpt-0.9.8}/cython/teukolsky_wrap.pyx +1 -0
  13. pybhpt-0.9.8/docs/Makefile +12 -0
  14. pybhpt-0.9.8/docs/about.md +26 -0
  15. pybhpt-0.9.8/docs/background/geo.md +76 -0
  16. pybhpt-0.9.8/docs/background/radial.md +38 -0
  17. pybhpt-0.9.8/docs/background/swsh.md +42 -0
  18. pybhpt-0.9.8/docs/background/teuk.md +42 -0
  19. pybhpt-0.9.8/docs/conf.py +51 -0
  20. pybhpt-0.9.8/docs/index.md +67 -0
  21. pybhpt-0.9.8/docs/installation.md +88 -0
  22. pybhpt-0.9.8/docs/notebooks/fluxes.ipynb +323 -0
  23. pybhpt-0.9.8/docs/notebooks/geodesics.ipynb +416 -0
  24. pybhpt-0.9.8/docs/notebooks/radial.ipynb +185 -0
  25. pybhpt-0.9.8/docs/notebooks/swsh.ipynb +311 -0
  26. pybhpt-0.9.8/docs/notebooks/teuk.ipynb +380 -0
  27. pybhpt-0.9.8/docs/notebooks/waveform.ipynb +465 -0
  28. pybhpt-0.9.8/docs/pybhpt.flux.md +24 -0
  29. pybhpt-0.9.8/docs/pybhpt.geo.md +18 -0
  30. pybhpt-0.9.8/docs/pybhpt.hertz.md +11 -0
  31. pybhpt-0.9.8/docs/pybhpt.metric.md +11 -0
  32. pybhpt-0.9.8/docs/pybhpt.radial.md +25 -0
  33. pybhpt-0.9.8/docs/pybhpt.redshift.md +11 -0
  34. pybhpt-0.9.8/docs/pybhpt.swsh.md +20 -0
  35. pybhpt-0.9.8/docs/pybhpt.teuk.md +19 -0
  36. pybhpt-0.9.8/docs/requirements.txt +7 -0
  37. pybhpt-0.9.8/pybhpt/geo.py +767 -0
  38. {pybhpt-0.9.4 → pybhpt-0.9.8}/pybhpt/radial.py +70 -7
  39. pybhpt-0.9.8/pybhpt/swsh.py +484 -0
  40. {pybhpt-0.9.4 → pybhpt-0.9.8}/pybhpt/teuk.py +197 -6
  41. {pybhpt-0.9.4 → pybhpt-0.9.8}/pyproject.toml +1 -1
  42. pybhpt-0.9.4/pybhpt/geo.py +0 -407
  43. pybhpt-0.9.4/pybhpt/swsh.py +0 -347
  44. {pybhpt-0.9.4 → pybhpt-0.9.8}/.gitmodules +0 -0
  45. {pybhpt-0.9.4 → pybhpt-0.9.8}/CMakeLists.txt +0 -0
  46. {pybhpt-0.9.4 → pybhpt-0.9.8}/LICENSE +0 -0
  47. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/bessel.hpp +0 -0
  48. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/cf.hpp +0 -0
  49. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/fluxes.hpp +0 -0
  50. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/gsn_asymp.hpp +0 -0
  51. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/hertz.hpp +0 -0
  52. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/hypergeo_f.hpp +0 -0
  53. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/hypergeo_u.hpp +0 -0
  54. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/kerr.hpp +0 -0
  55. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/metric.hpp +0 -0
  56. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/metriccoeffs.hpp +0 -0
  57. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/monodromy.hpp +0 -0
  58. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/mst.hpp +0 -0
  59. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/nusolver.hpp +0 -0
  60. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/radialsolver.hpp +0 -0
  61. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/redshift.hpp +0 -0
  62. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/regularization.hpp +0 -0
  63. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/resflux.hpp +0 -0
  64. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/sourceintegration.hpp +0 -0
  65. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/specialfunc.hpp +0 -0
  66. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/teukolsky.hpp +0 -0
  67. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/include/utils.hpp +0 -0
  68. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/bessel.cpp +0 -0
  69. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/cf.cpp +0 -0
  70. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/fluxes.cpp +0 -0
  71. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/gsn_asymp.cpp +0 -0
  72. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/hertz.cpp +0 -0
  73. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/hypergeo_f.cpp +0 -0
  74. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/hypergeo_u.cpp +0 -0
  75. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/kerr.cpp +0 -0
  76. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/metric.cpp +0 -0
  77. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/monodromy.cpp +0 -0
  78. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/mst.cpp +0 -0
  79. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/nusolver.cpp +0 -0
  80. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/radialsolver.cpp +0 -0
  81. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/redshift.cpp +0 -0
  82. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/regularization.cpp +0 -0
  83. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/resflux.cpp +0 -0
  84. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/sourceintegration.cpp +0 -0
  85. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/specialfunc.cpp +0 -0
  86. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/teukolsky.cpp +0 -0
  87. {pybhpt-0.9.4 → pybhpt-0.9.8}/cpp/src/utils.cpp +0 -0
  88. {pybhpt-0.9.4 → pybhpt-0.9.8}/cython/flux_wrap.pyx +0 -0
  89. {pybhpt-0.9.4 → pybhpt-0.9.8}/cython/radialsolver_wrap.pyx +0 -0
  90. {pybhpt-0.9.4 → pybhpt-0.9.8}/cython/redshift_wrap.pyx +0 -0
  91. {pybhpt-0.9.4 → pybhpt-0.9.8}/environment-extended.yml +0 -0
  92. {pybhpt-0.9.4 → pybhpt-0.9.8}/environment.yml +0 -0
  93. {pybhpt-0.9.4 → pybhpt-0.9.8}/notebooks/flux-example.ipynb +0 -0
  94. {pybhpt-0.9.4 → pybhpt-0.9.8}/pybhpt/__init__.py +0 -0
  95. {pybhpt-0.9.4 → pybhpt-0.9.8}/pybhpt/flux.py +0 -0
  96. {pybhpt-0.9.4 → pybhpt-0.9.8}/pybhpt/hertz.py +0 -0
  97. {pybhpt-0.9.4 → pybhpt-0.9.8}/pybhpt/metric.py +0 -0
  98. {pybhpt-0.9.4 → pybhpt-0.9.8}/pybhpt/redshift.py +0 -0
  99. {pybhpt-0.9.4 → pybhpt-0.9.8}/tests/test_geo.py +0 -0
  100. {pybhpt-0.9.4 → pybhpt-0.9.8}/tests/test_radial.py +0 -0
  101. {pybhpt-0.9.4 → pybhpt-0.9.8}/tests/test_teuk.py +0 -0
@@ -0,0 +1,13 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-22.04
5
+ tools:
6
+ python: "3.9"
7
+
8
+ python:
9
+ install:
10
+ - requirements: docs/requirements.txt
11
+
12
+ sphinx:
13
+ configuration: docs/conf.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pybhpt
3
- Version: 0.9.4
3
+ Version: 0.9.8
4
4
  Summary: Black Hole Perturbation Theory and Self-Force Algorithms in Python
5
5
  Author-Email: Zach Nasipak <znasipak@gmail.com>
6
6
  License: GPL
@@ -30,11 +30,11 @@ Description-Content-Type: text/markdown
30
30
 
31
31
  # pybhpt
32
32
 
33
- A python package for solving problems in black hole perturbation theory
33
+ A python package for solving problems in black hole perturbation theory.
34
34
 
35
35
  `pybhpt` is a collection of numerical tools for analyzing perturbations of Kerr spacetime, particularly the self-forces and metric-perturbations experienced by small bodies moving in a Kerr background. Subpackages include:
36
36
 
37
- - `pybhpt.geodesic`: a module that generates bound timelike geodesics in Kerr spacetime
37
+ - `pybhpt.geodesic`: a module that generates bound periodic timelike geodesics in Kerr spacetime
38
38
  - `pybhpt.radial`: a module that calculates homogeneous solutions of the radial Teukolsky equation
39
39
  - `pybhpt.swsh`: a module that constructs the spin-weighted spheroidal harmonics
40
40
  - `pybhpt.teuk`: a module that evaluates the inhomogeneous solutions (Teukolsky amplitudes) of the radial Teukolsky equation due to a point-particle on a bound timelike Kerr geodesic
@@ -43,9 +43,11 @@ A python package for solving problems in black hole perturbation theory
43
43
  - `pybhpt.metric`: a module that produces the coefficients needed to reconstruct the metric from the Hertz potentials
44
44
  - `pybhpt.redshift`: a module that computes the generalized Detweiler redshift invariant in a variety of gauges
45
45
 
46
+ See the [Documentation](https://pybhpt.readthedocs.io/en/latest/) pages for more information about the package, including User Guides and API. References and author information can be found at the bottom of the README.
47
+
46
48
  ## Quick Installation
47
49
 
48
- Tagged releases of `pybhpt` are available as wheel packages for macOS and 64-bit Linux on [PyPI](https://pypi.org/project/matplotlib/). Install using `pip`:
50
+ Tagged releases of `pybhpt` are available as wheel packages for macOS and 64-bit Linux on [PyPI](https://pypi.org/project/pybhpt). Install using `pip`:
49
51
  ```
50
52
  python3 -m pip install pybhpt
51
53
  ```
@@ -130,6 +132,14 @@ To include the necessary compiler on Linux:
130
132
  conda install gcc_linux-64 gxx_linux-64
131
133
  ```
132
134
 
135
+ ## References
136
+
137
+ Theoretical background for the code and explanations of the numerical methods used within are summarized in the references below:
138
+
139
+ - Z. Nasipak, *Metric reconstruction and the Hamiltonian for eccentric, precessing binaries in the small-mass-ratio limit* (2025) [arXiv:2507.07746](https://arxiv.org/abs/2507.07746)
140
+ - Z. Nasipak, *An adiabatic gravitational waveform model for compact objects undergoing quasi-circular inspirals into rotating massive black holes*, Phys. Rev. D 109, 044020 (2024) [arXiv:2310.19706](https://arxiv.org/abs/2310.19706)
141
+ - Z. Nasipak, *Adiabatic evolution due to the conservative scalar self-force during orbital resonances*, Phys. Rev. D 106, 064042 (2022) [arXiv:2207.02224](https://arxiv.org/abs/2207.02224)
142
+
133
143
  ## Authors
134
144
 
135
145
  Zachary Nasipak
@@ -1,10 +1,10 @@
1
1
  # pybhpt
2
2
 
3
- A python package for solving problems in black hole perturbation theory
3
+ A python package for solving problems in black hole perturbation theory.
4
4
 
5
5
  `pybhpt` is a collection of numerical tools for analyzing perturbations of Kerr spacetime, particularly the self-forces and metric-perturbations experienced by small bodies moving in a Kerr background. Subpackages include:
6
6
 
7
- - `pybhpt.geodesic`: a module that generates bound timelike geodesics in Kerr spacetime
7
+ - `pybhpt.geodesic`: a module that generates bound periodic timelike geodesics in Kerr spacetime
8
8
  - `pybhpt.radial`: a module that calculates homogeneous solutions of the radial Teukolsky equation
9
9
  - `pybhpt.swsh`: a module that constructs the spin-weighted spheroidal harmonics
10
10
  - `pybhpt.teuk`: a module that evaluates the inhomogeneous solutions (Teukolsky amplitudes) of the radial Teukolsky equation due to a point-particle on a bound timelike Kerr geodesic
@@ -13,9 +13,11 @@ A python package for solving problems in black hole perturbation theory
13
13
  - `pybhpt.metric`: a module that produces the coefficients needed to reconstruct the metric from the Hertz potentials
14
14
  - `pybhpt.redshift`: a module that computes the generalized Detweiler redshift invariant in a variety of gauges
15
15
 
16
+ See the [Documentation](https://pybhpt.readthedocs.io/en/latest/) pages for more information about the package, including User Guides and API. References and author information can be found at the bottom of the README.
17
+
16
18
  ## Quick Installation
17
19
 
18
- Tagged releases of `pybhpt` are available as wheel packages for macOS and 64-bit Linux on [PyPI](https://pypi.org/project/matplotlib/). Install using `pip`:
20
+ Tagged releases of `pybhpt` are available as wheel packages for macOS and 64-bit Linux on [PyPI](https://pypi.org/project/pybhpt). Install using `pip`:
19
21
  ```
20
22
  python3 -m pip install pybhpt
21
23
  ```
@@ -100,6 +102,14 @@ To include the necessary compiler on Linux:
100
102
  conda install gcc_linux-64 gxx_linux-64
101
103
  ```
102
104
 
105
+ ## References
106
+
107
+ Theoretical background for the code and explanations of the numerical methods used within are summarized in the references below:
108
+
109
+ - Z. Nasipak, *Metric reconstruction and the Hamiltonian for eccentric, precessing binaries in the small-mass-ratio limit* (2025) [arXiv:2507.07746](https://arxiv.org/abs/2507.07746)
110
+ - Z. Nasipak, *An adiabatic gravitational waveform model for compact objects undergoing quasi-circular inspirals into rotating massive black holes*, Phys. Rev. D 109, 044020 (2024) [arXiv:2310.19706](https://arxiv.org/abs/2310.19706)
111
+ - Z. Nasipak, *Adiabatic evolution due to the conservative scalar self-force during orbital resonances*, Phys. Rev. D 106, 064042 (2022) [arXiv:2207.02224](https://arxiv.org/abs/2207.02224)
112
+
103
113
  ## Authors
104
114
 
105
115
  Zachary Nasipak
@@ -12,12 +12,15 @@ else
12
12
  CORES=$(sysctl -n hw.ncpu)
13
13
  fi
14
14
 
15
- # Download source
15
+ # Download source (use mirror redirector + retries)
16
16
  mkdir -p /tmp/gsl-src
17
17
  cd /tmp/gsl-src
18
- curl -LO https://ftp.gnu.org/gnu/gsl/gsl-${GSL_VERSION}.tar.gz
19
- tar -xzf gsl-${GSL_VERSION}.tar.gz
20
- cd gsl-${GSL_VERSION}
18
+ curl --fail --location --retry 5 --retry-delay 5 \
19
+ "https://ftpmirror.gnu.org/gsl/gsl-${GSL_VERSION}.tar.gz" \
20
+ -o "gsl-${GSL_VERSION}.tar.gz"
21
+
22
+ tar -xzf "gsl-${GSL_VERSION}.tar.gz"
23
+ cd "gsl-${GSL_VERSION}"
21
24
 
22
25
  # Build & install
23
26
  echo "Configuring GSL ${GSL_VERSION}..."
@@ -97,11 +97,17 @@ public:
97
97
  double getPolarPosition(int pos);
98
98
  double getAzimuthalAccumulation(int j, int pos);
99
99
 
100
+ double getPsiRadialOfMinoTime(double lambda);
101
+ double getPsiPolarOfMinoTime(double lambda);
102
+
100
103
  double getTimePositionOfMinoTime(double lambda);
101
104
  double getRadialPositionOfMinoTime(double lambda);
102
105
  double getPolarPositionOfMinoTime(double lambda);
103
106
  double getAzimuthalPositionOfMinoTime(double lambda);
104
107
 
108
+ Vector getPsiRadialOfMinoTime(Vector lambda);
109
+ Vector getPsiPolarOfMinoTime(Vector lambda);
110
+
105
111
  Vector getTimePositionOfMinoTime(Vector lambda);
106
112
  Vector getRadialPositionOfMinoTime(Vector lambda);
107
113
  Vector getPolarPositionOfMinoTime(Vector lambda);
@@ -86,7 +86,6 @@ int spectral_matrix(const int &s, const int &lmin, const int &m, const double &g
86
86
  int spectral_matrix_sparse_init(const int &s, const int &lmin, const int &m, const double &g, gsl_spmatrix* mat);
87
87
  int spectral_matrix_sparse(const int &s, const int &lmin, const int &m, const double &g, gsl_spmatrix* mat, const size_t &nmax);
88
88
 
89
-
90
89
  // Solve eigensystem of spectral matrix
91
90
  int spectral_solver(const int &s, const int &l, const int &m, const double &g, double& la, Vector& bvec);
92
91
 
@@ -329,6 +329,12 @@ double GeodesicSource::getAzimuthalAccumulation(int j, int pos){
329
329
  }
330
330
  }
331
331
 
332
+ double GeodesicSource::getPsiRadialOfMinoTime(double lambda){
333
+ return kepler_phase_of_angle(lambda*_geoConstants.upsilonR, _geoCoefficients.r);
334
+ }
335
+ double GeodesicSource::getPsiPolarOfMinoTime(double lambda){
336
+ return kepler_phase_of_angle(lambda*_geoConstants.upsilonTheta, _geoCoefficients.theta);
337
+ }
332
338
  double GeodesicSource::getTimePositionOfMinoTime(double lambda){
333
339
  return lambda*_geoConstants.upsilonT + phip_of_angle(lambda*_geoConstants.upsilonR, _geoCoefficients.tR) + phip_of_angle(lambda*_geoConstants.upsilonTheta, _geoCoefficients.tTheta);
334
340
  }
@@ -350,6 +356,22 @@ Vector GeodesicSource::getPositionOfMinoTime(double lambda){
350
356
  return xp;
351
357
  }
352
358
 
359
+ Vector GeodesicSource::getPsiRadialOfMinoTime(Vector lambda){
360
+ Vector xp(lambda.size());
361
+ for(size_t i = 0; i < xp.size(); i++){
362
+ xp[i] = getPsiRadialOfMinoTime(lambda[i]);
363
+ }
364
+ return xp;
365
+ }
366
+
367
+ Vector GeodesicSource::getPsiPolarOfMinoTime(Vector lambda){
368
+ Vector xp(lambda.size());
369
+ for(size_t i = 0; i < xp.size(); i++){
370
+ xp[i] = getPsiPolarOfMinoTime(lambda[i]);
371
+ }
372
+ return xp;
373
+ }
374
+
353
375
  Vector GeodesicSource::getTimePositionOfMinoTime(Vector lambda){
354
376
  Vector xp(lambda.size());
355
377
  for(size_t i = 0; i < xp.size(); i++){
@@ -3217,7 +3217,7 @@ ComplexMatrix tetrad_velocity_radial(Complex (*subfunc)(double, double, double,
3217
3217
  for(size_t jr = 1; jr < r.size() - 1; jr++){
3218
3218
  rp = r[jr];
3219
3219
  zp = z[0];
3220
- double deltaUr = sqrt(kerr_geo_Vr(a, En, Lz, Qc, rp));
3220
+ double deltaUr = std::sqrt(std::abs(kerr_geo_Vr(a, En, Lz, Qc, rp)));
3221
3221
  // take into account when the radial velocity is positive
3222
3222
  ua[jr][0] = subfunc(a, En, Lz, Qc, rp, zp, deltaUr);
3223
3223
  ua[u1rSize - jr][0] = subfunc(a, En, Lz, Qc, rp, zp, -deltaUr);
@@ -3234,7 +3234,7 @@ ComplexMatrix tetrad_velocity_radial(Complex (*subfunc)(double, double, double,
3234
3234
  for(size_t jz = 1; jz < z.size() - 1; jz++){
3235
3235
  rp = r[jr];
3236
3236
  zp = z[jz];
3237
- double deltaUr = sqrt(kerr_geo_Vr(a, En, Lz, Qc, rp));
3237
+ double deltaUr = std::sqrt(std::abs(kerr_geo_Vr(a, En, Lz, Qc, rp)));
3238
3238
  // take into account when the radial velocity is positive
3239
3239
  ua[jr][jz] = subfunc(a, En, Lz, Qc, rp, zp, deltaUr);
3240
3240
  ua[jr][u1zSize - jz] = subfunc(a, En, Lz, Qc, rp, zp, deltaUr);
@@ -718,7 +718,10 @@ Complex Yslm(const int &s, const int &l, const int &m, const double &th, const d
718
718
 
719
719
  double Yslm(const int &s, const int &l, const int &m, const double &th){
720
720
  if( s == 0 ) return Ylm(l, m, th);
721
-
721
+ if( th == 0. ){
722
+ if (m != -s) return 0.;
723
+ else return pow(-1, s)*Yslm(0, l, 0, 0.);
724
+ }
722
725
  int lmin = std::max(std::abs(m), l - std::abs(s));
723
726
  int lmax = l + std::abs(s);
724
727
  double yslm = 0;
@@ -54,11 +54,17 @@ cdef extern from "geo.hpp":
54
54
  double getPolarPosition(int pos)
55
55
  double getAzimuthalAccumulation(int j, int pos)
56
56
 
57
+ double getPsiRadialOfMinoTime(double la)
58
+ double getPsiPolarOfMinoTime(double la)
59
+
57
60
  double getTimePositionOfMinoTime(double la)
58
61
  double getRadialPositionOfMinoTime(double la)
59
62
  double getPolarPositionOfMinoTime(double la)
60
63
  double getAzimuthalPositionOfMinoTime(double la)
61
64
 
65
+ vector[double] getPsiRadialOfMinoTime(vector[double] la)
66
+ vector[double] getPsiPolarOfMinoTime(vector[double] la)
67
+
62
68
  vector[double] getTimePositionOfMinoTime(vector[double] la)
63
69
  vector[double] getRadialPositionOfMinoTime(vector[double] la)
64
70
  vector[double] getPolarPositionOfMinoTime(vector[double] la)
@@ -187,6 +193,24 @@ cdef class KerrGeodesic:
187
193
  def mode_carter_frequency(self, np.ndarray[ndim=1, dtype=np.int64_t] kvec):
188
194
  return np.dot(kvec,(self.carterfrequencies))
189
195
 
196
+ cdef void getPsiRadialOfMinoTimeArray(self, np.float64_t *psi, np.float64_t *la, int n):
197
+ for i in range(n):
198
+ psi[i] = self.geocpp.getPsiRadialOfMinoTime(la[i])
199
+ cdef void getPsiPolarOfMinoTimeArray(self, np.float64_t *psi, np.float64_t *la, int n):
200
+ for i in range(n):
201
+ psi[i] = self.geocpp.getPsiPolarOfMinoTime(la[i])
202
+
203
+ cdef void getPsiRadialOfTimeArray(self, np.float64_t *psi, np.float64_t *t, int n):
204
+ cdef double tmp
205
+ for i in range(n):
206
+ tmp = self.geocpp.getMinoTimeOfTime(t[i])
207
+ psi[i] = self.geocpp.getPsiRadialOfMinoTime(tmp)
208
+ cdef void getPsiPolarOfTimeArray(self, np.float64_t *psi, np.float64_t *t, int n):
209
+ cdef double tmp
210
+ for i in range(n):
211
+ tmp = self.geocpp.getMinoTimeOfTime(t[i])
212
+ psi[i] = self.geocpp.getPsiPolarOfMinoTime(tmp)
213
+
190
214
  cdef void getTimePositionOfMinoTimeArray(self, np.float64_t *t, np.float64_t *la, int n):
191
215
  for i in range(n):
192
216
  t[i] = self.geocpp.getTimePositionOfMinoTime(la[i])
@@ -199,8 +223,86 @@ cdef class KerrGeodesic:
199
223
  cdef void getAzimuthalPositionOfMinoTimeArray(self, np.float64_t *t, np.float64_t *la, int n):
200
224
  for i in range(n):
201
225
  t[i] = self.geocpp.getAzimuthalPositionOfMinoTime(la[i])
226
+
227
+ cdef void getRadialPositionOfTimeArray(self, np.float64_t *r, np.float64_t *t, int n):
228
+ cdef double tmp
229
+ for i in range(n):
230
+ tmp = self.geocpp.getMinoTimeOfTime(t[i])
231
+ r[i] = self.geocpp.getRadialPositionOfMinoTime(tmp)
232
+ cdef void getPolarPositionOfTimeArray(self, np.float64_t *th, np.float64_t *t, int n):
233
+ cdef double tmp
234
+ for i in range(n):
235
+ tmp = self.geocpp.getMinoTimeOfTime(t[i])
236
+ th[i] = self.geocpp.getPolarPositionOfMinoTime(tmp)
237
+ cdef void getAzimuthalPositionOfTimeArray(self, np.float64_t *ph, np.float64_t *t, int n):
238
+ cdef double tmp
239
+ for i in range(n):
240
+ tmp = self.geocpp.getMinoTimeOfTime(t[i])
241
+ ph[i] = self.geocpp.getAzimuthalPositionOfMinoTime(tmp)
242
+
243
+ def psi_radial(self, double la):
244
+ return self.geocpp.getPsiRadialOfMinoTime(la)
245
+
246
+ def psi_polar(self, double la):
247
+ return self.geocpp.getPsiPolarOfMinoTime(la)
248
+
249
+ def psi_radial_time(self, double t):
250
+ cdef double tmp = self.geocpp.getMinoTimeOfTime(t)
251
+ return self.geocpp.getPsiRadialOfMinoTime(tmp)
252
+
253
+ def psi_polar_time(self, double t):
254
+ cdef double tmp = self.geocpp.getMinoTimeOfTime(t)
255
+ return self.geocpp.getPsiPolarOfMinoTime(tmp)
256
+
257
+ def psi_radial_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] la):
258
+ cdef int n = la.shape[0]
259
+ cdef np.ndarray[ndim=1, dtype=np.float64_t] psi = np.empty(n, dtype = np.float64)
260
+ self.getPsiRadialOfMinoTimeArray(&psi[0], &la[0], n)
261
+ return psi
262
+
263
+ def psi_polar_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] la):
264
+ cdef int n = la.shape[0]
265
+ cdef np.ndarray[ndim=1, dtype=np.float64_t] psi = np.empty(n, dtype = np.float64)
266
+ self.getPsiPolarOfMinoTimeArray(&psi[0], &la[0], n)
267
+ return psi
268
+
269
+ def psi_radial_time_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] t):
270
+ cdef int n = t.shape[0]
271
+ cdef np.ndarray[ndim=1, dtype=np.float64_t] psi = np.empty(n, dtype = np.float64)
272
+ self.getPsiRadialOfTimeArray(&psi[0], &t[0], n)
273
+ return psi
274
+
275
+ def psi_polar_time_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] t):
276
+ cdef int n = t.shape[0]
277
+ cdef np.ndarray[ndim=1, dtype=np.float64_t] psi = np.empty(n, dtype = np.float64)
278
+ self.getPsiPolarOfTimeArray(&psi[0], &t[0], n)
279
+ return psi
280
+
281
+ def time_position(self, double la):
282
+ return self.geocpp.getTimePositionOfMinoTime(la)
283
+
284
+ def radial_position(self, double la):
285
+ return self.geocpp.getRadialPositionOfMinoTime(la)
202
286
 
203
- def time_position(self, np.ndarray[ndim=1, dtype=np.float64_t] la):
287
+ def polar_position(self, double la):
288
+ return self.geocpp.getPolarPositionOfMinoTime(la)
289
+
290
+ def azimuthal_position(self, double la):
291
+ return self.geocpp.getAzimuthalPositionOfMinoTime(la)
292
+
293
+ def radial_position_time(self, double t):
294
+ cdef double tmp = self.geocpp.getMinoTimeOfTime(t)
295
+ return self.geocpp.getRadialPositionOfMinoTime(tmp)
296
+
297
+ def polar_position_time(self, double t):
298
+ cdef double tmp = self.geocpp.getMinoTimeOfTime(t)
299
+ return self.geocpp.getPolarPositionOfMinoTime(tmp)
300
+
301
+ def azimuthal_position_time(self, double t):
302
+ cdef double tmp = self.geocpp.getMinoTimeOfTime(t)
303
+ return self.geocpp.getAzimuthalPositionOfMinoTime(tmp)
304
+
305
+ def time_position_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] la):
204
306
  cdef int n = la.shape[0]
205
307
  cdef np.ndarray[ndim=1, dtype=np.float64_t] t = np.empty(n, dtype = np.float64)
206
308
  self.getTimePositionOfMinoTimeArray(&t[0], &la[0], n)
@@ -209,23 +311,70 @@ cdef class KerrGeodesic:
209
311
  # t[i] = self.geocpp.getTimePositionOfMinoTime(la[i])
210
312
  return t
211
313
 
212
- def radial_position(self, np.ndarray[ndim=1, dtype=np.float64_t] la):
314
+ def radial_position_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] la):
213
315
  cdef int n = la.shape[0]
214
316
  cdef np.ndarray[ndim=1, dtype=np.float64_t] x = np.empty(n, dtype = np.float64)
215
317
  self.getRadialPositionOfMinoTimeArray(&x[0], &la[0], n)
216
318
  return x
217
319
 
218
- def polar_position(self, np.ndarray[ndim=1, dtype=np.float64_t] la):
320
+ def polar_position_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] la):
219
321
  cdef int n = la.shape[0]
220
322
  cdef np.ndarray[ndim=1, dtype=np.float64_t] x = np.empty(n, dtype = np.float64)
221
323
  self.getPolarPositionOfMinoTimeArray(&x[0], &la[0], n)
222
324
  return x
223
325
 
224
- def azimuthal_position(self, np.ndarray[ndim=1, dtype=np.float64_t] la):
326
+ def azimuthal_position_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] la):
225
327
  cdef int n = la.shape[0]
226
328
  cdef np.ndarray[ndim=1, dtype=np.float64_t] x = np.empty(n, dtype = np.float64)
227
329
  self.getAzimuthalPositionOfMinoTimeArray(&x[0], &la[0], n)
228
330
  return x
331
+
332
+ def radial_position_time_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] t):
333
+ cdef int n = t.shape[0]
334
+ cdef np.ndarray[ndim=1, dtype=np.float64_t] x = np.empty(n, dtype = np.float64)
335
+ self.getRadialPositionOfTimeArray(&x[0], &t[0], n)
336
+ return x
337
+
338
+ def polar_position_time_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] t):
339
+ cdef int n = t.shape[0]
340
+ cdef np.ndarray[ndim=1, dtype=np.float64_t] x = np.empty(n, dtype = np.float64)
341
+ self.getPolarPositionOfTimeArray(&x[0], &t[0], n)
342
+ return x
343
+
344
+ def azimuthal_position_time_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] t):
345
+ cdef int n = t.shape[0]
346
+ cdef np.ndarray[ndim=1, dtype=np.float64_t] x = np.empty(n, dtype = np.float64)
347
+ self.getAzimuthalPositionOfTimeArray(&x[0], &t[0], n)
348
+ return x
349
+
350
+ def position(self, double la):
351
+ return np.array([self.geocpp.getTimePositionOfMinoTime(la), self.geocpp.getRadialPositionOfMinoTime(la), self.geocpp.getPolarPositionOfMinoTime(la), self.geocpp.getAzimuthalPositionOfMinoTime(la)])
352
+
353
+ def position_time(self, double t):
354
+ cdef double tmp = self.geocpp.getMinoTimeOfTime(t)
355
+ return np.array([self.geocpp.getRadialPositionOfMinoTime(tmp), self.geocpp.getPolarPositionOfMinoTime(tmp), self.geocpp.getAzimuthalPositionOfMinoTime(tmp)])
356
+
357
+ def position_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] la):
358
+ cdef np.ndarray[ndim=2, dtype=np.float64_t] xp = np.empty((la.shape[0], 4), dtype=np.float64)
359
+ for i in range(la.shape[0]):
360
+ xp[i] = self.position(la[i])
361
+ return xp.T
362
+
363
+ def position_time_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] t):
364
+ cdef np.ndarray[ndim=2, dtype=np.float64_t] xp = np.empty((t.shape[0], 3), dtype=np.float64)
365
+ for i in range(t.shape[0]):
366
+ xp[i] = self.position_time(t[i])
367
+ return xp.T
368
+
369
+ def mino_time(self, double t):
370
+ return self.geocpp.getMinoTimeOfTime(t)
371
+
372
+ def mino_time_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] t):
373
+ cdef int n = t.shape[0]
374
+ cdef np.ndarray[ndim=1, dtype=np.float64_t] la = np.empty(n, dtype = np.float64)
375
+ for i in range(n):
376
+ la[i] = self.mino_time(t[i])
377
+ return la
229
378
 
230
379
  def get_time_accumulation(self, int j):
231
380
  cdef vector[double] deltaX_cpp = self.geocpp.getTimeAccumulation(j)
@@ -259,18 +408,6 @@ cdef class KerrGeodesic:
259
408
  deltaX[i] = deltaX_cpp[i]
260
409
  return deltaX
261
410
 
262
- def position(self, double la):
263
- return np.array([self.geocpp.getTimePositionOfMinoTime(la), self.geocpp.getRadialPositionOfMinoTime(la), self.geocpp.getPolarPositionOfMinoTime(la), self.geocpp.getAzimuthalPositionOfMinoTime(la)])
264
-
265
- def position_vec(self, np.ndarray[ndim=1, dtype=np.float64_t] la):
266
- cdef np.ndarray[ndim=2, dtype=np.float64_t] xp = np.empty((la.shape[0], 4), dtype=np.float64)
267
- for i in range(la.shape[0]):
268
- xp[i] = self.position(la[i])
269
- return xp.T
270
-
271
- def mino_time(self, double t):
272
- return self.geocpp.getMinoTimeOfTime(t)
273
-
274
411
  def get_time_coefficients(self, int j):
275
412
  cdef vector[double] deltaX_cpp = self.geocpp.getTimeCoefficients(j)
276
413
  cdef int n = deltaX_cpp.size()
@@ -0,0 +1,70 @@
1
+ from libcpp.vector cimport vector
2
+ from libcpp.complex cimport complex as cpp_complex
3
+ import numpy as np
4
+ cimport numpy as np
5
+
6
+ cdef extern from "gsl/gsl_errno.h":
7
+ void gsl_set_error_handler_off()
8
+
9
+ # If you need to disable GSL error handling, do so in a targeted way within specific functions.
10
+ # gsl_set_error_handler_off() # Removed global call to avoid masking numerical errors.
11
+
12
+ cdef extern from "swsh.hpp":
13
+ cdef cppclass SpinWeightedHarmonic:
14
+ SpinWeightedHarmonic(int s, int L, int m, double gamma, vector[double]& theta)
15
+
16
+ int getSpinWeight()
17
+ int getSpheroidalModeNumber()
18
+ int getAzimuthalModeNumber()
19
+ double getSpheroidicity()
20
+ double getEigenvalue()
21
+ vector[double] getCouplingCoefficient()
22
+ double getCouplingCoefficient(int l)
23
+ int getMinCouplingModeNumber()
24
+ int getMaxCouplingModeNumber()
25
+
26
+ int generateSolutionsAndDerivatives()
27
+ int generateCouplingCoefficients()
28
+ int generateSolutions()
29
+ int generateDerivatives()
30
+
31
+ vector[double] getArguments()
32
+ vector[double] getSolution()
33
+ vector[double] getDerivative()
34
+ vector[double] getSecondDerivative()
35
+
36
+ double getArguments(int pos)
37
+ double getSolution(int pos)
38
+ double getDerivative(int pos)
39
+ double getSecondDerivative(int pos)
40
+
41
+ double Asljm(int &s, int &l, int &j, int &m)
42
+ double dAsljm(int &s, int &l, int &j, int &m)
43
+
44
+ double clebsch(int &j1, int &j2, int &j, int &m1, int &m2, int &m)
45
+ double w3j(int &j1, int &j2, int &j, int &m1, int &m2, int &m)
46
+
47
+ cpp_complex[double] Sslm(int &s, int &l, int &m, double &g, double &th, double &ph)
48
+ double Sslm(int &s, int &l, int &m, double &g, double &th)
49
+
50
+ double Sslm(int &s, int &l, int &m, double &g, vector[double]& bvec, double &th)
51
+ double Sslm_derivative(int &s, int &l, int &m, double &g, vector[double]& bvec, double &th)
52
+ double Sslm_secondDerivative(int &s, int &l, int &m, double &g, double& la, double &th, double &Slm, double &SlmP)
53
+
54
+ double swsh_eigenvalue(int &s, int &l, int &m, double &g)
55
+
56
+ cpp_complex[double] Yslm(int &s, int &l, int &m, double &th, double &ph)
57
+ double Yslm(int &s, int &l, int &m, double &th) except +
58
+ double Yslm_derivative(int &s, int &l, int &m, double &th)
59
+
60
+ def YslmCy(int s, int l, int m, double theta):
61
+ return Yslm(s, l, m, theta)
62
+
63
+ def YslmCy_derivative(int s, int l, int m, double theta):
64
+ return Yslm_derivative(s, l, m, theta)
65
+
66
+ def clebschCy(int j1, int j2, int j, int m1, int m2, int m):
67
+ return clebsch(j1, j2, j, m1, m2, m)
68
+
69
+ def w3jCy(int j1, int j2, int j, int m1, int m2, int m):
70
+ return w3j(j1, j2, j, m1, m2, m)
@@ -7,6 +7,7 @@ cimport numpy as np
7
7
  # from geo_wrap cimport GeodesicSource
8
8
  include "geo_wrap.pyx"
9
9
  include "radialsolver_wrap.pyx"
10
+ include "swsh_wrap.pyx"
10
11
 
11
12
  cdef extern from "teukolsky.hpp":
12
13
  cdef cppclass SpinWeightedHarmonic:
@@ -0,0 +1,12 @@
1
+ # Clean target to remove build directory
2
+ clean:
3
+ rm -rf _build
4
+ # Minimal Sphinx Makefile
5
+ SPHINXOPTS =
6
+ SPHINXBUILD = sphinx-build
7
+ SOURCEDIR = .
8
+ BUILDDIR = _build
9
+
10
+ .PHONY: html
11
+ html:
12
+ $(SPHINXBUILD) -b html $(SOURCEDIR) $(BUILDDIR)/html $(SPHINXOPTS)
@@ -0,0 +1,26 @@
1
+ # About
2
+
3
+ `pybhpt` is a collection of numerical tools for analyzing perturbations of Kerr spacetime, particularly the self-forces and metric-perturbations experienced by small bodies moving in a Kerr background.
4
+
5
+ ## Subpackages
6
+ - [`pybhpt.geo`](pybhpt.geo): generates bound periodic timelike geodesics in Kerr spacetime
7
+ - [`pybhpt.radial`](pybhpt.radial): calculates homogeneous solutions of the radial Teukolsky equation
8
+ - [`pybhpt.swsh`](pybhpt.swsh): constructs the spin-weighted spheroidal harmonics
9
+ - [`pybhpt.teuk`](pybhpt.teuk): evaluates inhomogeneous solutions (Teukolsky amplitudes) of the radial Teukolsky equation due to a point-particle on a bound timelike Kerr geodesic
10
+ - [`pybhpt.flux`](pybhpt.flux): produces gravitational wave fluxes sourced by a point-particle on a generic bound timelike Kerr geodesic
11
+ - [`pybhpt.hertz`](pybhpt.hertz): solves for the Hertz potentials for the CCK and AAB metric reconstruction procedures
12
+ - [`pybhpt.metric`](pybhpt.metric): produces coefficients needed to reconstruct the metric from the Hertz potentials
13
+ - [`pybhpt.redshift`](pybhpt.redshift): computes the generalized Detweiler redshift invariant in a variety of gauges
14
+
15
+ One can find out more information about each module by exploring the User Guides or clicking on the subpackages, which are linked to the API.
16
+
17
+ ## References
18
+
19
+ Theoretical background for the code and explanations of the numerical methods used within are summarized in the references below:
20
+
21
+ - Z. Nasipak, *Metric reconstruction and the Hamiltonian for eccentric, precessing binaries in the small-mass-ratio limit* (2025) [arXiv:2507.07746](https://arxiv.org/abs/2507.07746)
22
+ - Z. Nasipak, *An adiabatic gravitational waveform model for compact objects undergoing quasi-circular inspirals into rotating massive black holes*, Phys. Rev. D 109, 044020 (2024) [arXiv:2310.19706](https://arxiv.org/abs/2310.19706)
23
+ - Z. Nasipak, *Adiabatic evolution due to the conservative scalar self-force during orbital resonances*, Phys. Rev. D 106, 064042 (2022) [arXiv:2207.02224](https://arxiv.org/abs/2207.02224)
24
+
25
+ ## Authors
26
+ Zachary Nasipak
@@ -0,0 +1,76 @@
1
+ # Geodesics in Kerr
2
+
3
+ We work in a Kerr background with angular momentum and mass parameters $(J, M)$ and use Boyer-Lindquist coordinates $(t, r, \theta, \phi)$.
4
+
5
+ Bound periodic timelike geodesics in Kerr spacetime are defined in terms of the turning points of the motion:
6
+
7
+ - $r_\mathrm{min}$ : minimum Boyer-Lindquist radius
8
+ - $r_\mathrm{max}$ : maximum Boyer-Lindquist radius
9
+ - $\theta_\mathrm{min}$ : minimum Boyer-Lindquist polar angle
10
+ - $\pi-\theta_\mathrm{min}$ : maximum Boyer-Lindquist polar angle
11
+
12
+ From these we parametrize the geodesic in terms of the generalized Keplerian parameters:
13
+
14
+ - $a$ : the dimensionless Kerr spin parameter
15
+ - $p$ : the dimensionless semilatus rectum
16
+ - $e$ : the orbital eccentricty
17
+ - $x$ : cosine of the orbital inclination
18
+
19
+ where $a = J/M^2$, $pM = 2r_\mathrm{max}r_\mathrm{min}/(r_\mathrm{min}+r_\mathrm{max})$ and $e = (r_\mathrm{max}-r_\mathrm{min})/(r_\mathrm{min}+r_\mathrm{max})$. The motion can also be described in terms of the conserved orbital constants:
20
+
21
+ - $E$ : the specific orbital energy
22
+ - $L_z$ : the specific orbital angular momentum
23
+ - $Q$ : the Carter constant
24
+
25
+ which have units $\{1, M, M^2\}$, respectively, along with the mass of the small body $\mu$.
26
+
27
+ With these conserved quantities, we obtain four first-order ordinary differential equations (ODEs) for $x_p^\mu$, which decouple when parametrized in terms of the Mino(-Carter) time parameter $\lambda$,
28
+ $$\begin{align}
29
+ \frac{dt_p}{d\lambda} &= V_{tr}(r_p) + V_{t\theta}(\theta_p),
30
+ \\
31
+ \frac{dr_p}{d\lambda} &= \pm \sqrt{V_r(r_p)},
32
+ \\
33
+ \frac{d\theta_p}{d\lambda} &= \pm \sqrt{V_\theta(\theta_p)},
34
+ \\
35
+ \frac{d\phi_p}{d\lambda} &= V_{\phi r}(r_p) + V_{\phi \theta}(\theta_p),
36
+ \end{align}$$
37
+ where $d\lambda = \Sigma^{-1} d\tau$, and the potential functions are given by
38
+ $$\begin{align}
39
+ V_{r}(r) &= { P^2(r) - \Delta\left(r^2 + {K} \right),}
40
+ &
41
+ V_{\theta}(\theta) &= {{Q} - {L}_z^2 \cot^2\theta - a^2 (1 -{E}^2)\cos^2\theta,}
42
+ \\
43
+ V_{tr}(r) &= \frac{r^2+a^2}{\Delta}P(r),
44
+ &
45
+ V_{t\theta}(\theta) &= a{L}_z - a^2 {E} \sin^2\theta,
46
+ \\
47
+ V_{\phi r}(r) &= \frac{a}{\Delta}P(r),
48
+ &
49
+ V_{\phi \theta}(\theta) &= {L}_z \csc^2\theta - a {E},
50
+ \end{align}$$
51
+
52
+ with $P(r) = (r^2+a^2){E} - a {L}_z$.
53
+
54
+ The resulting bound solutions can be separated into terms that are periodic with respect to the Mino time radial and polar frequencies $\Upsilon_r$ and $\Upsilon_\theta$ and terms that grow secularly with the Mino time rates $\Upsilon_t$ and $\Upsilon_\phi$.
55
+ Therefore, the fundamental coordinate time frequencies are given by
56
+ $$\begin{align}
57
+ \Omega_r &= \frac{\Upsilon_r}{\Upsilon_t},
58
+ &
59
+ \Omega_\theta &= \frac{\Upsilon_\theta}{\Upsilon_t},
60
+ &
61
+ \Omega_\phi &= \frac{\Upsilon_\phi}{\Upsilon_t}.
62
+ \end{align}$$
63
+
64
+ We then represent the radial and polar motion by
65
+ $$\begin{align}
66
+ r_p(\lambda) &= \Delta r^{(r)}(\Upsilon_r\lambda) = \Delta r^{(r)}(\Upsilon_r\lambda + 2\pi),
67
+ \\
68
+ \theta_p(\lambda) &= \Delta \theta^{(\theta)}(\Upsilon_\theta\lambda) = \Delta \theta^{(\theta)}(\Upsilon_\theta\lambda + 2\pi),
69
+ \end{align}$$
70
+ while time and azimuthal angle grow as
71
+ $$\begin{align}
72
+ t_p(\lambda) &= \Upsilon_t \lambda + \Delta t^{(r)}(\Upsilon_r\lambda) + \Delta t^{(\theta)}(\Upsilon_\theta\lambda),
73
+ \\
74
+ \phi_p(\lambda) &= \Upsilon_\phi \lambda + \Delta \phi^{(r)}(\Upsilon_r\lambda) + \Delta \phi^{(\theta)}(\Upsilon_\theta\lambda),
75
+ \end{align}$$
76
+ where $\Delta t^{(r)}$, $\Delta \phi^{(r)}$, $\Delta t^{(\theta)}$, and $\Delta \phi^{(\theta)}$ are $2\pi$-periodic odd functions.