einsteinengine 0.5.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.
- einsteinengine-0.5.0/PKG-INFO +63 -0
- einsteinengine-0.5.0/README.md +44 -0
- einsteinengine-0.5.0/einsteinengine/__init__.py +0 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/__init__.py +0 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/christoffel.py +39 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/connection.py +111 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/core.py +136 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/einstein_tensor.py +57 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/energy_momentum.py +45 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/geodesics.py +96 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/metric.py +60 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/ricci_scalar.py +34 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/ricci_tensor.py +21 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/riemann.py +116 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/spin_connection.py +43 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/tensor.py +439 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/tetrad.py +51 -0
- einsteinengine-0.5.0/einsteinengine/symbolic/weyl.py +83 -0
- einsteinengine-0.5.0/einsteinengine.egg-info/PKG-INFO +63 -0
- einsteinengine-0.5.0/einsteinengine.egg-info/SOURCES.txt +25 -0
- einsteinengine-0.5.0/einsteinengine.egg-info/dependency_links.txt +1 -0
- einsteinengine-0.5.0/einsteinengine.egg-info/requires.txt +4 -0
- einsteinengine-0.5.0/einsteinengine.egg-info/top_level.txt +1 -0
- einsteinengine-0.5.0/pyproject.toml +30 -0
- einsteinengine-0.5.0/setup.cfg +4 -0
- einsteinengine-0.5.0/tests/test_schwarchild.py +56 -0
- einsteinengine-0.5.0/tests/test_weyl.py +38 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: einsteinengine
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: A high-performance, object-oriented symbolic tensor engine for General Relativity.
|
|
5
|
+
Author-email: Rafael Salazar <rsalazard2005@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/RSalazarD/EinsteinEngine
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/RSalazarD/EinsteinEngine/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: sympy>=1.12
|
|
16
|
+
Requires-Dist: symengine>=0.11.0
|
|
17
|
+
Requires-Dist: numpy>=1.24.0
|
|
18
|
+
Requires-Dist: scipy>=1.10.0
|
|
19
|
+
|
|
20
|
+
# EinsteinEngine
|
|
21
|
+
|
|
22
|
+
**A high-performance, object-oriented symbolic tensor engine for General Relativity, powered by Python and C++.**
|
|
23
|
+
|
|
24
|
+
EinsteinEngine is designed to solve the performance bottlenecks of traditional pure-Python symbolic calculators. By wrapping `SymEngine` (C++) backend inside a Python API, it computes Christoffel Symbols, Riemann Tensors, and other complex relativistic structures faster than standard pure Python libraries.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Key Features
|
|
29
|
+
|
|
30
|
+
* **⚡ C++ Backend:** Mathematical heavy-lifting (partial derivatives, massive tensor contractions) is routed directly to `SymEngine`, bypassing Python's native performance limits.
|
|
31
|
+
* **🧠 Smart Memoization:** Built-in memory caching prevents redundant calculations of highly complex objects like inverse metric tensors.
|
|
32
|
+
* **📦 Clean Object-Oriented API:** Complex tensor pipelines are reduced to a few lines of readable code using class inheritance.
|
|
33
|
+
* **🛡️ Exact Mathematics:** Built to handle rational numbers securely, preventing floating-point contamination and ensuring textbook-perfect algebraic simplifications.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
EinsteinEngine calculates the entire Riemann Curvature Tensor of a Black Hole in just two lines of code:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import sympy as sp
|
|
43
|
+
from einsteinpy.symbolic.metric import MetricTensor
|
|
44
|
+
from einsteinpy.symbolic.riemann import RiemannCurvatureTensor
|
|
45
|
+
|
|
46
|
+
# 1. Define your symbols and metric array
|
|
47
|
+
t, r, theta, phi = sp.symbols('t r theta phi', real=True)
|
|
48
|
+
M = sp.symbols('M', real=True)
|
|
49
|
+
|
|
50
|
+
g_schwarzschild = [
|
|
51
|
+
[-(1 - 2*M/r), 0, 0, 0],
|
|
52
|
+
[0, 1/(1 - 2*M/r), 0, 0],
|
|
53
|
+
[0, 0, r**2, 0],
|
|
54
|
+
[0, 0, 0, r**2 * sp.sin(theta)**2]
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
# 2. Run the EinsteinEngine Pipeline
|
|
58
|
+
metric = MetricTensor(g_schwarzschild, [t, r, theta, phi], name="Schwarzschild")
|
|
59
|
+
riemann = RiemannCurvatureTensor.from_metric(metric)
|
|
60
|
+
|
|
61
|
+
# 3. Extract exact, simplified textbook results
|
|
62
|
+
print(riemann.get_component(1, 0, 1, 0))
|
|
63
|
+
# Output: 2*M*(2*M - r)/r**4
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# EinsteinEngine
|
|
2
|
+
|
|
3
|
+
**A high-performance, object-oriented symbolic tensor engine for General Relativity, powered by Python and C++.**
|
|
4
|
+
|
|
5
|
+
EinsteinEngine is designed to solve the performance bottlenecks of traditional pure-Python symbolic calculators. By wrapping `SymEngine` (C++) backend inside a Python API, it computes Christoffel Symbols, Riemann Tensors, and other complex relativistic structures faster than standard pure Python libraries.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Key Features
|
|
10
|
+
|
|
11
|
+
* **⚡ C++ Backend:** Mathematical heavy-lifting (partial derivatives, massive tensor contractions) is routed directly to `SymEngine`, bypassing Python's native performance limits.
|
|
12
|
+
* **🧠 Smart Memoization:** Built-in memory caching prevents redundant calculations of highly complex objects like inverse metric tensors.
|
|
13
|
+
* **📦 Clean Object-Oriented API:** Complex tensor pipelines are reduced to a few lines of readable code using class inheritance.
|
|
14
|
+
* **🛡️ Exact Mathematics:** Built to handle rational numbers securely, preventing floating-point contamination and ensuring textbook-perfect algebraic simplifications.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
EinsteinEngine calculates the entire Riemann Curvature Tensor of a Black Hole in just two lines of code:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import sympy as sp
|
|
24
|
+
from einsteinpy.symbolic.metric import MetricTensor
|
|
25
|
+
from einsteinpy.symbolic.riemann import RiemannCurvatureTensor
|
|
26
|
+
|
|
27
|
+
# 1. Define your symbols and metric array
|
|
28
|
+
t, r, theta, phi = sp.symbols('t r theta phi', real=True)
|
|
29
|
+
M = sp.symbols('M', real=True)
|
|
30
|
+
|
|
31
|
+
g_schwarzschild = [
|
|
32
|
+
[-(1 - 2*M/r), 0, 0, 0],
|
|
33
|
+
[0, 1/(1 - 2*M/r), 0, 0],
|
|
34
|
+
[0, 0, r**2, 0],
|
|
35
|
+
[0, 0, 0, r**2 * sp.sin(theta)**2]
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# 2. Run the EinsteinEngine Pipeline
|
|
39
|
+
metric = MetricTensor(g_schwarzschild, [t, r, theta, phi], name="Schwarzschild")
|
|
40
|
+
riemann = RiemannCurvatureTensor.from_metric(metric)
|
|
41
|
+
|
|
42
|
+
# 3. Extract exact, simplified textbook results
|
|
43
|
+
print(riemann.get_component(1, 0, 1, 0))
|
|
44
|
+
# Output: 2*M*(2*M - r)/r**4
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import symengine as se
|
|
2
|
+
from einsteinengine.symbolic.connection import BaseConnection
|
|
3
|
+
class ChristoffelSymbols(BaseConnection):
|
|
4
|
+
"""
|
|
5
|
+
Computes the Christoffel Symbols of the second kind (Gamma^lambda_{mu nu})
|
|
6
|
+
optimized via SymEngine backend processing.
|
|
7
|
+
"""
|
|
8
|
+
@classmethod
|
|
9
|
+
def from_metric(cls, metric, verbose=False):
|
|
10
|
+
"""
|
|
11
|
+
Computes the Christoffel Symbols of the second kind (Gamma^lambda_{mu nu})
|
|
12
|
+
optimized via SymEngine backend processing.
|
|
13
|
+
"""
|
|
14
|
+
if verbose:
|
|
15
|
+
print(f"Computing Christoffel Symbols for '{metric.name}' in C++...")
|
|
16
|
+
|
|
17
|
+
g = metric._data
|
|
18
|
+
g_inv = metric.inv()._data
|
|
19
|
+
syms = metric.syms
|
|
20
|
+
dims = metric.dims
|
|
21
|
+
|
|
22
|
+
# Initialize a 3D grid structure for Gamma (4x4x4)
|
|
23
|
+
Gamma = [[[se.sympify(0) for _ in range(dims)] for _ in range(dims)] for _ in range(dims)]
|
|
24
|
+
|
|
25
|
+
# Heavy-lifting partial derivative loops triggered directly in C++
|
|
26
|
+
for lambda_ in range(dims):
|
|
27
|
+
for mu in range(dims):
|
|
28
|
+
for nu in range(dims):
|
|
29
|
+
tmp_sum = se.sympify(0)
|
|
30
|
+
for sigma in range(dims):
|
|
31
|
+
term1 = se.diff(g[nu][sigma], syms[mu])
|
|
32
|
+
term2 = se.diff(g[mu][sigma], syms[nu])
|
|
33
|
+
term3 = se.diff(g[mu][nu], syms[sigma])
|
|
34
|
+
tmp_sum += g_inv[lambda_][sigma] * (term1 + term2 - term3)
|
|
35
|
+
|
|
36
|
+
Gamma[lambda_][mu][nu] = se.Rational(1, 2) * tmp_sum
|
|
37
|
+
|
|
38
|
+
# Devuelve instanciando la nueva clase madre BaseConnection
|
|
39
|
+
return cls(Gamma, syms, config="ull", name=f"Christoffel_{metric.name}", verbose=verbose)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from einsteinengine.symbolic.core import BaseRelativityObject
|
|
2
|
+
|
|
3
|
+
class BaseConnection(BaseRelativityObject):
|
|
4
|
+
"""
|
|
5
|
+
Base class for mathematical Connections (e.g., Levi-Civita, Spin, Affine).
|
|
6
|
+
Inherits coordinate and array management from BaseRelativityObject.
|
|
7
|
+
|
|
8
|
+
IMPORTANT: Connections are NOT tensors. They do not transform homogeneously
|
|
9
|
+
under coordinate changes, and operations like raising/lowering all indices
|
|
10
|
+
using the metric do not apply in the standard tensorial way.
|
|
11
|
+
"""
|
|
12
|
+
def __init__(self, arr, syms, config="ull", name="GenericConnection", verbose=False):
|
|
13
|
+
# 1. Delegate the heavy lifting to the parent object
|
|
14
|
+
super().__init__(arr, syms, name=name, verbose=verbose)
|
|
15
|
+
|
|
16
|
+
# 2. Connection index configuration (usually one upper, two lower)
|
|
17
|
+
self.config = config
|
|
18
|
+
|
|
19
|
+
# 3. Validation: Connections in standard GR are 3-index objects
|
|
20
|
+
if self._data and isinstance(arr, list):
|
|
21
|
+
rank = self._calculate_rank(arr)
|
|
22
|
+
if rank != 3:
|
|
23
|
+
raise ValueError(f"A connection must have exactly 3 indices. Got {rank}.")
|
|
24
|
+
|
|
25
|
+
if self.verbose:
|
|
26
|
+
print(f"[{self.name}] Connection initialized. Note: This object is NOT a tensor.")
|
|
27
|
+
|
|
28
|
+
def _calculate_rank(self, arr):
|
|
29
|
+
"""
|
|
30
|
+
Calculates the mathematical rank (number of indices) of the array
|
|
31
|
+
by measuring the depth of the nested lists.
|
|
32
|
+
"""
|
|
33
|
+
if isinstance(arr, list):
|
|
34
|
+
return 1 + self._calculate_rank(arr[0])
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
# --- Core geometric operations for Connections ---
|
|
38
|
+
|
|
39
|
+
def covariant_derivative(self, tensor_obj, verbose=False):
|
|
40
|
+
r"""
|
|
41
|
+
Computes the covariant derivative (\nabla_\mu) of a given tensor.
|
|
42
|
+
The derivative adds a new covariant index (lower 'l') at the beginning of the tensor.
|
|
43
|
+
Nabla_\mu T^{a}_{b} = \partial_\mu T^{a}_{b} + \Gamma^a_{\mu \sigma} T^\sigma_b - \Gamma^\sigma_{\mu b} T^a_\sigma
|
|
44
|
+
"""
|
|
45
|
+
import symengine as se
|
|
46
|
+
|
|
47
|
+
if verbose:
|
|
48
|
+
print(f"[{self.name}] Computing covariant derivative for tensor '{tensor_obj.name}'...")
|
|
49
|
+
|
|
50
|
+
dims = self.dims
|
|
51
|
+
syms = self.syms
|
|
52
|
+
rank = len(tensor_obj.config)
|
|
53
|
+
T_data = tensor_obj.get_raw_data()
|
|
54
|
+
Gamma = self._data # self is the connection, config 'ull': Gamma[up][down][down]
|
|
55
|
+
|
|
56
|
+
# Helper to extract the tensor component safely regardless of rank
|
|
57
|
+
def get_T(indices):
|
|
58
|
+
val = T_data
|
|
59
|
+
for idx in indices:
|
|
60
|
+
val = val[idx]
|
|
61
|
+
return val
|
|
62
|
+
|
|
63
|
+
# The covariant derivative ADDS a new covariant index at the front.
|
|
64
|
+
# e.g., T^\alpha_\beta (config "ul") -> \nabla_\mu T^\alpha_\beta (config "lul")
|
|
65
|
+
new_config = 'l' + tensor_obj.config
|
|
66
|
+
|
|
67
|
+
def build_cov_dir(current_indices):
|
|
68
|
+
# Base case: we have selected all coordinates for the new tensor
|
|
69
|
+
if len(current_indices) == rank + 1:
|
|
70
|
+
mu = current_indices[0] # The derivative index (\nabla_\mu)
|
|
71
|
+
T_indices = current_indices[1:] # The original tensor indices
|
|
72
|
+
|
|
73
|
+
# 1. Base term: Partial derivative \partial_\mu T^{...}
|
|
74
|
+
term = se.diff(get_T(T_indices), syms[mu])
|
|
75
|
+
|
|
76
|
+
# 2. Correction terms: loop over each index of the original tensor
|
|
77
|
+
for p in range(rank):
|
|
78
|
+
idx_type = tensor_obj.config[p]
|
|
79
|
+
current_idx_val = T_indices[p]
|
|
80
|
+
|
|
81
|
+
if idx_type == 'u':
|
|
82
|
+
# Add connection term for contravariant index
|
|
83
|
+
# + \sum_\sigma \Gamma^{current_idx_val}_{\mu \sigma} * T^{... \sigma ...}
|
|
84
|
+
for sigma in range(dims):
|
|
85
|
+
mod_indices = list(T_indices)
|
|
86
|
+
mod_indices[p] = sigma
|
|
87
|
+
term += Gamma[current_idx_val][mu][sigma] * get_T(mod_indices)
|
|
88
|
+
|
|
89
|
+
elif idx_type == 'l':
|
|
90
|
+
# Subtract connection term for covariant index
|
|
91
|
+
# - \sum_\sigma \Gamma^{\sigma}_{\mu current_idx_val} * T^{... \sigma ...}
|
|
92
|
+
for sigma in range(dims):
|
|
93
|
+
mod_indices = list(T_indices)
|
|
94
|
+
mod_indices[p] = sigma
|
|
95
|
+
term -= Gamma[sigma][mu][current_idx_val] * get_T(mod_indices)
|
|
96
|
+
|
|
97
|
+
return term
|
|
98
|
+
|
|
99
|
+
# Recursive case: dive deeper into the dimensions
|
|
100
|
+
else:
|
|
101
|
+
return [build_cov_dir(current_indices + [d]) for d in range(dims)]
|
|
102
|
+
|
|
103
|
+
cov_data = build_cov_dir([])
|
|
104
|
+
|
|
105
|
+
# Local import to avoid circular dependencies
|
|
106
|
+
from einsteinengine.symbolic.tensor import BaseRelativityTensor
|
|
107
|
+
|
|
108
|
+
final_name = f"CovDeriv_{tensor_obj.name}"
|
|
109
|
+
return BaseRelativityTensor(cov_data, syms, config=new_config, name=final_name, verbose=verbose)
|
|
110
|
+
|
|
111
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import symengine as se
|
|
2
|
+
import sympy as sp
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
class BaseRelativityObject:
|
|
6
|
+
"""
|
|
7
|
+
Absolute base class for any geometric object in EinsteinEngine.
|
|
8
|
+
Handles coordinate symbols, dimensions, and C++ SymEngine integration.
|
|
9
|
+
Does NOT assume tensor transformation laws.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, arr, syms, name="GeometricObject", verbose=False):
|
|
12
|
+
self.name = name
|
|
13
|
+
self.verbose = verbose
|
|
14
|
+
|
|
15
|
+
# Coordinate Management (Our implicit Chart)
|
|
16
|
+
self.syms = [se.sympify(s) for s in syms]
|
|
17
|
+
self.dims = len(self.syms)
|
|
18
|
+
|
|
19
|
+
# Convert mathematical arrays to high-performance C++ objects
|
|
20
|
+
self._data = self._symenginify_array(arr)
|
|
21
|
+
|
|
22
|
+
if self.verbose:
|
|
23
|
+
print(f"[{self.name}] Instantiated in {self.dims}D spacetime.")
|
|
24
|
+
|
|
25
|
+
def get_raw_data(self):
|
|
26
|
+
"""Returns the internal SymEngine matrix/array."""
|
|
27
|
+
return self._data
|
|
28
|
+
|
|
29
|
+
def _symenginify_array(self, arr):
|
|
30
|
+
"""
|
|
31
|
+
Recursively converts Python lists/SymPy objects into SymEngine objects.
|
|
32
|
+
Works for 1D vectors, 2D matrices, or 4D Riemann structures.
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(arr, list):
|
|
35
|
+
return [self._symenginify_array(item) for item in arr]
|
|
36
|
+
else:
|
|
37
|
+
# Handles integers, floats, and SymPy expressions
|
|
38
|
+
return se.sympify(arr)
|
|
39
|
+
|
|
40
|
+
def _calculate_rank(self, arr):
|
|
41
|
+
"""Calculates the mathematical rank dynamically."""
|
|
42
|
+
if isinstance(arr, list):
|
|
43
|
+
return 1 + self._calculate_rank(arr[0])
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
def _algebraic_contraction(self, idx1, idx2):
|
|
47
|
+
"""
|
|
48
|
+
Hidden mathematical engine: Contracts two indices of an N-dimensional nested list.
|
|
49
|
+
Works recursively for any rank dynamically.
|
|
50
|
+
"""
|
|
51
|
+
# Aseguramos que idx1 es el menor para no liarnos al reordenar
|
|
52
|
+
if idx1 > idx2:
|
|
53
|
+
idx1, idx2 = idx2, idx1
|
|
54
|
+
|
|
55
|
+
rank = self._calculate_rank(self._data)
|
|
56
|
+
new_rank = rank - 2
|
|
57
|
+
|
|
58
|
+
# Función auxiliar para extraer el valor en una coordenada N-dimensional
|
|
59
|
+
def get_element(indices):
|
|
60
|
+
val = self._data
|
|
61
|
+
for i in indices:
|
|
62
|
+
val = val[i]
|
|
63
|
+
return val
|
|
64
|
+
|
|
65
|
+
# Función recursiva que construye la nueva matriz con N-2 dimensiones
|
|
66
|
+
def build_contracted(current_free_coords):
|
|
67
|
+
# Si ya tenemos suficientes coordenadas libres, sumamos el índice mudo (dummy)
|
|
68
|
+
if len(current_free_coords) == new_rank:
|
|
69
|
+
tmp_sum = se.sympify(0)
|
|
70
|
+
for dummy in range(self.dims):
|
|
71
|
+
# Reconstruimos la coordenada original completa
|
|
72
|
+
orig_coords = [0] * rank
|
|
73
|
+
orig_coords[idx1] = dummy
|
|
74
|
+
orig_coords[idx2] = dummy
|
|
75
|
+
|
|
76
|
+
# Rellenamos el resto de huecos con las coordenadas libres
|
|
77
|
+
free_ptr = 0
|
|
78
|
+
for i in range(rank):
|
|
79
|
+
if i != idx1 and i != idx2:
|
|
80
|
+
orig_coords[i] = current_free_coords[free_ptr]
|
|
81
|
+
free_ptr += 1
|
|
82
|
+
|
|
83
|
+
tmp_sum += get_element(orig_coords)
|
|
84
|
+
return tmp_sum
|
|
85
|
+
else:
|
|
86
|
+
# Si faltan coordenadas, seguimos escarbando (recursión)
|
|
87
|
+
return [build_contracted(current_free_coords + [d]) for d in range(self.dims)]
|
|
88
|
+
|
|
89
|
+
# Lanzamos la recursión desde cero
|
|
90
|
+
return build_contracted([])
|
|
91
|
+
|
|
92
|
+
def simplify(self, in_place=True):
|
|
93
|
+
"""
|
|
94
|
+
Applies algebraic and trigonometric simplification to the object's data.
|
|
95
|
+
It uses SymPy's pattern recognition engine to collapse equations
|
|
96
|
+
and then converts the data back to SymEngine for fast future computations.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
in_place (bool): If True, modifies the object's internal data.
|
|
100
|
+
If False, returns a new object with the simplified data,
|
|
101
|
+
leaving the original untouched.
|
|
102
|
+
"""
|
|
103
|
+
if self.verbose:
|
|
104
|
+
print(f"[{self.name}] Applying deep simplification... (This may take a while for large tensors)")
|
|
105
|
+
|
|
106
|
+
# Recursive helper function to dig into nested lists of any tensor rank
|
|
107
|
+
def _recursive_simplify(arr):
|
|
108
|
+
if isinstance(arr, list):
|
|
109
|
+
return [_recursive_simplify(item) for item in arr]
|
|
110
|
+
else:
|
|
111
|
+
# 1. Convert SymEngine object to pure SymPy (sp.sympify)
|
|
112
|
+
# 2. Apply deep algebraic/trigonometric simplification (sp.simplify)
|
|
113
|
+
# 3. Shield it back into C++ SymEngine for performance (se.sympify)
|
|
114
|
+
return se.sympify(sp.simplify(sp.sympify(arr)))
|
|
115
|
+
|
|
116
|
+
simplified_data = _recursive_simplify(self._data)
|
|
117
|
+
|
|
118
|
+
if in_place:
|
|
119
|
+
self._data = simplified_data
|
|
120
|
+
if self.verbose:
|
|
121
|
+
print(f"[{self.name}] Simplification complete.")
|
|
122
|
+
|
|
123
|
+
# Return self to allow method chaining (e.g., tensor.simplify().get_raw_data())
|
|
124
|
+
return self
|
|
125
|
+
else:
|
|
126
|
+
# Creamos un clon exacto del objeto actual (sea Tensor, Conexión, etc.)
|
|
127
|
+
new_obj = copy.copy(self)
|
|
128
|
+
# Le inyectamos los datos limpios al clon
|
|
129
|
+
new_obj._data = simplified_data
|
|
130
|
+
if self.verbose:
|
|
131
|
+
print(f"[{self.name}] Created simplified copy.")
|
|
132
|
+
return new_obj
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import symengine as se
|
|
2
|
+
from einsteinengine.symbolic.tensor import BaseRelativityTensor
|
|
3
|
+
|
|
4
|
+
class EinsteinTensor(BaseRelativityTensor):
|
|
5
|
+
|
|
6
|
+
@classmethod
|
|
7
|
+
def from_components(cls, metric, ricci_tensor, ricci_scalar, verbose=False):
|
|
8
|
+
"""
|
|
9
|
+
Computes the Einstein Tensor (G_mu_nu) combining the geometric components:
|
|
10
|
+
G_mu_nu = R_mu_nu - (1/2) * R * g_mu_nu
|
|
11
|
+
"""
|
|
12
|
+
if verbose:
|
|
13
|
+
print(f"Building Einstein Tensor for '{metric.name}'...")
|
|
14
|
+
|
|
15
|
+
# 1. Extract pure C++ matrices from our objects
|
|
16
|
+
g_data = metric.get_raw_data()
|
|
17
|
+
R_mu_nu = ricci_tensor.get_raw_data()
|
|
18
|
+
R_scalar = ricci_scalar.get_raw_data()
|
|
19
|
+
|
|
20
|
+
syms = metric.syms
|
|
21
|
+
dims = metric.dims
|
|
22
|
+
|
|
23
|
+
# 2. Initialize a 2D matrix for G_mu_nu
|
|
24
|
+
G_tensor = [[se.sympify(0) for _ in range(dims)] for _ in range(dims)]
|
|
25
|
+
|
|
26
|
+
# Exact symbolic fraction to avoid floating point precision issues
|
|
27
|
+
half = se.Rational(1, 2)
|
|
28
|
+
|
|
29
|
+
# 3. Apply the fundamental formula cell by cell
|
|
30
|
+
for mu in range(dims):
|
|
31
|
+
for nu in range(dims):
|
|
32
|
+
G_tensor[mu][nu] = R_mu_nu[mu][nu] - half * R_scalar * g_data[mu][nu]
|
|
33
|
+
|
|
34
|
+
# 4. Instantiate as a rank-2 covariant tensor (config="ll")
|
|
35
|
+
clean_name = metric.name.replace("Metric_", "")
|
|
36
|
+
return cls(G_tensor, syms, config="ll", name=f"Einstein_{clean_name}", verbose=verbose)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_metric(cls, metric, verbose=False):
|
|
40
|
+
"""
|
|
41
|
+
Convenience pipeline method: Computes the Einstein Tensor directly from a Metric.
|
|
42
|
+
"""
|
|
43
|
+
if verbose:
|
|
44
|
+
print(f"--- Auto-generating Curvature Pipeline for '{metric.name}' ---")
|
|
45
|
+
|
|
46
|
+
# Local imports
|
|
47
|
+
from einsteinengine.symbolic.riemann import RiemannCurvatureTensor
|
|
48
|
+
from einsteinengine.symbolic.ricci_tensor import RicciTensor
|
|
49
|
+
from einsteinengine.symbolic.ricci_scalar import RicciScalar
|
|
50
|
+
|
|
51
|
+
riemann = RiemannCurvatureTensor.from_metric(metric, verbose=verbose)
|
|
52
|
+
ricci_tensor = RicciTensor.from_riemann(riemann, verbose=verbose)
|
|
53
|
+
ricci_scalar = RicciScalar.from_ricci_tensor_and_metric(ricci_tensor, metric, verbose=verbose)
|
|
54
|
+
|
|
55
|
+
# Pass the calculated components to our main constructor
|
|
56
|
+
return cls.from_components(metric, ricci_tensor, ricci_scalar, verbose=verbose)
|
|
57
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import symengine as se
|
|
2
|
+
from einsteinengine.symbolic.tensor import BaseRelativityTensor
|
|
3
|
+
|
|
4
|
+
class EnergyMomentumTensor(BaseRelativityTensor):
|
|
5
|
+
|
|
6
|
+
@classmethod
|
|
7
|
+
def from_perfect_fluid(cls, metric, density, pressure, four_velocity_cov, verbose=False):
|
|
8
|
+
r"""
|
|
9
|
+
Constructs the Energy-Momentum Tensor for a Perfect Fluid.
|
|
10
|
+
Formula: T_\mu\nu = (\rho + p) u_\mu u_\nu + p g_\mu\nu
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
metric (MetricTensor): The metric tensor of the spacetime.
|
|
14
|
+
density (sp.Expr): The energy density (\rho).
|
|
15
|
+
pressure (sp.Expr): The isotropic pressure (p).
|
|
16
|
+
four_velocity_cov (list): The covariant four-velocity vector u_\mu (e.g., [-1, 0, 0, 0]).
|
|
17
|
+
"""
|
|
18
|
+
if verbose:
|
|
19
|
+
print(f"Building Perfect Fluid Energy-Momentum Tensor for '{metric.name}'...")
|
|
20
|
+
|
|
21
|
+
dims = metric.dims
|
|
22
|
+
syms = metric.syms
|
|
23
|
+
g_data = metric.get_raw_data()
|
|
24
|
+
|
|
25
|
+
# 1. Symenginify physical parameters
|
|
26
|
+
rho = se.sympify(density)
|
|
27
|
+
p = se.sympify(pressure)
|
|
28
|
+
u_mu = [se.sympify(val) for val in four_velocity_cov]
|
|
29
|
+
|
|
30
|
+
# 2. Initialize the 2D tensor T_\mu\nu
|
|
31
|
+
T_data = [[se.sympify(0) for _ in range(dims)] for _ in range(dims)]
|
|
32
|
+
|
|
33
|
+
# Enthalpy factor: (\rho + p)
|
|
34
|
+
enthalpy = rho + p
|
|
35
|
+
|
|
36
|
+
# 3. Apply the perfect fluid formula cell by cell
|
|
37
|
+
for mu in range(dims):
|
|
38
|
+
for nu in range(dims):
|
|
39
|
+
term1 = enthalpy * u_mu[mu] * u_mu[nu]
|
|
40
|
+
term2 = p * g_data[mu][nu]
|
|
41
|
+
T_data[mu][nu] = term1 + term2
|
|
42
|
+
|
|
43
|
+
fluid_name = f"Fluid_{metric.name}"
|
|
44
|
+
# Returned as a doubly covariant tensor (config 'll')
|
|
45
|
+
return cls(T_data, syms, config="ll", name=fluid_name, verbose=verbose)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import sympy as sp
|
|
2
|
+
import symengine as se
|
|
3
|
+
|
|
4
|
+
class Geodesics:
|
|
5
|
+
r"""
|
|
6
|
+
Computes the geodesic equations of motion for a given space-time.
|
|
7
|
+
d^2 x^\mu / d\tau^2 = - \Gamma^\mu_{\alpha\beta} (dx^\alpha / d\tau) (dx^\beta / d\tau)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, christoffel, param_name="tau", verbose=False):
|
|
11
|
+
self.christoffel = christoffel
|
|
12
|
+
self.syms = christoffel.syms
|
|
13
|
+
self.dims = christoffel.dims
|
|
14
|
+
self.verbose = verbose
|
|
15
|
+
|
|
16
|
+
# --- THEORETICAL VARIABLES (SymPy) ---
|
|
17
|
+
# Used ONLY for LaTeX display. SymEngine doesn't support abstract uncomputed Functions well.
|
|
18
|
+
self.tau_sp = sp.Symbol(param_name, real=True)
|
|
19
|
+
self.x_funcs_sp = [sp.Function(s.name)(self.tau_sp) for s in self.syms]
|
|
20
|
+
|
|
21
|
+
# --- PERFORMANCE VARIABLES (SymEngine / C++) ---
|
|
22
|
+
# Algebraic velocities used for high-speed numerical simulations
|
|
23
|
+
self.v_syms_se = [se.Symbol(f"v_{s.name}") for s in self.syms]
|
|
24
|
+
|
|
25
|
+
if self.verbose:
|
|
26
|
+
print(f"Initialized Geodesics equations generator. Affine parameter: {self.tau_sp}")
|
|
27
|
+
|
|
28
|
+
def get_equations(self, substitute_velocities=True, simplify=False):
|
|
29
|
+
"""
|
|
30
|
+
Generates the right-hand side (accelerations) of the geodesic equations.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
substitute_velocities (bool):
|
|
34
|
+
- If True (Default): C++ HIGH PERFORMANCE MODE. Returns algebraic system for SciPy.
|
|
35
|
+
- If False: PYTHON THEORETICAL MODE. Returns pure differential equations.
|
|
36
|
+
simplify (bool): If True, attempts to simplify the final expressions (can be slow).
|
|
37
|
+
"""
|
|
38
|
+
if self.verbose:
|
|
39
|
+
print("Constructing the geodesic differential equations...")
|
|
40
|
+
|
|
41
|
+
Gamma = self.christoffel.get_raw_data()
|
|
42
|
+
accelerations = []
|
|
43
|
+
|
|
44
|
+
for mu in range(self.dims):
|
|
45
|
+
# Accumulator initialized purely in C++
|
|
46
|
+
accel_mu = se.sympify(0) if substitute_velocities else sp.sympify(0)
|
|
47
|
+
|
|
48
|
+
for alpha in range(self.dims):
|
|
49
|
+
for beta in range(self.dims):
|
|
50
|
+
# Fetch the Christoffel symbol component
|
|
51
|
+
gamma_se = se.sympify(Gamma[mu][alpha][beta])
|
|
52
|
+
|
|
53
|
+
if gamma_se == 0:
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
if substitute_velocities:
|
|
57
|
+
# 1. NUMERICAL MODE (SymEngine C++)
|
|
58
|
+
# Pure, blazing fast algebra
|
|
59
|
+
vel_alpha = self.v_syms_se[alpha]
|
|
60
|
+
vel_beta = self.v_syms_se[beta]
|
|
61
|
+
accel_mu += - gamma_se * vel_alpha * vel_beta
|
|
62
|
+
else:
|
|
63
|
+
# 2. THEORETICAL MODE (SymPy fallback)
|
|
64
|
+
# We must cross the bridge to Python to use sp.Derivative and sp.Function
|
|
65
|
+
gamma_sp = sp.sympify(gamma_se)
|
|
66
|
+
subs_dict = {self.syms[i]: self.x_funcs_sp[i] for i in range(self.dims)}
|
|
67
|
+
gamma_sp = gamma_sp.subs(subs_dict)
|
|
68
|
+
|
|
69
|
+
vel_alpha = sp.Derivative(self.x_funcs_sp[alpha], self.tau_sp)
|
|
70
|
+
vel_beta = sp.Derivative(self.x_funcs_sp[beta], self.tau_sp)
|
|
71
|
+
accel_mu += - gamma_sp * vel_alpha * vel_beta
|
|
72
|
+
|
|
73
|
+
# Performance delegation for simplification
|
|
74
|
+
if simplify:
|
|
75
|
+
if self.verbose: print(f"Simplifying equation for coordinate {self.syms[mu]}...")
|
|
76
|
+
# 1. C++ -> Python (sp.sympify)
|
|
77
|
+
cleaned_python_eq = sp.simplify(sp.sympify(accel_mu))
|
|
78
|
+
# 3. Python -> C++ (se.sympify)
|
|
79
|
+
accelerations.append(se.sympify(cleaned_python_eq))
|
|
80
|
+
else:
|
|
81
|
+
accelerations.append(accel_mu)
|
|
82
|
+
|
|
83
|
+
return accelerations
|
|
84
|
+
|
|
85
|
+
def display_equations(self):
|
|
86
|
+
"""
|
|
87
|
+
Helper method to render the theoretical differential equations in Jupyter Notebooks.
|
|
88
|
+
"""
|
|
89
|
+
from IPython.display import display, Math
|
|
90
|
+
|
|
91
|
+
# Force theoretical mode and skip simplification for instant rendering
|
|
92
|
+
eqs = self.get_equations(substitute_velocities=False, simplify=False)
|
|
93
|
+
for i, eq in enumerate(eqs):
|
|
94
|
+
coord = self.syms[i].name
|
|
95
|
+
latex_str = f"\\frac{{d^2 {coord}}}{{d\\tau^2}} = {sp.latex(eq)}"
|
|
96
|
+
display(Math(latex_str))
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import symengine as se
|
|
2
|
+
import sympy as sp
|
|
3
|
+
from einsteinengine.symbolic.tensor import BaseRelativityTensor
|
|
4
|
+
|
|
5
|
+
class MetricTensor(BaseRelativityTensor):
|
|
6
|
+
r"""
|
|
7
|
+
Class representing the Metric Tensor ($g_{\mu\nu}$)
|
|
8
|
+
Inherits from BaseRelativityTensor
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, arr, syms, config="ll", name="MetricTensor", verbose=False):
|
|
12
|
+
# Call the parent constructor (tensor.py) to handle the heavy lifting
|
|
13
|
+
# setting up indices, names, and converting everything to SymEngine
|
|
14
|
+
super().__init__(arr, syms, config=config, name=name,verbose=verbose)
|
|
15
|
+
|
|
16
|
+
if verbose:
|
|
17
|
+
print(f"[{self.name}] Métrica validada y lista para cálculos.")
|
|
18
|
+
|
|
19
|
+
if len(self._data) != len(self._data[0]):
|
|
20
|
+
raise ValueError("Metric must be a squared matrix")
|
|
21
|
+
|
|
22
|
+
self.dims = len(syms)
|
|
23
|
+
|
|
24
|
+
# Cache interna para la inversa, para no recalcularla múltiples veces
|
|
25
|
+
self._inverse_tensor = None
|
|
26
|
+
|
|
27
|
+
def inv(self):
|
|
28
|
+
"""
|
|
29
|
+
Computes the inverse metric tensor (g^{mu nu}).
|
|
30
|
+
Uses a caching mechanism to avoid redundant calculations.
|
|
31
|
+
"""
|
|
32
|
+
# If already computed, return it instantly
|
|
33
|
+
if self._inverse_tensor is not None:
|
|
34
|
+
return self._inverse_tensor
|
|
35
|
+
|
|
36
|
+
# If not, run the SymPy inversion pipeline
|
|
37
|
+
|
|
38
|
+
sp_matrix = sp.Matrix(self._data)
|
|
39
|
+
dims = sp_matrix.shape[0]
|
|
40
|
+
|
|
41
|
+
# Handle cases where SymEngine structures read the data as a flat 1D column
|
|
42
|
+
if sp_matrix.shape == (dims * dims, 1):
|
|
43
|
+
sp_matrix = sp_matrix.reshape(dims, dims)
|
|
44
|
+
|
|
45
|
+
# Perform the algebraic inversion securely
|
|
46
|
+
sp_inv = sp_matrix.inv()
|
|
47
|
+
|
|
48
|
+
# Safely parse the SymPy Matrix back into standard nested lists
|
|
49
|
+
inv_arr = sp_inv.tolist()
|
|
50
|
+
|
|
51
|
+
# Store the instance before returning it
|
|
52
|
+
self._inverse_tensor = self.__class__(
|
|
53
|
+
inv_arr,
|
|
54
|
+
self.syms,
|
|
55
|
+
config="uu",
|
|
56
|
+
name=f"{self.name}_inv",
|
|
57
|
+
verbose=False
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return self._inverse_tensor
|