The goal of {ricci} is to provide a compact1 R interface for performing tensor calculations. This is achieved by labeling (upper and lower) index slots of R’s array
and making use of Ricci calculus conventions to implicitly trigger contractions and diagonal subsetting. Explicit tensor operations, such as addition, subtraction, multiplication of tensors via the standard operators (*
, +
, -
), raising and lowering indices, taking symmetric or antisymmetric tensor parts, as well as the Kronecker product are available. Common tensors like the Kronecker delta, Levi Civita epsilon, and certain metric tensors are provided. An effort was made to provide the user with meaningful error messages.
{ricci} uses the calculus package (Guidotti 2022) behind the scenes to perform calculations and provides an alternative interface to a subset of its functionality. Notably, {calculus} also supports symbolic calculations which also enables {ricci} to do the same.
Installation
You can install the development version of ricci from GitHub with:
# install.packages("pak")
pak::pak("lschneiderbauer/ricci")
Examples
The central object is R’s array
. Adding index slot labels allows us to perform common tensor operations implicitly. After the desired calculations have been carried out we can remove the labels to obtain an ordinary array
.
The following example shows how to express the contraction of two tensors, where one index has to be raised, and subsequent diagonal subsetting. For demonstration purposes we use an arbitrary array of rank 3.
library(ricci)
# numeric data
a <- array(1:(2^3), dim = c(2,2,2))
# create labeled array (tensor)
(a %_% .(i, j, k) *
# mutliply with a labeled array (tensor) and raise index i and k
a %_% .(i, l, k) |> r(i, k, g = g_mink(2))) |>
# * -i and +i as well as -k and +k dimension are implictely contracted
# the result is a tensor of rank 2
sym(j, l) |> # symmetrize over i and l
subst(l -> j) |> # rename index and trigger diagonal subsetting
as_a(j) # we unlabel the tensor with index order (j)
#> [1] 8 8
The same instructions work for a symbolic array:
library(ricci)
# symbolic data
a <- array(paste0("a", 1:(2^3)), dim = c(2,2,2))
(a %_% .(i, j, k) *
# mutliply with a labeled array (tensor) and raise index i and k
a %_% .(i, l, k) |> r(i, k, g = g_mink(2))) |>
# * -i and +i as well as -k and +k dimension are implictely contracted
# the result is a tensor of rank 2
sym(j, l) |> # symmetrize over i and l
subst(l -> j) |> # rename index and trigger diagonal subsetting
as_a(j) # we unlabel the tensor with index order (j)
#> [1] "((a1) * (((a1) * -1) * -1) + (a2) * (((a2) * 1) * -1) + (a5) * (((a5) * -1) * 1) + (a6) * (((a6) * 1) * 1) + (a1) * (((a1) * -1) * -1) + (a2) * (((a2) * 1) * -1) + (a5) * (((a5) * -1) * 1) + (a6) * (((a6) * 1) * 1)) / 2"
#> [2] "((a3) * (((a3) * -1) * -1) + (a4) * (((a4) * 1) * -1) + (a7) * (((a7) * -1) * 1) + (a8) * (((a8) * 1) * 1) + (a3) * (((a3) * -1) * -1) + (a4) * (((a4) * 1) * -1) + (a7) * (((a7) * -1) * 1) + (a8) * (((a8) * 1) * 1)) / 2"
Below we outline more details on possible individual operations.
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:
By default, indices are assumed to be lower indices. We can use a “+” prefix to create an upper index label.
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.
Diagonal subsetting
Repeated labels on the same position (upper or lower) will trigger diagonal subsetting.
Tensor multiplication w/ contractions and subsetting
f <- a %_% .(i, j, k) * a %_% .(+i, j, +k)
f
#> <Labeled Array> [2] .(-j)
#> [1] "(a1) * (a1) + (a2) * (a2) + (a5) * (a5) + (a6) * (a6)"
#> [2] "(a3) * (a3) + (a4) * (a4) + (a7) * (a7) + (a8) * (a8)"
# retrieve array
f |> as_a(j)
#> [1] "(a1) * (a1) + (a2) * (a2) + (a5) * (a5) + (a6) * (a6)"
#> [2] "(a3) * (a3) + (a4) * (a4) + (a7) * (a7) + (a8) * (a8)"
Tensor addition
Tensor addition is taking care of correct index slot matching (by index labels), so the position of the index does not matter.
g <- a %_% .(i, j, k) + a %_% .(j, i, k)
g
#> <Labeled Array> [2x2x2] .(-i, -j, -k)
g |> as_a(i, j, k)
#> , , 1
#>
#> [,1] [,2]
#> [1,] "a1 + a1" "a3 + a2"
#> [2,] "a2 + a3" "a4 + a4"
#>
#> , , 2
#>
#> [,1] [,2]
#> [1,] "a5 + a5" "a7 + a6"
#> [2,] "a6 + a7" "a8 + a8"