Index notation for tensors#

AUTHORS:

  • Eric Gourgoulhon, Michal Bejger (2014-2015): initial version

  • Léo Brunswic (2019): add multiple symmetries and multiple contractions

class sage.tensor.modules.tensor_with_indices.TensorWithIndices(tensor, indices)[source]#

Bases: SageObject

Index notation for tensors.

This is a technical class to allow one to write some tensor operations (contractions and symmetrizations) in index notation.

INPUT:

  • tensor – a tensor (or a tensor field)

  • indices – string containing the indices, as single letters; the contravariant indices must be stated first and separated from the covariant indices by the character _

EXAMPLES:

Index representation of tensors on a rank-3 free module:

sage: M = FiniteRankFreeModule(QQ, 3, name='M')
sage: e = M.basis('e')
sage: a = M.tensor((2,0), name='a')
sage: a[:] = [[1,2,3], [4,5,6], [7,8,9]]
sage: b = M.tensor((0,2), name='b')
sage: b[:] = [[-1,2,-3], [-4,5,6], [7,-8,9]]
sage: t = a*b ; t.set_name('t') ; t
Type-(2,2) tensor t on the 3-dimensional vector space M over the
 Rational Field
sage: from sage.tensor.modules.tensor_with_indices import TensorWithIndices
sage: T = TensorWithIndices(t, '^ij_kl') ; T
t^ij_kl
>>> from sage.all import *
>>> M = FiniteRankFreeModule(QQ, Integer(3), name='M')
>>> e = M.basis('e')
>>> a = M.tensor((Integer(2),Integer(0)), name='a')
>>> a[:] = [[Integer(1),Integer(2),Integer(3)], [Integer(4),Integer(5),Integer(6)], [Integer(7),Integer(8),Integer(9)]]
>>> b = M.tensor((Integer(0),Integer(2)), name='b')
>>> b[:] = [[-Integer(1),Integer(2),-Integer(3)], [-Integer(4),Integer(5),Integer(6)], [Integer(7),-Integer(8),Integer(9)]]
>>> t = a*b ; t.set_name('t') ; t
Type-(2,2) tensor t on the 3-dimensional vector space M over the
 Rational Field
>>> from sage.tensor.modules.tensor_with_indices import TensorWithIndices
>>> T = TensorWithIndices(t, '^ij_kl') ; T
t^ij_kl

The TensorWithIndices object is returned by the square bracket operator acting on the tensor and fed with the string specifying the indices:

sage: a['^ij']
a^ij
sage: type(a['^ij'])
<class 'sage.tensor.modules.tensor_with_indices.TensorWithIndices'>
sage: b['_ef']
b_ef
sage: t['^ij_kl']
t^ij_kl
>>> from sage.all import *
>>> a['^ij']
a^ij
>>> type(a['^ij'])
<class 'sage.tensor.modules.tensor_with_indices.TensorWithIndices'>
>>> b['_ef']
b_ef
>>> t['^ij_kl']
t^ij_kl

The symbol ‘^’ may be omitted, since the distinction between covariant and contravariant indices is performed by the index position relative to the symbol ‘_’:

sage: t['ij_kl']
t^ij_kl
>>> from sage.all import *
>>> t['ij_kl']
t^ij_kl

Also, LaTeX notation may be used:

sage: t['^{ij}_{kl}']
t^ij_kl
>>> from sage.all import *
>>> t['^{ij}_{kl}']
t^ij_kl

If some operation is asked in the index notation, the resulting tensor is returned, not a TensorWithIndices object; for instance, for a symmetrization:

sage: s = t['^(ij)_kl'] ; s  # the symmetrization on i,j is indicated by parentheses
Type-(2,2) tensor on the 3-dimensional vector space M over the
 Rational Field
sage: s.symmetries()
symmetry: (0, 1);  no antisymmetry
sage: s == t.symmetrize(0,1)
True
>>> from sage.all import *
>>> s = t['^(ij)_kl'] ; s  # the symmetrization on i,j is indicated by parentheses
Type-(2,2) tensor on the 3-dimensional vector space M over the
 Rational Field
>>> s.symmetries()
symmetry: (0, 1);  no antisymmetry
>>> s == t.symmetrize(Integer(0),Integer(1))
True

The letters denoting the indices can be chosen freely; since they carry no information, they can even be replaced by dots:

sage: t['^(..)_..'] == t.symmetrize(0,1)
True
>>> from sage.all import *
>>> t['^(..)_..'] == t.symmetrize(Integer(0),Integer(1))
True

Similarly, for an antisymmetrization:

sage: s = t['^ij_[kl]'] ; s # the symmetrization on k,l is indicated by square brackets
Type-(2,2) tensor on the 3-dimensional vector space M over the Rational
 Field
sage: s.symmetries()
no symmetry;  antisymmetry: (2, 3)
sage: s == t.antisymmetrize(2,3)
True
>>> from sage.all import *
>>> s = t['^ij_[kl]'] ; s # the symmetrization on k,l is indicated by square brackets
Type-(2,2) tensor on the 3-dimensional vector space M over the Rational
 Field
>>> s.symmetries()
no symmetry;  antisymmetry: (2, 3)
>>> s == t.antisymmetrize(Integer(2),Integer(3))
True

One can also perform multiple symmetrization-antisymmetrizations:

sage: aa = a*a
sage: aa['(..)(..)'] == aa.symmetrize(0,1).symmetrize(2,3)
True
sage: aa == aa['(..)(..)'] + aa['[..][..]'] + aa['(..)[..]'] + aa['[..](..)']
True
>>> from sage.all import *
>>> aa = a*a
>>> aa['(..)(..)'] == aa.symmetrize(Integer(0),Integer(1)).symmetrize(Integer(2),Integer(3))
True
>>> aa == aa['(..)(..)'] + aa['[..][..]'] + aa['(..)[..]'] + aa['[..](..)']
True

Another example of an operation indicated by indices is a contraction:

sage: s = t['^ki_kj'] ; s  # contraction on the repeated index k
Type-(1,1) tensor on the 3-dimensional vector space M over the Rational
 Field
sage: s == t.trace(0,2)
True
>>> from sage.all import *
>>> s = t['^ki_kj'] ; s  # contraction on the repeated index k
Type-(1,1) tensor on the 3-dimensional vector space M over the Rational
 Field
>>> s == t.trace(Integer(0),Integer(2))
True

Indices not involved in the contraction may be replaced by dots:

sage: s == t['^k._k.']
True
>>> from sage.all import *
>>> s == t['^k._k.']
True

The contraction of two tensors is indicated by repeated indices and the * operator:

sage: s = a['^ik'] * b['_kj'] ; s
Type-(1,1) tensor on the 3-dimensional vector space M over the Rational
 Field
sage: s == a.contract(1, b, 0)
True
sage: s = t['^.k_..'] * b['_.k'] ; s
Type-(1,3) tensor on the 3-dimensional vector space M over the Rational
 Field
sage: s == t.contract(1, b, 1)
True
sage: t['^{ik}_{jl}']*b['_{mk}'] == s # LaTeX notation
True
>>> from sage.all import *
>>> s = a['^ik'] * b['_kj'] ; s
Type-(1,1) tensor on the 3-dimensional vector space M over the Rational
 Field
>>> s == a.contract(Integer(1), b, Integer(0))
True
>>> s = t['^.k_..'] * b['_.k'] ; s
Type-(1,3) tensor on the 3-dimensional vector space M over the Rational
 Field
>>> s == t.contract(Integer(1), b, Integer(1))
True
>>> t['^{ik}_{jl}']*b['_{mk}'] == s # LaTeX notation
True

Contraction on two indices:

sage: s = a['^kl'] * b['_kl'] ; s
105
sage: s == (a*b)['^kl_kl']
True
sage: s == (a*b)['_kl^kl']
True
sage: s == a.contract(0,1, b, 0,1)
True
>>> from sage.all import *
>>> s = a['^kl'] * b['_kl'] ; s
105
>>> s == (a*b)['^kl_kl']
True
>>> s == (a*b)['_kl^kl']
True
>>> s == a.contract(Integer(0),Integer(1), b, Integer(0),Integer(1))
True

The square bracket operator acts in a similar way on TensorWithIndices:

sage: b = +a["ij"] ; b._tensor.set_name("b") # create a copy of a["ij"]
sage: b
b^ij
sage: b[:]
[1 2 3]
[4 5 6]
[7 8 9]
sage: b[0,0] == 1
True
sage: b["ji"]
b^ji
sage: b["(ij)"][:]
[1 3 5]
[3 5 7]
[5 7 9]
sage: b["(ij)"] == b["(ij)"]["ij"]
True
>>> from sage.all import *
>>> b = +a["ij"] ; b._tensor.set_name("b") # create a copy of a["ij"]
>>> b
b^ij
>>> b[:]
[1 2 3]
[4 5 6]
[7 8 9]
>>> b[Integer(0),Integer(0)] == Integer(1)
True
>>> b["ji"]
b^ji
>>> b["(ij)"][:]
[1 3 5]
[3 5 7]
[5 7 9]
>>> b["(ij)"] == b["(ij)"]["ij"]
True

However, it keeps track of indices:

sage: b["ij"] = a["ji"]
sage: b[:] == a[:]
False
sage: b[:] == a[:].transpose()
True
>>> from sage.all import *
>>> b["ij"] = a["ji"]
>>> b[:] == a[:]
False
>>> b[:] == a[:].transpose()
True

Arithmetics:

sage: 2*a['^ij']
X^ij
sage: (2*a['^ij'])._tensor == 2*a
True
sage: 2*t['ij_kl']
X^ij_kl
sage: +a['^ij']
+a^ij
sage: +t['ij_kl']
+t^ij_kl
sage: -a['^ij']
-a^ij
sage: -t['ij_kl']
-t^ij_kl
sage: a["^(..)"]["ij"] == 1/2*(a["^ij"] + a["^ji"])
True
>>> from sage.all import *
>>> Integer(2)*a['^ij']
X^ij
>>> (Integer(2)*a['^ij'])._tensor == Integer(2)*a
True
>>> Integer(2)*t['ij_kl']
X^ij_kl
>>> +a['^ij']
+a^ij
>>> +t['ij_kl']
+t^ij_kl
>>> -a['^ij']
-a^ij
>>> -t['ij_kl']
-t^ij_kl
>>> a["^(..)"]["ij"] == Integer(1)/Integer(2)*(a["^ij"] + a["^ji"])
True

The output indices are the ones of the left term of the addition:

sage: a["^(..)"]["ji"] == 1/2*(a["^ij"] + a["^ji"])
False
sage: (a*a)["^..(ij)"]["abij"] == 1/2*((a*a)["^abij"] + (a*a)["^abji"])
True
sage: c = 1/2*((a*a)["^abij"] + (a*a)["^ijab"])
sage: from itertools import product
sage: all(c[i,j,k,l] == c[k,l,i,j] for i,j,k,l in product(range(3),repeat=4))
True
>>> from sage.all import *
>>> a["^(..)"]["ji"] == Integer(1)/Integer(2)*(a["^ij"] + a["^ji"])
False
>>> (a*a)["^..(ij)"]["abij"] == Integer(1)/Integer(2)*((a*a)["^abij"] + (a*a)["^abji"])
True
>>> c = Integer(1)/Integer(2)*((a*a)["^abij"] + (a*a)["^ijab"])
>>> from itertools import product
>>> all(c[i,j,k,l] == c[k,l,i,j] for i,j,k,l in product(range(Integer(3)),repeat=Integer(4)))
True

Non-digit unicode identifier characters are allowed:

sage: a['^μξ']
a^μξ
>>> from sage.all import *
>>> a['^μξ']
a^μξ

Conventions are checked and non acceptable indices raise ValueError, for instance:

sage: a['([..])']  # nested symmetries
Traceback (most recent call last):
...
ValueError: index conventions not satisfied
sage: a['(..']  # unbalanced parenthis
Traceback (most recent call last):
...
ValueError: index conventions not satisfied
sage: a['ii']  # repeated indices of the same type
Traceback (most recent call last):
...
ValueError: index conventions not satisfied: repeated indices of same type
sage: (a*a)['^(ij)^(kl)']  # multiple indices group of the same type
Traceback (most recent call last):
...
ValueError: index conventions not satisfied
sage: a["^\u2663\u2665"]  # non-word-constituent
Traceback (most recent call last):
...
ValueError: index conventions not satisfied
>>> from sage.all import *
>>> a['([..])']  # nested symmetries
Traceback (most recent call last):
...
ValueError: index conventions not satisfied
>>> a['(..']  # unbalanced parenthis
Traceback (most recent call last):
...
ValueError: index conventions not satisfied
>>> a['ii']  # repeated indices of the same type
Traceback (most recent call last):
...
ValueError: index conventions not satisfied: repeated indices of same type
>>> (a*a)['^(ij)^(kl)']  # multiple indices group of the same type
Traceback (most recent call last):
...
ValueError: index conventions not satisfied
>>> a["^\u2663\u2665"]  # non-word-constituent
Traceback (most recent call last):
...
ValueError: index conventions not satisfied
permute_indices(permutation)[source]#

Return a tensor with indices with permuted indices.

INPUT:

  • permutation – permutation that has to be applied to the indices the input should be a list containing the second line of the permutation in Cauchy notation.

OUTPUT:

  • an instance of TensorWithIndices whose indices names and place are those of self but whose components have been permuted with permutation.

EXAMPLES:

sage: M = FiniteRankFreeModule(QQ, 3, name='M')
sage: e = M.basis('e')
sage: a = M.tensor((2,0), name='a')
sage: a[:] = [[1,2,3], [4,5,6], [7,8,9]]
sage: b = M.tensor((2,0), name='b')
sage: b[:] = [[-1,2,-3], [-4,5,6], [7,-8,9]]
sage: identity = [0,1]
sage: transposition = [1,0]
sage: a["ij"].permute_indices(identity) == a["ij"]
True
sage: a["ij"].permute_indices(transposition)[:] == a[:].transpose()
True
sage: cycle = [1,2,3,0] # the cyclic permutation sending 0 to 1
sage: (a*b)[0,1,2,0] == (a*b)["ijkl"].permute_indices(cycle)[1,2,0,0]
True
>>> from sage.all import *
>>> M = FiniteRankFreeModule(QQ, Integer(3), name='M')
>>> e = M.basis('e')
>>> a = M.tensor((Integer(2),Integer(0)), name='a')
>>> a[:] = [[Integer(1),Integer(2),Integer(3)], [Integer(4),Integer(5),Integer(6)], [Integer(7),Integer(8),Integer(9)]]
>>> b = M.tensor((Integer(2),Integer(0)), name='b')
>>> b[:] = [[-Integer(1),Integer(2),-Integer(3)], [-Integer(4),Integer(5),Integer(6)], [Integer(7),-Integer(8),Integer(9)]]
>>> identity = [Integer(0),Integer(1)]
>>> transposition = [Integer(1),Integer(0)]
>>> a["ij"].permute_indices(identity) == a["ij"]
True
>>> a["ij"].permute_indices(transposition)[:] == a[:].transpose()
True
>>> cycle = [Integer(1),Integer(2),Integer(3),Integer(0)] # the cyclic permutation sending 0 to 1
>>> (a*b)[Integer(0),Integer(1),Integer(2),Integer(0)] == (a*b)["ijkl"].permute_indices(cycle)[Integer(1),Integer(2),Integer(0),Integer(0)]
True
update()[source]#

Return the tensor contains in self if it differs from that used for creating self, otherwise return self.

EXAMPLES:

sage: from sage.tensor.modules.tensor_with_indices import TensorWithIndices
sage: M = FiniteRankFreeModule(QQ, 3, name='M')
sage: e = M.basis('e')
sage: a = M.tensor((1,1),  name='a')
sage: a[:] = [[1,-2,3], [-4,5,-6], [7,-8,9]]
sage: a_ind = TensorWithIndices(a, 'i_j') ; a_ind
a^i_j
sage: a_ind.update()
a^i_j
sage: a_ind.update() is a_ind
True
sage: a_ind = TensorWithIndices(a, 'k_k') ; a_ind
scalar
sage: a_ind.update()
15
>>> from sage.all import *
>>> from sage.tensor.modules.tensor_with_indices import TensorWithIndices
>>> M = FiniteRankFreeModule(QQ, Integer(3), name='M')
>>> e = M.basis('e')
>>> a = M.tensor((Integer(1),Integer(1)),  name='a')
>>> a[:] = [[Integer(1),-Integer(2),Integer(3)], [-Integer(4),Integer(5),-Integer(6)], [Integer(7),-Integer(8),Integer(9)]]
>>> a_ind = TensorWithIndices(a, 'i_j') ; a_ind
a^i_j
>>> a_ind.update()
a^i_j
>>> a_ind.update() is a_ind
True
>>> a_ind = TensorWithIndices(a, 'k_k') ; a_ind
scalar
>>> a_ind.update()
15