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.
@@ -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)