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.
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
@@ -0,0 +1,34 @@
1
+ import symengine as se
2
+ from einsteinengine.symbolic.tensor import BaseRelativityTensor
3
+
4
+ class RicciScalar(BaseRelativityTensor):
5
+
6
+ @classmethod
7
+ def from_ricci_tensor_and_metric(cls, ricci_tensor, metric, verbose=False):
8
+ """
9
+ Computes the Ricci Scalar by contracting the Ricci Tensor
10
+ with the inverse Metric Tensor using the universal contraction engine
11
+ """
12
+ if verbose:
13
+ print("Building Ricci Scalar by contracting Ricci Tensor with inverse metric...")
14
+
15
+ g_inv = metric.inv()
16
+
17
+ # R_mu_nu has config 'll'. g^mu_nu has config 'uu'.
18
+ scalar_tensor = ricci_tensor.multiply_and_contract(
19
+ g_inv,
20
+ pairs=[(0, 0), (1, 1)],
21
+ new_name=f"RicciScalar_{metric.name}"
22
+ )
23
+
24
+ # Re-instantiate as RicciScalar to maintain strict semantics (Domain-Driven Design)
25
+ return cls(
26
+ scalar_tensor.get_raw_data(),
27
+ metric.syms,
28
+ config="", # Empty configuration because it is rank 0
29
+ name=scalar_tensor.name,
30
+ verbose=verbose
31
+ )
32
+
33
+
34
+
@@ -0,0 +1,21 @@
1
+ from einsteinengine.symbolic.tensor import BaseRelativityTensor
2
+
3
+ class RicciTensor(BaseRelativityTensor):
4
+
5
+ @classmethod
6
+ def from_riemann(cls, riemann, verbose=False):
7
+ """
8
+ Computes the Ricci Tensor (R_mu_nu) by contracting the 1st (upper)
9
+ and 3rd (lower) indices of the Riemann Tensor.
10
+ """
11
+ if verbose:
12
+ print(f"Building Ricci Tensor from {riemann.name}...")
13
+
14
+ # Riemann has config 'ulll'.
15
+ # Index 0 = 'u' (lambda)
16
+ # Index 2 = 'l' (lambda contracted)
17
+ contracted_tensor = riemann.contract_indices(0, 2, new_name=f"Ricci_{riemann.name}")
18
+
19
+ return cls(contracted_tensor.get_raw_data(), riemann.syms, config=contracted_tensor.config, name=contracted_tensor.name, verbose=verbose)
20
+
21
+