{ricci} uses nonstandard evaluation to specify tensor indices (see
.()
). The main purpose of using nonstandard evaluation is
to safe a lot of quotation marks which otherwise would clutter the code
(i
instead of "i"
). Using nonstandard
evaluation also allows to specify some operations in more direct way
(e.g. substitution is specified as subst(x, i -> f)
is
short for substitute index i
with f
).
A key point about specifying the index labels is that we can make use
of Ricci
calculus conventions and pack a bunch of different operations into
one operator (*
). There is no separate “inner product”,
“outer product”, “dot product”, “Kronecker product” nor “element-wise
product”. All products are represented by a single product
*
. The arrangement of the indices determines what product
is carried out, as is in the spirit of Ricci
calculus.
In this package we are using the terms “labeled array” and “tensor”
synonymously. While in literature the word “tensor” is sometimes used to
specifically refer to a “tensor field” we do not make that
identification, and a tensor can exist without an underlying
differential manifold. To model tensor fields one can make use
of “symbolic” array
s, i.e. arrays that do not contain
numerical values, but strings that contain mathematical expressions
(like "sin(x)"
for sin(x),
where x
can be interpreted as a manifold coordinate), see
vignette("tensor_fields", package = "ricci")
for more
information.
A typical workflow when working with {ricci} is
Create labeled arrays from arrays.
Perform calculations making use of Ricci calculus conventions.
Unlabel results to obtain normal
array()
objects.
Creating a labeled array (tensor)
We can use the array a
to create a labeled array
(tensor) with lower index labels i, j, and k:
a_{ijk}
By default, indices are assumed to be lower indices. We can use a “+” prefix to create an upper index label.
a_{ij}^{\;\;k}
Performing calculations
Creating index labels on its own is not very interesting nor helpful. The act of labeling tensor index slots becomes useful when the labels are set such that they trigger implicit calculations, or they are combined with other tensors via multiplication or addition.
Contraction
Repeated index labels with opposite position are implicitly contracted.
b_k=a_{i\;k}^{\;i}
Diagonal subsetting
Repeated labels on the same position (upper or lower) will trigger diagonal subsetting.
c_{ik}=a_{iik}
Outer product
The same conventions apply for arbitrary tensor multiplication.
d_{ijklmn}=a_{ijk}a_{lmn}
Kronecker product
A Kronecker product is simply a tensor product whose underlying vector space basis is relabeled. In the present context this is realized by combining multiple index labels into one. The associated dimension to the new label is then simply the product of the dimensions associated to the old index labels respectively.
Addition and Substraction
Tensor addition or subtraction is taking care of correct index slot matching (by index labels), so the position of the index does not matter.
h_{ijk} = a_{ijk} + a_{jik}
Unlabel a tensor
After we are done with our calculations we usually want to retrieve
an unlabeled array again to use the result elsewhere and get on with
life. In contrast to a labeled array (tensor) of this package an R
array
has a well-defined dimension ordering and so when
stripping labels of a tensor one has to specify an index order.
g |> as_a(i, j, k)
#> , , 1
#>
#> [,1] [,2]
#> [1,] 1 9
#> [2,] 4 16
#>
#> , , 2
#>
#> [,1] [,2]
#> [1,] 25 49
#> [2,] 36 64
The same works with the standard generic as.array()
.
However, to avoid nonstandard evaluation in its S3 method, we wrap
indices using .()
.