ricci

{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” arrays, 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

  1. Create labeled arrays from arrays.

  2. Perform calculations making use of Ricci calculus conventions.

  3. Unlabel results to obtain normal array() objects.

Creating a labeled array (tensor)

library(ricci)

a <- array(1:(2^3), dim = c(2, 2, 2))

We can use the array a to create a labeled array (tensor) with lower index labels i, j, and k:

\[ a_{ijk} \]

a %_% .(i, j, k)
#> <Labeled Array> [2x2x2] .(-i, -j, -k)

By default, indices are assumed to be lower indices. We can use a “+” prefix to create an upper index label.

\[ a_{ij}^{\;\;k} \]

a %_% .(i, j, +k)
#> <Labeled Array> [2x2x2] .(-i, -j, +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} \]

b <- a %_% .(i, +i, k)
b
#> <Labeled Array> [2] .(-k)
#> [1]  5 13

Diagonal subsetting

Repeated labels on the same position (upper or lower) will trigger diagonal subsetting.

\[ c_{ik}=a_{iik} \]

c <- a %_% .(i, i, k)
c
#> <Labeled Array> [2x2] .(-i, -k)
#>      [,1] [,2]
#> [1,]    1    5
#> [2,]    4    8

Outer product

The same conventions apply for arbitrary tensor multiplication.

\[ d_{ijklmn}=a_{ijk}a_{lmn} \]

d <- a %_% .(i, j, k) * a %_% .(l, m, n)
d
#> <Labeled Array> [2x2x2x2x2x2] .(-i, -j, -k, -l, -m, -n)

Inner product

\[ e=a_{ijk}a^{ijk} \]

e <- a %_% .(i, j, k) * a %_% .(+i, +j, +k)
e
#> <Scalar>
#> [1] 204

Mixed product: tensor multiplication w/ contractions and subsetting

\[ f_j=a_{ijk}a^{i\;k}_{\;j} \]

f <- a %_% .(i, j, k) * a %_% .(+i, j, +k)
f
#> <Labeled Array> [2] .(-j)
#> [1]  66 138

Element-wise product

\[ g_{ijk}=a_{ijk}a_{ijk} \]

g <- a %_% .(i, j, k) * a %_% .(i, j, k)
g
#> <Labeled Array> [2x2x2] .(-i, -j, -k)

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.

(a %_% .(i, j, k) * a %_% .(l, m, n)) |>
  kron(.(i, l) -> r, .(j, m) -> p, .(k, n) -> q)
#> <Labeled Array> [4x4x4] .(-q, -p, -r)

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} \]

h <- a %_% .(i, j, k) + a %_% .(j, i, k)
h
#> <Labeled Array> [2x2x2] .(-i, -j, -k)

(Anti-)symmetrization

Taking the symmetric or antisymmetric part w.r.t. certain indices is a standard tool in Ricci calculus.

a %_% .(i, j, k) |> sym(i, j)
#> <Labeled Array> [2x2x2] .(-i, -j, -k)
a %_% .(i, j, k) |> sym(i, j, k)
#> <Labeled Array> [2x2x2] .(-i, -j, -k)

a %_% .(i, j, k) |> asym(i, j)
#> <Labeled Array> [2x2x2] .(-i, -j, -k)
a %_% .(i, j, k) |> asym(i, j, k)
#> <Labeled Array> [2x2x2] .(-i, -j, -k)

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

as.array(g, .(i, j, k))
#> , , 1
#> 
#>      [,1] [,2]
#> [1,]    1    9
#> [2,]    4   16
#> 
#> , , 2
#> 
#>      [,1] [,2]
#> [1,]   25   49
#> [2,]   36   64