Skip to content

Commit 637dd24

Browse files
authored
Corbett/types and docs (#9)
* Specify type of MPO and pass OpCacheVec as strings * Docs and examples
1 parent e0cb5bf commit 637dd24

File tree

7 files changed

+423
-150
lines changed

7 files changed

+423
-150
lines changed

README.md

Lines changed: 60 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![Coverage](https://codecov.io/gh/ITensor/ITensorMPOConstruction.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/ITensor/ITensorMPOConstruction.jl)
77
[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle)
88

9-
A fast algorithm for constructing a Matrix Product Operator (MPO) from a sum of local operators. This is a replacement for `MPO(os::OpSum, sites::Vector{<:Index})`. In all cases examined so far this algorithm constructs an MPO with a smaller (or equal) bond dimension faster than the competition.
9+
A fast algorithm for constructing a Matrix Product Operator (MPO) from a sum of local operators. This is a replacement for `MPO(os::OpSum, sites::Vector{<:Index})`. In all cases examined so far this algorithm constructs an MPO with a smaller (or equal) bond dimension faster than the competition. All runtimes below are taken from a single sample on a 2021 MacBook Pro with the M1 Max CPU and 32GB of memory.
1010

1111
## Installation
1212

@@ -17,15 +17,15 @@ julia> using Pkg; Pkg.add(url="https://github.com/ITensor/ITensorMPOConstruction
1717

1818
## Constraints
1919

20-
This algorithm shares same constraints as ITensor's default algorithm.
20+
This algorithm shares the same constraints as ITensors' default algorithm.
2121

22-
* The operator must be expressed as a sum of products of single site operators. For example a CNOT could not appear in the sum since it is a two site operator.
23-
* When dealing with Fermionic systems the parity of each term in the sum must be even. That is the combined number of creation and annihilation operators must be even.
22+
1. The operator must be expressed as a sum of products of single site operators. For example a CNOT could not appear in the sum since it is a two site operator.
23+
2. When dealing with Fermionic systems the parity of each term in the sum must be even. That is the combined number of creation and annihilation operators in each term in the sum must be even.
2424

2525
There are also two additional constraints:
2626

27-
* Each term in the sum of products representation can only have a single operator acting on a site. For example a term such as $\mathbf{X}^{(1)} \mathbf{X}^{(1)}$ is not allowed. However, there is a pre-processing utility that can automatically replace $\mathbf{X}^{(1)} \mathbf{X}^{(1)}$ with $\mathbf{I}^{(1)}$. This is not a hard requirement for the algorithm, just a simplification to improve performance. Many operators of interest can be easily expressed in a form where only a single operator acts on each site in a term.
28-
* When constructing a quantum number (QN) conserving operator the total flux of the operator must be zero. It would be trivial to remove this constraint.
27+
3. Each term in the sum of products representation can only have a single operator acting on a site. For example a term such as $\mathbf{X}^{(1)} \mathbf{X}^{(1)}$ is not allowed. However, there is a pre-processing utility that can automatically replace $\mathbf{X}^{(1)} \mathbf{X}^{(1)}$ with $\mathbf{I}^{(1)}$. This is not a hard requirement for the algorithm but a simplification to improve performance.
28+
4. When constructing a quantum number conserving operator the total flux of the operator must be zero. It would be trivial to remove this constraint.
2929

3030
## `MPO_new`
3131

@@ -36,8 +36,22 @@ function MPO_new(os::OpSum, sites::Vector{<:Index}; kwargs...)::MPO
3636
```
3737

3838
The optional keyword arguments are
39-
* `tol::Real`: The tolerance used in the sparse QR decomposition. The default value is calculated separately for each QR decomposition, it is almost always a good value.
40-
* `basisOpCacheVec::OpCacheVec`: A list of operators to use as a basis for each site. The operators on at each site are expressed as one of these basis operators.
39+
* `basisOpCacheVec`: A list of operators to use as a basis for each site. The operators on each site are expressed as one of these basis operators.
40+
* `tol::Real`: The tolerance used in the sparse QR decomposition (which is done by SPQR). The default value is the SPQR default which is calculated separately for each QR decomposition. If you want a MPO that is accurate up to floating point errors the default tolerance should work well. If instead you want to compress the MPO the value `tol` will differ from the `cutoff` passed to `ITensor.MPO` since the truncation method is completely different. If you want to replicate the same truncation behavior first construct the MPO with a suitably small (or default) `tol` and then use `ITensors.truncate!`.
41+
42+
## Examples: Fermi-Hubbard Hamiltonian in Real Space
43+
44+
The one dimensional Fermi-Hubbard Hamiltonian with periodic boundary conditions on $N$ sites can be expressed in real space as
45+
46+
$$
47+
\mathcal{H} = -t \sum_{i = 1}^N \sum_{\sigma \in (\uparrow, \downarrow)} \left( c^\dagger_{i, \sigma} c_{i + 1, \sigma} + c^\dagger_{i, \sigma} c_{i - 1, \sigma} \right) + U \sum_{i = 1}^N n_{i, \uparrow} n_{i, \downarrow} \ ,
48+
$$
49+
50+
where the periodic boundary conditions enforce that $c_k = c_{k + N}$. For this Hamiltonian all that needs to be done to switch over to using ITensorMPOConstruction is switch `MPO(os, sites)` to `MPO_New(os, sites)`.
51+
52+
https://github.com/ITensor/ITensorMPOConstruction.jl/blob/main/examples/fermi-hubbard.jl#L4-L24
53+
54+
For $N = 1000$ both ITensors and ITensorMPOConstruction can construct an MPO of bond dimension 10 in under two seconds. For a compelling reason to use ITensorMPOConstruction we need to look at a more complicated Hamiltonian.
4155

4256
## Examples: Fermi-Hubbard Hamiltonian in Momentum Space
4357

@@ -47,86 +61,57 @@ $$
4761
\mathcal{H} = \sum_{k = 1}^N \epsilon(k) \left( n_{k, \downarrow} + n_{k, \uparrow} \right) + \frac{U}{N} \sum_{p, q, k = 1}^N c^\dagger_{p - k, \uparrow} c^\dagger_{q + k, \downarrow} c_{q, \downarrow} c_{p, \uparrow}
4862
$$
4963

50-
where $\epsilon(k) = -2 t \cos(\frac{2 \pi k}{N})$ and $c_k = c_{k + N}$. Below is a plot of the bond dimension of the MPO produced by ITensors' default algorithm, [Renormalizer](https://github.com/shuaigroup/Renormalizer) which uses the [bipartite-graph algorithm](https://doi.org/10.1063/5.0018149), and `ITensorMPOConstruction`.
64+
where $\epsilon(k) = -2 t \cos(\frac{2 \pi k}{N})$ and $c_k = c_{k + N}$. The code to construct the `OpSum` is shown below.
5165

52-
![](./docs/plot-generators/fh.png)
66+
https://github.com/ITensor/ITensorMPOConstruction.jl/blob/main/examples/fermi-hubbard.jl#L26-L49
5367

54-
Of note is that the bond dimension of the MPO produced by Renormalizer scales as $O(N^2)$, both ITensors and ITensorMPOConstruction however produce an MPO with a bond dimension that scales as $O(N)$. Below is a table of the time it took to construct the MPO for various number of sites. Some warm up was done for the Julia calculations to avoid measuring compilation overhead. Data recorded on 2021 MacBook Pro with the M1 Max CPU and 32GB of memory.
68+
Unlike the previous example, some more involved changes will be required to use ITensorMPOConstruction. This is because the `OpSum` has multiple operators acting on the same site, violating constraint #3. For example when $k = 0$ in the second loop we have terms of the form $c^\dagger_{p, \uparrow} c^\dagger_{q, \downarrow} c_{q, \downarrow} c_{p, \uparrow}$. You could always create a special case for $k = 0$ and rewrite it as $n_{p, \uparrow} n_{q, \downarrow}$. However if using "Electron" sites then you would also need to consider other cases such as when $p = q$, this would introduce a lot of extraneous complication. Luckily ITensorMPOConstruction provides a method to automatically perform these transformations. If you provide a set of operators for each site to `MPO_new` it will attempt to express the operators acting on each site as a single one of these "basis" operators. The code to do this is shown below.
5569

56-
| $N$ | ITensors | Renormalizer | ITensorMPOConstruction |
57-
|-----|----------|--------------|------------------------|
58-
| 10 | 0.35s | 0.26 | 0.03s |
59-
| 20 | 27s | 3.4s | 0.15s |
60-
| 30 | N/A | 17s | 0.41s |
61-
| 40 | N/A | 59s | 0.93s |
62-
| 50 | N/A | 244s | 1.8s |
63-
| 100 | N/A | N/A | 20s |
64-
| 200 | N/A | N/A | 310s |
70+
https://github.com/ITensor/ITensorMPOConstruction.jl/blob/main/examples/fermi-hubbard.jl#L51-L76
6571

72+
### `OpIDSum`
6673

67-
The code for this example can be found in [examples/fermi-hubbard.jl](https://github.com/ITensor/ITensorMPOConstruction.jl/blob/main/examples/fermi-hubbard.jl). Just like with ITensors, the terms in the Hamiltonian are put into an `OpSum` and then the `OpSum` is transformed into an `MPO`. The code to produce the `OpSum` is
74+
For $N = 200$ constructing the `OpSum` takes 42s and constructing the MPO from the `OpSum` with ITensorMPOConstruction takes another 306s. For some systems constructing the `OpSum` can actually be the bottleneck. In these cases you can construct an `OpIDSum` instead.
6875

69-
```julia
70-
os = OpSum{Float64}()
71-
for k in 1:N
72-
epsilon = -2 * t * cospi(2 * k / N)
73-
os .+= epsilon, "Nup", k
74-
os .+= epsilon, "Ndn", k
75-
end
76-
77-
for p in 1:N
78-
for q in 1:N
79-
for k in 1:N
80-
os .+= U / N, "Cdagup", mod1(p - k, N), "Cdagdn", mod1(q + k, N), "Cdn", q, "Cup", p
81-
end
82-
end
83-
end
84-
```
76+
`OpIDSum` plays the same roll as `OpSum` but in a much more efficient manner. To specify an operator in a term of an `OpSum` you specify a string (the operator's name) and a site index, whereas to specify an operator in a term of an `OpIDSum` you specify an `OpID` which contains an operator index and a site. The operator index is the index of the operator in the provided basis for the site.
8577

86-
The astute reader will notice that this `OpSum` has multiple operators acting on the same site. For example, it contains the term `"Cdagup", 1, "Cdagdn", 1, "Cdn", 1, "Cup", 1`. If we try and pass this `OpSum` to `MPO_New` directly it will throw an error. However, `"Cdagup", 1, "Cdagdn", 1, "Cdn", 1, "Cup", 1` is equivalent to `"Nup * Ndn", 1` and by passing a set of basis operators to `MPO_New` we can automatically convert any product of operators acting on a single site into one of these single basis operators. This is accomplished by the following
78+
For $N = 200$ constructing an `OpIDSum` takes only 0.4s. Shown below is code to construct the Hamiltonian using an `OpIDSum`.
8779

88-
```julia
89-
sites = siteinds("Electron", N; conserve_qns=true)
90-
91-
operatorNames = [
92-
"I",
93-
"Cdn",
94-
"Cup",
95-
"Cdagdn",
96-
"Cdagup",
97-
"Ndn",
98-
"Nup",
99-
"Cup * Cdn",
100-
"Cup * Cdagdn",
101-
"Cup * Ndn",
102-
"Cdagup * Cdn",
103-
"Cdagup * Cdagdn",
104-
"Cdagup * Ndn",
105-
"Nup * Cdn",
106-
"Nup * Cdagdn",
107-
"Nup * Ndn",
108-
]
109-
110-
opCacheVec = [
111-
[OpInfo(ITensors.Op(name, n), sites[n]) for name in operatorNames] for
112-
n in eachindex(sites)
113-
]
114-
115-
return MPO_new(os, sites; basisOpCacheVec=opCacheVec)
116-
```
80+
https://github.com/ITensor/ITensorMPOConstruction.jl/blob/main/examples/fermi-hubbard.jl#L79-L130
81+
82+
## Benchmarks: Fermi-Hubbard Hamiltonian in Momentum Space
83+
84+
Below is a plot of the bond dimension of the MPO produced by ITensors' default algorithm, [Renormalizer](https://github.com/shuaigroup/Renormalizer) which uses the [bipartite-graph algorithm](https://doi.org/10.1063/5.0018149), and ITensorMPOConstruction.
85+
86+
![](./docs/plot-generators/fh.png)
87+
88+
Of note is that the bond dimension of the MPO produced by Renormalizer scales as $O(N^2)$, both ITensors and ITensorMPOConstruction however produce an MPO with a bond dimension that scales as $O(N)$.
89+
90+
Below is a table of the time it took to construct the MPO (including the time it took to specify the Hamiltonian) for various number of sites. For ITensorMPOConstruction an `OpIDSum` was used. Some warm up was done for the Julia calculations to avoid measuring compilation overhead.
91+
92+
| $N$ | ITensors | Renormalizer | ITensorMPOConstruction |
93+
|-----|----------|--------------|------------------------|
94+
| 10 | 0.35s | 0.26 | 0.04s |
95+
| 20 | 27s | 3.4s | 0.10s |
96+
| 30 | N/A | 17s | 0.24s |
97+
| 40 | N/A | 59s | 0.59s |
98+
| 50 | N/A | 244s | 1.2s |
99+
| 100 | N/A | N/A | 16s |
100+
| 200 | N/A | N/A | 283s |
117101

118-
## Examples: Electronic Structure Hamiltonian
102+
## Benchmarks: Electronic Structure Hamiltonian
119103

120-
After looking at the previous example one might assume that the relative speed of ITensorMPOConstruction over Renormalizer might be due to the fact that for the Fermi-Hubbard Hamiltonian ITensorMPOConstruction is able to construct a much more compact MPO. In the case of the electronic structure Hamiltonian all algorithms produce MPOs of similar bond dimensions.
104+
After looking at the previous example you might assume that the relative speed of ITensorMPOConstruction over Renormalizer might be due to the fact that for the Fermi-Hubbard Hamiltonian ITensorMPOConstruction is able to construct a more compact MPO. In the case of the electronic structure Hamiltonian all algorithms produce MPOs of similar bond dimensions.
121105

122106
![](./docs/plot-generators/es.png)
123107

124-
However ITensorMPOConstruction is still an order of magnitude faster than Renormalizer. The code for this example can be found in [examples/electronic-structure.jl](https://github.com/ITensor/ITensorMPOConstruction.jl/blob/main/examples/electronic-structure.jl). The run time to generate these MPOs are shown below (again on a 2021 MacBook Pro with the M1 Max CPU and 32GB of memory).
108+
However, ITensorMPOConstruction is still an order of magnitude faster than Renormalizer. The code for this example can be found in [examples/electronic-structure.jl](https://github.com/ITensor/ITensorMPOConstruction.jl/blob/main/examples/electronic-structure.jl). The run time to generate these MPOs (including the time it took to specify the Hamiltonian) are shown below. For ITensorMPOConstruction an `OpIDSum` was used.
125109

126110
| $N$ | ITensors | Renormalizer | ITensorMPOConstruction |
127111
|-----|----------|--------------|------------------------|
128-
| 10 | 3.0s | 2.1s | 0.37s |
129-
| 20 | 498s | 61s | 7.1s |
130-
| 30 | N/A | 458s | 46s |
131-
| 40 | N/A | 2250s | 183 |
132-
| 50 | N/A | N/A | 510s |
112+
| 10 | 3.0s | 2.1s | 0.31s |
113+
| 20 | 498s | 61s | 5.9s |
114+
| 30 | N/A | 458s | 36s |
115+
| 40 | N/A | 2250s | 162s |
116+
| 50 | N/A | N/A | 504s |
117+
| 60 | N/A | N/A | 1323s |

examples/electronic-structure.jl

Lines changed: 119 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,43 @@
11
using ITensorMPOConstruction
22
using ITensors
33

4-
function electronic_structure_example(
4+
function electronic_structure(
55
N::Int, complexBasisFunctions::Bool; useITensorsAlg::Bool=false
66
)::MPO
7-
coeff() = rand() + 1im * complexBasisFunctions * rand()
7+
coeff() = !complexBasisFunctions ? rand() : rand() + 1im * rand()
88

99
os = complexBasisFunctions ? OpSum{ComplexF64}() : OpSum{Float64}()
10-
for a in 1:N
11-
for b in a:N
12-
epsilon_ab = coeff()
13-
os .+= epsilon_ab, "Cdagup", a, "Cup", b
14-
os .+= epsilon_ab, "Cdagdn", a, "Cdn", b
15-
16-
a == b && continue
17-
os .+= conj(epsilon_ab), "Cdagup", b, "Cup", a
18-
os .+= conj(epsilon_ab), "Cdagdn", b, "Cdn", a
10+
@time "\tConstructing OpSum" let
11+
for a in 1:N
12+
for b in a:N
13+
epsilon_ab = coeff()
14+
os .+= epsilon_ab, "Cdagup", a, "Cup", b
15+
os .+= epsilon_ab, "Cdagdn", a, "Cdn", b
16+
17+
a == b && continue
18+
os .+= conj(epsilon_ab), "Cdagup", b, "Cup", a
19+
os .+= conj(epsilon_ab), "Cdagdn", b, "Cdn", a
20+
end
1921
end
20-
end
2122

22-
for j in 1:N
23-
for s_j in ("dn", "up")
24-
for k in 1:N
25-
s_k = s_j
26-
(s_k, k) <= (s_j, j) && continue
23+
for j in 1:N
24+
for s_j in ("dn", "up")
25+
for k in 1:N
26+
s_k = s_j
27+
(s_k, k) <= (s_j, j) && continue
2728

28-
for l in 1:N
29-
for s_l in ("dn", "up")
30-
(s_l, l) <= (s_j, j) && continue
29+
for l in 1:N
30+
for s_l in ("dn", "up")
31+
(s_l, l) <= (s_j, j) && continue
3132

32-
for m in 1:N
33-
s_m = s_l
34-
(s_m, m) <= (s_k, k) && continue
33+
for m in 1:N
34+
s_m = s_l
35+
(s_m, m) <= (s_k, k) && continue
3536

36-
value = coeff()
37-
os .+= value, "Cdag$s_j", j, "Cdag$s_l", l, "C$s_m", m, "C$s_k", k
38-
os .+= conj(value), "Cdag$s_k", k, "Cdag$s_m", m, "C$s_l", l, "C$s_j", j
37+
value = coeff()
38+
os .+= value, "Cdag$s_j", j, "Cdag$s_l", l, "C$s_m", m, "C$s_k", k
39+
os .+= conj(value), "Cdag$s_k", k, "Cdag$s_m", m, "C$s_l", l, "C$s_j", j
40+
end
3941
end
4042
end
4143
end
@@ -47,7 +49,7 @@ function electronic_structure_example(
4749

4850
## The only additional step required is to provide an operator basis in which to represent the OpSum.
4951
if useITensorsAlg
50-
return MPO(os, sites)
52+
return @time "\tConstrucing MPO" MPO(os, sites)
5153
else
5254
operatorNames = [
5355
"I",
@@ -73,18 +75,102 @@ function electronic_structure_example(
7375
n in eachindex(sites)
7476
]
7577

76-
return MPO_new(os, sites; basisOpCacheVec=opCacheVec)
78+
return @time "\tConstrucing MPO" MPO_new(os, sites; basisOpCacheVec=opCacheVec)
7779
end
7880
end
7981

80-
let
81-
N = 25
82+
function electronic_structure_OpIDSum(N::Int, complexBasisFunctions::Bool)::MPO
83+
operatorNames = [
84+
[
85+
"I",
86+
"Cdn",
87+
"Cup",
88+
"Cdagdn",
89+
"Cdagup",
90+
"Ndn",
91+
"Nup",
92+
"Cup * Cdn",
93+
"Cup * Cdagdn",
94+
"Cup * Ndn",
95+
"Cdagup * Cdn",
96+
"Cdagup * Cdagdn",
97+
"Cdagup * Ndn",
98+
"Nup * Cdn",
99+
"Nup * Cdagdn",
100+
"Nup * Ndn",
101+
] for _ in 1:N
102+
]
103+
104+
= false
105+
= true
106+
107+
opC(k::Int, spin::Bool) = OpID(2 + spin, k)
108+
opCdag(k::Int, spin::Bool) = OpID(4 + spin, k)
109+
opN(k::Int, spin::Bool) = OpID(6 + spin, k)
110+
111+
coeff() = !complexBasisFunctions ? rand() : rand() + 1im * rand()
82112

83-
# Ensure compilation
84-
electronic_structure_example(5, false)
113+
os = complexBasisFunctions ? OpIDSum{ComplexF64}() : OpIDSum{Float64}()
114+
@time "\tConstructing OpIDSum" let
115+
for a in 1:N
116+
for b in a:N
117+
epsilon_ab = coeff()
118+
push!(os, epsilon_ab, opCdag(a, ), opC(b, ))
119+
push!(os, epsilon_ab, opCdag(a, ), opC(b, ))
120+
121+
a == b && continue
122+
push!(os, conj(epsilon_ab), opCdag(b, ), opC(a, ))
123+
push!(os, conj(epsilon_ab), opCdag(b, ), opC(a, ))
124+
end
125+
end
126+
127+
for j in 1:N
128+
for s_j in (, )
129+
for k in 1:N
130+
s_k = s_j
131+
(s_k, k) <= (s_j, j) && continue
132+
133+
for l in 1:N
134+
for s_l in (, )
135+
(s_l, l) <= (s_j, j) && continue
136+
137+
for m in 1:N
138+
s_m = s_l
139+
(s_m, m) <= (s_k, k) && continue
140+
141+
value = coeff()
142+
push!(os, value, opCdag(j, s_j), opCdag(l, s_l), opC(m, s_m), opC(k, s_k))
143+
push!(
144+
os, conj(value), opCdag(k, s_k), opCdag(m, s_m), opC(l, s_l), opC(j, s_j)
145+
)
146+
end
147+
end
148+
end
149+
end
150+
end
151+
end
152+
end
153+
154+
sites = siteinds("Electron", N; conserve_qns=true)
155+
return @time "\tConstructing MPO" MPO_new(
156+
os, sites, operatorNames; basisOpCacheVec=operatorNames
157+
)
158+
end
159+
160+
let
161+
N = 20
162+
useITensorsAlg = false
85163

86164
println("Constructing the Electronic Structure MPO for $N orbitals")
87-
@time mpo = electronic_structure_example(N, false)
165+
@time "Total" mpo = electronic_structure(N, false; useITensorsAlg=useITensorsAlg)
166+
println("The maximum bond dimension is $(maxlinkdim(mpo))")
167+
end
168+
169+
let
170+
N = 20
171+
172+
println("Constructing the Electronic Structure MPO for $N orbitals using OpIDSum")
173+
@time "Total" mpo = electronic_structure_OpIDSum(N, false)
88174
println("The maximum bond dimension is $(maxlinkdim(mpo))")
89175
end
90176

0 commit comments

Comments
 (0)