gravityp 0.1.0__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.
gravityp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Álvaro Salazar Cuadros
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: gravityp
3
+ Version: 0.1.0
4
+ Summary: GRAVITYp is a Python library to study the optical appearance of spherically symmetric compact objects in General Relativity. It stands for Geodesic RAys and Visualization of IntensiTY profiles.
5
+ Author-email: Alvaro Salazar-Cuadros <a.salazar@uva.es>, Diego Saez-Chillon Gomez <diego.saez@uva.es>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/alvaroslzar/GRAVITYp
8
+ Project-URL: Issues, https://github.com/alvaroslzar/GRAVITYp/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: numpy>=1.26
15
+ Requires-Dist: scipy>=1.11
16
+ Requires-Dist: matplotlib>=3.8
17
+ Provides-Extra: notebooks
18
+ Requires-Dist: jupyter>=1.0; extra == "notebooks"
19
+ Requires-Dist: ipykernel>=6.0; extra == "notebooks"
20
+ Dynamic: license-file
21
+
22
+ # GRAVITYp
23
+
24
+ **GRAVITYp** is a Python package for studying the optical appearance of spherically symmetric compact objects in General Relativity.
25
+ It stands for **G**eodesic **RA**ys and **V**isualization of **I**ntensi**TY** **p**rofiles.
26
+
27
+ ## About the project
28
+
29
+ This is the Python implementation of **GRAVITYp**, which is originally a numerical code in Wolfram language developed by G.J. Olmo, D. Rubiera García, J.L. Rosa, D. Sáez-Chillón and collaborators [arXiv:2307.06778v2](https://arxiv.org/abs/2307.06778v2).
30
+ It was developed to study the optical appearance or *shadow* cast by black holes, wormholes and different kinds of exotic compact objects, inspired by the work of S.E. Gralla et al. [arXiv:1906.00873](https://arxiv.org/abs/1906.00873).
31
+
32
+ Then, Á. Salazar Cuadros preferred to work on Python seeking faster execution times for simulations and a language better suited for Version Control Systems (VCS), and that's how the Python implementation was born.
33
+ The goal of this open source version is to provide open access to anyone interested in using the package or learning how simulated black hole shadow images are generated.
34
+
35
+ ## Installation
36
+
37
+ You can clone `GRAVITYp` locally in editable mode for research and development:
38
+
39
+ ```bash
40
+ git clone git@github.com:alvaroslzar/GRAVITYp.git # SSH
41
+ git clone https://github.com/alvaroslzar/GRAVITYp.git # HTTPS
42
+ ```
43
+
44
+ Then, install it in your virtual environment
45
+
46
+ ```bash
47
+ cd GRAVITYp
48
+ pip install -e .
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ ### Quick example
54
+
55
+ ```python
56
+ import numpy as np
57
+ from gravityp import make_geodesics_plot
58
+
59
+ def g_rr(r: float, dummy) -> float:
60
+ """Radial function g^{rr} of the Schwarzschild metric in units of M=1"""
61
+ return 1 - 2./r
62
+
63
+ def g_thth(r: float, dummy) -> float:
64
+ """Areal radius squared g^{theta theta} of the Schwarzschild in units of M=1"""
65
+ return r**2
66
+
67
+ # Dictionary codifying Schwarzschild metric and required params
68
+ Schwarzschild_kwargs = {
69
+ 'r_phs': [3.,], # Photon sphere at r=3M
70
+ 'inner_edge': 2., # Inner edge of disk at horizon r=2M
71
+ 'radial_fun': g_rr, # Above defined radial function
72
+ 'radial_params': None, # g_rr has no additional params
73
+ 'areal': g_thth, # Above defined areal radius squared
74
+ 'areal_params': None, # g_thth has no additional params
75
+ }
76
+
77
+ # Array of impact parameters from 0M to 10M
78
+ impact_parameters = np.arange(0,10,0.2)
79
+ make_geodesics_plot(impact_parameters, figsize=(4,4), **Schwarzschild_kwargs)
80
+ ```
81
+
82
+ The output image looks like this:
83
+
84
+ ![ray-tracing](docs/images/README_ray_tracing.png)
85
+
86
+ For a more detailed explanation, check the Jupyter notebooks in the [Examples](Examples/) folder for details.
87
+
88
+ ### Intensity profiles and black hole shadows
89
+
90
+ The primary use is to obtain the observed intensity profile $I_\mathrm{ob}(b)$ as a function of the impact parameter $b$ given some predefined emission profile $I_\mathrm{em}(r)$.
91
+ The emitted light is due to the matter in the accretion disk, which is assumed to be both geometrically and optically thin.
92
+ Moreover, the orientation is assumed to be face-on with respect to the accretion disk, so the optical appearance has have rotational symmetry.
93
+
94
+ In the following images, the emission profile of the accretion disk has been chosen to peak at the event horizon $r=2M$, and the optical appearance corresponds to a Schwarzschild black hole.ç
95
+ The code properly reproduces the predicted light rings up to two intersections with the accretion disk, as well as the central brightness depression or *shadow* characteristic of compact objects such as black holes.
96
+
97
+ ![emitted](docs/images/README_emitted.png)
98
+ ![observed](docs/images/README_observed.png)
99
+ ![shadow](docs/images/README_shadow.png)
100
+
101
+ ### Further references
102
+
103
+ Watch Prof. D. Rubiera García's [talk](https://www.youtube.com/live/q0a4RXdxk4o?si=njtj2yOmCwim25ix) for an overview of the main concepts involved in the ray-tracing method and analysis of intensity profiles.
104
+
105
+ For an introductory discussion of the inner workings of the Wolfram Mathematica code, watch Prof. G.J. Olmo's [tutorial](https://www.youtube.com/live/f5-s2gVd5xE?si=xCtJxFQikmkLehdU); for a more user-friendly application, watch Dr. J.L. Rosa's [talk](https://www.youtube.com/live/9k8qMq9V814?si=MtXYMg0exd_6_37t).
106
+
107
+ Finally, for a more advanced topic regarding compact objects, watch Prof. D. Sáez-Chillón's [contribution](https://www.youtube.com/watch?v=8pR8kE_ABzQ).
108
+
109
+ ## Citation
110
+
111
+ The use of **GRAVITYp** in scientific publications must be properly acknowledged.
112
+ Please cite the following:
113
+
114
+ **BibTeX**
115
+ ```
116
+ @article{Nojiri:2026tjn,
117
+ author = "Nojiri, Shin'ichi and Odintsov, Sergei D. and S{\'a}ez-Chill{\'o}n G{\'o}mez, Diego and Cuadros, {\'A}lvaro Salazar",
118
+ title = "{Horizon singularity, energy conditions and shadows in time-dependent and spherically symmetric spacetime}",
119
+ eprint = "2608.15740",
120
+ archivePrefix = "arXiv",
121
+ primaryClass = "gr-qc",
122
+ reportNumber = "KEK-TH-2861, KEK-Cosmo-0429",
123
+ month = "8",
124
+ year = "2026"
125
+ }
126
+ ```
127
+
128
+ ## Publications using GRAVITYp
129
+
130
+ Let us know if you use **GRAVITYp** in your publication, and we will add it to the [list of publications](/docs/PUBLICATIONS.md)!
131
+
132
+ ## Author
133
+
134
+ The author and mantainer of the Python code is Álvaro Salazar Cuadros.
135
+
136
+ ## License
137
+
138
+ The software is licensed under the [MIT license](LICENSE).
@@ -0,0 +1,117 @@
1
+ # GRAVITYp
2
+
3
+ **GRAVITYp** is a Python package for studying the optical appearance of spherically symmetric compact objects in General Relativity.
4
+ It stands for **G**eodesic **RA**ys and **V**isualization of **I**ntensi**TY** **p**rofiles.
5
+
6
+ ## About the project
7
+
8
+ This is the Python implementation of **GRAVITYp**, which is originally a numerical code in Wolfram language developed by G.J. Olmo, D. Rubiera García, J.L. Rosa, D. Sáez-Chillón and collaborators [arXiv:2307.06778v2](https://arxiv.org/abs/2307.06778v2).
9
+ It was developed to study the optical appearance or *shadow* cast by black holes, wormholes and different kinds of exotic compact objects, inspired by the work of S.E. Gralla et al. [arXiv:1906.00873](https://arxiv.org/abs/1906.00873).
10
+
11
+ Then, Á. Salazar Cuadros preferred to work on Python seeking faster execution times for simulations and a language better suited for Version Control Systems (VCS), and that's how the Python implementation was born.
12
+ The goal of this open source version is to provide open access to anyone interested in using the package or learning how simulated black hole shadow images are generated.
13
+
14
+ ## Installation
15
+
16
+ You can clone `GRAVITYp` locally in editable mode for research and development:
17
+
18
+ ```bash
19
+ git clone git@github.com:alvaroslzar/GRAVITYp.git # SSH
20
+ git clone https://github.com/alvaroslzar/GRAVITYp.git # HTTPS
21
+ ```
22
+
23
+ Then, install it in your virtual environment
24
+
25
+ ```bash
26
+ cd GRAVITYp
27
+ pip install -e .
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Quick example
33
+
34
+ ```python
35
+ import numpy as np
36
+ from gravityp import make_geodesics_plot
37
+
38
+ def g_rr(r: float, dummy) -> float:
39
+ """Radial function g^{rr} of the Schwarzschild metric in units of M=1"""
40
+ return 1 - 2./r
41
+
42
+ def g_thth(r: float, dummy) -> float:
43
+ """Areal radius squared g^{theta theta} of the Schwarzschild in units of M=1"""
44
+ return r**2
45
+
46
+ # Dictionary codifying Schwarzschild metric and required params
47
+ Schwarzschild_kwargs = {
48
+ 'r_phs': [3.,], # Photon sphere at r=3M
49
+ 'inner_edge': 2., # Inner edge of disk at horizon r=2M
50
+ 'radial_fun': g_rr, # Above defined radial function
51
+ 'radial_params': None, # g_rr has no additional params
52
+ 'areal': g_thth, # Above defined areal radius squared
53
+ 'areal_params': None, # g_thth has no additional params
54
+ }
55
+
56
+ # Array of impact parameters from 0M to 10M
57
+ impact_parameters = np.arange(0,10,0.2)
58
+ make_geodesics_plot(impact_parameters, figsize=(4,4), **Schwarzschild_kwargs)
59
+ ```
60
+
61
+ The output image looks like this:
62
+
63
+ ![ray-tracing](docs/images/README_ray_tracing.png)
64
+
65
+ For a more detailed explanation, check the Jupyter notebooks in the [Examples](Examples/) folder for details.
66
+
67
+ ### Intensity profiles and black hole shadows
68
+
69
+ The primary use is to obtain the observed intensity profile $I_\mathrm{ob}(b)$ as a function of the impact parameter $b$ given some predefined emission profile $I_\mathrm{em}(r)$.
70
+ The emitted light is due to the matter in the accretion disk, which is assumed to be both geometrically and optically thin.
71
+ Moreover, the orientation is assumed to be face-on with respect to the accretion disk, so the optical appearance has have rotational symmetry.
72
+
73
+ In the following images, the emission profile of the accretion disk has been chosen to peak at the event horizon $r=2M$, and the optical appearance corresponds to a Schwarzschild black hole.ç
74
+ The code properly reproduces the predicted light rings up to two intersections with the accretion disk, as well as the central brightness depression or *shadow* characteristic of compact objects such as black holes.
75
+
76
+ ![emitted](docs/images/README_emitted.png)
77
+ ![observed](docs/images/README_observed.png)
78
+ ![shadow](docs/images/README_shadow.png)
79
+
80
+ ### Further references
81
+
82
+ Watch Prof. D. Rubiera García's [talk](https://www.youtube.com/live/q0a4RXdxk4o?si=njtj2yOmCwim25ix) for an overview of the main concepts involved in the ray-tracing method and analysis of intensity profiles.
83
+
84
+ For an introductory discussion of the inner workings of the Wolfram Mathematica code, watch Prof. G.J. Olmo's [tutorial](https://www.youtube.com/live/f5-s2gVd5xE?si=xCtJxFQikmkLehdU); for a more user-friendly application, watch Dr. J.L. Rosa's [talk](https://www.youtube.com/live/9k8qMq9V814?si=MtXYMg0exd_6_37t).
85
+
86
+ Finally, for a more advanced topic regarding compact objects, watch Prof. D. Sáez-Chillón's [contribution](https://www.youtube.com/watch?v=8pR8kE_ABzQ).
87
+
88
+ ## Citation
89
+
90
+ The use of **GRAVITYp** in scientific publications must be properly acknowledged.
91
+ Please cite the following:
92
+
93
+ **BibTeX**
94
+ ```
95
+ @article{Nojiri:2026tjn,
96
+ author = "Nojiri, Shin'ichi and Odintsov, Sergei D. and S{\'a}ez-Chill{\'o}n G{\'o}mez, Diego and Cuadros, {\'A}lvaro Salazar",
97
+ title = "{Horizon singularity, energy conditions and shadows in time-dependent and spherically symmetric spacetime}",
98
+ eprint = "2608.15740",
99
+ archivePrefix = "arXiv",
100
+ primaryClass = "gr-qc",
101
+ reportNumber = "KEK-TH-2861, KEK-Cosmo-0429",
102
+ month = "8",
103
+ year = "2026"
104
+ }
105
+ ```
106
+
107
+ ## Publications using GRAVITYp
108
+
109
+ Let us know if you use **GRAVITYp** in your publication, and we will add it to the [list of publications](/docs/PUBLICATIONS.md)!
110
+
111
+ ## Author
112
+
113
+ The author and mantainer of the Python code is Álvaro Salazar Cuadros.
114
+
115
+ ## License
116
+
117
+ The software is licensed under the [MIT license](LICENSE).
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gravityp"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="Alvaro Salazar-Cuadros", email="a.salazar@uva.es" },
10
+ { name="Diego Saez-Chillon Gomez", email="diego.saez@uva.es" },
11
+
12
+ ]
13
+ description = "GRAVITYp is a Python library to study the optical appearance of spherically symmetric compact objects in General Relativity. It stands for Geodesic RAys and Visualization of IntensiTY profiles."
14
+ readme = "README.md"
15
+ requires-python = ">=3.12"
16
+ dependencies = [
17
+ "numpy>=1.26",
18
+ "scipy>=1.11",
19
+ "matplotlib>=3.8",
20
+ ]
21
+ classifiers = [
22
+ "Programming Language :: Python :: 3",
23
+ "Operating System :: OS Independent",
24
+ ]
25
+ license = "MIT"
26
+ license-files = ["LICEN[CS]E*"]
27
+
28
+ [project.optional-dependencies]
29
+ notebooks = [
30
+ "jupyter>=1.0",
31
+ "ipykernel>=6.0",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/alvaroslzar/GRAVITYp"
36
+ Issues = "https://github.com/alvaroslzar/GRAVITYp/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,46 @@
1
+ """
2
+ GRAVITYp: Python package to study the optical appearance of spherically symmetric
3
+ compact objects in General Relativity.
4
+
5
+ Source: https://github.com/alvaroslzar/GRAVITYp
6
+ """
7
+
8
+ from .utils import *
9
+ from .geodesic_integration import *
10
+ from .ray_tracing import *
11
+ from .rings import *
12
+ from .transfer_functions import *
13
+ from .emission_models import *
14
+ from .shadows import *
15
+ from .plots import *
16
+
17
+ import email.utils
18
+ from importlib.metadata import metadata, PackageNotFoundError
19
+
20
+ __name__ = "gravityp"
21
+
22
+ try:
23
+ _meta = metadata(__name__)
24
+ __version__ = _meta.get("Version", "0.1.0")
25
+
26
+ # setuptools packs pyproject.toml authors into the 'Author-email' header
27
+ _author_email_header = _meta.get("Author-email")
28
+
29
+ if _author_email_header:
30
+ # Parse header into [("Name", "email@domain.com"), ...]
31
+ _parsed_contacts = email.utils.getaddresses([_author_email_header])
32
+
33
+ _names = [name for name, _ in _parsed_contacts if name]
34
+ _emails = [addr for _, addr in _parsed_contacts if addr]
35
+
36
+ # Format as strings so help(gravityp) displays them cleanly
37
+ __author__ = ", ".join(_names) if _names else _meta.get("Author")
38
+ __email__ = ", ".join(_emails) if _emails else None
39
+ else:
40
+ __author__ = _meta.get("Author")
41
+ __email__ = None
42
+
43
+ except PackageNotFoundError:
44
+ __version__ = "0.1.0"
45
+ __author__ = "Gonzalo J. Olmo, Diego Rubiera-Garcia, João Luís Rosa, Diego Sáez-Chillón Gómez, Álvaro Salazar-Cuadros"
46
+ __email__ = None
@@ -0,0 +1,45 @@
1
+ """This module contains some predefined emission profiles
2
+ """
3
+
4
+ import numpy as np
5
+ import scipy.optimize as optimize
6
+
7
+
8
+ __all__ = [
9
+ "intensity_at_ISCO",
10
+ "intensity_at_photon_ring",
11
+ "intensity_at_inner_edge",
12
+ "standard_Unbound",
13
+ "normalized_Standard_Unbound",
14
+ "exponential",
15
+ "uniform"
16
+ ]
17
+
18
+
19
+ def intensity_at_ISCO(r, r_ISCO):
20
+ func = lambda x: (1./(x-(r_ISCO-1))**2) / (1./(r_ISCO-(r_ISCO-1))**2)
21
+ return np.piecewise(r, [r>=r_ISCO,r<r_ISCO] , [func,0] )
22
+
23
+ def intensity_at_photon_ring(r, r_ph):
24
+ func = lambda x: (1./(x-(r_ph-1))**3) / (1./(r_ph-(r_ph-1))**3)
25
+ return np.piecewise(r, [r>=r_ph,r<r_ph] , [func,0] )
26
+
27
+ def intensity_at_inner_edge(r, inner_edge):
28
+ func = lambda x: ( np.pi/2-np.arctan(x-5) ) / ( np.pi/2-np.arctan(inner_edge-5) )
29
+ return np.piecewise(r, [r>=inner_edge,r<inner_edge] , [func,0] )
30
+
31
+ def standard_Unbound(r, mu: float=8., sigma: float=2., gamma: float=2.):
32
+ return ( np.exp(-0.5*(gamma+np.arcsinh((r-mu)/sigma))**2) /
33
+ np.sqrt((r-mu)**2+sigma**2) )
34
+
35
+ def normalized_Standard_Unbound(r, mu: float=8., sigma: float=2., gamma: float=2.):
36
+ # FIXME: for some values of params, it doesn't find the minimum
37
+ neg_fun = lambda r: -standard_Unbound(r, mu, sigma, gamma)
38
+ minimum = optimize.minimize(neg_fun, 0).fun
39
+ return neg_fun(r)/minimum
40
+
41
+ def exponential(r):
42
+ return np.exp(-r)
43
+
44
+ def uniform(r):
45
+ return np.ones(shape=r.shape)
@@ -0,0 +1,192 @@
1
+ """This module contains all the functions needed for the geodesic integration
2
+ routine
3
+ """
4
+
5
+ import numpy as np
6
+ import scipy.integrate as integrate
7
+ import scipy.optimize as optimize
8
+ from bisect import bisect_right
9
+
10
+ from .utils import to_tuple
11
+
12
+
13
+ __all__ = [
14
+ "potential",
15
+ "get_increasing_maxima",
16
+ "find_interval",
17
+ "find_rmin",
18
+ "geodesic_in",
19
+ "geodesic_out",
20
+ "compute_outgoing",
21
+ "compute_geodesic",
22
+ ]
23
+
24
+ DEFAULT_NPOINTS = 100000 # ODE integration resolution
25
+ DEFAULT_R0 = 1000.0 # Asymptotic boundary for initial conditions
26
+ MINIMUM_R = 1e-6 # Minimum value of r for integration
27
+
28
+
29
+ def potential(r, radial_fun, radial_params, areal, areal_params):
30
+ """
31
+ Effective potential of a spherically symmetric metric of the form:
32
+
33
+ ds^2 = - radial_fun(r; radial_params)dt^2 + 1/radial_fun(r; radial_params)dr^2 + areal(r; areal_params)dOmega^2
34
+
35
+ Parameters:
36
+ r (float): radial coordinate of the metric
37
+ radial_fun (callable): -gtt metric function. Its first argument must be r
38
+ radial_params (float | tuple): additional parameters for radial_fun
39
+ areal (callable): areal radius squared. Its first argument must be r
40
+ areal_params (float | tuple): additional parameters for areal
41
+ """
42
+ return radial_fun(r,*to_tuple(radial_params)) / areal(r,*to_tuple(areal_params))
43
+
44
+ def get_increasing_maxima(r_phs, potential):
45
+ """Returns increasing local maxima of the potential with their photon sphere values"""
46
+ if len(r_phs) >= 1: # There has to be at least 1 local maximum
47
+ r_phs = np.array(r_phs)
48
+ r_phs = np.sort(r_phs)[::-1] # descending order of
49
+ photon_sph = r_phs[0].reshape(1,)
50
+ local_maxima = potential(photon_sph)
51
+ for r_ph in r_phs:
52
+ maximum = potential(r_ph)
53
+ if maximum > local_maxima.max(): # We take increasing local maxima
54
+ local_maxima = np.append(local_maxima, maximum)
55
+ photon_sph = np.append(photon_sph, r_ph)
56
+ return photon_sph[::-1], local_maxima[::-1] # Swap to ascending order of photon_spheres
57
+ else:
58
+ return np.array([]), np.array([])
59
+
60
+ def find_interval(inverse_b2, photon_sph, local_maxima, lower_bound=1e-4, upper_bound=100):
61
+ """Returns the bounds (r1,r2) where the rmin is found for a given 1/b^2"""
62
+ # NOTE: photon_sph and local_maxima have to be ordered by get_increasing_maxima
63
+ if len(photon_sph)==0 and len(local_maxima)==0: # Case 1: there is no local maximum
64
+ return (lower_bound, upper_bound)
65
+ if inverse_b2 >= local_maxima[0]: # Case 2: 1/b^2 higher than all local maxima
66
+ return (lower_bound, photon_sph[0])
67
+ if inverse_b2 <= local_maxima[-1]: # Case 3: 1/b^2 lower than all local maxima
68
+ return (photon_sph[-1], upper_bound)
69
+
70
+ # Case 4: there are more than 1 local maximum and 1/b^2 lies between two of them
71
+ # We negate both the array elements and 1/b^2 so the logic mirrors an increasing array
72
+ idx = bisect_right(local_maxima, -inverse_b2, key=lambda x: -x)
73
+ return (photon_sph[idx - 1], photon_sph[idx])
74
+
75
+ def find_rmin(b, r_phs, inner_edge, radial_fun, radial_params, areal, areal_params):
76
+ """
77
+ Finds the minimum radius of the trajectory using Brent's method.
78
+
79
+ Parameters:
80
+ b (float): impact parameter of the photon trajectory
81
+ r_phs (array): array with the position of photon spheres (maxima) of the potential
82
+ inner_edge (float): value of the r coordinate of the inner edge (event horizon, throat, etc.)
83
+ potential (callable): effective potential
84
+
85
+ Returns:
86
+ i) If b > bmin, then rmin is found by solving V(r)-1/b^2 = 0
87
+ ii) If b < bmin, then rmin = inner_edge.
88
+ """
89
+ pot = lambda r: potential(r, radial_fun, radial_params, areal, areal_params) # V(r)
90
+ photon_sph, local_maxima = get_increasing_maxima(r_phs, pot)
91
+ bounds = find_interval(1/b**2, photon_sph, local_maxima) # Interval for Brent's method
92
+ fun = lambda r: pot(r)-1/b**2
93
+ if fun(bounds[0])*fun(bounds[1]) >= 0: # No root if there is no sign change
94
+ root = 0
95
+ else:
96
+ root, results = optimize.brentq(fun, *bounds, full_output=True)
97
+ return max( max(root, inner_edge) , MINIMUM_R )
98
+
99
+ def geodesic_in(phi, r, b, radial_fun, radial_params, areal, areal_params):
100
+ """Expression for dphi/dr in the incoming null geodesic equation"""
101
+ areal_params = to_tuple(areal_params)
102
+ radial_params = to_tuple(radial_params)
103
+ return -( b / np.sqrt(areal(r,*areal_params)) )/np.sqrt( areal(r,*areal_params) - b**2*radial_fun(r,*radial_params) )
104
+
105
+ def geodesic_out(phi, r, b, radial_fun, radial_params, areal, areal_params):
106
+ """Expression for dphi/dr in the outgoing null geodesic equation"""
107
+ areal_params = to_tuple(areal_params)
108
+ radial_params = to_tuple(radial_params)
109
+ return +( b / np.sqrt(areal(r,*areal_params)) )/np.sqrt( areal(r,*areal_params) - b**2*radial_fun(r,*radial_params) )
110
+
111
+ def compute_outgoing(r_phs, inner_edge, rmin) -> bool:
112
+ """Returns a boolean that tells whether to compute outgoing geodesic after reaching rmin"""
113
+ compute = False
114
+ if (len(r_phs)>0): # There is at least 1 photon sphere
115
+ innermost_photon_sphere = np.array(r_phs).min()
116
+ if (innermost_photon_sphere < MINIMUM_R):
117
+ if (rmin > MINIMUM_R):
118
+ compute = True
119
+ # if (rmin > inner_edge) and (innermost_photon_sphere > 0): # Probamos con inner_edge mejor
120
+ else:
121
+ # print('Hola')
122
+ if (rmin > inner_edge) and (rmin > MINIMUM_R): # Probamos con inner_edge mejor
123
+ compute = True
124
+ else: # There is no photon sphere
125
+ compute = True
126
+ return compute
127
+
128
+ def compute_geodesic(b, r_phs, inner_edge, radial_fun, radial_params, areal, areal_params,
129
+ Npoints=DEFAULT_NPOINTS, r0=DEFAULT_R0):
130
+ """
131
+ Computes the null geodesic trajectory in Schwarzschild spacetime.
132
+
133
+ Integrates the geodesic equations to trace light ray paths. For incoming rays,
134
+ integrates from r0 down to rmin. For rays escaping to infinity (rmin > 3),
135
+ also integrates the outgoing trajectory from rmin back to r0.
136
+
137
+ Parameters:
138
+ b (float): Impact parameter in units of M=1
139
+ Npoints (int): Number of radial points for integration. Default: DEFAULT_NPOINTS
140
+ r0 (float): Starting radius. Default: DEFAULT_R0
141
+
142
+ Returns:
143
+ tuple[np.ndarray, np.ndarray]: (rr, phi) where rr is the radial coordinate
144
+ array and phi is the azimuthal angle array along the geodesic
145
+ """
146
+ rmin = find_rmin(b, r_phs, inner_edge, radial_fun, radial_params, areal, areal_params)
147
+ rr = np.geomspace(r0, rmin, Npoints) # We must ensure rmin > 0 in find_rmin for np.geomspace
148
+
149
+ phi0 = b/rr[0] # good approximation for r0>>rSch and better than phi0=0
150
+ phi = integrate.odeint(
151
+ geodesic_in, phi0, rr, (b,radial_fun,radial_params,areal,areal_params)
152
+ ).reshape(-1,)
153
+ phi = phi[~np.isnan(phi)]
154
+ rr = rr[:len(phi)]
155
+
156
+ # NOTE: does not work properly with high r0 and low Npoints for outgoing geodesics
157
+ # FIXME: si len(r_phs)=0, solo integra incoming.
158
+ if compute_outgoing(r_phs, inner_edge, rmin):
159
+ rout = rr[::-1]
160
+ phi0 = 2*phi[-1]-phi[-2] # Linear approx. for next angle. Required for strict monotonicity!!
161
+ phi_out = integrate.odeint(
162
+ geodesic_out, phi0, rout, (b,radial_fun,radial_params,areal,areal_params)
163
+ ).reshape(-1,)
164
+ rr = np.concatenate([rr,rout])
165
+ phi = np.concatenate([phi,phi_out])
166
+
167
+ return (rr,phi)
168
+
169
+
170
+ if __name__=="__main__":
171
+
172
+ def g_rr(r: float, dummy) -> float:
173
+ """Radial function g^{rr} of the Schwarzschild metric in units of M=1"""
174
+ return 1 - 2./r
175
+
176
+ def g_thth(r: float, dummy) -> float:
177
+ """Areal radius squared g^{theta theta} of the Schwarzschild in units of M=1"""
178
+ return r**2
179
+
180
+ # Dictionary codifying Schwarzschild metric and required params
181
+ Schwarzschild_kwargs = {
182
+ 'r_phs': [3.,], # Photon sphere at r=3M
183
+ 'inner_edge': 2., # Inner edge of disk at horizon r=2M
184
+ 'radial_fun': g_rr, # Above defined radial function
185
+ 'radial_params': None, # g_rr has no additional params
186
+ 'areal': g_thth, # Above defined areal radius squared
187
+ 'areal_params': None, # g_thth has no additional params
188
+ }
189
+
190
+
191
+ rr, phi = compute_geodesic(3, **Schwarzschild_kwargs)
192
+ print(rr, phi)