Reputation: 13
I am working on a C++ library that relies on tensor contractions. I won't post the full application here, but I've distilled it down to the following.
We define a toy rank-4 tensor, which is nothing but (0, 1, ..., 15) reshaped:
Eigen::Tensor<double, 4> T (2, 2, 2, 2);
for (size_t i = 0; i < 2; i++) {
for (size_t j = 0; j < 2; j++) {
for (size_t k = 0; k < 2; k++) {
for (size_t l = 0; l < 2; l++) {
T(i, j, k, l) = l + 2 * k + 4 * j + 8 * i;
}
}
}
}
and a rank-2 tensor to contract with, which is nothing but (1, 2, 3, 4) reshaped:
Eigen::Tensor<double, 2> A (2, 2);
for (size_t i = 0; i < 2; i++) {
for (size_t j = 0; j < 2; j++) {
A(i, j) = 1 + j + 2 * i;
}
}
To contract two tensors in Eigen, we have to specify a contraction pair. Our goal is to contract the first two indices of the tensors, as in T(ijkl)*A(ib)=M(bjkl)
. With my current understanding of the Tensor module in Eigen, we will write the contraction pair as
Eigen::array<Eigen::IndexPair<int>, 1> contraction_pair = {Eigen::IndexPair<int>(0, 0)};
However, I think it should be possible to use the exact same contraction pair to perform the contraction A(ib)*T(ijkl)=N(bjkl)
. Unfortunately, this is not the case, and the elements of M
are
0 0 0 0 24
0 0 0 1 32
0 0 1 0 28
0 0 1 1 38
0 1 0 0 32
0 1 0 1 44
0 1 1 0 36
0 1 1 1 50
1 0 0 0 40
1 0 0 1 56
1 0 1 0 44
1 0 1 1 62
1 1 0 0 48
1 1 0 1 68
1 1 1 0 52
1 1 1 1 74
while these of N
are
0 0 0 0 24
0 0 0 1 28
0 0 1 0 32
0 0 1 1 36
0 1 0 0 40
0 1 0 1 44
0 1 1 0 48
0 1 1 1 52
1 0 0 0 32
1 0 0 1 38
1 0 1 0 44
1 0 1 1 50
1 1 0 0 56
1 1 0 1 62
1 1 1 0 68
1 1 1 1 74
I have tested the same toy tensors in numpy, using einsum:
T = np.arange(16).reshape(2, 2, 2, 2)
A = np.arange(1, 5).reshape(2, 2)
contraction1 = np.einsum('ijkl,ia->ajkl', integrals, C)
contraction2 = np.einsum('ia,ijkl->ajkl', C, integrals)
and both contraction1
and contraction2
are
0 0 0 0 24
0 0 0 1 28
0 0 1 0 32
0 0 1 1 36
0 1 0 0 40
0 1 0 1 44
0 1 1 0 48
0 1 1 1 52
1 0 0 0 32
1 0 0 1 38
1 0 1 0 44
1 0 1 1 50
1 1 0 0 56
1 1 0 1 62
1 1 1 0 68
1 1 1 1 74
which coincide with the case A(ib)*T(ijkl)=N(bjkl)
in Eigen. What causes Eigen to not give the same result in both cases?
Upvotes: 1
Views: 276
Reputation: 53089
The Eigen
interface appears to take only the contraction axes for specification. It therefore has to decide itself how to arrange the non-contracted axes. The obvious way would be to keep the original axes in order, first the first argument's then second's.
To confirm this we can either
np.einsum
specifying different output layouts and compare to the output of Eigen
:.
import numpy as np
T = np.arange(16).reshape(2, 2, 2, 2)
A = np.arange(1, 5).reshape(2, 2)
print(np.einsum('ijkl,ia->ajkl', T, A))
# [[[[24 28]
# [32 36]]
# [[40 44]
# [48 52]]]
# [[[32 38]
# [44 50]]
# [[56 62]
# [68 74]]]]
print(np.einsum('ijkl,ia->jkla', T, A))
# [[[[24 32]
# [28 38]]
# [[32 44]
# [36 50]]]
# [[[40 56]
# [44 62]]
# [[48 68]
# [52 74]]]]
Eigen
(thanks @lelemmen):.
M.shuffle(Eigen::array<int, 4> {3, 0, 1, 2})
(M_shuffled == N).all()
# 1
Upvotes: 1