Tensors
Tensors are objects that transform in specific ways under coordinate changes. They are categorized by how they transform, so an (m, n)-tensor follows m contravariant and n covariant transformations. The order of a tensor is given by m + n.
The simplest tensor is a (0, 0)-tensor, a scalar. A (1, 0)-tensor is also known as a vector, and a (0, 1)-tensor is known as a covector or one-form. TensorFlux uses Julia's adjoint to specify the variance of an index.
julia> v = Tensor([1, 2]) # A vector
(1, 0)-Tensor:
[1, 2]
(:contra,)
julia> ω = Tensor([-3, 1]') # A covector
(0, 1)-Tensor:
[-3, 1]
(:co,)
The Tensor type stores the variance of each index, as well as the components.
julia> v.data
2-element Vector{Int64}:
1
2
julia> ω.variance
(:co,)
The next step up are order 2 tensors. A (1, 1)-tensor is also known as a linear map. An example of a (0, 2)- and a (2, 0)-tensor is the metric and inverse metric.
julia> L = Tensor([[4, -2]', [1, 1]']) # A linear map
(1, 1)-Tensor:
[4 -2; 1 1]
(:contra, :co)
julia> g = Tensor([[2, 1]', [1, 2]']') # A (0, 2)-tensor
(0, 2)-Tensor:
[2 1; 1 2]
(:co, :co)
This pattern continues to tensors of higher order.
Indexing
Tensors need to be indexed for operations. Indexing a Tensor returns an IndexedTensor type, and either symbolic or integer indicies can be used.
Indexing follows the convention that contravariant indices are written on top (first), and covariant indices are written on the bottom (second). This distinction is arbitrary for a tensor of constant variance. Thus any (k, 0)- or (0, k)-tensor can be indexed as
julia> T[i1, i2...ik]
While a (k, l)-tensor is indexed as
julia> T[i1, i2...ik][j1, j2...jl]
Indexing with pure integers will return a scalar.
julia> v = Tensor([1, 2])
julia> v[1] # Pure integer (1, 0)-tensor
1
julia> L = Tensor([[4, -2]', [1, 1]'])
julia> L[1][2] # Pure integer (1, 1)-tensor
-2
Indexing with pure symbols or a mix of integers and symbols will return an indexed tensor. In the mixed case, the data will be spliced.
julia> v[:i] # Pure symbolic (1, 0)-tensor
(1, 0)-Tensor:
[1, 2]
(:contra,)
(:i,), ()
julia> L[:i][:j] # Pure symbolic (1, 1)-tensor
(1, 1)-Tensor:
[4 -2; 1 1]
(:contra, :co)
(:i,), (:j,)
julia> L[2][:k] # Mixed (1, 1)-tensor
(0, 1)-Tensor:
[1, 1]
(:co,)
(), (:k,)
The same convention of indexing applies to other objects with slight modifications.
Operations
A fundamental operation with tensors is the tensor product. It takes an (m, n)-tensor and a (p, q) tensor and returns an (m + p, n + q)-tensor.
julia> v = Tensor([2, -1])
julia> w = Tensor([3, 4])
julia> α = Tensor([2, 3]')
julia> L = v ⊗ w # Tensor product of two vectors
(2, 0)-Tensor:
[6 8; -3 -4]
(:contra, :contra)
julia> L ⊗ α ⊗ v # Tensor product of a (2, 0)-tensor, a covector, and a vector
(3, 1)-Tensor:
[24 32; -12 -16;;; 36 48; -18 -24;;;; -12 -16; 6 8;;; -18 -24; 9 12]
(:contra, :contra, :co, :contra)
Another fundamental operation is contraction. Contraction takes a linear combination along the repeated indices, and is the extension of matrix-vector multiplication. Contraction requires that, in each pair, one index is contravariant and the other is covariant.
julia> α[:i] * v[:i] # A covector contracted with a vector
1
julia> A = v ⊗ α # A linear map
julia> A[:i][:j] * v[:j] # A linear map contracted with a vector
(1, 0)-Tensor:
[2, -1]
(:contra,)
(:i,), ()
julia> L[:i, :j] * A[:k][:j] # A (2, 0)-tensor contracted with a linear map
(2, 0)-Tensor:
[72 -36; -36 18]
(:contra, :contra)
(:i, :k), ()
Contraction also allows you to take the trace of a tensor across a pair of indices.
julia> A[:i][:i]
1
Scaling is applied across all elements.
julia> 2 * L[:i, :j] # Scaling
(2, 0)-Tensor:
[12 16; -6 -8]
(:contra, :contra)
(:i, :j), ()
Addition and subtraction are element-wise.
julia> M = Tensor([[4, 2], [-1, 1]])
julia> L[:i, :j] + M[:i, :j] # Addition
(2, 0)-Tensor:
[10 10; -4 -3]
(:contra, :contra)
(:i, :j), ()
julia> L[:i, :j] - M[:i, :j] # Subtraction
(2, 0)-Tensor:
[2 6; -2 -5]
(:contra, :contra)
(:i, :j), ()