einsteinengine 0.5.0__tar.gz → 1.0.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.
Files changed (33) hide show
  1. einsteinengine-1.0.0/LICENSE +21 -0
  2. einsteinengine-1.0.0/PKG-INFO +133 -0
  3. einsteinengine-1.0.0/README.md +112 -0
  4. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/christoffel.py +38 -38
  5. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/connection.py +111 -111
  6. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/core.py +137 -135
  7. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/einstein_tensor.py +57 -57
  8. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/energy_momentum.py +44 -44
  9. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/geodesics.py +95 -95
  10. einsteinengine-1.0.0/einsteinengine/symbolic/metric.py +102 -0
  11. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/ricci_scalar.py +33 -33
  12. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/ricci_tensor.py +20 -20
  13. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/riemann.py +116 -116
  14. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/spin_connection.py +42 -42
  15. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/tensor.py +504 -439
  16. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/tetrad.py +50 -50
  17. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/weyl.py +82 -82
  18. einsteinengine-1.0.0/einsteinengine.egg-info/PKG-INFO +133 -0
  19. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine.egg-info/SOURCES.txt +2 -0
  20. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/pyproject.toml +29 -29
  21. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/setup.cfg +4 -4
  22. einsteinengine-1.0.0/tests/test_extended_symbolic_features.py +159 -0
  23. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/tests/test_schwarchild.py +92 -56
  24. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/tests/test_weyl.py +37 -37
  25. einsteinengine-0.5.0/PKG-INFO +0 -63
  26. einsteinengine-0.5.0/README.md +0 -44
  27. einsteinengine-0.5.0/einsteinengine/symbolic/metric.py +0 -60
  28. einsteinengine-0.5.0/einsteinengine.egg-info/PKG-INFO +0 -63
  29. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/__init__.py +0 -0
  30. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine/symbolic/__init__.py +0 -0
  31. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine.egg-info/dependency_links.txt +0 -0
  32. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine.egg-info/requires.txt +0 -0
  33. {einsteinengine-0.5.0 → einsteinengine-1.0.0}/einsteinengine.egg-info/top_level.txt +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rafael Salazar
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,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: einsteinengine
3
+ Version: 1.0.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
+ License-File: LICENSE
16
+ Requires-Dist: sympy>=1.12
17
+ Requires-Dist: symengine>=0.11.0
18
+ Requires-Dist: numpy>=1.24.0
19
+ Requires-Dist: scipy>=1.10.0
20
+ Dynamic: license-file
21
+
22
+ # EinsteinEngine
23
+
24
+ EinsteinEngine is a symbolic tensor library for General Relativity built around a simple idea: make tensor calculations faster and more structured without giving up exact symbolic mathematics.
25
+
26
+ The project combines Python with SymEngine for the heavy symbolic work, and exposes an object-oriented API for common relativistic objects such as metrics, Christoffel symbols, curvature tensors, Ricci tensors, Einstein tensors, geodesics, tetrads and spin connections.
27
+
28
+ ## What the library can do today
29
+
30
+ EinsteinEngine currently provides a practical toolkit for symbolic GR workflows, including:
31
+
32
+ - Metric tensor construction and validation
33
+ - Metric inversion for diagonal and standard symbolic cases
34
+ - Christoffel symbol computation
35
+ - Riemann tensor construction
36
+ - Ricci tensor and Ricci scalar computation
37
+ - Einstein tensor construction
38
+ - Geodesic equation generation
39
+ - Tetrad and spin connection support
40
+ - Tensor component access, contraction, index raising/lowering and basic tensor arithmetic
41
+ - Rich LaTeX-style rendering for notebooks and a plain-text fallback for lighter output
42
+
43
+ The focus of the library is not only correctness, but also performance in symbolic pipelines that would otherwise become very expensive in pure SymPy.
44
+
45
+ ## Installation
46
+
47
+ From PyPI:
48
+
49
+ ```bash
50
+ pip install einsteinengine
51
+ ```
52
+
53
+ From source:
54
+
55
+ ```bash
56
+ git clone https://github.com/RSalazarD/EinsteinEngine.git
57
+ cd EinsteinEngine
58
+ pip install -e .
59
+ ```
60
+
61
+ ## Quick start
62
+
63
+ ```python
64
+ import sympy as sp
65
+ from einsteinengine.symbolic.metric import MetricTensor
66
+ from einsteinengine.symbolic.riemann import RiemannCurvatureTensor
67
+
68
+ # Coordinates and parameters
69
+ t, r, theta, phi = sp.symbols('t r theta phi', real=True)
70
+ M = sp.symbols('M', real=True)
71
+
72
+ # Schwarzschild metric
73
+ g_schwarzschild = [
74
+ [-(1 - 2*M/r), 0, 0, 0],
75
+ [0, 1/(1 - 2*M/r), 0, 0],
76
+ [0, 0, r**2, 0],
77
+ [0, 0, 0, r**2 * sp.sin(theta)**2],
78
+ ]
79
+
80
+ metric = MetricTensor(g_schwarzschild, [t, r, theta, phi], name="Schwarzschild")
81
+ riemann = RiemannCurvatureTensor.from_metric(metric, verbose=False)
82
+
83
+ print(riemann.get_component(1, 0, 1, 0))
84
+ ```
85
+
86
+ A typical output for the Schwarzschild case is the known exact component:
87
+
88
+ ```python
89
+ 2*M*(2*M - r)/r**4
90
+ ```
91
+
92
+ ## Current strengths
93
+
94
+ EinsteinEngine is particularly useful when you want to:
95
+
96
+ - build symbolic curvature tensors from a metric,
97
+ - test GR expressions quickly in notebooks or scripts,
98
+ - compare symbolic performance against pure SymPy,
99
+ - explore exact tensor structures without writing all the algebra by hand.
100
+
101
+ ## Benchmarks
102
+
103
+ A simple benchmark script is available in the benchmarks folder to compare EinsteinEngine against pure SymPy on a representative symbolic workload.
104
+
105
+ Run it from the project root with:
106
+
107
+ ```bash
108
+ python benchmarks/compare_sympy_vs_einsteinengine.py
109
+ ```
110
+
111
+ The script prints timing information for both engines and a rough speedup estimate.
112
+
113
+ ## Testing
114
+
115
+ The project includes a pytest suite covering core computations such as metric handling, curvature tensors, geodesics and rendering behaviour.
116
+
117
+ Run tests with:
118
+
119
+ ```bash
120
+ pytest
121
+ ```
122
+
123
+ ## Roadmap and future directions
124
+
125
+ This project is already usable for a broad set of symbolic GR tasks, but there is still room to grow. Possible future work includes:
126
+
127
+ - more optimized tensor contractions and higher-rank workflows,
128
+ - broader support for non-diagonal and more general metrics,
129
+ - improved simplification strategies with configurable modes,
130
+ - additional GR utilities such as more advanced invariants and field equations,
131
+ - better documentation and more benchmark examples.
132
+
133
+ The goal is to keep the library practical today while expanding toward a more complete symbolic GR toolkit over time.
@@ -0,0 +1,112 @@
1
+ # EinsteinEngine
2
+
3
+ EinsteinEngine is a symbolic tensor library for General Relativity built around a simple idea: make tensor calculations faster and more structured without giving up exact symbolic mathematics.
4
+
5
+ The project combines Python with SymEngine for the heavy symbolic work, and exposes an object-oriented API for common relativistic objects such as metrics, Christoffel symbols, curvature tensors, Ricci tensors, Einstein tensors, geodesics, tetrads and spin connections.
6
+
7
+ ## What the library can do today
8
+
9
+ EinsteinEngine currently provides a practical toolkit for symbolic GR workflows, including:
10
+
11
+ - Metric tensor construction and validation
12
+ - Metric inversion for diagonal and standard symbolic cases
13
+ - Christoffel symbol computation
14
+ - Riemann tensor construction
15
+ - Ricci tensor and Ricci scalar computation
16
+ - Einstein tensor construction
17
+ - Geodesic equation generation
18
+ - Tetrad and spin connection support
19
+ - Tensor component access, contraction, index raising/lowering and basic tensor arithmetic
20
+ - Rich LaTeX-style rendering for notebooks and a plain-text fallback for lighter output
21
+
22
+ The focus of the library is not only correctness, but also performance in symbolic pipelines that would otherwise become very expensive in pure SymPy.
23
+
24
+ ## Installation
25
+
26
+ From PyPI:
27
+
28
+ ```bash
29
+ pip install einsteinengine
30
+ ```
31
+
32
+ From source:
33
+
34
+ ```bash
35
+ git clone https://github.com/RSalazarD/EinsteinEngine.git
36
+ cd EinsteinEngine
37
+ pip install -e .
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ import sympy as sp
44
+ from einsteinengine.symbolic.metric import MetricTensor
45
+ from einsteinengine.symbolic.riemann import RiemannCurvatureTensor
46
+
47
+ # Coordinates and parameters
48
+ t, r, theta, phi = sp.symbols('t r theta phi', real=True)
49
+ M = sp.symbols('M', real=True)
50
+
51
+ # Schwarzschild metric
52
+ g_schwarzschild = [
53
+ [-(1 - 2*M/r), 0, 0, 0],
54
+ [0, 1/(1 - 2*M/r), 0, 0],
55
+ [0, 0, r**2, 0],
56
+ [0, 0, 0, r**2 * sp.sin(theta)**2],
57
+ ]
58
+
59
+ metric = MetricTensor(g_schwarzschild, [t, r, theta, phi], name="Schwarzschild")
60
+ riemann = RiemannCurvatureTensor.from_metric(metric, verbose=False)
61
+
62
+ print(riemann.get_component(1, 0, 1, 0))
63
+ ```
64
+
65
+ A typical output for the Schwarzschild case is the known exact component:
66
+
67
+ ```python
68
+ 2*M*(2*M - r)/r**4
69
+ ```
70
+
71
+ ## Current strengths
72
+
73
+ EinsteinEngine is particularly useful when you want to:
74
+
75
+ - build symbolic curvature tensors from a metric,
76
+ - test GR expressions quickly in notebooks or scripts,
77
+ - compare symbolic performance against pure SymPy,
78
+ - explore exact tensor structures without writing all the algebra by hand.
79
+
80
+ ## Benchmarks
81
+
82
+ A simple benchmark script is available in the benchmarks folder to compare EinsteinEngine against pure SymPy on a representative symbolic workload.
83
+
84
+ Run it from the project root with:
85
+
86
+ ```bash
87
+ python benchmarks/compare_sympy_vs_einsteinengine.py
88
+ ```
89
+
90
+ The script prints timing information for both engines and a rough speedup estimate.
91
+
92
+ ## Testing
93
+
94
+ The project includes a pytest suite covering core computations such as metric handling, curvature tensors, geodesics and rendering behaviour.
95
+
96
+ Run tests with:
97
+
98
+ ```bash
99
+ pytest
100
+ ```
101
+
102
+ ## Roadmap and future directions
103
+
104
+ This project is already usable for a broad set of symbolic GR tasks, but there is still room to grow. Possible future work includes:
105
+
106
+ - more optimized tensor contractions and higher-rank workflows,
107
+ - broader support for non-diagonal and more general metrics,
108
+ - improved simplification strategies with configurable modes,
109
+ - additional GR utilities such as more advanced invariants and field equations,
110
+ - better documentation and more benchmark examples.
111
+
112
+ The goal is to keep the library practical today while expanding toward a more complete symbolic GR toolkit over time.
@@ -1,39 +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
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
39
  return cls(Gamma, syms, config="ull", name=f"Christoffel_{metric.name}", verbose=verbose)
@@ -1,111 +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
-
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
+