einsteinengine 0.5.0__py3-none-any.whl
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/__init__.py +0 -0
- einsteinengine/symbolic/__init__.py +0 -0
- einsteinengine/symbolic/christoffel.py +39 -0
- einsteinengine/symbolic/connection.py +111 -0
- einsteinengine/symbolic/core.py +136 -0
- einsteinengine/symbolic/einstein_tensor.py +57 -0
- einsteinengine/symbolic/energy_momentum.py +45 -0
- einsteinengine/symbolic/geodesics.py +96 -0
- einsteinengine/symbolic/metric.py +60 -0
- einsteinengine/symbolic/ricci_scalar.py +34 -0
- einsteinengine/symbolic/ricci_tensor.py +21 -0
- einsteinengine/symbolic/riemann.py +116 -0
- einsteinengine/symbolic/spin_connection.py +43 -0
- einsteinengine/symbolic/tensor.py +439 -0
- einsteinengine/symbolic/tetrad.py +51 -0
- einsteinengine/symbolic/weyl.py +83 -0
- einsteinengine-0.5.0.dist-info/METADATA +63 -0
- einsteinengine-0.5.0.dist-info/RECORD +20 -0
- einsteinengine-0.5.0.dist-info/WHEEL +5 -0
- einsteinengine-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import symengine as se
|
|
2
|
+
from einsteinengine.symbolic.tensor import BaseRelativityTensor
|
|
3
|
+
|
|
4
|
+
class RiemannCurvatureTensor(BaseRelativityTensor):
|
|
5
|
+
""" Riemann standard index configuration: 1 upper, 3 lower (ulll -> R^rho_{sigma mu nu})"""
|
|
6
|
+
|
|
7
|
+
@classmethod
|
|
8
|
+
def from_christoffel(cls, christoffel, verbose=False):
|
|
9
|
+
"""
|
|
10
|
+
Computes the 256 components of the Riemann Curvature Tensor from Christoffel inputs.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
if verbose:
|
|
14
|
+
print("Building Riemann Curvature Tensor...")
|
|
15
|
+
|
|
16
|
+
Gamma = christoffel.get_raw_data()
|
|
17
|
+
syms = christoffel.syms
|
|
18
|
+
dims = len(syms)
|
|
19
|
+
|
|
20
|
+
# Initialize a 4D grid structure (4x4x4x4 = 256 elements)
|
|
21
|
+
R = [[[[se.sympify(0) for _ in range(dims)] for _ in range(dims)] for _ in range(dims)] for _ in range(dims)]
|
|
22
|
+
|
|
23
|
+
for rho in range(dims):
|
|
24
|
+
for sigma in range(dims):
|
|
25
|
+
for mu in range(dims):
|
|
26
|
+
for nu in range(dims):
|
|
27
|
+
# R^rho_{sigma mu nu} = d(Gamma)/d(mu) - d(Gamma)/d(nu) + contractions term1 = se.diff(Gamma[rho][nu][sigma], syms[mu])
|
|
28
|
+
term1 = se.diff(Gamma[rho][nu][sigma], syms[mu])
|
|
29
|
+
term2 = se.diff(Gamma[rho][mu][sigma], syms[nu])
|
|
30
|
+
|
|
31
|
+
term3 = se.sympify(0)
|
|
32
|
+
term4 = se.sympify(0)
|
|
33
|
+
for lam in range(dims):
|
|
34
|
+
term3 += Gamma[rho][mu][lam] * Gamma[lam][nu][sigma]
|
|
35
|
+
term4 += Gamma[rho][nu][lam] * Gamma[lam][mu][sigma]
|
|
36
|
+
|
|
37
|
+
R[rho][sigma][mu][nu] = term1 - term2 + term3 - term4
|
|
38
|
+
|
|
39
|
+
return cls(R, syms, config="ulll", name="RiemannCurvature", verbose=verbose)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_metric(cls, metric, verbose=False):
|
|
43
|
+
"""
|
|
44
|
+
Pipeline connection: Metric -> Christoffel -> Riemann Curvature Tensor
|
|
45
|
+
"""
|
|
46
|
+
if verbose:
|
|
47
|
+
print(f"Triggering pipeline execution for metric: '{metric.name}'")
|
|
48
|
+
|
|
49
|
+
from einsteinengine.symbolic.christoffel import ChristoffelSymbols
|
|
50
|
+
|
|
51
|
+
intermediate_christoffel = ChristoffelSymbols.from_metric(metric, verbose=verbose)
|
|
52
|
+
return cls.from_christoffel(intermediate_christoffel, verbose=verbose)
|
|
53
|
+
|
|
54
|
+
def kretschmann_scalar(self, metric, simplify=False, verbose=False):
|
|
55
|
+
"""
|
|
56
|
+
Computes the Kretschmann scalar K = R^{abcd} R_{abcd}.
|
|
57
|
+
Automatically fetches the cached inverse metric to optimize performance.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
metric (BaseRelativityTensor): Covariant metric tensor ('ll').
|
|
61
|
+
simplify (bool): Whether to simplify the resulting scalar.
|
|
62
|
+
verbose (bool): Whether to print detailed information about the computation.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
sp.Expr: The simplified scalar mathematical expression.
|
|
66
|
+
"""
|
|
67
|
+
import sympy as sp
|
|
68
|
+
|
|
69
|
+
if verbose:
|
|
70
|
+
print(f"[{self.name}] Computing Kretschmann Scalar...")
|
|
71
|
+
|
|
72
|
+
# 1. Fetch the inverse metric (Instantly returns if already cached!)
|
|
73
|
+
try:
|
|
74
|
+
metric_inv = metric.inv()
|
|
75
|
+
except AttributeError:
|
|
76
|
+
raise TypeError("The 'metric' argument must have an 'inv()' method.")
|
|
77
|
+
|
|
78
|
+
# 2. Obtain the fully covariant Riemann Tensor (R_{abcd} -> config 'llll')
|
|
79
|
+
if self.config == 'ulll':
|
|
80
|
+
if verbose: print(f"[{self.name}] Lowering first index to get R_{{abcd}}...")
|
|
81
|
+
R_down = self.lower_index(0, metric, verbose=False)
|
|
82
|
+
elif self.config == 'llll':
|
|
83
|
+
R_down = self
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError(f"Unexpected index configuration '{self.config}' for Riemann tensor. Expected 'ulll' or 'llll'.")
|
|
86
|
+
|
|
87
|
+
# 3. Obtain the fully contravariant Riemann Tensor (R^{abcd} -> config 'uuuu')
|
|
88
|
+
# We sequentially raise all 4 indices using the cached inverse
|
|
89
|
+
if verbose: print(f"[{self.name}] Raising all indices to get R^{{abcd}}...")
|
|
90
|
+
R_up = R_down.raise_index(0, metric_inv, verbose=False) \
|
|
91
|
+
.raise_index(1, metric_inv, verbose=False) \
|
|
92
|
+
.raise_index(2, metric_inv, verbose=False) \
|
|
93
|
+
.raise_index(3, metric_inv, verbose=False)
|
|
94
|
+
|
|
95
|
+
# 4. Perform the full contraction: sum( R_{abcd} * R^{abcd} )
|
|
96
|
+
if verbose: print(f"[{self.name}] Executing full scalar contraction (256 terms)...")
|
|
97
|
+
K_val = 0
|
|
98
|
+
down_data = R_down.get_raw_data()
|
|
99
|
+
up_data = R_up.get_raw_data()
|
|
100
|
+
dims = self.dims
|
|
101
|
+
|
|
102
|
+
for a in range(dims):
|
|
103
|
+
for b in range(dims):
|
|
104
|
+
for c in range(dims):
|
|
105
|
+
for d in range(dims):
|
|
106
|
+
term = down_data[a][b][c][d] * up_data[a][b][c][d]
|
|
107
|
+
if term != 0: # Optimization: avoid adding zeros
|
|
108
|
+
K_val += term
|
|
109
|
+
|
|
110
|
+
if simplify:
|
|
111
|
+
if verbose: print(f"[{self.name}] Simplifying the invariant scalar... (This might take a while)")
|
|
112
|
+
return sp.simplify(K_val)
|
|
113
|
+
else:
|
|
114
|
+
if verbose: print(f"[{self.name}] Returning raw scalar (Simplification skipped for performance).")
|
|
115
|
+
return K_val
|
|
116
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import symengine as se
|
|
2
|
+
from einsteinengine.symbolic.core import BaseRelativityObject
|
|
3
|
+
|
|
4
|
+
class SpinConnection(BaseRelativityObject):
|
|
5
|
+
|
|
6
|
+
@classmethod
|
|
7
|
+
def from_tetrad_and_christoffel(cls, tetrad, christoffel, verbose=False):
|
|
8
|
+
r"""
|
|
9
|
+
Computes the Spin Connection \omega_{\mu \ \ b}^{\ a} fields.
|
|
10
|
+
Requires a Tetrad e_\mu^a (config 'lu') and Christoffel Symbols \Gamma^nu_{\mu\sigma}.
|
|
11
|
+
"""
|
|
12
|
+
if verbose:
|
|
13
|
+
print("Computing Spin Connection fields from Tetrad and Christoffel...")
|
|
14
|
+
|
|
15
|
+
dims = tetrad.dims
|
|
16
|
+
syms = tetrad.syms
|
|
17
|
+
|
|
18
|
+
# 1. Gather component raw data
|
|
19
|
+
e_mu_a = tetrad.get_raw_data() # e_\mu^a (config 'lu')
|
|
20
|
+
inv_tetrad = tetrad.get_inverse()
|
|
21
|
+
e_up_mu_flat_b = inv_tetrad.get_raw_data() # e^\nu_b (config 'ul')
|
|
22
|
+
Gamma = christoffel.get_raw_data() # \Gamma^\nu_{\mu\sigma}
|
|
23
|
+
|
|
24
|
+
# 2. Initialize 3D array: [mu][a][b]
|
|
25
|
+
omega = [[[se.sympify(0) for _ in range(dims)] for _ in range(dims)] for _ in range(dims)]
|
|
26
|
+
|
|
27
|
+
# 3. Compute the analytical formulation
|
|
28
|
+
for mu in range(dims):
|
|
29
|
+
for a in range(dims):
|
|
30
|
+
for b in range(dims):
|
|
31
|
+
tmp_sum = se.sympify(0)
|
|
32
|
+
for nu in range(dims):
|
|
33
|
+
# Term 1: e_\nu^a * \partial_\mu(e^\nu_b)
|
|
34
|
+
partial_term = se.diff(e_up_mu_flat_b[nu][b], syms[mu])
|
|
35
|
+
tmp_sum += e_mu_a[nu][a] * partial_term
|
|
36
|
+
|
|
37
|
+
# Term 2: e_\nu^a * \Gamma^\nu_{\mu\sigma} * e^\sigma_b
|
|
38
|
+
for sigma in range(dims):
|
|
39
|
+
tmp_sum += e_mu_a[nu][a] * Gamma[nu][mu][sigma] * e_up_mu_flat_b[sigma][b]
|
|
40
|
+
|
|
41
|
+
omega[mu][a][b] = tmp_sum
|
|
42
|
+
|
|
43
|
+
return cls(omega, syms, name=f"SpinConnection_{tetrad.name}", verbose=verbose)
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
from einsteinengine.symbolic.core import BaseRelativityObject
|
|
2
|
+
import symengine as se
|
|
3
|
+
import sympy as sp
|
|
4
|
+
import itertools
|
|
5
|
+
|
|
6
|
+
class BaseRelativityTensor(BaseRelativityObject):
|
|
7
|
+
"""
|
|
8
|
+
Base class for strict tensor objects (Metric, Riemann, Ricci, etc.).
|
|
9
|
+
Inherits coordinate and array management from BaseRelativityObject.
|
|
10
|
+
Adds tensor-specific properties like index configuration (e.g., 'll', 'uu', 'ul').
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, arr, syms, config="ll", name="GenericTensor", verbose=False):
|
|
14
|
+
# 1. Delegate the heavy lifting to the parent object (coordinates & SymEngine)
|
|
15
|
+
super().__init__(arr, syms, name=name, verbose=verbose)
|
|
16
|
+
|
|
17
|
+
# 2. Tensor-exclusive physical properties
|
|
18
|
+
self.config = config
|
|
19
|
+
|
|
20
|
+
# 3. Component validation
|
|
21
|
+
# Ensure the length of the configuration matches the tensor rank
|
|
22
|
+
# (e.g., a 2D matrix needs a 2-letter config like 'll')
|
|
23
|
+
if self._data and isinstance(arr, list):
|
|
24
|
+
rank = self._calculate_rank(arr)
|
|
25
|
+
if len(self.config) != rank:
|
|
26
|
+
raise ValueError(f"Configuration length ({len(self.config)}) does not match tensor rank ({rank})")
|
|
27
|
+
|
|
28
|
+
if self.verbose:
|
|
29
|
+
print(f"[{self.name}] Tensor initialized with index configuration: '{self.config}'.")
|
|
30
|
+
|
|
31
|
+
def _symenginify_array(self, arr):
|
|
32
|
+
"""
|
|
33
|
+
Recursive method to convert any nested list of SymPy
|
|
34
|
+
objects or raw strings into SymEngine objects.
|
|
35
|
+
"""
|
|
36
|
+
if isinstance(arr, (list, tuple)):
|
|
37
|
+
return [self._symenginify_array(element) for element in arr]
|
|
38
|
+
else:
|
|
39
|
+
return se.sympify(arr)
|
|
40
|
+
|
|
41
|
+
def get_component(self, *indices):
|
|
42
|
+
"""Calculates and simplifies a single component."""
|
|
43
|
+
val = self._data
|
|
44
|
+
for i in indices:
|
|
45
|
+
val = val[i]
|
|
46
|
+
return sp.simplify(sp.sympify(val))
|
|
47
|
+
|
|
48
|
+
def _sympyfy_and_simplify(self, arr):
|
|
49
|
+
"""
|
|
50
|
+
Recursive method to convert from SymEngine to Sympy and simplify
|
|
51
|
+
"""
|
|
52
|
+
if isinstance(arr, (list, tuple)):
|
|
53
|
+
return [self._sympyfy_and_simplify(element) for element in arr]
|
|
54
|
+
else:
|
|
55
|
+
# converts SymEngine in SymPy and simplifies
|
|
56
|
+
sympy_expr = sp.sympify(arr)
|
|
57
|
+
return sp.trigsimp(sympy_expr)
|
|
58
|
+
|
|
59
|
+
def to_latex(self, *indices):
|
|
60
|
+
"""
|
|
61
|
+
If indices are provided (e.g. to_latex(0, 1, 1)), simplifies only that component.
|
|
62
|
+
If no arguments are passed, converts the entire tensor to LaTeX.
|
|
63
|
+
"""
|
|
64
|
+
if indices:
|
|
65
|
+
# Specific component
|
|
66
|
+
val = self.get_component(*indices)
|
|
67
|
+
return sp.latex(val)
|
|
68
|
+
else:
|
|
69
|
+
# Entire tensor (caution: can be very large)
|
|
70
|
+
return sp.latex(self._data)
|
|
71
|
+
|
|
72
|
+
def _repr_latex_(self):
|
|
73
|
+
"""
|
|
74
|
+
Special method hooks into Jupyter Notebook's display system.
|
|
75
|
+
When the object name is evaluated in a cell, Jupyter automatically
|
|
76
|
+
calls this method to render the output as LaTeX.
|
|
77
|
+
"""
|
|
78
|
+
return f"$$ {self.to_latex()} $$"
|
|
79
|
+
|
|
80
|
+
def get_non_zero(self):
|
|
81
|
+
"""
|
|
82
|
+
Returns a list of tuples (indices, value) for all non-zero components.
|
|
83
|
+
"""
|
|
84
|
+
non_zeros = []
|
|
85
|
+
|
|
86
|
+
# For any range tensors
|
|
87
|
+
def _recurse(arr, current_indices):
|
|
88
|
+
for i, val in enumerate(arr):
|
|
89
|
+
if isinstance(val, list):
|
|
90
|
+
_recurse(val, current_indices + [i])
|
|
91
|
+
else:
|
|
92
|
+
if val != 0:
|
|
93
|
+
non_zeros.append((current_indices + [i], val))
|
|
94
|
+
|
|
95
|
+
_recurse(self._data, [])
|
|
96
|
+
return non_zeros
|
|
97
|
+
|
|
98
|
+
def contract_indices(self, idx1, idx2, new_name=None):
|
|
99
|
+
"""
|
|
100
|
+
Contracts one upper ('u') and one lower ('l') index of the tensor.
|
|
101
|
+
Returns a brand new BaseRelativityTensor of rank N-2.
|
|
102
|
+
"""
|
|
103
|
+
if idx1 == idx2:
|
|
104
|
+
raise ValueError("Cannot contract an index with itself.")
|
|
105
|
+
|
|
106
|
+
rank = len(self.config)
|
|
107
|
+
if idx1 >= rank or idx2 >= rank:
|
|
108
|
+
raise IndexError(f"Tensor has rank {rank}. Cannot contract indices at {idx1} and {idx2}.")
|
|
109
|
+
|
|
110
|
+
if self.config[idx1] == self.config[idx2]:
|
|
111
|
+
raise ValueError(f"Invalid contraction: indices {idx1} and {idx2} have the same variance ('{self.config[idx1]}').")
|
|
112
|
+
|
|
113
|
+
# Computing de new string of indices
|
|
114
|
+
new_config = ""
|
|
115
|
+
for i, char in enumerate(self.config):
|
|
116
|
+
if i != idx1 and i != idx2:
|
|
117
|
+
new_config += char
|
|
118
|
+
|
|
119
|
+
if self.verbose:
|
|
120
|
+
print(f"Contracting indices {idx1} and {idx2} of {self.name}...")
|
|
121
|
+
|
|
122
|
+
new_data = self._algebraic_contraction(idx1, idx2)
|
|
123
|
+
final_name = new_name if new_name else f"Contracted_{self.name}"
|
|
124
|
+
|
|
125
|
+
# Devolvemos un nuevo tensor matemáticamente válido
|
|
126
|
+
return BaseRelativityTensor(new_data, self.syms, config=new_config, name=final_name, verbose=self.verbose)
|
|
127
|
+
|
|
128
|
+
def multiply_and_contract(self, other, pairs, new_name=None):
|
|
129
|
+
"""
|
|
130
|
+
Internal contraction of two tensors (A * B).
|
|
131
|
+
Avoids building the outer product intermediate tensor.
|
|
132
|
+
'pairs' is a list of tuples: [(index_A, index_B), ...] which we are contracting
|
|
133
|
+
"""
|
|
134
|
+
rank_self = len(self.config)
|
|
135
|
+
rank_other = len(other.config)
|
|
136
|
+
|
|
137
|
+
# Separate free indices (remaining) from contracted indices
|
|
138
|
+
self_contracted = [p[0] for p in pairs]
|
|
139
|
+
other_contracted = [p[1] for p in pairs]
|
|
140
|
+
|
|
141
|
+
self_free = [i for i in range(rank_self) if i not in self_contracted]
|
|
142
|
+
other_free = [i for i in range(rank_other) if i not in other_contracted]
|
|
143
|
+
|
|
144
|
+
# Validate tensor physics (Covariant must contract with Contravariant)
|
|
145
|
+
for s_idx, o_idx in pairs:
|
|
146
|
+
if self.config[s_idx] == other.config[o_idx]:
|
|
147
|
+
raise ValueError(f"Invalid contraction at pair ({s_idx}, {o_idx}): both have variance '{self.config[s_idx]}'.")
|
|
148
|
+
|
|
149
|
+
# The new configuration is the concatenation of the free indices
|
|
150
|
+
new_config = "".join([self.config[i] for i in self_free]) + \
|
|
151
|
+
"".join([other.config[i] for i in other_free])
|
|
152
|
+
|
|
153
|
+
# Fast helper function to read nested matrix values dynamically
|
|
154
|
+
def get_val(data, coords):
|
|
155
|
+
val = data
|
|
156
|
+
for c in coords: val = val[c]
|
|
157
|
+
return val
|
|
158
|
+
|
|
159
|
+
# Combinatorial Engine (einsum approach)
|
|
160
|
+
dims = self.dims
|
|
161
|
+
num_free = len(self_free) + len(other_free)
|
|
162
|
+
num_dummy = len(pairs)
|
|
163
|
+
|
|
164
|
+
# If the result is a scalar (0 free indices), handle the direct sum
|
|
165
|
+
if num_free == 0:
|
|
166
|
+
result = se.sympify(0)
|
|
167
|
+
for dummy_vals in itertools.product(range(dims), repeat=num_dummy):
|
|
168
|
+
coords_self = [0] * rank_self
|
|
169
|
+
coords_other = [0] * rank_other
|
|
170
|
+
for p_idx, (s_idx, o_idx) in enumerate(pairs):
|
|
171
|
+
coords_self[s_idx] = dummy_vals[p_idx]
|
|
172
|
+
coords_other[o_idx] = dummy_vals[p_idx]
|
|
173
|
+
result += get_val(self._data, coords_self) * get_val(other.get_raw_data(), coords_other)
|
|
174
|
+
|
|
175
|
+
final_name = new_name if new_name else "ContractedScalar"
|
|
176
|
+
return BaseRelativityTensor(result, self.syms, config="", name=final_name, verbose=self.verbose)
|
|
177
|
+
|
|
178
|
+
# 5. If it is not a scalar, build the resulting matrix recursively
|
|
179
|
+
def build_result(free_coords_flat):
|
|
180
|
+
if len(free_coords_flat) == num_free:
|
|
181
|
+
tmp_sum = se.sympify(0)
|
|
182
|
+
# Loop only over the dummy indices (the contracted ones)
|
|
183
|
+
for dummy_vals in itertools.product(range(dims), repeat=num_dummy):
|
|
184
|
+
coords_self = [0] * rank_self
|
|
185
|
+
coords_other = [0] * rank_other
|
|
186
|
+
|
|
187
|
+
# Set the free coordinates
|
|
188
|
+
for i, free_idx in enumerate(self_free):
|
|
189
|
+
coords_self[free_idx] = free_coords_flat[i]
|
|
190
|
+
for i, free_idx in enumerate(other_free):
|
|
191
|
+
coords_other[free_idx] = free_coords_flat[len(self_free) + i]
|
|
192
|
+
|
|
193
|
+
# Set the dummy coordinates (the summed ones)
|
|
194
|
+
for p_idx, (s_idx, o_idx) in enumerate(pairs):
|
|
195
|
+
coords_self[s_idx] = dummy_vals[p_idx]
|
|
196
|
+
coords_other[o_idx] = dummy_vals[p_idx]
|
|
197
|
+
|
|
198
|
+
# Multiply and sum "on the fly"
|
|
199
|
+
tmp_sum += get_val(self._data, coords_self) * get_val(other.get_raw_data(), coords_other)
|
|
200
|
+
return tmp_sum
|
|
201
|
+
else:
|
|
202
|
+
return [build_result(free_coords_flat + [d]) for d in range(dims)]
|
|
203
|
+
|
|
204
|
+
final_data = build_result([])
|
|
205
|
+
final_name = new_name if new_name else f"Contracted_{self.name}_{other.name}"
|
|
206
|
+
|
|
207
|
+
return BaseRelativityTensor(final_data, self.syms, config=new_config, name=final_name, verbose=self.verbose)
|
|
208
|
+
|
|
209
|
+
def transform_coordinates(self, new_syms, old_coords_in_terms_of_new, new_name=None):
|
|
210
|
+
"""
|
|
211
|
+
Transforms the tensor to a new coordinate system using Jacobian matrices.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
new_syms (list): List of new coordinate symbols (e.g., [X, Y, Z, T]).
|
|
215
|
+
old_coords_in_terms_of_new (list): Equations defining old symbols as functions of new symbols.
|
|
216
|
+
"""
|
|
217
|
+
import itertools
|
|
218
|
+
if self.verbose:
|
|
219
|
+
print(f"[{self.name}] Performing general coordinate transformation...")
|
|
220
|
+
|
|
221
|
+
rank = len(self.config)
|
|
222
|
+
dims = self.dims
|
|
223
|
+
new_dims = len(new_syms)
|
|
224
|
+
new_se_syms = [se.sympify(s) for s in new_syms]
|
|
225
|
+
|
|
226
|
+
# 1. Compute the forward Jacobian matrix: J^\mu_\alpha = \partial(old^\mu) / \partial(new^\alpha)
|
|
227
|
+
# Rows: old coordinates (\mu), Columns: new coordinates (\alpha)
|
|
228
|
+
jacobian = [[se.diff(old_coords_in_terms_of_new[mu], new_se_syms[alpha])
|
|
229
|
+
for alpha in range(new_dims)] for mu in range(dims)]
|
|
230
|
+
|
|
231
|
+
# 2. Compute the inverse Jacobian matrix: J_inv^\alpha_\mu = \partial(new^\alpha) / \partial(old^\mu)
|
|
232
|
+
# Required for upper ('u') contravariant indices transformation
|
|
233
|
+
jac_matrix = se.Matrix(jacobian)
|
|
234
|
+
jac_inverse = jac_matrix.inv()
|
|
235
|
+
|
|
236
|
+
# Helper reader for old data nested positions
|
|
237
|
+
def get_old_val(coords):
|
|
238
|
+
val = self._data
|
|
239
|
+
for c in coords: val = val[c]
|
|
240
|
+
return val
|
|
241
|
+
|
|
242
|
+
# 3. Recursive matrix builder for the transformed tensor
|
|
243
|
+
def build_transformed(current_new_coords):
|
|
244
|
+
if len(current_new_coords) == rank:
|
|
245
|
+
# Inside the cell: apply the multi-index contraction with Jacobians
|
|
246
|
+
tmp_sum = se.sympify(0)
|
|
247
|
+
# Iterate over all combinations of the old coordinate indices
|
|
248
|
+
for old_coords in itertools.product(range(dims), repeat=rank):
|
|
249
|
+
old_value = get_old_val(old_coords)
|
|
250
|
+
|
|
251
|
+
# Compute the transformation multiplier factor
|
|
252
|
+
factor = se.sympify(1)
|
|
253
|
+
for i in range(rank):
|
|
254
|
+
mu = old_coords[i] # Old index position
|
|
255
|
+
alpha = current_new_coords[i] # New index position
|
|
256
|
+
|
|
257
|
+
if self.config[i] == 'l':
|
|
258
|
+
# Covariant index transforms with forward Jacobian
|
|
259
|
+
factor *= jacobian[mu][alpha]
|
|
260
|
+
elif self.config[i] == 'u':
|
|
261
|
+
# Contravariant index transforms with inverse Jacobian
|
|
262
|
+
factor *= jac_inverse[alpha, mu]
|
|
263
|
+
|
|
264
|
+
tmp_sum += old_value * factor
|
|
265
|
+
|
|
266
|
+
# Substitute old variables with the new equations to eliminate old symbols
|
|
267
|
+
for idx, old_s in enumerate(self.syms):
|
|
268
|
+
tmp_sum = tmp_sum.subs(old_s, old_coords_in_terms_of_new[idx])
|
|
269
|
+
return tmp_sum
|
|
270
|
+
else:
|
|
271
|
+
return [build_transformed(current_new_coords + [d]) for d in range(new_dims)]
|
|
272
|
+
|
|
273
|
+
transformed_data = build_transformed([])
|
|
274
|
+
final_name = new_name if new_name else f"Transformed_{self.name}"
|
|
275
|
+
|
|
276
|
+
# Return tensor under the new coordinate patch
|
|
277
|
+
return BaseRelativityTensor(transformed_data, new_se_syms, config=self.config, name=final_name, verbose=self.verbose)
|
|
278
|
+
|
|
279
|
+
# --- Tensor Arithmetic ---
|
|
280
|
+
|
|
281
|
+
def __add__(self, other):
|
|
282
|
+
"""Allows addition using the '+' operator: T = A + B"""
|
|
283
|
+
if not isinstance(other, BaseRelativityTensor):
|
|
284
|
+
raise TypeError("You can only add a Tensor to another Tensor.")
|
|
285
|
+
if self.dims != other.dims or self.config != other.config:
|
|
286
|
+
raise ValueError(f"Cannot add tensors with different indices or dimensions. Got {self.config} and {other.config}")
|
|
287
|
+
|
|
288
|
+
# Recursive addition for any tensor rank
|
|
289
|
+
def _recursive_add(a, b):
|
|
290
|
+
if isinstance(a, list):
|
|
291
|
+
return [_recursive_add(x, y) for x, y in zip(a, b)]
|
|
292
|
+
return a + b
|
|
293
|
+
|
|
294
|
+
new_data = _recursive_add(self._data, other._data)
|
|
295
|
+
return BaseRelativityTensor(new_data, self.syms, self.config, name=f"({self.name} + {other.name})", verbose=self.verbose)
|
|
296
|
+
|
|
297
|
+
def __sub__(self, other):
|
|
298
|
+
"""Allows subtraction using the '-' operator: T = A - B"""
|
|
299
|
+
if not isinstance(other, BaseRelativityTensor):
|
|
300
|
+
raise TypeError("You can only subtract a Tensor from another Tensor.")
|
|
301
|
+
if self.dims != other.dims or self.config != other.config:
|
|
302
|
+
raise ValueError("Cannot subtract tensors with different index configurations.")
|
|
303
|
+
|
|
304
|
+
def _recursive_sub(a, b):
|
|
305
|
+
if isinstance(a, list):
|
|
306
|
+
return [_recursive_sub(x, y) for x, y in zip(a, b)]
|
|
307
|
+
return a - b
|
|
308
|
+
|
|
309
|
+
new_data = _recursive_sub(self._data, other._data)
|
|
310
|
+
return BaseRelativityTensor(new_data, self.syms, self.config, name=f"({self.name} - {other.name})", verbose=self.verbose)
|
|
311
|
+
|
|
312
|
+
def __mul__(self, scalar):
|
|
313
|
+
"""Allows right scalar multiplication: T = A * 5"""
|
|
314
|
+
import symengine as se
|
|
315
|
+
|
|
316
|
+
# We only allow multiplication by scalars (numbers or SymEngine/SymPy expressions)
|
|
317
|
+
if isinstance(scalar, BaseRelativityTensor):
|
|
318
|
+
raise TypeError("Use specific tensor product methods to multiply two tensors, not the '*' operator.")
|
|
319
|
+
|
|
320
|
+
scalar_val = se.sympify(scalar)
|
|
321
|
+
|
|
322
|
+
def _recursive_mul(a):
|
|
323
|
+
if isinstance(a, list):
|
|
324
|
+
return [_recursive_mul(x) for x in a]
|
|
325
|
+
return a * scalar_val
|
|
326
|
+
|
|
327
|
+
new_data = _recursive_mul(self._data)
|
|
328
|
+
return BaseRelativityTensor(new_data, self.syms, self.config, name=f"(ScalarMul_{self.name})", verbose=self.verbose)
|
|
329
|
+
|
|
330
|
+
def __rmul__(self, scalar):
|
|
331
|
+
"""Allows left scalar multiplication: T = 5 * A"""
|
|
332
|
+
# Commutative operation, just call the normal __mul__
|
|
333
|
+
return self.__mul__(scalar)
|
|
334
|
+
|
|
335
|
+
def lower_index(self, pos, metric, verbose=False):
|
|
336
|
+
"""
|
|
337
|
+
Lowers a contravariant ('u') index at the specified position using the metric tensor.
|
|
338
|
+
T_{... mu ...} = sum_alpha (g_{mu alpha} * T^{... alpha ...})
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
pos (int): The position of the index to lower (0-indexed).
|
|
342
|
+
metric (BaseRelativityTensor): The covariant metric tensor (config 'll').
|
|
343
|
+
"""
|
|
344
|
+
import symengine as se
|
|
345
|
+
# 1. Physical and dimensional validation
|
|
346
|
+
if self.config[pos] != 'u':
|
|
347
|
+
raise ValueError(f"Index at position {pos} is already covariant ('l') or invalid. Config: {self.config}")
|
|
348
|
+
if metric.config != "ll":
|
|
349
|
+
raise ValueError("Metric must be purely covariant ('ll') to lower an index.")
|
|
350
|
+
|
|
351
|
+
if verbose:
|
|
352
|
+
print(f"[{self.name}] Lowering index at position {pos}...")
|
|
353
|
+
dims = self.dims
|
|
354
|
+
T_data = self.get_raw_data()
|
|
355
|
+
g_data = metric.get_raw_data()
|
|
356
|
+
rank = len(self.config)
|
|
357
|
+
|
|
358
|
+
# 2. Update the configuration string dynamically (e.g., 'uul' -> 'lul')
|
|
359
|
+
new_config = self.config[:pos] + 'l' + self.config[pos+1:]
|
|
360
|
+
# Helper to extract the old tensor component safely
|
|
361
|
+
def get_T(indices):
|
|
362
|
+
val = T_data
|
|
363
|
+
for idx in indices:
|
|
364
|
+
val = val[idx]
|
|
365
|
+
return val
|
|
366
|
+
|
|
367
|
+
# Recursive builder to maintain exact index positions
|
|
368
|
+
def build_lowered(current_indices):
|
|
369
|
+
if len(current_indices) == rank:
|
|
370
|
+
mu = current_indices[pos] # The index we are lowering
|
|
371
|
+
term_sum = se.sympify(0)
|
|
372
|
+
|
|
373
|
+
# Perform the Einstein summation over the dummy index 'alpha'
|
|
374
|
+
for alpha in range(dims):
|
|
375
|
+
old_indices = list(current_indices)
|
|
376
|
+
old_indices[pos] = alpha
|
|
377
|
+
term_sum += g_data[mu][alpha] * get_T(old_indices)
|
|
378
|
+
return term_sum
|
|
379
|
+
else:
|
|
380
|
+
return [build_lowered(current_indices + [d]) for d in range(dims)]
|
|
381
|
+
|
|
382
|
+
new_data = build_lowered([])
|
|
383
|
+
new_name = f"{self.name}_lowered_{pos}"
|
|
384
|
+
|
|
385
|
+
# Return as a new Tensor object, allowing for method chaining
|
|
386
|
+
return self.__class__(new_data, self.syms, config=new_config, name=new_name, verbose=verbose)
|
|
387
|
+
|
|
388
|
+
def raise_index(self, pos, metric_inv, verbose=False):
|
|
389
|
+
"""
|
|
390
|
+
Raises a covariant ('l') index at the specified position using the inverse metric tensor.
|
|
391
|
+
T^{... mu ...} = sum_alpha (g^{mu alpha} * T_{... alpha ...})
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
pos (int): The position of the index to raise (0-indexed).
|
|
395
|
+
metric_inv (BaseRelativityTensor): The contravariant inverse metric tensor (config 'uu').
|
|
396
|
+
"""
|
|
397
|
+
import symengine as se
|
|
398
|
+
|
|
399
|
+
if self.config[pos] != 'l':
|
|
400
|
+
raise ValueError(f"Index at position {pos} is already contravariant ('u') or invalid. Config: {self.config}")
|
|
401
|
+
if metric_inv.config != "uu":
|
|
402
|
+
raise ValueError("Must provide the inverse metric ('uu') to raise an index.")
|
|
403
|
+
|
|
404
|
+
if verbose:
|
|
405
|
+
print(f"[{self.name}] Raising index at position {pos}...")
|
|
406
|
+
|
|
407
|
+
dims = self.dims
|
|
408
|
+
T_data = self.get_raw_data()
|
|
409
|
+
g_inv_data = metric_inv.get_raw_data()
|
|
410
|
+
rank = len(self.config)
|
|
411
|
+
|
|
412
|
+
new_config = self.config[:pos] + 'u' + self.config[pos+1:]
|
|
413
|
+
|
|
414
|
+
def get_T(indices):
|
|
415
|
+
val = T_data
|
|
416
|
+
for idx in indices:
|
|
417
|
+
val = val[idx]
|
|
418
|
+
return val
|
|
419
|
+
|
|
420
|
+
def build_raised(current_indices):
|
|
421
|
+
if len(current_indices) == rank:
|
|
422
|
+
mu = current_indices[pos]
|
|
423
|
+
term_sum = se.sympify(0)
|
|
424
|
+
|
|
425
|
+
for alpha in range(dims):
|
|
426
|
+
old_indices = list(current_indices)
|
|
427
|
+
old_indices[pos] = alpha
|
|
428
|
+
term_sum += g_inv_data[mu][alpha] * get_T(old_indices)
|
|
429
|
+
return term_sum
|
|
430
|
+
else:
|
|
431
|
+
return [build_raised(current_indices + [d]) for d in range(dims)]
|
|
432
|
+
|
|
433
|
+
new_data = build_raised([])
|
|
434
|
+
new_name = f"{self.name}_raised_{pos}"
|
|
435
|
+
|
|
436
|
+
return self.__class__(new_data, self.syms, config=new_config, name=new_name, verbose=verbose)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import symengine as se
|
|
2
|
+
from einsteinengine.symbolic.tensor import BaseRelativityTensor
|
|
3
|
+
|
|
4
|
+
class Tetrad(BaseRelativityTensor):
|
|
5
|
+
def __init__(self, arr, syms, config="lu", name="Tetrad", verbose=False):
|
|
6
|
+
"""
|
|
7
|
+
Initializes a Tetrad (Vierbein) object.
|
|
8
|
+
Default configuration 'lu' implies: index 0 is curved (lower), index 1 is flat (upper).
|
|
9
|
+
"""
|
|
10
|
+
super().__init__(arr, syms, config=config, name=name, verbose=verbose)
|
|
11
|
+
|
|
12
|
+
def get_inverse(self):
|
|
13
|
+
r"""
|
|
14
|
+
Computes the inverse tetrad e^\mu_a.
|
|
15
|
+
Returns a new Tetrad object with configuration 'ul' (curved upper, flat lower).
|
|
16
|
+
"""
|
|
17
|
+
# Matrix inversion of the nested list
|
|
18
|
+
mat = se.Matrix(self._data)
|
|
19
|
+
inv_mat = mat.inv()
|
|
20
|
+
|
|
21
|
+
# The inverse matrix transposes the index positioning implicitly: e^\mu_a
|
|
22
|
+
inv_data = [[inv_mat[i, j] for j in range(self.dims)] for i in range(self.dims)]
|
|
23
|
+
|
|
24
|
+
return Tetrad(inv_data, self.syms, config="ul", name=f"Inverse_{self.name}", verbose=self.verbose)
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_diagonal_metric(cls, metric, signs=None, verbose=False):
|
|
28
|
+
r"""
|
|
29
|
+
Factory method to automatically construct a Tetrad from a diagonal metric tensor.
|
|
30
|
+
g_\mu\nu = e_\mu^a e_\nu^b \eta_{ab}
|
|
31
|
+
"""
|
|
32
|
+
if verbose:
|
|
33
|
+
print(f"Extracting diagonal Tetrad from metric '{metric.name}'...")
|
|
34
|
+
|
|
35
|
+
g_data = metric.get_raw_data()
|
|
36
|
+
dims = metric.dims
|
|
37
|
+
syms = metric.syms
|
|
38
|
+
|
|
39
|
+
# Initialize an empty 4x4 matrix for e_\mu^a
|
|
40
|
+
e_data = [[se.sympify(0) for _ in range(dims)] for _ in range(dims)]
|
|
41
|
+
|
|
42
|
+
# Default Minkowski signature: (-1, 1, 1, 1)
|
|
43
|
+
if signs is None:
|
|
44
|
+
signs = [-1] + [1] * (dims - 1)
|
|
45
|
+
|
|
46
|
+
for i in range(dims):
|
|
47
|
+
# For diagonal metrics, e_\mu^a = sqrt(|g_\mu\mu|)
|
|
48
|
+
val = se.sqrt(se.Abs(g_data[i][i]))
|
|
49
|
+
e_data[i][i] = val
|
|
50
|
+
|
|
51
|
+
return cls(e_data, syms, config="lu", name=f"Tetrad_{metric.name}", verbose=verbose)
|