├── .flake8 ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── _config.yml ├── doc ├── earth_black_hole_animation.png ├── earth_black_hole_animation_zoomed.png ├── earth_black_hole_plot.png ├── gros_logo.png ├── gros_logo.svg ├── gros_logo_1280x640.png └── mercury_plot.png ├── setup.py └── src └── gros ├── __init__.py ├── examples ├── earth_black_hole_trajectory_animate.py ├── earth_black_hole_trajectory_plot_only.py └── mercury_trajectory.py ├── metric ├── __init__.py └── schwarzschild.py ├── tests ├── metric │ └── test_schwarzschild.py └── utils │ └── test_datahandling.py └── utils ├── __init__.py ├── const.py ├── datahandling.py ├── log.py └── transforms.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: ["3.9", "3.10", "3.11"] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v3 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | python -m pip install flake8 pytest 28 | # if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 29 | pip install . 30 | - name: Lint with flake8 31 | run: | 32 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics 33 | - name: Test with pytest 34 | run: | 35 | pytest -vvv src/gros/tests 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.venv/ 2 | *.cpython-37.pyc 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Python: Current File", 6 | "type": "python", 7 | "request": "launch", 8 | "program": "${file}", 9 | "console": "integratedTerminal", 10 | "envFile": "${workspaceFolder}/.env", 11 | "env": { 12 | "PYTHONPATH": "${workspaceFolder}/src/:${workspaceFolder}/src/gros/:${env:PYTHONPATH}" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": false, 3 | "python.linting.flake8Enabled": true, 4 | "python.linting.enabled": true, 5 | "python.formatting.provider": "black", 6 | "python.pythonPath": ".venv/bin/python" 7 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "echo workspaceFolder", 6 | "type": "shell", 7 | "command": "echo ${workspaceFolder}" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 BjoB (Björn Barschtipan) 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ----------------- 6 | 7 | # Status 8 | 9 | 10 | [![bytebat](https://github.com/bytebat/gros/actions/workflows/ci.yml/badge.svg)](https://github.com/bytebat/gros/actions/workflows/ci.yml/badge.svg) 11 | [![Generic badge](https://img.shields.io/badge/powered%20by-astropy-blue.svg)](https://img.shields.io/badge/powered--by-astropy-blue) 12 | [![Generic badge](https://img.shields.io/badge/powered%20by-plotly-blue.svg)](https://img.shields.io/badge/powered--by-plotly-blue) 13 | 14 | # Overview 15 | 16 | **gros** is a python package to numerically calculate and simulate particle trajectories based on the field equations of general relativity. A user needs to define a certain metric by providing the mass of a central gravitational attractor and the start coordinates and velocity of the test particle. 17 | 18 | # Installation 19 | 20 | Clone the repository and install the package via pip in your choosen environment: 21 | 22 | ```sh 23 | pip install . 24 | ``` 25 | 26 | # Theoretical background 27 | 28 | To simulate particle trajectories around a spherically symmetric body, we use the *Schwarzschild* solution of Einstein's field equations, describing the exterior spacetime for our use case. Starting from the field equations 29 | 30 | $$\large R_{\mu\nu}-\frac{1}{2}g_{\mu\nu}R=\frac{8\pi%20G}{c^4}T_{\mu\nu}$$ 31 | 32 | with a vanishing energy momentum tensor 33 | 34 | $$\large T_{\mu\nu}=0$$ 35 | 36 | the *Schwarzschild* metric can be derived as 37 | 38 | $$\large ds^2=g_{\mu\nu}dx^\mu dx^\nu=c^2 (1-\frac{r_s}{r})dt^2-\frac{1}{1-r_s/r}dr^2-r^2d\theta^2-r^2sin^2\theta d\phi^2$$ 39 | 40 | with the *Schwarzschild radius* $r_s=2GM/c^2$ and the space time coordinates based on spherical coordinates $(x^0,x^1,x^2,x^3) \mapsto (ct, r,\theta, \phi)$. 41 | 42 | The intrinsic space time curvature can be derived from the metric by evaluating the *Christoffel symbols* given with 43 | 44 | $$\large \Gamma_{\alpha\nu}^{\beta}=\frac{1}{2}g^{\mu\beta}(\partial_{\alpha}g_{\mu\nu}+\partial_{\nu}g_{\mu\alpha}-\partial_\mu g_{\alpha\nu})$$ 45 | 46 | After calculating these coefficients and using the proper time as parameter, the motion of of a particle in the gravitational field can be retrieved by solving the system of differential equations given with the geodesic equations 47 | 48 | $$\large \frac{d^2 x^\mu}{d\tau^2}+\Gamma_{\alpha\beta}^{\mu}\frac{dx^\alpha}{d\tau}\frac{dx^\beta}{d\tau}=0$$ 49 | 50 | # Examples 51 | 52 | Some simple simulations can be found in the [examples](https://github.com/BjoB/gros/tree/master/src/gros/examples) directory. 53 | 54 | ## Particle on Mercury's orbit 55 | 56 | This example simulates a particle orbiting the sun with some initial orbital parameters taken from . After calculating the trajectory, an animation will be generated, which can be used to track the particle with a previously choosen step size. 57 | 58 |

59 | 60 |

61 | 62 | ## Earth as a black hole 63 | 64 | What if earth was a black hole? The according example shows how a particle would act in short distance of 30m. Especially the perihelion precession is visualized as a direct effect of general relativity. Additionally the gravitational time dilation can be tracked along the animation frames with τ as the proper time of the particle. t is the calculated coordinate time, which can be seen as the measured proper time of a hypothetical observer positioned infinitely far away from the gravitational center. 65 | 66 |

67 | 68 |

69 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /doc/earth_black_hole_animation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebat/gros/2e0e68a315249cb8707a344a0586ccd77a2f1b94/doc/earth_black_hole_animation.png -------------------------------------------------------------------------------- /doc/earth_black_hole_animation_zoomed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebat/gros/2e0e68a315249cb8707a344a0586ccd77a2f1b94/doc/earth_black_hole_animation_zoomed.png -------------------------------------------------------------------------------- /doc/earth_black_hole_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebat/gros/2e0e68a315249cb8707a344a0586ccd77a2f1b94/doc/earth_black_hole_plot.png -------------------------------------------------------------------------------- /doc/gros_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebat/gros/2e0e68a315249cb8707a344a0586ccd77a2f1b94/doc/gros_logo.png -------------------------------------------------------------------------------- /doc/gros_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 28 | 32 | 36 | 37 | 46 | 55 | 64 | 73 | 83 | 84 | 112 | 114 | 115 | 117 | image/svg+xml 118 | 120 | 121 | 122 | 123 | 124 | 129 | 185 | 207 | 218 | 243 | 248 | 251 | 256 | 262 | 268 | 274 | 280 | 286 | 292 | 298 | 304 | 310 | 315 | 320 | 325 | 330 | 335 | 340 | 345 | 350 | 355 | 356 | 361 | 367 | 373 | 379 | 385 | 391 | 397 | 403 | 409 | 415 | 420 | 425 | 430 | 435 | 440 | 445 | 450 | 455 | 460 | 461 | 466 | 472 | 478 | 484 | 490 | 496 | 502 | 508 | 514 | 520 | 525 | 530 | 535 | 540 | 545 | 550 | 555 | 560 | 565 | 566 | 571 | 577 | 583 | 589 | 595 | 601 | 607 | 613 | 619 | 625 | 630 | 635 | 640 | 645 | 650 | 655 | 660 | 665 | 670 | 671 | 676 | 682 | 688 | 694 | 700 | 706 | 712 | 718 | 724 | 730 | 735 | 740 | 745 | 750 | 755 | 760 | 765 | 770 | 775 | 776 | 781 | 787 | 793 | 799 | 805 | 811 | 817 | 823 | 829 | 835 | 840 | 845 | 850 | 855 | 860 | 865 | 870 | 875 | 880 | 881 | 886 | 892 | 898 | 904 | 910 | 916 | 922 | 928 | 934 | 940 | 945 | 950 | 955 | 960 | 965 | 970 | 975 | 980 | 985 | 986 | 991 | 997 | 1003 | 1009 | 1015 | 1021 | 1027 | 1033 | 1039 | 1045 | 1050 | 1055 | 1060 | 1065 | 1070 | 1075 | 1080 | 1085 | 1090 | 1091 | 1096 | 1102 | 1108 | 1114 | 1120 | 1126 | 1132 | 1138 | 1144 | 1150 | 1155 | 1160 | 1165 | 1170 | 1175 | 1180 | 1185 | 1190 | 1195 | 1196 | 1201 | 1214 | 1227 | 1240 | 1253 | 1266 | 1279 | 1280 | 1285 | 1297 | 1309 | 1321 | 1333 | 1345 | 1357 | 1369 | 1381 | 1388 | 1395 | 1396 | 1403 | 1410 | 1411 | 1412 | 1417 | G R O S 1429 | eneral 1440 | elativity 1451 | rbit 1462 | imulator 1473 | 1474 | 1475 | 1476 | 1477 | -------------------------------------------------------------------------------- /doc/gros_logo_1280x640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebat/gros/2e0e68a315249cb8707a344a0586ccd77a2f1b94/doc/gros_logo_1280x640.png -------------------------------------------------------------------------------- /doc/mercury_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebat/gros/2e0e68a315249cb8707a344a0586ccd77a2f1b94/doc/mercury_plot.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | setup( 9 | name='gros', 10 | version='0.1.0', 11 | description='General Relativity Orbit Simulator', 12 | url='https://github.com/BjoB/gros', 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | author='Björn Barschtipan', 16 | author_email='', 17 | license='MIT', 18 | packages=find_packages(where='src'), 19 | package_dir={'': 'src'}, 20 | zip_safe=False, 21 | setup_requires=['wheel'], 22 | python_requires='>=3.5', 23 | install_requires=[ 24 | 'astropy>=4.0', 25 | 'colorlog', 26 | 'numpy', 27 | 'pandas', 28 | 'plotly>=4.0', 29 | 'scipy', 30 | ], 31 | extras_require={ 32 | 'dev': [ 33 | 'black', 34 | 'pycodestyle', 35 | 'pyflakes', 36 | 'pytest', 37 | ], 38 | }, 39 | ) 40 | -------------------------------------------------------------------------------- /src/gros/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebat/gros/2e0e68a315249cb8707a344a0586ccd77a2f1b94/src/gros/__init__.py -------------------------------------------------------------------------------- /src/gros/examples/earth_black_hole_trajectory_animate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from numpy import pi 4 | 5 | from gros.metric import schwarzschild as ss 6 | from gros.utils import const 7 | 8 | 9 | def earth_black_hole_trajectory(): 10 | ss_metric = ss.SchwarzschildMetric( 11 | M=const.M_earth, 12 | initial_vec_pos=[30, pi/2, 0], 13 | initial_vec_v=[0, 0, 20000] 14 | ) 15 | 16 | traj = ss_metric.calculate_trajectory(proptime_end=1.2e-4, step_size=1e-6) 17 | traj.plot(animation_step_size=1e-6) 18 | 19 | 20 | if __name__ == "__main__": 21 | earth_black_hole_trajectory() 22 | -------------------------------------------------------------------------------- /src/gros/examples/earth_black_hole_trajectory_plot_only.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from numpy import pi 4 | 5 | from gros.metric import schwarzschild as ss 6 | from gros.utils import const 7 | 8 | 9 | def earth_black_hole_trajectory(): 10 | ss_metric = ss.SchwarzschildMetric( 11 | M=const.M_earth, 12 | initial_vec_pos=[100, pi/2, 0], 13 | initial_vec_v=[0, 0, 5000] 14 | ) 15 | 16 | traj = ss_metric.calculate_trajectory(proptime_end=0.005, step_size=1e-5) 17 | traj.plot() 18 | 19 | 20 | if __name__ == "__main__": 21 | earth_black_hole_trajectory() 22 | -------------------------------------------------------------------------------- /src/gros/examples/mercury_trajectory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from numpy import pi 4 | 5 | from gros.metric import schwarzschild as ss 6 | from gros.utils import const 7 | 8 | 9 | def mercury_trajectory(): 10 | 11 | # init metric with mass of sun and mercury orbit parameters 12 | ss_metric = ss.SchwarzschildMetric( 13 | M=const.M_solar, 14 | initial_vec_pos=[57.9e6 * 1000, pi/2, 0], 15 | initial_vec_v=[0, 0, (47.4 / 57.9e6)] 16 | ) 17 | 18 | radius_sun = 6.9634e8 19 | 20 | # calculate trajectory for one orbital period 21 | traj = ss_metric.calculate_trajectory(proptime_end=88 * 24 * 3600, step_size=3600) 22 | 23 | # animate with one frame per day 24 | traj.plot(attractor_radius=radius_sun, animation_step_size=24 * 3600) 25 | 26 | # comment out to show mercury's perihelion precession: 27 | # traj = ss_metric.calculate_trajectory(proptime_end=100 * 88 * 24 * 3600, step_size=86400) 28 | # traj.plot(attractor_radius=radius_sun) 29 | 30 | 31 | if __name__ == "__main__": 32 | mercury_trajectory() 33 | -------------------------------------------------------------------------------- /src/gros/metric/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebat/gros/2e0e68a315249cb8707a344a0586ccd77a2f1b94/src/gros/metric/__init__.py -------------------------------------------------------------------------------- /src/gros/metric/schwarzschild.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from astropy import units as u 3 | from scipy import integrate 4 | 5 | from gros.utils import const, log, datahandling as dh 6 | 7 | 8 | logger = log.init_logger(__name__) 9 | 10 | 11 | class SchwarzschildMetric: 12 | """ 13 | Class for calculations with the Schwarschild metric, 14 | e.g. for solving the geodesic equation for an orbiting object. 15 | 16 | Note: See following lecture notes for a deeper look into the used formulas: 17 | https://web.stanford.edu/~oas/SI/SRGR/notes/SchwarzschildSolution.pdf 18 | https://arxiv.org/pdf/0904.4184.pdf 19 | http://www.physics.usu.edu/Wheeler/GenRel/Lectures/GRNotesDecSchwarzschildGeodesicsPost.pdf 20 | """ 21 | 22 | @u.quantity_input(M=u.kg, time=u.s) 23 | def __init__(self, M, initial_vec_pos, initial_vec_v, time=0 * u.s): 24 | """ 25 | Initializes a Schwarzschild metric with given mass, 26 | start time and spatial coordinates and velocities of a test particle. 27 | 28 | Arguments: 29 | M -- mass of gravitational center [kg] 30 | initial_vec_pos -- initial spherical coordinates [r[m], theta[rad], phi[rad]] 31 | sperical_velos -- initial spherical velocities [r', theta', phi'] 32 | time -- initial time [s] 33 | """ 34 | self.M = M.value 35 | self.rs = calc_schwarzschild_radius(M).value 36 | self.a = -self.rs 37 | 38 | logger.info("Event horizon for given mass is at r={}m.".format(self.rs)) 39 | 40 | self.initial_vec_x_u = np.hstack( 41 | ( 42 | time.value, 43 | initial_vec_pos, 44 | self._calc_initial_dt_dtau(initial_vec_pos, initial_vec_v), 45 | initial_vec_v, 46 | ) 47 | ) 48 | 49 | def schwarzschild_radius(self): 50 | """ 51 | Returns the Schwarzschild radius for the initialized mass. 52 | """ 53 | return self.rs 54 | 55 | def calculate_trajectory(self, proptime_end, proptime_start=0, step_size=1e-3): 56 | """ 57 | Solve geodesic equations for provided time interval. 58 | 59 | Arguments: 60 | proptime_end -- end of interval [s] 61 | proptime_start -- start of inteval [s] 62 | step_size -- step size [s] 63 | 64 | Returns: 65 | (N,9) numpy ndarray with N = number of interations, 66 | single row is [tau, t, x, y, z] 67 | """ 68 | tau = [] 69 | y = [] 70 | 71 | for cur_tau, cur_val in self.trajectory_generator( 72 | proptime_end=proptime_end, 73 | proptime_start=proptime_start, 74 | step_size=step_size, 75 | ): 76 | tau.append(cur_tau), y.append(cur_val) 77 | 78 | logger.debug( 79 | "tau={tau}s, t={t}s, r={r}m, dt/dtau={dtdtau}".format( 80 | tau=cur_tau, t=cur_val[0], r=cur_val[1], dtdtau=cur_val[4] 81 | ) 82 | ) 83 | 84 | np_tau = np.reshape(tau, (len(tau), 1)) 85 | np_y = np.array(y) 86 | 87 | # only extract rows for [tau, t, x, y, z] 88 | return dh.SpaceTimeData(np.hstack((np_tau, np_y[:, [0, 1, 2, 3]])), self.rs) 89 | 90 | def trajectory_generator(self, proptime_end, proptime_start=0, step_size=1e-3): 91 | """ 92 | Generator for solving the next time step of the geodesic's ODE system. 93 | 94 | Arguments: 95 | proptime_end -- end of interval [s] 96 | proptime_start -- start proper time [s] 97 | step_size -- step size [s] 98 | 99 | Yields: 100 | proper time, 101 | vector composed of the updated 4-vector and 4-velocity 102 | => [t, r, theta, phi, dt/dtau, dr/dtau, dtheta/dtau, dphi/dtau] 103 | """ 104 | ode_solver = integrate.RK45( 105 | self._calc_next_velo_and_acc_vec, 106 | t0=proptime_start, 107 | y0=self.initial_vec_x_u, 108 | t_bound=1e100, 109 | rtol=0.25 * step_size, 110 | first_step=0.75 * step_size, 111 | max_step=5 * step_size, 112 | ) 113 | 114 | while True: 115 | yield ode_solver.t, ode_solver.y 116 | ode_solver.step() 117 | 118 | current_radius = ode_solver.y[1] 119 | rs = -self.a 120 | rs_border = rs * 1.01 121 | 122 | if ode_solver.t >= proptime_end: 123 | break 124 | 125 | if current_radius <= rs_border: 126 | logger.warning( 127 | "Nearly reached event horizon at r={}m. Stopping here!".format(rs) 128 | ) 129 | break 130 | 131 | def _calc_christoffel_symbols(self, r, theta): 132 | """ 133 | Calculate the Christoffel symbols for the initialized mass 134 | at the coordinates r, theta. 135 | 136 | Arguments: 137 | r -- radius in m 138 | theta -- angle in radians (z-axis) 139 | 140 | Returns: 141 | Christoffel symbols as (4,4,4) numpy array 142 | """ 143 | a = self.a 144 | a_div_r = a / r 145 | chrs = np.zeros(shape=(4, 4, 4), dtype=float) 146 | 147 | chrs[0, 0, 1] = chrs[0, 1, 0] = -0.5 * a / ((r ** 2) * (1 + a_div_r)) 148 | chrs[1, 0, 0] = -0.5 * (1 + a_div_r) * (const.c.value ** 2) * a_div_r / r 149 | chrs[1, 1, 1] = -chrs[0, 0, 1] 150 | chrs[2, 1, 2] = chrs[2, 2, 1] = 1 / r 151 | chrs[1, 2, 2] = -1 * (r + a) 152 | chrs[3, 1, 3] = chrs[3, 3, 1] = chrs[2, 1, 2] 153 | chrs[1, 3, 3] = -1 * (r + a) * (np.sin(theta) ** 2) 154 | chrs[3, 2, 3] = chrs[3, 3, 2] = 1 / np.tan(theta) 155 | chrs[2, 3, 3] = -1 * np.sin(theta) * np.cos(theta) 156 | return chrs 157 | 158 | def _calc_initial_dt_dtau(self, vec_pos, vec_v): 159 | """ 160 | Calculates the initial time velocity dt/dtau. 161 | 162 | Arguments: 163 | vec_pos -- spatial part of four-vector (r, theta, phi) 164 | vec_v -- spatial part of four-velocity (dr/dtau, dtheta/dtau, dphi/dtau) 165 | 166 | Returns: 167 | Initial time velocity dt/dtau 168 | """ 169 | a, c = self.a, const.c.value 170 | c2 = c ** 2 171 | 172 | temp1 = (1 / (c2 * (1 + a / vec_pos[0]))) * (vec_v[0] ** 2) 173 | temp2 = ((vec_pos[0] ** 2) / c2) * (vec_v[1] ** 2) 174 | temp3 = (vec_pos[0] ** 2) / c2 * (np.sin(vec_pos[1]) ** 2) * (vec_v[2] ** 2) 175 | dt_dtau_squared = (1 + temp1 + temp2 + temp3) / (1 + a / vec_pos[0]) 176 | return np.sqrt(dt_dtau_squared) 177 | 178 | def _calc_next_velo_and_acc_vec(self, proper_time, vec_x_u): 179 | """ 180 | Calculates the acceleration components u' of a particle in the gravitational field 181 | based on the current position (four-vector x) and the corresponding four-velocity u. 182 | 183 | Arguments: 184 | vec_x_u -- combined four-vector x and four-velocity u, 185 | [x, u] = [t, r, theta, phi, dt/dtau, dr/dtau, dtheta/dtau, dphi/dtau] 186 | Returns: 187 | 8-component-vector [u, u'], composed of the derivatives of the input vector 188 | """ 189 | derivs = np.zeros(shape=vec_x_u.shape, dtype=vec_x_u.dtype) 190 | chs = self._calc_christoffel_symbols(r=vec_x_u[1], theta=vec_x_u[2]) 191 | 192 | derivs[:4] = vec_x_u[4:8] 193 | derivs[4] = -2 * chs[0, 0, 1] * vec_x_u[4] * vec_x_u[5] 194 | derivs[5] = -1 * ( 195 | chs[1, 0, 0] * (vec_x_u[4] ** 2) 196 | + chs[1, 1, 1] * (vec_x_u[5] ** 2) 197 | + chs[1, 2, 2] * (vec_x_u[6] ** 2) 198 | + chs[1, 3, 3] * (vec_x_u[7] ** 2) 199 | ) 200 | derivs[6] = -2 * chs[2, 2, 1] * vec_x_u[6] * vec_x_u[5] - chs[2, 3, 3] * ( 201 | vec_x_u[7] ** 2 202 | ) 203 | derivs[7] = -2 * ( 204 | chs[3, 1, 3] * vec_x_u[5] * vec_x_u[7] 205 | + chs[3, 2, 3] * vec_x_u[6] * vec_x_u[7] 206 | ) 207 | 208 | return derivs 209 | 210 | 211 | # TODO: 212 | # class Geodesic(SchwarzschildMetric): 213 | # """[summary] 214 | 215 | # Arguments: 216 | # SchwarzschildMetric {[type]} -- [description] 217 | # """ 218 | # @u.quantity_input(M=u.kg, stop_time=u.s, start_time=u.s) 219 | # def __init__(self, M, initial_vec_pos, initial_vec_v, stop_time, start_time=0 * u.s): 220 | # super.__init__(M, initial_vec_pos, initial_vec_v, start_time) 221 | 222 | 223 | @u.quantity_input(M=u.kg) 224 | def calc_schwarzschild_radius(M=u.kg): 225 | """ 226 | Calculates the Schwarzschild radius of a point mass M, 227 | which defines the event horizon of a Schwarzschild black hole. 228 | 229 | Arguments: 230 | M -- mass [kg] 231 | 232 | Returns: 233 | Schwarzschild radius [m] 234 | """ 235 | return 2 * const.G * M / (const.c ** 2) 236 | -------------------------------------------------------------------------------- /src/gros/tests/metric/test_schwarzschild.py: -------------------------------------------------------------------------------- 1 | from gros.metric import schwarzschild as ss 2 | from gros.utils import const 3 | from numpy import pi 4 | from numpy import sqrt 5 | 6 | 7 | def test_calc_schwarzschild_trajectory(): 8 | r = 140000000000 9 | 10 | ss_metric = ss.SchwarzschildMetric( 11 | M=const.M_solar, 12 | initial_vec_pos=[r, pi/2, 0.3], 13 | initial_vec_v=[1000, 0.0, 0.0] 14 | ) 15 | 16 | for val in ss_metric.trajectory_generator(proptime_end=10, step_size=1): 17 | print("tau = {}s, t = {}s, r = {}m, dt/dtau = {}".format(val[0], val[1][0], val[1][1], val[1][4])) 18 | 19 | dt_factor = 1/sqrt(1 - ss_metric.schwarzschild_radius() / r) 20 | print("theoretical dt/dtau = {}".format(dt_factor)) 21 | 22 | assert(val[1][0] > val[0]) 23 | 24 | 25 | test_calc_schwarzschild_trajectory() 26 | -------------------------------------------------------------------------------- /src/gros/tests/utils/test_datahandling.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from gros.utils import datahandling as dh 4 | 5 | 6 | def test_general_data_handling(): 7 | testdata = np.array([ 8 | [0, 0, 1, 2, 3], 9 | [1, 1, 4, 5, 6] 10 | ]) 11 | traj_data = dh.SpaceTimeData(testdata, 1000) 12 | assert(traj_data.size() == 2) 13 | 14 | 15 | test_general_data_handling() 16 | -------------------------------------------------------------------------------- /src/gros/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .const import * 2 | from .datahandling import * 3 | from .log import * 4 | from .transforms import * 5 | -------------------------------------------------------------------------------- /src/gros/utils/const.py: -------------------------------------------------------------------------------- 1 | from astropy import constants 2 | 3 | 4 | c = constants.c 5 | G = constants.G 6 | M_solar = constants.M_sun 7 | M_earth = constants.M_earth 8 | -------------------------------------------------------------------------------- /src/gros/utils/datahandling.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | import pandas as pd 4 | import plotly.graph_objects as go 5 | 6 | from gros.utils import log, transforms as tf 7 | 8 | 9 | logger = log.init_logger(__name__) 10 | DATA_COLUMNS = ["tau", "t", "x", "y", "z"] 11 | 12 | 13 | class SpaceTimeData: 14 | """ 15 | Class for wrapping spacetime data points. 16 | """ 17 | 18 | def __init__( 19 | self, trajectory, rs, conversion=tf.CoordinateConversion.spherical_to_cartesian 20 | ): 21 | """ 22 | Initialize dataset of trajectory points. 23 | Arguments: 24 | rs -- Schwarzschild radius of center mass [m] 25 | """ 26 | if not isinstance(trajectory, np.ndarray): 27 | raise ValueError("dataset must be numpy ndarray!") 28 | if trajectory.shape != (len(trajectory), 5): 29 | raise ValueError("dataset shape needs to be (N,5)!") 30 | 31 | self.rs = rs 32 | self.max_num_anim_frames = 100 33 | 34 | if trajectory.any(): 35 | self.df = pd.DataFrame(trajectory, columns=DATA_COLUMNS) 36 | if conversion is tf.CoordinateConversion.spherical_to_cartesian: 37 | for traj_point in trajectory: 38 | ( 39 | traj_point[2], 40 | traj_point[3], 41 | traj_point[4], 42 | ) = tf.spherical_to_cartesian( 43 | r=traj_point[2], theta=traj_point[3], phi=traj_point[4] 44 | ) 45 | 46 | def size(self): 47 | """ 48 | Returns number of datapoints. 49 | """ 50 | return len(self.df.index) 51 | 52 | def plot(self, attractor_radius=0, animation_step_size=0, theme="dark"): 53 | """ 54 | Plots the provided data points in a 3D scatter plot. 55 | 56 | Arguments: 57 | attractor_radius {int} -- Adds an additional sphere with given radius [m] to the plot (default: {0}) 58 | animation_step_size {int} -- step size [s] > 0 will create an according animation (default: {0}) 59 | theme {str} -- Plotting theme (default: {"dark"}) 60 | """ 61 | TRAJ_COLOR = "skyblue" 62 | SINGULARITY_COLOR = "darkviolet" 63 | BLACK_HOLE_COLOR = "darkviolet" 64 | 65 | data = self.df 66 | 67 | axis_min = min([data["x"].min(), data["y"].min(), data["z"].min()]) 68 | axis_max = max([data["x"].max(), data["y"].max(), data["z"].max()]) 69 | 70 | x = data["x"] 71 | y = data["y"] 72 | z = data["z"] 73 | 74 | # create marker for singularity 75 | singularity = go.Scatter3d( 76 | x=[0], 77 | y=[0], 78 | z=[0], 79 | mode="markers", 80 | marker=dict(size=1, color=SINGULARITY_COLOR), 81 | text="singularity", 82 | name="singularity", 83 | hoverinfo="text", 84 | ) 85 | 86 | # attractor sphere 87 | attractor = self._create_sphere(attractor_radius, "attractor", "yellow", 0.1) 88 | 89 | # black hole sphere at r=rs 90 | black_hole = self._create_sphere(self.rs, "black hole", BLACK_HOLE_COLOR, 0.2) 91 | 92 | # trajectory 93 | traj_plot = go.Scatter3d( 94 | x=x, 95 | y=y, 96 | z=z, 97 | text="trajectory", 98 | name="trajectory", 99 | mode="lines", 100 | line=dict(width=2, color=TRAJ_COLOR), 101 | ) 102 | 103 | plot_data = [ 104 | go.Scatter3d(name="dummy"), 105 | singularity, 106 | black_hole, 107 | attractor, 108 | traj_plot, 109 | ] 110 | 111 | fig = go.Figure( 112 | data=plot_data, 113 | layout=go.Layout( 114 | scene=dict( 115 | xaxis=dict(range=[axis_min, axis_max],), 116 | yaxis=dict(range=[axis_min, axis_max],), 117 | zaxis=dict(range=[axis_min, axis_max],), 118 | aspectmode="cube", 119 | ), 120 | title_text="Particle orbit in gravitational field", 121 | hovermode="closest", 122 | updatemenus=[], 123 | ), 124 | ) 125 | 126 | self._add_aninmation_frames(fig, animation_step_size) 127 | 128 | if theme == "dark": 129 | fig.update_layout(template="plotly_dark") 130 | fig.show() 131 | 132 | def _create_sphere(self, radius, name, color, opacity): 133 | sph_theta = np.linspace(0, 2 * np.pi) 134 | sph_phi = np.linspace(0, 2 * np.pi) 135 | sph_theta, sph_phi = np.meshgrid(sph_phi, sph_theta) 136 | 137 | sph_x, sph_y, sph_z = tf.spherical_to_cartesian(radius, sph_theta, sph_phi) 138 | 139 | sphere = go.Mesh3d( 140 | x=sph_x.flatten(), 141 | y=sph_y.flatten(), 142 | z=sph_z.flatten(), 143 | alphahull=0, 144 | opacity=opacity, 145 | color=color, 146 | text=name, 147 | name=name, 148 | ) 149 | 150 | return sphere 151 | 152 | def _add_aninmation_frames(self, fig, anim_step_size): 153 | """Adds animation frames with given (index) step size to the figure. 154 | 155 | Arguments: 156 | fig -- plotly figure 157 | anim_step_size -- animation step size [s] 158 | """ 159 | if anim_step_size <= 0: 160 | return 161 | 162 | # buttons 163 | updatemenus_dict = { 164 | # "bgcolor": "#333333", 165 | "font": {"color": "#000000"}, 166 | "type": "buttons", 167 | "buttons": [ 168 | { 169 | "label": "Play", 170 | "method": "animate", 171 | "args": [ 172 | None, 173 | { 174 | "frame": {"duration": 500, "redraw": True}, 175 | "fromcurrent": True, 176 | "transition": { 177 | "duration": 300, 178 | "easing": "quadratic-in-out", 179 | }, 180 | }, 181 | ], 182 | }, 183 | { 184 | "label": "Pause", 185 | "method": "animate", 186 | "args": [ 187 | [None], 188 | { 189 | "frame": {"duration": 0, "redraw": False}, 190 | "mode": "immediate", 191 | "transition": {"duration": 0}, 192 | }, 193 | ], 194 | }, 195 | ], 196 | "showactive": False, 197 | } 198 | 199 | # animation frame traits 200 | frame_step_size = math.ceil(anim_step_size / self.df["tau"].max() * self.size()) 201 | 202 | if frame_step_size > self.max_num_anim_frames: 203 | frame_step_size = self.max_num_anim_frames 204 | logger.warning( 205 | "'animation_step_size' is too large. \ 206 | Maximium number of animation frames will be limited to {}.".format( 207 | self.max_num_anim_frames 208 | ) 209 | ) 210 | 211 | # sliders 212 | sliders_dict = { 213 | "active": 0, 214 | "yanchor": "top", 215 | "xanchor": "left", 216 | "currentvalue": { 217 | "font": {"size": 12}, 218 | "prefix": "tau, t:
", 219 | "visible": True, 220 | "xanchor": "right", 221 | }, 222 | "transition": {"duration": 300, "easing": "cubic-in-out"}, 223 | "pad": {"b": 10, "t": 50}, 224 | "len": 0.9, 225 | "x": 0.1, 226 | "y": 0, 227 | "steps": [], 228 | } 229 | 230 | # append frames and slider steps 231 | anim_frames = [] 232 | for k in range(1, self.size(), frame_step_size): 233 | current_tau = self.df["tau"][k] 234 | current_t = self.df["t"][k] 235 | 236 | frame = go.Frame( 237 | data=[ 238 | go.Scatter3d( 239 | x=[self.df["x"][k]], 240 | y=[self.df["y"][k]], 241 | z=[self.df["z"][k]], 242 | mode="markers", 243 | marker=dict(color="red", size=3), 244 | name="particle", 245 | text="particle", 246 | ) 247 | ], 248 | name=str(current_tau), 249 | ) 250 | anim_frames.append(frame) 251 | 252 | slider_step = { 253 | "args": [ 254 | [str(current_tau)], 255 | { 256 | "frame": {"duration": 300, "redraw": True}, 257 | "mode": "immediate", 258 | "transition": {"duration": 300}, 259 | }, 260 | ], 261 | "label": "{}s
{}s".format(current_tau, current_t), 262 | "method": "animate", 263 | } 264 | sliders_dict["steps"].append(slider_step) 265 | 266 | # update figure data 267 | fig.frames = anim_frames 268 | fig.update_layout(updatemenus=[updatemenus_dict], sliders=[sliders_dict]) 269 | -------------------------------------------------------------------------------- /src/gros/utils/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import colorlog 3 | 4 | 5 | def init_logger(module_name): 6 | log_format = ( 7 | '%(asctime)s - ' 8 | '%(name)s - ' 9 | '%(levelname)s - ' 10 | '%(message)s' 11 | ) 12 | colorlog_format = ( 13 | '%(log_color)s ' 14 | f'{log_format}' 15 | ) 16 | 17 | colorlog.basicConfig(format=colorlog_format) 18 | logging.Formatter(log_format) 19 | 20 | logger = logging.getLogger(module_name) 21 | logger.setLevel(logging.DEBUG) 22 | return logger 23 | -------------------------------------------------------------------------------- /src/gros/utils/transforms.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from enum import Enum 3 | 4 | 5 | class CoordinateSystem(Enum): 6 | cartesian = 1 7 | spherical = 2 8 | 9 | 10 | class CoordinateConversion(Enum): 11 | spherical_to_cartesian = 1 12 | cartesian_to_spherical = 2 13 | 14 | 15 | def spherical_to_cartesian(r, theta, phi): 16 | """Transforms coordinates from spherical to cartesian. 17 | 18 | Returns: 19 | cartesian coordinates 20 | """ 21 | x = r * np.sin(theta) * np.cos(phi) 22 | y = r * np.sin(theta) * np.sin(phi) 23 | z = r * np.cos(theta) 24 | 25 | return x, y, z 26 | 27 | 28 | def spherical_to_cartesian_with_vel(r, theta, phi, v_r, v_theta, v_phi): 29 | """Transforms coordinates and according velocities from spherical to cartesian. 30 | 31 | Returns: 32 | cartesian coordinates and velocities 33 | """ 34 | x, y, z = spherical_to_cartesian(r, theta, phi) 35 | 36 | v_x = ( 37 | np.sin(theta) * np.cos(phi) * v_r 38 | - r * np.sin(theta) * np.sin(phi) * v_phi 39 | + r * np.cos(theta) * np.cos(phi) * v_theta 40 | ) 41 | v_y = ( 42 | np.sin(theta) * np.sin(phi) * v_r 43 | + r * np.cos(theta) * np.sin(phi) * v_theta 44 | + r * np.sin(theta) * np.cos(phi) * v_phi 45 | ) 46 | v_z = np.cos(theta) * v_r - r * np.sin(theta) * v_theta 47 | 48 | return x, y, z, v_x, v_y, v_z 49 | --------------------------------------------------------------------------------