Spin-1 Heisenberg ring: Compressed storage of state vectors
In this example we explore the concept of a
sparse Hilbert space, and illustrate how using the
compressed_state_view objects
can lead to a dramatic reduction in memory footprint. To this end, we consider
a ring of \(N\) spins \(S=1\) interacting via the antiferromagnetic
Heisenberg exchange coupling,
The state space of each spin is spanned by the \(S_z\)-triplet
\(|0\rangle, |\pm1\rangle\), and the dimension of the model’s Hilbert space
is thus \(3^N\). From the computing standpoint, however, it takes 2 bits to
specify one of the three basis states, with the bit combination 0b11 not
representing any physical state. According to the
convention adopted in libcommute, one then needs a bit
string of length \(2N\) to encode a basis state of the entire system.
Although not all such strings correspond to physical basis states (most of the
strings are, in fact, unphysical for \(N\geq 3\)), naively one would have to
allocate an array of size \(4^N\) to store a state vector compatible with
linear operators acting in this Hilbert space. This situation
is an example of a sparse Hilbert space, i.e. of a space whose basis state
indices do not form a continuous integer range. Sparse Hilbert spaces are
characterized by non-power-of-two dimensions.
It is a valid (and faster) option to allocate std::vector
(Eigen::Vector, etc) objects of the size \(4^N\) to store state
vectors, even though some of their elements will never be referenced. Another
option offered by libcommute is to allocate containers of the exponentially
smaller size \(3^N\), and to employ the
compressed_state_view adaptor
that – at the cost of a certain speed loss – translates indices of
the physical basis states onto the continuous range \([0;3^N-1]\).
1// 'state_vector_eigen3.hpp' must be included before 'libcommute.hpp'.
2// This way libcommute will know how to deal with Eigen's vector types.
3#include <libcommute/loperator/state_vector_eigen3.hpp>
4
5#include <libcommute/libcommute.hpp>
6
7#include <Eigen/Eigenvalues>
8
9using namespace libcommute;
10
11int main() {
12 int const N = 6; // Number of spins
13
14 // Expression of the Heisenberg Hamiltonian to be constructed.
15 // Every creation and annihilation operator found in the expression carries
16 // one integer index (ring site).
17 static_indices::expr_real<int> H;
18
19 // The following functions return operators with statically typed indices.
20 using static_indices::S_z; // Spin projection operator S_z
21 using static_indices::S_p; // Spin-raising operator S_+
22 using static_indices::S_m; // Spin-lowering operator S_-
23
24 // Heisenberg interaction of the S=1 spins
25 for(int i = 0; i < N; ++i) {
26 int j = (i + 1) % N;
27 H += S_z<3>(i) * S_z<3>(j) +
28 0.5 * (S_p<3>(i) * S_m<3>(j) + S_m<3>(i) * S_p<3>(j));
29 }
30
31 // Automatically analyse structure of 'H' and construct a Hilbert space.
32 auto hs = make_hilbert_space(H);
33
34 // The Hilbert space 'hs' is sparse: The minimal required container size for
35 // state vectors exceeds the dimension of the space.
36 std::cout << "Is the Hilbert space sparse? " << hs.is_sparse() << '\n';
37 std::cout << "Hilbert space dimension: " << hs.dim() << '\n';
38 std::cout << "Minimal container size for state vectors: " << hs.vec_size()
39 << '\n';
40
41 auto const dim = hs.dim(); // 3^N
42
43 // Construct an 'loperator' object that represents action of the expression
44 // 'H' on state vectors in the Hilbert space 'hs'.
45 auto Hop = make_loperator(H, hs);
46
47 // Preallocate an input state vector of the length 3^N.
48 // This container is compressed, i.e. it stores quantum amplitudes of
49 // the *physical* basis states only!
50 Eigen::VectorXd psi(dim);
51
52 // Preallocate a Hamiltonian matrix to be filled and diagonalized.
53 // This matrix is also compressed.
54 Eigen::MatrixXd Hmat(dim, dim);
55
56 // Create a view of the compressed state 'psi'.
57 auto psi_view = make_const_comp_state_view(psi, hs);
58
59 // Fill 'Hmat'
60 // Important: k takes on values corresponding to the *physical* basis states
61 // only.
62 foreach(hs, [dim, &hs, &Hop, &psi, &psi_view, &Hmat](sv_index_type k) {
63 // Translate k into the compressed (continuous) range [0; 3^N-1]
64 auto const k_comp = psi_view.map_index(k);
65
66 // Reset vector |\psi> to zero.
67 psi = Eigen::VectorXd::Zero((int)dim);
68
69 // Now, 'psi' is the basis vector corresponding to 'k'.
70 psi((int)k_comp) = 1;
71
72 // Create a view of the column of 'Hmat' that corresponds to 'k'.
73 auto HMat_col_view = make_comp_state_view(Hmat.col((int)k_comp), hs);
74
75 // Act with 'Hop' on the basis state 'k' and write the result into
76 // the column of 'Hmat' that also corresponds to 'k'.
77 Hop(psi_view, HMat_col_view);
78 });
79
80 // Diagonalize 'Hmat'.
81 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> diag(Hmat,
82 Eigen::EigenvaluesOnly);
83
84 std::cout << "10 lowest eigenvalues of H = "
85 << diag.eigenvalues().head(10).transpose() << '\n';
86
87 return 0;
88}