Working with Tensors


The Basics

The variable type tensor is introduced with CLUScript v2.2 and implemented in CLUCalc v4.2. In the long run this tensor type will probably replace the matrix type since it is much more general and more convenient to work with. For the moment, both types exist in parallel, though.

In the following I mean by the valence of a tensor, its number of indices. This is often also refered to as the rank of a tensor. However, I would like to reserve to term rank for the meaning it has for matrices.

A tensor variable is basically a multi-dimensional array of scalar values. The nice feature of working with tensor variables is that implicit summations over indices can be expressed rather succinctly, as will be shown in section Implicit Loops.

Defining Tensors

A tensor of valence three with dimensions 2, 3 and 4 is created in the following way.

    ?T = Tensor(3, [2,3,4]);

This generates the following output

T (2x3x4) =
0000
0000
0000

0000
0000
0000

It is also possible to create a tensor directly from a list or from a matrix.

?T1 = Tensor([[[1,2,3],[2,3,4]], [[4,5,6], [5,6,7]]]);
?T2 = Tensor(Matrix(2,3));

This generates the output

T1 (2x2x3) =
123
234

456
567

T2 (2x3) =
000
000

Accessing Components

The components of a tensor can be accessed by using round brackets, just as for matrices. For example,

T = Tensor([[[1,2,3],[2,3,4]], [[4,5,6], [5,6,7]]]);
?T(2,1,3);
T(1,1,1) = 10;
?T;

has the output

Constant = 6

T (2x2x3) =
1023
234

456
567

Basic Operations

The operators +, -, * and / are defined between tensor variables and scalars. Operations between tensors are explained in Implicit Loops. For example,

    ?T = Tensor([[1,2,3],[4,5,6]]);
    ?"T + 1 = " + (T + 1);
    ?"1 - T = " + (1 - T);
    ?"T * 2 = " + (T * 2);
    ?"T / 2 = " + (T / 2);
    ?"2 / T = " + (2 / T);

The output is

T (2x3)=
123
456

T + 1 =
234
567

1 - T =
0-1-2
-3-4-5

T * 2 =
246
81012

T / 2 =
0.511.5
22.53

2 / T =
210.667
0.50.40.333


Implicit Loops

In order to make full use of tensor variables, you need to know about the generation of implicit loops by using counting indices. There are two different types of counting indices: contraction- and point-indices. Contraction-indices are negative integer values, while point-indices are values between -1 and 0. Contraction indices have the effect of generating a summation loop over the range of the respective index as is explained in Tensor Contraction. Point-indices on the other hand, imply a loop over all values of an index, but there is no implicit summation. This is explained in more detail in Point Indices.

Printing Tensor Components

All components of a tensor are printed to the output window by using the standard ? operator, as shown before. However, it is also possible to only print a subset of elements by using a counting index. Consider the following example.

    ?T = Tensor([[1,2,3],[4,5,6]]);
    ?T(1, -1);
    ?T(-1, 1);

The output is

T (2x3)=
123
456

Constant (3)=
123

Constant (2)=
14

That is, the expression ?T(1, -1) prints the values of T in the first row, while ?T(-1, 1) prints the first column. Basically, the idea of counting indices is that when an operation is applied to an expression like T(-1, 1), then it is applied to the set of elements that are contained in T when counting through all allowed values of the first index.

Assigning Tensor Values

It is also possible to assign values only to a particular part of a tensor. For example,

    A = Tensor(2, [2,3]);
    A(1, -1) = 2;
    ?A;

gives

A (2x3) =
222
000

That is, all elements in the first row of A are set to the value 2. Only setting the diagonal values can be done in the following way

    A = Tensor(2, [2,3]);
    A(-1, -1) = 2;
    ?A;

which gives

A (2x3) =
200
020

One can also assign parts of one tensor to parts of another tensor.

    A = Tensor([[1,2], [3,4]]);
    B = Tensor([[2,3], [1,4]]);

    A(-1,1) = B(2,-1);
    ?A;
    
    ?C = A(1,-1);

This generates the following output.

A (2x2) =
12
44


C (2) =
12

Tensor Contraction

The following example evaluates the standard matrix product between two 2-valence tensors (i.e. matrices). The equivalent mathematical expressions are

\[ A = \left(\begin{array}{cc} 1 & 2 \\ 3 & 4 \end{array}\right), \quad B = \left(\begin{array}{cc} 2 & 3 \\ 1 & 4 \end{array}\right), \quad C_{i,k} = \sum_{j=1}^2\;A_{i,j}\,B_{j,k}. \]

    ?A = Tensor([[1,2], [3,4]]);
    ?B = Tensor([[2,3], [1,4]]);

    ?C = A(-1,-2) * B(-2, -3);

This results in

A (2x2) =
12
34

B (2x2) =
23
14

C (2x2) =
411
1025

Hence, there is an implicit summation over the index values at the positions where a repeated negative appears. It may also help to define some integer variables to simplify the CLUScript expression. For example,

    i = -1; j = -2; k = -3;
    
    A = Tensor([[1,2], [3,4]]);
    B = Tensor([[2,3], [1,4]]);

    C = A(i, j) * B(j, k);

This has the same effect as the previous example, since there will be a contraction over the index j. Note that the order of the elements in C is according to the values of the counting indices. That is, in the above example -1 is the first dimension of C and -3 the second dimension. Hence, D = A(k, j) * B(j, i) generates a tensor that is transposed with respect to tensor C.

Note that instead of contracting with a product, this can also be done with division. For example, the equivalent to the mathematical expressions

\[ A = \left(\begin{array}{cc} 1 & 2 \\ 3 & 4 \end{array}\right), \quad B = \left(\begin{array}{cc} 2 & 3 \\ 1 & 4 \end{array}\right), \quad C_{i,k} = \sum_{j=1}^2\;\frac{A_{i,j}}{B_{j,k}}, \]

is the following CLUScript.

    ?A = Tensor([[1,2], [3,4]]);
    ?B = Tensor([[2,3], [1,4]]);

    ?C = A(-1,-2) / B(-2, -3);

The output is

A (2x2)=
12
34

B (2x2)=
23
14

C (2x2)=
2.50.833
5.52

Point Indices

Point-indices denote indices over which implicit loops are to be executed, however without summation over the index range. For example, suppose you have two tensors of the same size and you would simply like to multiply the corresponding values. Mathematically this may be expressed as

\[ A = \left(\begin{array}{cc} 1 & 2 \\ 3 & 4 \end{array}\right), \quad B = \left(\begin{array}{cc} 2 & 3 \\ 1 & 4 \end{array}\right), \quad C_{i,j} = \;A_{i,j}\,B_{i,j}. \]

The corresponding CLUScript looks like this

?A = Tensor([[1,2], [3,4]]);
?B = Tensor([[2,3], [1,4]]);

?C = A(-.1,-.2) * B(-.1, -.2);

which generates the output

A (2x2) =
12
34

B (2x2) =
23
14

C (2x2) =
26
316

It is also possible to mix contraction- and point-indices. For example, suppose you would like to evaluate the scalar product between corresponding column vectors in two tensors. Mathematically this can be written as

\[ A = \left(\begin{array}{cc} 1 & 2 \\ 3 & 4 \end{array}\right), \quad B = \left(\begin{array}{cc} 2 & 3 \\ 1 & 4 \end{array}\right), \quad C_{j} = \sum_{i=1}^2 \;A_{i,j}\,B_{i,j}. \]

The corresponding CLUScript looks like this.

?A = Tensor([[1,2], [3,4]]);
?B = Tensor([[2,3], [1,4]]);

?C = A(-1,-.2) * B(-1, -.2);

which generates the output

A (2x2) =
12
34

B (2x2) =
23
14

C (2) =
522

Restricting Index Ranges

So far all counting indices were always evaluated over their whole allowed range. However, quite often it is desirable to restrain a counting index to a subset of index values. In CLUScript this can be done in two different ways, by either giving a range or a list of index values.

    ?A = Tensor([[1,2,3,4], [5,6,7,8]]);

    ?A(-1, [-2,1,3]); // Range for -2
    ?A(-1, [-2, [1,3,4]]); // Index List

This generates the output

A (2x4) =
1234
5678

Constant (2x3) =
123
567

Constant (2x3) =
134
578

Addition and Subtraction

Implicit loops can also be perfomed when adding and subtracting tensors, as well as adding or subtracting constants to/from tensors. Here is an example,

    ?T = Tensor([[2,0,0], [0,1,0]]);
    ?S1 = T - 1;
    ?S2 = 1 - T;

which generates the output

T (2x3)=
200
010

S1 (2x3)=
1-1-1
-10-1

S2 (2x3)=
-111
101

That is, subtracting a constant from a tensor, subtracts it from each component of the tensor. This, of course, also works when using counting indices. Here is an example where a tensor is subtracted from its transpose.

    ?T = Tensor([[1,2], [3,4]]);
    ?S = T(-2,-1) - T(-1,-2);

The output is

T (2x2)=
12
34

S (2x2)=
01
-10


Representing GA Operations

The inner, outer and geometric product can all be represented as bilinear functions, while operations like the reverse or dual can be represented as linear functions. In section Introduction it is shown why this is case. The basic idea is to write the components of multivectors as vectors. A product of two multivectors can then be written as the contraction of their vector representations with a tensor. For example, let the components of multivector A and B be given by $\alpha^i$ and $\beta^i$ , respectively. Then the geometric product of A and B can be written as

\[ \gamma^k = \sum_i\,\sum_j\;\alpha^i\,\beta^j\;{g^k}_{ij}, \]

where the $\gamma^k$ denote the components of the resultant multivector, and ${g^k}_{ij}$ denotes the components of the tensor encoding the geometric product. This translation of geometric algebra operations into tensor contractions allows the application of linear algebra solution methods to geometric algebra.

Note that equations of the above type could also be evaluated in CLUScript before by using the function GetMVProductMatrix(). However, using this function the product tensor ${g^k}_{ij}$ could not be obtained by itself, but only its contraction with a vector, which results in a matrix. If we want to treat more complex equation using tensor, we need to be able to deal with the product tensors themselves.

The above calculation can be performed in CLUScript using tensors in the following way.

    vA = VecE3(1,2,0);
    vB = VecE3(2,0,3);

    // Transformation of multivector into tensor
    tA = MV2Tensor(vA); // Column vector
    tB = MV2Tensor(vB); // Column vector
    
    // Print first column as row
    ?tA(-1,1);
    ?tB(-1,1);
    
    // Generate geometric product tensor for
    // 3D-Euclidean space
    tG = GAOpTensor(GA_E3, MVOP_GEO);
    ?"Size of tG = " + Str(Size(tG));
    
    // Contraction of vectors with tensor
    ?tC = tA(-1,1) * tB(-2,1) * tG(-3,-1,-2);
    
    // Transformation of tensor into multivector
    ?vC = Tensor2MV(tC);
    ?vA * vB;

This generates the output

Constant (8) =
01200000

Constant (8) =
02030000

Size of tG = [ 8 , 8 , 8 ]
tC (8) =
20006-3-40

vC (1) =
[ 2 + 6 e23 + -3 e31 + -4 e12 ]


Constant = 2 + 6 e23 + -3 e31 + -4 e12

The variables tA and tB are tensors of dimensions 8x1, i.e. they are column vectors. The tensor tG has dimension 8x8x8. The contraction of the vectors with the tensor is equivalent to the geometric product of the corresponding multivectors. It is also possible to pass a list of vectors to MV2Tensor(), in which case a tensor is created that contains the components of the multivectors in its columns. The following script generates the same output as the previous one.

// Create list of multivectors
lVec = [];
lVec << VecE3(1,2,0);
lVec << VecE3(2,0,3);

// Transformation of multivectors into tensor.
// The multivectors form the columns of the tensor.
tVec = MV2Tensor(lVec); 

// Print columns separately as rows
?tVec(-1,1);
?tVec(-1,2);

// Generate geometric product tensor for
// 3D-Euclidean space
tG = GAOpTensor(GA_E3, MVOP_GEO);
?"Size of tG = " + Str(Size(tG));

// Contraction of vectors with tensor
?tC = tVec(-1,1) * tVec(-2,2) * tG(-3,-1,-2);

// Transformation of tensor into multivector
?vC = Tensor2MV(tC);
// Geometric product of all vectors in list lVec.
?prod(lVec);

Masking

Typically only a few components of the multivetors we are working with are non-zero. In the above example, the initial multivectors are 3D-vectors, but they are still represented by 8-dimensional vectors in the tensor form. In projective and conformal space the ratio of used components to unused components is usually even much higher. In order to reduce the numerical complexity, it is therefore possible to define masks that define which components of a multivector are mapped into the tensor form. Again this is best explained with an example.

    // Create list of multivectors
    lVec = [];
    lVec << VecE3(1,2,0);
    lVec << VecE3(2,0,3);

    // Mask for vectors
    lVecMask = 
        [ 0, // Scalar
        1, 2, 3, // Vector
        0, 0, 0, // Bivector
        0 // Trivector
        ];
            
    // Transformation of multivectors into tensor.
    // The multivectors form the columns of the tensor.
    tVec = MV2Tensor(lVec, lVecMask); 

    // Print columns separately as rows
    // There are only three components now
    ?tVec(-1,1);
    ?tVec(-1,2);

    // Generate geometric product tensor for
    // 3D-Euclidean space using mask.
    lResMask = 0;
    tG = GAOpTensor(GA_E3, MVOP_GEO, lVecMask, lVecMask, lResMask);
    ?"Size of tG = " + Str(Size(tG));
    ?"Result Mask = " + Str(lResMask);

    // Contraction of vectors with tensor
    ?tC = tVec(-1,1) * tVec(-2,2) * tG(-3,-1,-2);

    // Transformation of tensor into multivector
    ?vC = Tensor2MV(tC, lResMask);

    // Geometric product of all elements in lVec
    ?prod(lVec);

This generates the output

Constant (3)=
120

Constant (3)=
203

Size of tG = [ 4 , 3 , 3 ]
Result Mask = [ 1 , 0 , 0 , 0 , 2 , 3 , 4 , 0 ]
tC (4)=
26-3-4

vC (1)=
[ 2 + 6 e23 + -3 e31 + -4 e12 ]


Constant = 2 + 6 e23 + -3 e31 + -4 e12

The variable lVecMask is a list with eight entries, relating to the eight dimensions of the Geometric Algebra of 3D-Euclidean space. In the above example lVecMask is defined such that the entries relating to the vector components are given consecutive non-zero values. The tensors generated from the multivectors by using this mask, then only contain the three vector components.

The tensor representing the geometric product has to have different dimensions now as well. Therefore, the mask used to generate the tensor tVec has to be specified in the function GAOpTensor(). This function takes as last parameter a variable that either already contains a result mask, or in which the resultant mask will be stored. In the above example lResMask is set to zero, which tells the function GAOpTensor() that it should set this variable to the resultant mask. The resultant mask depends on the two other masks given. It basically describes, which components in a multivector can at most be non-zero under the given operation. In the above example the two mask lVecMask specifies that the left and right operands of the geometric products will be vectors. It then follows that the result of the geometric product will generate a multivector that has at most a scalar and three bivector components. Hence, the result mask in lResMask.

The geometric product is then executed by contraction of the two vectors in tVec with the geometric product tensor tG. Note that tG is now of dimensions 4x3x3. In order to transform the resultant tensor tC back into a multivector the resultant mask lResMask has to be given in the function Tensor2MV().

If we were only interested in a particular subset of components of the resultant multivector, we could have also specified lResMask accordingly before a call to GAOpTensor().