A Toolkit for Working with Models of Interacting Electrons
Fermions.jl is a toolkit for designing and analysing second-quantised many-particle Hamiltonians of electrons, potentially interacting with each other. The main point in designing this library is to abstract away the detailed task of writing matrices for many-body Hamiltonians and operators (for correlations functions) with large Hilbert spaces; all operators (including Hamiltonians) can be specified using predefined symbols, and the library then provides functions for diagonalising such Hamiltonians and computing observables within the states. (In case you are not accustomed to using second-quantised operators, check this brief explanation.)
This library was borne out of a need to numerically construct and solve fermionic Hamiltonians in the course of my doctoral research. While there are similar julia libraries such as Marco-Di-Tullio/Fermionic.jl and qojulia/QuantumOptics.jl, fermions.jl is much more intuitive since it works directly on predefined basis states and allows defining arbitrary fermionic operators and quantum mechanical states. There is no need to interact with complicated and abstract classes and objects in order to use this library; everything is defined purely in terms of simple datastructures such as dictionaries, vectors and tuples. This makes the entire process transparent and intuitive.
These are some of the useful features of this toolkit:
-
High-level of freedom in constructing fermionic Hamiltonians. All Hamiltonians that can be represented as a tensor product of 2-dimensional fermionic Fock-space operators can be modelled using fermions.jl. This covers all purely-electronic Hamiltonians, for example.
-
Uses optimised algorithms that make use of symmetries of the problem.
-
Provides a wide range of inbuilt functions for calculating various interesting quantities. The ability to construct any general correlation function by using fermionic operators further extends the range of possibilities.
-
Provides an iterative diagonalization function that is useful to working with systems of larger sizes.
You might find this library useful if you
-
spend a lot of time studying Hamiltonian models of fermions, particularly ones that cannot be solved analytically, or
-
use a similar library in another language (QuTip in python, for example), but want to migrate to Julia.
You will not find this useful if you
-
mostly work with bosonic systems and open quantum systems, or
-
work in the thermodynamic limit (using methods like quantum Monte Carlo, numerical RG).
The system must have a running Julia installation in order to use fermions.jl:
### install Julia on Unix-ish systems
$ curl -fsSL https://install.julialang.org | sh
### install Julia on windows
> winget install julia -s msstore
Once a running Julia environment is available, start the Julia REPL by running julia
from a terminal, and run the following commands to install the fermions.jl library:
julia> import Pkg
julia> Pkg.add(url="https://github.com/abhirup-m/fermions.jl")
This write-up is designed to show you very quickly how to start using this toolkit. It demonstrates the typical kind of calculations that are possible using the fermions.jl library. The model considered here is a prototypical one, involving spinless electrons hopping on a 1D lattice, with open boundary conditions (the electrons cannot hop beyond the left and right edges of the chain).
Schematic picture of the model being considered here. Circles represent lattice sites and arrows represent electron hopping processes across sites.
The Hamiltonian of the model is very simple; there are only two kinds of processes - one that starts from a lattice site
In this write-up, we will now show how to accomplish the following:
-
Define the above Hamiltonian and diagonalise it to obtain the spectrum.
-
Analyse the ground state and calculate the distribution of particles on the lattice in the ground state.
-
Calculate thermal correlation functions such as the local particle density at a non-zero temperature.
-
Calculate other useful quantities such as the entanglement of a region of space and the spectral function.
This quick-start example is also available as a notebook.
using fermions # Importing fermions.jl library
using Plots, Measures # Importing tools for plotting
Plots.theme(:dark)
Most applications of this libray require us to define a choice of basis states. The most convenient basis for us is the real space basis, consisting of states such [1, 0, 1, 1, ..., 0, 1]
, where each number in the vector can be 0 or 1, and the 1
) or unoccupied(0
). Within the library, we define the basis states using the function BasisStates:
basis = fermions.BasisStates(numSites, occupancy)
Here, the first argument numSites
is an Int
that represents the number of lattice sites that must be taken into account while creating the basis, while the second (optional) argument occupancy
is another Int
which represents the number of occupied particles in the basis states. For example, if we choose basis = fermions.BasisStates(2, 1)
, that will return the basis states involving two lattice sites and one electron occupying those sites, leading to the basis [1, 0], [0, 1]
.
numSites = 2 # number of lattice sites
occupancy = 1
basis = fermions.BasisStates(numSites, occupancy)
2-element Vector{Dict{BitVector, Float64}}:
Dict([1, 0] => 1.0)
Dict([0, 1] => 1.0)
The two basis states show the two possible combinations: [1, 0]
is the state with the first site occupied and the second site unoccupied, while [0, 1]
has the opposite configuration.
Any general second-quantised Hamiltonian can be represented, while using the library, as a 1D array. In order to explain how to create the Hamiltonian, we will start by giving a very simple example. The (non-Hermitian) Hamiltonian
H = [
("+-", [1, 2], 1.0),
("+-", [3, 4], -1.0),
]
Each tuple (...)
within the array represents a term of the full Hamiltonian. The term ("+-", [1, 2], 1.0)
; the string "+-"
encodes the fact that the operator is a product of a creation operator(+
) and an annihilation operator(-
), the vector [1,2]
indicates that the +
and -
operators act on the first and second indices within in each basis states, and the third (Float
) 1.0
indicates the strength of this term.
The second tuple ("+-", [3, 4], -1.0)
can be understood as a representation of the second operator
# designing the tight-binding Hamiltonian
function TightBindHamiltonian(numSites)
# define the array of tuples to store all terms in the Hamiltonian
hamiltonianTerms = Tuple{String, Vector{Int64}, Float64}[]
# loop over all lattice sites (skip the end, because we have c^\dagger_i+1
for i in 1:numSites-1
# the term c^\dagger_i c_i+1
term1 = ("+-", [i, i+1], 1.0)
# the term c^\dagger_i+1 c_i
term2 = ("+-", [i+1, i], 1.0)
# add both terms to the Hamiltonian
push!(hamiltonianTerms, term1)
push!(hamiltonianTerms, term2)
end
return hamiltonianTerms
end
Correlations are expectation values of observable operators, computed in the ground state: GenCorrelation(state, operator)
, where state
can be one of the eigenstates (where we want to compute the correlation), and operator
has to be constructed in the same way as the Hamiltonian.
As another physically-motivated demonstration, we will now calculate the real-space distribution of the ground state function, which is just the likelyhood of finding the electron at a given point in space. The probablity distribution at the operator
for this can therefore be constructed as operator=[("n", [i], 1.0)]
. The complete thing is shown below.
# get basis, Hamiltonian and eigenvectors.
numSites = 50
basis = fermions.BasisStates(numSites, 1)
hamiltonian = TightBindHamiltonian(numSites)
eigvals, eigvecs = fermions.Spectrum(hamiltonian, basis)
# Define the local density operator, which is just the number operator n_i at a site i.
# Within the fermions.jl library, the number operator is represented using "n", just as c
# and c^† are represented using "-" and "+".
densityOperators = [[("n", [i], 1.0)] for i in 1:numSites]
# calculate the expectation value of the local density operator in the ground state
# and a mid-spectrum state. the expectation value is calculated using an internal
# function GenCorrelation(state, operator), which returns the quantity <state|operator|state>.
distribution_low = [fermions.GenCorrelation(eigvecs[1], operator) for operator in densityOperators]
distribution_mid = [fermions.GenCorrelation(eigvecs[div(numSites, 2)], operator) for operator in densityOperators]
Plots.scatter([distribution_low, distribution_mid], thickness_scaling=1.4, linewidth=3, legend=true,
xlabel="odd lattice sites", ylabel="probability distribution", labels=["ground state" "excited state"], margin=-1mm)
As a second demonstration of the physics, we calculate the entanglement entropy of real space regions. Entanglement entropy is a measure of entanglement, quantifying how much a region of space is "connected" to the other regions. The source of the connection is the presence of the hopping term in the hamiltonian; as the electron hops from one end of the system to the other, it carries information with it, leading to non-local connections between distant regions. fermions.jl
provides several functions to calculate various entanglement measures such as the von Neumann entropy (vnEntropy()
), mutual information (mutInfo
) and tripartite information (tripInfo
). For this model, we will just calculate the von Neumann entropy of any given lattice site vnEntropy(state, [i])
, where state
is as usual the ground state, and [i]
indicates the set of indices whose entanglement entropy will be calculated. Since we want to calculate the entropy of just the
# define regions of space of increasing size
subsystemIndices = [[i] for i in 1:numSites]
# calculate entanglement entropy of these regions
SEE = [fermions.vnEntropy(eigvecs[1], indices) for indices in subsystemIndices]
Plots.plot(SEE, thickness_scaling=1.4, linewidth=3, legend=false,
xlabel="lattice site", ylabel="entanglement entropy", margin=-1mm)
One can also calculate thermal averages using the library. Similar to the GenCorrelation
function that computes expectation values in a particular state, the ThermalAverage(eigvecs, eigvals, operator, beta)
function accepts the complete set of eigenvectors
# define a range of temperatures to perform calculations at
invTempRange = 10 .^ range(-1, 2.5, 20)
# define the operators whose thermal average we wish to calculate
leftOccOperator = [("n", [1], 1.0)] # occupancy of left edge
centerOccOperator = [("n", [div(numSites, 2)], 1.0)] # occupancy of center
# calculate thermal averages over the given range of inverse temperature
leftOccAverage = [fermions.ThermalAverage(eigvecs, eigvals, leftOccOperator, beta) for beta in invTempRange]
centerOccAverage = [fermions.ThermalAverage(eigvecs, eigvals, centerOccOperator, beta) for beta in invTempRange]
Plots.plot(1 ./ invTempRange, [leftOccAverage, centerOccAverage];
thickness_scaling=1.5, linewidth=3,
xaxis=:log10, xlabel="temp. / \$\\Delta E\$", ylabel="local occupancy",
labels=["left edge" "center"], leftmargin=-5mm, bottommargin=-3mm)
As a final demonstration, we show how to calculate the local spectral function for the model. A spectral function
where
The spectral function is calculated using the provided function SpecFunc(eigvecs, eigvals, probe, probeDag, freqArr, broadening)
, where eigvecs, eigvals
constitute the spectrum of the system, probe
and probeDag
are the excitation and relaxation operators freqArr
is the set of frequency points at which the spectral function must be calculated, and broadening
is the width of the Lorentzian that will be used to replace poles. For the snippet below, we have taken the local operators
numSites = 40 # number of lattice sites
# Get basis states for a system with 0, 1 and 2 occupied sites.
# The ground state will be taken from the 1 occupied site system,
# but the excitations will take it to the other systems.
basis_N = fermions.BasisStates(numSites, 1)
basis_Nminus1 = fermions.BasisStates(numSites, 0)
basis_Nplus1 = fermions.BasisStates(numSites, 2)
# diagonalise all three systems
tightBindHam = TightBindHamiltonian(numSites)
eigvals_N, eigvecs_N = fermions.Spectrum(tightBindHam, basis_N)
eigvals_Nminus1, eigvecs_Nminus1 = fermions.Spectrum(tightBindHam, basis_Nminus1)
eigvals_Nplus1, eigvecs_Nplus1 = fermions.Spectrum(tightBindHam, basis_Nplus1)
# define the set of frequency points
freqArr = collect(range(-2.5, stop=2.5, step=0.01))
# define the excitation and relaxation operators
probe = [("-", [div(numSites, 2)], 1.0)]
probeDag = [("+", [div(numSites, 2)], 1.0)]
# get spectral function
specfunc = fermions.SpecFunc([eigvals_N; eigvals_Nminus1; eigvals_Nplus1],
[eigvecs_N; eigvecs_Nminus1; eigvecs_Nplus1], probe, probeDag, freqArr, 1e-2)
p = Plots.plot(freqArr, specfunc, thickness_scaling=1.5, linewidth=2, legend=false,
xlabel="frequency \$\\omega\$", ylabel="spectral function\$", margin=-2mm)
display(p)
The entire library is built on the idea of using second-quantisation to study many-body Hamiltonians, so a discussion of that is certainly pertinent. Second quantised operators are a convenient way of writing many-body Hamiltonians. This library is only concerned with electrons; these are particles whose states must be antisymmetric with respect to an exchange of the particles. For example, a state of electrons at positions 1 and 2 has to be
As an example, we wish to design the Hamiltonian of a single electron hopping across a 1D chain of lattice sites labelled as
Now, the indices 1 and 2 can be represented by any consecutive indices