Skip to content

Commit 0fdcd15

Browse files
authoredMar 18, 2025
Implement charge shift for LocalOperator and add corresponding examples (#135)
* Implement `add_physical_charge` for `LocalOperator` * Format * Add some more models * Start adding some tests * Drop some charges * Actually drop some charges * Tweak Fermi-Hubbard test * Tweak and lighten fermi-hubbard test * Relax xxz test * Bose-hubbard test is very broken * Fix Bose-Hubbard test, some updates * Update kwarg names * Add reference for Fermi-Hubbard * Add expanded Bose-Hubbard example * Update Bose-Hubbard test, remove XXZ and Fermi-Hubbard tests * Add access to linsearch maxiter and maxfg * Add XXZ and Fermi-Hubbard example stubs * Reduce optimization maxiter in some tests * Use proper keyword syntax
1 parent 3f62147 commit 0fdcd15

File tree

11 files changed

+390
-7
lines changed

11 files changed

+390
-7
lines changed
 

‎examples/bose_hubbard.jl

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using Test
2+
using Random
3+
using PEPSKit
4+
using TensorKit
5+
using KrylovKit
6+
using OptimKit
7+
8+
using MPSKit: add_physical_charge
9+
10+
# This example demonstrates the simulation of the two-dimensional Bose-Hubbard model using
11+
# PEPSKit.jl. In particular, it showcases the use of internal symmetries and finite particle
12+
# densities in PEPS simulations.
13+
14+
## Defining the model
15+
16+
# We will construct the Bose-Hubbard model Hamiltonian through the
17+
# [`bose_hubbard_model` function from MPSKitModels.jl](https://quantumkithub.github.io/MPSKitModels.jl/dev/man/models/#MPSKitModels.bose_hubbard_model),
18+
# as reexported by PEPSKit.jl. We'll simulate the model in its Mott-insulating phase
19+
# where the ratio U/t is large, since in this phase we expect the ground state to be well
20+
# approximated by a PEPS with a manifest global U(1) symmetry. Furthermore, we'll impose
21+
# a cutoff at 2 bosons per site, set the chemical potential to zero and use a simple 1x1
22+
# unit cell.
23+
24+
t = 1.0
25+
U = 30.0
26+
cutoff = 2
27+
mu = 0.0
28+
lattice = InfiniteSquare(1, 1)
29+
30+
# We'll impose an explicit global U(1) symmetry as well as a fixed particle number density
31+
# in our simulations. We can do this by setting the `symmetry` keyword argument to `U1Irrep`
32+
# and passing one as the particle number density keyword argument `n`.
33+
34+
symmetry = U1Irrep
35+
n = 1
36+
37+
# We can then construct the Hamiltonian, and inspect the corresponding lattice of physical
38+
# spaces.
39+
40+
H = bose_hubbard_model(ComplexF64, symmetry, lattice; cutoff, t, U, n)
41+
Pspaces = H.lattice
42+
43+
# Note that the physical space contains U(1) charges -1, 0 and +1. Indeed, imposing a
44+
# particle number density of +1 corresponds to shifting the physical charges by -1 to
45+
# 're-center' the physical charges around the desired density. When we do this with a cutoff
46+
# of two bosons per site, i.e. starting from U(1) charges 0, 1 and 2 on the physical level,
47+
# we indeed get the observed charges.
48+
49+
## Characterizing the virtual spaces
50+
51+
# When running PEPS simulations with explicit internal symmetries, specifying the structure
52+
# of the virtual spaces of the PEPS and its environment becomes a bit more involved. For the
53+
# environment, one could in principle allow the virtual space to be chosen dynamically
54+
# during the boundary contraction using CTMRG by using a truncation scheme that allows for
55+
# this (e.g. using alg=:truncdim or alg=:truncbelow to truncate to a fixed total bond
56+
# dimension or singular value cutoff respectively). For the PEPS virtual space however, the
57+
# structure has to be specified before the optimization.
58+
59+
# While there are a host of techniques to do this in an informed way (e.g. starting from
60+
# a simple update result), here we just specify the virtual space manually. Since we're
61+
# dealing with a model at unit filling our physical space only contains integer U(1) irreps.
62+
# Therefore, we'll build our PEPS and environment spaces using integer U(1) irreps centered
63+
# around the zero charge.
64+
65+
Vpeps = U1Space(0 => 2, 1 => 1, -1 => 1)
66+
Venv = U1Space(0 => 6, 1 => 4, -1 => 4, 2 => 2, -2 => 2)
67+
68+
## Finding the ground state
69+
70+
# Having defined our Hamiltonian and spaces, it is just a matter of pluggin this into the
71+
# optimization framework in the usual way to find the ground state.
72+
73+
# specify algorithms and tolerances
74+
boundary_alg = (; tol=1e-8, alg=:simultaneous, verbosity=2, trscheme=(; alg=:fixedspace))
75+
gradient_alg = (; tol=1e-6, maxiter=10, alg=:eigsolver, iterscheme=:diffgauge)
76+
optimizer_alg = (; tol=1e-4, alg=:lbfgs, verbosity=3, maxiter=200, ls_maxiter=2, ls_maxfg=2)
77+
reuse_env = true
78+
79+
# initialize state
80+
Nspaces = fill(Vpeps, size(lattice)...)
81+
Espaces = fill(Vpeps, size(lattice)...)
82+
Random.seed!(2928528935) # for reproducibility
83+
ψ₀ = InfinitePEPS(randn, ComplexF64, Pspaces, Nspaces, Espaces)
84+
env₀ = CTMRGEnv(ψ₀, Venv)
85+
env₀, = leading_boundary(env₀, ψ₀; boundary_alg...)
86+
87+
# optimize
88+
ψ, env, E, info = fixedpoint(
89+
H, ψ₀, env₀; boundary_alg, gradient_alg, optimizer_alg, reuse_env
90+
)
91+
92+
## Check the result
93+
94+
# We can compare our PEPS result to the energy obtained using a cylinder-MPS calculation
95+
# using a cylinder circumference of Ly = 7 and a bond dimension of 446, which yields
96+
# E = -0.273284888
97+
98+
E_ref = -0.273284888
99+
@test E E_ref rtol = 1e-3

‎examples/fermi_hubbard.jl

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using Test
2+
using Random
3+
using PEPSKit
4+
using TensorKit
5+
using KrylovKit
6+
using OptimKit
7+
8+
using MPSKit: add_physical_charge
9+
10+
## The Fermi-Hubbard model with fℤ₂ ⊠ U1 symmetry, at large U and half filling
11+
12+
# reference: https://www.osti.gov/servlets/purl/1565498
13+
# energy should end up at E_ref ≈ 4 * -0.5244140625 = -2.09765625, but takes a lot of time
14+
E_ref = -2.0
15+
16+
# parameters
17+
t = 1.0
18+
U = 8.0
19+
lattice = InfiniteSquare(2, 2)
20+
fermion = fℤ₂
21+
particle_symmetry = U1Irrep
22+
spin_symmetry = Trivial
23+
S = fermion particle_symmetry # symmetry sector
24+
25+
# spaces
26+
D = 1
27+
Vpeps = Vect[S]((0, 0) => 2 * D, (1, 1) => D, (1, -1) => D)
28+
χ = 1
29+
Venv = Vect[S](
30+
(0, 0) => 4 * χ, (1, -1) => 2 * χ, (1, 1) => 2 * χ, (0, 2) => χ, (0, -2) => χ
31+
)
32+
Saux = S((1, -1)) # impose half filling
33+
34+
# algorithms
35+
boundary_alg = (; tol=1e-8, alg=:simultaneous, verbosity=2, trscheme=(; alg=:fixedspace))
36+
gradient_alg = (; tol=1e-6, alg=:eigsolver, maxiter=10, iterscheme=:diffgauge)
37+
optimizer_alg = (; tol=1e-4, alg=:lbfgs, verbosity=3, maxiter=100, ls_maxiter=2, ls_maxfg=2)
38+
reuse_env = true
39+
40+
# Hamiltonian
41+
H0 = hubbard_model(ComplexF64, particle_symmetry, spin_symmetry, lattice; t, U)
42+
H = add_physical_charge(H0, fill(Saux, size(H0.lattice)...))
43+
Pspaces = H.lattice
44+
45+
# initialize state
46+
Nspaces = fill(Vpeps, size(lattice)...)
47+
Espaces = fill(Vpeps, size(lattice)...)
48+
Random.seed!(2928528937)
49+
ψ₀ = InfinitePEPS(randn, ComplexF64, Pspaces, Nspaces, Espaces)
50+
env₀ = CTMRGEnv(ψ₀, Venv)
51+
env₀, = leading_boundary(env₀, ψ₀; boundary_alg...)
52+
53+
# optimize
54+
ψ, env, E, info = fixedpoint(
55+
H, ψ₀, env₀; boundary_alg, gradient_alg, optimizer_alg, reuse_env
56+
)
57+
@test E < E_ref

‎examples/xxz.jl

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Test
2+
using Random
3+
using PEPSKit
4+
using TensorKit
5+
using KrylovKit
6+
using OptimKit
7+
8+
using MPSKit: add_physical_charge
9+
10+
## Néel order in the XXZ model
11+
12+
# parameters
13+
J = 1.0
14+
Delta = 1.0
15+
spin = 1//2
16+
symmetry = U1Irrep
17+
lattice = InfiniteSquare(2, 2)
18+
19+
# spaces
20+
Vpeps = U1Space(0 => 2, 1 => 1, -1 => 1)
21+
Venv = U1Space(0 => 6, 1 => 4, -1 => 4, 2 => 2, -2 => 2)
22+
# staggered auxiliary physical charges -> encode Néel order directly in the ansatz
23+
Saux = [
24+
U1Irrep(-1//2) U1Irrep(1//2)
25+
U1Irrep(1//2) U1Irrep(-1//2)
26+
]
27+
28+
# algorithms
29+
boundary_alg = (; tol=1e-8, alg=:simultaneous, verbosity=2, trscheme=(; alg=:fixedspace))
30+
gradient_alg = (; tol=1e-6, alg=:eigsolver, maxiter=10, iterscheme=:diffgauge)
31+
optimizer_alg = (; tol=1e-4, alg=:lbfgs, verbosity=3, maxiter=100, ls_maxiter=2, ls_maxfg=2)
32+
reuse_env = true
33+
34+
# Hamiltonian
35+
H0 = heisenberg_XXZ(ComplexF64, symmetry, lattice; J, Delta, spin)
36+
H = add_physical_charge(H0, Saux)
37+
Pspaces = H.lattice
38+
39+
# initialize state
40+
Nspaces = fill(Vpeps, size(lattice)...)
41+
Espaces = fill(Vpeps, size(lattice)...)
42+
Random.seed!(2928528935)
43+
ψ₀ = InfinitePEPS(randn, ComplexF64, Pspaces, Nspaces, Espaces)
44+
env₀ = CTMRGEnv(ψ₀, Venv)
45+
env₀, = leading_boundary(env₀, ψ₀; boundary_alg...)
46+
47+
# optimize
48+
ψ, env, E, info = fixedpoint(
49+
H, ψ₀, env₀; boundary_alg, gradient_alg, optimizer_alg, reuse_env
50+
)
51+
@test E < -0.666
52+
# ends up at E = -0.669..., but takes a while

‎src/Defaults.jl

+4
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ Module containing default algorithm parameter values and arguments.
6868
- `:gradientdescent` : Gradient descent algorithm, see the [OptimKit README](https://github.com/Jutho/OptimKit.jl)
6969
- `:conjugategradient` : Conjugate gradient algorithm, see the [OptimKit README](https://github.com/Jutho/OptimKit.jl)
7070
- `:lbfgs` : L-BFGS algorithm, see the [OptimKit README](https://github.com/Jutho/OptimKit.jl)
71+
* `ls_maxiter=$(Defaults.ls_maxiter)` : Maximum number of iterations for the line search in each step of the optimization.
72+
* `ls_maxfg=$(Defaults.ls_maxfg)` : Maximum number of function evaluations for the line search in each step of the optimization.
7173
* `lbfgs_memory=$(Defaults.lbfgs_memory)` : Size of limited memory representation of BFGS Hessian matrix.
7274
7375
## OhMyThreads scheduler
@@ -117,6 +119,8 @@ const optimizer_tol = 1e-4
117119
const optimizer_maxiter = 100
118120
const optimizer_verbosity = 3
119121
const optimizer_alg = :lbfgs # ∈ {:gradientdescent, :conjugategradient, :lbfgs}
122+
const ls_maxiter = 10
123+
const ls_maxfg = 20
120124
const lbfgs_memory = 20
121125

122126
# OhMyThreads scheduler defaults

‎src/PEPSKit.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export ReflectDepth, ReflectWidth, Rotate, RotateReflect
9494
export symmetrize!, symmetrize_retract_and_finalize!
9595
export showtypeofgrad
9696
export InfiniteSquare, vertices, nearest_neighbours, next_nearest_neighbours
97-
export transverse_field_ising, heisenberg_XYZ, j1_j2
97+
export transverse_field_ising, heisenberg_XYZ, heisenberg_XXZ, j1_j2, bose_hubbard_model
9898
export pwave_superconductor, hubbard_model, tj_model
9999

100100
end # module

‎src/algorithms/optimization/peps_optimization.jl

+4-2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ function _OptimizationAlgorithm(;
7474
tol=Defaults.optimizer_tol,
7575
maxiter=Defaults.optimizer_maxiter,
7676
verbosity=Defaults.optimizer_verbosity,
77+
ls_maxiter=Defaults.ls_maxiter,
78+
ls_maxfg=Defaults.ls_maxfg,
7779
lbfgs_memory=Defaults.lbfgs_memory,
7880
# TODO: add linesearch, ... to kwargs and defaults?
7981
)
@@ -84,9 +86,9 @@ function _OptimizationAlgorithm(;
8486

8587
# instantiate algorithm
8688
return if alg_type <: LBFGS
87-
alg_type(lbfgs_memory; gradtol=tol, maxiter, verbosity)
89+
alg_type(lbfgs_memory; gradtol=tol, maxiter, verbosity, ls_maxiter, ls_maxfg)
8890
else
89-
alg_type(; gradtol=tol, maxiter, verbosity)
91+
alg_type(; gradtol=tol, maxiter, verbosity, ls_maxiter, ls_maxfg)
9092
end
9193
end
9294

‎src/operators/localoperator.jl

+64
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,67 @@ function Base.rot180(H::LocalOperator)
208208
)
209209
return LocalOperator(lattice2, terms2...)
210210
end
211+
212+
# Charge shifting
213+
# ---------------
214+
215+
TensorKit.sectortype(O::LocalOperator) = sectortype(typeof(O))
216+
TensorKit.sectortype(::Type{<:LocalOperator{T,S}}) where {T,S} = sectortype(S)
217+
218+
@generated function _fuse_isomorphisms(
219+
op::AbstractTensorMap{<:Any,S,N,N}, fs::Vector{<:AbstractTensorMap{<:Any,S,1,2}}
220+
) where {S,N}
221+
op_out_e = tensorexpr(:op_out, -(1:N), -((1:N) .+ N))
222+
op_e = tensorexpr(:op, 1:3:(3 * N), 2:3:(3 * N))
223+
f_es = map(1:N) do i
224+
j = 3 * (i - 1) + 1
225+
return tensorexpr(:(fs[$i]), -i, (j, j + 2))
226+
end
227+
f_dag_es = map(1:N) do i
228+
j = 3 * (i - 1) + 1
229+
return tensorexpr(:(fs[$i]), -(N + i), (j + 1, j + 2))
230+
end
231+
multiplication_ex = Expr(
232+
:call, :*, op_e, f_es..., map(x -> Expr(:call, :conj, x), f_dag_es)...
233+
)
234+
return macroexpand(@__MODULE__, :(return @tensor $op_out_e := $multiplication_ex))
235+
end
236+
237+
"""
238+
Fuse identities on auxiliary physical spaces into a given operator.
239+
"""
240+
function _fuse_ids(op::AbstractTensorMap{T,S,N,N}, Ps::NTuple{N,S}) where {T,S,N}
241+
# make isomorphisms
242+
fs = map(1:N) do i
243+
return isomorphism(fuse(space(op, i), Ps[i]), space(op, i) Ps[i])
244+
end
245+
# and fuse them into the operator
246+
return _fuse_isomorphisms(op, fs)
247+
end
248+
249+
"""
250+
MPSKit.add_physical_charge(H::LocalOperator, charges::AbstractMatrix{<:Sector}) where {S}
251+
252+
Change the spaces of a `LocalOperator` by fusing in an auxiliary charge on every site,
253+
according to a given matrix of 'auxiliary' physical charges.
254+
"""
255+
function MPSKit.add_physical_charge(H::LocalOperator, charges::AbstractMatrix{<:Sector})
256+
size(H.lattice) == size(charges) ||
257+
throw(ArgumentError("Incompatible lattice and auxiliary charge sizes"))
258+
sectortype(H) === eltype(charges) ||
259+
throw(SectorMismatch("Incompatible lattice and auxiliary charge sizes"))
260+
261+
# make indexing periodic, for convenience
262+
Paux = PeriodicArray(map(c -> Vect[typeof(c)](c => 1), charges))
263+
264+
# new physical spaces
265+
Pspaces = map(fuse, H.lattice, Paux)
266+
267+
new_terms = map(H.terms) do (sites, op)
268+
Paux_slice = map(Base.Fix1(getindex, Paux), sites)
269+
return sites => _fuse_ids(op, Paux_slice)
270+
end
271+
= LocalOperator(Pspaces, new_terms...)
272+
273+
return
274+
end

‎src/operators/models.jl

+52
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,21 @@ function MPSKitModels.heisenberg_XYZ(
6060
)
6161
end
6262

63+
function MPSKitModels.heisenberg_XXZ(
64+
T::Type{<:Number}, S::Type{<:Sector}, lattice::InfiniteSquare; J=1.0, Delta=1.0, spin=1
65+
)
66+
h =
67+
J * (
68+
(S_plusmin(T, S; spin=spin) + S_minplus(T, S; spin=spin)) / 2 +
69+
Delta * S_zz(T, S; spin=spin)
70+
)
71+
rmul!(h, 1 / 4)
72+
spaces = fill(domain(h)[1], (lattice.Nrows, lattice.Ncols))
73+
return LocalOperator(
74+
spaces, (neighbor => h for neighbor in nearest_neighbours(lattice))...
75+
)
76+
end
77+
6378
"""
6479
j1_j2([elt::Type{T}], [symm::Type{S}], [lattice::InfiniteSquare];
6580
J1=1.0, J2=1.0, spin=1//2, sublattice=true)
@@ -141,6 +156,7 @@ function MPSKitModels.hubbard_model(
141156
mu=0.0,
142157
n::Integer=0,
143158
)
159+
# TODO: just add this
144160
@assert n == 0 "Currently no support for imposing a fixed particle number"
145161
N = MPSKitModels.e_number(T, particle_symmetry, spin_symmetry)
146162
pspace = space(N, 1)
@@ -154,6 +170,42 @@ function MPSKitModels.hubbard_model(
154170
return nearest_neighbour_hamiltonian(fill(pspace, size(lattice)), h)
155171
end
156172

173+
function MPSKitModels.bose_hubbard_model(
174+
elt::Type{<:Number},
175+
symmetry::Type{<:Sector},
176+
lattice::InfiniteSquare;
177+
cutoff::Integer=5,
178+
t=1.0,
179+
U=1.0,
180+
mu=0.0,
181+
n::Integer=0,
182+
)
183+
hopping_term =
184+
a_plusmin(elt, symmetry; cutoff=cutoff) + a_minplus(elt, symmetry; cutoff=cutoff)
185+
N = a_number(elt, symmetry; cutoff=cutoff)
186+
interaction_term = MPSKitModels.contract_onesite(N, N - id(domain(N)))
187+
188+
spaces = fill(space(N, 1), (lattice.Nrows, lattice.Ncols))
189+
190+
H = LocalOperator(
191+
spaces,
192+
(neighbor => -t * hopping_term for neighbor in nearest_neighbours(lattice))...,
193+
((idx,) => U / 2 * interaction_term - mu * N for idx in vertices(lattice))...,
194+
)
195+
196+
if symmetry === Trivial
197+
iszero(n) || throw(ArgumentError("imposing particle number requires `U₁` symmetry"))
198+
elseif symmetry === U1Irrep
199+
isinteger(2n) ||
200+
throw(ArgumentError("`U₁` symmetry requires halfinteger particle number"))
201+
H = MPSKit.add_physical_charge(H, fill(U1Irrep(-n), size(spaces)...)) # TODO: settle on convention here?
202+
else
203+
throw(ArgumentError("symmetry not implemented"))
204+
end
205+
206+
return H
207+
end
208+
157209
function MPSKitModels.tj_model(
158210
T::Type{<:Number},
159211
particle_symmetry::Type{<:Sector},

0 commit comments

Comments
 (0)