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

\[\hat H = g \sum_i \mathbf{S}_i \cdot \mathbf{S}_{i+1}\]

conserves three projections of the total spin

\[\mathbf{S} = \sum_i \mathbf{S}_i\]

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),

\[\hat H = \epsilon (\hat S_{z,0} + \hat S_{z,1}) + \omega \hat a^\dagger \hat a + g_0 (\hat a^\dagger \hat S_{-,0} + \hat a \hat S_{+,0}) + g_1 (\hat a^\dagger \hat S_{-,1} + \hat a \hat S_{+,1}).\]

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.

\[\hat H = -t\sum_{\langle i,j \rangle, \sigma} c^\dagger_{i,\sigma} c_{j,\sigma} -\mu \sum_{i, \sigma} n_{i,\sigma} + U \sum_i n_{i,\uparrow} n_{i,\downarrow}.\]

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

[GM94]

“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

[TC68]

“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

[SKFP16]

“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)