|
SpECTRE
v2025.08.19
|
SpECTRE's TensorExpressions interface allows you to write tensor equations in SpECTRE in C++ with syntax that resembles tensor index notation. To use it, simply add this include to the top of your file:
The following guide assumes a basic understanding of the Tensor class and tnsr type aliases. RHS refers to the right hand side expression that we wish to compute and LHS refers to the resulting left hand side tensor that stores the result of computing the RHS expression.
TensorExpressions are arithmetic expressions of Tensors that can be evaluated using tenex::evaluate. Terms used in the expression may be Tensors or numbers (see supported types).
As a simple example of how TensorExpressions are used, if you would like to raise the index of some Tensor R with some inverse spacetime metric Tensor g, i.e. \(R^c{}_b = R_{ab} g^{ac}\), you can compute this with TensorExpressions by doing:
where R_up, R, and g are rank 2 spacetime Tensors and the ti::* variables are TensorIndexs representing generic tensor indices. Here is a breakdown of the different parts of this line:
R(ti::a, ti::b) * g(ti::A, ti::C)Tensor is R_upTensor's indices are the template arguments to evaluate: ti::C, ti::bThe LHS Symmetry will be deduced from the RHS tensors' symmetries and order of operations.
Alternatively, if you already have a LHS Tensor variable, you can pass it into the following evaluate overload, where the LHS Tensor provided will be assigned to the result of the RHS expression:
Note that to use this evaluate overload, the LHS Tensor does not need to be previously sized unless the data type is a Blaze vector type (e.g. DataVector) and the RHS expression contains no Tensor terms (see example where sizing is necessary). This overload is useful in a couple cases:
ti::C, ti::b) could theoretically be deduced from the index types of R_up, we still require specifying them because this isn't the case for all equations and we would like to have a unified interface. See this example, which demonstrates a case where you might want to specify the LHS symmetry and where the LHS index order would not be deducible.TensorIndexs represent generic tensor indices and are supplied as comma-separated lists in two places:
Each TensorIndex takes the form ti::* where * is a letter that encodes index properties:
A/a - H/h indicate spacetime indices, I/i - N/n indicate spatial indices, and T/t indicates a concrete time index. This is what is currently defined, but more spatial and spacetime indices (letters) can easily be added if needed. Note that there is no precedence or difference between the indices of some type, e.g. ti::a, ti::b, ... ti::h are equivalentThe properties of each TensorIndex and the Tensor's indices (typelist of TensorIndexTypes) must be compatible:
Tensor's index is spacetime, you can use a spacetime TensorIndex, spatial TensorIndex, or concrete time TensorIndexTensor's index is spatial, you must use a spatial TensorIndexTo demonstrate correct and incorrect usage, let's say we have tensors \(R_{ab}\) (two spacetime indices, e.g. type tnsr::ab) and \(S_{ij}\) (two spatial indices, e.g. type tnsr::ij):
In the following examples:
R is type tnsr::ab<DataVector, 3>S is type tnsr::ab<DataVector, 3>T is type Scalar<DataVector>U is type tnsr::Ab<DataVector, 3>V is type tnsr::aBC<DataVector, 3>G is type tnsr::a<DataVector, 3>H is type tnsr::A<DataVector, 3>\(L_{ab} = R_{ab} + S_{ba}\)
\(L = 1 - T\)
\(L = U^{a}{}_{a}\)
\(L^b = V_{a}{}^{ba}\)
\(L = G_a H^{a}\)
\(L_{cb} = T G_a G_c U^{a}{}_{b}\)
\(L_a = \frac{G_a}{2}\)
\(L_{ba} = \frac{R_{ab}}{T}\)
\(L = \frac{5}{U^{a}{}_{a} + 1}\)
\(L = \sqrt{T}\)
\(L = \sqrt{G_a H^a}\)
When using the evaluate overload that returns the LHS Tensor, the Symmetry of the LHS Tensor will be deduced from the RHS expression. However, in some cases the deduced LHS symmetry may not be what you want. To specify it yourself, you can pass your LHS Tensor (that has the desired Symmetry) to the evaluate overload that takes the LHS Tensor as the first argument.
For example, if we have \(L_{ab} = R_a R_b\), the indices of \(L\) are symmetric. However, when we do:
the type of L will be tnsr::ab because it is not known at compile time that the two vectors in the product are the same. To override the deduced symmetry and make it a symmetric result, we can create a tnsr::aa and pass it into the other overload:
You can assign a number (e.g. double) to a Tensor of any rank to fill all components with that value, e.g. \(L_{ab} = -1\). How you do that is slightly different depending on the underlying data type of your Tensor:
Tensor's data type is a number type (e.g. double, std::complex<double>):Tensor's data type is a Blaze vector type (e.g. DataVector, ComplexDataVector), the Tensor must first be sized before calling evaluate because there is no sizing information (from a Tensor component) in the RHS expression:See supported number types for the data types that the RHS number can be.
If a Tensor has spacetime indices, you can use generic spatial indices and concrete time indices to refer to a subset of the components, as we see in literature.
Lapse \(\alpha\) computed from the spacetime metric \(g_{ab}\) and shift \(\beta^i\):
\(\alpha = \sqrt{\beta^i g_{it} - g_{tt}}\)
Related to the previous example, you can also use generic spatial indices and concrete time indices for the spacetime indices of the LHS Tensor to assign subsets of the LHS Tensor's components.
Spacetime metric \(g_{ab}\) computed from the lapse \(\alpha\), shift \(\beta^i\), and spatial metric \(\gamma_{ij}\):
\(g_{tt} = -\alpha^2 + \beta^m \beta^n \gamma_{mn}\)
\(g_{ti} = \gamma_{mi} \beta^m\)
\(g_{ij} = \gamma_{ij}\)
You can use the LHS Tensor in the RHS expression to emulate operations like +=, *=, etc. For example, say you would like to emulate the following:
You can emulate the += operation by calling update instead of evaluate:
One limitation is that when using the LHS tensor in the RHS expression, the LHS tensor's index order must be the same on the LHS and RHS. This means that the order of the TensorIndex template parameters of update must match the order of the TensorIndex arguments in the parentheses that come after the LHS Tensor in the RHS expression. For example, the following is not allowed and will yield a runtime error:
For all operations, mathematical legality is checked at compile time. The compiler will catch what is not sound to write on paper, which includes things like no repeated indices, can't divide by a tensor with rank > 0, and that spatial dimensions, frames, valences, index types (spatial or spacetime), and ranks of tensors match where they should.
Here are some examples of illegal math that the compiler will catch:
The RHS expression may contain a mixture of number terms and Tensor terms, e.g. 0.5 * T(ti::a).
Currently supported data types for number terms:
doublestd::complex<double>Currently supported underlying data types for Tensor terms:
doublestd::complex<double>DataVectorComplexDataVectorSupport for more types can be added.
It's possible for terms in the RHS expression to have different data types. The mixture of doubles and Tensor<double>s is shown in the subtraction example and the division examples.
It's also possible for terms to be a mixture of both real-valued and complex-valued numbers or Tensors. For example, we can compute \(z^i = x^i + i y*i\), where \(x\) and \(y\) are real-valued Tensors, i is a std::complex<double>, and \(z\) is a complex-valued Tensor.
In the above example, a std::complex<double> is multiplied by a Tensor<DataVector>, which can be thought to have an "intermediate" type Tensor<ComplexDataVector>, and then a Tensor<DataVector> is added to that intermediate Tensor<ComplexDataVector> to yield the resulting type, Tensor<ComplexDataVector>.
The following table shows the data type that results from performing a binary operation (+, -, *, /) between two terms of given data types:
double | std::complex<double> | Tensor<double> | Tensor<std::complex<double>> | Tensor<DataVector> | ||
|---|---|---|---|---|---|---|
double | double | - | - | - | - | - |
std::complex<double> | std::complex<double> | std::complex<double> | - | - | - | - |
Tensor<double> | Tensor<double> | Tensor<std::complex<double>> | Tensor<double> | - | - | - |
Tensor<std::complex<double>> | Tensor<std::complex<double>> | Tensor<std::complex<double>> | Tensor<std::complex<double>> | Tensor<std::complex<double>> | - | - |
Tensor<DataVector> | Tensor<DataVector> | Tensor<ComplexDataVector>* | Not supported | Not supported | Tensor<DataVector> | - |
Tensor<ComplexDataVector> | Tensor<ComplexDataVector> | Tensor<ComplexDataVector> | Not supported | Not supported | Tensor<ComplexDataVector> | Tensor<ComplexDataVector> |
std::complex<double> and Tensor<DataVector> is multiplication. This is because Blaze does not support addition, subtraction, nor division between std::complex<double> and DataVector.