Tensor

class diofant.tensor.tensor._TensorManager[source]

Class to manage tensor properties.

Notes

Tensors belong to tensor commutation groups; each group has a label comm; there are predefined labels:

0 tensors commuting with any other tensor

1 tensors anticommuting among themselves

2 tensors not commuting, apart with those with comm=0

Other groups can be defined using set_comm; tensors in those groups commute with those with comm=0; by default they do not commute with any other group.

clear()[source]

Clear the TensorManager.

comm_i2symbol(i)[source]

Returns the symbol corresponding to the commutation group number.

comm_symbols2i(i)[source]

get the commutation group number corresponding to i

i can be a symbol or a number or a string

If i is not already defined its commutation group number is set.

get_comm(i, j)[source]

Return the commutation parameter for commutation group numbers i, j

see _TensorManager.set_comm

set_comm(i, j, c)[source]

set the commutation parameter c for commutation groups i, j

Parameters:
  • i, j (symbols representing commutation groups)
  • c (group commutation number)

Notes

i, j can be symbols, strings or numbers, apart from 0, 1 and 2 which are reserved respectively for commuting, anticommuting tensors and tensors not commuting with any other group apart with the commuting tensors. For the remaining cases, use this method to set the commutation rules; by default c=None.

The group commutation number c is assigned in correspondence to the group commutation symbols; it can be

0 commuting

1 anticommuting

None no commutation property

Examples

G and GH do not commute with themselves and commute with each other; A is commuting.

>>> Lorentz = TensorIndexType('Lorentz')
>>> i0, i1, i2, i3, i4 = tensor_indices('i0:5', Lorentz)
>>> A = tensorhead('A', [Lorentz], [[1]])
>>> G = tensorhead('G', [Lorentz], [[1]], 'Gcomm')
>>> GH = tensorhead('GH', [Lorentz], [[1]], 'GHcomm')
>>> TensorManager.set_comm('Gcomm', 'GHcomm', 0)
>>> (GH(i1)*G(i0)).canon_bp()
G(i0)*GH(i1)
>>> (G(i1)*G(i0)).canon_bp()
G(i1)*G(i0)
>>> (G(i1)*A(i0)).canon_bp()
A(i0)*G(i1)
set_comms(*args)[source]

set the commutation group numbers c for symbols i, j

Parameters:args (sequence of (i, j, c))
class diofant.tensor.tensor.TensorIndexType[source]

A TensorIndexType is characterized by its name and its metric.

Parameters:
  • name (name of the tensor type)
  • metric (metric symmetry or metric object or None)

dim : dimension, it can be a symbol or an integer or None

eps_dim : dimension of the epsilon tensor

dummy_fmt : name of the head of dummy indices

``name``
``metric_name``

it is ‘metric’ or metric.name

Type:str
``metric_antisym``
``metric``

the metric tensor

Type:TensorType
``delta``
Type:Kronecker delta
``epsilon``
Type:the Levi-Civita epsilon tensor
``dim``
``dim_eps``
``dummy_fmt``
``data``
Type:a property to add ndarray values, to work in a specified basis.

Notes

The metric parameter can be: metric = False symmetric metric (in Riemannian geometry)

metric = True antisymmetric metric (for spinor calculus)

metric = None there is no metric

metric can be an object having name and antisym attributes.

If there is a metric the metric is used to raise and lower indices.

In the case of antisymmetric metric, the following raising and lowering conventions will be adopted:

psi(a) = g(a, b)*psi(-b); chi(-a) = chi(b)*g(-b, -a)

g(-a, b) = delta(-a, b); g(b, -a) = -delta(a, -b)

where delta(-a, b) = delta(b, -a) is the Kronecker delta (see TensorIndex for the conventions on indices).

If there is no metric it is not possible to raise or lower indices; e.g. the index of the defining representation of SU(N) is ‘covariant’ and the conjugate representation is ‘contravariant’; for N > 2 they are linearly independent.

eps_dim is by default equal to dim, if the latter is an integer; else it can be assigned (for use in naive dimensional regularization); if eps_dim is not an integer epsilon is None.

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> Lorentz.metric
metric(Lorentz,Lorentz)

Examples with metric components data added, this means it is working on a fixed basis:

>>> Lorentz.data = [1, -1, -1, -1]
>>> print(sstr(Lorentz))
TensorIndexType(Lorentz, 0)
>>> print(sstr(Lorentz.data))
[[1 0 0 0]
[0 -1 0 0]
[0 0 -1 0]
[0 0 0 -1]]
class diofant.tensor.tensor.TensorIndex[source]

Represents an abstract tensor index.

Parameters:
  • name (name of the index, or True if you want it to be automatically assigned)
  • tensortype (TensorIndexType of the index)
  • is_up (flag for contravariant index)
``name``
``tensortype``
``is_up``

Notes

Tensor indices are contracted with the Einstein summation convention.

An index can be in contravariant or in covariant form; in the latter case it is represented prepending a - to the index name.

Dummy indices have a name with head given by tensortype._dummy_fmt

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> i = TensorIndex('i', Lorentz); i
i
>>> sym1 = TensorSymmetry(*get_symmetric_group_sgs(1))
>>> S1 = TensorType([Lorentz], sym1)
>>> A, B = S1('A B')
>>> A(i)*B(-i)
A(L_0)*B(-L_0)

If you want the index name to be automatically assigned, just put True in the name field, it will be generated using the reserved character _ in front of its name, in order to avoid conflicts with possible existing indices:

>>> i0 = TensorIndex(True, Lorentz)
>>> i0
_i0
>>> i1 = TensorIndex(True, Lorentz)
>>> i1
_i1
>>> A(i0)*B(-i1)
A(_i0)*B(-_i1)
>>> A(i0)*B(-i0)
A(L_0)*B(-L_0)
diofant.tensor.tensor.tensor_indices(s, typ)[source]

Returns list of tensor indices given their names and their types

Parameters:
  • s (string of comma separated names of indices)
  • typ (list of TensorIndexType of the indices)

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> a, b, c, d = tensor_indices('a b c d', Lorentz)
class diofant.tensor.tensor.TensorSymmetry[source]

Monoterm symmetry of a tensor

Parameters:bsgs (tuple (base, sgs) BSGS of the symmetry of the tensor)
``base``

base of the BSGS

Type:Tuple
``generators``

generators of the BSGS

Type:Tuple
``rank``

rank of the tensor

Type:Tuple

Notes

A tensor can have an arbitrary monoterm symmetry provided by its BSGS. Multiterm symmetries, like the cyclic symmetry of the Riemann tensor, are not covered.

Examples

Define a symmetric tensor

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> sym2 = TensorSymmetry(get_symmetric_group_sgs(2))
>>> S2 = TensorType([Lorentz]*2, sym2)
>>> V = S2('V')
diofant.tensor.tensor.tensorsymmetry(*args)[source]

Return a TensorSymmetry object.

One can represent a tensor with any monoterm slot symmetry group using a BSGS.

args can be a BSGS args[0] base args[1] sgs

Usually tensors are in (direct products of) representations of the symmetric group; args can be a list of lists representing the shapes of Young tableaux

Notes

For instance: [[1]] vector [[1]*n] symmetric tensor of rank n [[n]] antisymmetric tensor of rank n [[2, 2]] monoterm slot symmetry of the Riemann tensor [[1],[1]] vector*vector [[2],[1],[1] (antisymmetric tensor)*vector*vector

Notice that with the shape [2, 2] we associate only the monoterm symmetries of the Riemann tensor; this is an abuse of notation, since the shape [2, 2] corresponds usually to the irreducible representation characterized by the monoterm symmetries and by the cyclic symmetry.

Examples

Symmetric tensor using a Young tableau

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> sym2 = tensorsymmetry([1, 1])
>>> S2 = TensorType([Lorentz]*2, sym2)
>>> V = S2('V')

Symmetric tensor using a BSGS (base, strong generator set)

>>> sym2 = tensorsymmetry(*get_symmetric_group_sgs(2))
>>> S2 = TensorType([Lorentz]*2, sym2)
>>> V = S2('V')
class diofant.tensor.tensor.TensorType[source]

Class of tensor types.

Parameters:
  • index_types (list of TensorIndexType of the tensor indices)
  • symmetry (TensorSymmetry of the tensor)
``index_types``
``symmetry``
``types``
Type:list of TensorIndexType without repetitions

Examples

Define a symmetric tensor

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> sym2 = tensorsymmetry([1, 1])
>>> S2 = TensorType([Lorentz]*2, sym2)
>>> V = S2('V')
class diofant.tensor.tensor.TensorHead[source]

Tensor head of the tensor

Parameters:
  • name (name of the tensor)
  • typ (list of TensorIndexType)
  • comm (commutation group number)
``name``
``index_types``
``rank``
``types``
Type:equal to typ.types
``symmetry``
Type:equal to typ.symmetry
``comm``

commutation group

Type:int

Notes

A TensorHead belongs to a commutation group, defined by a symbol on number comm (see _TensorManager.set_comm); tensors in a commutation group have the same commutation properties; by default comm is 0, the group of the commuting tensors.

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> A = tensorhead('A', [Lorentz, Lorentz], [[1], [1]])

Examples with ndarray values, the components data assigned to the TensorHead object are assumed to be in a fully-contravariant representation. In case it is necessary to assign components data which represents the values of a non-fully covariant tensor, see the other examples.

>>> Lorentz.data = [1, -1, -1, -1]
>>> i0, i1 = tensor_indices('i0:2', Lorentz)
>>> A.data = [[j+2*i for j in range(4)] for i in range(4)]

in order to retrieve data, it is also necessary to specify abstract indices enclosed by round brackets, then numerical indices inside square brackets.

>>> A(i0, i1)[0, 0]
0
>>> A(i0, i1)[2, 3] == 3+2*2
True

Notice that square brackets create a valued tensor expression instance:

>>> A(i0, i1)
A(i0, i1)

To view the data, just type:

>>> print(sstr(A.data))
[[0 1 2 3]
 [2 3 4 5]
 [4 5 6 7]
 [6 7 8 9]]

Turning to a tensor expression, covariant indices get the corresponding components data corrected by the metric:

>>> print(sstr(A(i0, -i1).data))
[[0 -1 -2 -3]
 [2 -3 -4 -5]
 [4 -5 -6 -7]
 [6 -7 -8 -9]]
>>> print(sstr(A(-i0, -i1).data))
[[0 -1 -2 -3]
 [-2 3 4 5]
 [-4 5 6 7]
 [-6 7 8 9]]

while if all indices are contravariant, the ndarray remains the same

>>> print(sstr(A(i0, i1).data))
 [[0 1 2 3]
 [2 3 4 5]
 [4 5 6 7]
 [6 7 8 9]]

When all indices are contracted and components data are added to the tensor, accessing the data will return a scalar, no numpy object. In fact, numpy ndarrays are dropped to scalars if they contain only one element.

>>> A(i0, -i0)
A(L_0, -L_0)
>>> A(i0, -i0).data
-18

It is also possible to assign components data to an indexed tensor, i.e. a tensor with specified covariant and contravariant components. In this example, the covariant components data of the Electromagnetic tensor are injected into \(A\):

>>> Ex, Ey, Ez, Bx, By, Bz = symbols('E_x E_y E_z B_x B_y B_z')
>>> c = symbols('c', positive=True)

Let’s define \(F\), an antisymmetric tensor, we have to assign an antisymmetric matrix to it, because \([[2]]\) stands for the Young tableau representation of an antisymmetric set of two elements:

>>> F = tensorhead('A', [Lorentz, Lorentz], [[2]])
>>> F(-i0, -i1).data = [
... [0, Ex/c, Ey/c, Ez/c],
... [-Ex/c, 0, -Bz, By],
... [-Ey/c, Bz, 0, -Bx],
... [-Ez/c, -By, Bx, 0]]

Now it is possible to retrieve the contravariant form of the Electromagnetic tensor:

>>> print(sstr(F(i0, i1).data))
[[0 -E_x/c -E_y/c -E_z/c]
 [E_x/c 0 -B_z B_y]
 [E_y/c B_z 0 -B_x]
 [E_z/c -B_y B_x 0]]

and the mixed contravariant-covariant form:

>>> print(sstr(F(i0, -i1).data))
[[0 E_x/c E_y/c E_z/c]
 [E_x/c 0 B_z -B_y]
 [E_y/c -B_z 0 B_x]
 [E_z/c B_y -B_x 0]]

To convert the numpy’s ndarray to a diofant matrix, just cast:

>>> Matrix(F.data)
Matrix([
[    0, -E_x/c, -E_y/c, -E_z/c],
[E_x/c,      0,   -B_z,    B_y],
[E_y/c,    B_z,      0,   -B_x],
[E_z/c,   -B_y,    B_x,      0]])

Still notice, in this last example, that accessing components data from a tensor without specifying the indices is equivalent to assume that all indices are contravariant.

It is also possible to store symbolic components data inside a tensor, for example, define a four-momentum-like tensor:

>>> P = tensorhead('P', [Lorentz], [[1]])
>>> E, px, py, pz = symbols('E p_x p_y p_z', positive=True)
>>> P.data = [E, px, py, pz]

The contravariant and covariant components are, respectively:

>>> print(sstr(P(i0).data))
[E p_x p_y p_z]
>>> print(sstr(P(-i0).data))
[E -p_x -p_y -p_z]

The contraction of a 1-index tensor by itself is usually indicated by a power by two:

>>> P(i0)**2
E**2 - p_x**2 - p_y**2 - p_z**2

As the power by two is clearly identical to \(P_\mu P^\mu\), it is possible to simply contract the TensorHead object, without specifying the indices

>>> P**2
E**2 - p_x**2 - p_y**2 - p_z**2
commutes_with(other)[source]

Returns 0 if self and other commute, 1 if they anticommute.

Returns None if self and other neither commute nor anticommute.

class diofant.tensor.tensor.TensExpr[source]

Abstract base class for tensor expressions

Notes

A tensor expression is an expression formed by tensors; currently the sums of tensors are distributed.

A TensExpr can be a TensAdd or a TensMul.

TensAdd objects are put in canonical form using the Butler-Portugal algorithm for canonicalization under monoterm symmetries.

TensMul objects are formed by products of component tensors, and include a coefficient, which is a Diofant expression.

In the internal representation contracted indices are represented by (ipos1, ipos2, icomp1, icomp2), where icomp1 is the position of the component tensor with contravariant index, ipos1 is the slot which the index occupies in that component tensor.

Contracted indices are therefore nameless in the internal representation.

get_matrix()[source]

Returns ndarray components data as a matrix, if components data are available and ndarray dimension does not exceed 2.

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> sym2 = tensorsymmetry([1]*2)
>>> S2 = TensorType([Lorentz]*2, sym2)
>>> A = S2('A')

The tensor A is symmetric in its indices, as can be deduced by the [1, 1] Young tableau when constructing \(sym2\). One has to be careful to assign symmetric component data to A, as the symmetry properties of data are currently not checked to be compatible with the defined tensor symmetry.

>>> Lorentz.data = [1, -1, -1, -1]
>>> i0, i1 = tensor_indices('i0:2', Lorentz)
>>> A.data = [[j+i for j in range(4)] for i in range(4)]
>>> A(i0, i1).get_matrix()
Matrix([
[0, 1, 2, 3],
[1, 2, 3, 4],
[2, 3, 4, 5],
[3, 4, 5, 6]])

It is possible to perform usual operation on matrices, such as the matrix multiplication:

>>> A(i0, i1).get_matrix()*ones(4, 1)
Matrix([
[ 6],
[10],
[14],
[18]])
>>> del A.data
class diofant.tensor.tensor.TensAdd[source]

Sum of tensors

Parameters:free_args (list of the free indices)
``args``

of addends

Type:tuple
``rank``

rank of the tensor

Type:tuple
``free_args``

of the free indices in sorted order

Type:list

Notes

Sum of more than one tensor are put automatically in canonical form.

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> a, b = tensor_indices('a b', Lorentz)
>>> p, q = tensorhead('p q', [Lorentz], [[1]])
>>> t = p(a) + q(a); t
p(a) + q(a)
>>> t(b)
p(b) + q(b)

Examples with components data added to the tensor expression:

>>> Lorentz.data = [1, -1, -1, -1]
>>> a, b = tensor_indices('a, b', Lorentz)
>>> p.data = [2, 3, -2, 7]
>>> q.data = [2, 3, -2, 7]
>>> t = p(a) + q(a); t
p(a) + q(a)
>>> t(b)
p(b) + q(b)

The following are: 2**2 - 3**2 - 2**2 - 7**2 ==> -58

>>> (p(a)*p(-a)).data
-58
>>> p(a)**2
-58
canon_bp()[source]

canonicalize using the Butler-Portugal algorithm for canonicalization under monoterm symmetries.

contract_metric(g)[source]

Raise or lower indices with the metric g

Parameters:
  • g (metric)
  • contract_all (if True, eliminate all g which are contracted)

Notes

static from_TIDS_list(coeff, tids_list)[source]

Given a list of coefficients and a list of TIDS objects, construct a TensAdd instance, equivalent to the one that would result from creating single instances of TensMul and then adding them.

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> i, j = tensor_indices('i j', Lorentz)
>>> A, B = tensorhead('A B', [Lorentz]*2, [[1]*2])
>>> eA = 3*A(i, j)
>>> eB = 2*B(j, i)
>>> t1 = eA._tids
>>> t2 = eB._tids
>>> c1 = eA.coeff
>>> c2 = eB.coeff
>>> TensAdd.from_TIDS_list([c1, c2], [t1, t2])
2*B(i, j) + 3*A(i, j)

If the coefficient parameter is a scalar, then it will be applied as a coefficient on all TIDS objects.

>>> TensAdd.from_TIDS_list(4, [t1, t2])
4*A(i, j) + 4*B(i, j)
fun_eval(*index_tuples)[source]

Return a tensor with free indices substituted according to index_tuples

Parameters:index_types (list of tuples (old_index, new_index))

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> i, j, k, l = tensor_indices('i j k l', Lorentz)
>>> A, B = tensorhead('A B', [Lorentz]*2, [[1]*2])
>>> t = A(i, k)*B(-k, -j) + A(i, -j)
>>> t.fun_eval((i, k), (-j, l))
A(k, L_0)*B(l, -L_0) + A(k, l)
substitute_indices(*index_tuples)[source]

Return a tensor with free indices substituted according to index_tuples

Parameters:index_types (list of tuples (old_index, new_index))

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> i, j, k, l = tensor_indices('i j k l', Lorentz)
>>> A, B = tensorhead('A B', [Lorentz]*2, [[1]*2])
>>> t = A(i, k)*B(-k, -j); t
A(i, L_0)*B(-L_0, -j)
>>> t.substitute_indices((i, j), (j, k))
A(j, L_0)*B(-L_0, -k)
class diofant.tensor.tensor.TensMul[source]

Product of tensors

Parameters:
  • coeff (Diofant coefficient of the tensor)
  • args
``components``
Type:list of TensorHead of the component tensors
``types``
Type:list of nonrepeated TensorIndexType
``free``
Type:list of (ind, ipos, icomp), see Notes
``dum``
Type:list of (ipos1, ipos2, icomp1, icomp2), see Notes
``ext_rank``

rank of the tensor counting the dummy indices

Type:tuple
``rank``

rank of the tensor

Type:tuple
``coeff``

Diofant coefficient of the tensor

Type:Expr
``free_args``

list of the free indices in sorted order

Type:list
``is_canon_bp``
Type:True if the tensor in in canonical form

Notes

args[0] list of TensorHead of the component tensors.

args[1] list of (ind, ipos, icomp) where ind is a free index, ipos is the slot position of ind in the icomp-th component tensor.

args[2] list of tuples representing dummy indices. (ipos1, ipos2, icomp1, icomp2) indicates that the contravariant dummy index is the ipos1-th slot position in the icomp1-th component tensor; the corresponding covariant index is in the ipos2 slot position in the icomp2-th component tensor.

canon_bp()[source]

Canonicalize using the Butler-Portugal algorithm for canonicalization under monoterm symmetries.

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> m0, m1, m2 = tensor_indices('m0 m1 m2', Lorentz)
>>> A = tensorhead('A', [Lorentz]*2, [[2]])
>>> t = A(m0, -m1)*A(m1, -m0)
>>> t.canon_bp()
-A(L_0, L_1)*A(-L_0, -L_1)
>>> t = A(m0, -m1)*A(m1, -m2)*A(m2, -m0)
>>> t.canon_bp()
0
contract_metric(g)[source]

Raise or lower indices with the metric g

Parameters:g (metric)

Notes

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> m0, m1, m2 = tensor_indices('m0 m1 m2', Lorentz)
>>> g = Lorentz.metric
>>> p, q = tensorhead('p q', [Lorentz], [[1]])
>>> t = p(m0)*q(m1)*g(-m0, -m1)
>>> t.canon_bp()
metric(L_0, L_1)*p(-L_0)*q(-L_1)
>>> t.contract_metric(g).canon_bp()
p(L_0)*q(-L_0)
fun_eval(*index_tuples)[source]

Return a tensor with free indices substituted according to index_tuples

index_types list of tuples (old_index, new_index)

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> i, j, k, l = tensor_indices('i j k l', Lorentz)
>>> A, B = tensorhead('A B', [Lorentz]*2, [[1]*2])
>>> t = A(i, k)*B(-k, -j); t
A(i, L_0)*B(-L_0, -j)
>>> t.fun_eval((i, k), (-j, l))
A(k, L_0)*B(-L_0, l)
get_indices()[source]

Returns the list of indices of the tensor

The indices are listed in the order in which they appear in the component tensors. The dummy indices are given a name which does not collide with the names of the free indices.

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> m0, m1, m2 = tensor_indices('m0 m1 m2', Lorentz)
>>> g = Lorentz.metric
>>> p, q = tensorhead('p q', [Lorentz], [[1]])
>>> t = p(m1)*g(m0, m2)
>>> t.get_indices()
[m1, m0, m2]
perm2tensor(g, canon_bp=False)[source]

Returns the tensor corresponding to the permutation g

For further details, see the method in TIDS with the same name.

sorted_components()[source]

Returns a tensor with sorted components calling the corresponding method in a TIDS object.

split()[source]

Returns a list of tensors, whose product is self

Dummy indices contracted among different tensor components become free indices with the same name as the one used to represent the dummy indices.

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> a, b, c, d = tensor_indices('a b c d', Lorentz)
>>> A, B = tensorhead('A B', [Lorentz]*2, [[1]*2])
>>> t = A(a, b)*B(-b, c)
>>> t
A(a, L_0)*B(-L_0, c)
>>> t.split()
[A(a, L_0), B(-L_0, c)]
diofant.tensor.tensor.canon_bp(p)[source]

Butler-Portugal canonicalization

diofant.tensor.tensor.tensor_mul(*a)[source]

product of tensors

diofant.tensor.tensor.riemann_cyclic_replace(t_r)[source]

replace Riemann tensor with an equivalent expression

R(m,n,p,q) -> 2/3*R(m,n,p,q) - 1/3*R(m,q,n,p) + 1/3*R(m,p,n,q)

diofant.tensor.tensor.riemann_cyclic(t2)[source]

replace each Riemann tensor with an equivalent expression satisfying the cyclic identity.

This trick is discussed in the reference guide to Cadabra.

Examples

>>> Lorentz = TensorIndexType('Lorentz', dummy_fmt='L')
>>> i, j, k, l = tensor_indices('i j k l', Lorentz)
>>> R = tensorhead('R', [Lorentz]*4, [[2, 2]])
>>> t = R(i, j, k, l)*(R(-i, -j, -k, -l) - 2*R(-i, -k, -j, -l))
>>> riemann_cyclic(t)
0