Usage examples
Holstein model
This example shows how to construct Hamiltonian of the Holstein model on a square lattice and to check that the total number of electrons is a conserved quantity of the model.
The Hamiltonian considered here is a sum of three terms.
An electronic tight-binding model on a square lattice with only nearest-neighbour hopping allowed,
\[\hat H_\text{e} = -t \sum_\sigma \sum_{\langle i,j\rangle} c^\dagger_{i,\sigma} c_{j,\sigma}.\]A harmonic oscillator at each lattice site (a localized phonon),
\[\hat H_\text{ph} = \omega_0 \sum_i a^\dagger_i a_i.\]A coupling between the electrons and the phonons.
\[\hat H_\text{e-ph} = g \sum_\sigma \sum_i n_{i,\sigma}(a^\dagger_i + a_i).\]
Instead of writing the sums over lattice sites explicitly, we call library
functions tight_binding()
,
dispersion()
and
holstein_int()
.
We also make use of the NetworkX package
to easily generate the adjacency matrix of the periodic square lattice.
1#
2# Holstein model on a square lattice with periodic boundary conditions.
3#
4
5from itertools import product
6import numpy as np
7
8from pycommute.expression import ExpressionR, conj, FERMION, BOSON
9from pycommute.expression import n
10from pycommute.models import tight_binding, dispersion, holstein_int
11
12from networkx.generators.lattice import grid_2d_graph
13from networkx.linalg.graphmatrix import adjacency_matrix
14
15#
16# Let us define Hamiltonian of an electronic tight-binding model
17# on a square lattice.
18#
19
20# Number of lattice sites in each direction
21# (the total number of sites is N*N).
22N = 10
23
24# Electron hopping constant - energy parameter of the TB model.
25t = 2.0
26
27# Use NetworkX to construct the periodic square lattice (graph)
28lat = grid_2d_graph(N, N, periodic=True)
29
30# Create lists of indices for electronic spin-up and spin-down operators.
31# lat.nodes() returns a list of N^2 pairs of site indices (x, y).
32indices_up = [(x, y, "up") for x, y in lat.nodes()]
33indices_dn = [(x, y, "down") for x, y in lat.nodes()]
34
35# A sum of tight-binding Hamiltonians for both spins.
36# The hopping matrix passed to tight_binding() is proportional to the
37# adjacency matrix of the lattice.
38hopping_matrix = -t * adjacency_matrix(lat).todense()
39H_e = tight_binding(hopping_matrix, indices=indices_up) \
40 + tight_binding(hopping_matrix, indices=indices_dn)
41
42#
43# Hamiltonian of phonons localized at lattice sites.
44#
45
46# Frequency of the localized phonon.
47w0 = 0.5
48
49# A lists of indices for bosonic operators, simply the (x, y) pairs
50indices_phonon = lat.nodes()
51
52# All N^2 phonons have the same frequency
53phonon_freqs = w0 * np.ones(N ** 2)
54H_ph = dispersion(phonon_freqs, indices=indices_phonon, statistics=BOSON)
55
56#
57# Hamiltonian of electron-phonon coupling.
58#
59
60# Electron-phonon coupling constant.
61g = 0.1
62
63H_e_ph = holstein_int(g * np.ones(N ** 2),
64 indices_up=indices_up,
65 indices_dn=indices_dn,
66 indices_boson=indices_phonon)
67
68# Complete Holstein Hamiltonian.
69H_H = H_e + H_ph + H_e_ph
70
71# Print H_H. There will be quite a lot of terms for the 100-site lattice!
72print("H_H =", H_H)
73
74# Check hermiticity of H_H.
75print(r"H_H - H_H^\dagger =", H_H - conj(H_H))
76
77# Check that H_H commutes with the total number of electrons N_e.
78N_e = ExpressionR()
79for spin in ("up", "down"):
80 for x, y in product(range(N), range(N)):
81 N_e += n(x, y, spin)
82
83print("[H_H, N_e] =", H_H * N_e - N_e * H_H)
84
85#
86# Iteration interface
87#
88
89# Iterate over monomial-coefficient pairs in polynomial expression H_H
90for monomial, coeff in H_H:
91 print("Coefficient:", coeff)
92 # Iterate over algebra generators (creation/annihilation operators)
93 # in the monomial
94 for generator in monomial:
95 # Detect what algebra this generator belongs to
96 if generator.algebra_id == FERMION:
97 print("\tFermionic", end=' ')
98 elif generator.algebra_id == BOSON:
99 print("\tBosonic", end=' ')
100 # Creation or annihilation operator?
101 if generator.dagger:
102 print("creation operator", end=' ')
103 else:
104 print("annihilation operator", end=' ')
105 # Extract indices carried by the generator
106 print("with indices", list(generator.indices))
107 # N.B. generator.indices is an instance of a special tuple-like type
108 # `pycommute.expression.Indices`. Types of its elements are restricted
109 # to integer and string, and its comparison operators behave differently
110 # from those of the Python tuple. Otherwise, it supports `len()`,
111 # indexed element access and iteration protocol.
Spin-1/2 Heisenberg chain
The spin-1/2 Heisenberg chain is a textbook example of an integrable quantum system. Its Hamiltonian
conserves three projections of the total spin
as well as a series of higher order charges \(Q_n\). Existence of these
charges can be derived from the transfer matrix theory. Explicit expressions
for \(Q_n\) were obtained in [GM94]. The following script constructs
Hamiltonian of the Heisenberg chain with periodic boundary conditions
(see heisenberg()
)
and checks that \([\hat H, \mathbf{S}] = 0\), \([\hat H, Q_n] = 0\) and
\([Q_n, Q_m] = 0\) for \(m,n = 3,4,5\).
1#
2# Periodic spin-1/2 Heisenberg chain and its integrals of motion
3#
4# Expressions for the integrals of motion are taken from
5#
6# "Quantum Integrals of Motion for the Heisenberg Spin Chain",
7# M. P. Grabowski and P. Mathieu
8# Mod. Phys. Lett. A, Vol. 09, No. 24, pp. 2197-2206 (1994),
9# https://doi.org/10.1142/S0217732394002057
10#
11
12from numpy import array, zeros, dot, cross
13from pycommute.expression import S_x, S_y, S_z
14from pycommute.models import heisenberg
15
16# Number of spins in the chain
17N = 20
18# Heisenberg exchange constant
19g = 2
20
21# List of 3-component spin vectors {S_0, S_1, ..., S_{N-1}}
22S = [array([S_x(i), S_y(i), S_z(i)]) for i in range(N)]
23
24# Matrix of exchange constants between spins i and j
25exchange_matrix = zeros((N, N))
26# Set elements corresponding to the nearest neighbors to -g
27# (index shift modulo N ensures periodic boundary conditions).
28for i in range(N):
29 exchange_matrix[i, (i + 1) % N] = -g
30
31# Hamiltonian of the spin-1/2 Heisenberg chain.
32H = heisenberg(exchange_matrix)
33
34# Total spin of the chain.
35S_tot = array(sum(S))
36
37# All three components of S commute with the Hamiltonian.
38print("[H, S_x] =", (H * S_tot[0] - S_tot[0] * H))
39print("[H, S_y] =", (H * S_tot[1] - S_tot[1] * H))
40print("[H, S_z] =", (H * S_tot[2] - S_tot[2] * H))
41
42# Higher charge Q_3 (1st line of Eq. (10)).
43Q3 = sum(dot(cross(S[i], S[(i + 1) % N]), S[(i + 2) % N]) for i in range(N))
44print("[H, Q3] =", (H * Q3 - Q3 * H))
45
46# Higher charge Q_4 (2nd line of Eq. (10)).
47Q4 = sum(4.0 * dot(
48 cross(
49 cross(S[i], S[(i + 1) % N]), S[(i + 2) % N]
50 ), S[(i + 3) % N]) for i in range(N))
51Q4 += sum(dot(S[i], S[(i + 2) % N]) for i in range(N))
52print("[H, Q4] =", (H * Q4 - Q4 * H))
53
54# Higher charge Q_5 (3rd line of Eq. (10)).
55Q5 = sum(4.0 * dot(
56 cross(
57 cross(
58 cross(S[i], S[(i + 1) % N]),
59 S[(i + 2) % N]
60 ), S[(i + 3) % N]),
61 S[(i + 4) % N]) for i in range(N))
62Q5 += sum(dot(cross(S[i], S[(i + 2) % N]), S[(i + 3) % N]) for i in range(N))
63Q5 += sum(dot(cross(S[i], S[(i + 1) % N]), S[(i + 3) % N]) for i in range(N))
64print("[H, Q5] =", (H * Q5 - Q5 * H))
65
66# Check that the higher charges pairwise commute.
67print("[Q3, Q4] =", (Q3 * Q4 - Q4 * Q3))
68print("[Q3, Q5] =", (Q3 * Q5 - Q5 * Q3))
69print("[Q4, Q5] =", (Q4 * Q5 - Q5 * Q4))
Spectrum of Tavis-Cummings model
In this example we demonstrate how to use tools from the
loperator
module along with NumPy’s eigensolvers to
diagonalize Hamiltonians of simple quantum models.
We will consider a two-atom Jaynes-Cummings (Tavis-Cummings) Hamiltonian [TC68], which models a system of two qubits coherently coupled via a bosonic degree of freedom (quantum oscillator),
An expression of this form can be conveniently generated by a single call to
jaynes_cummings()
.
Since the occupation number of the bosonic mode can formally grow to \(+\infty\), we have to artificially restrict the dimension of the bosonic Hilbert space in order to be able to construct a finite matrix representation of our problem.
Note
Performance of repeated calls to
LOperatorR
or to
LOperatorC
in order to construct
a matrix representation of a linear operator is limited by Python method
call overhead. For large-scale problems it is advised to call the utility
function make_matrix()
.
1#
2# Diagonalization of a two-qubit Tavis-Cummings model.
3#
4
5import numpy as np
6
7# Utility function used to construct the generalized Jaynes-Cummings model.
8from pycommute.models import jaynes_cummings
9
10# Hilbert spaces.
11from pycommute.loperator import HilbertSpace, make_space_boson, make_space_spin
12
13# Real-valued linear operator object.
14from pycommute.loperator import LOperatorR
15
16# Build the matrix form of a linear operator.
17from pycommute.loperator import make_matrix
18
19# Transition frequencies of qubits.
20eps = np.array([1.0, 1.0])
21
22# Oscillator frequency.
23# (In a more general case of M oscillators, omega would be a length-M array).
24omega = np.array([0.8])
25
26# Qubit-oscillator coupling constants as a 2x1 array.
27g = np.array([[0.5], [0.6]])
28
29# Create the Tavis-Cummings Hamiltonian.
30H = jaynes_cummings(eps, omega, g)
31
32# Construct state space of our problem as a direct product of two
33# two-dimensional Hilbert spaces (qubits) and one truncated bosonic Hilbert
34# space.
35# make_space_boson(4) returns the truncated bosonic space with allowed
36# occupation numbers N = 0, 1, ..., (2^4-1).
37hs = HilbertSpace([
38 make_space_spin(1 / 2, 0), # Qubit 1: spin-1/2, index 0
39 make_space_spin(1 / 2, 1), # Qubit 2: spin-1/2, index 1
40 make_space_boson(4, 0) # Oscillator, index 0
41])
42
43# Construct a linear operator corresponding to 'H' and acting in the Hilbert
44# space 'hs'.
45H_op = LOperatorR(H, hs)
46
47#
48# Prepare a matrix representation of 'H_op'
49#
50
51# Method I (manual).
52H_mat1 = np.zeros((hs.dim, hs.dim))
53for i in range(hs.dim):
54 # A column vector psi = {0, 0, ..., 1, ..., 0}
55 psi = np.zeros(hs.dim)
56 psi[i] = 1.0
57 # Act with H_op on psi and store the result the i-th column of H_mat
58 H_mat1[:, i] = H_op * psi
59
60# Method II (automatic and faster).
61H_mat2 = make_matrix(H_op, hs)
62print("Max difference between H_mat and H_mat2:",
63 np.max(np.abs(H_mat1 - H_mat2)))
64
65# Use NumPy to compute eigenvalues of H_mat
66E = np.linalg.eigvals(H_mat1)
67
68print("Energies:", np.sort(E))
Sectors of a multi-orbital interaction Hamiltonian
When implementing an exact diagonalization algorithm, it is usually advantageous to take an extra preparatory step and split the Hilbert space of the problem into subspaces (sectors) characterized by a set of quantum numbers. In many situations, it is possible to find the sectors even without knowing the quantities conserved by the Hamiltonian (see, for instance, [SKFP16]).
In the script shown below we construct an electron-electron interaction
Hamiltonian of an atomic \(d\)-shell using
Slater
parametrization. Then, we employ
the SpacePartition
and
BasisMapper
utility classes (and
optionally the make_matrix()
function)
to independently diagonalize the Hamiltonian within each sector.
1#
2# Sector-wise diagonalization of the Slater interaction Hamiltonian.
3#
4
5import numpy as np
6
7# Utility function used to construct the Slater interaction Hamiltonian.
8from pycommute.models import slater_int
9
10# Hilbert space.
11from pycommute.loperator import HilbertSpace
12
13# Real-valued linear operator object.
14from pycommute.loperator import LOperatorR
15# Automatic space partition and basis state mapper
16from pycommute.loperator import make_space_partition, BasisMapper
17# Build the matrix form of a linear operator
18from pycommute.loperator import make_matrix
19
20# Values of Slater radial integrals F^0, F^2, F^4
21F = np.array([4.0, 1.0, 0.2])
22
23# Build an expression for the interaction Hamiltonian.
24H = slater_int(F)
25
26# Analyze structure of 'H' and construct a suitable Hilbert space.
27hs = HilbertSpace(H)
28
29# Construct a linear operator corresponding to H and acting in the Hilbert
30# space 'hs'.
31H_op = LOperatorR(H, hs)
32
33# The first value returned by 'make_space_partition()' is a
34# SpacePartition object. It represents a partition of the full Hilbert space
35# into sectors, i.e. subspaces invariant under action of 'H_op'.
36#
37# The second returned value is a dictionary {(i, j): value} of all non-vanishing
38# matrix elements of H_{ij}. By definition, all matrix elements of 'H_op'
39# between different sectors vanish.
40sp, matrix_elements = make_space_partition(H_op, hs)
41
42# Print out information about the revealed sectors.
43print("Dimension of full Hilbert space:", sp.dim)
44print("Total number of sectors:", sp.n_subspaces)
45for i in range(sp.dim):
46 print("Many-body basis state %d belongs to sector %d" % (i, sp[i]))
47
48# Compile lists of basis states spanning each sector.
49sectors = sp.subspace_bases()
50
51# Diagonalize 'H_op' within each sector
52for n, sector in enumerate(sectors):
53 sector_dim = len(sector)
54 print("Diagonalizing sector %d of H_op" % n)
55 print("Sector size is %d" % sector_dim)
56
57 # A BasisMapper object translates indices of basis states from the full
58 # Hilbert space to a given sector.
59 basis_mapper = BasisMapper(sector)
60
61 #
62 # Prepare a matrix representation of 'H_op' within current sector.
63 #
64
65 # Method I (manual).
66 H_mat1 = np.zeros((sector_dim, sector_dim))
67 for i in range(sector_dim):
68 # A column vector psi = {0, 0, ..., 1, ..., 0}
69 psi = np.zeros(sector_dim)
70 psi[i] = 1.0
71 # This vector will receive the result of H_op * psi
72 phi = np.zeros(sector_dim)
73
74 # Since both 'psi' and 'phi' are 1D NumPy arrays with size of the chosen
75 # sector, 'H_op' cannot act on them directly as it expects vectors of
76 # size hs.dim.
77 # Instead, we are going to use our basis mapper object to construct
78 # special views of psi and phi.
79 psi_view = basis_mapper(psi)
80 phi_view = basis_mapper(phi)
81
82 # Now, 'H_op' can act on the mapped views.
83 H_op(psi_view, phi_view)
84
85 # Store H_op * psi in the i-th column of 'H_mat1'.
86 H_mat1[:, i] = phi
87
88 # Method II (automatic and faster).
89 H_mat2 = make_matrix(H_op, sector)
90 print("Max difference between H_mat and H_mat2:",
91 np.max(np.abs(H_mat1 - H_mat2)))
92
93 # Use NumPy to compute eigenvalues of 'H_mat1'.
94 E = np.linalg.eigvals(H_mat1)
95 print("Energies:", np.sort(E))
N-fermion sector views
This example shows how to partially diagonalize the single band Fermi-Hubbard model on a large 2D square lattice with periodic boundary conditions.
The number of atoms in the considered lattice (cluster) is set to 16, which makes for an intractably large Hilbert space of dimension \(2^{32}\). Since the Hamiltonian of the Hubbard model preserves total numbers of spin-up and spin-down electrons, one can diagonalize the Hamiltonian within a single \(N\)-fermion sector or within a \((N_\uparrow, N_\downarrow)\)-multisector.
An \(N\)-fermion
sector
is a subspace of
a full Hilbert space, which is spanned by all basis states with a fixed total
occupation of fermionic degrees of freedom (FDOF). Similarly, a
multisector
is
spanned by all basis states with a fixed occupation \(N_1\) of a subset of
the FDOF \(\{S_1\}\), occupation \(N_2\) of another subset
\(\{S_2\}\) and so on. There can be any number of pairs
\((\{S_i\}, N_i)\) (sectors contributing to the multisector) as long as
all the subsets \(\{S_i\}\) are disjoint.
In our example we consider a moderately sized sector with \(N = 2\) (\(\dim = {32 \choose 2} = 496\)) and a multisector with \(N_\uparrow = 1, N_\downarrow = 1\) (\(\dim = {16 \choose 1}{16 \choose 1} = 256\)).
Note
In general, the N-fermion (multi)sector views and functions do not require a purely fermionic system. The definitions of a sector and a multisector stand in presence of bosons or spins.
1import numpy as np
2
3# Utility functions used to construct the Fermi-Hubbard Hamiltonian.
4from pycommute.models import tight_binding, dispersion, hubbard_int
5
6# Hilbert space.
7from pycommute.loperator import HilbertSpace
8
9# Real-valued linear operator object.
10from pycommute.loperator import LOperatorR
11# Real-valued N-fermion (multi)sector views and related functions
12from pycommute.loperator import (
13 NFermionSectorViewR,
14 NFermionMultiSectorViewR,
15 n_fermion_sector_size,
16 n_fermion_multisector_size
17)
18
19from networkx.generators.lattice import grid_2d_graph
20from networkx.linalg.graphmatrix import adjacency_matrix
21
22#
23# Let us define Hamiltonian of a tight-binding model on a square lattice.
24#
25
26# Number of lattice sites in each direction
27# (the total number of sites is Nx * Ny).
28Nx = 4
29Ny = 4
30
31# Hopping constant
32t = 0.5
33# Chemical potential
34mu = 1.0
35# Coulomb repulsion
36U = 2.0
37
38# Use NetworkX to construct the periodic square lattice (graph)
39lat = grid_2d_graph(Nx, Ny, periodic=True)
40
41# Create lists of indices for spin-up and spin-down operators.
42# lat.nodes() returns a list of Nx * Ny pairs of site indices (x, y).
43indices_up = [(x, y, "up") for x, y in lat.nodes()]
44indices_dn = [(x, y, "down") for x, y in lat.nodes()]
45
46# A sum of tight-binding Hamiltonians for both spins.
47# The hopping matrix passed to tight_binding() is proportional to the
48# adjacency matrix of the lattice.
49hopping_matrix = -t * adjacency_matrix(lat).todense()
50H = tight_binding(hopping_matrix, indices=indices_up) \
51 + tight_binding(hopping_matrix, indices=indices_dn)
52
53# Add the chemical potential terms
54H += dispersion(-mu * np.ones(len(indices_up)), indices=indices_up)
55H += dispersion(-mu * np.ones(len(indices_dn)), indices=indices_dn)
56
57# Add the Hubbard interaction term
58H += hubbard_int(U * np.ones(len(indices_up)),
59 indices_up=indices_up,
60 indices_dn=indices_dn)
61
62# Analyze structure of H and construct a suitable Hilbert space.
63hs = HilbertSpace(H)
64print("Full Hilbert space dimension:", hs.dim)
65
66# Construct a linear operator corresponding to H and acting in the Hilbert
67# space 'hs'.
68H_op = LOperatorR(H, hs)
69
70#
71# Diagonalize the N = 2 sector of the model using 'NFermionSectorViewR'
72#
73
74N = 2
75
76sector_size = n_fermion_sector_size(hs, N)
77print("Size of the N = 2 sector:", sector_size)
78
79# Prepare a matrix representation of H_op within the N = 2 sector.
80H_mat = np.zeros((sector_size, sector_size))
81for i in range(sector_size):
82 # A column vector psi = {0, 0, ..., 1, ..., 0}
83 psi = np.zeros(sector_size)
84 psi[i] = 1.0
85 # This vector will receive the result of H_op * psi
86 phi = np.zeros(sector_size)
87
88 # Since both psi and phi are 1D NumPy arrays with size of the chosen
89 # sector, H_op cannot act on them directly as it expects vectors of
90 # size hs.dim.
91 # Instead, we are going to use real-valued 2-fermion sector views of
92 # psi and phi.
93 psi_view = NFermionSectorViewR(psi, hs, N)
94 phi_view = NFermionSectorViewR(phi, hs, N)
95
96 # Now, H_op can act on the mapped views.
97 H_op(psi_view, phi_view)
98
99 # Store H_op * psi in the i-th column of H_mat.
100 H_mat[:, i] = phi
101
102# Use NumPy to compute eigenvalues of H_mat.
103E = np.linalg.eigvals(H_mat).real
104print("10 lowest eigenvalues of the N = 2 sector:", np.sort(E)[:10])
105
106#
107# Diagonalize the N_up = 1, N_down = 1 multisector of the model using
108# 'NFermionMultiSectorViewR'
109#
110
111N_up = 1
112N_dn = 1
113
114# Define sectors contributing to the multisector
115sector_up = (indices_up, N_up)
116sector_dn = (indices_dn, N_dn)
117
118sectors = [sector_up, sector_dn]
119
120multisector_size = n_fermion_multisector_size(hs, sectors)
121print("Size of the N_up = 1, N_down = 1 multisector:", multisector_size)
122
123# Prepare a matrix representation of H_op within the multisector.
124H_mat = np.zeros((multisector_size, multisector_size))
125for i in range(multisector_size):
126 # A column vector psi = {0, 0, ..., 1, ..., 0}
127 psi = np.zeros(multisector_size)
128 psi[i] = 1.0
129 # This vector will receive the result of H_op * psi
130 phi = np.zeros(multisector_size)
131
132 # Since both psi and phi are 1D NumPy arrays with size of the chosen
133 # sector, H_op cannot act on them directly as it expects vectors of
134 # size hs.dim.
135 # Instead, we are going to use real-valued multisector views of psi and phi.
136 psi_view = NFermionMultiSectorViewR(psi, hs, sectors)
137 phi_view = NFermionMultiSectorViewR(phi, hs, sectors)
138
139 # Now, H_op can act on the mapped views.
140 H_op(psi_view, phi_view)
141
142 # Store H_op * psi in the i-th column of H_mat.
143 H_mat[:, i] = phi
144
145# Use NumPy to compute eigenvalues of H_mat.
146E = np.linalg.eigvals(H_mat).real
147print("10 lowest eigenvalues of the multisector:", np.sort(E)[:10])
References
“Quantum Integrals of Motion for the Heisenberg Spin Chain”, M. P. Grabowski and P. Mathieu, Mod. Phys. Lett. A, Vol. 09, No. 24, pp. 2197-2206 (1994), https://doi.org/10.1142/S0217732394002057
“Exact Solution for an N-Molecule-Radiation-Field Hamiltonian”, M. Tavis and F. W. Cummings Phys. Rev. 170, 379 (1968) https://doi.org/10.1103/PhysRev.170.379
“TRIQS/CTHYB: A continuous-time quantum Monte Carlo hybridisation expansion solver for quantum impurity problems”, P. Seth, I. Krivenko, M. Ferrero and O. Parcollet, Comp. Phys. Comm. 200, March 2016, 274-284, http://dx.doi.org/10.1016/j.cpc.2015.10.023 (section 4.2)