Usage examples
Kanamori interaction Hamiltonian: Basic usage of expressions
The Kanamori multiorbital Hamiltonian describes Coulomb repulsion of \(t_{2g}\) electronic states as relevant to a transition-metal ion in a cubic crystal field with an octahedral environment [GdMM2013]. In its rotationally invariant form, the Hamiltonian reads
The orbital indices \(m\), \(m'\) run over the \(t_{2g}\) triplet \(p_x, p_y, p_z\). \(U\) and \(J\) are Coulomb integrals (independent parameters of the model). The rotationally invariant Hamiltonian can be expressed in terms of a few integrals of motion,
where \(\hat N\) is the total number of electrons on the shell,
\(\mathbf{S}\) is the total spin vector,
and \(\mathbf{L}\) is the total orbital isospin,
The following script verifies that \(\hat H_K = \hat H_{t_{2g}}\) indeed.
1#
2# ## Kanamori interaction Hamiltonian
3# ### and its expression in terms of integrals of motion N, S^2 and L^2.
4#
5# "Strong Correlations from Hund's Coupling",
6# A. Georges, L. de' Medici and J. Mravlje,
7# Annu. Rev. Condens. Matter Phys. 2013. 4:137–78,
8# https://doi.org/10.1146/annurev-conmatphys-020911-125045
9#
10
11from itertools import product
12
13# Polynomial expression with real and complex coefficients
14from pycommute.expression import ExpressionR, ExpressionC
15
16# Factory functions returning expressions for fermionic creation, annihilation
17# and occupation number operators.
18from pycommute.expression import c_dag, c, n
19
20# Orbital degeneracy of the shell (t_{2g} triplet)
21n_orbs = 3
22# Coulomb integrals U and J
23U = 4.0
24J = 0.2
25
26# Kanamori Hamiltonian (Eq. (2))
27H_K = ExpressionR()
28
29# Intraorbital density-density interaction terms
30for m in range(n_orbs):
31 H_K += U * n(m, "up") * n(m, "down")
32
33# Interorbital density-density interaction terms (different spins)
34for m1, m2 in product(range(n_orbs), range(n_orbs)):
35 if m1 == m2:
36 continue
37 H_K += (U - 2 * J) * n(m1, "up") * n(m2, "down")
38
39# Interorbital density-density interaction terms (same spin)
40for m1, m2 in product(range(n_orbs), range(n_orbs)):
41 if m1 >= m2:
42 continue
43 H_K += (U - 3 * J) * n(m1, "up") * n(m2, "up")
44 H_K += (U - 3 * J) * n(m1, "down") * n(m2, "down")
45
46# Spin-flip terms
47for m1, m2 in product(range(n_orbs), range(n_orbs)):
48 if m1 == m2:
49 continue
50 H_K += \
51 -J * c_dag(m1, "up") * c(m1, "down") * c_dag(m2, "down") * c(m2, "up")
52
53# Pair-hopping terms
54for m1, m2 in product(range(n_orbs), range(n_orbs)):
55 if m1 == m2:
56 continue
57 H_K += J * c_dag(m1, "up") * c_dag(m1, "down") * c(m2, "down") * c(m2, "up")
58
59# Print the Hamiltonian
60print("H_K =", H_K)
61
62#
63# ### Integrals of motion N, S^2 and L^2 (Eq. (4))
64#
65
66# Total number of particles.
67N = ExpressionR()
68for m in range(n_orbs):
69 N += n(m, "up") + n(m, "down")
70
71# Total spin operators S_x, S_y, S_z.
72Sx = ExpressionC()
73Sy = ExpressionC()
74Sz = ExpressionC()
75for m in range(n_orbs):
76 Sx += 0.5 * (c_dag(m, "up") * c(m, "down") + c_dag(m, "down") * c(m, "up"))
77 Sy += 0.5j * (c_dag(m, "down") * c(m, "up") - c_dag(m, "up") * c(m, "down"))
78 Sz += 0.5 * (n(m, "up") - n(m, "down"))
79
80# Operator S^2 = S_x S_x + S_y S_y + S_z S_z.
81S2 = Sx * Sx + Sy * Sy + Sz * Sz
82
83
84# Levi-Civita symbol \epsilon_{ijk}.
85def eps(i, j, k):
86 return (j - i) * (k - j) * (k - i) / 2
87
88
89# Orbital isospin generators L_x, L_y, L_z.
90Lx = ExpressionC()
91Ly = ExpressionC()
92Lz = ExpressionC()
93
94for spin in ("up", "down"):
95 for m1, m2 in product(range(n_orbs), range(n_orbs)):
96 Lx += 1j * eps(0, m1, m2) * c_dag(m1, spin) * c(m2, spin)
97 Ly += 1j * eps(1, m1, m2) * c_dag(m1, spin) * c(m2, spin)
98 Lz += 1j * eps(2, m1, m2) * c_dag(m1, spin) * c(m2, spin)
99
100# Operator L^2 = L_x L_x + L_y L_y + L_z L_z.
101L2 = Lx * Lx + Ly * Ly + Lz * Lz
102
103#
104# ### Hamiltonian as a function of N, S^2 and L^2 (Eq. (7))
105#
106
107H_t2g = (U - 3 * J) / 2 * N * (N - 1.0) - 2 * J * S2 - J / 2 * L2 \
108 + (5.0 / 2) * J * N
109
110# Must be zero
111print("H_K - H_t2g =", H_K - H_t2g)
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# ### Hamiltonian of the electronic tight-binding model on a square lattice
17#
18
19# Number of lattice sites in each direction
20# (the total number of sites is N*N).
21N = 10
22
23# Electron hopping constant - energy parameter of the TB model.
24t = 2.0
25
26# Use NetworkX to construct the periodic square lattice (graph)
27lat = grid_2d_graph(N, N, periodic=True)
28
29# Create lists of indices for electronic spin-up and spin-down operators.
30# lat.nodes() returns a list of N^2 pairs of site indices (x, y).
31indices_up = [(x, y, "up") for x, y in lat.nodes()]
32indices_dn = [(x, y, "down") for x, y in lat.nodes()]
33
34# A sum of tight-binding Hamiltonians for both spins.
35# The hopping matrix passed to tight_binding() is proportional to the
36# adjacency matrix of the lattice.
37hopping_matrix = -t * adjacency_matrix(lat).todense()
38H_e = tight_binding(hopping_matrix, indices=indices_up) \
39 + tight_binding(hopping_matrix, indices=indices_dn)
40
41#
42# ### Hamiltonian of phonons localized at lattice sites
43#
44
45# Frequency of the localized phonon.
46w0 = 0.5
47
48# A lists of indices for bosonic operators, simply the (x, y) pairs
49indices_phonon = lat.nodes()
50
51# All N^2 phonons have the same frequency
52phonon_freqs = w0 * np.ones(N ** 2)
53H_ph = dispersion(phonon_freqs, indices=indices_phonon, statistics=BOSON)
54
55#
56# ### Hamiltonian of electron-phonon coupling
57#
58
59# Electron-phonon coupling constant.
60g = 0.1
61
62H_e_ph = holstein_int(g * np.ones(N ** 2),
63 indices_up=indices_up,
64 indices_dn=indices_dn,
65 indices_boson=indices_phonon)
66
67# Complete Holstein Hamiltonian.
68H_H = H_e + H_ph + H_e_ph
69
70# Print H_H. There will be quite a lot of terms for the 100-site lattice!
71print("H_H =", H_H)
72
73# Check hermiticity of H_H.
74print(r"H_H - H_H^\dagger =", H_H - conj(H_H))
75
76# Check that H_H commutes with the total number of electrons N_e.
77N_e = ExpressionR()
78for spin in ("up", "down"):
79 for x, y in product(range(N), range(N)):
80 N_e += n(x, y, spin)
81
82print("[H_H, N_e] =", H_H * N_e - N_e * H_H)
83
84#
85# ### Iteration interface
86#
87
88# Iterate over monomial-coefficient pairs in polynomial expression H_H
89for monomial, coeff in H_H:
90 print("Coefficient:", coeff)
91 # Iterate over algebra generators (creation/annihilation operators)
92 # in the monomial
93 for generator in monomial:
94 # Detect what algebra this generator belongs to
95 if generator.algebra_id == FERMION:
96 print("\tFermionic", end=' ')
97 elif generator.algebra_id == BOSON:
98 print("\tBosonic", end=' ')
99 # Creation or annihilation operator?
100 if generator.dagger:
101 print("creation operator", end=' ')
102 else:
103 print("annihilation operator", end=' ')
104 # Extract indices carried by the generator
105 print("with indices", list(generator.indices))
106 # N.B. generator.indices is an instance of a special tuple-like type
107 # `pycommute.expression.Indices`. Types of its elements are restricted
108 # to integer and string, and its comparison operators behave differently
109 # from those of the Python tuple. Otherwise, it supports `len()`,
110 # 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_tot 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
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(16, ...) returns the truncated bosonic space with allowed
36# occupation numbers N = 0, 1, ..., 15.
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(16, 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.vec_size, hs.vec_size))
53for i in range(hs.vec_size):
54 # A column vector psi = {0, 0, ..., 1, ..., 0}
55 psi = np.zeros(hs.vec_size)
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_mat1 and H_mat2:",
63 np.max(np.abs(H_mat1 - H_mat2)))
64
65#
66# ### Use NumPy to compute eigenvalues of `H_mat1`
67#
68
69E = np.linalg.eigvals(H_mat1)
70
71print("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 parameterization. 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 the 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_mat1 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.
1#
2# ## Fermi-Hubbard model on a large square lattice
3# ### and diagonalization of its moderately sized sectors
4#
5
6import numpy as np
7
8# Utility functions used to construct the Fermi-Hubbard Hamiltonian.
9from pycommute.models import tight_binding, dispersion, hubbard_int
10
11# Hilbert space.
12from pycommute.loperator import HilbertSpace
13
14# Real-valued linear operator object.
15from pycommute.loperator import LOperatorR
16# Real-valued N-fermion (multi)sector views and related functions
17from pycommute.loperator import (
18 NFermionSectorViewR,
19 NFermionMultiSectorViewR,
20 n_fermion_sector_size,
21 n_fermion_multisector_size
22)
23
24from networkx.generators.lattice import grid_2d_graph
25from networkx.linalg.graphmatrix import adjacency_matrix
26
27#
28# ### Hamiltonian of a tight-binding model on a square lattice
29#
30
31# Number of lattice sites in each direction
32# (the total number of sites is Nx * Ny).
33Nx = 4
34Ny = 4
35
36# Hopping constant
37t = 0.5
38# Chemical potential
39mu = 1.0
40# Coulomb repulsion
41U = 2.0
42
43# Use NetworkX to construct the periodic square lattice (graph)
44lat = grid_2d_graph(Nx, Ny, periodic=True)
45
46# Create lists of indices for spin-up and spin-down operators.
47# lat.nodes() returns a list of Nx * Ny pairs of site indices (x, y).
48indices_up = [(x, y, "up") for x, y in lat.nodes()]
49indices_dn = [(x, y, "down") for x, y in lat.nodes()]
50
51# A sum of tight-binding Hamiltonians for both spins.
52# The hopping matrix passed to tight_binding() is proportional to the
53# adjacency matrix of the lattice.
54hopping_matrix = -t * adjacency_matrix(lat).todense()
55H = tight_binding(hopping_matrix, indices=indices_up) \
56 + tight_binding(hopping_matrix, indices=indices_dn)
57
58# Add the chemical potential terms
59H += dispersion(-mu * np.ones(len(indices_up)), indices=indices_up)
60H += dispersion(-mu * np.ones(len(indices_dn)), indices=indices_dn)
61
62# Add the Hubbard interaction term
63H += hubbard_int(U * np.ones(len(indices_up)),
64 indices_up=indices_up,
65 indices_dn=indices_dn)
66
67# Analyze structure of H and construct a suitable Hilbert space.
68hs = HilbertSpace(H)
69print("Full Hilbert space dimension:", hs.dim)
70
71# Construct a linear operator corresponding to H and acting in the Hilbert
72# space 'hs'.
73H_op = LOperatorR(H, hs)
74
75#
76# ### Diagonalize the N = 2 sector of the model
77#
78
79N = 2
80
81sector_size = n_fermion_sector_size(hs, N)
82print("Size of the N = 2 sector:", sector_size)
83
84# Prepare a matrix representation of H_op within the N = 2 sector.
85H_mat = np.zeros((sector_size, sector_size))
86for i in range(sector_size):
87 # A column vector psi = {0, 0, ..., 1, ..., 0}
88 psi = np.zeros(sector_size)
89 psi[i] = 1.0
90 # This vector will receive the result of H_op * psi
91 phi = np.zeros(sector_size)
92
93 # Since both psi and phi are 1D NumPy arrays with size of the chosen
94 # sector, H_op cannot act on them directly as it expects vectors of
95 # size hs.dim.
96 # Instead, we are going to use real-valued 2-fermion sector views of
97 # psi and phi.
98 psi_view = NFermionSectorViewR(psi, hs, N)
99 phi_view = NFermionSectorViewR(phi, hs, N)
100
101 # Now, H_op can act on the mapped views.
102 H_op(psi_view, phi_view)
103
104 # Store H_op * psi in the i-th column of H_mat.
105 H_mat[:, i] = phi
106
107# Use NumPy to compute eigenvalues of H_mat.
108E = np.linalg.eigvals(H_mat).real
109print("10 lowest eigenvalues of the N = 2 sector:", np.sort(E)[:10])
110
111#
112# ### Diagonalize the N_up = 1, N_down = 1 multisector of the model
113#
114
115N_up = 1
116N_dn = 1
117
118# Define sectors contributing to the multisector
119sector_up = (indices_up, N_up)
120sector_dn = (indices_dn, N_dn)
121
122sectors = [sector_up, sector_dn]
123
124multisector_size = n_fermion_multisector_size(hs, sectors)
125print("Size of the N_up = 1, N_down = 1 multisector:", multisector_size)
126
127# Prepare a matrix representation of H_op within the multisector.
128H_mat = np.zeros((multisector_size, multisector_size))
129for i in range(multisector_size):
130 # A column vector psi = {0, 0, ..., 1, ..., 0}
131 psi = np.zeros(multisector_size)
132 psi[i] = 1.0
133 # This vector will receive the result of H_op * psi
134 phi = np.zeros(multisector_size)
135
136 # Since both psi and phi are 1D NumPy arrays with size of the chosen
137 # sector, H_op cannot act on them directly as it expects vectors of
138 # size hs.dim.
139 # Instead, we are going to use real-valued multisector views of psi and phi.
140 psi_view = NFermionMultiSectorViewR(psi, hs, sectors)
141 phi_view = NFermionMultiSectorViewR(phi, hs, sectors)
142
143 # Now, H_op can act on the mapped views.
144 H_op(psi_view, phi_view)
145
146 # Store H_op * psi in the i-th column of H_mat.
147 H_mat[:, i] = phi
148
149# Use NumPy to compute eigenvalues of H_mat.
150E = np.linalg.eigvals(H_mat).real
151print("10 lowest eigenvalues of the multisector:", np.sort(E)[:10])
Advanced: A user-defined algebra
It is possible to define generators of a new algebra in addition to the
fermionic, bosonic and spin operators to be used in
expressions. A basic introduction into the topic can be
found on the respective documentation page of
libcommute. Here, we show how to define the algebra of Dirac
\(\gamma\)-matrices in a Python script by extending the base class
pycommute.expression.Generator.
1#
2# ## User-defined algebra of Dirac gamma matrices
3#
4
5# Algebra generators and expressions
6from pycommute.expression import (Indices,
7 Generator,
8 MIN_USER_DEFINED_ALGEBRA_ID,
9 Monomial,
10 ExpressionC,
11 conj)
12
13
14#
15# Our generator type: a gamma matrix with one integer index \mu = 0, ..., 3
16#
17class GeneratorGamma(Generator):
18
19 # Initialize the base class by passing the index \mu to it.
20 def __init__(self, index):
21 Generator.__init__(self, Indices(index))
22
23 # Algebra ID of this generator. In this case, the lowest algebra ID
24 # available to user-defined algebras. Further algebras can use IDs
25 # MIN_USER_DEFINED_ALGEBRA_ID + 1, ...
26 def algebra_id(self):
27 return MIN_USER_DEFINED_ALGEBRA_ID
28
29 # This method will be called for g1 = self and g2 such that g1 > g2.
30 # We must transform the product g1 * g2 and put it into the
31 # canonical order,
32 #
33 # g1 * g2 -> -g2 * g1 + 2\eta(g1, g2) = -g2 * g1 + f.
34 def swap_with(self, g2, f):
35 # Update the LinearFunctionGen object 'f'
36 #
37 # Set the constant term 2\eta(g1, g2), where \eta is the diagonal
38 # Minkowski metric tensor.
39 mu = self.indices[0]
40 nu = g2.indices[0]
41 f.const_term = (mu == nu) * (2 if mu == 0 else -2)
42 # 'f' gets no contributions linear in the generators.
43 f.terms = []
44 return -1 # Return the coefficient in front of g2 * g1.
45
46 # This method will be called for g1 = self and g2 that are already
47 # canonically ordered, g1 <= g2.
48 #
49 # It tries to simplify squares of gamma matrices,
50 # f = (\gamma^0)^2 = I_4
51 # f = (\gamma^k)^2 = -I_4 for k = 1,2,3.
52 def simplify_prod(self, g2, f):
53 if self == g2:
54 # Update the LinearFunctionGen object 'f'.
55 #
56 # The constant term of 'f' is equal to either 1 or -1, depending
57 # on which gamma matrix is being squared.
58 mu = self.indices[0]
59 f.const_term = 1 if (mu == 0) else -1
60 # 'f' gets no contributions linear in the generators.
61 f.terms = []
62 return True # A square of a gamma matrix has been simplified.
63 else:
64 return False # No simplification has been applied.
65
66 # Compute Hermitian conjugate of a gamma matrix g = self,
67 # f = (\gamma_0)^+ = \gamma_0
68 # f = (\gamma_k)^+ = -\gamma_k, k=1,2,3.
69 #
70 # N.B.: If this method is not overridden in the subclass, all generators
71 # of the new algebra are assumed to be Hermitian.
72 def conj(self, f):
73 # Update the LinearFunctionGen object 'f'
74 #
75 # Hermitian conjugate of 'g' contains no constant contribution.
76 f.const_term = 0
77 # The only linear in generators term is proportional to the argument
78 # g = self, and the coefficient in front of it depends on the index.
79 mu = self.indices[0]
80 f.terms = [(self, 1 if (mu == 0) else -1)]
81
82 # String representation of this generator
83 def __repr__(self):
84 mu = self.indices[0]
85 return f"γ^{mu}"
86
87
88# Convenience function: Make an expression out of one gamma matrix.
89def gamma(mu):
90 return ExpressionC(1.0, Monomial([GeneratorGamma(mu)]))
91
92
93#
94# ### Check that expressions with gamma matrices behave as expected
95#
96
97# Minkowski metric tensor
98def eta(mu, nu):
99 return (mu == nu) * (1.0 if mu == 0 else -1.0)
100
101
102# Check the commutation relations
103for mu in range(4):
104 for nu in range(4):
105 gamma_mu = gamma(mu)
106 gamma_nu = gamma(nu)
107
108 print(f"{{{gamma_mu}, {gamma_nu}}} - 2η({mu}, {nu}) =",
109 (gamma_mu * gamma_nu + gamma_nu * gamma_mu - 2 * eta(mu, nu)))
110
111# \gamma^5
112gamma5 = 1j * gamma(0) * gamma(1) * gamma(2) * gamma(3)
113
114# \gamma^5 is Hermitian ...
115print("γ^5 - conj(γ^5) =", gamma5 - conj(gamma5))
116# ... and anti-commutes with \gamma_\mu.
117for mu in range(4):
118 gamma_mu = gamma(mu)
119 print(f"{{γ^5, {gamma_mu}}} =", gamma5 * gamma_mu + gamma_mu * gamma5)
References
“Strong Correlations from Hund’s Coupling”, A. Georges, L. de’ Medici and J. Mravlje, Annu. Rev. Condens. Matter Phys. 2013. 4:137–78, https://doi.org/10.1146/annurev-conmatphys-020911-125045
“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)