diff --git a/.github/workflows/DocumentationCleanup.yml b/.github/workflows/DocumentationCleanup.yml new file mode 100644 index 000000000..5be23b964 --- /dev/null +++ b/.github/workflows/DocumentationCleanup.yml @@ -0,0 +1,33 @@ +name: Doc Preview Cleanup + +on: + pull_request: + types: [closed] + +# Ensure that only one "Doc Preview Cleanup" workflow is force pushing at a time +concurrency: + group: doc-preview-cleanup + cancel-in-progress: false + +jobs: + doc-preview-cleanup: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@v4 + with: + ref: gh-pages + - name: Delete preview and history + push changes + run: | + if [ -d "${preview_dir}" ]; then + git config user.name "Documenter.jl" + git config user.email "documenter@juliadocs.github.io" + git rm -rf "${preview_dir}" + git commit -m "delete preview" + git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree}) + git push --force origin gh-pages-new:gh-pages + fi + env: + preview_dir: previews/PR${{ github.event.number }} diff --git a/.github/workflows/PR_comment.yml b/.github/workflows/PR_comment.yml new file mode 100644 index 000000000..ae43f219c --- /dev/null +++ b/.github/workflows/PR_comment.yml @@ -0,0 +1,17 @@ +name: Docs preview comment +on: + pull_request: + types: [labeled] + +permissions: + pull-requests: write +jobs: + pr_comment: + runs-on: ubuntu-latest + steps: + - name: Create PR comment + if: github.event_name == 'pull_request' && github.repository == github.event.pull_request.head.repo.full_name && github.event.label.name == 'documentation' + uses: thollander/actions-comment-pull-request@v3 + with: + message: 'After the build completes, the updated documentation will be available [here](https://quantumkithub.github.io/PEPSKit.jl/previews/PR${{ github.event.number }}/)' + comment-tag: 'preview-doc' diff --git a/.gitignore b/.gitignore index f06d26994..b8339a652 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ docs/build Manifest.toml examples/.ipynb_checkpoints .vscode/ -.DS_Store \ No newline at end of file +.DS_Store +**/dev/ diff --git a/Project.toml b/Project.toml index 5c634a799..0a9410844 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ version = "0.5.0" Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000" KrylovKit = "0b1a1467-8014-51b9-945f-bf0ae24f4b77" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" @@ -27,6 +28,7 @@ Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" Accessors = "0.1" ChainRulesCore = "1.0" Compat = "3.46, 4.2" +DocStringExtensions = "0.9.3" FiniteDifferences = "0.12" KrylovKit = "0.9.5" LinearAlgebra = "1" diff --git a/README.md b/README.md index 67193c29a..b4b94c64c 100644 --- a/README.md +++ b/README.md @@ -30,35 +30,40 @@ The package can be installed through the Julia general registry, via the package pkg> add PEPSKit ``` +## Key features + +- Construction and manipulation of infinite projected entangled-pair states (PEPS) +- Contraction of infinite PEPS using the corner transfer matrix renormalization group (CTMRG) and boundary MPS methods +- Native support for symmetric tensors through [TensorKit](https://github.com/Jutho/TensorKit.jl), including fermionic tensors +- PEPS optimization using automatic differentiation (AD) provided through [Zygote](https://fluxml.ai/Zygote.jl/stable/) +- Imaginary time evolution algorithms +- Support for PEPS with generic unit cells +- Support for classical 2D partition functions and projected entangled-pair operators (PEPOs) +- Extensible system for custom states, operators and algorithms + ## Quickstart After following the installation process, it should now be possible to load the packages and start simulating. -For example, in order to obtain the groundstate of the 2D Heisenberg model, we can use the following code: +For example, in order to obtain the ground state of the 2D Heisenberg model, we can use the following code: ```julia -using TensorKit, PEPSKit, KrylovKit, OptimKit +using TensorKit, PEPSKit # construct the Hamiltonian -H = heisenberg_XYZ(InfiniteSquare(); Jx=-1, Jy=1, Jz=-1) # sublattice rotation to obtain single-site unit cell +H = heisenberg_XYZ(InfiniteSquare()) -# choose the bond dimensions +# configure the parameters D = 2 -chi = 20 - -# configure the algorithm parameters -boundary_alg = (; - alg=:simultaneous, tol=1e-10, verbosity=2, trscheme=(; alg=:truncdim, η=chi) -) -optimizer_alg = (; alg=:lbfgs, tol=1e-4, verbosity=3) -gradient_alg = (; alg=:linsolver) -reuse_env = true - -# and find the ground state -state = InfinitePEPS(2, D) -env0, = leading_boundary(CTMRGEnv(state, ComplexSpace(chi)), state; boundary_alg...) -peps, env, E, = fixedpoint( - H, state, env0; boundary_alg, optimizer_alg, gradient_alg, reuse_env -) +χ = 20 +ctmrg_tol = 1e-10 +grad_tol = 1e-4 + +# initialize a PEPS and CTMRG environment +peps₀ = InfinitePEPS(2, D) +env₀, = leading_boundary(CTMRGEnv(peps₀, ComplexSpace(χ)), peps₀; tol=ctmrg_tol) + +# ground state search +peps, env, E, = fixedpoint(H, peps₀, env₀; tol=grad_tol, boundary_alg=(; tol=ctmrg_tol)) @show E # -0.6625... ``` diff --git a/docs/Project.toml b/docs/Project.toml index 818c9dd4d..01115f08f 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,6 +1,12 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +KrylovKit = "0b1a1467-8014-51b9-945f-bf0ae24f4b77" +MPSKit = "bb1c41ca-d63c-52ed-829e-0820dda26502" +MPSKitModels = "ca635005-6f8c-4cd1-b51d-8491250ef2ab" PEPSKit = "52969e89-939e-4361-9b68-9bc7cde4bdeb" +TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" [compat] -Documenter = "0.27" +Documenter = "1.0" diff --git a/docs/make.jl b/docs/make.jl index 1814ee546..b097f2c73 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,28 +1,78 @@ +# if docs is not the current active environment, switch to it +if Base.active_project() != joinpath(@__DIR__, "Project.toml") + using Pkg + Pkg.activate(@__DIR__) + Pkg.develop(PackageSpec(; path=joinpath(@__DIR__, ".."))) + Pkg.resolve() + Pkg.instantiate() +end + using Documenter +using DocumenterCitations +using DocumenterInterLinks using PEPSKit +using MPSKitModels: MPSKitModels # used for docstrings + +# bibliography +bibpath = joinpath(@__DIR__, "src", "assets", "pepskit.bib") +bib = CitationBibliography(bibpath; style=:authoryear) + +# interlinks +# Zygote didn't update to documenter v1 yet... +links = InterLinks( + "TensorKit" => "https://jutho.github.io/TensorKit.jl/stable/", + "KrylovKit" => "https://jutho.github.io/KrylovKit.jl/stable/", + "MPSKit" => "https://quantumkithub.github.io/MPSKit.jl/stable/", + "MPSKitModels" => "https://quantumkithub.github.io/MPSKitModels.jl/dev/", + # "Zygote" => "https://fluxml.ai/Zygote.jl/stable/", + "ChainRulesCore" => "https://juliadiff.org/ChainRulesCore.jl/stable/", +) + +# explicitly set math engine +mathengine = MathJax3( + Dict( + :loader => Dict("load" => ["[tex]/physics"]), + :tex => Dict( + "inlineMath" => [["\$", "\$"], ["\\(", "\\)"]], + "tags" => "ams", + "packages" => ["base", "ams", "autoload", "physics"], + ), + ), +) + +# examples pages +examples_optimization = + joinpath.(["heisenberg", "bose_hubbard", "xxz", "fermi_hubbard"], Ref("index.md")) +examples_time_evolution = joinpath.(["heisenberg_su", "hubbard_su"], Ref("index.md")) +examples_partition_functions = + joinpath.( + ["2d_ising_partition_function", "3d_ising_partition_function"], Ref("index.md") + ) +examples_boundary_mps = joinpath.(["boundary_mps"], Ref("index.md")) makedocs(; - modules=[PEPSKit], + modules=[PEPSKit, MPSKitModels], sitename="PEPSKit.jl", format=Documenter.HTML(; - prettyurls=get(ENV, "CI", nothing) == "true", - mathengine=MathJax3( - Dict( - :loader => Dict("load" => ["[tex]/physics"]), - :tex => Dict( - "inlineMath" => [["\$", "\$"], ["\\(", "\\)"]], - "tags" => "ams", - "packages" => ["base", "ams", "autoload", "physics"], - ), - ), - ), + prettyurls=get(ENV, "CI", nothing) == "true", mathengine, size_threshold=1024000 ), pages=[ "Home" => "index.md", - "Manual" => "man/intro.md", - "Examples" => "examples/index.md", + "Manual" => ["man/models.md", "man/multithreading.md", "man/precompilation.md"], + "Examples" => [ + "examples/index.md", + "Optimization" => joinpath.(Ref("examples"), examples_optimization), + "Time Evolution" => joinpath.(Ref("examples"), examples_time_evolution), + "Partition Functions" => + joinpath.(Ref("examples"), examples_partition_functions), + "Boundary MPS" => joinpath.(Ref("examples"), examples_boundary_mps), + ], "Library" => "lib/lib.md", + "References" => "references.md", ], + checkdocs=:none, + # checkdocs_ignored_modules=[MPSKitModels], # doesn't seem to work... + plugins=[bib, links], ) -deploydocs(; repo="https://github.com/QuantumKitHub/PEPSKit.jl.git") +deploydocs(; repo="github.com/QuantumKitHub/PEPSKit.jl.git", push_preview=true) diff --git a/docs/src/assets/pepskit.bib b/docs/src/assets/pepskit.bib new file mode 100644 index 000000000..808deb3f0 --- /dev/null +++ b/docs/src/assets/pepskit.bib @@ -0,0 +1,136 @@ +@article{francuz_stable_2025, + title = {Stable and Efficient Differentiation of Tensor Network Algorithms}, + author = {Francuz, Anna and Schuch, Norbert and Vanhecke, Bram}, + year = {2025}, + month = mar, + journal = {Physical Review Research}, + volume = {7}, + number = {1}, + pages = {013237}, + publisher = {American Physical Society}, + doi = {10.1103/PhysRevResearch.7.013237} +} + +@article{evenbly_gauge_2018, + title = {Gauge fixing, canonical forms, and optimal truncations in tensor networks with closed loops}, + author = {Evenbly, Glen}, + journal = {Phys. Rev. B}, + volume = {98}, + issue = {8}, + pages = {085155}, + numpages = {11}, + year = {2018}, + month = {Aug}, + publisher = {American Physical Society}, + doi = {10.1103/PhysRevB.98.085155}, + url = {https://link.aps.org/doi/10.1103/PhysRevB.98.085155} +} + +@article{sandvik_computational_2011, + author = {Sandvik, Anders W.}, + title = {Computational Studies of Quantum Spin Systems}, + journal = {AIP Conference Proceedings}, + volume = {1297}, + number = {1}, + pages = {135-338}, + year = {2010}, + month = {11}, + issn = {0094-243X}, + doi = {10.1063/1.3518900}, + url = {https://doi.org/10.1063/1.3518900} +} + +@article{vanderstraeten_residual_2018, + title = {Residual entropies for three-dimensional frustrated spin systems with tensor networks}, + author = {Vanderstraeten, Laurens and Vanhecke, Bram and Verstraete, Frank}, + journal = {Phys. Rev. E}, + volume = {98}, + issue = {4}, + pages = {042145}, + numpages = {8}, + year = {2018}, + month = {Oct}, + publisher = {American Physical Society}, + doi = {10.1103/PhysRevE.98.042145}, + url = {https://link.aps.org/doi/10.1103/PhysRevE.98.042145} +} + +@article{qin_benchmark_2016, + title = {Benchmark study of the two-dimensional Hubbard model with auxiliary-field quantum Monte Carlo method}, + author = {Qin, Mingpu and Shi, Hao and Zhang, Shiwei}, + journal = {Phys. Rev. B}, + volume = {94}, + issue = {8}, + pages = {085103}, + numpages = {12}, + year = {2016}, + month = {Aug}, + publisher = {American Physical Society}, + doi = {10.1103/PhysRevB.94.085103}, + url = {https://link.aps.org/doi/10.1103/PhysRevB.94.085103} +} + +@article{corboz_variational_2016, + title = {Variational optimization with infinite projected entangled-pair states}, + author = {Corboz, Philippe}, + journal = {Phys. Rev. B}, + volume = {94}, + issue = {3}, + pages = {035133}, + numpages = {11}, + year = {2016}, + month = {Jul}, + publisher = {American Physical Society}, + doi = {10.1103/PhysRevB.94.035133}, + url = {https://link.aps.org/doi/10.1103/PhysRevB.94.035133} +} + +@article{haegeman_diagonalizing_2017, + title = {Diagonalizing {{Transfer Matrices}} and {{Matrix Product Operators}}: {{A Medley}} of {{Exact}} and {{Computational Methods}}}, + shorttitle = {Diagonalizing {{Transfer Matrices}} and {{Matrix Product Operators}}}, + author = {Haegeman, Jutho and Verstraete, Frank}, + year = {2017}, + journal = {Annual Review of Condensed Matter Physics}, + volume = {8}, + number = {1}, + pages = {355--406}, + doi = {10.1146/annurev-conmatphys-031016-025507}, + keywords = {Bethe ansatz,entanglement,equilibrium and nonequilibrium statistical physics,fusion tensor categories,many-body physics,quantum spin chains,tensor networks} +} + +@article{vanderstraeten_tangentspace_2019, + title = {Tangent-Space Methods for Uniform Matrix Product States}, + author = {Vanderstraeten, Laurens and Haegeman, Jutho and Verstraete, Frank}, + year = {2019}, + month = jan, + journal = {SciPost Physics Lecture Notes}, + pages = {007}, + issn = {2590-1990}, + doi = {10.21468/SciPostPhysLectNotes.7}, + langid = {english}, + language = {en} +} + +@article{hasenbusch_monte_2001, + author = {Hasenbusch, Martin}, + title = {Monte Carlo Studies of the Three-Dimensional Ising Model in Equilibrium}, + journal = {International Journal of Modern Physics C}, + volume = {12}, + number = {07}, + pages = {911-1009}, + year = {2001}, + doi = {10.1142/S0129183101002383}, +} + +@article{vanderstraeten_variational_2022, + title = {Variational Methods for Contracting Projected Entangled-Pair States}, + author = {Vanderstraeten, Laurens and Burgelman, Lander and Ponsioen, Boris and Van Damme, Maarten and Vanhecke, Bram and Corboz, Philippe and Haegeman, Jutho and Verstraete, Frank}, + year = {2022}, + month = may, + journal = {Physical Review B}, + volume = {105}, + number = {19}, + pages = {195140}, + publisher = {American Physical Society}, + doi = {10.1103/PhysRevB.105.195140} +} diff --git a/docs/src/examples/2d_ising_partition_function/index.md b/docs/src/examples/2d_ising_partition_function/index.md new file mode 100644 index 000000000..ad8c4c354 --- /dev/null +++ b/docs/src/examples/2d_ising_partition_function/index.md @@ -0,0 +1,217 @@ +```@meta +EditURL = "../../../../examples/2d_ising_partition_function/main.jl" +``` + +[![](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/QuantumKitHub/PEPSKit.jl/gh-pages?filepath=dev/examples/2d_ising_partition_function/main.ipynb) +[![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](https://nbviewer.jupyter.org/github/QuantumKitHub/PEPSKit.jl/blob/gh-pages/dev/examples/2d_ising_partition_function/main.ipynb) +[![](https://img.shields.io/badge/download-project-orange)](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/QuantumKitHub/PEPSKit.jl/examples/tree/gh-pages/dev/examples/2d_ising_partition_function) + + +# [The 2D classical Ising model using CTMRG](@id e_2d_ising) + +While PEPSKit has a lot of use in quantum systems, describing states using InfinitePEPS that can be contracted via CTMRG or [boundary MPS techniques](@ref e_boundary_mps), here we shift our focus to classical physics. +We consider the 2D classical Ising model and compute its partition function defined as: + +```math +\mathcal{Z}(\beta) = \sum_{\{s\}} \exp(-\beta H(s)) \text{ with } H(s) = -J \sum_{\langle i, j \rangle} s_i s_j . +``` + +where the classical spins $s_i \in \{+1, -1\}$ are located on the vertices $i$ of a 2D +square lattice. The idea is to encode the partition function as an infinite square network +consisting of local rank-4 tensors, which can then be contracted using CTMRG. An infinite +square network of these rank-4 tensors can be represented as an +[`InfinitePartitionFunction`](@ref) object, as we will see. + +But first, let's seed the RNG and import all required modules: + +````julia +using Random, LinearAlgebra +using TensorKit, PEPSKit +using QuadGK +Random.seed!(234923); +```` + +## Defining the partition function + +The first step is to define the rank-4 tensor that, when contracted on a square lattice, +evaluates to the partition function value at a given $\beta$. This is done through a +[fairly generic procedure](@cite haegeman_diagonalizing_2017) where the interaction weights +are distributed among vertex tensors in an appropriate way. Concretely, here we first define +a 'link' matrix containing the Boltzmann weights associated to all possible spin +configurations across a given link on the lattice. Next, we define site tensors as +delta-tensors that ensiure that the spin value on all adjacent links is the same. Since we +only want tensors on the sites in the end, we can symmetrically absorb the link weight +tensors into the site tensors, which gives us exactly the kind of network we're looking for. +Since we later want to compute the magnetization and energy to check our results, we define +the appropriate rank-4 tensors here as well while we're at it. + +````julia +function classical_ising(; beta=log(1 + sqrt(2)) / 2, J=1.0) + K = beta * J + + # Boltzmann weights + t = ComplexF64[exp(K) exp(-K); exp(-K) exp(K)] + r = eigen(t) + nt = r.vectors * sqrt(Diagonal(r.values)) * r.vectors + + # local partition function tensor + O = zeros(2, 2, 2, 2) + O[1, 1, 1, 1] = 1 + O[2, 2, 2, 2] = 1 + @tensor o[-1 -2; -3 -4] := O[3 4; 2 1] * nt[-3; 3] * nt[-4; 4] * nt[-2; 2] * nt[-1; 1] + + # magnetization tensor + M = copy(O) + M[2, 2, 2, 2] *= -1 + @tensor m[-1 -2; -3 -4] := M[1 2; 3 4] * nt[-1; 1] * nt[-2; 2] * nt[-3; 3] * nt[-4; 4] + + # bond interaction tensor and energy-per-site tensor + e = ComplexF64[-J J; J -J] .* nt + @tensor e_hor[-1 -2; -3 -4] := + O[1 2; 3 4] * nt[-1; 1] * nt[-2; 2] * nt[-3; 3] * e[-4; 4] + @tensor e_vert[-1 -2; -3 -4] := + O[1 2; 3 4] * nt[-1; 1] * nt[-2; 2] * e[-3; 3] * nt[-4; 4] + e = e_hor + e_vert + + # fixed tensor map space for all three + TMS = ℂ^2 ⊗ ℂ^2 ← ℂ^2 ⊗ ℂ^2 + + return TensorMap(o, TMS), TensorMap(m, TMS), TensorMap(e, TMS) +end; +```` + +So let's initialize these tensors at inverse temperature ``\beta=0.6``, check that +they are indeed rank-4 and construct the corresponding `InfinitePartitionFunction`: + +````julia +beta = 0.6 +O, M, E = classical_ising(; beta) +@show space(O) +Z = InfinitePartitionFunction(O) +```` + +```` +InfinitePartitionFunction{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 2, 2, Vector{ComplexF64}}}(TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 2, 2, Vector{ComplexF64}}[TensorMap((ℂ^2 ⊗ ℂ^2) ← (ℂ^2 ⊗ ℂ^2)): +[:, :, 1, 1] = + 3.169519816780443 + 0.0im 0.4999999999999995 + 0.0im + 0.4999999999999995 + 0.0im 0.1505971059561009 + 0.0im + +[:, :, 2, 1] = + 0.4999999999999995 + 0.0im 0.1505971059561009 + 0.0im + 0.1505971059561009 + 0.0im 0.4999999999999995 + 0.0im + +[:, :, 1, 2] = + 0.4999999999999995 + 0.0im 0.1505971059561009 + 0.0im + 0.1505971059561009 + 0.0im 0.4999999999999995 + 0.0im + +[:, :, 2, 2] = + 0.1505971059561009 + 0.0im 0.4999999999999995 + 0.0im + 0.4999999999999995 + 0.0im 3.169519816780443 + 0.0im +;;]) +```` + +## Contracting the partition function + +Next, we can contract the partition function as per usual by constructing a `CTMRGEnv` with +a specified environment virtual space and calling `leading_boundary` with appropriate +settings: + +````julia +Venv = ℂ^20 +env₀ = CTMRGEnv(Z, Venv) +env, = leading_boundary(env₀, Z; tol=1e-8, maxiter=500); +```` + +```` +[ Info: CTMRG init: obj = +1.767587313024e+00 -1.536527975696e+00im err = 1.0000e+00 +[ Info: CTMRG conv 62: obj = +3.353928644031e+00 err = 4.7636155793e-09 time = 10.20 sec + +```` + +Note that CTMRG environments for partition functions differ from the PEPS environments only +by the edge tensors. Instead of two legs connecting the edges and the PEPS-PEPS sandwich, +there is only one leg connecting the edges and the partition function tensor, meaning that +the edge tensors are now rank-3: + +````julia +space.(env.edges) +```` + +```` +4×1×1 Array{TensorKit.TensorMapSpace{TensorKit.ComplexSpace, 2, 1}, 3}: +[:, :, 1] = + (ℂ^20 ⊗ ℂ^2) ← ℂ^20 + (ℂ^20 ⊗ ℂ^2) ← ℂ^20 + (ℂ^20 ⊗ (ℂ^2)') ← ℂ^20 + (ℂ^20 ⊗ (ℂ^2)') ← ℂ^20 +```` + +To compute the value of the partition function, we have to contract `Z` with the converged +environment using [`network_value`](@ref). Additionally, we will compute the magnetization +and energy (per site), again using [`expectation_value`](@ref) but this time also specifying +the index in the unit cell where we want to insert the local tensor: + +````julia +λ = network_value(Z, env) +m = expectation_value(Z, (1, 1) => M, env) +e = expectation_value(Z, (1, 1) => E, env) +@show λ m e; +```` + +```` +λ = 3.3539286440313765 - 3.486341495761219e-16im +m = 0.9736086674403004 + 7.16942808669034e-17im +e = -1.8637796145082448 + 0.0im + +```` + +## Comparing against the exact Onsager solution + +In order to assess our results, we will compare against the +[exact Onsager solution](https://en.wikipedia.org/wiki/Square_lattice_Ising_model#Exact_solution) +of the 2D classical Ising model. To that end, we compute the exact free energy, +magnetization and energy per site (where we use `quadgk` to perform integrals of an +auxiliary variable from $0$ to $\pi/2$): + +````julia +function classical_ising_exact(; beta=log(1 + sqrt(2)) / 2, J=1.0) + K = beta * J + + k = 1 / sinh(2 * K)^2 + F = quadgk( + theta -> log(cosh(2 * K)^2 + 1 / k * sqrt(1 + k^2 - 2 * k * cos(2 * theta))), 0, pi + )[1] + f = -1 / beta * (log(2) / 2 + 1 / (2 * pi) * F) + + m = 1 - (sinh(2 * K))^(-4) > 0 ? (1 - (sinh(2 * K))^(-4))^(1 / 8) : 0 + + E = quadgk(theta -> 1 / sqrt(1 - (4 * k) * (1 + k)^(-2) * sin(theta)^2), 0, pi / 2)[1] + e = -J * cosh(2 * K) / sinh(2 * K) * (1 + 2 / pi * (2 * tanh(2 * K)^2 - 1) * E) + + return f, m, e +end + +f_exact, m_exact, e_exact = classical_ising_exact(; beta); +```` + +And indeed, we do find agreement between the exact and CTMRG values (keeping in mind that +energy accuracy is limited by the environment dimension and the lack of proper +extrapolation): + +````julia +@show (-log(λ) / beta - f_exact) / f_exact +@show (abs(m) - abs(m_exact)) / abs(m_exact) +@show (e - e_exact) / e_exact; +```` + +```` +(-(log(λ)) / beta - f_exact) / f_exact = -1.1009271732942546e-15 - 8.58980335690302e-17im +(abs(m) - abs(m_exact)) / abs(m_exact) = -1.1403175236145204e-16 +(e - e_exact) / e_exact = -0.02373206809908996 - 0.0im + +```` + +--- + +*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* + diff --git a/docs/src/examples/2d_ising_partition_function/main.ipynb b/docs/src/examples/2d_ising_partition_function/main.ipynb new file mode 100644 index 000000000..8c92051f9 --- /dev/null +++ b/docs/src/examples/2d_ising_partition_function/main.ipynb @@ -0,0 +1,272 @@ +{ + "cells": [ + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Markdown #hide" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "# The 2D classical Ising model using CTMRG\n", + "\n", + "While PEPSKit has a lot of use in quantum systems, describing states using InfinitePEPS that can be contracted via CTMRG or boundary MPS techniques, here we shift our focus to classical physics.\n", + "We consider the 2D classical Ising model and compute its partition function defined as:\n", + "\n", + "$$\n", + "\\mathcal{Z}(\\beta) = \\sum_{\\{s\\}} \\exp(-\\beta H(s)) \\text{ with } H(s) = -J \\sum_{\\langle i, j \\rangle} s_i s_j .\n", + "$$\n", + "\n", + "where the classical spins $s_i \\in \\{+1, -1\\}$ are located on the vertices $i$ of a 2D\n", + "square lattice. The idea is to encode the partition function as an infinite square network\n", + "consisting of local rank-4 tensors, which can then be contracted using CTMRG. An infinite\n", + "square network of these rank-4 tensors can be represented as an\n", + "`InfinitePartitionFunction` object, as we will see.\n", + "\n", + "But first, let's seed the RNG and import all required modules:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Random, LinearAlgebra\n", + "using TensorKit, PEPSKit\n", + "using QuadGK\n", + "Random.seed!(234923);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Defining the partition function\n", + "\n", + "The first step is to define the rank-4 tensor that, when contracted on a square lattice,\n", + "evaluates to the partition function value at a given $\\beta$. This is done through a\n", + "[fairly generic procedure](@cite haegeman_diagonalizing_2017) where the interaction weights\n", + "are distributed among vertex tensors in an appropriate way. Concretely, here we first define\n", + "a 'link' matrix containing the Boltzmann weights associated to all possible spin\n", + "configurations across a given link on the lattice. Next, we define site tensors as\n", + "delta-tensors that ensiure that the spin value on all adjacent links is the same. Since we\n", + "only want tensors on the sites in the end, we can symmetrically absorb the link weight\n", + "tensors into the site tensors, which gives us exactly the kind of network we're looking for.\n", + "Since we later want to compute the magnetization and energy to check our results, we define\n", + "the appropriate rank-4 tensors here as well while we're at it." + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "function classical_ising(; beta=log(1 + sqrt(2)) / 2, J=1.0)\n", + " K = beta * J\n", + "\n", + " # Boltzmann weights\n", + " t = ComplexF64[exp(K) exp(-K); exp(-K) exp(K)]\n", + " r = eigen(t)\n", + " nt = r.vectors * sqrt(Diagonal(r.values)) * r.vectors\n", + "\n", + " # local partition function tensor\n", + " O = zeros(2, 2, 2, 2)\n", + " O[1, 1, 1, 1] = 1\n", + " O[2, 2, 2, 2] = 1\n", + " @tensor o[-1 -2; -3 -4] := O[3 4; 2 1] * nt[-3; 3] * nt[-4; 4] * nt[-2; 2] * nt[-1; 1]\n", + "\n", + " # magnetization tensor\n", + " M = copy(O)\n", + " M[2, 2, 2, 2] *= -1\n", + " @tensor m[-1 -2; -3 -4] := M[1 2; 3 4] * nt[-1; 1] * nt[-2; 2] * nt[-3; 3] * nt[-4; 4]\n", + "\n", + " # bond interaction tensor and energy-per-site tensor\n", + " e = ComplexF64[-J J; J -J] .* nt\n", + " @tensor e_hor[-1 -2; -3 -4] :=\n", + " O[1 2; 3 4] * nt[-1; 1] * nt[-2; 2] * nt[-3; 3] * e[-4; 4]\n", + " @tensor e_vert[-1 -2; -3 -4] :=\n", + " O[1 2; 3 4] * nt[-1; 1] * nt[-2; 2] * e[-3; 3] * nt[-4; 4]\n", + " e = e_hor + e_vert\n", + "\n", + " # fixed tensor map space for all three\n", + " TMS = ℂ^2 ⊗ ℂ^2 ← ℂ^2 ⊗ ℂ^2\n", + "\n", + " return TensorMap(o, TMS), TensorMap(m, TMS), TensorMap(e, TMS)\n", + "end;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "So let's initialize these tensors at inverse temperature $\\beta=0.6$, check that\n", + "they are indeed rank-4 and construct the corresponding `InfinitePartitionFunction`:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "beta = 0.6\n", + "O, M, E = classical_ising(; beta)\n", + "@show space(O)\n", + "Z = InfinitePartitionFunction(O)" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Contracting the partition function\n", + "\n", + "Next, we can contract the partition function as per usual by constructing a `CTMRGEnv` with\n", + "a specified environment virtual space and calling `leading_boundary` with appropriate\n", + "settings:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "Venv = ℂ^20\n", + "env₀ = CTMRGEnv(Z, Venv)\n", + "env, = leading_boundary(env₀, Z; tol=1e-8, maxiter=500);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Note that CTMRG environments for partition functions differ from the PEPS environments only\n", + "by the edge tensors. Instead of two legs connecting the edges and the PEPS-PEPS sandwich,\n", + "there is only one leg connecting the edges and the partition function tensor, meaning that\n", + "the edge tensors are now rank-3:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "space.(env.edges)" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "To compute the value of the partition function, we have to contract `Z` with the converged\n", + "environment using `network_value`. Additionally, we will compute the magnetization\n", + "and energy (per site), again using `expectation_value` but this time also specifying\n", + "the index in the unit cell where we want to insert the local tensor:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "λ = network_value(Z, env)\n", + "m = expectation_value(Z, (1, 1) => M, env)\n", + "e = expectation_value(Z, (1, 1) => E, env)\n", + "@show λ m e;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Comparing against the exact Onsager solution\n", + "\n", + "In order to assess our results, we will compare against the\n", + "[exact Onsager solution](https://en.wikipedia.org/wiki/Square_lattice_Ising_model#Exact_solution)\n", + "of the 2D classical Ising model. To that end, we compute the exact free energy,\n", + "magnetization and energy per site (where we use `quadgk` to perform integrals of an\n", + "auxiliary variable from $0$ to $\\pi/2$):" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "function classical_ising_exact(; beta=log(1 + sqrt(2)) / 2, J=1.0)\n", + " K = beta * J\n", + "\n", + " k = 1 / sinh(2 * K)^2\n", + " F = quadgk(\n", + " theta -> log(cosh(2 * K)^2 + 1 / k * sqrt(1 + k^2 - 2 * k * cos(2 * theta))), 0, pi\n", + " )[1]\n", + " f = -1 / beta * (log(2) / 2 + 1 / (2 * pi) * F)\n", + "\n", + " m = 1 - (sinh(2 * K))^(-4) > 0 ? (1 - (sinh(2 * K))^(-4))^(1 / 8) : 0\n", + "\n", + " E = quadgk(theta -> 1 / sqrt(1 - (4 * k) * (1 + k)^(-2) * sin(theta)^2), 0, pi / 2)[1]\n", + " e = -J * cosh(2 * K) / sinh(2 * K) * (1 + 2 / pi * (2 * tanh(2 * K)^2 - 1) * E)\n", + "\n", + " return f, m, e\n", + "end\n", + "\n", + "f_exact, m_exact, e_exact = classical_ising_exact(; beta);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "And indeed, we do find agreement between the exact and CTMRG values (keeping in mind that\n", + "energy accuracy is limited by the environment dimension and the lack of proper\n", + "extrapolation):" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "@show (-log(λ) / beta - f_exact) / f_exact\n", + "@show (abs(m) - abs(m_exact)) / abs(m_exact)\n", + "@show (e - e_exact) / e_exact;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "---\n", + "\n", + "*This notebook was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*" + ], + "metadata": {} + } + ], + "nbformat_minor": 3, + "metadata": { + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.10.4" + }, + "kernelspec": { + "name": "julia-1.10", + "display_name": "Julia 1.10.4", + "language": "julia" + } + }, + "nbformat": 4 +} diff --git a/docs/src/examples/3d_ising_partition_function/index.md b/docs/src/examples/3d_ising_partition_function/index.md new file mode 100644 index 000000000..8cae565b6 --- /dev/null +++ b/docs/src/examples/3d_ising_partition_function/index.md @@ -0,0 +1,405 @@ +```@meta +EditURL = "../../../../examples/3d_ising_partition_function/main.jl" +``` + +[![](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/QuantumKitHub/PEPSKit.jl/gh-pages?filepath=dev/examples/3d_ising_partition_function/main.ipynb) +[![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](https://nbviewer.jupyter.org/github/QuantumKitHub/PEPSKit.jl/blob/gh-pages/dev/examples/3d_ising_partition_function/main.ipynb) +[![](https://img.shields.io/badge/download-project-orange)](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/QuantumKitHub/PEPSKit.jl/examples/tree/gh-pages/dev/examples/3d_ising_partition_function) + + +# [The 3D classical Ising model](@id e_3d_ising) + +In this example, we will showcase how one can use PEPSKit to study 3D classical statistical +mechanics models. In particular, we will consider a specific case of the 3D classical Ising +model, but the same techniques can be applied to other 3D classical models as well. + +As compared to simulations of [2D partition functions](@ref e_2d_ising), the workflow +presented in this example is a bit more experimental and less 'black-box'. Therefore, it +also serves as a demonstration of some of the more internal functionality of PEPSKit, +and how one can adapt it to less 'standard' kinds of problems. + +Let us consider the partition function of the classical Ising model, + +```math +\mathcal{Z}(\beta) = \sum_{\{s\}} \exp(-\beta H(s)) \text{ with } H(s) = -J \sum_{\langle i, j \rangle} s_i s_j . +``` + +where the classical spins $s_i \in \{+1, -1\}$ are located on the vertices $i$ of a 3D +cubic lattice. The partition function of this model can be represented as a 3D tensor +network with a rank-6 tensor at each vertex of the lattice. Such a network can be contracted +by finding the fixed point of the corresponding transfer operator, in exactly the same +spirit as the [boundary MPS methods](@ref e_boundary_mps) demonstrated in another example. + +Let's start by making the example deterministic and importing the required packages: + +````julia +using Random +using LinearAlgebra +using PEPSKit, TensorKit +using KrylovKit, OptimKit, Zygote + +Random.seed!(81812781144); +```` + +## Defining the partition function + +Just as in the 2D case, the first step is to define the partition function as a tensor +network. The procedure is exactly the same as before, the only difference being that now +every spin participates in interactions associated to six links adjacent to that site. This +means that the partition function can be written as an infinite 3D network with a single +constituent rank-6 [`PEPSKit.PEPOTensor`](@ref) `O` located at each site of the cubic +lattice. To verify our example we will check the magnetization and energy, so we also define +the corresponding rank-6 tensors `M` and `E` while we're at it. + +````julia +function three_dimensional_classical_ising(; beta, J=1.0) + K = beta * J + + # Boltzmann weights + t = ComplexF64[exp(K) exp(-K); exp(-K) exp(K)] + r = eigen(t) + q = r.vectors * sqrt(LinearAlgebra.Diagonal(r.values)) * r.vectors + + # local partition function tensor + O = zeros(2, 2, 2, 2, 2, 2) + O[1, 1, 1, 1, 1, 1] = 1 + O[2, 2, 2, 2, 2, 2] = 1 + @tensor o[-1 -2; -3 -4 -5 -6] := + O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6] + + # magnetization tensor + M = copy(O) + M[2, 2, 2, 2, 2, 2] *= -1 + @tensor m[-1 -2; -3 -4 -5 -6] := + M[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6] + + # bond interaction tensor and energy-per-site tensor + e = ComplexF64[-J J; J -J] .* q + @tensor e_x[-1 -2; -3 -4 -5 -6] := + O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * e[-4; 4] * q[-5; 5] * q[-6; 6] + @tensor e_y[-1 -2; -3 -4 -5 -6] := + O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * e[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6] + @tensor e_z[-1 -2; -3 -4 -5 -6] := + O[1 2; 3 4 5 6] * e[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6] + e = e_x + e_y + e_z + + # fixed tensor map space for all three + TMS = ℂ^2 ⊗ (ℂ^2)' ← ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)' + + return TensorMap(o, TMS), TensorMap(m, TMS), TensorMap(e, TMS) +end; +```` + +Let's initialize these tensors at inverse temperature ``\beta=0.2391``, which corresponds to +a slightly lower temperature than the critical value ``\beta_c=0.2216544…`` + +````julia +beta = 0.2391 +O, M, E = three_dimensional_classical_ising(; beta) +O isa PEPSKit.PEPOTensor +```` + +```` +true +```` + +## Contracting the partition function + +To contract our infinite 3D partition function, we first reinterpret it as an infinite power +of a slice-to-slice transfer operator ``T``, where ``T`` can be seen as an infinite 2D +projected entangled-pair operator (PEPO) which consists of the rank-6 tensor `O` at each +site of an infinite 2D square lattice. In the same spirit as the boundary MPS approach, all +we need to contract the whole partition function is to find the leading eigenvector of this +PEPO. The fixed point of such a PEPO can be parametrized as a PEPS, and for the case of a +Hermitian transfer operator we can find this PEPS through [variational optimization](@cite +vanderstraeten_residual_2018). + +Indeed, for a Hermitian transfer operator ``T`` we can characterize the fixed point PEPS +``|\psi\rangle`` which satisfies the eigenvalue equation +``T |\psi\rangle = \Lambda |\psi\rangle`` corresponding to the largest magnitude eigenvalue +``\Lambda`` as the solution of a variational problem + +```math +|\psi\rangle = \text{argmin}_{|\psi\rangle} \left ( \lim_{N \to ∞} - \frac{1}{N} \log \left( \frac{\langle \psi | T | \psi \rangle}{\langle \psi | \psi \rangle} \right) \right ) , +``` + +where ``N`` is the diverging number of sites of the 2D transfer operator ``T``. The function +minimized in this expression is exactly the free energy per site of the partition function, +so we essentially find the fixed-point PEPS by variationally minimizing the free energy. + +### Defining the cost function + +Using PEPSKit.jl, this cost function and its gradient can be computed, after which we can +use [OptimKit.jl](https://github.com/Jutho/OptimKit.jl) to actually optimize it. We can +immediately recognize the denominator ``\langle \psi | \psi \rangle`` as the familiar PEPS +norm, where we can compute the norm per site as the [`network_value`](@ref) of the +corresponding [`InfiniteSquareNetwork`](@ref) by contracting it with the CTMRG algorithm. +Similarly, the numerator ``\langle \psi | T | \psi \rangle`` is nothing more than an +`InfiniteSquareNetwork` consisting of three layers corresponding to the ket, transfer +operator and bra objects. This object can also be constructed and contracted in a +straightforward way, so we can again compute its `network_value`. + +To define our cost function, we then need to construct the transfer operator as an +[`InfinitePEPO`](@ref), construct the two infinite 2D contractible networks for the +numerator and denominator from the current PEPS and this transfer operator, and specify a +contraction algorithm we can use to compute the values of these two networks. In addition, +we'll specify the specific reverse rule algorithm that will be used to compute the gradient +of this cost function. + +````julia +boundary_alg = SimultaneousCTMRG(; maxiter=150, tol=1e-8, verbosity=1) +rrule_alg = EigSolver(; + solver_alg=KrylovKit.Arnoldi(; maxiter=30, tol=1e-6, eager=true), iterscheme=:diffgauge +) +T = InfinitePEPO(O) + +function pepo_costfun((peps, env_double_layer, env_triple_layer)) + # use Zygote to compute the gradient automatically + E, gs = withgradient(peps) do ψ + # construct the PEPS norm network + n_double_layer = InfiniteSquareNetwork(ψ) + # contract this network + env_double_layer′, info = PEPSKit.hook_pullback( + leading_boundary, + env_double_layer, + n_double_layer, + boundary_alg; + alg_rrule=rrule_alg, + ) + # construct the PEPS-PEPO-PEPS overlap network + n_triple_layer = InfiniteSquareNetwork(ψ, T) + # contract this network + env_triple_layer′, info = PEPSKit.hook_pullback( + leading_boundary, + env_triple_layer, + n_triple_layer, + boundary_alg; + alg_rrule=rrule_alg, + ) + # update the environments for reuse + PEPSKit.ignore_derivatives() do + PEPSKit.update!(env_double_layer, env_double_layer′) + PEPSKit.update!(env_triple_layer, env_triple_layer′) + end + # compute the network values per site + λ3 = network_value(n_triple_layer, env_triple_layer) + λ2 = network_value(n_double_layer, env_double_layer) + # use this to compute the actual cost function + return -log(real(λ3 / λ2)) + end + g = only(gs) + return E, g +end; +```` + +There are a few things to note about this cost function definition. Since we will pass it to +the `OptimKit.optimize`, we require it to return both our cost function and the +corresponding gradient. To do this, we simply use the `withgradient` method from Zygote.jl +to automatically compute the gradient of the cost function straight from the primal +computation. Since our cost function involves contractions using `leading_boundary`, we also +have to specify exactly how Zygote should handle the backpropagation of the gradient through +this function. This can be done using the [`PEPSKit.hook_pullback`](@ref) function from +PEPSKit.jl, which allows to hook into the pullback of a given function by specifying a +specific algorithm for the pullback computation. Here, we opted to use an Arnoldi method to +solve the linear problem defining the gradient of the network contraction at its fixed +point. This is exactly the workflow that internally underlies [`PEPSKit.fixedpoint`](@ref), and +more info on particular gradient algorithms can be found in the corresponding docstrings. + +### Characterizing the optimization manifold + +In order to make the best use of OptimKit.jl, we should specify some properties of the +manifold on which we are optimizing. Looking at our cost function defined above, a point on +our optimization manifold corresponds to a `Tuple` of three objects. The first is an +`InfinitePEPS` encoding the fixed point we are actually optimizing, while the second and +third are `CTMRGEnv` objects corresponding to the environments of the double and triple +layer networks ``\langle \psi | \psi \rangle`` and ``\langle \psi | T | \psi \rangle`` +respectively. While the environments are just there so we can reuse them between subsequent +contractions and we don't need to think about them much, optimizing over the manifold of +`InfinitePEPS` requires a bit more care. + +In particular, we need to define two kinds of operations on this manifold: a retraction and +a transport. The retraction, corresponding to the `retract` keyword argument of +`OptimKit.optimize`, specifies how to move from a point on a manifold along a given descent +direction to obtain a new manifold point. The transport, corresponding to the `transport!` +keyword argument of `OptimKit.optimize`, specifies how to transport a descent direction at a +given manifold point to a valid descent direction at a different manifold point according to +the appropriate metric. For a more detailed explanation we refer to the +[OptimKit.jl README](https://github.com/Jutho/OptimKit.jl). In PEPSKit.jl, these two +procedures are defined through the [`PEPSKit.peps_retract`](@ref) and +[`PEPSKit.peps_transport!`](@ref) methods. While it is instructive to read the corresponding +docstrings in order to understand what these actually do, here we can just blindly reuse +them where the only difference is that we have to pass along an extra environment since our +cost function requires two distinct contractions as opposed to the setting of Hamiltonian +PEPS optimization which only requires a double-layer contraction. + +````julia +function pepo_retract((peps, env_double_layer, env_triple_layer), η, α) + (peps´, env_double_layer´), ξ = PEPSKit.peps_retract((peps, env_double_layer), η, α) + env_triple_layer´ = deepcopy(env_triple_layer) + return (peps´, env_double_layer´, env_triple_layer´), ξ +end +function pepo_transport!( + ξ, + (peps, env_double_layer, env_triple_layer), + η, + α, + (peps´, env_double_layer´, env_triple_layer´), +) + return PEPSKit.peps_transport!( + ξ, (peps, env_double_layer), η, α, (peps´, env_double_layer´) + ) +end; +```` + +### Finding the fixed point + +All that is left then is to specify the virtual spaces of the PEPS and the two environments, +initialize them in the appropriate way, choose an optimization algortithm and call the +`optimize` function from OptimKit.jl to get our desired PEPS fixed point. + +````julia +Vpeps = ℂ^2 +Venv = ℂ^12 + +psi0 = initializePEPS(T, Vpeps) +env2_0 = CTMRGEnv(InfiniteSquareNetwork(psi0), Venv) +env3_0 = CTMRGEnv(InfiniteSquareNetwork(psi0, T), Venv) + +optimizer_alg = LBFGS(32; maxiter=100, gradtol=1e-5, verbosity=3) + +(psi_final, env2_final, env3_final), f, = optimize( + pepo_costfun, + (psi0, env2_0, env3_0), + optimizer_alg; + inner=PEPSKit.real_inner, + retract=pepo_retract, + (transport!)=(pepo_transport!), +); +```` + +```` +[ Info: LBFGS: initializing with f = -0.554073395182, ‖∇f‖ = 7.7844e-01 +┌ Warning: The function `scale!!` is not implemented for (values of) type `Tuple{InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}}, Float64}`; +│ this fallback will disappear in future versions of VectorInterface.jl +└ @ VectorInterface ~/.julia/packages/VectorInterface/J6qCR/src/fallbacks.jl:91 +┌ Warning: CTMRG cancel 150: obj = +1.702942227203e+01 +1.438609955721e-07im err = 2.4390784904e-05 time = 14.97 sec +└ @ PEPSKit ~/git/PEPSKit.jl/src/algorithms/ctmrg/ctmrg.jl:155 +[ Info: LBFGS: iter 1, time 194.26 s: f = -0.777080930424, ‖∇f‖ = 3.1305e-02, α = 7.10e+02, m = 0, nfg = 7 +┌ Warning: The function `add!!` is not implemented for (values of) type `Tuple{InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}}, InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}}, Int64, VectorInterface.One}`; +│ this fallback will disappear in future versions of VectorInterface.jl +└ @ VectorInterface ~/.julia/packages/VectorInterface/J6qCR/src/fallbacks.jl:163 +[ Info: LBFGS: iter 2, time 197.93 s: f = -0.784111515920, ‖∇f‖ = 2.0103e-02, α = 1.00e+00, m = 1, nfg = 1 +[ Info: LBFGS: iter 3, time 199.60 s: f = -0.792705733264, ‖∇f‖ = 2.3327e-02, α = 1.00e+00, m = 2, nfg = 1 +[ Info: LBFGS: iter 4, time 200.55 s: f = -0.796289732161, ‖∇f‖ = 2.2475e-02, α = 1.00e+00, m = 3, nfg = 1 +[ Info: LBFGS: iter 5, time 201.31 s: f = -0.799674902231, ‖∇f‖ = 7.0288e-03, α = 1.00e+00, m = 4, nfg = 1 +[ Info: LBFGS: iter 6, time 202.01 s: f = -0.800082100115, ‖∇f‖ = 1.2717e-03, α = 1.00e+00, m = 5, nfg = 1 +[ Info: LBFGS: iter 7, time 202.82 s: f = -0.800110603120, ‖∇f‖ = 1.3384e-03, α = 1.00e+00, m = 6, nfg = 1 +[ Info: LBFGS: iter 8, time 203.52 s: f = -0.800262202003, ‖∇f‖ = 2.4945e-03, α = 1.00e+00, m = 7, nfg = 1 +[ Info: LBFGS: iter 9, time 204.21 s: f = -0.800450505439, ‖∇f‖ = 2.9259e-03, α = 1.00e+00, m = 8, nfg = 1 +[ Info: LBFGS: iter 10, time 205.05 s: f = -0.800764917063, ‖∇f‖ = 1.7221e-03, α = 1.00e+00, m = 9, nfg = 1 +[ Info: LBFGS: iter 11, time 205.88 s: f = -0.800876048822, ‖∇f‖ = 2.2475e-03, α = 1.00e+00, m = 10, nfg = 1 +[ Info: LBFGS: iter 12, time 206.58 s: f = -0.801100867388, ‖∇f‖ = 1.5561e-03, α = 1.00e+00, m = 11, nfg = 1 +[ Info: LBFGS: iter 13, time 207.45 s: f = -0.801317048785, ‖∇f‖ = 1.1561e-03, α = 1.00e+00, m = 12, nfg = 1 +[ Info: LBFGS: iter 14, time 208.22 s: f = -0.801373050522, ‖∇f‖ = 7.1300e-04, α = 1.00e+00, m = 13, nfg = 1 +[ Info: LBFGS: iter 15, time 208.98 s: f = -0.801388615258, ‖∇f‖ = 2.8462e-04, α = 1.00e+00, m = 14, nfg = 1 +[ Info: LBFGS: iter 16, time 209.78 s: f = -0.801394633326, ‖∇f‖ = 2.7607e-04, α = 1.00e+00, m = 15, nfg = 1 +[ Info: LBFGS: iter 17, time 210.48 s: f = -0.801408061547, ‖∇f‖ = 3.6096e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 18, time 211.41 s: f = -0.801509542237, ‖∇f‖ = 1.9822e-03, α = 1.00e+00, m = 17, nfg = 1 +[ Info: LBFGS: iter 19, time 212.36 s: f = -0.801578405298, ‖∇f‖ = 1.8040e-03, α = 1.00e+00, m = 18, nfg = 1 +[ Info: LBFGS: iter 20, time 215.65 s: f = -0.801694524832, ‖∇f‖ = 2.9356e-03, α = 5.48e-01, m = 19, nfg = 3 +[ Info: LBFGS: iter 21, time 217.92 s: f = -0.801761920585, ‖∇f‖ = 1.1993e-03, α = 3.82e-01, m = 20, nfg = 2 +[ Info: LBFGS: iter 22, time 218.94 s: f = -0.801797785496, ‖∇f‖ = 6.0337e-04, α = 1.00e+00, m = 21, nfg = 1 +[ Info: LBFGS: iter 23, time 221.10 s: f = -0.801808747834, ‖∇f‖ = 3.7053e-04, α = 5.24e-01, m = 22, nfg = 2 +[ Info: LBFGS: iter 24, time 222.14 s: f = -0.801812729196, ‖∇f‖ = 3.0781e-04, α = 1.00e+00, m = 23, nfg = 1 +[ Info: LBFGS: iter 25, time 223.24 s: f = -0.801816445181, ‖∇f‖ = 2.9994e-04, α = 1.00e+00, m = 24, nfg = 1 +[ Info: LBFGS: iter 26, time 224.28 s: f = -0.801824712580, ‖∇f‖ = 3.6497e-04, α = 1.00e+00, m = 25, nfg = 1 +[ Info: LBFGS: iter 27, time 225.47 s: f = -0.801839673918, ‖∇f‖ = 5.4217e-04, α = 1.00e+00, m = 26, nfg = 1 +[ Info: LBFGS: iter 28, time 226.66 s: f = -0.801857480042, ‖∇f‖ = 2.7918e-04, α = 1.00e+00, m = 27, nfg = 1 +[ Info: LBFGS: iter 29, time 227.86 s: f = -0.801864556175, ‖∇f‖ = 1.2323e-04, α = 1.00e+00, m = 28, nfg = 1 +[ Info: LBFGS: iter 30, time 228.94 s: f = -0.801865599394, ‖∇f‖ = 8.6049e-05, α = 1.00e+00, m = 29, nfg = 1 +[ Info: LBFGS: iter 31, time 230.08 s: f = -0.801867571015, ‖∇f‖ = 8.8673e-05, α = 1.00e+00, m = 30, nfg = 1 +[ Info: LBFGS: iter 32, time 231.29 s: f = -0.801870391456, ‖∇f‖ = 2.6510e-04, α = 1.00e+00, m = 31, nfg = 1 +[ Info: LBFGS: iter 33, time 232.57 s: f = -0.801874799763, ‖∇f‖ = 2.7836e-04, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 34, time 233.84 s: f = -0.801877566434, ‖∇f‖ = 1.8512e-04, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 35, time 235.10 s: f = -0.801878506140, ‖∇f‖ = 2.0603e-04, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 36, time 236.21 s: f = -0.801878994665, ‖∇f‖ = 5.6086e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 37, time 237.29 s: f = -0.801879153394, ‖∇f‖ = 6.2420e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 38, time 238.40 s: f = -0.801879354902, ‖∇f‖ = 6.0532e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 39, time 239.51 s: f = -0.801880114487, ‖∇f‖ = 6.2680e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 40, time 240.72 s: f = -0.801881471358, ‖∇f‖ = 6.2392e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 41, time 242.04 s: f = -0.801882270540, ‖∇f‖ = 9.4942e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 42, time 243.95 s: f = -0.801882598674, ‖∇f‖ = 5.1910e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 43, time 245.85 s: f = -0.801882711254, ‖∇f‖ = 2.6275e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 44, time 246.97 s: f = -0.801882805203, ‖∇f‖ = 2.9425e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 45, time 248.06 s: f = -0.801883026107, ‖∇f‖ = 2.7910e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 46, time 249.21 s: f = -0.801883400172, ‖∇f‖ = 3.7426e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 47, time 250.38 s: f = -0.801883717581, ‖∇f‖ = 5.4717e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 48, time 251.58 s: f = -0.801883966703, ‖∇f‖ = 2.9045e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 49, time 252.76 s: f = -0.801884163647, ‖∇f‖ = 3.0661e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 50, time 253.97 s: f = -0.801884391105, ‖∇f‖ = 4.1905e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 51, time 255.20 s: f = -0.801884815983, ‖∇f‖ = 6.9018e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 52, time 256.37 s: f = -0.801885013427, ‖∇f‖ = 3.8025e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 53, time 257.66 s: f = -0.801885126302, ‖∇f‖ = 1.9306e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 54, time 258.84 s: f = -0.801885184513, ‖∇f‖ = 3.3083e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 55, time 260.08 s: f = -0.801885308658, ‖∇f‖ = 4.9014e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 56, time 261.40 s: f = -0.801885502272, ‖∇f‖ = 1.1303e-04, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 57, time 262.77 s: f = -0.801885922461, ‖∇f‖ = 7.5880e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 58, time 264.15 s: f = -0.801886457901, ‖∇f‖ = 7.2957e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 59, time 265.39 s: f = -0.801886614664, ‖∇f‖ = 6.8816e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 60, time 267.82 s: f = -0.801886696733, ‖∇f‖ = 3.0687e-05, α = 4.26e-01, m = 32, nfg = 2 +[ Info: LBFGS: iter 61, time 269.06 s: f = -0.801886716271, ‖∇f‖ = 2.1581e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 62, time 270.16 s: f = -0.801886732686, ‖∇f‖ = 1.7659e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 63, time 271.80 s: f = -0.801886790357, ‖∇f‖ = 4.1045e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 64, time 273.78 s: f = -0.801886827022, ‖∇f‖ = 4.0831e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 65, time 275.59 s: f = -0.801886871472, ‖∇f‖ = 4.1034e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 66, time 277.47 s: f = -0.801886949562, ‖∇f‖ = 5.1171e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 67, time 279.44 s: f = -0.801887066612, ‖∇f‖ = 4.5902e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 68, time 281.52 s: f = -0.801887172301, ‖∇f‖ = 7.4810e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 69, time 283.60 s: f = -0.801887249759, ‖∇f‖ = 3.9619e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 70, time 285.46 s: f = -0.801887292124, ‖∇f‖ = 1.3999e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 71, time 287.42 s: f = -0.801887312574, ‖∇f‖ = 1.0813e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 72, time 289.39 s: f = -0.801887349749, ‖∇f‖ = 1.1335e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 73, time 291.40 s: f = -0.801887427066, ‖∇f‖ = 1.9028e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 74, time 293.37 s: f = -0.801887495625, ‖∇f‖ = 1.9286e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: iter 75, time 297.21 s: f = -0.801887521350, ‖∇f‖ = 2.5284e-05, α = 5.43e-01, m = 32, nfg = 2 +[ Info: LBFGS: iter 76, time 299.15 s: f = -0.801887560129, ‖∇f‖ = 2.7094e-05, α = 1.00e+00, m = 32, nfg = 1 +[ Info: LBFGS: converged after 77 iterations and time 300.65 s: f = -0.801887571684, ‖∇f‖ = 7.8819e-06 + +```` + +### Verifying the result + +Having found the fixed point, we have essentially contracted the entire partition function +and we can start computing observables. The free energy per site for example is just given by +the final value of the cost function we have just optimized. + +````julia +@show f +```` + +```` +-0.8018875716841548 +```` + +As another check, we can compute the magnetization per site and compare it to a [reference +value obtaind through Monte-Carlo simulations](@cite hasenbusch_monte_2001). + +````julia +n3_final = InfiniteSquareNetwork(psi_final, T) +num = PEPSKit.contract_local_tensor((1, 1, 1), M, n3_final, env3_final) +denom = PEPSKit._contract_site((1, 1), n3_final, env3_final) +m = abs(num / denom) + +m_ref = 0.667162 + +@show abs(m - m_ref) +```` + +```` +0.00011017609950225715 +```` + +--- + +*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* + diff --git a/docs/src/examples/3d_ising_partition_function/main.ipynb b/docs/src/examples/3d_ising_partition_function/main.ipynb new file mode 100644 index 000000000..cf04acfdb --- /dev/null +++ b/docs/src/examples/3d_ising_partition_function/main.ipynb @@ -0,0 +1,407 @@ +{ + "cells": [ + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Markdown #hide" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "# The 3D classical Ising model\n", + "\n", + "In this example, we will showcase how one can use PEPSKit to study 3D classical statistical\n", + "mechanics models. In particular, we will consider a specific case of the 3D classical Ising\n", + "model, but the same techniques can be applied to other 3D classical models as well.\n", + "\n", + "As compared to simulations of 2D partition functions, the workflow\n", + "presented in this example is a bit more experimental and less 'black-box'. Therefore, it\n", + "also serves as a demonstration of some of the more internal functionality of PEPSKit,\n", + "and how one can adapt it to less 'standard' kinds of problems.\n", + "\n", + "Let us consider the partition function of the classical Ising model,\n", + "\n", + "$$\n", + "\\mathcal{Z}(\\beta) = \\sum_{\\{s\\}} \\exp(-\\beta H(s)) \\text{ with } H(s) = -J \\sum_{\\langle i, j \\rangle} s_i s_j .\n", + "$$\n", + "\n", + "where the classical spins $s_i \\in \\{+1, -1\\}$ are located on the vertices $i$ of a 3D\n", + "cubic lattice. The partition function of this model can be represented as a 3D tensor\n", + "network with a rank-6 tensor at each vertex of the lattice. Such a network can be contracted\n", + "by finding the fixed point of the corresponding transfer operator, in exactly the same\n", + "spirit as the boundary MPS methods demonstrated in another example.\n", + "\n", + "Let's start by making the example deterministic and importing the required packages:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Random\n", + "using LinearAlgebra\n", + "using PEPSKit, TensorKit\n", + "using KrylovKit, OptimKit, Zygote\n", + "\n", + "Random.seed!(81812781144);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Defining the partition function\n", + "\n", + "Just as in the 2D case, the first step is to define the partition function as a tensor\n", + "network. The procedure is exactly the same as before, the only difference being that now\n", + "every spin participates in interactions associated to six links adjacent to that site. This\n", + "means that the partition function can be written as an infinite 3D network with a single\n", + "constituent rank-6 `PEPSKit.PEPOTensor` `O` located at each site of the cubic\n", + "lattice. To verify our example we will check the magnetization and energy, so we also define\n", + "the corresponding rank-6 tensors `M` and `E` while we're at it." + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "function three_dimensional_classical_ising(; beta, J=1.0)\n", + " K = beta * J\n", + "\n", + " # Boltzmann weights\n", + " t = ComplexF64[exp(K) exp(-K); exp(-K) exp(K)]\n", + " r = eigen(t)\n", + " q = r.vectors * sqrt(LinearAlgebra.Diagonal(r.values)) * r.vectors\n", + "\n", + " # local partition function tensor\n", + " O = zeros(2, 2, 2, 2, 2, 2)\n", + " O[1, 1, 1, 1, 1, 1] = 1\n", + " O[2, 2, 2, 2, 2, 2] = 1\n", + " @tensor o[-1 -2; -3 -4 -5 -6] :=\n", + " O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6]\n", + "\n", + " # magnetization tensor\n", + " M = copy(O)\n", + " M[2, 2, 2, 2, 2, 2] *= -1\n", + " @tensor m[-1 -2; -3 -4 -5 -6] :=\n", + " M[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6]\n", + "\n", + " # bond interaction tensor and energy-per-site tensor\n", + " e = ComplexF64[-J J; J -J] .* q\n", + " @tensor e_x[-1 -2; -3 -4 -5 -6] :=\n", + " O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * e[-4; 4] * q[-5; 5] * q[-6; 6]\n", + " @tensor e_y[-1 -2; -3 -4 -5 -6] :=\n", + " O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * e[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6]\n", + " @tensor e_z[-1 -2; -3 -4 -5 -6] :=\n", + " O[1 2; 3 4 5 6] * e[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6]\n", + " e = e_x + e_y + e_z\n", + "\n", + " # fixed tensor map space for all three\n", + " TMS = ℂ^2 ⊗ (ℂ^2)' ← ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)'\n", + "\n", + " return TensorMap(o, TMS), TensorMap(m, TMS), TensorMap(e, TMS)\n", + "end;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Let's initialize these tensors at inverse temperature $\\beta=0.2391$, which corresponds to\n", + "a slightly lower temperature than the critical value $\\beta_c=0.2216544…$" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "beta = 0.2391\n", + "O, M, E = three_dimensional_classical_ising(; beta)\n", + "O isa PEPSKit.PEPOTensor" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Contracting the partition function\n", + "\n", + "To contract our infinite 3D partition function, we first reinterpret it as an infinite power\n", + "of a slice-to-slice transfer operator $T$, where $T$ can be seen as an infinite 2D\n", + "projected entangled-pair operator (PEPO) which consists of the rank-6 tensor `O` at each\n", + "site of an infinite 2D square lattice. In the same spirit as the boundary MPS approach, all\n", + "we need to contract the whole partition function is to find the leading eigenvector of this\n", + "PEPO. The fixed point of such a PEPO can be parametrized as a PEPS, and for the case of a\n", + "Hermitian transfer operator we can find this PEPS through [variational optimization](@cite\n", + "vanderstraeten_residual_2018).\n", + "\n", + "Indeed, for a Hermitian transfer operator $T$ we can characterize the fixed point PEPS\n", + "$|\\psi\\rangle$ which satisfies the eigenvalue equation\n", + "$T |\\psi\\rangle = \\Lambda |\\psi\\rangle$ corresponding to the largest magnitude eigenvalue\n", + "$\\Lambda$ as the solution of a variational problem\n", + "\n", + "$$\n", + "|\\psi\\rangle = \\text{argmin}_{|\\psi\\rangle} \\left ( \\lim_{N \\to ∞} - \\frac{1}{N} \\log \\left( \\frac{\\langle \\psi | T | \\psi \\rangle}{\\langle \\psi | \\psi \\rangle} \\right) \\right ) ,\n", + "$$\n", + "\n", + "where $N$ is the diverging number of sites of the 2D transfer operator $T$. The function\n", + "minimized in this expression is exactly the free energy per site of the partition function,\n", + "so we essentially find the fixed-point PEPS by variationally minimizing the free energy.\n", + "\n", + "### Defining the cost function\n", + "\n", + "Using PEPSKit.jl, this cost function and its gradient can be computed, after which we can\n", + "use [OptimKit.jl](https://github.com/Jutho/OptimKit.jl) to actually optimize it. We can\n", + "immediately recognize the denominator $\\langle \\psi | \\psi \\rangle$ as the familiar PEPS\n", + "norm, where we can compute the norm per site as the `network_value` of the\n", + "corresponding `InfiniteSquareNetwork` by contracting it with the CTMRG algorithm.\n", + "Similarly, the numerator $\\langle \\psi | T | \\psi \\rangle$ is nothing more than an\n", + "`InfiniteSquareNetwork` consisting of three layers corresponding to the ket, transfer\n", + "operator and bra objects. This object can also be constructed and contracted in a\n", + "straightforward way, so we can again compute its `network_value`.\n", + "\n", + "To define our cost function, we then need to construct the transfer operator as an\n", + "`InfinitePEPO`, construct the two infinite 2D contractible networks for the\n", + "numerator and denominator from the current PEPS and this transfer operator, and specify a\n", + "contraction algorithm we can use to compute the values of these two networks. In addition,\n", + "we'll specify the specific reverse rule algorithm that will be used to compute the gradient\n", + "of this cost function." + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "boundary_alg = SimultaneousCTMRG(; maxiter=150, tol=1e-8, verbosity=1)\n", + "rrule_alg = EigSolver(;\n", + " solver_alg=KrylovKit.Arnoldi(; maxiter=30, tol=1e-6, eager=true), iterscheme=:diffgauge\n", + ")\n", + "T = InfinitePEPO(O)\n", + "\n", + "function pepo_costfun((peps, env_double_layer, env_triple_layer))\n", + " # use Zygote to compute the gradient automatically\n", + " E, gs = withgradient(peps) do ψ\n", + " # construct the PEPS norm network\n", + " n_double_layer = InfiniteSquareNetwork(ψ)\n", + " # contract this network\n", + " env_double_layer′, info = PEPSKit.hook_pullback(\n", + " leading_boundary,\n", + " env_double_layer,\n", + " n_double_layer,\n", + " boundary_alg;\n", + " alg_rrule=rrule_alg,\n", + " )\n", + " # construct the PEPS-PEPO-PEPS overlap network\n", + " n_triple_layer = InfiniteSquareNetwork(ψ, T)\n", + " # contract this network\n", + " env_triple_layer′, info = PEPSKit.hook_pullback(\n", + " leading_boundary,\n", + " env_triple_layer,\n", + " n_triple_layer,\n", + " boundary_alg;\n", + " alg_rrule=rrule_alg,\n", + " )\n", + " # update the environments for reuse\n", + " PEPSKit.ignore_derivatives() do\n", + " PEPSKit.update!(env_double_layer, env_double_layer′)\n", + " PEPSKit.update!(env_triple_layer, env_triple_layer′)\n", + " end\n", + " # compute the network values per site\n", + " λ3 = network_value(n_triple_layer, env_triple_layer)\n", + " λ2 = network_value(n_double_layer, env_double_layer)\n", + " # use this to compute the actual cost function\n", + " return -log(real(λ3 / λ2))\n", + " end\n", + " g = only(gs)\n", + " return E, g\n", + "end;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "There are a few things to note about this cost function definition. Since we will pass it to\n", + "the `OptimKit.optimize`, we require it to return both our cost function and the\n", + "corresponding gradient. To do this, we simply use the `withgradient` method from Zygote.jl\n", + "to automatically compute the gradient of the cost function straight from the primal\n", + "computation. Since our cost function involves contractions using `leading_boundary`, we also\n", + "have to specify exactly how Zygote should handle the backpropagation of the gradient through\n", + "this function. This can be done using the `PEPSKit.hook_pullback` function from\n", + "PEPSKit.jl, which allows to hook into the pullback of a given function by specifying a\n", + "specific algorithm for the pullback computation. Here, we opted to use an Arnoldi method to\n", + "solve the linear problem defining the gradient of the network contraction at its fixed\n", + "point. This is exactly the workflow that internally underlies `PEPSKit.fixedpoint`, and\n", + "more info on particular gradient algorithms can be found in the corresponding docstrings.\n", + "\n", + "### Characterizing the optimization manifold\n", + "\n", + "In order to make the best use of OptimKit.jl, we should specify some properties of the\n", + "manifold on which we are optimizing. Looking at our cost function defined above, a point on\n", + "our optimization manifold corresponds to a `Tuple` of three objects. The first is an\n", + "`InfinitePEPS` encoding the fixed point we are actually optimizing, while the second and\n", + "third are `CTMRGEnv` objects corresponding to the environments of the double and triple\n", + "layer networks $\\langle \\psi | \\psi \\rangle$ and $\\langle \\psi | T | \\psi \\rangle$\n", + "respectively. While the environments are just there so we can reuse them between subsequent\n", + "contractions and we don't need to think about them much, optimizing over the manifold of\n", + "`InfinitePEPS` requires a bit more care.\n", + "\n", + "In particular, we need to define two kinds of operations on this manifold: a retraction and\n", + "a transport. The retraction, corresponding to the `retract` keyword argument of\n", + "`OptimKit.optimize`, specifies how to move from a point on a manifold along a given descent\n", + "direction to obtain a new manifold point. The transport, corresponding to the `transport!`\n", + "keyword argument of `OptimKit.optimize`, specifies how to transport a descent direction at a\n", + "given manifold point to a valid descent direction at a different manifold point according to\n", + "the appropriate metric. For a more detailed explanation we refer to the\n", + "[OptimKit.jl README](https://github.com/Jutho/OptimKit.jl). In PEPSKit.jl, these two\n", + "procedures are defined through the `PEPSKit.peps_retract` and\n", + "`PEPSKit.peps_transport!` methods. While it is instructive to read the corresponding\n", + "docstrings in order to understand what these actually do, here we can just blindly reuse\n", + "them where the only difference is that we have to pass along an extra environment since our\n", + "cost function requires two distinct contractions as opposed to the setting of Hamiltonian\n", + "PEPS optimization which only requires a double-layer contraction." + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "function pepo_retract((peps, env_double_layer, env_triple_layer), η, α)\n", + " (peps´, env_double_layer´), ξ = PEPSKit.peps_retract((peps, env_double_layer), η, α)\n", + " env_triple_layer´ = deepcopy(env_triple_layer)\n", + " return (peps´, env_double_layer´, env_triple_layer´), ξ\n", + "end\n", + "function pepo_transport!(\n", + " ξ,\n", + " (peps, env_double_layer, env_triple_layer),\n", + " η,\n", + " α,\n", + " (peps´, env_double_layer´, env_triple_layer´),\n", + ")\n", + " return PEPSKit.peps_transport!(\n", + " ξ, (peps, env_double_layer), η, α, (peps´, env_double_layer´)\n", + " )\n", + "end;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "### Finding the fixed point\n", + "\n", + "All that is left then is to specify the virtual spaces of the PEPS and the two environments,\n", + "initialize them in the appropriate way, choose an optimization algortithm and call the\n", + "`optimize` function from OptimKit.jl to get our desired PEPS fixed point." + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "Vpeps = ℂ^2\n", + "Venv = ℂ^12\n", + "\n", + "psi0 = initializePEPS(T, Vpeps)\n", + "env2_0 = CTMRGEnv(InfiniteSquareNetwork(psi0), Venv)\n", + "env3_0 = CTMRGEnv(InfiniteSquareNetwork(psi0, T), Venv)\n", + "\n", + "optimizer_alg = LBFGS(32; maxiter=100, gradtol=1e-5, verbosity=3)\n", + "\n", + "(psi_final, env2_final, env3_final), f, = optimize(\n", + " pepo_costfun,\n", + " (psi0, env2_0, env3_0),\n", + " optimizer_alg;\n", + " inner=PEPSKit.real_inner,\n", + " retract=pepo_retract,\n", + " (transport!)=(pepo_transport!),\n", + ");" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "### Verifying the result\n", + "\n", + "Having found the fixed point, we have essentially contracted the entire partition function\n", + "and we can start computing observables. The free energy per site for example is just given by\n", + "the final value of the cost function we have just optimized." + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "@show f" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "As another check, we can compute the magnetization per site and compare it to a [reference\n", + "value obtaind through Monte-Carlo simulations](@cite hasenbusch_monte_2001)." + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "n3_final = InfiniteSquareNetwork(psi_final, T)\n", + "num = PEPSKit.contract_local_tensor((1, 1, 1), M, n3_final, env3_final)\n", + "denom = PEPSKit._contract_site((1, 1), n3_final, env3_final)\n", + "m = abs(num / denom)\n", + "\n", + "m_ref = 0.667162\n", + "\n", + "@show abs(m - m_ref)" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "---\n", + "\n", + "*This notebook was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*" + ], + "metadata": {} + } + ], + "nbformat_minor": 3, + "metadata": { + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.10.4" + }, + "kernelspec": { + "name": "julia-1.10", + "display_name": "Julia 1.10.4", + "language": "julia" + } + }, + "nbformat": 4 +} diff --git a/docs/src/examples/bose_hubbard/index.md b/docs/src/examples/bose_hubbard/index.md new file mode 100644 index 000000000..972ca0ce0 --- /dev/null +++ b/docs/src/examples/bose_hubbard/index.md @@ -0,0 +1,330 @@ +```@meta +EditURL = "../../../../examples/bose_hubbard/main.jl" +``` + +[![](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/QuantumKitHub/PEPSKit.jl/gh-pages?filepath=dev/examples/bose_hubbard/main.ipynb) +[![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](https://nbviewer.jupyter.org/github/QuantumKitHub/PEPSKit.jl/blob/gh-pages/dev/examples/bose_hubbard/main.ipynb) +[![](https://img.shields.io/badge/download-project-orange)](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/QuantumKitHub/PEPSKit.jl/examples/tree/gh-pages/dev/examples/bose_hubbard) + + +# Optimizing the $U(1)$-symmetric Bose-Hubbard model + +This example demonstrates the simulation of the two-dimensional Bose-Hubbard model. In +particular, the point will be to showcase the use of internal symmetries and finite +particle densities in PEPS ground state searches. As we will see, incorporating symmetries +into the simulation consists of initializing a symmetric Hamiltonian, PEPS state and CTM +environment - made possible through TensorKit. + +But first let's seed the RNG and import the required modules: + +````julia +using Random +using TensorKit, PEPSKit +using MPSKit: add_physical_charge +Random.seed!(2928528935); +```` + +## Defining the model + +We will construct the Bose-Hubbard model Hamiltonian through the +[`bose_hubbard_model`](https://quantumkithub.github.io/MPSKitModels.jl/dev/man/models/#MPSKitModels.bose_hubbard_model), +function from MPSKitModels as reexported by PEPSKit. We'll simulate the model in its +Mott-insulating phase where the ratio $U/t$ is large, since in this phase we expect the +ground state to be well approximated by a PEPS with a manifest global $U(1)$ symmetry. +Furthermore, we'll impose a cutoff at 2 bosons per site, set the chemical potential to zero +and use a simple $1 \times 1$ unit cell: + +````julia +t = 1.0 +U = 30.0 +cutoff = 2 +mu = 0.0 +lattice = InfiniteSquare(1, 1); +```` + +Next, we impose an explicit global $U(1)$ symmetry as well as a fixed particle number +density in our simulations. We can do this by setting the `symmetry` argument of the +Hamiltonian constructor to `U1Irrep` and passing one as the particle number density +keyword argument `n`: + +````julia +symmetry = U1Irrep +n = 1 +H = bose_hubbard_model(ComplexF64, symmetry, lattice; cutoff, t, U, n); +```` + +Before we continue, it might be interesting to inspect the corresponding lattice physical +spaces (which is here just a $1 \times 1$ matrix due to the single-site unit cell): + +````julia +physical_spaces = physicalspace(H) +```` + +```` +1×1 Matrix{TensorKit.GradedSpace{TensorKitSectors.U1Irrep, TensorKit.SortedVectorDict{TensorKitSectors.U1Irrep, Int64}}}: + Rep[TensorKitSectors.U₁](0=>1, 1=>1, -1=>1) +```` + +Note that the physical space contains $U(1)$ charges -1, 0 and +1. Indeed, imposing a +particle number density of +1 corresponds to shifting the physical charges by -1 to +'re-center' the physical charges around the desired density. When we do this with a cutoff +of two bosons per site, i.e. starting from $U(1)$ charges 0, 1 and 2 on the physical level, +we indeed get the observed charges. + +## Characterizing the virtual spaces + +When running PEPS simulations with explicit internal symmetries, specifying the structure of +the virtual spaces of the PEPS and its environment becomes a bit more involved. For the +environment, one could in principle allow the virtual space to be chosen dynamically during +the boundary contraction using CTMRG by using a truncation scheme that allows for this +(e.g. using `alg=:truncdim` or `alg=:truncbelow` to truncate to a fixed total bond dimension +or singular value cutoff respectively). For the PEPS virtual space however, the structure +has to be specified before the optimization. + +While there are a host of techniques to do this in an informed way (e.g. starting from a +simple update result), here we just specify the virtual space manually. Since we're dealing +with a model at unit filling our physical space only contains integer $U(1)$ irreps. +Therefore, we'll build our PEPS and environment spaces using integer $U(1)$ irreps centered +around the zero charge: + +````julia +V_peps = U1Space(0 => 2, 1 => 1, -1 => 1) +V_env = U1Space(0 => 6, 1 => 4, -1 => 4, 2 => 2, -2 => 2); +```` + +## Finding the ground state + +Having defined our Hamiltonian and spaces, it is just a matter of plugging this into the +optimization framework in the usual way to find the ground state. So, we first specify all +algorithms and their tolerances: + +````julia +boundary_alg = (; tol=1e-8, alg=:simultaneous, trscheme=(; alg=:fixedspace)) +gradient_alg = (; tol=1e-6, maxiter=10, alg=:eigsolver, iterscheme=:diffgauge) +optimizer_alg = (; tol=1e-4, alg=:lbfgs, maxiter=150, ls_maxiter=2, ls_maxfg=2); +```` + +!!! note + Taking CTMRG gradients and optimizing symmetric tensors tends to be more problematic + than with dense tensors. In particular, this means that one frequently needs to tweak + the `boundary_alg`, `gradient_alg` and `optimizer_alg` settings. There rarely is a + general-purpose set of settings which will always work, so instead one has to adjust + the simulation settings for each specific application. For example, it might help to + switch between the CTMRG flavors `alg=:simultaneous` and `alg=:sequential` to + improve convergence. The evaluation of the CTMRG gradient can be instable, so there it + is advised to try the different `iterscheme=:diffgauge` and `iterscheme=:fixed` schemes + as well as different `alg` keywords. Of course the tolerances of the algorithms and + their subalgorithms also have to be compatible. For more details on the available + options, see the [`fixedpoint`](@ref) docstring. + +Keep in mind that the PEPS is constructed from a unit cell of spaces, so we have to make a +matrix of `V_peps` spaces: + +````julia +virtual_spaces = fill(V_peps, size(lattice)...) +peps₀ = InfinitePEPS(randn, ComplexF64, physical_spaces, virtual_spaces) +env₀, = leading_boundary(CTMRGEnv(peps₀, V_env), peps₀; boundary_alg...); +```` + +```` +[ Info: CTMRG init: obj = +1.693461429863e+00 +8.390974048721e-02im err = 1.0000e+00 +[ Info: CTMRG conv 19: obj = +1.181834754305e+01 -1.514255254857e-11im err = 3.6943032119e-09 time = 8.27 sec + +```` + +And at last, we optimize (which might take a bit): + +````julia +peps, env, E, info = fixedpoint( + H, peps₀, env₀; boundary_alg, gradient_alg, optimizer_alg, verbosity=3 +) +@show E; +```` + +```` +[ Info: LBFGS: initializing with f = 9.360531870693, ‖∇f‖ = 1.6957e+01 +┌ Warning: The function `scale!!` is not implemented for (values of) type `Tuple{InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.GradedSpace{TensorKitSectors.U1Irrep, TensorKit.SortedVectorDict{TensorKitSectors.U1Irrep, Int64}}, 1, 4, Vector{ComplexF64}}}, Float64}`; +│ this fallback will disappear in future versions of VectorInterface.jl +└ @ VectorInterface ~/.julia/packages/VectorInterface/J6qCR/src/fallbacks.jl:91 +[ Info: LBFGS: iter 1, time 862.65 s: f = 0.112865330403, ‖∇f‖ = 5.9876e+00, α = 1.56e+02, m = 0, nfg = 7 +┌ Warning: The function `add!!` is not implemented for (values of) type `Tuple{InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.GradedSpace{TensorKitSectors.U1Irrep, TensorKit.SortedVectorDict{TensorKitSectors.U1Irrep, Int64}}, 1, 4, Vector{ComplexF64}}}, InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.GradedSpace{TensorKitSectors.U1Irrep, TensorKit.SortedVectorDict{TensorKitSectors.U1Irrep, Int64}}, 1, 4, Vector{ComplexF64}}}, Int64, VectorInterface.One}`; +│ this fallback will disappear in future versions of VectorInterface.jl +└ @ VectorInterface ~/.julia/packages/VectorInterface/J6qCR/src/fallbacks.jl:163 +[ Info: LBFGS: iter 2, time 875.83 s: f = 0.031016651818, ‖∇f‖ = 4.7981e+00, α = 5.55e-01, m = 1, nfg = 2 +[ Info: LBFGS: iter 3, time 878.38 s: f = -0.073286659944, ‖∇f‖ = 1.4991e+00, α = 1.00e+00, m = 2, nfg = 1 +[ Info: LBFGS: iter 4, time 881.06 s: f = -0.113074511097, ‖∇f‖ = 1.4104e+00, α = 1.00e+00, m = 3, nfg = 1 +┌ Warning: Linesearch not converged after 2 iterations and 3 function evaluations: +│ α = 4.53e-02, dϕ = -5.09e-01, ϕ - ϕ₀ = -2.42e-02 +└ @ OptimKit ~/.julia/packages/OptimKit/G6i79/src/linesearches.jl:148 +[ Info: LBFGS: iter 5, time 888.55 s: f = -0.137293934550, ‖∇f‖ = 1.3317e+00, α = 4.53e-02, m = 4, nfg = 3 +┌ Warning: Linesearch not converged after 2 iterations and 3 function evaluations: +│ α = 4.19e-02, dϕ = -3.58e-01, ϕ - ϕ₀ = -1.56e-02 +└ @ OptimKit ~/.julia/packages/OptimKit/G6i79/src/linesearches.jl:148 +[ Info: LBFGS: iter 6, time 896.03 s: f = -0.152882613133, ‖∇f‖ = 1.2515e+00, α = 4.19e-02, m = 5, nfg = 3 +[ Info: LBFGS: iter 7, time 906.18 s: f = -0.167778524643, ‖∇f‖ = 3.0370e+00, α = 3.97e-01, m = 6, nfg = 4 +[ Info: LBFGS: iter 8, time 908.59 s: f = -0.200610144885, ‖∇f‖ = 8.4562e-01, α = 1.00e+00, m = 7, nfg = 1 +[ Info: LBFGS: iter 9, time 911.30 s: f = -0.214869049363, ‖∇f‖ = 5.6088e-01, α = 1.00e+00, m = 8, nfg = 1 +[ Info: LBFGS: iter 10, time 913.43 s: f = -0.222910672089, ‖∇f‖ = 9.8015e-01, α = 1.00e+00, m = 9, nfg = 1 +[ Info: LBFGS: iter 11, time 915.49 s: f = -0.230707833300, ‖∇f‖ = 4.2302e-01, α = 1.00e+00, m = 10, nfg = 1 +[ Info: LBFGS: iter 12, time 917.75 s: f = -0.238105633372, ‖∇f‖ = 2.5801e-01, α = 1.00e+00, m = 11, nfg = 1 +[ Info: LBFGS: iter 13, time 919.64 s: f = -0.247331854867, ‖∇f‖ = 3.2459e-01, α = 1.00e+00, m = 12, nfg = 1 +[ Info: LBFGS: iter 14, time 920.70 s: f = -0.253845651144, ‖∇f‖ = 2.4014e-01, α = 1.00e+00, m = 13, nfg = 1 +[ Info: LBFGS: iter 15, time 922.02 s: f = -0.261289607017, ‖∇f‖ = 3.3777e-01, α = 1.00e+00, m = 14, nfg = 1 +[ Info: LBFGS: iter 16, time 922.92 s: f = -0.267178486858, ‖∇f‖ = 2.0556e-01, α = 1.00e+00, m = 15, nfg = 1 +[ Info: LBFGS: iter 17, time 923.72 s: f = -0.269417408686, ‖∇f‖ = 1.4442e-01, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 18, time 924.84 s: f = -0.270255942689, ‖∇f‖ = 7.8602e-02, α = 1.00e+00, m = 17, nfg = 1 +[ Info: LBFGS: iter 19, time 925.66 s: f = -0.270672366603, ‖∇f‖ = 6.3259e-02, α = 1.00e+00, m = 18, nfg = 1 +[ Info: LBFGS: iter 20, time 926.46 s: f = -0.271220543802, ‖∇f‖ = 8.8755e-02, α = 1.00e+00, m = 19, nfg = 1 +[ Info: LBFGS: iter 21, time 927.59 s: f = -0.271543527453, ‖∇f‖ = 4.5233e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 22, time 928.38 s: f = -0.271650756946, ‖∇f‖ = 3.4057e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 23, time 929.16 s: f = -0.271894826592, ‖∇f‖ = 3.1507e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 24, time 930.27 s: f = -0.272056350007, ‖∇f‖ = 3.7796e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 25, time 931.08 s: f = -0.272233548851, ‖∇f‖ = 2.8370e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 26, time 931.89 s: f = -0.272383908712, ‖∇f‖ = 2.2903e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 27, time 933.01 s: f = -0.272455167221, ‖∇f‖ = 4.0448e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 28, time 933.79 s: f = -0.272553540301, ‖∇f‖ = 2.1431e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 29, time 934.56 s: f = -0.272695812729, ‖∇f‖ = 2.3543e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 30, time 935.67 s: f = -0.272771651851, ‖∇f‖ = 2.0076e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 31, time 936.45 s: f = -0.272799750606, ‖∇f‖ = 4.7874e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 32, time 937.23 s: f = -0.272878619780, ‖∇f‖ = 1.7612e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 33, time 938.34 s: f = -0.272929741282, ‖∇f‖ = 1.5455e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 34, time 939.14 s: f = -0.273001510559, ‖∇f‖ = 2.3641e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 35, time 939.94 s: f = -0.273050577801, ‖∇f‖ = 1.6253e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 36, time 941.06 s: f = -0.273061460544, ‖∇f‖ = 2.5195e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 37, time 941.84 s: f = -0.273079875270, ‖∇f‖ = 7.2299e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 38, time 942.63 s: f = -0.273083995266, ‖∇f‖ = 6.2322e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 39, time 943.75 s: f = -0.273091490830, ‖∇f‖ = 7.8002e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 40, time 944.54 s: f = -0.273097801979, ‖∇f‖ = 9.2495e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 41, time 945.33 s: f = -0.273103085883, ‖∇f‖ = 5.9273e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 42, time 946.45 s: f = -0.273106428950, ‖∇f‖ = 5.6938e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 43, time 947.24 s: f = -0.273111266609, ‖∇f‖ = 9.0725e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 44, time 948.06 s: f = -0.273121071175, ‖∇f‖ = 1.2386e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 45, time 949.20 s: f = -0.273137645098, ‖∇f‖ = 1.2942e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 46, time 950.82 s: f = -0.273141136808, ‖∇f‖ = 1.4282e-02, α = 1.35e-01, m = 20, nfg = 2 +[ Info: LBFGS: iter 47, time 951.96 s: f = -0.273154777599, ‖∇f‖ = 7.0317e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 48, time 952.79 s: f = -0.273161148021, ‖∇f‖ = 3.4952e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 49, time 953.57 s: f = -0.273164175099, ‖∇f‖ = 4.7738e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 50, time 954.68 s: f = -0.273166242670, ‖∇f‖ = 5.4242e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 51, time 955.48 s: f = -0.273168481006, ‖∇f‖ = 3.6862e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 52, time 956.27 s: f = -0.273172228386, ‖∇f‖ = 4.8463e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 53, time 957.40 s: f = -0.273174650673, ‖∇f‖ = 6.4469e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 54, time 958.18 s: f = -0.273178852481, ‖∇f‖ = 7.4990e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 55, time 958.98 s: f = -0.273186651745, ‖∇f‖ = 8.2158e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 56, time 960.10 s: f = -0.273190544009, ‖∇f‖ = 8.6751e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 57, time 960.89 s: f = -0.273194527074, ‖∇f‖ = 2.7365e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 58, time 961.68 s: f = -0.273195816168, ‖∇f‖ = 2.9114e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 59, time 962.81 s: f = -0.273197625913, ‖∇f‖ = 3.0896e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 60, time 963.60 s: f = -0.273198605154, ‖∇f‖ = 1.0394e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 61, time 964.39 s: f = -0.273202461926, ‖∇f‖ = 3.0652e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 62, time 965.50 s: f = -0.273203714519, ‖∇f‖ = 2.0133e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 63, time 966.28 s: f = -0.273204828019, ‖∇f‖ = 2.5951e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 64, time 968.19 s: f = -0.273205465822, ‖∇f‖ = 4.1444e-03, α = 4.87e-01, m = 20, nfg = 2 +[ Info: LBFGS: iter 65, time 969.01 s: f = -0.273206458356, ‖∇f‖ = 2.9919e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 66, time 969.82 s: f = -0.273208249293, ‖∇f‖ = 1.6948e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 67, time 970.96 s: f = -0.273208839969, ‖∇f‖ = 3.0193e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 68, time 971.74 s: f = -0.273209433106, ‖∇f‖ = 1.8534e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 69, time 972.52 s: f = -0.273210019598, ‖∇f‖ = 1.7898e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 70, time 973.63 s: f = -0.273211075315, ‖∇f‖ = 2.7930e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 71, time 974.42 s: f = -0.273212703695, ‖∇f‖ = 3.6612e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 72, time 975.22 s: f = -0.273214163920, ‖∇f‖ = 6.1973e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 73, time 976.34 s: f = -0.273216147362, ‖∇f‖ = 2.7120e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 74, time 977.11 s: f = -0.273217199407, ‖∇f‖ = 2.2842e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 75, time 977.91 s: f = -0.273218358117, ‖∇f‖ = 3.0566e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 76, time 979.04 s: f = -0.273219638996, ‖∇f‖ = 3.8811e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 77, time 979.83 s: f = -0.273221240011, ‖∇f‖ = 4.4440e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 78, time 980.62 s: f = -0.273222619191, ‖∇f‖ = 2.8356e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 79, time 981.74 s: f = -0.273223777532, ‖∇f‖ = 2.2842e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 80, time 982.53 s: f = -0.273224631407, ‖∇f‖ = 2.6013e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 81, time 983.33 s: f = -0.273225621866, ‖∇f‖ = 2.7625e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 82, time 984.46 s: f = -0.273226222472, ‖∇f‖ = 2.5785e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 83, time 985.23 s: f = -0.273226603039, ‖∇f‖ = 1.2203e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 84, time 986.03 s: f = -0.273226890308, ‖∇f‖ = 1.1848e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 85, time 987.16 s: f = -0.273227256564, ‖∇f‖ = 1.8281e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 86, time 987.96 s: f = -0.273227922952, ‖∇f‖ = 2.0191e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 87, time 989.91 s: f = -0.273228177117, ‖∇f‖ = 2.7409e-03, α = 3.30e-01, m = 20, nfg = 2 +[ Info: LBFGS: iter 88, time 990.72 s: f = -0.273228564518, ‖∇f‖ = 1.5762e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 89, time 991.52 s: f = -0.273228984011, ‖∇f‖ = 1.3435e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 90, time 992.64 s: f = -0.273229366257, ‖∇f‖ = 1.9460e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 91, time 993.46 s: f = -0.273230207912, ‖∇f‖ = 2.9080e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 92, time 994.25 s: f = -0.273231202568, ‖∇f‖ = 3.4541e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 93, time 995.36 s: f = -0.273232085510, ‖∇f‖ = 1.8388e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 94, time 996.15 s: f = -0.273232631167, ‖∇f‖ = 1.1594e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 95, time 996.93 s: f = -0.273232992148, ‖∇f‖ = 1.7787e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 96, time 998.04 s: f = -0.273233316686, ‖∇f‖ = 1.5449e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 97, time 998.85 s: f = -0.273233676058, ‖∇f‖ = 1.8482e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 98, time 999.62 s: f = -0.273233957076, ‖∇f‖ = 1.5452e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 99, time 1000.72 s: f = -0.273234130308, ‖∇f‖ = 1.2047e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 100, time 1001.53 s: f = -0.273234491906, ‖∇f‖ = 1.3723e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 101, time 1002.32 s: f = -0.273234862722, ‖∇f‖ = 2.2468e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 102, time 1003.43 s: f = -0.273235395986, ‖∇f‖ = 1.8201e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 103, time 1004.23 s: f = -0.273235884374, ‖∇f‖ = 1.7284e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 104, time 1005.03 s: f = -0.273236273215, ‖∇f‖ = 1.3732e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 105, time 1006.16 s: f = -0.273236575790, ‖∇f‖ = 1.4651e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 106, time 1006.95 s: f = -0.273236948216, ‖∇f‖ = 1.8153e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 107, time 1007.75 s: f = -0.273237435467, ‖∇f‖ = 2.6401e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 108, time 1008.87 s: f = -0.273237969174, ‖∇f‖ = 1.3197e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 109, time 1009.67 s: f = -0.273238333588, ‖∇f‖ = 1.0300e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 110, time 1010.46 s: f = -0.273238606412, ‖∇f‖ = 1.3512e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 111, time 1011.58 s: f = -0.273238763494, ‖∇f‖ = 2.0868e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 112, time 1012.38 s: f = -0.273238992097, ‖∇f‖ = 1.0034e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 113, time 1013.17 s: f = -0.273239242010, ‖∇f‖ = 1.0445e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 114, time 1014.29 s: f = -0.273239539038, ‖∇f‖ = 1.4904e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 115, time 1015.09 s: f = -0.273239986741, ‖∇f‖ = 1.4425e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 116, time 1017.02 s: f = -0.273240116483, ‖∇f‖ = 1.9754e-03, α = 2.24e-01, m = 20, nfg = 2 +[ Info: LBFGS: iter 117, time 1017.83 s: f = -0.273240380183, ‖∇f‖ = 9.8175e-04, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 118, time 1018.62 s: f = -0.273240575426, ‖∇f‖ = 8.6345e-04, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 119, time 1019.74 s: f = -0.273240832012, ‖∇f‖ = 1.4287e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 120, time 1020.53 s: f = -0.273241210750, ‖∇f‖ = 1.8219e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 121, time 1022.47 s: f = -0.273241482573, ‖∇f‖ = 2.4856e-03, α = 5.47e-01, m = 20, nfg = 2 +[ Info: LBFGS: iter 122, time 1023.28 s: f = -0.273241934058, ‖∇f‖ = 1.5033e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 123, time 1024.10 s: f = -0.273242166393, ‖∇f‖ = 1.2112e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 124, time 1025.25 s: f = -0.273242322900, ‖∇f‖ = 1.0134e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 125, time 1026.06 s: f = -0.273242508033, ‖∇f‖ = 1.0745e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 126, time 1026.87 s: f = -0.273242887106, ‖∇f‖ = 2.1256e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 127, time 1028.00 s: f = -0.273243341793, ‖∇f‖ = 1.6582e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 128, time 1028.81 s: f = -0.273243624575, ‖∇f‖ = 1.0434e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 129, time 1029.63 s: f = -0.273243926598, ‖∇f‖ = 1.0170e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 130, time 1030.76 s: f = -0.273244130570, ‖∇f‖ = 1.7663e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 131, time 1031.56 s: f = -0.273244384615, ‖∇f‖ = 1.4424e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 132, time 1032.37 s: f = -0.273244721419, ‖∇f‖ = 1.5925e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 133, time 1033.50 s: f = -0.273244883706, ‖∇f‖ = 1.0143e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 134, time 1034.30 s: f = -0.273244978610, ‖∇f‖ = 9.4449e-04, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 135, time 1035.10 s: f = -0.273245503204, ‖∇f‖ = 1.2344e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 136, time 1036.22 s: f = -0.273245739413, ‖∇f‖ = 2.7009e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 137, time 1037.03 s: f = -0.273246059451, ‖∇f‖ = 1.4108e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 138, time 1037.84 s: f = -0.273246259501, ‖∇f‖ = 6.7238e-04, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 139, time 1038.97 s: f = -0.273246408570, ‖∇f‖ = 9.2687e-04, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 140, time 1039.79 s: f = -0.273246576606, ‖∇f‖ = 1.2150e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 141, time 1040.60 s: f = -0.273246729060, ‖∇f‖ = 1.3157e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 142, time 1041.73 s: f = -0.273246873413, ‖∇f‖ = 7.6123e-04, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 143, time 1042.52 s: f = -0.273247027465, ‖∇f‖ = 7.9430e-04, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 144, time 1043.32 s: f = -0.273247180548, ‖∇f‖ = 1.0894e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 145, time 1044.45 s: f = -0.273247387426, ‖∇f‖ = 2.2553e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 146, time 1045.24 s: f = -0.273247645050, ‖∇f‖ = 1.1468e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 147, time 1046.05 s: f = -0.273247846912, ‖∇f‖ = 9.1650e-04, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 148, time 1047.17 s: f = -0.273248001067, ‖∇f‖ = 8.8945e-04, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 149, time 1048.03 s: f = -0.273248315567, ‖∇f‖ = 1.2684e-03, α = 1.00e+00, m = 20, nfg = 1 +┌ Warning: LBFGS: not converged to requested tol after 150 iterations and time 1050.00 s: f = -0.273248467359, ‖∇f‖ = 1.1430e-03 +└ @ OptimKit ~/.julia/packages/OptimKit/G6i79/src/lbfgs.jl:197 +E = -0.2732484673593871 + +```` + +We can compare our PEPS result to the energy obtained using a cylinder-MPS calculation +using a cylinder circumference of $L_y = 7$ and a bond dimension of 446, which yields +$E = -0.273284888$: + +````julia +E_ref = -0.273284888 +@show (E - E_ref) / E_ref; +```` + +```` +(E - E_ref) / E_ref = -0.00013326986676584008 + +```` + +--- + +*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* + diff --git a/docs/src/examples/bose_hubbard/main.ipynb b/docs/src/examples/bose_hubbard/main.ipynb new file mode 100644 index 000000000..dd4a9f707 --- /dev/null +++ b/docs/src/examples/bose_hubbard/main.ipynb @@ -0,0 +1,259 @@ +{ + "cells": [ + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Markdown #hide" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "# Optimizing the $U(1)$-symmetric Bose-Hubbard model\n", + "\n", + "This example demonstrates the simulation of the two-dimensional Bose-Hubbard model. In\n", + "particular, the point will be to showcase the use of internal symmetries and finite\n", + "particle densities in PEPS ground state searches. As we will see, incorporating symmetries\n", + "into the simulation consists of initializing a symmetric Hamiltonian, PEPS state and CTM\n", + "environment - made possible through TensorKit.\n", + "\n", + "But first let's seed the RNG and import the required modules:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Random\n", + "using TensorKit, PEPSKit\n", + "using MPSKit: add_physical_charge\n", + "Random.seed!(2928528935);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Defining the model\n", + "\n", + "We will construct the Bose-Hubbard model Hamiltonian through the\n", + "[`bose_hubbard_model`](https://quantumkithub.github.io/MPSKitModels.jl/dev/man/models/#MPSKitModels.bose_hubbard_model),\n", + "function from MPSKitModels as reexported by PEPSKit. We'll simulate the model in its\n", + "Mott-insulating phase where the ratio $U/t$ is large, since in this phase we expect the\n", + "ground state to be well approximated by a PEPS with a manifest global $U(1)$ symmetry.\n", + "Furthermore, we'll impose a cutoff at 2 bosons per site, set the chemical potential to zero\n", + "and use a simple $1 \\times 1$ unit cell:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "t = 1.0\n", + "U = 30.0\n", + "cutoff = 2\n", + "mu = 0.0\n", + "lattice = InfiniteSquare(1, 1);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Next, we impose an explicit global $U(1)$ symmetry as well as a fixed particle number\n", + "density in our simulations. We can do this by setting the `symmetry` argument of the\n", + "Hamiltonian constructor to `U1Irrep` and passing one as the particle number density\n", + "keyword argument `n`:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "symmetry = U1Irrep\n", + "n = 1\n", + "H = bose_hubbard_model(ComplexF64, symmetry, lattice; cutoff, t, U, n);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Before we continue, it might be interesting to inspect the corresponding lattice physical\n", + "spaces (which is here just a $1 \\times 1$ matrix due to the single-site unit cell):" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "physical_spaces = physicalspace(H)" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Note that the physical space contains $U(1)$ charges -1, 0 and +1. Indeed, imposing a\n", + "particle number density of +1 corresponds to shifting the physical charges by -1 to\n", + "'re-center' the physical charges around the desired density. When we do this with a cutoff\n", + "of two bosons per site, i.e. starting from $U(1)$ charges 0, 1 and 2 on the physical level,\n", + "we indeed get the observed charges.\n", + "\n", + "## Characterizing the virtual spaces\n", + "\n", + "When running PEPS simulations with explicit internal symmetries, specifying the structure of\n", + "the virtual spaces of the PEPS and its environment becomes a bit more involved. For the\n", + "environment, one could in principle allow the virtual space to be chosen dynamically during\n", + "the boundary contraction using CTMRG by using a truncation scheme that allows for this\n", + "(e.g. using `alg=:truncdim` or `alg=:truncbelow` to truncate to a fixed total bond dimension\n", + "or singular value cutoff respectively). For the PEPS virtual space however, the structure\n", + "has to be specified before the optimization.\n", + "\n", + "While there are a host of techniques to do this in an informed way (e.g. starting from a\n", + "simple update result), here we just specify the virtual space manually. Since we're dealing\n", + "with a model at unit filling our physical space only contains integer $U(1)$ irreps.\n", + "Therefore, we'll build our PEPS and environment spaces using integer $U(1)$ irreps centered\n", + "around the zero charge:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "V_peps = U1Space(0 => 2, 1 => 1, -1 => 1)\n", + "V_env = U1Space(0 => 6, 1 => 4, -1 => 4, 2 => 2, -2 => 2);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Finding the ground state\n", + "\n", + "Having defined our Hamiltonian and spaces, it is just a matter of plugging this into the\n", + "optimization framework in the usual way to find the ground state. So, we first specify all\n", + "algorithms and their tolerances:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "boundary_alg = (; tol=1e-8, alg=:simultaneous, trscheme=(; alg=:fixedspace))\n", + "gradient_alg = (; tol=1e-6, maxiter=10, alg=:eigsolver, iterscheme=:diffgauge)\n", + "optimizer_alg = (; tol=1e-4, alg=:lbfgs, maxiter=150, ls_maxiter=2, ls_maxfg=2);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "!!! note\n", + "\tTaking CTMRG gradients and optimizing symmetric tensors tends to be more problematic\n", + " than with dense tensors. In particular, this means that one frequently needs to tweak\n", + " the `boundary_alg`, `gradient_alg` and `optimizer_alg` settings. There rarely is a\n", + " general-purpose set of settings which will always work, so instead one has to adjust\n", + " the simulation settings for each specific application. For example, it might help to\n", + " switch between the CTMRG flavors `alg=:simultaneous` and `alg=:sequential` to\n", + " improve convergence. The evaluation of the CTMRG gradient can be instable, so there it\n", + " is advised to try the different `iterscheme=:diffgauge` and `iterscheme=:fixed` schemes\n", + " as well as different `alg` keywords. Of course the tolerances of the algorithms and\n", + " their subalgorithms also have to be compatible. For more details on the available\n", + " options, see the `fixedpoint` docstring.\n", + "\n", + "Keep in mind that the PEPS is constructed from a unit cell of spaces, so we have to make a\n", + "matrix of `V_peps` spaces:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "virtual_spaces = fill(V_peps, size(lattice)...)\n", + "peps₀ = InfinitePEPS(randn, ComplexF64, physical_spaces, virtual_spaces)\n", + "env₀, = leading_boundary(CTMRGEnv(peps₀, V_env), peps₀; boundary_alg...);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "And at last, we optimize (which might take a bit):" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "peps, env, E, info = fixedpoint(\n", + " H, peps₀, env₀; boundary_alg, gradient_alg, optimizer_alg, verbosity=3\n", + ")\n", + "@show E;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "We can compare our PEPS result to the energy obtained using a cylinder-MPS calculation\n", + "using a cylinder circumference of $L_y = 7$ and a bond dimension of 446, which yields\n", + "$E = -0.273284888$:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "E_ref = -0.273284888\n", + "@show (E - E_ref) / E_ref;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "---\n", + "\n", + "*This notebook was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*" + ], + "metadata": {} + } + ], + "nbformat_minor": 3, + "metadata": { + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.11.4" + }, + "kernelspec": { + "name": "julia-1.11", + "display_name": "Julia 1.11.4", + "language": "julia" + } + }, + "nbformat": 4 +} diff --git a/docs/src/examples/boundary_mps/index.md b/docs/src/examples/boundary_mps/index.md new file mode 100644 index 000000000..a98e199d5 --- /dev/null +++ b/docs/src/examples/boundary_mps/index.md @@ -0,0 +1,284 @@ +```@meta +EditURL = "../../../../examples/boundary_mps/main.jl" +``` + +[![](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/QuantumKitHub/PEPSKit.jl/gh-pages?filepath=dev/examples/boundary_mps/main.ipynb) +[![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](https://nbviewer.jupyter.org/github/QuantumKitHub/PEPSKit.jl/blob/gh-pages/dev/examples/boundary_mps/main.ipynb) +[![](https://img.shields.io/badge/download-project-orange)](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/QuantumKitHub/PEPSKit.jl/examples/tree/gh-pages/dev/examples/boundary_mps) + + +# [Boundary MPS contractions of 2D networks] (@id e_boundary_mps) + +Instead of using CTMRG to contract the network encoding the norm of an infinite PEPS, one +can also use so-called [boundary MPS methods](@cite haegeman_diagonalizing_2017) to contract +this network. In this example, we will demonstrate how to use [the VUMPS algorithm](@cite +vanderstraeten_tangentspace_2019) to do so. + +Before we start, we'll fix the random seed for reproducability: + +````julia +using Random +Random.seed!(29384293742893); +```` + +Besides `TensorKit` and `PEPSKit`, here we also need to load the +[`MPSKit.jl`](https://quantumkithub.github.io/MPSKit.jl/stable/) package which implements a +host of tools for working with 1D matrix product states (MPS), including the VUMPS +algorithm: + +````julia +using TensorKit, PEPSKit, MPSKit +```` + +## Computing a PEPS norm + +We start by initializing a random infinite PEPS. Let us use uniformly distributed complex +entries using `rand` (which sometimes lead to better convergence than Gaussian distributed +`randn` elements): + +````julia +peps₀ = InfinitePEPS(rand, ComplexF64, ComplexSpace(2), ComplexSpace(2)) +```` + +```` +InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}}(TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}[TensorMap(ℂ^2 ← (ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)')): +[:, :, 1, 1, 1] = + 0.8343040072662887 + 0.15425705836788395im 0.4612746978522435 + 0.7411151918989216im + 0.6640771294125087 + 0.4428356798799721im 0.9163597170532635 + 0.24145695415210522im + +[:, :, 2, 1, 1] = + 0.44289651954161835 + 0.5968081052313008im 0.5473659268881094 + 0.37528062658773764im + 0.00644367423621961 + 0.9414462569909486im 0.36006028879229457 + 0.6157267258321241im + +[:, :, 1, 2, 1] = + 0.04956065285909117 + 0.26119820734171617im 0.9153298540884296 + 0.3990244910357601im + 0.17944112964295234 + 0.4233545106724528im 0.020358359069476473 + 0.6501897922267199im + +[:, :, 2, 2, 1] = + 0.040493161136161526 + 0.03501665486055905im 0.2591040734810338 + 0.8830094105726012im + 0.781658280511654 + 0.9662812119384394im 0.8169988652653896 + 0.674481616952991im + +[:, :, 1, 1, 2] = + 0.2242833355717867 + 0.14929928451790686im 0.6883051212688887 + 0.588769359105893im + 0.046322385671192734 + 0.8543796191082029im 0.6437874016748227 + 0.257253015722232im + +[:, :, 2, 1, 2] = + 0.8719996187768273 + 0.25052026742300637im 0.5714417314833022 + 0.9944321644519715im + 0.4273547968422168 + 0.6068478826937488im 0.4946426302106661 + 0.8353867377249198im + +[:, :, 1, 2, 2] = + 0.6857354516279699 + 0.0952105576480895im 0.14591923452838773 + 0.0853564870114002im + 0.6779060054394721 + 0.4947495895268207im 0.9280821860668365 + 0.931316796924268im + +[:, :, 2, 2, 2] = + 0.3716373366637086 + 0.2556099109043021im 0.7954831107819061 + 0.016909518973250215im + 0.9376161032047406 + 0.6320864548041844im 0.7900851372111909 + 0.5457560526661245im +;;]) +```` + +To compute its norm, usually we would construct a double-layer `InfiniteSquareNetwork` which +encodes the bra-ket PEPS overlap and then contract this infinite square network, for example +using CTMRG. Here however, we will use another approach. If we take out a single row of this +infinite norm network, we can interpret it as a 2D row-to-row transfer operator ``T``. Here, +this transfer operator consists of an effective local rank-4 tensor at every site of a 2D +square lattice, where the local effective tensor is given by the contraction of a bra and +ket [`PEPSKit.PEPSTensor`](@ref) across their physical leg. Since the network we want to +contract can be interpreted as the infinite power of ``T``, we can contract it by finding +its leading eigenvector as a 1D MPS, which we call the boundary MPS. + +In PEPSKit.jl, we can directly construct the transfer operator corresponding to a PEPS norm +network from a given infinite PEPS as an [`InfiniteTransferPEPS`](@ref) object. +Additionally, we need to specify which direction should be facing north (`dir=1` +corresponding to north, counting clockwise) and which row is selected from the north - but +since we have a trivial unit cell there is only one row: + +````julia +dir = 1 ## does not rotate the partition function +row = 1 +T = InfiniteTransferPEPS(peps₀, dir, row) +```` + +```` +single site MPSKit.InfiniteMPO{Tuple{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}, TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}}}: +╷ ⋮ +┼ O[1]: (TensorMap(ℂ^2 ← (ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)')), TensorMap(ℂ^2 ← (ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)'))) +╵ ⋮ + +```` + +Since we'll find the leading eigenvector of ``T`` as a boundary MPS, we first need to +construct an initial guess to supply to our algorithm. We can do this using the +[`initialize_mps`](@ref) function, which constructs a random MPS with a specific virtual +space for a given transfer operator. Here, we'll build an initial guess for the boundary MPS +with a bond dimension of 20: + +````julia +mps₀ = initialize_mps(T, [ComplexSpace(20)]) +```` + +```` +single site InfiniteMPS: +│ ⋮ +│ C[1]: TensorMap(ℂ^20 ← ℂ^20) +├── AL[1]: TensorMap((ℂ^20 ⊗ ℂ^2 ⊗ (ℂ^2)') ← ℂ^20) +│ ⋮ + +```` + +Note that this will just construct a MPS with random Gaussian entries based on the physical +spaces of the supplied transfer operator. Of course, one might come up with a better initial +guess (leading to better convergence) depending on the application. To find the leading +boundary MPS fixed point, we call [`leading_boundary`](@ref) using the +[`MPSKit.VUMPS`](@extref) algorithm from MPSKit: + +````julia +mps, env, ϵ = leading_boundary(mps₀, T, VUMPS(; tol=1e-6, verbosity=2)); +```` + +```` +[ Info: VUMPS init: obj = +5.052950412844e+00 +1.493192627823e-02im err = 8.4684e-01 +[ Info: VUMPS conv 4: obj = +1.744071150138e+01 +2.417441557995e-08im err = 1.9047772246e-07 time = 3.69 sec + +```` + +The norm of the state per unit cell is then given by the expectation value +$\langle \psi_\text{MPS} | \mathbb{T} | \psi_\text{MPS} \rangle$: + +````julia +norm_vumps = abs(prod(expectation_value(mps, T))) +```` + +```` +17.440711501378814 +```` + +This can be compared to the result obtained using CTMRG, where we see that the results match: + +````julia +env_ctmrg, = leading_boundary( + CTMRGEnv(peps₀, ComplexSpace(20)), peps₀; tol=1e-6, verbosity=2 +) +norm_ctmrg = abs(norm(peps₀, env_ctmrg)) +@show abs(norm_vumps - norm_ctmrg) / norm_vumps; +```` + +```` +[ Info: CTMRG init: obj = -5.556349490423e-01 +1.605938670370e+00im err = 1.0000e+00 +[ Info: CTMRG conv 37: obj = +1.744071151099e+01 err = 3.2056303631e-07 time = 4.37 sec +abs(norm_vumps - norm_ctmrg) / norm_vumps = 5.510362083182129e-10 + +```` + +## Working with unit cells + +For PEPS with non-trivial unit cells, the principle is exactly the same. The only difference +is that now the transfer operator of the PEPS norm partition function has multiple rows or +'lines', each of which can be represented by an [`InfiniteTransferPEPS`](@ref) object. Such +a multi-line transfer operator is represented by a `MultilineTransferPEPS` object. In this +case, the boundary MPS is an [`MultilineMPS`](@extref) object, which should be initialized +by specifying a virtual space for each site in the partition function unit cell. + +First, we construct a PEPS with a $2 \times 2$ unit cell using the `unitcell` keyword +argument and then define the corresponding transfer operator, where we again specify the +direction which will be facing north: + +````julia +peps₀_2x2 = InfinitePEPS( + rand, ComplexF64, ComplexSpace(2), ComplexSpace(2); unitcell=(2, 2) +) +T_2x2 = PEPSKit.MultilineTransferPEPS(peps₀_2x2, dir); +```` + +Now, the procedure is the same as before: We compute the norm once using VUMPS, once using CTMRG and then compare. + +````julia +mps₀_2x2 = initialize_mps(T_2x2, fill(ComplexSpace(20), 2, 2)) +mps_2x2, = leading_boundary(mps₀_2x2, T_2x2, VUMPS(; tol=1e-6, verbosity=2)) +norm_2x2_vumps = abs(prod(expectation_value(mps_2x2, T_2x2))) + +env_ctmrg_2x2, = leading_boundary( + CTMRGEnv(peps₀_2x2, ComplexSpace(20)), peps₀_2x2; tol=1e-6, verbosity=2 +) +norm_2x2_ctmrg = abs(norm(peps₀_2x2, env_ctmrg_2x2)) + +@show abs(norm_2x2_vumps - norm_2x2_ctmrg) / norm_2x2_vumps; +```` + +```` +[ Info: VUMPS init: obj = +6.668046237341e+02 -1.267878277078e+01im err = 8.7901e-01 +[ Info: VUMPS conv 69: obj = +9.723958968917e+04 -3.481605377714e-03im err = 6.3841720875e-07 time = 4.12 sec +[ Info: CTMRG init: obj = +1.074898090007e+03 -2.096255594496e+02im err = 1.0000e+00 +[ Info: CTMRG conv 41: obj = +9.723959008610e+04 err = 6.0518230963e-07 time = 1.54 sec +abs(norm_2x2_vumps - norm_2x2_ctmrg) / norm_2x2_vumps = 4.08201516090106e-9 + +```` + +Again, the results are compatible. Note that for larger unit cells and non-Hermitian PEPS +[the VUMPS algorithm may become unstable](@cite vanderstraeten_variational_2022), in which +case the CTMRG algorithm is recommended. + +## Contracting PEPO overlaps + +Using exactly the same machinery, we can contract 2D networks which encode the expectation +value of a PEPO for a given PEPS state. As an example, we can consider the overlap of the +PEPO correponding to the partition function of [3D classical Ising model](@ref e_3d_ising) +with our random PEPS from before and evaluate the overlap $\langle \psi_\text{PEPS} | +O_\text{PEPO} | \psi_\text{PEPS} \rangle$. + +The classical Ising PEPO is defined as follows: + +````julia +function ising_pepo(β; unitcell=(1, 1, 1)) + t = ComplexF64[exp(β) exp(-β); exp(-β) exp(β)] + q = sqrt(t) + + O = zeros(2, 2, 2, 2, 2, 2) + O[1, 1, 1, 1, 1, 1] = 1 + O[2, 2, 2, 2, 2, 2] = 1 + @tensor o[-1 -2; -3 -4 -5 -6] := + O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6] + O = TensorMap(o, ℂ^2 ⊗ (ℂ^2)' ← ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)') + + return InfinitePEPO(O; unitcell) +end; +```` + +To evaluate the overlap, we instantiate the PEPO and the corresponding [`InfiniteTransferPEPO`](@ref) +in the right direction, on the right row of the partition function (trivial here): + +````julia +pepo = ising_pepo(1) +transfer_pepo = InfiniteTransferPEPO(peps₀, pepo, 1, 1) +```` + +```` +single site MPSKit.InfiniteMPO{Tuple{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}, TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}, TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 2, 4, Vector{ComplexF64}}}}: +╷ ⋮ +┼ O[1]: (TensorMap(ℂ^2 ← (ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)')), TensorMap(ℂ^2 ← (ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)')), TensorMap((ℂ^2 ⊗ (ℂ^2)') ← (ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)'))) +╵ ⋮ + +```` + +As before, we converge the boundary MPS using VUMPS and then compute the expectation value: + +````julia +mps₀_pepo = initialize_mps(transfer_pepo, [ComplexSpace(20)]) +mps_pepo, = leading_boundary(mps₀_pepo, transfer_pepo, VUMPS(; tol=1e-6, verbosity=2)) +norm_pepo = abs(prod(expectation_value(mps_pepo, transfer_pepo))); +@show norm_pepo; +```` + +```` +[ Info: VUMPS init: obj = +3.309203535702e+01 -4.227375981212e-01im err = 9.3280e-01 +[ Info: VUMPS conv 5: obj = +2.483696258643e+02 +2.387851822319e-07im err = 5.0174146749e-08 time = 2.69 sec +norm_pepo = 248.36962586428106 + +```` + +These objects and routines can be used to optimize PEPS fixed points of 3D partition +functions, see for example [Vanderstraeten et al.](@cite vanderstraeten_residual_2018) + +--- + +*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* + diff --git a/docs/src/examples/boundary_mps/main.ipynb b/docs/src/examples/boundary_mps/main.ipynb new file mode 100644 index 000000000..6b23bb78a --- /dev/null +++ b/docs/src/examples/boundary_mps/main.ipynb @@ -0,0 +1,349 @@ +{ + "cells": [ + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Markdown #hide" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "# [Boundary MPS contractions of 2D networks] (@id e_boundary_mps)\n", + "\n", + "Instead of using CTMRG to contract the network encoding the norm of an infinite PEPS, one\n", + "can also use so-called [boundary MPS methods](@cite haegeman_diagonalizing_2017) to contract\n", + "this network. In this example, we will demonstrate how to use [the VUMPS algorithm](@cite\n", + "vanderstraeten_tangentspace_2019) to do so.\n", + "\n", + "Before we start, we'll fix the random seed for reproducability:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Random\n", + "Random.seed!(29384293742893);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Besides `TensorKit` and `PEPSKit`, here we also need to load the\n", + "[`MPSKit.jl`](https://quantumkithub.github.io/MPSKit.jl/stable/) package which implements a\n", + "host of tools for working with 1D matrix product states (MPS), including the VUMPS\n", + "algorithm:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "using TensorKit, PEPSKit, MPSKit" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Computing a PEPS norm\n", + "\n", + "We start by initializing a random infinite PEPS. Let us use uniformly distributed complex\n", + "entries using `rand` (which sometimes lead to better convergence than Gaussian distributed\n", + "`randn` elements):" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "peps₀ = InfinitePEPS(rand, ComplexF64, ComplexSpace(2), ComplexSpace(2))" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "To compute its norm, usually we would construct a double-layer `InfiniteSquareNetwork` which\n", + "encodes the bra-ket PEPS overlap and then contract this infinite square network, for example\n", + "using CTMRG. Here however, we will use another approach. If we take out a single row of this\n", + "infinite norm network, we can interpret it as a 2D row-to-row transfer operator $T$. Here,\n", + "this transfer operator consists of an effective local rank-4 tensor at every site of a 2D\n", + "square lattice, where the local effective tensor is given by the contraction of a bra and\n", + "ket `PEPSKit.PEPSTensor` across their physical leg. Since the network we want to\n", + "contract can be interpreted as the infinite power of $T$, we can contract it by finding\n", + "its leading eigenvector as a 1D MPS, which we call the boundary MPS.\n", + "\n", + "In PEPSKit.jl, we can directly construct the transfer operator corresponding to a PEPS norm\n", + "network from a given infinite PEPS as an `InfiniteTransferPEPS` object.\n", + "Additionally, we need to specify which direction should be facing north (`dir=1`\n", + "corresponding to north, counting clockwise) and which row is selected from the north - but\n", + "since we have a trivial unit cell there is only one row:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "dir = 1 ## does not rotate the partition function\n", + "row = 1\n", + "T = InfiniteTransferPEPS(peps₀, dir, row)" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Since we'll find the leading eigenvector of $T$ as a boundary MPS, we first need to\n", + "construct an initial guess to supply to our algorithm. We can do this using the\n", + "`initialize_mps` function, which constructs a random MPS with a specific virtual\n", + "space for a given transfer operator. Here, we'll build an initial guess for the boundary MPS\n", + "with a bond dimension of 20:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "mps₀ = initialize_mps(T, [ComplexSpace(20)])" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Note that this will just construct a MPS with random Gaussian entries based on the physical\n", + "spaces of the supplied transfer operator. Of course, one might come up with a better initial\n", + "guess (leading to better convergence) depending on the application. To find the leading\n", + "boundary MPS fixed point, we call `leading_boundary` using the\n", + "`MPSKit.VUMPS` algorithm from MPSKit:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "mps, env, ϵ = leading_boundary(mps₀, T, VUMPS(; tol=1e-6, verbosity=2));" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "The norm of the state per unit cell is then given by the expectation value\n", + "$\\langle \\psi_\\text{MPS} | \\mathbb{T} | \\psi_\\text{MPS} \\rangle$:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "norm_vumps = abs(prod(expectation_value(mps, T)))" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "This can be compared to the result obtained using CTMRG, where we see that the results match:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "env_ctmrg, = leading_boundary(\n", + " CTMRGEnv(peps₀, ComplexSpace(20)), peps₀; tol=1e-6, verbosity=2\n", + ")\n", + "norm_ctmrg = abs(norm(peps₀, env_ctmrg))\n", + "@show abs(norm_vumps - norm_ctmrg) / norm_vumps;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Working with unit cells\n", + "\n", + "For PEPS with non-trivial unit cells, the principle is exactly the same. The only difference\n", + "is that now the transfer operator of the PEPS norm partition function has multiple rows or\n", + "'lines', each of which can be represented by an `InfiniteTransferPEPS` object. Such\n", + "a multi-line transfer operator is represented by a `MultilineTransferPEPS` object. In this\n", + "case, the boundary MPS is an `MultilineMPS` object, which should be initialized\n", + "by specifying a virtual space for each site in the partition function unit cell.\n", + "\n", + "First, we construct a PEPS with a $2 \\times 2$ unit cell using the `unitcell` keyword\n", + "argument and then define the corresponding transfer operator, where we again specify the\n", + "direction which will be facing north:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "peps₀_2x2 = InfinitePEPS(\n", + " rand, ComplexF64, ComplexSpace(2), ComplexSpace(2); unitcell=(2, 2)\n", + ")\n", + "T_2x2 = PEPSKit.MultilineTransferPEPS(peps₀_2x2, dir);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Now, the procedure is the same as before: We compute the norm once using VUMPS, once using CTMRG and then compare." + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "mps₀_2x2 = initialize_mps(T_2x2, fill(ComplexSpace(20), 2, 2))\n", + "mps_2x2, = leading_boundary(mps₀_2x2, T_2x2, VUMPS(; tol=1e-6, verbosity=2))\n", + "norm_2x2_vumps = abs(prod(expectation_value(mps_2x2, T_2x2)))\n", + "\n", + "env_ctmrg_2x2, = leading_boundary(\n", + " CTMRGEnv(peps₀_2x2, ComplexSpace(20)), peps₀_2x2; tol=1e-6, verbosity=2\n", + ")\n", + "norm_2x2_ctmrg = abs(norm(peps₀_2x2, env_ctmrg_2x2))\n", + "\n", + "@show abs(norm_2x2_vumps - norm_2x2_ctmrg) / norm_2x2_vumps;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Again, the results are compatible. Note that for larger unit cells and non-Hermitian PEPS\n", + "[the VUMPS algorithm may become unstable](@cite vanderstraeten_variational_2022), in which\n", + "case the CTMRG algorithm is recommended.\n", + "\n", + "## Contracting PEPO overlaps\n", + "\n", + "Using exactly the same machinery, we can contract 2D networks which encode the expectation\n", + "value of a PEPO for a given PEPS state. As an example, we can consider the overlap of the\n", + "PEPO correponding to the partition function of 3D classical Ising model\n", + "with our random PEPS from before and evaluate the overlap $\\langle \\psi_\\text{PEPS} |\n", + "O_\\text{PEPO} | \\psi_\\text{PEPS} \\rangle$.\n", + "\n", + "The classical Ising PEPO is defined as follows:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "function ising_pepo(β; unitcell=(1, 1, 1))\n", + " t = ComplexF64[exp(β) exp(-β); exp(-β) exp(β)]\n", + " q = sqrt(t)\n", + "\n", + " O = zeros(2, 2, 2, 2, 2, 2)\n", + " O[1, 1, 1, 1, 1, 1] = 1\n", + " O[2, 2, 2, 2, 2, 2] = 1\n", + " @tensor o[-1 -2; -3 -4 -5 -6] :=\n", + " O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6]\n", + " O = TensorMap(o, ℂ^2 ⊗ (ℂ^2)' ← ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)')\n", + "\n", + " return InfinitePEPO(O; unitcell)\n", + "end;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "To evaluate the overlap, we instantiate the PEPO and the corresponding `InfiniteTransferPEPO`\n", + "in the right direction, on the right row of the partition function (trivial here):" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "pepo = ising_pepo(1)\n", + "transfer_pepo = InfiniteTransferPEPO(peps₀, pepo, 1, 1)" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "As before, we converge the boundary MPS using VUMPS and then compute the expectation value:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "mps₀_pepo = initialize_mps(transfer_pepo, [ComplexSpace(20)])\n", + "mps_pepo, = leading_boundary(mps₀_pepo, transfer_pepo, VUMPS(; tol=1e-6, verbosity=2))\n", + "norm_pepo = abs(prod(expectation_value(mps_pepo, transfer_pepo)));\n", + "@show norm_pepo;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "These objects and routines can be used to optimize PEPS fixed points of 3D partition\n", + "functions, see for example [Vanderstraeten et al.](@cite vanderstraeten_residual_2018)" + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "---\n", + "\n", + "*This notebook was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*" + ], + "metadata": {} + } + ], + "nbformat_minor": 3, + "metadata": { + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.11.4" + }, + "kernelspec": { + "name": "julia-1.11", + "display_name": "Julia 1.11.4", + "language": "julia" + } + }, + "nbformat": 4 +} diff --git a/docs/src/examples/fermi_hubbard/index.md b/docs/src/examples/fermi_hubbard/index.md new file mode 100644 index 000000000..23f52e142 --- /dev/null +++ b/docs/src/examples/fermi_hubbard/index.md @@ -0,0 +1,235 @@ +```@meta +EditURL = "../../../../examples/fermi_hubbard/main.jl" +``` + +[![](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/QuantumKitHub/PEPSKit.jl/gh-pages?filepath=dev/examples/fermi_hubbard/main.ipynb) +[![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](https://nbviewer.jupyter.org/github/QuantumKitHub/PEPSKit.jl/blob/gh-pages/dev/examples/fermi_hubbard/main.ipynb) +[![](https://img.shields.io/badge/download-project-orange)](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/QuantumKitHub/PEPSKit.jl/examples/tree/gh-pages/dev/examples/fermi_hubbard) + + +# Fermi-Hubbard model with $f\mathbb{Z}_2 \boxtimes U(1)$ symmetry, at large $U$ and half-filling + +In this example, we will demonstrate how to handle fermionic PEPS tensors and how to +optimize them. To that end, we consider the two-dimensional Hubbard model + +```math +H = -t \sum_{\langle i,j \rangle} \sum_{\sigma} \left( c_{i,\sigma}^+ c_{j,\sigma}^- - +c_{i,\sigma}^- c_{j,\sigma}^+ \right) + U \sum_i n_{i,\uparrow}n_{i,\downarrow} - \mu \sum_i n_i +``` + +where $\sigma \in \{\uparrow,\downarrow\}$ and $n_{i,\sigma} = c_{i,\sigma}^+ c_{i,\sigma}^-$ +is the fermionic number operator. As in previous examples, using fermionic degrees of freedom +is a matter of creating tensors with the right symmetry sectors - the rest of the simulation +workflow remains the same. + +First though, we make the example deterministic by seeding the RNG, and we make our imports: + +````julia +using Random +using TensorKit, PEPSKit +using MPSKit: add_physical_charge +Random.seed!(2928528937); +```` + +## Defining the fermionic Hamiltonian + +Let us start by fixing the parameters of the Hubbard model. We're going to use a hopping of +$t=1$ and a large $U=8$ on a $2 \times 2$ unit cell: + +````julia +t = 1.0 +U = 8.0 +lattice = InfiniteSquare(2, 2); +```` + +In order to create fermionic tensors, one needs to define symmetry sectors using TensorKit's +`FermionParity`. Not only do we want use fermion parity but we also want our +particles to exploit the global $U(1)$ symmetry. The combined product sector can be obtained +using the [Deligne product](https://jutho.github.io/TensorKit.jl/stable/lib/sectors/#TensorKitSectors.deligneproduct-Tuple{Sector,%20Sector}), +called through `⊠` which is obtained by typing `\boxtimes+TAB`. We will not impose any extra +spin symmetry, so we have: + +````julia +fermion = fℤ₂ +particle_symmetry = U1Irrep +spin_symmetry = Trivial +S = fermion ⊠ particle_symmetry +```` + +```` +TensorKitSectors.ProductSector{Tuple{TensorKitSectors.FermionParity, TensorKitSectors.U1Irrep}} +```` + +The next step is defining graded virtual PEPS and environment spaces using `S`. Here we also +use the symmetry sector to impose half-filling. That is all we need to define the Hubbard +Hamiltonian: + +````julia +D, χ = 1, 1 +V_peps = Vect[S]((0, 0) => 2 * D, (1, 1) => D, (1, -1) => D) +V_env = Vect[S]( + (0, 0) => 4 * χ, (1, -1) => 2 * χ, (1, 1) => 2 * χ, (0, 2) => χ, (0, -2) => χ +) +S_aux = S((1, -1)) +H₀ = hubbard_model(ComplexF64, particle_symmetry, spin_symmetry, lattice; t, U) +H = add_physical_charge(H₀, fill(S_aux, size(H₀.lattice)...)); +```` + +## Finding the ground state + +Again, the procedure of ground state optimization is very similar to before. First, we +define all algorithmic parameters: + +````julia +boundary_alg = (; tol=1e-8, alg=:simultaneous, trscheme=(; alg=:fixedspace)) +gradient_alg = (; tol=1e-6, alg=:eigsolver, maxiter=10, iterscheme=:diffgauge) +optimizer_alg = (; tol=1e-4, alg=:lbfgs, maxiter=80, ls_maxiter=3, ls_maxfg=3) +```` + +```` +(tol = 0.0001, alg = :lbfgs, maxiter = 80, ls_maxiter = 3, ls_maxfg = 3) +```` + +Second, we initialize a PEPS state and environment (which we converge) constructed from +symmetric physical and virtual spaces: + +````julia +physical_spaces = physicalspace(H) +virtual_spaces = fill(V_peps, size(lattice)...) +peps₀ = InfinitePEPS(randn, ComplexF64, physical_spaces, virtual_spaces) +env₀, = leading_boundary(CTMRGEnv(peps₀, V_env), peps₀; boundary_alg...); +```` + +```` +[ Info: CTMRG init: obj = +5.484842275412e+04 +4.469243203539e+04im err = 1.0000e+00 +[ Info: CTMRG conv 26: obj = +8.371681846538e+04 -3.791428753175e-07im err = 7.4963852327e-09 time = 8.21 sec + +```` + +And third, we start the ground state search (this does take quite long): + +````julia +peps, env, E, info = fixedpoint( + H, peps₀, env₀; boundary_alg, gradient_alg, optimizer_alg, verbosity=3 +) +@show E; +```` + +```` +[ Info: LBFGS: initializing with f = 6.680719803101, ‖∇f‖ = 9.5842e+00 +┌ Warning: The function `scale!!` is not implemented for (values of) type `Tuple{InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.GradedSpace{TensorKitSectors.ProductSector{Tuple{TensorKitSectors.FermionParity, TensorKitSectors.U1Irrep}}, TensorKit.SortedVectorDict{TensorKitSectors.ProductSector{Tuple{TensorKitSectors.FermionParity, TensorKitSectors.U1Irrep}}, Int64}}, 1, 4, Vector{ComplexF64}}}, Float64}`; +│ this fallback will disappear in future versions of VectorInterface.jl +└ @ VectorInterface ~/.julia/packages/VectorInterface/J6qCR/src/fallbacks.jl:91 +┌ Warning: Linesearch not converged after 1 iterations and 4 function evaluations: +│ α = 2.50e+01, dϕ = -1.49e-01, ϕ - ϕ₀ = -2.88e+00 +└ @ OptimKit ~/.julia/packages/OptimKit/G6i79/src/linesearches.jl:148 +[ Info: LBFGS: iter 1, time 835.44 s: f = 3.801394787694, ‖∇f‖ = 2.3457e+01, α = 2.50e+01, m = 0, nfg = 4 +┌ Warning: The function `add!!` is not implemented for (values of) type `Tuple{InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.GradedSpace{TensorKitSectors.ProductSector{Tuple{TensorKitSectors.FermionParity, TensorKitSectors.U1Irrep}}, TensorKit.SortedVectorDict{TensorKitSectors.ProductSector{Tuple{TensorKitSectors.FermionParity, TensorKitSectors.U1Irrep}}, Int64}}, 1, 4, Vector{ComplexF64}}}, InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.GradedSpace{TensorKitSectors.ProductSector{Tuple{TensorKitSectors.FermionParity, TensorKitSectors.U1Irrep}}, TensorKit.SortedVectorDict{TensorKitSectors.ProductSector{Tuple{TensorKitSectors.FermionParity, TensorKitSectors.U1Irrep}}, Int64}}, 1, 4, Vector{ComplexF64}}}, Int64, VectorInterface.One}`; +│ this fallback will disappear in future versions of VectorInterface.jl +└ @ VectorInterface ~/.julia/packages/VectorInterface/J6qCR/src/fallbacks.jl:163 +┌ Warning: Linesearch not converged after 1 iterations and 4 function evaluations: +│ α = 2.50e+01, dϕ = -5.73e-03, ϕ - ϕ₀ = -3.81e+00 +└ @ OptimKit ~/.julia/packages/OptimKit/G6i79/src/linesearches.jl:148 +[ Info: LBFGS: iter 2, time 890.92 s: f = -0.009753189324, ‖∇f‖ = 3.2047e+00, α = 2.50e+01, m = 0, nfg = 4 +[ Info: LBFGS: iter 3, time 900.05 s: f = -0.115219717423, ‖∇f‖ = 2.7847e+00, α = 1.00e+00, m = 1, nfg = 1 +[ Info: LBFGS: iter 4, time 908.73 s: f = -0.616462986123, ‖∇f‖ = 2.3685e+00, α = 1.00e+00, m = 2, nfg = 1 +[ Info: LBFGS: iter 5, time 916.72 s: f = -0.817865359874, ‖∇f‖ = 1.9095e+00, α = 1.00e+00, m = 3, nfg = 1 +[ Info: LBFGS: iter 6, time 924.48 s: f = -0.990425015686, ‖∇f‖ = 2.3830e+00, α = 1.00e+00, m = 4, nfg = 1 +[ Info: LBFGS: iter 7, time 931.58 s: f = -1.142986439459, ‖∇f‖ = 1.5684e+00, α = 1.00e+00, m = 5, nfg = 1 +[ Info: LBFGS: iter 8, time 938.01 s: f = -1.239591181120, ‖∇f‖ = 3.4861e+00, α = 1.00e+00, m = 6, nfg = 1 +[ Info: LBFGS: iter 9, time 945.00 s: f = -1.438708542563, ‖∇f‖ = 1.3377e+00, α = 1.00e+00, m = 7, nfg = 1 +[ Info: LBFGS: iter 10, time 951.30 s: f = -1.524142766825, ‖∇f‖ = 1.3499e+00, α = 1.00e+00, m = 8, nfg = 1 +[ Info: LBFGS: iter 11, time 965.03 s: f = -1.620143211649, ‖∇f‖ = 1.1928e+00, α = 1.75e-01, m = 9, nfg = 2 +[ Info: LBFGS: iter 12, time 978.38 s: f = -1.682030774949, ‖∇f‖ = 9.4585e-01, α = 2.41e-01, m = 10, nfg = 2 +[ Info: LBFGS: iter 13, time 984.74 s: f = -1.722173660258, ‖∇f‖ = 1.3961e+00, α = 1.00e+00, m = 11, nfg = 1 +[ Info: LBFGS: iter 14, time 991.74 s: f = -1.771649839243, ‖∇f‖ = 6.2967e-01, α = 1.00e+00, m = 12, nfg = 1 +[ Info: LBFGS: iter 15, time 998.01 s: f = -1.809425620292, ‖∇f‖ = 5.1874e-01, α = 1.00e+00, m = 13, nfg = 1 +[ Info: LBFGS: iter 16, time 1004.35 s: f = -1.860257660187, ‖∇f‖ = 7.0707e-01, α = 1.00e+00, m = 14, nfg = 1 +[ Info: LBFGS: iter 17, time 1011.32 s: f = -1.894073433816, ‖∇f‖ = 6.7099e-01, α = 1.00e+00, m = 15, nfg = 1 +[ Info: LBFGS: iter 18, time 1017.63 s: f = -1.923565778264, ‖∇f‖ = 5.6311e-01, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 19, time 1024.66 s: f = -1.948747056517, ‖∇f‖ = 4.7890e-01, α = 1.00e+00, m = 17, nfg = 1 +[ Info: LBFGS: iter 20, time 1030.99 s: f = -1.969585552903, ‖∇f‖ = 4.1660e-01, α = 1.00e+00, m = 18, nfg = 1 +[ Info: LBFGS: iter 21, time 1037.41 s: f = -1.982637358938, ‖∇f‖ = 4.3422e-01, α = 1.00e+00, m = 19, nfg = 1 +[ Info: LBFGS: iter 22, time 1044.48 s: f = -1.993882710416, ‖∇f‖ = 3.1362e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 23, time 1050.89 s: f = -2.002938619798, ‖∇f‖ = 3.0798e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 24, time 1057.30 s: f = -2.014146064233, ‖∇f‖ = 3.3262e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 25, time 1064.50 s: f = -2.022239330954, ‖∇f‖ = 4.2937e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 26, time 1070.92 s: f = -2.030245493641, ‖∇f‖ = 2.0179e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 27, time 1078.05 s: f = -2.035169726141, ‖∇f‖ = 1.6346e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 28, time 1085.19 s: f = -2.038915730445, ‖∇f‖ = 1.6570e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 29, time 1091.75 s: f = -2.041961016975, ‖∇f‖ = 2.2790e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 30, time 1098.99 s: f = -2.045467456219, ‖∇f‖ = 1.0966e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 31, time 1105.48 s: f = -2.047243458561, ‖∇f‖ = 9.2405e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 32, time 1112.69 s: f = -2.049202803483, ‖∇f‖ = 1.2184e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 33, time 1119.08 s: f = -2.050191917638, ‖∇f‖ = 1.3044e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 34, time 1125.47 s: f = -2.050986114708, ‖∇f‖ = 5.9665e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 35, time 1132.64 s: f = -2.051548091457, ‖∇f‖ = 5.5253e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 36, time 1139.10 s: f = -2.051993308206, ‖∇f‖ = 6.2588e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 37, time 1146.24 s: f = -2.052324002624, ‖∇f‖ = 1.1928e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 38, time 1152.87 s: f = -2.052936230102, ‖∇f‖ = 4.9216e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 39, time 1159.10 s: f = -2.053164325823, ‖∇f‖ = 3.3410e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 40, time 1166.05 s: f = -2.053418129203, ‖∇f‖ = 3.7314e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 41, time 1172.35 s: f = -2.053649981748, ‖∇f‖ = 6.3612e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 42, time 1178.56 s: f = -2.053879953203, ‖∇f‖ = 3.4038e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 43, time 1185.49 s: f = -2.054050515673, ‖∇f‖ = 2.9152e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 44, time 1191.74 s: f = -2.054259903099, ‖∇f‖ = 3.9095e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 45, time 1198.69 s: f = -2.054388805929, ‖∇f‖ = 6.7475e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 46, time 1204.88 s: f = -2.054563154978, ‖∇f‖ = 3.0486e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 47, time 1211.31 s: f = -2.054666133101, ‖∇f‖ = 2.3929e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 48, time 1218.31 s: f = -2.054764670097, ‖∇f‖ = 2.9961e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 49, time 1224.59 s: f = -2.054936790198, ‖∇f‖ = 3.5407e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 50, time 1237.87 s: f = -2.055058405443, ‖∇f‖ = 5.1106e-02, α = 5.17e-01, m = 20, nfg = 2 +[ Info: LBFGS: iter 51, time 1244.16 s: f = -2.055253894176, ‖∇f‖ = 3.1080e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 52, time 1251.20 s: f = -2.055461219872, ‖∇f‖ = 2.9077e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 53, time 1257.57 s: f = -2.055733194309, ‖∇f‖ = 4.5784e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 54, time 1264.73 s: f = -2.055960164237, ‖∇f‖ = 7.1631e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 55, time 1271.03 s: f = -2.056334000687, ‖∇f‖ = 5.1447e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 56, time 1277.42 s: f = -2.056801416149, ‖∇f‖ = 4.8803e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 57, time 1284.42 s: f = -2.057222872354, ‖∇f‖ = 5.7077e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 58, time 1290.71 s: f = -2.057705132019, ‖∇f‖ = 8.5536e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 59, time 1297.71 s: f = -2.058233824137, ‖∇f‖ = 6.6099e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 60, time 1304.05 s: f = -2.058618411767, ‖∇f‖ = 8.2058e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 61, time 1310.35 s: f = -2.058860905381, ‖∇f‖ = 8.8034e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 62, time 1317.34 s: f = -2.059344181668, ‖∇f‖ = 6.7163e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 63, time 1323.93 s: f = -2.059884025175, ‖∇f‖ = 1.1005e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 64, time 1330.95 s: f = -2.060366638147, ‖∇f‖ = 7.3906e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 65, time 1337.42 s: f = -2.060748895891, ‖∇f‖ = 5.7350e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 66, time 1343.90 s: f = -2.061217694695, ‖∇f‖ = 1.0218e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 67, time 1351.08 s: f = -2.061747836243, ‖∇f‖ = 6.5473e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 68, time 1357.59 s: f = -2.061935488163, ‖∇f‖ = 7.7435e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 69, time 1364.81 s: f = -2.062292588164, ‖∇f‖ = 1.1031e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 70, time 1371.28 s: f = -2.062776748901, ‖∇f‖ = 8.7133e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 71, time 1377.76 s: f = -2.063311285039, ‖∇f‖ = 6.3871e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 72, time 1384.96 s: f = -2.063848732928, ‖∇f‖ = 7.8582e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 73, time 1391.44 s: f = -2.064428066762, ‖∇f‖ = 8.0994e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 74, time 1398.73 s: f = -2.064797991263, ‖∇f‖ = 2.1328e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 75, time 1405.20 s: f = -2.065223995463, ‖∇f‖ = 1.0827e-01, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 76, time 1411.72 s: f = -2.065622191643, ‖∇f‖ = 7.4192e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 77, time 1418.93 s: f = -2.066318151347, ‖∇f‖ = 9.4270e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 78, time 1433.31 s: f = -2.067468309902, ‖∇f‖ = 1.5613e-01, α = 4.14e-01, m = 20, nfg = 2 +[ Info: LBFGS: iter 79, time 1448.12 s: f = -2.068248367388, ‖∇f‖ = 1.9031e-01, α = 2.81e-01, m = 20, nfg = 2 +┌ Warning: LBFGS: not converged to requested tol after 80 iterations and time 1461.23 s: f = -2.069293119751, ‖∇f‖ = 3.6413e-01 +└ @ OptimKit ~/.julia/packages/OptimKit/G6i79/src/lbfgs.jl:197 +E = -2.0692931197508764 + +```` + +Finally, let's compare the obtained energy against a reference energy from a QMC study by +[Qin et al.](@cite qin_benchmark_2016). With the parameters specified above, they obtain an +energy of $E_\text{ref} \approx 4 \times -0.5244140625 = -2.09765625$ (the factor 4 comes +from the $2 \times 2$ unit cell that we use here). Thus, we find: + +````julia +E_ref = -2.09765625 +@show (E - E_ref) / E_ref; +```` + +```` +(E - E_ref) / E_ref = -0.013521343284498413 + +```` + +--- + +*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* + diff --git a/docs/src/examples/fermi_hubbard/main.ipynb b/docs/src/examples/fermi_hubbard/main.ipynb new file mode 100644 index 000000000..08d7a510f --- /dev/null +++ b/docs/src/examples/fermi_hubbard/main.ipynb @@ -0,0 +1,221 @@ +{ + "cells": [ + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Markdown #hide" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "# Fermi-Hubbard model with $f\\mathbb{Z}_2 \\boxtimes U(1)$ symmetry, at large $U$ and half-filling\n", + "\n", + "In this example, we will demonstrate how to handle fermionic PEPS tensors and how to\n", + "optimize them. To that end, we consider the two-dimensional Hubbard model\n", + "\n", + "$$\n", + "H = -t \\sum_{\\langle i,j \\rangle} \\sum_{\\sigma} \\left( c_{i,\\sigma}^+ c_{j,\\sigma}^- -\n", + "c_{i,\\sigma}^- c_{j,\\sigma}^+ \\right) + U \\sum_i n_{i,\\uparrow}n_{i,\\downarrow} - \\mu \\sum_i n_i\n", + "$$\n", + "\n", + "where $\\sigma \\in \\{\\uparrow,\\downarrow\\}$ and $n_{i,\\sigma} = c_{i,\\sigma}^+ c_{i,\\sigma}^-$\n", + "is the fermionic number operator. As in previous examples, using fermionic degrees of freedom\n", + "is a matter of creating tensors with the right symmetry sectors - the rest of the simulation\n", + "workflow remains the same.\n", + "\n", + "First though, we make the example deterministic by seeding the RNG, and we make our imports:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Random\n", + "using TensorKit, PEPSKit\n", + "using MPSKit: add_physical_charge\n", + "Random.seed!(2928528937);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Defining the fermionic Hamiltonian\n", + "\n", + "Let us start by fixing the parameters of the Hubbard model. We're going to use a hopping of\n", + "$t=1$ and a large $U=8$ on a $2 \\times 2$ unit cell:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "t = 1.0\n", + "U = 8.0\n", + "lattice = InfiniteSquare(2, 2);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "In order to create fermionic tensors, one needs to define symmetry sectors using TensorKit's\n", + "`FermionParity`. Not only do we want use fermion parity but we also want our\n", + "particles to exploit the global $U(1)$ symmetry. The combined product sector can be obtained\n", + "using the [Deligne product](https://jutho.github.io/TensorKit.jl/stable/lib/sectors/#TensorKitSectors.deligneproduct-Tuple{Sector,%20Sector}),\n", + "called through `⊠` which is obtained by typing `\\boxtimes+TAB`. We will not impose any extra\n", + "spin symmetry, so we have:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "fermion = fℤ₂\n", + "particle_symmetry = U1Irrep\n", + "spin_symmetry = Trivial\n", + "S = fermion ⊠ particle_symmetry" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "The next step is defining graded virtual PEPS and environment spaces using `S`. Here we also\n", + "use the symmetry sector to impose half-filling. That is all we need to define the Hubbard\n", + "Hamiltonian:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "D, χ = 1, 1\n", + "V_peps = Vect[S]((0, 0) => 2 * D, (1, 1) => D, (1, -1) => D)\n", + "V_env = Vect[S](\n", + " (0, 0) => 4 * χ, (1, -1) => 2 * χ, (1, 1) => 2 * χ, (0, 2) => χ, (0, -2) => χ\n", + ")\n", + "S_aux = S((1, -1))\n", + "H₀ = hubbard_model(ComplexF64, particle_symmetry, spin_symmetry, lattice; t, U)\n", + "H = add_physical_charge(H₀, fill(S_aux, size(H₀.lattice)...));" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Finding the ground state\n", + "\n", + "Again, the procedure of ground state optimization is very similar to before. First, we\n", + "define all algorithmic parameters:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "boundary_alg = (; tol=1e-8, alg=:simultaneous, trscheme=(; alg=:fixedspace))\n", + "gradient_alg = (; tol=1e-6, alg=:eigsolver, maxiter=10, iterscheme=:diffgauge)\n", + "optimizer_alg = (; tol=1e-4, alg=:lbfgs, maxiter=80, ls_maxiter=3, ls_maxfg=3)" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Second, we initialize a PEPS state and environment (which we converge) constructed from\n", + "symmetric physical and virtual spaces:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "physical_spaces = physicalspace(H)\n", + "virtual_spaces = fill(V_peps, size(lattice)...)\n", + "peps₀ = InfinitePEPS(randn, ComplexF64, physical_spaces, virtual_spaces)\n", + "env₀, = leading_boundary(CTMRGEnv(peps₀, V_env), peps₀; boundary_alg...);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "And third, we start the ground state search (this does take quite long):" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "peps, env, E, info = fixedpoint(\n", + " H, peps₀, env₀; boundary_alg, gradient_alg, optimizer_alg, verbosity=3\n", + ")\n", + "@show E;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Finally, let's compare the obtained energy against a reference energy from a QMC study by\n", + "[Qin et al.](@cite qin_benchmark_2016). With the parameters specified above, they obtain an\n", + "energy of $E_\\text{ref} \\approx 4 \\times -0.5244140625 = -2.09765625$ (the factor 4 comes\n", + "from the $2 \\times 2$ unit cell that we use here). Thus, we find:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "E_ref = -2.09765625\n", + "@show (E - E_ref) / E_ref;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "---\n", + "\n", + "*This notebook was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*" + ], + "metadata": {} + } + ], + "nbformat_minor": 3, + "metadata": { + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.11.4" + }, + "kernelspec": { + "name": "julia-1.11", + "display_name": "Julia 1.11.4", + "language": "julia" + } + }, + "nbformat": 4 +} diff --git a/docs/src/examples/heisenberg/index.md b/docs/src/examples/heisenberg/index.md new file mode 100644 index 000000000..62d19f236 --- /dev/null +++ b/docs/src/examples/heisenberg/index.md @@ -0,0 +1,414 @@ +```@meta +EditURL = "../../../../examples/heisenberg/main.jl" +``` + +[![](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/QuantumKitHub/PEPSKit.jl/gh-pages?filepath=dev/examples/heisenberg/main.ipynb) +[![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](https://nbviewer.jupyter.org/github/QuantumKitHub/PEPSKit.jl/blob/gh-pages/dev/examples/heisenberg/main.ipynb) +[![](https://img.shields.io/badge/download-project-orange)](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/QuantumKitHub/PEPSKit.jl/examples/tree/gh-pages/dev/examples/heisenberg) + + +# [Optimizing the 2D Heisenberg model](@id examples_heisenberg) + +In this example we want to provide a basic rundown of PEPSKit's optimization workflow for +PEPS. To that end, we will consider the two-dimensional Heisenberg model on a square lattice + +```math +H = \sum_{\langle i,j \rangle} \left ( J_x S^{x}_i S^{x}_j + J_y S^{y}_i S^{y}_j + J_z S^{z}_i S^{z}_j \right ) +``` + +Here, we want to set $J_x = J_y = J_z = 1$ where the Heisenberg model is in the antiferromagnetic +regime. Due to the bipartite sublattice structure of antiferromagnetic order one needs a +PEPS ansatz with a $2 \times 2$ unit cell. This can be circumvented by performing a unitary +sublattice rotation on all B-sites resulting in a change of parameters to +$(J_x, J_y, J_z)=(-1, 1, -1)$. This gives us a unitarily equivalent Hamiltonian (with the +same spectrum) with a ground state on a single-site unit cell. + +Let us get started by fixing the random seed of this example to make it deterministic: + +````julia +using Random +Random.seed!(123456789); +```` + +We're going to need only two packages: `TensorKit`, since we use that for all the underlying +tensor operations, and `PEPSKit` itself. So let us import these: + +````julia +using TensorKit, PEPSKit +```` + +## Defining the Heisenberg Hamiltonian + +To create the sublattice rotated Heisenberg Hamiltonian on an infinite square lattice, we use +the `heisenberg_XYZ` method from [MPSKitModels](https://quantumkithub.github.io/MPSKitModels.jl/dev/) +which is redefined for the `InfiniteSquare` and reexported in PEPSKit: + +````julia +H = heisenberg_XYZ(InfiniteSquare(); Jx=-1, Jy=1, Jz=-1) +```` + +```` +LocalOperator{Tuple{Pair{Tuple{CartesianIndex{2}, CartesianIndex{2}}, TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 2, 2, Vector{ComplexF64}}}, Pair{Tuple{CartesianIndex{2}, CartesianIndex{2}}, TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 2, 2, Vector{ComplexF64}}}}, TensorKit.ComplexSpace}(TensorKit.ComplexSpace[ℂ^2;;], ((CartesianIndex(1, 1), CartesianIndex(1, 2)) => TensorMap((ℂ^2 ⊗ ℂ^2) ← (ℂ^2 ⊗ ℂ^2)): +[:, :, 1, 1] = + -0.25 + 0.0im 0.0 + 0.0im + 0.0 + 0.0im -0.5 + 0.0im + +[:, :, 2, 1] = + 0.0 + 0.0im 0.0 + 0.0im + 0.25 + 0.0im 0.0 + 0.0im + +[:, :, 1, 2] = + 0.0 + 0.0im 0.25 + 0.0im + 0.0 + 0.0im 0.0 + 0.0im + +[:, :, 2, 2] = + -0.5 + 0.0im 0.0 + 0.0im + 0.0 + 0.0im -0.25 + 0.0im +, (CartesianIndex(1, 1), CartesianIndex(2, 1)) => TensorMap((ℂ^2 ⊗ ℂ^2) ← (ℂ^2 ⊗ ℂ^2)): +[:, :, 1, 1] = + -0.25 + 0.0im 0.0 + 0.0im + 0.0 + 0.0im -0.5 + 0.0im + +[:, :, 2, 1] = + 0.0 + 0.0im 0.0 + 0.0im + 0.25 + 0.0im 0.0 + 0.0im + +[:, :, 1, 2] = + 0.0 + 0.0im 0.25 + 0.0im + 0.0 + 0.0im 0.0 + 0.0im + +[:, :, 2, 2] = + -0.5 + 0.0im 0.0 + 0.0im + 0.0 + 0.0im -0.25 + 0.0im +)) +```` + +## Setting up the algorithms and initial guesses + +Next, we set the simulation parameters. During optimization, the PEPS will be contracted +using CTMRG and the PEPS gradient will be computed by differentiating through the CTMRG +routine using AD. Since the algorithmic stack that implements this is rather elaborate, +the amount of settings one can configure is also quite large. To reduce this complexity, +PEPSKit defaults to (presumably) reasonable settings which also dynamically adapts to the +user-specified parameters. + +First, we set the bond dimension `Dbond` of the virtual PEPS indices and the environment +dimension `χenv` of the virtual corner and transfer matrix indices. + +````julia +Dbond = 2 +χenv = 16; +```` + +To configure the CTMRG algorithm, we create a `NamedTuple` containing different keyword +arguments. To see a description of all arguments, see the docstring of +[`leading_boundary`](@ref). Here, we want to converge the CTMRG environments up to a +specific tolerance and during the CTMRG run keep all index dimensions fixed: + +````julia +boundary_alg = (; tol=1e-10, trscheme=(; alg=:fixedspace)); +```` + +Let us also configure the optimizer algorithm. We are going to optimize the PEPS using the +L-BFGS optimizer from [OptimKit](https://github.com/Jutho/OptimKit.jl). Again, we specify +the convergence tolerance (for the gradient norm) as well as the maximal number of iterations +and the BFGS memory size (which is used to approximate the Hessian): + +````julia +optimizer_alg = (; alg=:lbfgs, tol=1e-4, maxiter=100, lbfgs_memory=16); +```` + +Additionally, during optimization, we want to reuse the previous CTMRG environment to +initialize the CTMRG run of the current optimization step using the `reuse_env` argument. +And to control the output information, we set the `verbosity`: + +````julia +reuse_env = true +verbosity = 3; +```` + +Next, we initialize a random PEPS which will be used as an initial guess for the +optimization. To get a PEPS with physical dimension 2 (since we have a spin-1/2 Hamiltonian) +with complex-valued random Gaussian entries, we set: + +````julia +peps₀ = InfinitePEPS(randn, ComplexF64, 2, Dbond) +```` + +```` +InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}}(TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}[TensorMap(ℂ^2 ← (ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)')): +[:, :, 1, 1, 1] = + 0.07382174258286094 + 0.12820373667088403im 0.7897519397510839 + 0.9113654266438473im + 0.2553716885006697 - 0.4358399804354269im -1.0272416446076236 - 0.12635062198157215im + +[:, :, 2, 1, 1] = + 0.16833628450178303 - 0.10088950122180829im -0.9702030532300809 + 0.010730752411986726im + -1.6804460553576506 + 0.29081053879369084im 0.6844811667615024 + 0.09101537356941222im + +[:, :, 1, 2, 1] = + 0.5085938050744258 + 0.3786892551842583im 1.0020057959636561 - 1.4704891009758718im + -0.6153328223084331 + 0.10417896606055738im 0.6024931811537675 - 1.0348374874397468im + +[:, :, 2, 2, 1] = + -0.027201695938305456 + 0.5778042099380925im 0.09232089635078945 + 0.6143070126937361im + 1.0707115218777772 - 0.5747168579241235im -0.5819741818511422 - 0.9842624134267605im + +[:, :, 1, 1, 2] = + 1.2332543810053822 - 1.7783531996396438im 0.8887723728085348 + 0.7809798723615474im + 1.2251189302516847 - 0.6853683793073324im 1.5333834584675397 - 0.13856216581406375im + +[:, :, 2, 1, 2] = + 0.1406381347783769 + 0.6630243440357264im -0.7294596235434386 + 0.40327909254711103im + 0.7212056487788236 + 0.24320971945037498im 0.9991347929322827 + 0.0017902515981375842im + +[:, :, 1, 2, 2] = + 0.34282910982693904 - 0.4865238029567361im 0.9380949844871762 - 0.6985342237892025im + -0.7437083517319159 - 0.6895708849529253im -0.8981092940164176 + 0.9720706252141459im + +[:, :, 2, 2, 2] = + -0.8897079923413616 - 0.7145412189457411im 0.07771261045117502 - 0.6400190994609709im + -1.6099412157243007 + 0.8855200965611144im 0.7357380595021633 + 0.4626916850143416im +;;]) +```` + +The last thing we need before we can start the optimization is an initial CTMRG environment. +Typically, a random environment which we converge on `peps₀` serves as a good starting point. +To contract a PEPS starting from an environment using CTMRG, we call [`leading_boundary`](@ref): + +````julia +env_random = CTMRGEnv(randn, ComplexF64, peps₀, ℂ^χenv); +env₀, info_ctmrg = leading_boundary(env_random, peps₀; boundary_alg...); +```` + +```` +[ Info: CTMRG init: obj = -2.749614463601e+00 +3.639628057806e+00im err = 1.0000e+00 +[ Info: CTMRG conv 27: obj = +9.727103564786e+00 err = 2.6201184615e-11 time = 0.17 sec + +```` + +Besides the converged environment, `leading_boundary` also returns a `NamedTuple` of +informational quantities such as the last maximal truncation error - that is, the SVD +approximation error incurred in the last CTMRG iteration, maximized over all spatial +directions and unit cell entries: + +````julia +@show info_ctmrg.truncation_error; +```` + +```` +info_ctmrg.truncation_error = 0.0017032153529848298 + +```` + +## Ground state search + +Finally, we can start the optimization by calling [`fixedpoint`](@ref) on `H` with our +settings for the boundary (CTMRG) algorithm and the optimizer. This might take a while +(especially the precompilation of AD code in this case): + +````julia +peps, env, E, info_opt = fixedpoint( + H, peps₀, env₀; boundary_alg, optimizer_alg, reuse_env, verbosity +); +```` + +```` +[ Info: LBFGS: initializing with f = 0.000601645310, ‖∇f‖ = 9.3548e-01 +┌ Warning: The function `scale!!` is not implemented for (values of) type `Tuple{InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}}, Float64}`; +│ this fallback will disappear in future versions of VectorInterface.jl +└ @ VectorInterface ~/.julia/packages/VectorInterface/J6qCR/src/fallbacks.jl:91 +[ Info: LBFGS: iter 1, time 643.37 s: f = -0.489783740840, ‖∇f‖ = 6.0020e-01, α = 5.94e+01, m = 0, nfg = 5 +┌ Warning: The function `add!!` is not implemented for (values of) type `Tuple{InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}}, InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 4, Vector{ComplexF64}}}, Int64, VectorInterface.One}`; +│ this fallback will disappear in future versions of VectorInterface.jl +└ @ VectorInterface ~/.julia/packages/VectorInterface/J6qCR/src/fallbacks.jl:163 +[ Info: LBFGS: iter 2, time 645.02 s: f = -0.501971411096, ‖∇f‖ = 5.3738e-01, α = 2.80e-01, m = 1, nfg = 2 +[ Info: LBFGS: iter 3, time 645.36 s: f = -0.523152816264, ‖∇f‖ = 3.9922e-01, α = 1.00e+00, m = 2, nfg = 1 +[ Info: LBFGS: iter 4, time 646.10 s: f = -0.538652145758, ‖∇f‖ = 4.1551e-01, α = 2.29e-01, m = 3, nfg = 2 +[ Info: LBFGS: iter 5, time 648.50 s: f = -0.549861364689, ‖∇f‖ = 4.4015e-01, α = 6.94e-02, m = 4, nfg = 4 +[ Info: LBFGS: iter 6, time 649.35 s: f = -0.568951023367, ‖∇f‖ = 4.8339e-01, α = 2.24e-01, m = 5, nfg = 2 +[ Info: LBFGS: iter 7, time 649.72 s: f = -0.586980871663, ‖∇f‖ = 4.2463e-01, α = 1.00e+00, m = 6, nfg = 1 +[ Info: LBFGS: iter 8, time 650.06 s: f = -0.599970185661, ‖∇f‖ = 2.1955e-01, α = 1.00e+00, m = 7, nfg = 1 +[ Info: LBFGS: iter 9, time 650.38 s: f = -0.606725496115, ‖∇f‖ = 1.9384e-01, α = 1.00e+00, m = 8, nfg = 1 +[ Info: LBFGS: iter 10, time 650.70 s: f = -0.624986498009, ‖∇f‖ = 2.9776e-01, α = 1.00e+00, m = 9, nfg = 1 +[ Info: LBFGS: iter 11, time 651.00 s: f = -0.638747320059, ‖∇f‖ = 2.3382e-01, α = 1.00e+00, m = 10, nfg = 1 +[ Info: LBFGS: iter 12, time 651.32 s: f = -0.645577148853, ‖∇f‖ = 2.9937e-01, α = 1.00e+00, m = 11, nfg = 1 +[ Info: LBFGS: iter 13, time 651.60 s: f = -0.650891062410, ‖∇f‖ = 1.4746e-01, α = 1.00e+00, m = 12, nfg = 1 +[ Info: LBFGS: iter 14, time 651.90 s: f = -0.654569099868, ‖∇f‖ = 7.0690e-02, α = 1.00e+00, m = 13, nfg = 1 +[ Info: LBFGS: iter 15, time 652.20 s: f = -0.655949603239, ‖∇f‖ = 5.0977e-02, α = 1.00e+00, m = 14, nfg = 1 +[ Info: LBFGS: iter 16, time 652.51 s: f = -0.657146001976, ‖∇f‖ = 5.8056e-02, α = 1.00e+00, m = 15, nfg = 1 +[ Info: LBFGS: iter 17, time 652.83 s: f = -0.658558478454, ‖∇f‖ = 5.0388e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 18, time 653.13 s: f = -0.659302065828, ‖∇f‖ = 4.0776e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 19, time 653.42 s: f = -0.659633838354, ‖∇f‖ = 2.2380e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 20, time 653.72 s: f = -0.659776177694, ‖∇f‖ = 2.1511e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 21, time 654.01 s: f = -0.659916031911, ‖∇f‖ = 2.0498e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 22, time 654.30 s: f = -0.660181523751, ‖∇f‖ = 1.7235e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 23, time 654.62 s: f = -0.660350536401, ‖∇f‖ = 1.8928e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 24, time 654.91 s: f = -0.660447076769, ‖∇f‖ = 1.0330e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 25, time 655.21 s: f = -0.660521574522, ‖∇f‖ = 1.0448e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 26, time 655.52 s: f = -0.660656071716, ‖∇f‖ = 1.8768e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 27, time 655.85 s: f = -0.660756412995, ‖∇f‖ = 3.2183e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 28, time 656.18 s: f = -0.660925447420, ‖∇f‖ = 1.3371e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 29, time 656.60 s: f = -0.661000634324, ‖∇f‖ = 9.8866e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 30, time 657.36 s: f = -0.661046316490, ‖∇f‖ = 9.1513e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 31, time 657.70 s: f = -0.661128304094, ‖∇f‖ = 9.6895e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 32, time 658.08 s: f = -0.661169144566, ‖∇f‖ = 1.3492e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 33, time 658.37 s: f = -0.661204525845, ‖∇f‖ = 9.6996e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 34, time 658.65 s: f = -0.661224003573, ‖∇f‖ = 6.2892e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 35, time 658.94 s: f = -0.661247137140, ‖∇f‖ = 4.4514e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 36, time 659.24 s: f = -0.661266456453, ‖∇f‖ = 5.3015e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 37, time 659.53 s: f = -0.661280686254, ‖∇f‖ = 9.2298e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 38, time 659.83 s: f = -0.661298851672, ‖∇f‖ = 5.9013e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 39, time 660.12 s: f = -0.661320547122, ‖∇f‖ = 6.2443e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 40, time 660.41 s: f = -0.661344887326, ‖∇f‖ = 9.9129e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 41, time 660.71 s: f = -0.661398950542, ‖∇f‖ = 1.6285e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 42, time 661.03 s: f = -0.661483277766, ‖∇f‖ = 1.6233e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 43, time 661.34 s: f = -0.661583013010, ‖∇f‖ = 2.8186e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 44, time 661.66 s: f = -0.661670888522, ‖∇f‖ = 3.9725e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 45, time 661.98 s: f = -0.661865434012, ‖∇f‖ = 1.3200e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 46, time 662.29 s: f = -0.661977354471, ‖∇f‖ = 1.5881e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 47, time 662.60 s: f = -0.662102076782, ‖∇f‖ = 2.0290e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 48, time 663.24 s: f = -0.662190125548, ‖∇f‖ = 2.2873e-02, α = 4.61e-01, m = 16, nfg = 2 +[ Info: LBFGS: iter 49, time 663.55 s: f = -0.662306892721, ‖∇f‖ = 1.3813e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 50, time 663.87 s: f = -0.662376465537, ‖∇f‖ = 1.9902e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 51, time 664.17 s: f = -0.662419493776, ‖∇f‖ = 1.2249e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 52, time 664.48 s: f = -0.662439251412, ‖∇f‖ = 7.3806e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 53, time 664.79 s: f = -0.662463629284, ‖∇f‖ = 5.1806e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 54, time 665.10 s: f = -0.662484473404, ‖∇f‖ = 4.6461e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 55, time 665.71 s: f = -0.662490501784, ‖∇f‖ = 6.3694e-03, α = 4.07e-01, m = 16, nfg = 2 +[ Info: LBFGS: iter 56, time 666.01 s: f = -0.662497687998, ‖∇f‖ = 2.9285e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 57, time 666.32 s: f = -0.662500949854, ‖∇f‖ = 2.1234e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 58, time 666.62 s: f = -0.662503723196, ‖∇f‖ = 4.1203e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 59, time 666.93 s: f = -0.662505780051, ‖∇f‖ = 3.0872e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 60, time 667.22 s: f = -0.662507116565, ‖∇f‖ = 1.9618e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 61, time 667.52 s: f = -0.662509290310, ‖∇f‖ = 1.5747e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 62, time 667.82 s: f = -0.662510568937, ‖∇f‖ = 1.3099e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 63, time 668.65 s: f = -0.662511109974, ‖∇f‖ = 2.7217e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 64, time 668.96 s: f = -0.662511878793, ‖∇f‖ = 1.0320e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 65, time 669.34 s: f = -0.662512042147, ‖∇f‖ = 5.9753e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 66, time 669.65 s: f = -0.662512275118, ‖∇f‖ = 6.6602e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 67, time 669.93 s: f = -0.662512678161, ‖∇f‖ = 9.0498e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 68, time 670.21 s: f = -0.662513114911, ‖∇f‖ = 1.8006e-03, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 69, time 670.51 s: f = -0.662513454844, ‖∇f‖ = 9.5988e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 70, time 670.80 s: f = -0.662513639773, ‖∇f‖ = 5.2576e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 71, time 671.09 s: f = -0.662513713403, ‖∇f‖ = 4.0696e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 72, time 671.39 s: f = -0.662513818843, ‖∇f‖ = 4.8084e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 73, time 671.70 s: f = -0.662513978848, ‖∇f‖ = 6.8463e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 74, time 672.29 s: f = -0.662514066816, ‖∇f‖ = 5.2125e-04, α = 5.38e-01, m = 16, nfg = 2 +[ Info: LBFGS: iter 75, time 672.57 s: f = -0.662514122809, ‖∇f‖ = 3.2924e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 76, time 672.86 s: f = -0.662514184291, ‖∇f‖ = 2.7038e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 77, time 673.17 s: f = -0.662514214654, ‖∇f‖ = 4.6682e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 78, time 673.46 s: f = -0.662514242510, ‖∇f‖ = 2.7698e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 79, time 673.74 s: f = -0.662514253309, ‖∇f‖ = 1.6244e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 80, time 674.02 s: f = -0.662514263613, ‖∇f‖ = 1.2004e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 81, time 674.30 s: f = -0.662514271751, ‖∇f‖ = 1.4760e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 82, time 674.58 s: f = -0.662514281056, ‖∇f‖ = 1.6558e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 83, time 674.87 s: f = -0.662514283704, ‖∇f‖ = 2.1824e-04, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: converged after 84 iterations and time 675.15 s: f = -0.662514288424, ‖∇f‖ = 5.9513e-05 + +```` + +Note that `fixedpoint` returns the final optimized PEPS, the last converged environment, +the final energy estimate as well as a `NamedTuple` of diagnostics. This allows us to, e.g., +analyze the number of cost function calls or the history of gradient norms to evaluate +the convergence rate: + +````julia +@show info_opt.fg_evaluations info_opt.gradnorms[1:10:end]; +```` + +```` +info_opt.fg_evaluations = 98 +info_opt.gradnorms[1:10:end] = [0.9354758925982428, 0.2977564979129917, 0.021510752143210195, 0.009151302712640632, 0.009912940904896769, 0.019901533415930574, 0.0019617677308353, 0.0005257609002981095, 0.00012003941670625574] + +```` + +Let's now compare the optimized energy against an accurate Quantum Monte Carlo estimate by +[Sandvik](@cite sandvik_computational_2011), where the energy per site was found to be +$E_{\text{ref}}=−0.6694421$. From our simple optimization we find: + +````julia +@show E; +```` + +```` +E = -0.6625142884244373 + +```` + +While this energy is in the right ballpark, there is still quite some deviation from the +accurate reference energy. This, however, can be attributed to the small bond dimension - an +optimization with larger bond dimension would approach this value much more closely. + +A more reasonable comparison would be against another finite bond dimension PEPS simulation. +For example, Juraj Hasik's data from $J_1\text{-}J_2$ +[PEPS simulations](https://github.com/jurajHasik/j1j2_ipeps_states/blob/main/single-site_pg-C4v-A1/j20.0/state_1s_A1_j20.0_D2_chi_opt48.dat) +yields $E_{D=2,\chi=16}=-0.660231\dots$ which is more in line with what we find here. + +## Compute the correlation lengths and transfer matrix spectra + +In practice, in order to obtain an accurate and variational energy estimate, one would need +to compute multiple energies at different environment dimensions and extrapolate in, e.g., +the correlation length or the second gap of the transfer matrix spectrum. For that, we would +need the [`correlation_length`](@ref) function, which computes the horizontal and vertical +correlation lengths and transfer matrix spectra for all unit cell coordinates: + +````julia +ξ_h, ξ_v, λ_h, λ_v = correlation_length(peps, env) +@show ξ_h ξ_v; +```` + +```` +ξ_h = [1.034117934253177] +ξ_v = [1.0240816290840877] + +```` + +## Computing observables + +As a last thing, we want to see how we can compute expectation values of observables, given +the optimized PEPS and its CTMRG environment. To compute, e.g., the magnetization, we first +need to define the observable as a `TensorMap`: + +````julia +σ_z = TensorMap([1.0 0.0; 0.0 -1.0], ℂ^2, ℂ^2) +```` + +```` +TensorMap(ℂ^2 ← ℂ^2): + 1.0 0.0 + 0.0 -1.0 + +```` + +In order to be able to contract it with the PEPS and environment, we define need to define a +`LocalOperator` and specify on which physical spaces and sites the observable acts. That way, +the PEPS-environment-operator contraction gets automatically generated (also works for +multi-site operators!). See the [`LocalOperator`](@ref) docstring for more details. +The magnetization is just a single-site observable, so we have: + +````julia +M = LocalOperator(fill(ℂ^2, 1, 1), (CartesianIndex(1, 1),) => σ_z) +```` + +```` +LocalOperator{Tuple{Pair{Tuple{CartesianIndex{2}}, TensorKit.TensorMap{Float64, TensorKit.ComplexSpace, 1, 1, Vector{Float64}}}}, TensorKit.ComplexSpace}(TensorKit.ComplexSpace[ℂ^2;;], ((CartesianIndex(1, 1),) => TensorMap(ℂ^2 ← ℂ^2): + 1.0 0.0 + 0.0 -1.0 +,)) +```` + +Finally, to evaluate the expecation value on the `LocalOperator`, we call: + +````julia +@show expectation_value(peps, M, env); +```` + +```` +expectation_value(peps, M, env) = -0.7554296202981441 + 2.541461125039123e-16im + +```` + +--- + +*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* + diff --git a/docs/src/examples/heisenberg/main.ipynb b/docs/src/examples/heisenberg/main.ipynb new file mode 100644 index 000000000..4e0c08458 --- /dev/null +++ b/docs/src/examples/heisenberg/main.ipynb @@ -0,0 +1,394 @@ +{ + "cells": [ + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Markdown #hide" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "# Optimizing the 2D Heisenberg model\n", + "\n", + "In this example we want to provide a basic rundown of PEPSKit's optimization workflow for\n", + "PEPS. To that end, we will consider the two-dimensional Heisenberg model on a square lattice\n", + "\n", + "$$\n", + "H = \\sum_{\\langle i,j \\rangle} \\left ( J_x S^{x}_i S^{x}_j + J_y S^{y}_i S^{y}_j + J_z S^{z}_i S^{z}_j \\right )\n", + "$$\n", + "\n", + "Here, we want to set $J_x = J_y = J_z = 1$ where the Heisenberg model is in the antiferromagnetic\n", + "regime. Due to the bipartite sublattice structure of antiferromagnetic order one needs a\n", + "PEPS ansatz with a $2 \\times 2$ unit cell. This can be circumvented by performing a unitary\n", + "sublattice rotation on all B-sites resulting in a change of parameters to\n", + "$(J_x, J_y, J_z)=(-1, 1, -1)$. This gives us a unitarily equivalent Hamiltonian (with the\n", + "same spectrum) with a ground state on a single-site unit cell.\n", + "\n", + "Let us get started by fixing the random seed of this example to make it deterministic:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Random\n", + "Random.seed!(123456789);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "We're going to need only two packages: `TensorKit`, since we use that for all the underlying\n", + "tensor operations, and `PEPSKit` itself. So let us import these:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "using TensorKit, PEPSKit" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Defining the Heisenberg Hamiltonian\n", + "\n", + "To create the sublattice rotated Heisenberg Hamiltonian on an infinite square lattice, we use\n", + "the `heisenberg_XYZ` method from [MPSKitModels](https://quantumkithub.github.io/MPSKitModels.jl/dev/)\n", + "which is redefined for the `InfiniteSquare` and reexported in PEPSKit:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "H = heisenberg_XYZ(InfiniteSquare(); Jx=-1, Jy=1, Jz=-1)" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Setting up the algorithms and initial guesses\n", + "\n", + "Next, we set the simulation parameters. During optimization, the PEPS will be contracted\n", + "using CTMRG and the PEPS gradient will be computed by differentiating through the CTMRG\n", + "routine using AD. Since the algorithmic stack that implements this is rather elaborate,\n", + "the amount of settings one can configure is also quite large. To reduce this complexity,\n", + "PEPSKit defaults to (presumably) reasonable settings which also dynamically adapts to the\n", + "user-specified parameters.\n", + "\n", + "First, we set the bond dimension `Dbond` of the virtual PEPS indices and the environment\n", + "dimension `χenv` of the virtual corner and transfer matrix indices." + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "Dbond = 2\n", + "χenv = 16;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "To configure the CTMRG algorithm, we create a `NamedTuple` containing different keyword\n", + "arguments. To see a description of all arguments, see the docstring of\n", + "`leading_boundary`. Here, we want to converge the CTMRG environments up to a\n", + "specific tolerance and during the CTMRG run keep all index dimensions fixed:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "boundary_alg = (; tol=1e-10, trscheme=(; alg=:fixedspace));" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Let us also configure the optimizer algorithm. We are going to optimize the PEPS using the\n", + "L-BFGS optimizer from [OptimKit](https://github.com/Jutho/OptimKit.jl). Again, we specify\n", + "the convergence tolerance (for the gradient norm) as well as the maximal number of iterations\n", + "and the BFGS memory size (which is used to approximate the Hessian):" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "optimizer_alg = (; alg=:lbfgs, tol=1e-4, maxiter=100, lbfgs_memory=16);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Additionally, during optimization, we want to reuse the previous CTMRG environment to\n", + "initialize the CTMRG run of the current optimization step using the `reuse_env` argument.\n", + "And to control the output information, we set the `verbosity`:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "reuse_env = true\n", + "verbosity = 3;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Next, we initialize a random PEPS which will be used as an initial guess for the\n", + "optimization. To get a PEPS with physical dimension 2 (since we have a spin-1/2 Hamiltonian)\n", + "with complex-valued random Gaussian entries, we set:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "peps₀ = InfinitePEPS(randn, ComplexF64, 2, Dbond)" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "The last thing we need before we can start the optimization is an initial CTMRG environment.\n", + "Typically, a random environment which we converge on `peps₀` serves as a good starting point.\n", + "To contract a PEPS starting from an environment using CTMRG, we call `leading_boundary`:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "env_random = CTMRGEnv(randn, ComplexF64, peps₀, ℂ^χenv);\n", + "env₀, info_ctmrg = leading_boundary(env_random, peps₀; boundary_alg...);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Besides the converged environment, `leading_boundary` also returns a `NamedTuple` of\n", + "informational quantities such as the last maximal truncation error - that is, the SVD\n", + "approximation error incurred in the last CTMRG iteration, maximized over all spatial\n", + "directions and unit cell entries:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "@show info_ctmrg.truncation_error;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Ground state search\n", + "\n", + "Finally, we can start the optimization by calling `fixedpoint` on `H` with our\n", + "settings for the boundary (CTMRG) algorithm and the optimizer. This might take a while\n", + "(especially the precompilation of AD code in this case):" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "peps, env, E, info_opt = fixedpoint(\n", + " H, peps₀, env₀; boundary_alg, optimizer_alg, reuse_env, verbosity\n", + ");" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Note that `fixedpoint` returns the final optimized PEPS, the last converged environment,\n", + "the final energy estimate as well as a `NamedTuple` of diagnostics. This allows us to, e.g.,\n", + "analyze the number of cost function calls or the history of gradient norms to evaluate\n", + "the convergence rate:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "@show info_opt.fg_evaluations info_opt.gradnorms[1:10:end];" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Let's now compare the optimized energy against an accurate Quantum Monte Carlo estimate by\n", + "[Sandvik](@cite sandvik_computational_2011), where the energy per site was found to be\n", + "$E_{\\text{ref}}=−0.6694421$. From our simple optimization we find:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "@show E;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "While this energy is in the right ballpark, there is still quite some deviation from the\n", + "accurate reference energy. This, however, can be attributed to the small bond dimension - an\n", + "optimization with larger bond dimension would approach this value much more closely.\n", + "\n", + "A more reasonable comparison would be against another finite bond dimension PEPS simulation.\n", + "For example, Juraj Hasik's data from $J_1\\text{-}J_2$\n", + "[PEPS simulations](https://github.com/jurajHasik/j1j2_ipeps_states/blob/main/single-site_pg-C4v-A1/j20.0/state_1s_A1_j20.0_D2_chi_opt48.dat)\n", + "yields $E_{D=2,\\chi=16}=-0.660231\\dots$ which is more in line with what we find here.\n", + "\n", + "## Compute the correlation lengths and transfer matrix spectra\n", + "\n", + "In practice, in order to obtain an accurate and variational energy estimate, one would need\n", + "to compute multiple energies at different environment dimensions and extrapolate in, e.g.,\n", + "the correlation length or the second gap of the transfer matrix spectrum. For that, we would\n", + "need the `correlation_length` function, which computes the horizontal and vertical\n", + "correlation lengths and transfer matrix spectra for all unit cell coordinates:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "ξ_h, ξ_v, λ_h, λ_v = correlation_length(peps, env)\n", + "@show ξ_h ξ_v;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Computing observables\n", + "\n", + "As a last thing, we want to see how we can compute expectation values of observables, given\n", + "the optimized PEPS and its CTMRG environment. To compute, e.g., the magnetization, we first\n", + "need to define the observable as a `TensorMap`:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "σ_z = TensorMap([1.0 0.0; 0.0 -1.0], ℂ^2, ℂ^2)" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "In order to be able to contract it with the PEPS and environment, we define need to define a\n", + "`LocalOperator` and specify on which physical spaces and sites the observable acts. That way,\n", + "the PEPS-environment-operator contraction gets automatically generated (also works for\n", + "multi-site operators!). See the `LocalOperator` docstring for more details.\n", + "The magnetization is just a single-site observable, so we have:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "M = LocalOperator(fill(ℂ^2, 1, 1), (CartesianIndex(1, 1),) => σ_z)" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Finally, to evaluate the expecation value on the `LocalOperator`, we call:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "@show expectation_value(peps, M, env);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "---\n", + "\n", + "*This notebook was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*" + ], + "metadata": {} + } + ], + "nbformat_minor": 3, + "metadata": { + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.11.4" + }, + "kernelspec": { + "name": "julia-1.11", + "display_name": "Julia 1.11.4", + "language": "julia" + } + }, + "nbformat": 4 +} diff --git a/docs/src/examples/heisenberg_su/index.md b/docs/src/examples/heisenberg_su/index.md new file mode 100644 index 000000000..935439790 --- /dev/null +++ b/docs/src/examples/heisenberg_su/index.md @@ -0,0 +1,206 @@ +```@meta +EditURL = "../../../../examples/heisenberg_su/main.jl" +``` + +[![](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/QuantumKitHub/PEPSKit.jl/gh-pages?filepath=dev/examples/heisenberg_su/main.ipynb) +[![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](https://nbviewer.jupyter.org/github/QuantumKitHub/PEPSKit.jl/blob/gh-pages/dev/examples/heisenberg_su/main.ipynb) +[![](https://img.shields.io/badge/download-project-orange)](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/QuantumKitHub/PEPSKit.jl/examples/tree/gh-pages/dev/examples/heisenberg_su) + + +# Simple update for the Heisenberg model + +In this example, we will use [`SimpleUpdate`](@ref) imaginary time evolution to treat +the two-dimensional Heisenberg model once again: + +```math +H = \sum_{\langle i,j \rangle} J_x S^{x}_i S^{x}_j + J_y S^{y}_i S^{y}_j + J_z S^{z}_i S^{z}_j. +``` + +In order to simulate the antiferromagnetic order of the Hamiltonian on a single-site unit +cell one typically applies a unitary sublattice rotation. Here, we will instead use a +$2 \times 2$ unit cell and set $J_x = J_y = J_z = 1$. + +Let's get started by seeding the RNG and importing all required modules: + +````julia +using Random +import Statistics: mean +using TensorKit, PEPSKit +import MPSKitModels: S_x, S_y, S_z, S_exchange +Random.seed!(0); +```` + +## Defining the Hamiltonian + +To construct the Heisenberg Hamiltonian as just discussed, we'll use `heisenberg_XYZ` and, +in addition, make it real (`real` and `imag` works for `LocalOperator`s) since we want to +use PEPS and environments with real entries. We can either initialize the Hamiltonian with +no internal symmetries (`symm = Trivial`) or use the global $U(1)$ symmetry +(`symm = U1Irrep`): + +````julia +symm = Trivial ## ∈ {Trivial, U1Irrep} +Nr, Nc = 2, 2 +H = real(heisenberg_XYZ(ComplexF64, symm, InfiniteSquare(Nr, Nc); Jx=1, Jy=1, Jz=1)); +```` + +## Simple updating + +We proceed by initializing a random weighted PEPS that will be evolved. First though, we +need to define the appropriate (symmetric) spaces: + +````julia +Dbond = 4 +χenv = 16 +if symm == Trivial + physical_space = ℂ^2 + bond_space = ℂ^Dbond + env_space = ℂ^χenv +elseif symm == U1Irrep + physical_space = ℂ[U1Irrep](1//2 => 1, -1//2 => 1) + bond_space = ℂ[U1Irrep](0 => Dbond ÷ 2, 1//2 => Dbond ÷ 4, -1//2 => Dbond ÷ 4) + env_space = ℂ[U1Irrep](0 => χenv ÷ 2, 1//2 => χenv ÷ 4, -1//2 => χenv ÷ 4) +else + error("not implemented") +end + +wpeps = InfiniteWeightPEPS(rand, Float64, physical_space, bond_space; unitcell=(Nr, Nc)); +```` + +Next, we can start the `SimpleUpdate` routine, successively decreasing the time intervals +and singular value convergence tolerances. Note that TensorKit allows to combine SVD +truncation schemes, which we use here to set a maximal bond dimension and at the same time +fix a truncation error (if that can be reached by remaining below `Dbond`): + +````julia +dts = [1e-2, 1e-3, 4e-4] +tols = [1e-6, 1e-8, 1e-8] +maxiter = 10000 +trscheme_peps = truncerr(1e-10) & truncdim(Dbond) + +for (dt, tol) in zip(dts, tols) + alg = SimpleUpdate(dt, tol, maxiter, trscheme_peps) + result = simpleupdate(wpeps, H, alg; bipartite=true) + global wpeps = result[1] +end +```` + +```` +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU iter 1 : dt = 1e-02, weight diff = 1.683e+00, time = 10.602 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU iter 500 : dt = 1e-02, weight diff = 3.879e-06, time = 0.002 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU conv 596 : dt = 1e-02, weight diff = 9.933e-07, time = 12.073 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU iter 1 : dt = 1e-03, weight diff = 2.135e-03, time = 0.002 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU iter 500 : dt = 1e-03, weight diff = 9.632e-07, time = 0.002 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU iter 1000 : dt = 1e-03, weight diff = 2.415e-07, time = 0.002 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU iter 1500 : dt = 1e-03, weight diff = 6.291e-08, time = 0.002 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU iter 2000 : dt = 1e-03, weight diff = 1.683e-08, time = 0.002 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU conv 2205 : dt = 1e-03, weight diff = 9.978e-09, time = 4.545 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU iter 1 : dt = 4e-04, weight diff = 1.418e-04, time = 0.002 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU iter 500 : dt = 4e-04, weight diff = 6.377e-08, time = 0.002 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU iter 1000 : dt = 4e-04, weight diff = 3.544e-08, time = 0.002 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU iter 1500 : dt = 4e-04, weight diff = 2.013e-08, time = 0.002 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU iter 2000 : dt = 4e-04, weight diff = 1.157e-08, time = 0.002 sec +[ Info: Space of x-weight at [1, 1] = ℂ^4 +[ Info: SU conv 2133 : dt = 4e-04, weight diff = 9.999e-09, time = 4.379 sec + +```` + +## Computing the ground-state energy and magnetizations + +In order to compute observable expectation values, we need to converge a CTMRG environment +on the evolved PEPS. Let's do so: + +````julia +peps = InfinitePEPS(wpeps) ## absorb the weights +env₀ = CTMRGEnv(rand, Float64, peps, env_space) +trscheme_env = truncerr(1e-10) & truncdim(χenv) +env, = leading_boundary( + env₀, + peps; + alg=:sequential, + projector_alg=:fullinfinite, + tol=1e-10, + trscheme=trscheme_env, +); +```` + +```` +[ Info: CTMRG init: obj = +8.705922473439e-05 err = 1.0000e+00 +[ Info: CTMRG conv 15: obj = +9.514115680898e-01 err = 6.0310868148e-11 time = 9.35 sec + +```` + +Finally, we'll measure the energy and different magnetizations. For the magnetizations, +the plan is to compute the expectation values unit cell entry-wise in different spin +directions: + +````julia +function compute_mags(peps::InfinitePEPS, env::CTMRGEnv) + lattice = collect(space(t, 1) for t in peps.A) + + # detect symmetry on physical axis + symm = sectortype(space(peps.A[1, 1])) + if symm == Trivial + S_ops = real.([S_x(symm), im * S_y(symm), S_z(symm)]) + elseif symm == U1Irrep + S_ops = real.([S_z(symm)]) ## only Sz preserves + end + + return map(Iterators.product(axes(peps, 1), axes(peps, 2), S_ops)) do (r, c, S) + expectation_value(peps, LocalOperator(lattice, (CartesianIndex(r, c),) => S), env) + end +end + +E = expectation_value(peps, H, env) / (Nr * Nc) +Ms = compute_mags(peps, env) +M_norms = map( + rc -> norm(Ms[rc[1], rc[2], :]), Iterators.product(axes(peps, 1), axes(peps, 2)) +) +@show E Ms M_norms; +```` + +```` +E = -0.6674685583160895 +Ms = [0.03199644951247372 -0.02980262049564095; -0.029802620502662757 0.03199644954619281;;; 2.2896091139871975e-12 -1.0506898420176606e-12; -2.118251543953617e-12 8.853279136399547e-13;;; 0.37559610906659674 -0.3757765476186203; -0.3757765476169772 0.37559610906659097] +M_norms = [0.37695650933147595 0.3769565093330746; 0.3769565093319919 0.3769565093343323] + +```` + +To assess the results, we will benchmark against data from [Corboz](@cite corboz_variational_2016), +which use manual gradients to perform a variational optimization of the Heisenberg model. +In particular, for the energy and magnetization they find $E_\text{ref} = -0.6675$ and +$M_\text{ref} = 0.3767$. Looking at the relative errors, we find general agreement, although +the accuracy is limited by the methodological limitations of the simple update algorithm as +well as finite bond dimension effects and a lacking extrapolation: + +````julia +E_ref = -0.6675 +M_ref = 0.3767 +@show (E - E_ref) / E_ref +@show (mean(M_norms) - M_ref) / E_ref; +```` + +```` +(E - E_ref) / E_ref = -4.7103646307789086e-5 +(mean(M_norms) - M_ref) / E_ref = -0.0003842836445223997 + +```` + +--- + +*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* + diff --git a/docs/src/examples/heisenberg_su/main.ipynb b/docs/src/examples/heisenberg_su/main.ipynb new file mode 100644 index 000000000..1c86122e1 --- /dev/null +++ b/docs/src/examples/heisenberg_su/main.ipynb @@ -0,0 +1,247 @@ +{ + "cells": [ + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Markdown #hide" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "# Simple update for the Heisenberg model\n", + "\n", + "In this example, we will use `SimpleUpdate` imaginary time evolution to treat\n", + "the two-dimensional Heisenberg model once again:\n", + "\n", + "$$\n", + "H = \\sum_{\\langle i,j \\rangle} J_x S^{x}_i S^{x}_j + J_y S^{y}_i S^{y}_j + J_z S^{z}_i S^{z}_j.\n", + "$$\n", + "\n", + "In order to simulate the antiferromagnetic order of the Hamiltonian on a single-site unit\n", + "cell one typically applies a unitary sublattice rotation. Here, we will instead use a\n", + "$2 \\times 2$ unit cell and set $J_x = J_y = J_z = 1$.\n", + "\n", + "Let's get started by seeding the RNG and importing all required modules:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Random\n", + "import Statistics: mean\n", + "using TensorKit, PEPSKit\n", + "import MPSKitModels: S_x, S_y, S_z, S_exchange\n", + "Random.seed!(0);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Defining the Hamiltonian\n", + "\n", + "To construct the Heisenberg Hamiltonian as just discussed, we'll use `heisenberg_XYZ` and,\n", + "in addition, make it real (`real` and `imag` works for `LocalOperator`s) since we want to\n", + "use PEPS and environments with real entries. We can either initialize the Hamiltonian with\n", + "no internal symmetries (`symm = Trivial`) or use the global $U(1)$ symmetry\n", + "(`symm = U1Irrep`):" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "symm = Trivial ## ∈ {Trivial, U1Irrep}\n", + "Nr, Nc = 2, 2\n", + "H = real(heisenberg_XYZ(ComplexF64, symm, InfiniteSquare(Nr, Nc); Jx=1, Jy=1, Jz=1));" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Simple updating\n", + "\n", + "We proceed by initializing a random weighted PEPS that will be evolved. First though, we\n", + "need to define the appropriate (symmetric) spaces:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "Dbond = 4\n", + "χenv = 16\n", + "if symm == Trivial\n", + " physical_space = ℂ^2\n", + " bond_space = ℂ^Dbond\n", + " env_space = ℂ^χenv\n", + "elseif symm == U1Irrep\n", + " physical_space = ℂ[U1Irrep](1//2 => 1, -1//2 => 1)\n", + " bond_space = ℂ[U1Irrep](0 => Dbond ÷ 2, 1//2 => Dbond ÷ 4, -1//2 => Dbond ÷ 4)\n", + " env_space = ℂ[U1Irrep](0 => χenv ÷ 2, 1//2 => χenv ÷ 4, -1//2 => χenv ÷ 4)\n", + "else\n", + " error(\"not implemented\")\n", + "end\n", + "\n", + "wpeps = InfiniteWeightPEPS(rand, Float64, physical_space, bond_space; unitcell=(Nr, Nc));" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Next, we can start the `SimpleUpdate` routine, successively decreasing the time intervals\n", + "and singular value convergence tolerances. Note that TensorKit allows to combine SVD\n", + "truncation schemes, which we use here to set a maximal bond dimension and at the same time\n", + "fix a truncation error (if that can be reached by remaining below `Dbond`):" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "dts = [1e-2, 1e-3, 4e-4]\n", + "tols = [1e-6, 1e-8, 1e-8]\n", + "maxiter = 10000\n", + "trscheme_peps = truncerr(1e-10) & truncdim(Dbond)\n", + "\n", + "for (dt, tol) in zip(dts, tols)\n", + " alg = SimpleUpdate(dt, tol, maxiter, trscheme_peps)\n", + " result = simpleupdate(wpeps, H, alg; bipartite=true)\n", + " global wpeps = result[1]\n", + "end" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Computing the ground-state energy and magnetizations\n", + "\n", + "In order to compute observable expectation values, we need to converge a CTMRG environment\n", + "on the evolved PEPS. Let's do so:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "peps = InfinitePEPS(wpeps) ## absorb the weights\n", + "env₀ = CTMRGEnv(rand, Float64, peps, env_space)\n", + "trscheme_env = truncerr(1e-10) & truncdim(χenv)\n", + "env, = leading_boundary(\n", + " env₀,\n", + " peps;\n", + " alg=:sequential,\n", + " projector_alg=:fullinfinite,\n", + " tol=1e-10,\n", + " trscheme=trscheme_env,\n", + ");" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Finally, we'll measure the energy and different magnetizations. For the magnetizations,\n", + "the plan is to compute the expectation values unit cell entry-wise in different spin\n", + "directions:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "function compute_mags(peps::InfinitePEPS, env::CTMRGEnv)\n", + " lattice = collect(space(t, 1) for t in peps.A)\n", + "\n", + " # detect symmetry on physical axis\n", + " symm = sectortype(space(peps.A[1, 1]))\n", + " if symm == Trivial\n", + " S_ops = real.([S_x(symm), im * S_y(symm), S_z(symm)])\n", + " elseif symm == U1Irrep\n", + " S_ops = real.([S_z(symm)]) ## only Sz preserves \n", + " end\n", + "\n", + " return map(Iterators.product(axes(peps, 1), axes(peps, 2), S_ops)) do (r, c, S)\n", + " expectation_value(peps, LocalOperator(lattice, (CartesianIndex(r, c),) => S), env)\n", + " end\n", + "end\n", + "\n", + "E = expectation_value(peps, H, env) / (Nr * Nc)\n", + "Ms = compute_mags(peps, env)\n", + "M_norms = map(\n", + " rc -> norm(Ms[rc[1], rc[2], :]), Iterators.product(axes(peps, 1), axes(peps, 2))\n", + ")\n", + "@show E Ms M_norms;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "To assess the results, we will benchmark against data from [Corboz](@cite corboz_variational_2016),\n", + "which use manual gradients to perform a variational optimization of the Heisenberg model.\n", + "In particular, for the energy and magnetization they find $E_\\text{ref} = -0.6675$ and\n", + "$M_\\text{ref} = 0.3767$. Looking at the relative errors, we find general agreement, although\n", + "the accuracy is limited by the methodological limitations of the simple update algorithm as\n", + "well as finite bond dimension effects and a lacking extrapolation:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "E_ref = -0.6675\n", + "M_ref = 0.3767\n", + "@show (E - E_ref) / E_ref\n", + "@show (mean(M_norms) - M_ref) / E_ref;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "---\n", + "\n", + "*This notebook was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*" + ], + "metadata": {} + } + ], + "nbformat_minor": 3, + "metadata": { + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.11.4" + }, + "kernelspec": { + "name": "julia-1.11", + "display_name": "Julia 1.11.4", + "language": "julia" + } + }, + "nbformat": 4 +} diff --git a/docs/src/examples/hubbard_su/index.md b/docs/src/examples/hubbard_su/index.md new file mode 100644 index 000000000..0d2b3048d --- /dev/null +++ b/docs/src/examples/hubbard_su/index.md @@ -0,0 +1,206 @@ +```@meta +EditURL = "../../../../examples/hubbard_su/main.jl" +``` + +[![](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/QuantumKitHub/PEPSKit.jl/gh-pages?filepath=dev/examples/hubbard_su/main.ipynb) +[![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](https://nbviewer.jupyter.org/github/QuantumKitHub/PEPSKit.jl/blob/gh-pages/dev/examples/hubbard_su/main.ipynb) +[![](https://img.shields.io/badge/download-project-orange)](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/QuantumKitHub/PEPSKit.jl/examples/tree/gh-pages/dev/examples/hubbard_su) + + +# Simple update for the Fermi-Hubbard model at half-filling + +Once again, we consider the Hubbard model but this time we obtain the ground-state PEPS by +imaginary time evolution. In particular, we'll use the [`SimpleUpdate`](@ref) algorithm. +As a reminder, we define the Hubbard model as + +```math +H = -t \sum_{\langle i,j \rangle} \sum_{\sigma} \left( c_{i,\sigma}^+ c_{j,\sigma}^- - +c_{i,\sigma}^- c_{j,\sigma}^+ \right) + U \sum_i n_{i,\uparrow}n_{i,\downarrow} - \mu \sum_i n_i +``` + +with $\sigma \in \{\uparrow,\downarrow\}$ and $n_{i,\sigma} = c_{i,\sigma}^+ c_{i,\sigma}^-$. + +Let's get started by seeding the RNG and importing the required modules: + +````julia +using Random +using TensorKit, PEPSKit +Random.seed!(12329348592498); +```` + +## Defining the Hamiltonian + +First, we define the Hubbard model at $t=1$ hopping and $U=6$ using `Trivial` sectors for +the particle and spin symmetries, and set $\mu = U/2$ for half-filling. The model will be +constructed on a $2 \times 2$ unit cell, so we have: + +````julia +t = 1 +U = 6 +Nr, Nc = 2, 2 +H = hubbard_model(Float64, Trivial, Trivial, InfiniteSquare(Nr, Nc); t, U, mu=U / 2); +```` + +## Running the simple update algorithm + +Next, we'll specify the virtual PEPS bond dimension and define the fermionic physical and +virtual spaces. The simple update algorithm evolves an infinite PEPS with weights on the +virtual bonds, so we here need to intialize an [`InfiniteWeightPEPS`](@ref). By default, +the bond weights will be identity. Unlike in the other examples, we here use tensors with +real `Float64` entries: + +````julia +Dbond = 8 +physical_space = Vect[fℤ₂](0 => 2, 1 => 2) +virtual_space = Vect[fℤ₂](0 => Dbond / 2, 1 => Dbond / 2) +wpeps = InfiniteWeightPEPS(rand, Float64, physical_space, virtual_space; unitcell=(Nr, Nc)); +```` + +Let's set the algorithm parameters: The plan is to successively decrease the time interval of +the Trotter-Suzuki as well as the convergence tolerance such that we obtain a more accurate +result at each iteration. To run the simple update, we call [`simpleupdate`](@ref) where we +use the keyword `bipartite=false` - meaning that we use the full $2 \times 2$ unit cell +without assuming a bipartite structure. Thus, we can start evolving: + +````julia +dts = [1e-2, 1e-3, 4e-4, 1e-4] +tols = [1e-6, 1e-8, 1e-8, 1e-8] +maxiter = 20000 + +for (n, (dt, tol)) in enumerate(zip(dts, tols)) + trscheme = truncerr(1e-10) & truncdim(Dbond) + alg = SimpleUpdate(dt, tol, maxiter, trscheme) + global wpeps, = simpleupdate(wpeps, H, alg; bipartite=false) +end +```` + +```` +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 1 : dt = 1e-02, weight diff = 2.355e+00, time = 19.473 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 500 : dt = 1e-02, weight diff = 3.984e-04, time = 0.020 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 1000 : dt = 1e-02, weight diff = 2.866e-06, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU conv 1061 : dt = 1e-02, weight diff = 9.956e-07, time = 38.226 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 1 : dt = 1e-03, weight diff = 6.070e-03, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 500 : dt = 1e-03, weight diff = 1.874e-06, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 1000 : dt = 1e-03, weight diff = 6.437e-07, time = 0.019 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 1500 : dt = 1e-03, weight diff = 2.591e-07, time = 0.019 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 2000 : dt = 1e-03, weight diff = 1.053e-07, time = 0.019 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 2500 : dt = 1e-03, weight diff = 4.280e-08, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 3000 : dt = 1e-03, weight diff = 1.741e-08, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU conv 3309 : dt = 1e-03, weight diff = 9.983e-09, time = 53.956 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 1 : dt = 4e-04, weight diff = 4.030e-04, time = 0.020 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 500 : dt = 4e-04, weight diff = 1.776e-07, time = 0.019 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 1000 : dt = 4e-04, weight diff = 7.091e-08, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 1500 : dt = 4e-04, weight diff = 3.997e-08, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 2000 : dt = 4e-04, weight diff = 2.622e-08, time = 0.020 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 2500 : dt = 4e-04, weight diff = 1.796e-08, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 3000 : dt = 4e-04, weight diff = 1.245e-08, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU conv 3303 : dt = 4e-04, weight diff = 9.997e-09, time = 54.244 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 1 : dt = 1e-04, weight diff = 2.014e-04, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 500 : dt = 1e-04, weight diff = 5.664e-08, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 1000 : dt = 1e-04, weight diff = 4.106e-08, time = 0.020 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 1500 : dt = 1e-04, weight diff = 3.033e-08, time = 0.021 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 2000 : dt = 1e-04, weight diff = 2.290e-08, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 2500 : dt = 1e-04, weight diff = 1.773e-08, time = 0.015 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 3000 : dt = 1e-04, weight diff = 1.410e-08, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU iter 3500 : dt = 1e-04, weight diff = 1.152e-08, time = 0.014 sec +[ Info: Space of x-weight at [1, 1] = Vect[FermionParity](0=>4, 1=>4) +[ Info: SU conv 3893 : dt = 1e-04, weight diff = 9.997e-09, time = 63.901 sec + +```` + +To obtain the evolved `InfiniteWeightPEPS` as an actual PEPS without weights on the bonds, +we can just call the following constructor: + +````julia +peps = InfinitePEPS(wpeps); +```` + +## Computing the ground-state energy + +In order to compute the energy expectation value with evolved PEPS, we need to converge a +CTMRG environment on it. We first converge an environment with a small enviroment dimension +and then use that to initialize another run with bigger environment dimension. We'll use +`trscheme=truncdim(χ)` for that such that the dimension is increased during the second CTMRG +run: + +````julia +χenv₀, χenv = 6, 16 +env_space = Vect[fℤ₂](0 => χenv₀ / 2, 1 => χenv₀ / 2) + +env = CTMRGEnv(rand, Float64, peps, env_space) +for χ in [χenv₀, χenv] + global env, = leading_boundary( + env, peps; alg=:sequential, tol=1e-5, trscheme=truncdim(χ) + ) +end +```` + +```` +[ Info: CTMRG init: obj = -1.542952315399e-10 err = 1.0000e+00 +┌ Warning: CTMRG cancel 100: obj = +6.169093163834e-01 err = 5.9784504048e-01 time = 32.80 sec +└ @ PEPSKit ~/repos/PEPSKit.jl/src/algorithms/ctmrg/ctmrg.jl:155 +[ Info: CTMRG init: obj = +6.169093163834e-01 err = 1.0000e+00 +[ Info: CTMRG conv 30: obj = +5.888235783866e-01 err = 4.9402085825e-06 time = 1.24 min + +```` + +We measure the energy by computing the `H` expectation value, where we have to make sure to +normalize with respect to the unit cell to obtain the energy per site: + +````julia +E = expectation_value(peps, H, env) / (Nr * Nc) +@show E; +```` + +```` +E = -3.633302571096653 + +```` + +Finally, we can compare the obtained ground-state energy against the literature, namely the +QMC estimates from [Qin et al.](@cite qin_benchmark_2016). We find that the results generally +agree: + +````julia +Es_exact = Dict(0 => -1.62, 2 => -0.176, 4 => 0.8603, 6 => -0.6567, 8 => -0.5243) +E_exact = Es_exact[U] - U / 2 +@show (E - E_exact) / E_exact; +```` + +```` +(E - E_exact) / E_exact = -0.006398509285242631 + +```` + +--- + +*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* + diff --git a/docs/src/examples/hubbard_su/main.ipynb b/docs/src/examples/hubbard_su/main.ipynb new file mode 100644 index 000000000..f2e5ffcaa --- /dev/null +++ b/docs/src/examples/hubbard_su/main.ipynb @@ -0,0 +1,229 @@ +{ + "cells": [ + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Markdown #hide" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "# Simple update for the Fermi-Hubbard model at half-filling\n", + "\n", + "Once again, we consider the Hubbard model but this time we obtain the ground-state PEPS by\n", + "imaginary time evolution. In particular, we'll use the `SimpleUpdate` algorithm.\n", + "As a reminder, we define the Hubbard model as\n", + "\n", + "$$\n", + "H = -t \\sum_{\\langle i,j \\rangle} \\sum_{\\sigma} \\left( c_{i,\\sigma}^+ c_{j,\\sigma}^- -\n", + "c_{i,\\sigma}^- c_{j,\\sigma}^+ \\right) + U \\sum_i n_{i,\\uparrow}n_{i,\\downarrow} - \\mu \\sum_i n_i\n", + "$$\n", + "\n", + "with $\\sigma \\in \\{\\uparrow,\\downarrow\\}$ and $n_{i,\\sigma} = c_{i,\\sigma}^+ c_{i,\\sigma}^-$.\n", + "\n", + "Let's get started by seeding the RNG and importing the required modules:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Random\n", + "using TensorKit, PEPSKit\n", + "Random.seed!(12329348592498);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Defining the Hamiltonian\n", + "\n", + "First, we define the Hubbard model at $t=1$ hopping and $U=6$ using `Trivial` sectors for\n", + "the particle and spin symmetries, and set $\\mu = U/2$ for half-filling. The model will be\n", + "constructed on a $2 \\times 2$ unit cell, so we have:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "t = 1\n", + "U = 6\n", + "Nr, Nc = 2, 2\n", + "H = hubbard_model(Float64, Trivial, Trivial, InfiniteSquare(Nr, Nc); t, U, mu=U / 2);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Running the simple update algorithm\n", + "\n", + "Next, we'll specify the virtual PEPS bond dimension and define the fermionic physical and\n", + "virtual spaces. The simple update algorithm evolves an infinite PEPS with weights on the\n", + "virtual bonds, so we here need to intialize an `InfiniteWeightPEPS`. By default,\n", + "the bond weights will be identity. Unlike in the other examples, we here use tensors with\n", + "real `Float64` entries:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "Dbond = 8\n", + "physical_space = Vect[fℤ₂](0 => 2, 1 => 2)\n", + "virtual_space = Vect[fℤ₂](0 => Dbond / 2, 1 => Dbond / 2)\n", + "wpeps = InfiniteWeightPEPS(rand, Float64, physical_space, virtual_space; unitcell=(Nr, Nc));" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Let's set the algorithm parameters: The plan is to successively decrease the time interval of\n", + "the Trotter-Suzuki as well as the convergence tolerance such that we obtain a more accurate\n", + "result at each iteration. To run the simple update, we call `simpleupdate` where we\n", + "use the keyword `bipartite=false` - meaning that we use the full $2 \\times 2$ unit cell\n", + "without assuming a bipartite structure. Thus, we can start evolving:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "dts = [1e-2, 1e-3, 4e-4, 1e-4]\n", + "tols = [1e-6, 1e-8, 1e-8, 1e-8]\n", + "maxiter = 20000\n", + "\n", + "for (n, (dt, tol)) in enumerate(zip(dts, tols))\n", + " trscheme = truncerr(1e-10) & truncdim(Dbond)\n", + " alg = SimpleUpdate(dt, tol, maxiter, trscheme)\n", + " global wpeps, = simpleupdate(wpeps, H, alg; bipartite=false)\n", + "end" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "To obtain the evolved `InfiniteWeightPEPS` as an actual PEPS without weights on the bonds,\n", + "we can just call the following constructor:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "peps = InfinitePEPS(wpeps);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Computing the ground-state energy\n", + "\n", + "In order to compute the energy expectation value with evolved PEPS, we need to converge a\n", + "CTMRG environment on it. We first converge an environment with a small enviroment dimension\n", + "and then use that to initialize another run with bigger environment dimension. We'll use\n", + "`trscheme=truncdim(χ)` for that such that the dimension is increased during the second CTMRG\n", + "run:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "χenv₀, χenv = 6, 16\n", + "env_space = Vect[fℤ₂](0 => χenv₀ / 2, 1 => χenv₀ / 2)\n", + "\n", + "env = CTMRGEnv(rand, Float64, peps, env_space)\n", + "for χ in [χenv₀, χenv]\n", + " global env, = leading_boundary(\n", + " env, peps; alg=:sequential, tol=1e-5, trscheme=truncdim(χ)\n", + " )\n", + "end" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "We measure the energy by computing the `H` expectation value, where we have to make sure to\n", + "normalize with respect to the unit cell to obtain the energy per site:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "E = expectation_value(peps, H, env) / (Nr * Nc)\n", + "@show E;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Finally, we can compare the obtained ground-state energy against the literature, namely the\n", + "QMC estimates from [Qin et al.](@cite qin_benchmark_2016). We find that the results generally\n", + "agree:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "Es_exact = Dict(0 => -1.62, 2 => -0.176, 4 => 0.8603, 6 => -0.6567, 8 => -0.5243)\n", + "E_exact = Es_exact[U] - U / 2\n", + "@show (E - E_exact) / E_exact;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "---\n", + "\n", + "*This notebook was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*" + ], + "metadata": {} + } + ], + "nbformat_minor": 3, + "metadata": { + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.11.4" + }, + "kernelspec": { + "name": "julia-1.11", + "display_name": "Julia 1.11.4", + "language": "julia" + } + }, + "nbformat": 4 +} diff --git a/docs/src/examples/index.md b/docs/src/examples/index.md index f33fdb82b..d17918489 100644 --- a/docs/src/examples/index.md +++ b/docs/src/examples/index.md @@ -1 +1,31 @@ -For now, refer to the [examples folder](https://github.com/QuantumKitHub/PEPSKit.jl/tree/master/examples) on GitHub. \ No newline at end of file +# [Overview](@id e_overview) + +Here we provide a number of commented example pages that serve as short tutorials on how to use PEPSKit in various situations. Applications span from two-dimensional quantum models, including fermionic Hamiltonians, to classical three-dimensional statistical mechanics: + +## Optimization + +```@contents +Pages = Main.examples_optimization +Depth = 1 +``` + +## Time Evolution + +```@contents +Pages = Main.examples_time_evolution +Depth = 1 +``` + +## Partition Functions + +```@contents +Pages = Main.examples_partition_functions +Depth = 1 +``` + +## Boundary MPS + +```@contents +Pages = Main.examples_boundary_mps +Depth = 1 +``` diff --git a/docs/src/examples/xxz/index.md b/docs/src/examples/xxz/index.md new file mode 100644 index 000000000..3c8e9ad89 --- /dev/null +++ b/docs/src/examples/xxz/index.md @@ -0,0 +1,226 @@ +```@meta +EditURL = "../../../../examples/xxz/main.jl" +``` + +[![](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/QuantumKitHub/PEPSKit.jl/gh-pages?filepath=dev/examples/xxz/main.ipynb) +[![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](https://nbviewer.jupyter.org/github/QuantumKitHub/PEPSKit.jl/blob/gh-pages/dev/examples/xxz/main.ipynb) +[![](https://img.shields.io/badge/download-project-orange)](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/QuantumKitHub/PEPSKit.jl/examples/tree/gh-pages/dev/examples/xxz) + + +# Néel order in the $U(1)$-symmetric XXZ model + +Here, we want to look at a special case of the Heisenberg model, where the $x$ and $y$ +couplings are equal, called the XXZ model + +```math +H_0 = J \big(\sum_{\langle i, j \rangle} S_i^x S_j^x + S_i^y S_j^y + \Delta S_i^z S_j^z \big) . +``` + +For appropriate $\Delta$, the model enters an antiferromagnetic phase (Néel order) which we +will force by adding staggered magnetic charges to $H_0$. Furthermore, since the XXZ +Hamiltonian obeys a $U(1)$ symmetry, we will make use of that and work with $U(1)$-symmetric +PEPS and CTMRG environments. For simplicity, we will consider spin-$1/2$ operators. + +But first, let's make this example deterministic and import the required packages: + +````julia +using Random +using TensorKit, PEPSKit +using MPSKit: add_physical_charge +Random.seed!(2928528935); +```` + +## Constructing the model + +Let us define the $U(1)$-symmetric XXZ Hamiltonian on a $2 \times 2$ unit cell with the +parameters: + +````julia +J = 1.0 +Delta = 1.0 +spin = 1//2 +symmetry = U1Irrep +lattice = InfiniteSquare(2, 2) +H₀ = heisenberg_XXZ(ComplexF64, symmetry, lattice; J, Delta, spin); +```` + +This ensures that our PEPS ansatz can support the bipartite Néel order. As discussed above, +we encode the Néel order directly in the ansatz by adding staggered auxiliary physical +charges: + +````julia +S_aux = [ + U1Irrep(-1//2) U1Irrep(1//2) + U1Irrep(1//2) U1Irrep(-1//2) +] +H = add_physical_charge(H₀, S_aux); +```` + +## Specifying the symmetric virtual spaces + +Before we create an initial PEPS and CTM environment, we need to think about which +symmetric spaces we need to construct. Since we want to exploit the global $U(1)$ symmetry +of the model, we will use TensorKit's `U1Space`s where we specify dimensions for each +symmetry sector. From the virtual spaces, we will need to construct a unit cell (a matrix) +of spaces which will be supplied to the PEPS constructor. The same is true for the physical +spaces, which can be extracted directly from the Hamiltonian `LocalOperator`: + +````julia +V_peps = U1Space(0 => 2, 1 => 1, -1 => 1) +V_env = U1Space(0 => 6, 1 => 4, -1 => 4, 2 => 2, -2 => 2) +virtual_spaces = fill(V_peps, size(lattice)...) +physical_spaces = physicalspace(H) +```` + +```` +2×2 Matrix{TensorKit.GradedSpace{TensorKitSectors.U1Irrep, TensorKit.SortedVectorDict{TensorKitSectors.U1Irrep, Int64}}}: + Rep[TensorKitSectors.U₁](0=>1, -1=>1) Rep[TensorKitSectors.U₁](0=>1, 1=>1) + Rep[TensorKitSectors.U₁](0=>1, 1=>1) Rep[TensorKitSectors.U₁](0=>1, -1=>1) +```` + +## Ground state search + +From this point onwards it's business as usual: Create an initial PEPS and environment +(using the symmetric spaces), specify the algorithmic parameters and optimize: + +````julia +boundary_alg = (; tol=1e-8, alg=:simultaneous, trscheme=(; alg=:fixedspace)) +gradient_alg = (; tol=1e-6, alg=:eigsolver, maxiter=10, iterscheme=:diffgauge) +optimizer_alg = (; tol=1e-4, alg=:lbfgs, maxiter=85, ls_maxiter=3, ls_maxfg=3) + +peps₀ = InfinitePEPS(randn, ComplexF64, physical_spaces, virtual_spaces) +env₀, = leading_boundary(CTMRGEnv(peps₀, V_env), peps₀; boundary_alg...); +```` + +```` +[ Info: CTMRG init: obj = -1.121020187593e+04 -6.991066478499e+03im err = 1.0000e+00 +[ Info: CTMRG conv 26: obj = +6.369731502336e+03 -8.500546755386e-08im err = 7.5599921139e-09 time = 2.05 sec + +```` + +Finally, we can optimize the PEPS with respect to the XXZ Hamiltonian. Note that the +optimization might take a while since precompilation of symmetric AD code takes longer and +because symmetric tensors do create a bit of overhead (which does pay off at larger bond +and environment dimensions): + +````julia +peps, env, E, info = fixedpoint( + H, peps₀, env₀; boundary_alg, gradient_alg, optimizer_alg, verbosity=3 +) +@show E; +```` + +```` +[ Info: LBFGS: initializing with f = -0.033045967451, ‖∇f‖ = 3.2952e-01 +┌ Warning: The function `scale!!` is not implemented for (values of) type `Tuple{InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.GradedSpace{TensorKitSectors.U1Irrep, TensorKit.SortedVectorDict{TensorKitSectors.U1Irrep, Int64}}, 1, 4, Vector{ComplexF64}}}, Float64}`; +│ this fallback will disappear in future versions of VectorInterface.jl +└ @ VectorInterface ~/.julia/packages/VectorInterface/J6qCR/src/fallbacks.jl:91 +┌ Warning: Linesearch not converged after 1 iterations and 4 function evaluations: +│ α = 2.50e+01, dϕ = -9.90e-03, ϕ - ϕ₀ = -1.53e-01 +└ @ OptimKit ~/.julia/packages/OptimKit/G6i79/src/linesearches.jl:148 +[ Info: LBFGS: iter 1, time 151.05 s: f = -0.185750019542, ‖∇f‖ = 1.8647e+00, α = 2.50e+01, m = 0, nfg = 4 +┌ Warning: The function `add!!` is not implemented for (values of) type `Tuple{InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.GradedSpace{TensorKitSectors.U1Irrep, TensorKit.SortedVectorDict{TensorKitSectors.U1Irrep, Int64}}, 1, 4, Vector{ComplexF64}}}, InfinitePEPS{TensorKit.TensorMap{ComplexF64, TensorKit.GradedSpace{TensorKitSectors.U1Irrep, TensorKit.SortedVectorDict{TensorKitSectors.U1Irrep, Int64}}, 1, 4, Vector{ComplexF64}}}, Int64, VectorInterface.One}`; +│ this fallback will disappear in future versions of VectorInterface.jl +└ @ VectorInterface ~/.julia/packages/VectorInterface/J6qCR/src/fallbacks.jl:163 +┌ Warning: Linesearch not converged after 1 iterations and 4 function evaluations: +│ α = 2.50e+01, dϕ = -1.84e-03, ϕ - ϕ₀ = -3.93e-01 +└ @ OptimKit ~/.julia/packages/OptimKit/G6i79/src/linesearches.jl:148 +[ Info: LBFGS: iter 2, time 198.17 s: f = -0.579230108476, ‖∇f‖ = 5.7732e-01, α = 2.50e+01, m = 0, nfg = 4 +[ Info: LBFGS: iter 3, time 209.90 s: f = -0.613445426304, ‖∇f‖ = 3.3947e-01, α = 1.00e+00, m = 1, nfg = 1 +[ Info: LBFGS: iter 4, time 220.81 s: f = -0.638685295144, ‖∇f‖ = 2.2104e-01, α = 1.00e+00, m = 2, nfg = 1 +[ Info: LBFGS: iter 5, time 232.32 s: f = -0.650336962208, ‖∇f‖ = 1.9524e-01, α = 1.00e+00, m = 3, nfg = 1 +[ Info: LBFGS: iter 6, time 243.27 s: f = -0.654880752783, ‖∇f‖ = 7.1842e-02, α = 1.00e+00, m = 4, nfg = 1 +[ Info: LBFGS: iter 7, time 253.29 s: f = -0.656075650331, ‖∇f‖ = 5.2129e-02, α = 1.00e+00, m = 5, nfg = 1 +[ Info: LBFGS: iter 8, time 264.02 s: f = -0.659041890147, ‖∇f‖ = 5.3917e-02, α = 1.00e+00, m = 6, nfg = 1 +[ Info: LBFGS: iter 9, time 274.83 s: f = -0.660552875456, ‖∇f‖ = 9.6848e-02, α = 1.00e+00, m = 7, nfg = 1 +[ Info: LBFGS: iter 10, time 284.84 s: f = -0.662163341463, ‖∇f‖ = 2.9524e-02, α = 1.00e+00, m = 8, nfg = 1 +[ Info: LBFGS: iter 11, time 295.73 s: f = -0.662506513828, ‖∇f‖ = 2.1440e-02, α = 1.00e+00, m = 9, nfg = 1 +[ Info: LBFGS: iter 12, time 305.73 s: f = -0.662847746095, ‖∇f‖ = 2.0917e-02, α = 1.00e+00, m = 10, nfg = 1 +[ Info: LBFGS: iter 13, time 316.59 s: f = -0.663230218002, ‖∇f‖ = 2.5387e-02, α = 1.00e+00, m = 11, nfg = 1 +[ Info: LBFGS: iter 14, time 323.15 s: f = -0.663678142653, ‖∇f‖ = 2.2924e-02, α = 1.00e+00, m = 12, nfg = 1 +[ Info: LBFGS: iter 15, time 328.96 s: f = -0.664034475269, ‖∇f‖ = 2.1574e-02, α = 1.00e+00, m = 13, nfg = 1 +[ Info: LBFGS: iter 16, time 340.52 s: f = -0.664687988771, ‖∇f‖ = 2.7632e-02, α = 1.00e+00, m = 14, nfg = 1 +[ Info: LBFGS: iter 17, time 363.13 s: f = -0.664947633065, ‖∇f‖ = 3.2552e-02, α = 4.47e-01, m = 15, nfg = 2 +[ Info: LBFGS: iter 18, time 374.71 s: f = -0.665379251393, ‖∇f‖ = 2.5817e-02, α = 1.00e+00, m = 16, nfg = 1 +[ Info: LBFGS: iter 19, time 386.41 s: f = -0.665603907305, ‖∇f‖ = 2.4951e-02, α = 1.00e+00, m = 17, nfg = 1 +[ Info: LBFGS: iter 20, time 397.29 s: f = -0.665762559605, ‖∇f‖ = 1.5130e-02, α = 1.00e+00, m = 18, nfg = 1 +[ Info: LBFGS: iter 21, time 408.95 s: f = -0.666003995146, ‖∇f‖ = 1.6565e-02, α = 1.00e+00, m = 19, nfg = 1 +[ Info: LBFGS: iter 22, time 420.69 s: f = -0.666347181648, ‖∇f‖ = 2.3025e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 23, time 432.27 s: f = -0.666599630335, ‖∇f‖ = 2.3462e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 24, time 444.52 s: f = -0.666793651525, ‖∇f‖ = 2.1908e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 25, time 456.87 s: f = -0.666949601577, ‖∇f‖ = 1.0867e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 26, time 469.23 s: f = -0.667058133735, ‖∇f‖ = 1.2475e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 27, time 480.73 s: f = -0.667165497296, ‖∇f‖ = 1.4340e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 28, time 492.42 s: f = -0.667263554939, ‖∇f‖ = 1.5667e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 29, time 504.11 s: f = -0.667357062063, ‖∇f‖ = 8.8494e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 30, time 515.48 s: f = -0.667450569240, ‖∇f‖ = 1.1496e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 31, time 527.47 s: f = -0.667569394671, ‖∇f‖ = 1.3976e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 32, time 539.89 s: f = -0.667657944766, ‖∇f‖ = 2.2321e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 33, time 551.52 s: f = -0.667799360459, ‖∇f‖ = 8.7543e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 34, time 563.83 s: f = -0.667852887856, ‖∇f‖ = 6.6668e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 35, time 576.15 s: f = -0.667926685233, ‖∇f‖ = 1.1902e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 36, time 588.14 s: f = -0.667979135052, ‖∇f‖ = 1.6349e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 37, time 595.62 s: f = -0.668039789983, ‖∇f‖ = 9.3108e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 38, time 608.27 s: f = -0.668087921855, ‖∇f‖ = 5.4669e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 39, time 619.58 s: f = -0.668109250700, ‖∇f‖ = 6.6628e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 40, time 627.01 s: f = -0.668159589044, ‖∇f‖ = 9.3986e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 41, time 634.57 s: f = -0.668230776691, ‖∇f‖ = 1.1534e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 42, time 646.27 s: f = -0.668277979949, ‖∇f‖ = 1.0707e-02, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 43, time 658.67 s: f = -0.668314491012, ‖∇f‖ = 4.4476e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 44, time 670.98 s: f = -0.668333048195, ‖∇f‖ = 5.0062e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 45, time 682.77 s: f = -0.668357479998, ‖∇f‖ = 7.1015e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 46, time 695.32 s: f = -0.668412792965, ‖∇f‖ = 9.9323e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 47, time 719.56 s: f = -0.668439289699, ‖∇f‖ = 1.1349e-02, α = 4.53e-01, m = 20, nfg = 2 +[ Info: LBFGS: iter 48, time 732.05 s: f = -0.668482319738, ‖∇f‖ = 6.5163e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 49, time 744.54 s: f = -0.668507687742, ‖∇f‖ = 3.4077e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 50, time 756.98 s: f = -0.668523331148, ‖∇f‖ = 4.4119e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 51, time 768.78 s: f = -0.668544159952, ‖∇f‖ = 6.8178e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 52, time 781.25 s: f = -0.668572411228, ‖∇f‖ = 8.9225e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 53, time 794.44 s: f = -0.668603530102, ‖∇f‖ = 5.4599e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 54, time 807.70 s: f = -0.668626672236, ‖∇f‖ = 3.2841e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 55, time 819.27 s: f = -0.668639698673, ‖∇f‖ = 4.2163e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 56, time 831.79 s: f = -0.668655954756, ‖∇f‖ = 5.4299e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 57, time 844.32 s: f = -0.668673395222, ‖∇f‖ = 5.4970e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 58, time 856.85 s: f = -0.668687854178, ‖∇f‖ = 3.7476e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 59, time 868.59 s: f = -0.668698487673, ‖∇f‖ = 3.3854e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 60, time 881.11 s: f = -0.668705111514, ‖∇f‖ = 3.8959e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 61, time 894.30 s: f = -0.668720251413, ‖∇f‖ = 4.7426e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 62, time 906.61 s: f = -0.668726232684, ‖∇f‖ = 7.2528e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 63, time 919.67 s: f = -0.668739847818, ‖∇f‖ = 2.5815e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 64, time 932.86 s: f = -0.668746591101, ‖∇f‖ = 2.1675e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 65, time 944.63 s: f = -0.668754620918, ‖∇f‖ = 3.0368e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 66, time 957.21 s: f = -0.668767041464, ‖∇f‖ = 3.2464e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 67, time 982.43 s: f = -0.668775239596, ‖∇f‖ = 4.1610e-03, α = 4.24e-01, m = 20, nfg = 2 +[ Info: LBFGS: iter 68, time 995.66 s: f = -0.668784522482, ‖∇f‖ = 2.0585e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 69, time 1008.92 s: f = -0.668792875861, ‖∇f‖ = 2.5737e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 70, time 1020.82 s: f = -0.668799555353, ‖∇f‖ = 3.0991e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 71, time 1034.03 s: f = -0.668807510786, ‖∇f‖ = 3.9740e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 72, time 1047.37 s: f = -0.668815529198, ‖∇f‖ = 2.8312e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 73, time 1059.65 s: f = -0.668820072176, ‖∇f‖ = 1.8277e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 74, time 1072.71 s: f = -0.668823045663, ‖∇f‖ = 2.1189e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 75, time 1086.03 s: f = -0.668829224417, ‖∇f‖ = 3.3000e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 76, time 1098.65 s: f = -0.668834661996, ‖∇f‖ = 3.3841e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 77, time 1112.36 s: f = -0.668839125190, ‖∇f‖ = 1.5494e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 78, time 1126.16 s: f = -0.668842178047, ‖∇f‖ = 1.8074e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 79, time 1140.07 s: f = -0.668846182434, ‖∇f‖ = 2.1704e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 80, time 1152.70 s: f = -0.668849851199, ‖∇f‖ = 4.3895e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 81, time 1165.98 s: f = -0.668854730352, ‖∇f‖ = 1.9014e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 82, time 1179.77 s: f = -0.668857445978, ‖∇f‖ = 1.6213e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 83, time 1192.89 s: f = -0.668861083091, ‖∇f‖ = 2.1544e-03, α = 1.00e+00, m = 20, nfg = 1 +[ Info: LBFGS: iter 84, time 1205.95 s: f = -0.668865914837, ‖∇f‖ = 2.4867e-03, α = 1.00e+00, m = 20, nfg = 1 +┌ Warning: LBFGS: not converged to requested tol after 85 iterations and time 1219.27 s: f = -0.668869945971, ‖∇f‖ = 4.5125e-03 +└ @ OptimKit ~/.julia/packages/OptimKit/G6i79/src/lbfgs.jl:197 +E = -0.6688699459708735 + +```` + +Note that for the specified parameters $J = \Delta = 1$, we simulated the same Hamiltonian as +in the [Heisenberg example](@ref examples_heisenberg). In that example, with a non-symmetric +$D=2$ PEPS simulation, we reached a ground-state energy of around $E_\text{D=2} = -0.6625\dots$. +Again comparing against [Sandvik's](@cite sandvik_computational_2011) accurate QMC estimate +``E_{\text{ref}}=−0.6694421``, we see that we already got closer to the reference energy. + +--- + +*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* + diff --git a/docs/src/examples/xxz/main.ipynb b/docs/src/examples/xxz/main.ipynb new file mode 100644 index 000000000..77737aaa4 --- /dev/null +++ b/docs/src/examples/xxz/main.ipynb @@ -0,0 +1,199 @@ +{ + "cells": [ + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Markdown #hide" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "# Néel order in the $U(1)$-symmetric XXZ model\n", + "\n", + "Here, we want to look at a special case of the Heisenberg model, where the $x$ and $y$\n", + "couplings are equal, called the XXZ model\n", + "\n", + "$$\n", + "H_0 = J \\big(\\sum_{\\langle i, j \\rangle} S_i^x S_j^x + S_i^y S_j^y + \\Delta S_i^z S_j^z \\big) .\n", + "$$\n", + "\n", + "For appropriate $\\Delta$, the model enters an antiferromagnetic phase (Néel order) which we\n", + "will force by adding staggered magnetic charges to $H_0$. Furthermore, since the XXZ\n", + "Hamiltonian obeys a $U(1)$ symmetry, we will make use of that and work with $U(1)$-symmetric\n", + "PEPS and CTMRG environments. For simplicity, we will consider spin-$1/2$ operators.\n", + "\n", + "But first, let's make this example deterministic and import the required packages:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "using Random\n", + "using TensorKit, PEPSKit\n", + "using MPSKit: add_physical_charge\n", + "Random.seed!(2928528935);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Constructing the model\n", + "\n", + "Let us define the $U(1)$-symmetric XXZ Hamiltonian on a $2 \\times 2$ unit cell with the\n", + "parameters:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "J = 1.0\n", + "Delta = 1.0\n", + "spin = 1//2\n", + "symmetry = U1Irrep\n", + "lattice = InfiniteSquare(2, 2)\n", + "H₀ = heisenberg_XXZ(ComplexF64, symmetry, lattice; J, Delta, spin);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "This ensures that our PEPS ansatz can support the bipartite Néel order. As discussed above,\n", + "we encode the Néel order directly in the ansatz by adding staggered auxiliary physical\n", + "charges:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "S_aux = [\n", + " U1Irrep(-1//2) U1Irrep(1//2)\n", + " U1Irrep(1//2) U1Irrep(-1//2)\n", + "]\n", + "H = add_physical_charge(H₀, S_aux);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Specifying the symmetric virtual spaces\n", + "\n", + "Before we create an initial PEPS and CTM environment, we need to think about which\n", + "symmetric spaces we need to construct. Since we want to exploit the global $U(1)$ symmetry\n", + "of the model, we will use TensorKit's `U1Space`s where we specify dimensions for each\n", + "symmetry sector. From the virtual spaces, we will need to construct a unit cell (a matrix)\n", + "of spaces which will be supplied to the PEPS constructor. The same is true for the physical\n", + "spaces, which can be extracted directly from the Hamiltonian `LocalOperator`:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "V_peps = U1Space(0 => 2, 1 => 1, -1 => 1)\n", + "V_env = U1Space(0 => 6, 1 => 4, -1 => 4, 2 => 2, -2 => 2)\n", + "virtual_spaces = fill(V_peps, size(lattice)...)\n", + "physical_spaces = physicalspace(H)" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Ground state search\n", + "\n", + "From this point onwards it's business as usual: Create an initial PEPS and environment\n", + "(using the symmetric spaces), specify the algorithmic parameters and optimize:" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "boundary_alg = (; tol=1e-8, alg=:simultaneous, trscheme=(; alg=:fixedspace))\n", + "gradient_alg = (; tol=1e-6, alg=:eigsolver, maxiter=10, iterscheme=:diffgauge)\n", + "optimizer_alg = (; tol=1e-4, alg=:lbfgs, maxiter=85, ls_maxiter=3, ls_maxfg=3)\n", + "\n", + "peps₀ = InfinitePEPS(randn, ComplexF64, physical_spaces, virtual_spaces)\n", + "env₀, = leading_boundary(CTMRGEnv(peps₀, V_env), peps₀; boundary_alg...);" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Finally, we can optimize the PEPS with respect to the XXZ Hamiltonian. Note that the\n", + "optimization might take a while since precompilation of symmetric AD code takes longer and\n", + "because symmetric tensors do create a bit of overhead (which does pay off at larger bond\n", + "and environment dimensions):" + ], + "metadata": {} + }, + { + "outputs": [], + "cell_type": "code", + "source": [ + "peps, env, E, info = fixedpoint(\n", + " H, peps₀, env₀; boundary_alg, gradient_alg, optimizer_alg, verbosity=3\n", + ")\n", + "@show E;" + ], + "metadata": {}, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Note that for the specified parameters $J = \\Delta = 1$, we simulated the same Hamiltonian as\n", + "in the Heisenberg example. In that example, with a non-symmetric\n", + "$D=2$ PEPS simulation, we reached a ground-state energy of around $E_\\text{D=2} = -0.6625\\dots$.\n", + "Again comparing against [Sandvik's](@cite sandvik_computational_2011) accurate QMC estimate\n", + "$E_{\\text{ref}}=−0.6694421$, we see that we already got closer to the reference energy." + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "---\n", + "\n", + "*This notebook was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*" + ], + "metadata": {} + } + ], + "nbformat_minor": 3, + "metadata": { + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.11.4" + }, + "kernelspec": { + "name": "julia-1.11", + "display_name": "Julia 1.11.4", + "language": "julia" + } + }, + "nbformat": 4 +} diff --git a/docs/src/index.md b/docs/src/index.md index e6a6face6..994995ea1 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -12,32 +12,53 @@ The package can be installed through the Julia general registry, via the package pkg> add PEPSKit ``` +## Key features + +- Construction and manipulation of infinite projected entangled-pair states (PEPS) +- Contraction of infinite PEPS using the corner transfer matrix renormalization group (CTMRG) and boundary MPS methods +- Native support for symmetric tensors through [TensorKit](https://github.com/Jutho/TensorKit.jl), including fermionic tensors +- PEPS optimization using automatic differentiation (AD) provided through [Zygote](https://fluxml.ai/Zygote.jl/stable/) +- Imaginary time evolution algorithms +- Support for PEPS with generic unit cells +- Support for classical 2D partition functions and projected entangled-pair operators (PEPOs) +- Extensible system for custom states, operators and algorithms + ## Quickstart After following the installation process, it should now be possible to load the packages and start simulating. -For example, in order to obtain the groundstate of the 2D Heisenberg model, we can use the following code: +For example, in order to obtain the ground state of the 2D Heisenberg model, we can use the following code: ```julia -using TensorKit, PEPSKit, KrylovKit, OptimKit +using TensorKit, PEPSKit -# constructing the Hamiltonian: -H = heisenberg_XYZ(InfiniteSquare(); Jx=-1, Jy=1, Jz=-1) # sublattice rotation to obtain single-site unit cell +# construct the Hamiltonian +H = heisenberg_XYZ(InfiniteSquare()) -# configuring the parameters +# configure the parameters D = 2 -chi = 20 -ctm_alg = SimultaneousCTMRG(; tol=1e-10, trscheme=truncdim(chi)) -opt_alg = PEPSOptimize(; - boundary_alg=ctm_alg, - optimizer_alg=LBFGS(4; maxiter=100, gradtol=1e-4, verbosity=3), - gradient_alg=LinSolver(), - reuse_env=true, -) +χ = 20 +ctmrg_tol = 1e-10 +grad_tol = 1e-4 + +# initialize a PEPS and CTMRG environment +peps₀ = InfinitePEPS(2, D) +env₀, = leading_boundary(CTMRGEnv(peps₀, ComplexSpace(χ)), peps₀; tol=ctmrg_tol) # ground state search -state = InfinitePEPS(2, D) -ctm, = leading_boundary(CTMRGEnv(state, ComplexSpace(chi)), state, ctm_alg) -peps, env, E, = fixedpoint(H, state, ctm, opt_alg) +peps, env, E, = fixedpoint(H, peps₀, env₀; tol=grad_tol, boundary_alg=(; tol=ctmrg_tol)) @show E # -0.6625... ``` + +For a more in-depth explanation of this simple example, check the [Optimizing the 2D Heisenberg model](@ref examples_heisenberg) tutorial or consult the Manual pages. + +## Table of contents + +A detailed rundown of PEPSKit's features can be found in the Manual section (not yet complete, more coming soon™), including: + +```@contents +Pages = ["man/models.md", "man/multithreading.md", "man/precompilation.md"] +Depth = 1 +``` + +Additionally, we provide a list of commented examples in the [Examples section](@ref e_overview) which showcases most of PEPSKit's capabilities in action. diff --git a/docs/src/man/environments.md b/docs/src/man/environments.md new file mode 100644 index 000000000..485cd75e4 --- /dev/null +++ b/docs/src/man/environments.md @@ -0,0 +1,4 @@ +# Environments + +!!! note + This section is still under construction. \ No newline at end of file diff --git a/docs/src/man/intro.md b/docs/src/man/intro.md deleted file mode 100644 index fcc39fe6d..000000000 --- a/docs/src/man/intro.md +++ /dev/null @@ -1 +0,0 @@ -Coming soon. \ No newline at end of file diff --git a/docs/src/man/models.md b/docs/src/man/models.md new file mode 100644 index 000000000..465ceb2c1 --- /dev/null +++ b/docs/src/man/models.md @@ -0,0 +1,64 @@ +# Models + +PEPSKit implements physical models through the [MPSKitModels.jl](https://quantumkithub.github.io/MPSKitModels.jl/dev/) package as [`PEPSKit.LocalOperator`](@ref) structs. +Here, we want to explain how users can define their own Hamiltonians and provide a list of +already implemented models. + +## Implementing custom models + +In order to define custom Hamiltonians, we leverage several of the useful tools provided in MPSKitModels. +In particular, we use many of the pre-defined [operators](@extref MPSKitModels Operators), which is especially useful when defining models with symmetric and fermionic tensors, since most of these operators can take a symmetry as an argument, returning the appropriate symmetric `TensorMap`. +In order to specify the lattice on which the Hamiltonian is defined, we construct two-dimensional lattices as subtypes of [`MPSKitModels.AbstractLattice`](@extref). +Note that so far, all models are defined on infinite square lattices, see [`InfiniteSquare`](@ref), but in the future, we plan to support other lattice geometries as well. +In order to specify tensors acting on particular lattice sites, there are a couple of handy methods that we want to point to: see `vertices`, `nearest_neighbors` and `next_nearest_neighbors` defined [here](https://github.com/QuantumKitHub/PEPSKit.jl/blob/master/src/operators/lattices/squarelattice.jl). + +For a simple example on how to implement a custom model, let's look at the implementation of the [`MPSKitModels.transverse_field_ising`](@extref) model: + +```julia +function MPSKitModels.transverse_field_ising( + T::Type{<:Number}, + S::Union{Type{Trivial},Type{Z2Irrep}}, + lattice::InfiniteSquare; + J=1.0, + g=1.0, +) + ZZ = rmul!(σᶻᶻ(T, S), -J) + X = rmul!(σˣ(T, S), g * -J) + spaces = fill(domain(X)[1], (lattice.Nrows, lattice.Ncols)) + return LocalOperator( + spaces, + (neighbor => ZZ for neighbor in nearest_neighbours(lattice))..., + ((idx,) => X for idx in vertices(lattice))..., + ) +end +``` + +This provides a good recipe for defining a model: + +1. Define the locally-acting tensors as `TensorMap`s. +2. Construct a matrix of the physical spaces these `TensorMap`s act on based on the lattice geometry. +3. Return a `LocalOperator` where we specify on which sites (e.g. on-site, nearest neighbor, etc.) the local tensors act. + +For more model implementations, check the [PEPSKit repository](https://github.com/QuantumKitHub/PEPSKit.jl/blob/master/src/operators/models.jl). + +## Implemented models + +While PEPSKit provides an interface for specifying custom Hamiltonians, it also provides a number of [pre-defined models](https://github.com/QuantumKitHub/PEPSKit.jl/blob/master/src/operators/models.jl). Some of these are models already defined in [MPSKitModels](@extref MPSKitModels Models), which are overloaded for two-dimensional lattices and re-exported, but there are new additions as well. The following models are provided: + +### MPSKitModels.jl models + +```@docs +MPSKitModels.transverse_field_ising +MPSKitModels.heisenberg_XYZ +MPSKitModels.heisenberg_XXZ +MPSKitModels.hubbard_model +MPSKitModels.bose_hubbard_model +MPSKitModels.tj_model +``` + +### PEPSKit.jl models + +```@docs +j1_j2_model +pwave_superconductor +``` diff --git a/docs/src/man/multithreading.md b/docs/src/man/multithreading.md new file mode 100644 index 000000000..4258daa11 --- /dev/null +++ b/docs/src/man/multithreading.md @@ -0,0 +1,20 @@ +# Multithreading + +Before detailing the multithreading capabilities of PEPSKit, there are some general remarks to be made about parallelism in Julia. +In particular, it is important to know the interaction of Julia threads and BLAS threads, and that the BLAS thread behavior is inconsistent among different vendors. +Since these details have been explained many times elsewhere, we here want to point towards the [MPSKit docs](https://quantumkithub.github.io/MPSKit.jl/stable/man/parallelism/), which provide a good rundown of the threading behavior and what to be aware of. + +PEPSKit's multithreading features are provided through [OhMyThreads.jl](https://juliafolds2.github.io/OhMyThreads.jl/stable/). +In addition, we also supply a differentiable parallel map, which parallelizes not only the forward pass but also the reverse pass of the map application, see [`PEPSKit.dtmap`](@ref). +The threading behaviour can be specified through a global `scheduler` that is interfaced through the [`set_scheduler!`](@ref) function: + +```@docs +set_scheduler! +``` + +By default, the OhMyThreads machinery will be used to parallelize certain parts of the code, if Julia started with multiple threads. +Cases where PEPSKit can leverage parallel threads are: + +- CTMRG steps using the `:simultaneous` scheme, where we parallelize over all unit cell coordinates and spatial directions +- The reverse pass of these parallelized CTMRG steps +- Evaluating expectation values of observables, where we parallelize over the terms contained in the [`LocalOperator`](@ref PEPSKit.LocalOperator) diff --git a/docs/src/man/operators.md b/docs/src/man/operators.md new file mode 100644 index 000000000..0098ec494 --- /dev/null +++ b/docs/src/man/operators.md @@ -0,0 +1,4 @@ +# Operators + +!!! note + This section is still under construction. diff --git a/docs/src/man/peps_optimization.md b/docs/src/man/peps_optimization.md new file mode 100644 index 000000000..78511bb76 --- /dev/null +++ b/docs/src/man/peps_optimization.md @@ -0,0 +1,4 @@ +# PEPS optimization + +!!! note + This section is still under construction. \ No newline at end of file diff --git a/docs/src/man/precompilation.md b/docs/src/man/precompilation.md new file mode 100644 index 000000000..7ce7cc62f --- /dev/null +++ b/docs/src/man/precompilation.md @@ -0,0 +1,115 @@ +# Precompilation using PrecompileTools.jl + +For certain PEPSKit applications, the "time to first execution" (TTFX) can be quite long. +If frequent recompilation is required this can become a significant time sink. +Especially in simulations involving AD code, the precompilation times of Zygote tend to be particularly bad. + +Fortunately, there is an easy way out using [PrecompileTools](https://julialang.github.io/PrecompileTools.jl/stable/). +By writing a precompilation script that executes and precompiles a toy problem which is suited to one's personal problem, one can cut down significantly on the TTFX. +To see how that works in the context of PEPSKit, we will closely follow the PrecompileTools [docs](https://julialang.github.io/PrecompileTools.jl/stable/#Tutorial:-local-%22Startup%22-packages). + +Let's say we have a project where we want to speed up the TTFX, located in a project environment called `YourProject`. +Inside that project folder, we generate a `Startup` module which will contain the toy problem that we want to precompile: + +``` +(YourProject) pkg> generate Startup + Generating project Startup: + Startup/Project.toml + Startup/src/Startup.jl + +(YourProject) pkg> dev ./Startup + Resolving package versions... + Updating `/YourProject/Project.toml` + [e9c42744] + Startup v0.1.0 `Startup` + Updating `/tmp/Project1/Manifest.toml` + [e9c42744] + Startup v0.1.0 `Startup` + +(YourProject) pkg> activate Startup/ + Activating project at `/YourProject/Startup` + +(Startup) pkg> add PrecompileTools YourPackages... +``` + +The `Startup` module should depend on `PrecompileTools` as well as all the packages (`YourPackages...`) that are required to run the precompilation toy problem. +Next, we edit the `Startup/src/Startup.jl` file and add to it all the code which we want PrecompileTools to compile. +We will here provide a basic example featuring Zygote AD code on various algorithmic combinations: + +```julia +module Startup + +using Random +using TensorKit, KrylovKit, OptimKit +using ChainRulesCore, Zygote +using MPSKit, MPSKitModels +using PEPSKit +using PrecompileTools + +@setup_workload begin + t₀ = time_ns() + Random.seed!(20918352394) + + # Hyperparameters + Dbond = 2 + χenv = 4 + gradtol = 1e-3 + maxiter = 4 + verbosity = -1 + H = heisenberg_XYZ(InfiniteSquare()) + + # Algorithmic settings + ctmrg_algs = [ + SimultaneousCTMRG(; maxiter, projector_alg=:halfinfinite, verbosity), + SequentialCTMRG(; maxiter, projector_alg=:halfinfinite, verbosity), + ] + gradient_algs = [ + LinSolver(; solver_alg=BiCGStab(; tol=gradtol), iterscheme=:fixed), + LinSolver(; solver_alg=BiCGStab(; tol=gradtol), iterscheme=:diffgauge), + EigSolver(; solver_alg=Arnoldi(; tol=gradtol, eager=true), iterscheme=:fixed), + ] + + # Initialize OhMyThreads scheduler (precompilation occurs before __init__ call) + set_scheduler!() + + @compile_workload begin + # Initialize PEPS and environments with different unit cells, number types and symmetries + @info "Precompiling workload: initializing PEPSs and environments" + peps = InfinitePEPS(randn, ComplexF64, ComplexSpace(Dbond), ComplexSpace(Dbond)) + + env, = leading_boundary(CTMRGEnv(peps, ComplexSpace(χenv)), peps; verbosity) + + # CTMRG + @info "Precompiling workload: CTMRG leading_boundary" + for ctmrg_alg in ctmrg_algs + leading_boundary(env, peps, ctmrg_alg) + end + + # Differentiate CTMRG leading_boundary + @info "Precompiling workload: backpropagation of leading_boundary" + for alg_rrule in gradient_algs + Zygote.withgradient(peps) do ψ + env′, = PEPSKit.hook_pullback( + leading_boundary, env, ψ, SimultaneousCTMRG(; verbosity); alg_rrule + ) + return cost_function(ψ, env′, H) + end + end + + # Optimize via fixedpoint using LBFGS + @info "Precompiling workload: LBFGS fixedpoint optimization" + fixedpoint(H, peps, env, opt_alg; tol=gradtol, maxiter, verbosity) + + # Compute correlation length + @info "Precompiling workload: correlation_length" + correlation_length(peps, env) + end + + duration = round((time_ns() - t₀) * 1e-9 / 60; digits=2) # minutes + @info "Precompiling workload: finished after $duration min" +end + +end +``` + +Finally, activate `YourProject` again - where we want to benefit from the shortened execution times - and run `using Startup`. +That way, all packages will be loaded with their precompiled code. +Of course, we may also have multiple start-up routines where the precompiled code is tailored towards the needs of the respective projects. diff --git a/docs/src/man/states.md b/docs/src/man/states.md new file mode 100644 index 000000000..e062e0f53 --- /dev/null +++ b/docs/src/man/states.md @@ -0,0 +1,4 @@ +# States + +!!! note + This section is still under construction. diff --git a/docs/src/man/symmetries.md b/docs/src/man/symmetries.md new file mode 100644 index 000000000..fdafecc93 --- /dev/null +++ b/docs/src/man/symmetries.md @@ -0,0 +1,4 @@ +# Symmetric and fermionic tensors + +!!! note + This section is still under construction. \ No newline at end of file diff --git a/docs/src/references.md b/docs/src/references.md new file mode 100644 index 000000000..da17eb062 --- /dev/null +++ b/docs/src/references.md @@ -0,0 +1,5 @@ +# References + +```@bibliography +* +``` diff --git a/examples/2d_ising_partition_function/main.jl b/examples/2d_ising_partition_function/main.jl new file mode 100644 index 000000000..2c3726fd0 --- /dev/null +++ b/examples/2d_ising_partition_function/main.jl @@ -0,0 +1,155 @@ +using Markdown #hide +md""" +# [The 2D classical Ising model using CTMRG](@id e_2d_ising) + +While PEPSKit has a lot of use in quantum systems, describing states using InfinitePEPS that can be contracted via CTMRG or [boundary MPS techniques](@ref e_boundary_mps), here we shift our focus to classical physics. +We consider the 2D classical Ising model and compute its partition function defined as: + +```math +\mathcal{Z}(\beta) = \sum_{\{s\}} \exp(-\beta H(s)) \text{ with } H(s) = -J \sum_{\langle i, j \rangle} s_i s_j . +``` + +where the classical spins $s_i \in \{+1, -1\}$ are located on the vertices $i$ of a 2D +square lattice. The idea is to encode the partition function as an infinite square network +consisting of local rank-4 tensors, which can then be contracted using CTMRG. An infinite +square network of these rank-4 tensors can be represented as an +[`InfinitePartitionFunction`](@ref) object, as we will see. + +But first, let's seed the RNG and import all required modules: +""" + +using Random, LinearAlgebra +using TensorKit, PEPSKit +using QuadGK +Random.seed!(234923); + +md""" +## Defining the partition function + +The first step is to define the rank-4 tensor that, when contracted on a square lattice, +evaluates to the partition function value at a given $\beta$. This is done through a +[fairly generic procedure](@cite haegeman_diagonalizing_2017) where the interaction weights +are distributed among vertex tensors in an appropriate way. Concretely, here we first define +a 'link' matrix containing the Boltzmann weights associated to all possible spin +configurations across a given link on the lattice. Next, we define site tensors as +delta-tensors that ensiure that the spin value on all adjacent links is the same. Since we +only want tensors on the sites in the end, we can symmetrically absorb the link weight +tensors into the site tensors, which gives us exactly the kind of network we're looking for. +Since we later want to compute the magnetization and energy to check our results, we define +the appropriate rank-4 tensors here as well while we're at it. +""" + +function classical_ising(; beta=log(1 + sqrt(2)) / 2, J=1.0) + K = beta * J + + ## Boltzmann weights + t = ComplexF64[exp(K) exp(-K); exp(-K) exp(K)] + r = eigen(t) + nt = r.vectors * sqrt(Diagonal(r.values)) * r.vectors + + ## local partition function tensor + O = zeros(2, 2, 2, 2) + O[1, 1, 1, 1] = 1 + O[2, 2, 2, 2] = 1 + @tensor o[-1 -2; -3 -4] := O[3 4; 2 1] * nt[-3; 3] * nt[-4; 4] * nt[-2; 2] * nt[-1; 1] + + ## magnetization tensor + M = copy(O) + M[2, 2, 2, 2] *= -1 + @tensor m[-1 -2; -3 -4] := M[1 2; 3 4] * nt[-1; 1] * nt[-2; 2] * nt[-3; 3] * nt[-4; 4] + + ## bond interaction tensor and energy-per-site tensor + e = ComplexF64[-J J; J -J] .* nt + @tensor e_hor[-1 -2; -3 -4] := + O[1 2; 3 4] * nt[-1; 1] * nt[-2; 2] * nt[-3; 3] * e[-4; 4] + @tensor e_vert[-1 -2; -3 -4] := + O[1 2; 3 4] * nt[-1; 1] * nt[-2; 2] * e[-3; 3] * nt[-4; 4] + e = e_hor + e_vert + + ## fixed tensor map space for all three + TMS = ℂ^2 ⊗ ℂ^2 ← ℂ^2 ⊗ ℂ^2 + + return TensorMap(o, TMS), TensorMap(m, TMS), TensorMap(e, TMS) +end; + +md""" +So let's initialize these tensors at inverse temperature ``\beta=0.6``, check that +they are indeed rank-4 and construct the corresponding `InfinitePartitionFunction`: +""" + +beta = 0.6 +O, M, E = classical_ising(; beta) +@show space(O) +Z = InfinitePartitionFunction(O) + +md""" +## Contracting the partition function + +Next, we can contract the partition function as per usual by constructing a `CTMRGEnv` with +a specified environment virtual space and calling `leading_boundary` with appropriate +settings: +""" + +Venv = ℂ^20 +env₀ = CTMRGEnv(Z, Venv) +env, = leading_boundary(env₀, Z; tol=1e-8, maxiter=500); + +md""" +Note that CTMRG environments for partition functions differ from the PEPS environments only +by the edge tensors. Instead of two legs connecting the edges and the PEPS-PEPS sandwich, +there is only one leg connecting the edges and the partition function tensor, meaning that +the edge tensors are now rank-3: +""" + +space.(env.edges) + +md""" +To compute the value of the partition function, we have to contract `Z` with the converged +environment using [`network_value`](@ref). Additionally, we will compute the magnetization +and energy (per site), again using [`expectation_value`](@ref) but this time also specifying +the index in the unit cell where we want to insert the local tensor: +""" + +λ = network_value(Z, env) +m = expectation_value(Z, (1, 1) => M, env) +e = expectation_value(Z, (1, 1) => E, env) +@show λ m e; + +md""" +## Comparing against the exact Onsager solution + +In order to assess our results, we will compare against the +[exact Onsager solution](https://en.wikipedia.org/wiki/Square_lattice_Ising_model#Exact_solution) +of the 2D classical Ising model. To that end, we compute the exact free energy, +magnetization and energy per site (where we use `quadgk` to perform integrals of an +auxiliary variable from $0$ to $\pi/2$): +""" + +function classical_ising_exact(; beta=log(1 + sqrt(2)) / 2, J=1.0) + K = beta * J + + k = 1 / sinh(2 * K)^2 + F = quadgk( + theta -> log(cosh(2 * K)^2 + 1 / k * sqrt(1 + k^2 - 2 * k * cos(2 * theta))), 0, pi + )[1] + f = -1 / beta * (log(2) / 2 + 1 / (2 * pi) * F) + + m = 1 - (sinh(2 * K))^(-4) > 0 ? (1 - (sinh(2 * K))^(-4))^(1 / 8) : 0 + + E = quadgk(theta -> 1 / sqrt(1 - (4 * k) * (1 + k)^(-2) * sin(theta)^2), 0, pi / 2)[1] + e = -J * cosh(2 * K) / sinh(2 * K) * (1 + 2 / pi * (2 * tanh(2 * K)^2 - 1) * E) + + return f, m, e +end + +f_exact, m_exact, e_exact = classical_ising_exact(; beta); + +md""" +And indeed, we do find agreement between the exact and CTMRG values (keeping in mind that +energy accuracy is limited by the environment dimension and the lack of proper +extrapolation): +""" + +@show (-log(λ) / beta - f_exact) / f_exact +@show (abs(m) - abs(m_exact)) / abs(m_exact) +@show (e - e_exact) / e_exact; diff --git a/examples/3d_ising_partition_function/main.jl b/examples/3d_ising_partition_function/main.jl new file mode 100644 index 000000000..98c3e9b23 --- /dev/null +++ b/examples/3d_ising_partition_function/main.jl @@ -0,0 +1,290 @@ +using Markdown #hide +md""" +# [The 3D classical Ising model](@id e_3d_ising) + +In this example, we will showcase how one can use PEPSKit to study 3D classical statistical +mechanics models. In particular, we will consider a specific case of the 3D classical Ising +model, but the same techniques can be applied to other 3D classical models as well. + +As compared to simulations of [2D partition functions](@ref e_2d_ising), the workflow +presented in this example is a bit more experimental and less 'black-box'. Therefore, it +also serves as a demonstration of some of the more internal functionality of PEPSKit, +and how one can adapt it to less 'standard' kinds of problems. + +Let us consider the partition function of the classical Ising model, + +```math +\mathcal{Z}(\beta) = \sum_{\{s\}} \exp(-\beta H(s)) \text{ with } H(s) = -J \sum_{\langle i, j \rangle} s_i s_j . +``` + +where the classical spins $s_i \in \{+1, -1\}$ are located on the vertices $i$ of a 3D +cubic lattice. The partition function of this model can be represented as a 3D tensor +network with a rank-6 tensor at each vertex of the lattice. Such a network can be contracted +by finding the fixed point of the corresponding transfer operator, in exactly the same +spirit as the [boundary MPS methods](@ref e_boundary_mps) demonstrated in another example. + +Let's start by making the example deterministic and importing the required packages: +""" + +using Random +using LinearAlgebra +using PEPSKit, TensorKit +using KrylovKit, OptimKit, Zygote + +Random.seed!(81812781144); + +md""" +## Defining the partition function + +Just as in the 2D case, the first step is to define the partition function as a tensor +network. The procedure is exactly the same as before, the only difference being that now +every spin participates in interactions associated to six links adjacent to that site. This +means that the partition function can be written as an infinite 3D network with a single +constituent rank-6 [`PEPSKit.PEPOTensor`](@ref) `O` located at each site of the cubic +lattice. To verify our example we will check the magnetization and energy, so we also define +the corresponding rank-6 tensors `M` and `E` while we're at it. +""" + +function three_dimensional_classical_ising(; beta, J=1.0) + K = beta * J + + ## Boltzmann weights + t = ComplexF64[exp(K) exp(-K); exp(-K) exp(K)] + r = eigen(t) + q = r.vectors * sqrt(LinearAlgebra.Diagonal(r.values)) * r.vectors + + ## local partition function tensor + O = zeros(2, 2, 2, 2, 2, 2) + O[1, 1, 1, 1, 1, 1] = 1 + O[2, 2, 2, 2, 2, 2] = 1 + @tensor o[-1 -2; -3 -4 -5 -6] := + O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6] + + ## magnetization tensor + M = copy(O) + M[2, 2, 2, 2, 2, 2] *= -1 + @tensor m[-1 -2; -3 -4 -5 -6] := + M[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6] + + ## bond interaction tensor and energy-per-site tensor + e = ComplexF64[-J J; J -J] .* q + @tensor e_x[-1 -2; -3 -4 -5 -6] := + O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * e[-4; 4] * q[-5; 5] * q[-6; 6] + @tensor e_y[-1 -2; -3 -4 -5 -6] := + O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * e[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6] + @tensor e_z[-1 -2; -3 -4 -5 -6] := + O[1 2; 3 4 5 6] * e[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6] + e = e_x + e_y + e_z + + ## fixed tensor map space for all three + TMS = ℂ^2 ⊗ (ℂ^2)' ← ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)' + + return TensorMap(o, TMS), TensorMap(m, TMS), TensorMap(e, TMS) +end; + +md""" +Let's initialize these tensors at inverse temperature ``\beta=0.2391``, which corresponds to +a slightly lower temperature than the critical value ``\beta_c=0.2216544…`` +""" + +beta = 0.2391 +O, M, E = three_dimensional_classical_ising(; beta) +O isa PEPSKit.PEPOTensor + +md""" +## Contracting the partition function + +To contract our infinite 3D partition function, we first reinterpret it as an infinite power +of a slice-to-slice transfer operator ``T``, where ``T`` can be seen as an infinite 2D +projected entangled-pair operator (PEPO) which consists of the rank-6 tensor `O` at each +site of an infinite 2D square lattice. In the same spirit as the boundary MPS approach, all +we need to contract the whole partition function is to find the leading eigenvector of this +PEPO. The fixed point of such a PEPO can be parametrized as a PEPS, and for the case of a +Hermitian transfer operator we can find this PEPS through [variational optimization](@cite +vanderstraeten_residual_2018). + +Indeed, for a Hermitian transfer operator ``T`` we can characterize the fixed point PEPS +``|\psi\rangle`` which satisfies the eigenvalue equation +``T |\psi\rangle = \Lambda |\psi\rangle`` corresponding to the largest magnitude eigenvalue +``\Lambda`` as the solution of a variational problem + +```math +|\psi\rangle = \text{argmin}_{|\psi\rangle} \left ( \lim_{N \to ∞} - \frac{1}{N} \log \left( \frac{\langle \psi | T | \psi \rangle}{\langle \psi | \psi \rangle} \right) \right ) , +``` + +where ``N`` is the diverging number of sites of the 2D transfer operator ``T``. The function +minimized in this expression is exactly the free energy per site of the partition function, +so we essentially find the fixed-point PEPS by variationally minimizing the free energy. + +### Defining the cost function + +Using PEPSKit.jl, this cost function and its gradient can be computed, after which we can +use [OptimKit.jl](https://github.com/Jutho/OptimKit.jl) to actually optimize it. We can +immediately recognize the denominator ``\langle \psi | \psi \rangle`` as the familiar PEPS +norm, where we can compute the norm per site as the [`network_value`](@ref) of the +corresponding [`InfiniteSquareNetwork`](@ref) by contracting it with the CTMRG algorithm. +Similarly, the numerator ``\langle \psi | T | \psi \rangle`` is nothing more than an +`InfiniteSquareNetwork` consisting of three layers corresponding to the ket, transfer +operator and bra objects. This object can also be constructed and contracted in a +straightforward way, so we can again compute its `network_value`. + +To define our cost function, we then need to construct the transfer operator as an +[`InfinitePEPO`](@ref), construct the two infinite 2D contractible networks for the +numerator and denominator from the current PEPS and this transfer operator, and specify a +contraction algorithm we can use to compute the values of these two networks. In addition, +we'll specify the specific reverse rule algorithm that will be used to compute the gradient +of this cost function. +""" + +boundary_alg = SimultaneousCTMRG(; maxiter=150, tol=1e-8, verbosity=1) +rrule_alg = EigSolver(; + solver_alg=KrylovKit.Arnoldi(; maxiter=30, tol=1e-6, eager=true), iterscheme=:diffgauge +) +T = InfinitePEPO(O) + +function pepo_costfun((peps, env_double_layer, env_triple_layer)) + ## use Zygote to compute the gradient automatically + E, gs = withgradient(peps) do ψ + ## construct the PEPS norm network + n_double_layer = InfiniteSquareNetwork(ψ) + ## contract this network + env_double_layer′, info = PEPSKit.hook_pullback( + leading_boundary, + env_double_layer, + n_double_layer, + boundary_alg; + alg_rrule=rrule_alg, + ) + ## construct the PEPS-PEPO-PEPS overlap network + n_triple_layer = InfiniteSquareNetwork(ψ, T) + ## contract this network + env_triple_layer′, info = PEPSKit.hook_pullback( + leading_boundary, + env_triple_layer, + n_triple_layer, + boundary_alg; + alg_rrule=rrule_alg, + ) + ## update the environments for reuse + PEPSKit.ignore_derivatives() do + PEPSKit.update!(env_double_layer, env_double_layer′) + PEPSKit.update!(env_triple_layer, env_triple_layer′) + end + ## compute the network values per site + λ3 = network_value(n_triple_layer, env_triple_layer) + λ2 = network_value(n_double_layer, env_double_layer) + ## use this to compute the actual cost function + return -log(real(λ3 / λ2)) + end + g = only(gs) + return E, g +end; + +md""" +There are a few things to note about this cost function definition. Since we will pass it to +the `OptimKit.optimize`, we require it to return both our cost function and the +corresponding gradient. To do this, we simply use the `withgradient` method from Zygote.jl +to automatically compute the gradient of the cost function straight from the primal +computation. Since our cost function involves contractions using `leading_boundary`, we also +have to specify exactly how Zygote should handle the backpropagation of the gradient through +this function. This can be done using the [`PEPSKit.hook_pullback`](@ref) function from +PEPSKit.jl, which allows to hook into the pullback of a given function by specifying a +specific algorithm for the pullback computation. Here, we opted to use an Arnoldi method to +solve the linear problem defining the gradient of the network contraction at its fixed +point. This is exactly the workflow that internally underlies [`PEPSKit.fixedpoint`](@ref), and +more info on particular gradient algorithms can be found in the corresponding docstrings. + +### Characterizing the optimization manifold + +In order to make the best use of OptimKit.jl, we should specify some properties of the +manifold on which we are optimizing. Looking at our cost function defined above, a point on +our optimization manifold corresponds to a `Tuple` of three objects. The first is an +`InfinitePEPS` encoding the fixed point we are actually optimizing, while the second and +third are `CTMRGEnv` objects corresponding to the environments of the double and triple +layer networks ``\langle \psi | \psi \rangle`` and ``\langle \psi | T | \psi \rangle`` +respectively. While the environments are just there so we can reuse them between subsequent +contractions and we don't need to think about them much, optimizing over the manifold of +`InfinitePEPS` requires a bit more care. + +In particular, we need to define two kinds of operations on this manifold: a retraction and +a transport. The retraction, corresponding to the `retract` keyword argument of +`OptimKit.optimize`, specifies how to move from a point on a manifold along a given descent +direction to obtain a new manifold point. The transport, corresponding to the `transport!` +keyword argument of `OptimKit.optimize`, specifies how to transport a descent direction at a +given manifold point to a valid descent direction at a different manifold point according to +the appropriate metric. For a more detailed explanation we refer to the +[OptimKit.jl README](https://github.com/Jutho/OptimKit.jl). In PEPSKit.jl, these two +procedures are defined through the [`PEPSKit.peps_retract`](@ref) and +[`PEPSKit.peps_transport!`](@ref) methods. While it is instructive to read the corresponding +docstrings in order to understand what these actually do, here we can just blindly reuse +them where the only difference is that we have to pass along an extra environment since our +cost function requires two distinct contractions as opposed to the setting of Hamiltonian +PEPS optimization which only requires a double-layer contraction. +""" + +function pepo_retract((peps, env_double_layer, env_triple_layer), η, α) + (peps´, env_double_layer´), ξ = PEPSKit.peps_retract((peps, env_double_layer), η, α) + env_triple_layer´ = deepcopy(env_triple_layer) + return (peps´, env_double_layer´, env_triple_layer´), ξ +end +function pepo_transport!( + ξ, + (peps, env_double_layer, env_triple_layer), + η, + α, + (peps´, env_double_layer´, env_triple_layer´), +) + return PEPSKit.peps_transport!( + ξ, (peps, env_double_layer), η, α, (peps´, env_double_layer´) + ) +end; + +md""" +### Finding the fixed point + +All that is left then is to specify the virtual spaces of the PEPS and the two environments, +initialize them in the appropriate way, choose an optimization algortithm and call the +`optimize` function from OptimKit.jl to get our desired PEPS fixed point. +""" + +Vpeps = ℂ^2 +Venv = ℂ^12 + +psi0 = initializePEPS(T, Vpeps) +env2_0 = CTMRGEnv(InfiniteSquareNetwork(psi0), Venv) +env3_0 = CTMRGEnv(InfiniteSquareNetwork(psi0, T), Venv) + +optimizer_alg = LBFGS(32; maxiter=100, gradtol=1e-5, verbosity=3) + +(psi_final, env2_final, env3_final), f, = optimize( + pepo_costfun, + (psi0, env2_0, env3_0), + optimizer_alg; + inner=PEPSKit.real_inner, + retract=pepo_retract, + (transport!)=(pepo_transport!), +); + +md""" +### Verifying the result + +Having found the fixed point, we have essentially contracted the entire partition function +and we can start computing observables. The free energy per site for example is just given by +the final value of the cost function we have just optimized. +""" + +@show f + +md""" +As another check, we can compute the magnetization per site and compare it to a [reference +value obtaind through Monte-Carlo simulations](@cite hasenbusch_monte_2001). +""" + +n3_final = InfiniteSquareNetwork(psi_final, T) +num = PEPSKit.contract_local_tensor((1, 1, 1), M, n3_final, env3_final) +denom = PEPSKit._contract_site((1, 1), n3_final, env3_final) +m = abs(num / denom) + +m_ref = 0.667162 + +@show abs(m - m_ref) diff --git a/examples/Cache.toml b/examples/Cache.toml new file mode 100644 index 000000000..ec730ded2 --- /dev/null +++ b/examples/Cache.toml @@ -0,0 +1,9 @@ +bose_hubbard = "66660dd5a6ab5cfe35f2d1cffde64a7978a3c5b154f0bd33a3fc37e4f18da4d1" +hubbard_su = "41a3a544f478d1f347e6c0930c7b00d8168bb8f833a4b3947387b5c1b0b5f3c5" +2d_ising_partition_function = "40cacb2e0805f7ca6207a84b3272b5924809d13bed1533fb373b5fa41a566b63" +3d_ising_partition_function = "2f129d89e82f7e0019000e609bef4f6d0e9089880e357d9196e387ff2f81f243" +boundary_mps = "31e14bd742c12acfc4facd5bc8f28752f025a1a523c72ffccc8581e38880d3cd" +heisenberg_su = "299b79f36b94b301dafa0d39fc54498b44dcdf0e4988e3c5416aa6c4e9e8f584" +xxz = "b5ab0efe6b9addb4b029250d7212d8b2e771136ca4ec4a4bfd7bccc8c688b949" +fermi_hubbard = "13d6d827a9851bef2c47cbe8894bf6eec9e1fb3222ecc24d2a876336553439ff" +heisenberg = "f6b057dacb058656ad903778fc00832c6c8a61df930118872e04006b086030c7" diff --git a/examples/Project.toml b/examples/Project.toml new file mode 100644 index 000000000..b66b36c4a --- /dev/null +++ b/examples/Project.toml @@ -0,0 +1,13 @@ +[deps] +KrylovKit = "0b1a1467-8014-51b9-945f-bf0ae24f4b77" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +MPSKit = "bb1c41ca-d63c-52ed-829e-0820dda26502" +MPSKitModels = "ca635005-6f8c-4cd1-b51d-8491250ef2ab" +OptimKit = "77e91f04-9b3b-57a6-a776-40b61faaebe0" +PEPSKit = "52969e89-939e-4361-9b68-9bc7cde4bdeb" +QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..e09f72dc4 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,25 @@ +# Examples + +This folder contains the examples and tutorials of this package. The files can be run as +scripts and are embedded into the docs using [Literate.jl](https://fredrikekre.github.io/Literate.jl/v2/). + +## Building the documentation + +The example files have to be built and updated manually. In order to trigger the file +generation, run: + +```bash +julia examples/make.jl +``` + +By default, this will only generate files when the input file has not changed. This is +achieved by keeping a checksum of the `main.jl` file in each example in a `Cache.toml`. +Total recompilation can be achieved by deleting this file, or alternatively you can just +delete the entries for which you wish to generate new files. + +## Contributing + +Contributions are welcome! Please open an issue or a pull request if you have any questions +or suggestions. The examples should be placed in their own folder, where the `main.jl` file +serves as the entry point. Any other files will be copied over to the `docs/src/examples` +folder, so you can use this to include images or other files. diff --git a/examples/bose_hubbard.jl b/examples/bose_hubbard.jl deleted file mode 100644 index 941cbca55..000000000 --- a/examples/bose_hubbard.jl +++ /dev/null @@ -1,99 +0,0 @@ -using Test -using Random -using PEPSKit -using TensorKit -using KrylovKit -using OptimKit - -using MPSKit: add_physical_charge - -# This example demonstrates the simulation of the two-dimensional Bose-Hubbard model using -# PEPSKit.jl. In particular, it showcases the use of internal symmetries and finite particle -# densities in PEPS simulations. - -## Defining the model - -# We will construct the Bose-Hubbard model Hamiltonian through the -# [`bose_hubbard_model` function from MPSKitModels.jl](https://quantumkithub.github.io/MPSKitModels.jl/dev/man/models/#MPSKitModels.bose_hubbard_model), -# as reexported by PEPSKit.jl. We'll simulate the model in its Mott-insulating phase -# where the ratio U/t is large, since in this phase we expect the ground state to be well -# approximated by a PEPS with a manifest global U(1) symmetry. Furthermore, we'll impose -# a cutoff at 2 bosons per site, set the chemical potential to zero and use a simple 1x1 -# unit cell. - -t = 1.0 -U = 30.0 -cutoff = 2 -mu = 0.0 -lattice = InfiniteSquare(1, 1) - -# We'll impose an explicit global U(1) symmetry as well as a fixed particle number density -# in our simulations. We can do this by setting the `symmetry` keyword argument to `U1Irrep` -# and passing one as the particle number density keyword argument `n`. - -symmetry = U1Irrep -n = 1 - -# We can then construct the Hamiltonian, and inspect the corresponding lattice of physical -# spaces. - -H = bose_hubbard_model(ComplexF64, symmetry, lattice; cutoff, t, U, n) -Pspaces = H.lattice - -# Note that the physical space contains U(1) charges -1, 0 and +1. Indeed, imposing a -# particle number density of +1 corresponds to shifting the physical charges by -1 to -# 're-center' the physical charges around the desired density. When we do this with a cutoff -# of two bosons per site, i.e. starting from U(1) charges 0, 1 and 2 on the physical level, -# we indeed get the observed charges. - -## Characterizing the virtual spaces - -# When running PEPS simulations with explicit internal symmetries, specifying the structure -# of the virtual spaces of the PEPS and its environment becomes a bit more involved. For the -# environment, one could in principle allow the virtual space to be chosen dynamically -# during the boundary contraction using CTMRG by using a truncation scheme that allows for -# this (e.g. using alg=:truncdim or alg=:truncbelow to truncate to a fixed total bond -# dimension or singular value cutoff respectively). For the PEPS virtual space however, the -# structure has to be specified before the optimization. - -# While there are a host of techniques to do this in an informed way (e.g. starting from -# a simple update result), here we just specify the virtual space manually. Since we're -# dealing with a model at unit filling our physical space only contains integer U(1) irreps. -# Therefore, we'll build our PEPS and environment spaces using integer U(1) irreps centered -# around the zero charge. - -Vpeps = U1Space(0 => 2, 1 => 1, -1 => 1) -Venv = U1Space(0 => 6, 1 => 4, -1 => 4, 2 => 2, -2 => 2) - -## Finding the ground state - -# Having defined our Hamiltonian and spaces, it is just a matter of pluggin this into the -# optimization framework in the usual way to find the ground state. - -# specify algorithms and tolerances -boundary_alg = (; tol=1e-8, alg=:simultaneous, verbosity=2, trscheme=(; alg=:fixedspace)) -gradient_alg = (; tol=1e-6, maxiter=10, alg=:eigsolver, iterscheme=:diffgauge) -optimizer_alg = (; tol=1e-4, alg=:lbfgs, verbosity=3, maxiter=200, ls_maxiter=2, ls_maxfg=2) -reuse_env = true - -# initialize state -Nspaces = fill(Vpeps, size(lattice)...) -Espaces = fill(Vpeps, size(lattice)...) -Random.seed!(2928528935) # for reproducibility -ψ₀ = InfinitePEPS(randn, ComplexF64, Pspaces, Nspaces, Espaces) -env₀ = CTMRGEnv(ψ₀, Venv) -env₀, = leading_boundary(env₀, ψ₀; boundary_alg...) - -# optimize -ψ, env, E, info = fixedpoint( - H, ψ₀, env₀; boundary_alg, gradient_alg, optimizer_alg, reuse_env -) - -## Check the result - -# We can compare our PEPS result to the energy obtained using a cylinder-MPS calculation -# using a cylinder circumference of Ly = 7 and a bond dimension of 446, which yields -# E = -0.273284888 - -E_ref = -0.273284888 -@test E ≈ E_ref rtol = 1e-3 diff --git a/examples/bose_hubbard/main.jl b/examples/bose_hubbard/main.jl new file mode 100644 index 000000000..9e93cf362 --- /dev/null +++ b/examples/bose_hubbard/main.jl @@ -0,0 +1,132 @@ +using Markdown #hide +md""" +# Optimizing the $U(1)$-symmetric Bose-Hubbard model + +This example demonstrates the simulation of the two-dimensional Bose-Hubbard model. In +particular, the point will be to showcase the use of internal symmetries and finite +particle densities in PEPS ground state searches. As we will see, incorporating symmetries +into the simulation consists of initializing a symmetric Hamiltonian, PEPS state and CTM +environment - made possible through TensorKit. + +But first let's seed the RNG and import the required modules: +""" + +using Random +using TensorKit, PEPSKit +using MPSKit: add_physical_charge +Random.seed!(2928528935); + +md""" +## Defining the model + +We will construct the Bose-Hubbard model Hamiltonian through the +[`bose_hubbard_model`](https://quantumkithub.github.io/MPSKitModels.jl/dev/man/models/#MPSKitModels.bose_hubbard_model), +function from MPSKitModels as reexported by PEPSKit. We'll simulate the model in its +Mott-insulating phase where the ratio $U/t$ is large, since in this phase we expect the +ground state to be well approximated by a PEPS with a manifest global $U(1)$ symmetry. +Furthermore, we'll impose a cutoff at 2 bosons per site, set the chemical potential to zero +and use a simple $1 \times 1$ unit cell: +""" + +t = 1.0 +U = 30.0 +cutoff = 2 +mu = 0.0 +lattice = InfiniteSquare(1, 1); + +md""" +Next, we impose an explicit global $U(1)$ symmetry as well as a fixed particle number +density in our simulations. We can do this by setting the `symmetry` argument of the +Hamiltonian constructor to `U1Irrep` and passing one as the particle number density +keyword argument `n`: +""" + +symmetry = U1Irrep +n = 1 +H = bose_hubbard_model(ComplexF64, symmetry, lattice; cutoff, t, U, n); + +md""" +Before we continue, it might be interesting to inspect the corresponding lattice physical +spaces (which is here just a $1 \times 1$ matrix due to the single-site unit cell): +""" + +physical_spaces = physicalspace(H) + +md""" +Note that the physical space contains $U(1)$ charges -1, 0 and +1. Indeed, imposing a +particle number density of +1 corresponds to shifting the physical charges by -1 to +'re-center' the physical charges around the desired density. When we do this with a cutoff +of two bosons per site, i.e. starting from $U(1)$ charges 0, 1 and 2 on the physical level, +we indeed get the observed charges. + +## Characterizing the virtual spaces + +When running PEPS simulations with explicit internal symmetries, specifying the structure of +the virtual spaces of the PEPS and its environment becomes a bit more involved. For the +environment, one could in principle allow the virtual space to be chosen dynamically during +the boundary contraction using CTMRG by using a truncation scheme that allows for this +(e.g. using `alg=:truncdim` or `alg=:truncbelow` to truncate to a fixed total bond dimension +or singular value cutoff respectively). For the PEPS virtual space however, the structure +has to be specified before the optimization. + +While there are a host of techniques to do this in an informed way (e.g. starting from a +simple update result), here we just specify the virtual space manually. Since we're dealing +with a model at unit filling our physical space only contains integer $U(1)$ irreps. +Therefore, we'll build our PEPS and environment spaces using integer $U(1)$ irreps centered +around the zero charge: +""" + +V_peps = U1Space(0 => 2, 1 => 1, -1 => 1) +V_env = U1Space(0 => 6, 1 => 4, -1 => 4, 2 => 2, -2 => 2); + +md""" +## Finding the ground state + +Having defined our Hamiltonian and spaces, it is just a matter of plugging this into the +optimization framework in the usual way to find the ground state. So, we first specify all +algorithms and their tolerances: +""" + +boundary_alg = (; tol=1e-8, alg=:simultaneous, trscheme=(; alg=:fixedspace)) +gradient_alg = (; tol=1e-6, maxiter=10, alg=:eigsolver, iterscheme=:diffgauge) +optimizer_alg = (; tol=1e-4, alg=:lbfgs, maxiter=150, ls_maxiter=2, ls_maxfg=2); + +md""" +!!! note + Taking CTMRG gradients and optimizing symmetric tensors tends to be more problematic + than with dense tensors. In particular, this means that one frequently needs to tweak + the `boundary_alg`, `gradient_alg` and `optimizer_alg` settings. There rarely is a + general-purpose set of settings which will always work, so instead one has to adjust + the simulation settings for each specific application. For example, it might help to + switch between the CTMRG flavors `alg=:simultaneous` and `alg=:sequential` to + improve convergence. The evaluation of the CTMRG gradient can be instable, so there it + is advised to try the different `iterscheme=:diffgauge` and `iterscheme=:fixed` schemes + as well as different `alg` keywords. Of course the tolerances of the algorithms and + their subalgorithms also have to be compatible. For more details on the available + options, see the [`fixedpoint`](@ref) docstring. + +Keep in mind that the PEPS is constructed from a unit cell of spaces, so we have to make a +matrix of `V_peps` spaces: +""" + +virtual_spaces = fill(V_peps, size(lattice)...) +peps₀ = InfinitePEPS(randn, ComplexF64, physical_spaces, virtual_spaces) +env₀, = leading_boundary(CTMRGEnv(peps₀, V_env), peps₀; boundary_alg...); + +md""" +And at last, we optimize (which might take a bit): +""" + +peps, env, E, info = fixedpoint( + H, peps₀, env₀; boundary_alg, gradient_alg, optimizer_alg, verbosity=3 +) +@show E; + +md""" +We can compare our PEPS result to the energy obtained using a cylinder-MPS calculation +using a cylinder circumference of $L_y = 7$ and a bond dimension of 446, which yields +$E = -0.273284888$: +""" + +E_ref = -0.273284888 +@show (E - E_ref) / E_ref; diff --git a/examples/boundary_mps.jl b/examples/boundary_mps.jl deleted file mode 100644 index 68c1ccb0f..000000000 --- a/examples/boundary_mps.jl +++ /dev/null @@ -1,86 +0,0 @@ -using Random -using PEPSKit -using TensorKit -using MPSKit -using LinearAlgebra - -include("ising_pepo.jl") - -# This example demonstrates some boundary-MPS methods for working with 2D projected -# entangled-pair states and operators. - -## Computing a PEPS norm - -# We start by initializing a random initial infinite PEPS -Random.seed!(29384293742893) -peps = InfinitePEPS(ComplexSpace(2), ComplexSpace(2)) - -# To compute its norm, we start by constructing the transfer operator corresponding to -# the partition function representing the overlap -T = InfiniteTransferPEPS(peps, 1, 1) - -# We then find its leading boundary MPS fixed point, where the corresponding eigenvalue -# encodes the norm of the state - -# Fist we build an initial guess for the boundary MPS, choosing a bond dimension of 20 -mps = initialize_mps(T, [ComplexSpace(20)]) - -# We then find the leading boundary MPS fixed point using the VUMPS algorithm -mps, env, ϵ = leading_boundary(mps, T, VUMPS()) - -# The norm of the state per unit cell is then given by the expectation value -N = abs(prod(expectation_value(mps, T))) - -# This can be compared to the result obtained using the CTMRG algorithm -ctm, = leading_boundary( - peps, SimultaneousCTMRG(; verbosity=1), CTMRGEnv(peps, ComplexSpace(20)) -) -N´ = abs(norm(peps, ctm)) - -@show abs(N - N´) / N - -## Working with unit cells - -# For PEPS with non-trivial unit cells, the principle is exactly the same. -# The only difference is that now the transfer operator of the PEPS norm partition function -# has multiple lines, each of which can be represented by an `InfiniteTransferPEPS` object. -# Such a multi-line transfer operator is represented by a `MultilineTransferPEPS` object. -# In this case, the boundary MPS is an `MultilineMPS` object, which should be initialized -# by specifying a virtual space for each site in the partition function unit cell. - -peps2 = InfinitePEPS(ComplexSpace(2), ComplexSpace(2); unitcell=(2, 2)) -T2 = PEPSKit.MultilineTransferPEPS(peps2, 1) - -mps2 = initialize_mps(T2, fill(ComplexSpace(20), 2, 2)) -mps2, env2, ϵ = leading_boundary(mps2, T2, VUMPS()) -N2 = abs(prod(expectation_value(mps2, T2))) - -ctm2, = leading_boundary( - peps2, SimultaneousCTMRG(; verbosity=1), CTMRGEnv(peps2, ComplexSpace(20)) -) -N2´ = abs(norm(peps2, ctm2)) - -@show abs(N2 - N2´) / N2 - -# Note that for larger unit cells and non-Hermitian PEPS the VUMPS algorithm may become -# unstable, in which case the CTMRG algorithm is recommended. - -## Contracting PEPO overlaps - -# Using exactly the same machinery, we can contract partition functions which encode the -# expectation value of a PEPO for a given PEPS state. -# As an example, we can consider the overlap of the PEPO correponding to the partition -# function of 3D classical ising model with our random PEPS from before and evaluate -# . - -pepo = ising_pepo(1) -T3 = InfiniteTransferPEPO(peps, pepo, 1, 1) - -mps3 = initialize_mps(T3, [ComplexSpace(20)]) -mps3, env3, ϵ = leading_boundary(mps3, T3, VUMPS()) -@show N3 = abs(prod(expectation_value(mps3, T3))) - -# These objects and routines can be used to optimize PEPS fixed points of 3D partition -# functions, see for example https://arxiv.org/abs/1805.10598 - -nothing diff --git a/examples/boundary_mps/main.jl b/examples/boundary_mps/main.jl new file mode 100644 index 000000000..51a4cef73 --- /dev/null +++ b/examples/boundary_mps/main.jl @@ -0,0 +1,180 @@ +using Markdown #hide +md""" +# [Boundary MPS contractions of 2D networks] (@id e_boundary_mps) + +Instead of using CTMRG to contract the network encoding the norm of an infinite PEPS, one +can also use so-called [boundary MPS methods](@cite haegeman_diagonalizing_2017) to contract +this network. In this example, we will demonstrate how to use [the VUMPS algorithm](@cite +vanderstraeten_tangentspace_2019) to do so. + +Before we start, we'll fix the random seed for reproducability: +""" + +using Random +Random.seed!(29384293742893); + +md""" +Besides `TensorKit` and `PEPSKit`, here we also need to load the +[`MPSKit.jl`](https://quantumkithub.github.io/MPSKit.jl/stable/) package which implements a +host of tools for working with 1D matrix product states (MPS), including the VUMPS +algorithm: +""" + +using TensorKit, PEPSKit, MPSKit + +md""" +## Computing a PEPS norm + +We start by initializing a random infinite PEPS. Let us use uniformly distributed complex +entries using `rand` (which sometimes lead to better convergence than Gaussian distributed +`randn` elements): +""" + +peps₀ = InfinitePEPS(rand, ComplexF64, ComplexSpace(2), ComplexSpace(2)) + +md""" +To compute its norm, usually we would construct a double-layer `InfiniteSquareNetwork` which +encodes the bra-ket PEPS overlap and then contract this infinite square network, for example +using CTMRG. Here however, we will use another approach. If we take out a single row of this +infinite norm network, we can interpret it as a 2D row-to-row transfer operator ``T``. Here, +this transfer operator consists of an effective local rank-4 tensor at every site of a 2D +square lattice, where the local effective tensor is given by the contraction of a bra and +ket [`PEPSKit.PEPSTensor`](@ref) across their physical leg. Since the network we want to +contract can be interpreted as the infinite power of ``T``, we can contract it by finding +its leading eigenvector as a 1D MPS, which we call the boundary MPS. + +In PEPSKit.jl, we can directly construct the transfer operator corresponding to a PEPS norm +network from a given infinite PEPS as an [`InfiniteTransferPEPS`](@ref) object. +Additionally, we need to specify which direction should be facing north (`dir=1` +corresponding to north, counting clockwise) and which row is selected from the north - but +since we have a trivial unit cell there is only one row: +""" + +dir = 1 ## does not rotate the partition function +row = 1 +T = InfiniteTransferPEPS(peps₀, dir, row) + +md""" +Since we'll find the leading eigenvector of ``T`` as a boundary MPS, we first need to +construct an initial guess to supply to our algorithm. We can do this using the +[`initialize_mps`](@ref) function, which constructs a random MPS with a specific virtual +space for a given transfer operator. Here, we'll build an initial guess for the boundary MPS +with a bond dimension of 20: +""" + +mps₀ = initialize_mps(T, [ComplexSpace(20)]) + +md""" + +Note that this will just construct a MPS with random Gaussian entries based on the physical +spaces of the supplied transfer operator. Of course, one might come up with a better initial +guess (leading to better convergence) depending on the application. To find the leading +boundary MPS fixed point, we call [`leading_boundary`](@ref) using the +[`MPSKit.VUMPS`](@extref) algorithm from MPSKit: +""" + +mps, env, ϵ = leading_boundary(mps₀, T, VUMPS(; tol=1e-6, verbosity=2)); + +md""" +The norm of the state per unit cell is then given by the expectation value +$\langle \psi_\text{MPS} | \mathbb{T} | \psi_\text{MPS} \rangle$: +""" + +norm_vumps = abs(prod(expectation_value(mps, T))) + +md""" +This can be compared to the result obtained using CTMRG, where we see that the results match: +""" + +env_ctmrg, = leading_boundary( + CTMRGEnv(peps₀, ComplexSpace(20)), peps₀; tol=1e-6, verbosity=2 +) +norm_ctmrg = abs(norm(peps₀, env_ctmrg)) +@show abs(norm_vumps - norm_ctmrg) / norm_vumps; + +md""" +## Working with unit cells + +For PEPS with non-trivial unit cells, the principle is exactly the same. The only difference +is that now the transfer operator of the PEPS norm partition function has multiple rows or +'lines', each of which can be represented by an [`InfiniteTransferPEPS`](@ref) object. Such +a multi-line transfer operator is represented by a `MultilineTransferPEPS` object. In this +case, the boundary MPS is an [`MultilineMPS`](@extref) object, which should be initialized +by specifying a virtual space for each site in the partition function unit cell. + +First, we construct a PEPS with a $2 \times 2$ unit cell using the `unitcell` keyword +argument and then define the corresponding transfer operator, where we again specify the +direction which will be facing north: +""" + +peps₀_2x2 = InfinitePEPS( + rand, ComplexF64, ComplexSpace(2), ComplexSpace(2); unitcell=(2, 2) +) +T_2x2 = PEPSKit.MultilineTransferPEPS(peps₀_2x2, dir); + +md""" +Now, the procedure is the same as before: We compute the norm once using VUMPS, once using CTMRG and then compare. +""" + +mps₀_2x2 = initialize_mps(T_2x2, fill(ComplexSpace(20), 2, 2)) +mps_2x2, = leading_boundary(mps₀_2x2, T_2x2, VUMPS(; tol=1e-6, verbosity=2)) +norm_2x2_vumps = abs(prod(expectation_value(mps_2x2, T_2x2))) + +env_ctmrg_2x2, = leading_boundary( + CTMRGEnv(peps₀_2x2, ComplexSpace(20)), peps₀_2x2; tol=1e-6, verbosity=2 +) +norm_2x2_ctmrg = abs(norm(peps₀_2x2, env_ctmrg_2x2)) + +@show abs(norm_2x2_vumps - norm_2x2_ctmrg) / norm_2x2_vumps; + +md""" +Again, the results are compatible. Note that for larger unit cells and non-Hermitian PEPS +[the VUMPS algorithm may become unstable](@cite vanderstraeten_variational_2022), in which +case the CTMRG algorithm is recommended. + +## Contracting PEPO overlaps + +Using exactly the same machinery, we can contract 2D networks which encode the expectation +value of a PEPO for a given PEPS state. As an example, we can consider the overlap of the +PEPO correponding to the partition function of [3D classical Ising model](@ref e_3d_ising) +with our random PEPS from before and evaluate the overlap $\langle \psi_\text{PEPS} | +O_\text{PEPO} | \psi_\text{PEPS} \rangle$. + +The classical Ising PEPO is defined as follows: +""" + +function ising_pepo(β; unitcell=(1, 1, 1)) + t = ComplexF64[exp(β) exp(-β); exp(-β) exp(β)] + q = sqrt(t) + + O = zeros(2, 2, 2, 2, 2, 2) + O[1, 1, 1, 1, 1, 1] = 1 + O[2, 2, 2, 2, 2, 2] = 1 + @tensor o[-1 -2; -3 -4 -5 -6] := + O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6] + O = TensorMap(o, ℂ^2 ⊗ (ℂ^2)' ← ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)') + + return InfinitePEPO(O; unitcell) +end; + +md""" +To evaluate the overlap, we instantiate the PEPO and the corresponding [`InfiniteTransferPEPO`](@ref) +in the right direction, on the right row of the partition function (trivial here): +""" + +pepo = ising_pepo(1) +transfer_pepo = InfiniteTransferPEPO(peps₀, pepo, 1, 1) + +md""" +As before, we converge the boundary MPS using VUMPS and then compute the expectation value: +""" + +mps₀_pepo = initialize_mps(transfer_pepo, [ComplexSpace(20)]) +mps_pepo, = leading_boundary(mps₀_pepo, transfer_pepo, VUMPS(; tol=1e-6, verbosity=2)) +norm_pepo = abs(prod(expectation_value(mps_pepo, transfer_pepo))); +@show norm_pepo; + +md""" +These objects and routines can be used to optimize PEPS fixed points of 3D partition +functions, see for example [Vanderstraeten et al.](@cite vanderstraeten_residual_2018) +""" diff --git a/examples/fermi_hubbard.jl b/examples/fermi_hubbard.jl deleted file mode 100644 index 165301ef3..000000000 --- a/examples/fermi_hubbard.jl +++ /dev/null @@ -1,57 +0,0 @@ -using Test -using Random -using PEPSKit -using TensorKit -using KrylovKit -using OptimKit - -using MPSKit: add_physical_charge - -## The Fermi-Hubbard model with fℤ₂ ⊠ U1 symmetry, at large U and half filling - -# reference: https://www.osti.gov/servlets/purl/1565498 -# energy should end up at E_ref ≈ 4 * -0.5244140625 = -2.09765625, but takes a lot of time -E_ref = -2.0 - -# parameters -t = 1.0 -U = 8.0 -lattice = InfiniteSquare(2, 2) -fermion = fℤ₂ -particle_symmetry = U1Irrep -spin_symmetry = Trivial -S = fermion ⊠ particle_symmetry # symmetry sector - -# spaces -D = 1 -Vpeps = Vect[S]((0, 0) => 2 * D, (1, 1) => D, (1, -1) => D) -χ = 1 -Venv = Vect[S]( - (0, 0) => 4 * χ, (1, -1) => 2 * χ, (1, 1) => 2 * χ, (0, 2) => χ, (0, -2) => χ -) -Saux = S((1, -1)) # impose half filling - -# algorithms -boundary_alg = (; tol=1e-8, alg=:simultaneous, verbosity=2, trscheme=(; alg=:fixedspace)) -gradient_alg = (; tol=1e-6, alg=:eigsolver, maxiter=10, iterscheme=:diffgauge) -optimizer_alg = (; tol=1e-4, alg=:lbfgs, verbosity=3, maxiter=100, ls_maxiter=2, ls_maxfg=2) -reuse_env = true - -# Hamiltonian -H0 = hubbard_model(ComplexF64, particle_symmetry, spin_symmetry, lattice; t, U) -H = add_physical_charge(H0, fill(Saux, size(H0.lattice)...)) -Pspaces = H.lattice - -# initialize state -Nspaces = fill(Vpeps, size(lattice)...) -Espaces = fill(Vpeps, size(lattice)...) -Random.seed!(2928528937) -ψ₀ = InfinitePEPS(randn, ComplexF64, Pspaces, Nspaces, Espaces) -env₀ = CTMRGEnv(ψ₀, Venv) -env₀, = leading_boundary(env₀, ψ₀; boundary_alg...) - -# optimize -ψ, env, E, info = fixedpoint( - H, ψ₀, env₀; boundary_alg, gradient_alg, optimizer_alg, reuse_env -) -@test E < E_ref diff --git a/examples/fermi_hubbard/main.jl b/examples/fermi_hubbard/main.jl new file mode 100644 index 000000000..aa8ca1dc1 --- /dev/null +++ b/examples/fermi_hubbard/main.jl @@ -0,0 +1,104 @@ +using Markdown #hide +md""" +# Fermi-Hubbard model with $f\mathbb{Z}_2 \boxtimes U(1)$ symmetry, at large $U$ and half-filling + +In this example, we will demonstrate how to handle fermionic PEPS tensors and how to +optimize them. To that end, we consider the two-dimensional Hubbard model + +```math +H = -t \sum_{\langle i,j \rangle} \sum_{\sigma} \left( c_{i,\sigma}^+ c_{j,\sigma}^- - +c_{i,\sigma}^- c_{j,\sigma}^+ \right) + U \sum_i n_{i,\uparrow}n_{i,\downarrow} - \mu \sum_i n_i +``` + +where $\sigma \in \{\uparrow,\downarrow\}$ and $n_{i,\sigma} = c_{i,\sigma}^+ c_{i,\sigma}^-$ +is the fermionic number operator. As in previous examples, using fermionic degrees of freedom +is a matter of creating tensors with the right symmetry sectors - the rest of the simulation +workflow remains the same. + +First though, we make the example deterministic by seeding the RNG, and we make our imports: +""" + +using Random +using TensorKit, PEPSKit +using MPSKit: add_physical_charge +Random.seed!(2928528937); + +md""" +## Defining the fermionic Hamiltonian + +Let us start by fixing the parameters of the Hubbard model. We're going to use a hopping of +$t=1$ and a large $U=8$ on a $2 \times 2$ unit cell: +""" + +t = 1.0 +U = 8.0 +lattice = InfiniteSquare(2, 2); + +md""" +In order to create fermionic tensors, one needs to define symmetry sectors using TensorKit's +`FermionParity`. Not only do we want use fermion parity but we also want our +particles to exploit the global $U(1)$ symmetry. The combined product sector can be obtained +using the [Deligne product](https://jutho.github.io/TensorKit.jl/stable/lib/sectors/#TensorKitSectors.deligneproduct-Tuple{Sector,%20Sector}), +called through `⊠` which is obtained by typing `\boxtimes+TAB`. We will not impose any extra +spin symmetry, so we have: +""" + +fermion = fℤ₂ +particle_symmetry = U1Irrep +spin_symmetry = Trivial +S = fermion ⊠ particle_symmetry + +md""" +The next step is defining graded virtual PEPS and environment spaces using `S`. Here we also +use the symmetry sector to impose half-filling. That is all we need to define the Hubbard +Hamiltonian: +""" + +D, χ = 1, 1 +V_peps = Vect[S]((0, 0) => 2 * D, (1, 1) => D, (1, -1) => D) +V_env = Vect[S]( + (0, 0) => 4 * χ, (1, -1) => 2 * χ, (1, 1) => 2 * χ, (0, 2) => χ, (0, -2) => χ +) +S_aux = S((1, -1)) +H₀ = hubbard_model(ComplexF64, particle_symmetry, spin_symmetry, lattice; t, U) +H = add_physical_charge(H₀, fill(S_aux, size(H₀.lattice)...)); + +md""" +## Finding the ground state + +Again, the procedure of ground state optimization is very similar to before. First, we +define all algorithmic parameters: +""" + +boundary_alg = (; tol=1e-8, alg=:simultaneous, trscheme=(; alg=:fixedspace)) +gradient_alg = (; tol=1e-6, alg=:eigsolver, maxiter=10, iterscheme=:diffgauge) +optimizer_alg = (; tol=1e-4, alg=:lbfgs, maxiter=80, ls_maxiter=3, ls_maxfg=3) + +md""" +Second, we initialize a PEPS state and environment (which we converge) constructed from +symmetric physical and virtual spaces: +""" + +physical_spaces = physicalspace(H) +virtual_spaces = fill(V_peps, size(lattice)...) +peps₀ = InfinitePEPS(randn, ComplexF64, physical_spaces, virtual_spaces) +env₀, = leading_boundary(CTMRGEnv(peps₀, V_env), peps₀; boundary_alg...); + +md""" +And third, we start the ground state search (this does take quite long): +""" + +peps, env, E, info = fixedpoint( + H, peps₀, env₀; boundary_alg, gradient_alg, optimizer_alg, verbosity=3 +) +@show E; + +md""" +Finally, let's compare the obtained energy against a reference energy from a QMC study by +[Qin et al.](@cite qin_benchmark_2016). With the parameters specified above, they obtain an +energy of $E_\text{ref} \approx 4 \times -0.5244140625 = -2.09765625$ (the factor 4 comes +from the $2 \times 2$ unit cell that we use here). Thus, we find: +""" + +E_ref = -2.09765625 +@show (E - E_ref) / E_ref; diff --git a/examples/heisenberg.jl b/examples/heisenberg.jl deleted file mode 100644 index 04394feb1..000000000 --- a/examples/heisenberg.jl +++ /dev/null @@ -1,31 +0,0 @@ -using LinearAlgebra -using TensorKit, OptimKit -using PEPSKit, KrylovKit - -# Square lattice Heisenberg Hamiltonian -# We use the parameters (J₁, J₂, J₃) = (-1, 1, -1) by default to capture -# the ground state in a single-site unit cell. This can be seen from -# sublattice rotating H from parameters (1, 1, 1) to (-1, 1, -1). -H = heisenberg_XYZ(InfiniteSquare(); Jx=-1, Jy=1, Jz=-1) - -# Parameters -χbond = 2 -χenv = 20 -ctm_alg = SimultaneousCTMRG(; tol=1e-10, verbosity=2) -opt_alg = PEPSOptimize(; - boundary_alg=ctm_alg, - optimizer_alg=LBFGS(4; maxiter=100, gradtol=1e-4, verbosity=3), - gradient_alg=LinSolver(; solver_alg=GMRES(; tol=1e-6, maxiter=100)), - reuse_env=true, -) - -# Ground state search -# We initialize a random PEPS with bond dimension χbond and from that converge -# a CTMRG environment with dimension χenv on the environment bonds before -# starting the optimization. The ground-state energy should approximately approach -# E/N = −0.6694421, which is a QMC estimate from https://arxiv.org/abs/1101.3281. -# Of course there is a noticable bias for small χbond and χenv. -ψ₀ = InfinitePEPS(2, χbond) -env₀, = leading_boundary(CTMRGEnv(ψ₀, ℂ^χenv), ψ₀, ctm_alg) -peps, env, E, = fixedpoint(H, ψ, env₀, opt_alg₀) -@show E diff --git a/examples/heisenberg/main.jl b/examples/heisenberg/main.jl new file mode 100644 index 000000000..4acfa18da --- /dev/null +++ b/examples/heisenberg/main.jl @@ -0,0 +1,187 @@ +using Markdown #hide +md""" +# [Optimizing the 2D Heisenberg model](@id examples_heisenberg) + +In this example we want to provide a basic rundown of PEPSKit's optimization workflow for +PEPS. To that end, we will consider the two-dimensional Heisenberg model on a square lattice + +```math +H = \sum_{\langle i,j \rangle} \left ( J_x S^{x}_i S^{x}_j + J_y S^{y}_i S^{y}_j + J_z S^{z}_i S^{z}_j \right ) +``` + +Here, we want to set $J_x = J_y = J_z = 1$ where the Heisenberg model is in the antiferromagnetic +regime. Due to the bipartite sublattice structure of antiferromagnetic order one needs a +PEPS ansatz with a $2 \times 2$ unit cell. This can be circumvented by performing a unitary +sublattice rotation on all B-sites resulting in a change of parameters to +$(J_x, J_y, J_z)=(-1, 1, -1)$. This gives us a unitarily equivalent Hamiltonian (with the +same spectrum) with a ground state on a single-site unit cell. + +Let us get started by fixing the random seed of this example to make it deterministic: +""" + +using Random +Random.seed!(123456789); + +md""" +We're going to need only two packages: `TensorKit`, since we use that for all the underlying +tensor operations, and `PEPSKit` itself. So let us import these: +""" + +using TensorKit, PEPSKit + +md""" +## Defining the Heisenberg Hamiltonian + +To create the sublattice rotated Heisenberg Hamiltonian on an infinite square lattice, we use +the `heisenberg_XYZ` method from [MPSKitModels](https://quantumkithub.github.io/MPSKitModels.jl/dev/) +which is redefined for the `InfiniteSquare` and reexported in PEPSKit: +""" + +H = heisenberg_XYZ(InfiniteSquare(); Jx=-1, Jy=1, Jz=-1) + +md""" +## Setting up the algorithms and initial guesses + +Next, we set the simulation parameters. During optimization, the PEPS will be contracted +using CTMRG and the PEPS gradient will be computed by differentiating through the CTMRG +routine using AD. Since the algorithmic stack that implements this is rather elaborate, +the amount of settings one can configure is also quite large. To reduce this complexity, +PEPSKit defaults to (presumably) reasonable settings which also dynamically adapts to the +user-specified parameters. + +First, we set the bond dimension `Dbond` of the virtual PEPS indices and the environment +dimension `χenv` of the virtual corner and transfer matrix indices. +""" + +Dbond = 2 +χenv = 16; + +md""" +To configure the CTMRG algorithm, we create a `NamedTuple` containing different keyword +arguments. To see a description of all arguments, see the docstring of +[`leading_boundary`](@ref). Here, we want to converge the CTMRG environments up to a +specific tolerance and during the CTMRG run keep all index dimensions fixed: +""" + +boundary_alg = (; tol=1e-10, trscheme=(; alg=:fixedspace)); + +md""" +Let us also configure the optimizer algorithm. We are going to optimize the PEPS using the +L-BFGS optimizer from [OptimKit](https://github.com/Jutho/OptimKit.jl). Again, we specify +the convergence tolerance (for the gradient norm) as well as the maximal number of iterations +and the BFGS memory size (which is used to approximate the Hessian): +""" + +optimizer_alg = (; alg=:lbfgs, tol=1e-4, maxiter=100, lbfgs_memory=16); + +md""" +Additionally, during optimization, we want to reuse the previous CTMRG environment to +initialize the CTMRG run of the current optimization step using the `reuse_env` argument. +And to control the output information, we set the `verbosity`: +""" + +reuse_env = true +verbosity = 3; + +md""" +Next, we initialize a random PEPS which will be used as an initial guess for the +optimization. To get a PEPS with physical dimension 2 (since we have a spin-1/2 Hamiltonian) +with complex-valued random Gaussian entries, we set: +""" + +peps₀ = InfinitePEPS(randn, ComplexF64, 2, Dbond) + +md""" +The last thing we need before we can start the optimization is an initial CTMRG environment. +Typically, a random environment which we converge on `peps₀` serves as a good starting point. +To contract a PEPS starting from an environment using CTMRG, we call [`leading_boundary`](@ref): +""" + +env_random = CTMRGEnv(randn, ComplexF64, peps₀, ℂ^χenv); +env₀, info_ctmrg = leading_boundary(env_random, peps₀; boundary_alg...); + +md""" +Besides the converged environment, `leading_boundary` also returns a `NamedTuple` of +informational quantities such as the last maximal truncation error - that is, the SVD +approximation error incurred in the last CTMRG iteration, maximized over all spatial +directions and unit cell entries: +""" + +@show info_ctmrg.truncation_error; + +md""" +## Ground state search + +Finally, we can start the optimization by calling [`fixedpoint`](@ref) on `H` with our +settings for the boundary (CTMRG) algorithm and the optimizer. This might take a while +(especially the precompilation of AD code in this case): +""" + +peps, env, E, info_opt = fixedpoint( + H, peps₀, env₀; boundary_alg, optimizer_alg, reuse_env, verbosity +); + +md""" +Note that `fixedpoint` returns the final optimized PEPS, the last converged environment, +the final energy estimate as well as a `NamedTuple` of diagnostics. This allows us to, e.g., +analyze the number of cost function calls or the history of gradient norms to evaluate +the convergence rate: +""" + +@show info_opt.fg_evaluations info_opt.gradnorms[1:10:end]; + +md""" +Let's now compare the optimized energy against an accurate Quantum Monte Carlo estimate by +[Sandvik](@cite sandvik_computational_2011), where the energy per site was found to be +$E_{\text{ref}}=−0.6694421$. From our simple optimization we find: +""" + +@show E; + +md""" +While this energy is in the right ballpark, there is still quite some deviation from the +accurate reference energy. This, however, can be attributed to the small bond dimension - an +optimization with larger bond dimension would approach this value much more closely. + +A more reasonable comparison would be against another finite bond dimension PEPS simulation. +For example, Juraj Hasik's data from $J_1\text{-}J_2$ +[PEPS simulations](https://github.com/jurajHasik/j1j2_ipeps_states/blob/main/single-site_pg-C4v-A1/j20.0/state_1s_A1_j20.0_D2_chi_opt48.dat) +yields $E_{D=2,\chi=16}=-0.660231\dots$ which is more in line with what we find here. + +## Compute the correlation lengths and transfer matrix spectra + +In practice, in order to obtain an accurate and variational energy estimate, one would need +to compute multiple energies at different environment dimensions and extrapolate in, e.g., +the correlation length or the second gap of the transfer matrix spectrum. For that, we would +need the [`correlation_length`](@ref) function, which computes the horizontal and vertical +correlation lengths and transfer matrix spectra for all unit cell coordinates: +""" + +ξ_h, ξ_v, λ_h, λ_v = correlation_length(peps, env) +@show ξ_h ξ_v; + +md""" +## Computing observables + +As a last thing, we want to see how we can compute expectation values of observables, given +the optimized PEPS and its CTMRG environment. To compute, e.g., the magnetization, we first +need to define the observable as a `TensorMap`: +""" + +σ_z = TensorMap([1.0 0.0; 0.0 -1.0], ℂ^2, ℂ^2) + +md""" +In order to be able to contract it with the PEPS and environment, we define need to define a +`LocalOperator` and specify on which physical spaces and sites the observable acts. That way, +the PEPS-environment-operator contraction gets automatically generated (also works for +multi-site operators!). See the [`LocalOperator`](@ref) docstring for more details. +The magnetization is just a single-site observable, so we have: +""" + +M = LocalOperator(fill(ℂ^2, 1, 1), (CartesianIndex(1, 1),) => σ_z) + +md""" +Finally, to evaluate the expecation value on the `LocalOperator`, we call: +""" + +@show expectation_value(peps, M, env); diff --git a/examples/heisenberg_evol/heis_tools.jl b/examples/heisenberg_evol/heis_tools.jl deleted file mode 100644 index b27f3148c..000000000 --- a/examples/heisenberg_evol/heis_tools.jl +++ /dev/null @@ -1,58 +0,0 @@ -using Test -using Printf -using Random -import Statistics: mean -using TensorKit -using PEPSKit - -module MeasureHeis - -export measure_heis - -using TensorKit -import MPSKitModels: S_x, S_y, S_z, S_exchange -using PEPSKit - -""" -Measure magnetization on each site -""" -function cal_mags(peps::InfinitePEPS, env::CTMRGEnv) - Nr, Nc = size(peps) - lattice = collect(space(t, 1) for t in peps.A) - # detect symmetry on physical axis - symm = sectortype(space(peps.A[1, 1])) - if symm == Trivial - Sas = real.([S_x(symm), im * S_y(symm), S_z(symm)]) - elseif symm == U1Irrep - # only Sz preserves - Sas = real.([S_z(symm)]) - else - throw(ArgumentError("Unrecognized symmetry on physical axis")) - end - return [ - collect( - expectation_value( - peps, LocalOperator(lattice, (CartesianIndex(r, c),) => Sa), env - ) for (r, c) in Iterators.product(1:Nr, 1:Nc) - ) for Sa in Sas - ] -end - -""" -Measure physical quantities for Heisenberg model -""" -function measure_heis(peps::InfinitePEPS, H::LocalOperator, env::CTMRGEnv) - results = Dict{String,Any}() - Nr, Nc = size(peps) - results["e_site"] = cost_function(peps, env, H) / (Nr * Nc) - results["mag"] = cal_mags(peps, env) - results["mag_norm"] = collect( - norm([mags[r, c] for mags in results["mag"]]) for - (r, c) in Iterators.product(1:Nr, 1:Nc) - ) - return results -end - -end - -import .MeasureHeis: measure_heis diff --git a/examples/heisenberg_evol/simpleupdate.jl b/examples/heisenberg_evol/simpleupdate.jl deleted file mode 100644 index f3cd64a6a..000000000 --- a/examples/heisenberg_evol/simpleupdate.jl +++ /dev/null @@ -1,57 +0,0 @@ -include("heis_tools.jl") - -# benchmark data is from Phys. Rev. B 94, 035133 (2016) -Dbond, χenv, symm = 4, 16, Trivial -trscheme_peps = truncerr(1e-10) & truncdim(Dbond) -trscheme_env = truncerr(1e-10) & truncdim(χenv) -Nr, Nc = 2, 2 -# Heisenberg model Hamiltonian -# (already only includes nearest neighbor terms) -ham = real(heisenberg_XYZ(ComplexF64, symm, InfiniteSquare(Nr, Nc); Jx=1.0, Jy=1.0, Jz=1.0)) - -# random initialization of 2x2 iPEPS with weights and CTMRGEnv (using real numbers) -if symm == Trivial - Pspace = ℂ^2 - Vspace = ℂ^Dbond - Espace = ℂ^χenv -elseif symm == U1Irrep - Pspace = ℂ[U1Irrep](1//2 => 1, -1//2 => 1) - Vspace = ℂ[U1Irrep](0 => Dbond ÷ 2, 1//2 => Dbond ÷ 4, -1//2 => Dbond ÷ 4) - Espace = ℂ[U1Irrep](0 => χenv ÷ 2, 1//2 => χenv ÷ 4, -1//2 => χenv ÷ 4) -else - error("Not implemented") -end -Random.seed!(0) -peps = InfiniteWeightPEPS(rand, Float64, Pspace, Vspace; unitcell=(Nr, Nc)) -# normalize vertex tensors -for ind in CartesianIndices(peps.vertices) - peps.vertices[ind] /= norm(peps.vertices[ind], Inf) -end - -# simple update -dts = [1e-2, 1e-3, 4e-4] -tols = [1e-6, 1e-8, 1e-8] -maxiter = 10000 -for (dt, tol) in zip(dts, tols) - alg = SimpleUpdate(dt, tol, maxiter, trscheme_peps) - result = simpleupdate(peps, ham, alg; bipartite=true) - global peps = result[1] -end -# measure physical quantities with CTMRG -peps_ = InfinitePEPS(peps) -env = CTMRGEnv(rand, Float64, peps_, Espace) -env, = leading_boundary( - env, - peps_; - alg=:sequential, - projector_alg=:fullinfinite, - tol=1e-10, - verbosity=2, - trscheme=trscheme_env, -) -meas = measure_heis(peps_, ham, env) -display(meas) -@info @sprintf("Energy = %.8f\n", meas["e_site"]) -@info @sprintf("Staggered magnetization = %.8f\n", mean(meas["mag_norm"])) -@test isapprox(meas["e_site"], -0.6675; atol=1e-3) -@test isapprox(mean(meas["mag_norm"]), 0.3767; atol=1e-3) diff --git a/examples/heisenberg_su/main.jl b/examples/heisenberg_su/main.jl new file mode 100644 index 000000000..06718cd51 --- /dev/null +++ b/examples/heisenberg_su/main.jl @@ -0,0 +1,140 @@ +using Markdown #hide +md""" +# Simple update for the Heisenberg model + +In this example, we will use [`SimpleUpdate`](@ref) imaginary time evolution to treat +the two-dimensional Heisenberg model once again: + +```math +H = \sum_{\langle i,j \rangle} J_x S^{x}_i S^{x}_j + J_y S^{y}_i S^{y}_j + J_z S^{z}_i S^{z}_j. +``` + +In order to simulate the antiferromagnetic order of the Hamiltonian on a single-site unit +cell one typically applies a unitary sublattice rotation. Here, we will instead use a +$2 \times 2$ unit cell and set $J_x = J_y = J_z = 1$. + +Let's get started by seeding the RNG and importing all required modules: +""" + +using Random +import Statistics: mean +using TensorKit, PEPSKit +import MPSKitModels: S_x, S_y, S_z, S_exchange +Random.seed!(0); + +md""" +## Defining the Hamiltonian + +To construct the Heisenberg Hamiltonian as just discussed, we'll use `heisenberg_XYZ` and, +in addition, make it real (`real` and `imag` works for `LocalOperator`s) since we want to +use PEPS and environments with real entries. We can either initialize the Hamiltonian with +no internal symmetries (`symm = Trivial`) or use the global $U(1)$ symmetry +(`symm = U1Irrep`): +""" + +symm = Trivial ## ∈ {Trivial, U1Irrep} +Nr, Nc = 2, 2 +H = real(heisenberg_XYZ(ComplexF64, symm, InfiniteSquare(Nr, Nc); Jx=1, Jy=1, Jz=1)); + +md""" +## Simple updating + +We proceed by initializing a random weighted PEPS that will be evolved. First though, we +need to define the appropriate (symmetric) spaces: +""" + +Dbond = 4 +χenv = 16 +if symm == Trivial + physical_space = ℂ^2 + bond_space = ℂ^Dbond + env_space = ℂ^χenv +elseif symm == U1Irrep + physical_space = ℂ[U1Irrep](1//2 => 1, -1//2 => 1) + bond_space = ℂ[U1Irrep](0 => Dbond ÷ 2, 1//2 => Dbond ÷ 4, -1//2 => Dbond ÷ 4) + env_space = ℂ[U1Irrep](0 => χenv ÷ 2, 1//2 => χenv ÷ 4, -1//2 => χenv ÷ 4) +else + error("not implemented") +end + +wpeps = InfiniteWeightPEPS(rand, Float64, physical_space, bond_space; unitcell=(Nr, Nc)); + +md""" +Next, we can start the `SimpleUpdate` routine, successively decreasing the time intervals +and singular value convergence tolerances. Note that TensorKit allows to combine SVD +truncation schemes, which we use here to set a maximal bond dimension and at the same time +fix a truncation error (if that can be reached by remaining below `Dbond`): +""" + +dts = [1e-2, 1e-3, 4e-4] +tols = [1e-6, 1e-8, 1e-8] +maxiter = 10000 +trscheme_peps = truncerr(1e-10) & truncdim(Dbond) + +for (dt, tol) in zip(dts, tols) + alg = SimpleUpdate(dt, tol, maxiter, trscheme_peps) + result = simpleupdate(wpeps, H, alg; bipartite=true) + global wpeps = result[1] +end + +md""" +## Computing the ground-state energy and magnetizations + +In order to compute observable expectation values, we need to converge a CTMRG environment +on the evolved PEPS. Let's do so: +""" + +peps = InfinitePEPS(wpeps) ## absorb the weights +env₀ = CTMRGEnv(rand, Float64, peps, env_space) +trscheme_env = truncerr(1e-10) & truncdim(χenv) +env, = leading_boundary( + env₀, + peps; + alg=:sequential, + projector_alg=:fullinfinite, + tol=1e-10, + trscheme=trscheme_env, +); + +md""" +Finally, we'll measure the energy and different magnetizations. For the magnetizations, +the plan is to compute the expectation values unit cell entry-wise in different spin +directions: +""" + +function compute_mags(peps::InfinitePEPS, env::CTMRGEnv) + lattice = collect(space(t, 1) for t in peps.A) + + ## detect symmetry on physical axis + symm = sectortype(space(peps.A[1, 1])) + if symm == Trivial + S_ops = real.([S_x(symm), im * S_y(symm), S_z(symm)]) + elseif symm == U1Irrep + S_ops = real.([S_z(symm)]) ## only Sz preserves + end + + return map(Iterators.product(axes(peps, 1), axes(peps, 2), S_ops)) do (r, c, S) + expectation_value(peps, LocalOperator(lattice, (CartesianIndex(r, c),) => S), env) + end +end + +E = expectation_value(peps, H, env) / (Nr * Nc) +Ms = compute_mags(peps, env) +M_norms = map( + rc -> norm(Ms[rc[1], rc[2], :]), Iterators.product(axes(peps, 1), axes(peps, 2)) +) +@show E Ms M_norms; + +md""" +To assess the results, we will benchmark against data from [Corboz](@cite corboz_variational_2016), +which use manual gradients to perform a variational optimization of the Heisenberg model. +In particular, for the energy and magnetization they find $E_\text{ref} = -0.6675$ and +$M_\text{ref} = 0.3767$. Looking at the relative errors, we find general agreement, although +the accuracy is limited by the methodological limitations of the simple update algorithm as +well as finite bond dimension effects and a lacking extrapolation: +""" + +E_ref = -0.6675 +M_ref = 0.3767 +@show (E - E_ref) / E_ref +@show (mean(M_norms) - M_ref) / E_ref; diff --git a/examples/hubbard_su.jl b/examples/hubbard_su.jl deleted file mode 100644 index 699a49dba..000000000 --- a/examples/hubbard_su.jl +++ /dev/null @@ -1,58 +0,0 @@ -using Test -using Random -using PEPSKit -using TensorKit - -# random initialization of 2x2 iPEPS with weights and CTMRGEnv (using real numbers) -Dbond, symm = 8, Trivial -Nr, Nc = 2, 2 -Random.seed!(10) -if symm == Trivial - Pspace = Vect[fℤ₂](0 => 2, 1 => 2) - Vspace = Vect[fℤ₂](0 => Dbond / 2, 1 => Dbond / 2) -else - error("Not implemented") -end -peps = InfiniteWeightPEPS(rand, Float64, Pspace, Vspace; unitcell=(Nr, Nc)) - -# normalize vertex tensors -for ind in CartesianIndices(peps.vertices) - peps.vertices[ind] /= norm(peps.vertices[ind], Inf) -end - -# Hubbard model Hamiltonian at half-filling -t, U = 1, 6 -ham = hubbard_model(Float64, Trivial, Trivial, InfiniteSquare(Nr, Nc); t, U, mu=U / 2) - -# simple update -dts = [1e-2, 1e-3, 4e-4, 1e-4] -tols = [1e-6, 1e-8, 1e-8, 1e-8] -maxiter = 20000 -for (n, (dt, tol)) in enumerate(zip(dts, tols)) - trscheme = truncerr(1e-10) & truncdim(Dbond) - alg = SimpleUpdate(dt, tol, maxiter, trscheme) - global peps, = simpleupdate(peps, ham, alg; bipartite=false) -end - -# absorb weight into site tensors -peps = InfinitePEPS(peps) - -# CTMRG -χenv0, χenv = 6, 20 -Espace = Vect[fℤ₂](0 => χenv0 / 2, 1 => χenv0 / 2) -env = CTMRGEnv(randn, Float64, peps, Espace) -for χ in [χenv0, χenv] - env, = leading_boundary(env, peps; alg=:sequential, maxiter=300, tol=1e-7) -end - -# Benchmark values of the ground state energy from -# Qin, M., Shi, H., & Zhang, S. (2016). Benchmark study of the two-dimensional Hubbard -# model with auxiliary-field quantum Monte Carlo method. Physical Review B, 94(8), 085103. -Es_exact = Dict(0 => -1.62, 2 => -0.176, 4 => 0.8603, 6 => -0.6567, 8 => -0.5243) -E_exact = Es_exact[U] - U / 2 - -# measure energy -E = cost_function(peps, env, ham) / (Nr * Nc) -@info "Energy = $E" -@info "Benchmark energy = $E_exact" -@test isapprox(E, E_exact; atol=5e-2) diff --git a/examples/hubbard_su/main.jl b/examples/hubbard_su/main.jl new file mode 100644 index 000000000..27a82eb3a --- /dev/null +++ b/examples/hubbard_su/main.jl @@ -0,0 +1,112 @@ +using Markdown #hide +md""" +# Simple update for the Fermi-Hubbard model at half-filling + +Once again, we consider the Hubbard model but this time we obtain the ground-state PEPS by +imaginary time evolution. In particular, we'll use the [`SimpleUpdate`](@ref) algorithm. +As a reminder, we define the Hubbard model as + +```math +H = -t \sum_{\langle i,j \rangle} \sum_{\sigma} \left( c_{i,\sigma}^+ c_{j,\sigma}^- - +c_{i,\sigma}^- c_{j,\sigma}^+ \right) + U \sum_i n_{i,\uparrow}n_{i,\downarrow} - \mu \sum_i n_i +``` + +with $\sigma \in \{\uparrow,\downarrow\}$ and $n_{i,\sigma} = c_{i,\sigma}^+ c_{i,\sigma}^-$. + +Let's get started by seeding the RNG and importing the required modules: +""" + +using Random +using TensorKit, PEPSKit +Random.seed!(12329348592498); + +md""" +## Defining the Hamiltonian + +First, we define the Hubbard model at $t=1$ hopping and $U=6$ using `Trivial` sectors for +the particle and spin symmetries, and set $\mu = U/2$ for half-filling. The model will be +constructed on a $2 \times 2$ unit cell, so we have: +""" + +t = 1 +U = 6 +Nr, Nc = 2, 2 +H = hubbard_model(Float64, Trivial, Trivial, InfiniteSquare(Nr, Nc); t, U, mu=U / 2); + +md""" +## Running the simple update algorithm + +Next, we'll specify the virtual PEPS bond dimension and define the fermionic physical and +virtual spaces. The simple update algorithm evolves an infinite PEPS with weights on the +virtual bonds, so we here need to intialize an [`InfiniteWeightPEPS`](@ref). By default, +the bond weights will be identity. Unlike in the other examples, we here use tensors with +real `Float64` entries: +""" + +Dbond = 8 +physical_space = Vect[fℤ₂](0 => 2, 1 => 2) +virtual_space = Vect[fℤ₂](0 => Dbond / 2, 1 => Dbond / 2) +wpeps = InfiniteWeightPEPS(rand, Float64, physical_space, virtual_space; unitcell=(Nr, Nc)); + +md""" +Let's set the algorithm parameters: The plan is to successively decrease the time interval of +the Trotter-Suzuki as well as the convergence tolerance such that we obtain a more accurate +result at each iteration. To run the simple update, we call [`simpleupdate`](@ref) where we +use the keyword `bipartite=false` - meaning that we use the full $2 \times 2$ unit cell +without assuming a bipartite structure. Thus, we can start evolving: +""" + +dts = [1e-2, 1e-3, 4e-4, 1e-4] +tols = [1e-6, 1e-8, 1e-8, 1e-8] +maxiter = 20000 + +for (n, (dt, tol)) in enumerate(zip(dts, tols)) + trscheme = truncerr(1e-10) & truncdim(Dbond) + alg = SimpleUpdate(dt, tol, maxiter, trscheme) + global wpeps, = simpleupdate(wpeps, H, alg; bipartite=false) +end + +md""" +To obtain the evolved `InfiniteWeightPEPS` as an actual PEPS without weights on the bonds, +we can just call the following constructor: +""" + +peps = InfinitePEPS(wpeps); + +md""" +## Computing the ground-state energy + +In order to compute the energy expectation value with evolved PEPS, we need to converge a +CTMRG environment on it. We first converge an environment with a small enviroment dimension +and then use that to initialize another run with bigger environment dimension. We'll use +`trscheme=truncdim(χ)` for that such that the dimension is increased during the second CTMRG +run: +""" + +χenv₀, χenv = 6, 16 +env_space = Vect[fℤ₂](0 => χenv₀ / 2, 1 => χenv₀ / 2) + +env = CTMRGEnv(rand, Float64, peps, env_space) +for χ in [χenv₀, χenv] + global env, = leading_boundary( + env, peps; alg=:sequential, tol=1e-5, trscheme=truncdim(χ) + ) +end + +md""" +We measure the energy by computing the `H` expectation value, where we have to make sure to +normalize with respect to the unit cell to obtain the energy per site: +""" + +E = expectation_value(peps, H, env) / (Nr * Nc) +@show E; + +md""" +Finally, we can compare the obtained ground-state energy against the literature, namely the +QMC estimates from [Qin et al.](@cite qin_benchmark_2016). We find that the results generally +agree: +""" + +Es_exact = Dict(0 => -1.62, 2 => -0.176, 4 => 0.8603, 6 => -0.6567, 8 => -0.5243) +E_exact = Es_exact[U] - U / 2 +@show (E - E_exact) / E_exact; diff --git a/examples/ising_pepo.jl b/examples/ising_pepo.jl deleted file mode 100644 index 4936e602f..000000000 --- a/examples/ising_pepo.jl +++ /dev/null @@ -1,23 +0,0 @@ -using TensorOperations -using TensorKit - -""" - ising_pepo(beta; unitcell=(1, 1, 1)) - -Return the PEPO tensor for partition function of the 3D classical Ising model at inverse -temperature `beta`. -""" -function ising_pepo(beta; unitcell=(1, 1, 1)) - t = ComplexF64[exp(beta) exp(-beta); exp(-beta) exp(beta)] - q = sqrt(t) - - O = zeros(2, 2, 2, 2, 2, 2) - O[1, 1, 1, 1, 1, 1] = 1 - O[2, 2, 2, 2, 2, 2] = 1 - @tensor o[-1 -2; -3 -4 -5 -6] := - O[1 2; 3 4 5 6] * q[-1; 1] * q[-2; 2] * q[-3; 3] * q[-4; 4] * q[-5; 5] * q[-6; 6] - - O = TensorMap(o, ℂ^2 ⊗ (ℂ^2)' ← ℂ^2 ⊗ ℂ^2 ⊗ (ℂ^2)' ⊗ (ℂ^2)') - - return InfinitePEPO(O; unitcell) -end diff --git a/examples/make.jl b/examples/make.jl new file mode 100644 index 000000000..0c2060e8a --- /dev/null +++ b/examples/make.jl @@ -0,0 +1,98 @@ +# if examples is not the current active environment, switch to it +if Base.active_project() != joinpath(@__DIR__, "Project.toml") + using Pkg + Pkg.activate(@__DIR__) + Pkg.develop(PackageSpec(; path=(@__DIR__) * "/../")) + Pkg.resolve() + Pkg.instantiate() +end + +using PEPSKit +using Literate +using TOML, SHA + +# ---------------------------------------------------------------------------------------- # +# Caching +# ---------------------------------------------------------------------------------------- # + +const CACHEFILE = joinpath(@__DIR__, "Cache.toml") + +getcache() = isfile(CACHEFILE) ? TOML.parsefile(CACHEFILE) : Dict{String,Any}() + +function iscached(name) + cache = getcache() + return haskey(cache, name) && cache[name] == checksum(name) +end + +function setcached(name) + cache = getcache() + cache[name] = checksum(name) + return open(f -> TOML.print(f, cache), CACHEFILE, "w") +end + +checksum(name) = bytes2hex(sha256(joinpath(@__DIR__, name, "main.jl"))) + +# ---------------------------------------------------------------------------------------- # +# Building +# ---------------------------------------------------------------------------------------- # + +attach_notebook_badge(name) = str -> attach_notebook_badge(name, str) +function attach_notebook_badge(name, str) + mybinder_badge_url = "https://mybinder.org/badge_logo.svg" + nbviewer_badge_url = "https://img.shields.io/badge/show-nbviewer-579ACA.svg" + download_badge_url = "https://img.shields.io/badge/download-project-orange" + mybinder = "[![]($mybinder_badge_url)](@__BINDER_ROOT_URL__/examples/$name/main.ipynb)" + nbviewer = "[![]($nbviewer_badge_url)](@__NBVIEWER_ROOT_URL__/examples/$name/main.ipynb)" + download = "[![]($download_badge_url)](https://minhaskamal.github.io/DownGit/#/home?url=https://github.com/QuantumKitHub/PEPSKit.jl/examples/tree/gh-pages/dev/examples/$name)" + + markdown_only(x) = "#md # " * x + return join(map(markdown_only, (mybinder, nbviewer, download)), "\n") * "\n\n" * str +end + +function build_example(name) + source_dir = joinpath(@__DIR__, "..", "examples", name) + source_file = joinpath(source_dir, "main.jl") + target_dir = joinpath(@__DIR__, "..", "docs", "src", "examples", name) + + if !iscached(name) + Literate.markdown( + source_file, + target_dir; + execute=true, + name="index", + preprocess=attach_notebook_badge(name), + mdstrings=true, + nbviewer_root_url="https://nbviewer.jupyter.org/github/QuantumKitHub/PEPSKit.jl/blob/gh-pages/dev", + binder_root_url="https://mybinder.org/v2/gh/QuantumKitHub/PEPSKit.jl/gh-pages?filepath=dev", + credits=false, + repo_root_url="https://github.com/QuantumKitHub/PEPSKit.jl", + ) + Literate.notebook( + source_file, + target_dir; + execute=false, + name="main", + preprocess=str -> replace(str, r"(? "\$"), + mdstrings=true, + credits=false, + ) + + foreach(filter(!=("main.jl"), readdir(source_dir))) do f + return cp(joinpath(source_dir, f), joinpath(target_dir, f); force=true) + end + setcached(name) + end +end + +function build() + return cd(@__DIR__) do + examples = filter(isdir, readdir(@__DIR__)) # filter out directories to ignore Cache.toml, etc. + return map(ex -> build_example(ex), examples) + end +end + +# ---------------------------------------------------------------------------------------- # +# Scripts +# ---------------------------------------------------------------------------------------- # + +build() diff --git a/examples/xxz.jl b/examples/xxz.jl deleted file mode 100644 index c32b108e7..000000000 --- a/examples/xxz.jl +++ /dev/null @@ -1,52 +0,0 @@ -using Test -using Random -using PEPSKit -using TensorKit -using KrylovKit -using OptimKit - -using MPSKit: add_physical_charge - -## Néel order in the XXZ model - -# parameters -J = 1.0 -Delta = 1.0 -spin = 1//2 -symmetry = U1Irrep -lattice = InfiniteSquare(2, 2) - -# spaces -Vpeps = U1Space(0 => 2, 1 => 1, -1 => 1) -Venv = U1Space(0 => 6, 1 => 4, -1 => 4, 2 => 2, -2 => 2) -# staggered auxiliary physical charges -> encode Néel order directly in the ansatz -Saux = [ - U1Irrep(-1//2) U1Irrep(1//2) - U1Irrep(1//2) U1Irrep(-1//2) -] - -# algorithms -boundary_alg = (; tol=1e-8, alg=:simultaneous, verbosity=2, trscheme=(; alg=:fixedspace)) -gradient_alg = (; tol=1e-6, alg=:eigsolver, maxiter=10, iterscheme=:diffgauge) -optimizer_alg = (; tol=1e-4, alg=:lbfgs, verbosity=3, maxiter=100, ls_maxiter=2, ls_maxfg=2) -reuse_env = true - -# Hamiltonian -H0 = heisenberg_XXZ(ComplexF64, symmetry, lattice; J, Delta, spin) -H = add_physical_charge(H0, Saux) -Pspaces = H.lattice - -# initialize state -Nspaces = fill(Vpeps, size(lattice)...) -Espaces = fill(Vpeps, size(lattice)...) -Random.seed!(2928528935) -ψ₀ = InfinitePEPS(randn, ComplexF64, Pspaces, Nspaces, Espaces) -env₀ = CTMRGEnv(ψ₀, Venv) -env₀, = leading_boundary(env₀, ψ₀; boundary_alg...) - -# optimize -ψ, env, E, info = fixedpoint( - H, ψ₀, env₀; boundary_alg, gradient_alg, optimizer_alg, reuse_env -) -@test E < -0.666 -# ends up at E = -0.669..., but takes a while diff --git a/examples/xxz/main.jl b/examples/xxz/main.jl new file mode 100644 index 000000000..81b2fad37 --- /dev/null +++ b/examples/xxz/main.jl @@ -0,0 +1,99 @@ +using Markdown #hide +md""" +# Néel order in the $U(1)$-symmetric XXZ model + +Here, we want to look at a special case of the Heisenberg model, where the $x$ and $y$ +couplings are equal, called the XXZ model + +```math +H_0 = J \big(\sum_{\langle i, j \rangle} S_i^x S_j^x + S_i^y S_j^y + \Delta S_i^z S_j^z \big) . +``` + +For appropriate $\Delta$, the model enters an antiferromagnetic phase (Néel order) which we +will force by adding staggered magnetic charges to $H_0$. Furthermore, since the XXZ +Hamiltonian obeys a $U(1)$ symmetry, we will make use of that and work with $U(1)$-symmetric +PEPS and CTMRG environments. For simplicity, we will consider spin-$1/2$ operators. + +But first, let's make this example deterministic and import the required packages: +""" + +using Random +using TensorKit, PEPSKit +using MPSKit: add_physical_charge +Random.seed!(2928528935); + +md""" +## Constructing the model + +Let us define the $U(1)$-symmetric XXZ Hamiltonian on a $2 \times 2$ unit cell with the +parameters: +""" + +J = 1.0 +Delta = 1.0 +spin = 1//2 +symmetry = U1Irrep +lattice = InfiniteSquare(2, 2) +H₀ = heisenberg_XXZ(ComplexF64, symmetry, lattice; J, Delta, spin); + +md""" +This ensures that our PEPS ansatz can support the bipartite Néel order. As discussed above, +we encode the Néel order directly in the ansatz by adding staggered auxiliary physical +charges: +""" + +S_aux = [ + U1Irrep(-1//2) U1Irrep(1//2) + U1Irrep(1//2) U1Irrep(-1//2) +] +H = add_physical_charge(H₀, S_aux); + +md""" +## Specifying the symmetric virtual spaces + +Before we create an initial PEPS and CTM environment, we need to think about which +symmetric spaces we need to construct. Since we want to exploit the global $U(1)$ symmetry +of the model, we will use TensorKit's `U1Space`s where we specify dimensions for each +symmetry sector. From the virtual spaces, we will need to construct a unit cell (a matrix) +of spaces which will be supplied to the PEPS constructor. The same is true for the physical +spaces, which can be extracted directly from the Hamiltonian `LocalOperator`: +""" + +V_peps = U1Space(0 => 2, 1 => 1, -1 => 1) +V_env = U1Space(0 => 6, 1 => 4, -1 => 4, 2 => 2, -2 => 2) +virtual_spaces = fill(V_peps, size(lattice)...) +physical_spaces = physicalspace(H) + +md""" +## Ground state search + +From this point onwards it's business as usual: Create an initial PEPS and environment +(using the symmetric spaces), specify the algorithmic parameters and optimize: +""" + +boundary_alg = (; tol=1e-8, alg=:simultaneous, trscheme=(; alg=:fixedspace)) +gradient_alg = (; tol=1e-6, alg=:eigsolver, maxiter=10, iterscheme=:diffgauge) +optimizer_alg = (; tol=1e-4, alg=:lbfgs, maxiter=85, ls_maxiter=3, ls_maxfg=3) + +peps₀ = InfinitePEPS(randn, ComplexF64, physical_spaces, virtual_spaces) +env₀, = leading_boundary(CTMRGEnv(peps₀, V_env), peps₀; boundary_alg...); + +md""" +Finally, we can optimize the PEPS with respect to the XXZ Hamiltonian. Note that the +optimization might take a while since precompilation of symmetric AD code takes longer and +because symmetric tensors do create a bit of overhead (which does pay off at larger bond +and environment dimensions): +""" + +peps, env, E, info = fixedpoint( + H, peps₀, env₀; boundary_alg, gradient_alg, optimizer_alg, verbosity=3 +) +@show E; + +md""" +Note that for the specified parameters $J = \Delta = 1$, we simulated the same Hamiltonian as +in the [Heisenberg example](@ref examples_heisenberg). In that example, with a non-symmetric +$D=2$ PEPS simulation, we reached a ground-state energy of around $E_\text{D=2} = -0.6625\dots$. +Again comparing against [Sandvik's](@cite sandvik_computational_2011) accurate QMC estimate +``E_{\text{ref}}=−0.6694421``, we see that we already got closer to the reference energy. +""" diff --git a/src/Defaults.jl b/src/Defaults.jl index 8beb97b79..9e80248cd 100644 --- a/src/Defaults.jl +++ b/src/Defaults.jl @@ -25,12 +25,12 @@ Module containing default algorithm parameter values and arguments. * `svd_fwd_alg=:$(Defaults.svd_fwd_alg)` : SVD algorithm that is used in the forward pass. - `:sdd`: TensorKit's wrapper for LAPACK's `_gesdd` - `:svd`: TensorKit's wrapper for LAPACK's `_gesvd` - - `:iterative`: Iterative SVD only computing the specifed number of singular values and vectors, see ['IterSVD'](@ref) + - `:iterative`: Iterative SVD only computing the specifed number of singular values and vectors, see [`IterSVD`](@ref PEPSKit.IterSVD) * `svd_rrule_tol=$(Defaults.svd_rrule_tol)` : Accuracy of SVD reverse-rule. * `svd_rrule_min_krylovdim=$(Defaults.svd_rrule_min_krylovdim)` : Minimal Krylov dimension of the reverse-rule algorithm (if it is a Krylov algorithm). * `svd_rrule_verbosity=$(Defaults.svd_rrule_verbosity)` : SVD gradient output verbosity. * `svd_rrule_alg=:$(Defaults.svd_rrule_alg)` : Reverse-rule algorithm for the SVD gradient. - - `:full`: Uses a modified version of TensorKit's reverse-rule for `tsvd` which doesn't solve any linear problem and instead requires access to the full SVD, see [`FullSVDReverseRule`](@ref). + - `:full`: Uses a modified version of TensorKit's reverse-rule for `tsvd` which doesn't solve any linear problem and instead requires access to the full SVD, see [`PEPSKit.FullSVDReverseRule`](@ref). - `:gmres`: GMRES iterative linear solver, see the [KrylovKit docs](https://jutho.github.io/KrylovKit.jl/stable/man/algorithms/#KrylovKit.GMRES) for details - `:bicgstab`: BiCGStab iterative linear solver, see the [KrylovKit docs](https://jutho.github.io/KrylovKit.jl/stable/man/algorithms/#KrylovKit.BiCGStab) for details - `:arnoldi`: Arnoldi Krylov algorithm, see the [KrylovKit docs](https://jutho.github.io/KrylovKit.jl/stable/man/algorithms/#KrylovKit.Arnoldi) for details @@ -39,8 +39,8 @@ Module containing default algorithm parameter values and arguments. ## Projectors * `projector_alg=:$(Defaults.projector_alg)` : Default variant of the CTMRG projector algorithm. - - `halfinfinite`: Projection via SVDs of half-infinite (two enlarged corners) CTMRG environments. - - `fullinfinite`: Projection via SVDs of full-infinite (all four enlarged corners) CTMRG environments. + - `:halfinfinite`: Projection via SVDs of half-infinite (two enlarged corners) CTMRG environments. + - `:fullinfinite`: Projection via SVDs of full-infinite (all four enlarged corners) CTMRG environments. * `projector_verbosity=$(Defaults.projector_verbosity)` : Projector output information verbosity. ## Fixed-point gradient @@ -49,10 +49,10 @@ Module containing default algorithm parameter values and arguments. * `gradient_maxiter=$(Defaults.gradient_maxiter)` : Maximal number of iterations for computing the CTMRG fixed-point gradient. * `gradient_verbosity=$(Defaults.gradient_verbosity)` : Gradient output information verbosity. * `gradient_linsolver=:$(Defaults.gradient_linsolver)` : Default linear solver for the `LinSolver` gradient algorithm. - - `:gmres` : GMRES iterative linear solver, see the [KrylovKit docs](https://jutho.github.io/KrylovKit.jl/stable/man/algorithms/#KrylovKit.GMRES) for details - - `:bicgstab` : BiCGStab iterative linear solver, see the [KrylovKit docs](https://jutho.github.io/KrylovKit.jl/stable/man/algorithms/#KrylovKit.BiCGStab) for details + - `:gmres` : GMRES iterative linear solver, see [`KrylovKit.GMRES`](@extref) for details + - `:bicgstab` : BiCGStab iterative linear solver, see [`KrylovKit.BiCGStab`](@extref) for details * `gradient_eigsolver=:$(Defaults.gradient_eigsolver)` : Default eigensolver for the `EigSolver` gradient algorithm. - - `:arnoldi` : Arnoldi Krylov algorithm, see the [KrylovKit docs](https://jutho.github.io/KrylovKit.jl/stable/man/algorithms/#KrylovKit.Arnoldi) for details + - `:arnoldi` : Arnoldi Krylov algorithm, see [`KrylovKit.Arnoldi`](@extref) for details * `gradient_eigsolver_eager=$(Defaults.gradient_eigsolver_eager)` : Enables `EigSolver` algorithm to finish before the full Krylov dimension is reached. * `gradient_iterscheme=:$(Defaults.gradient_iterscheme)` : Scheme for differentiating one CTMRG iteration. - `:fixed` : the differentiated CTMRG iteration uses a pre-computed SVD with a fixed set of gauges @@ -75,7 +75,7 @@ Module containing default algorithm parameter values and arguments. ## OhMyThreads scheduler -- `scheduler=Ref{Scheduler}(...)` : Multi-threading scheduler which can be accessed via `set_scheduler!`. +- `scheduler=Ref{Scheduler}(...)` : Multithreading scheduler which can be accessed via `set_scheduler!`. """ module Defaults @@ -131,7 +131,7 @@ const scheduler = Ref{Scheduler}() """ set_scheduler!([scheduler]; kwargs...) -Set `OhMyThreads` multi-threading scheduler parameters. +Set `OhMyThreads` multithreading scheduler parameters. The function either accepts a `scheduler` as an `OhMyThreads.Scheduler` or as a symbol where the corresponding parameters are specificed as keyword arguments. @@ -145,7 +145,7 @@ or equivalently with set_scheduler!(:static; ntasks=4, chunking=true) ``` For a detailed description of all schedulers and their keyword arguments consult the -[`OhMyThreads` documentation](https://juliafolds2.github.io/OhMyThreads.jl/stable/refs/api/#Schedulers). +[OhMyThreads](https://juliafolds2.github.io/OhMyThreads.jl/stable/refs/api/#OhMyThreads.Schedulers.Scheduler) documentation. If no `scheduler` is passed and only kwargs are provided, the `DynamicScheduler` constructor is used with the provided kwargs. diff --git a/src/PEPSKit.jl b/src/PEPSKit.jl index 1e8153a5a..6d053d3fa 100644 --- a/src/PEPSKit.jl +++ b/src/PEPSKit.jl @@ -11,6 +11,7 @@ import MPSKit: leading_boundary, loginit!, logiter!, logfinish!, logcancel!, phy using MPSKitModels using FiniteDifferences using OhMyThreads: tmap +using DocStringExtensions include("Defaults.jl") # Include first to allow for docstring interpolation with Defaults values @@ -74,7 +75,7 @@ export set_scheduler! export SVDAdjoint, FullSVDReverseRule, IterSVD export CTMRGEnv, SequentialCTMRG, SimultaneousCTMRG export FixedSpaceTruncation, HalfInfiniteProjector, FullInfiniteProjector -export LocalOperator +export LocalOperator, physicalspace export expectation_value, cost_function, product_peps, correlation_length, network_value export leading_boundary export PEPSOptimize, GeomSum, ManualIter, LinSolver, EigSolver @@ -94,7 +95,8 @@ export ReflectDepth, ReflectWidth, Rotate, RotateReflect export symmetrize!, symmetrize_retract_and_finalize! export showtypeofgrad export InfiniteSquare, vertices, nearest_neighbours, next_nearest_neighbours -export transverse_field_ising, heisenberg_XYZ, heisenberg_XXZ, j1_j2, bose_hubbard_model +export transverse_field_ising, + heisenberg_XYZ, heisenberg_XXZ, j1_j2_model, bose_hubbard_model export pwave_superconductor, hubbard_model, tj_model end # module diff --git a/src/algorithms/contractions/bondenv/als_solve.jl b/src/algorithms/contractions/bondenv/als_solve.jl index 486bb3407..1bc06b9d9 100644 --- a/src/algorithms/contractions/bondenv/als_solve.jl +++ b/src/algorithms/contractions/bondenv/als_solve.jl @@ -4,6 +4,8 @@ the fast full update article Physical Review B 92, 035142 (2015) =# """ +$(SIGNATURES) + Construct the tensor ``` ┌-----------------------------------┐ @@ -26,6 +28,8 @@ function _tensor_Ra( end """ +$(SIGNATURES) + Construct the tensor ``` ┌-----------------------------------┐ @@ -48,6 +52,8 @@ function _tensor_Sa( end """ +$(SIGNATURES) + Construct the tensor ``` ┌-----------------------------------┐ @@ -70,6 +76,8 @@ function _tensor_Rb( end """ +$(SIGNATURES) + Construct the tensor ``` ┌-----------------------------------┐ @@ -92,6 +100,8 @@ function _tensor_Sb( end """ +$(SIGNATURES) + Calculate the inner product ``` ┌--------------------------------┐ @@ -114,6 +124,8 @@ function inner_prod( end """ +$(SIGNATURES) + Calculate the fidelity between two evolution steps ``` |⟨a1,b1|a2,b2⟩|^2 @@ -131,6 +143,8 @@ function fidelity( end """ +$(SIGNATURES) + Contract the axis between `a` and `b` tensors ``` -- DX - a - D - b - DY -- @@ -150,6 +164,8 @@ function _combine_ab( end """ +$(SIGNATURES) + Calculate the cost function ``` f(a,b) = ‖ |a1,b1⟩ - |a2,b2⟩ ‖^2 @@ -166,6 +182,8 @@ function cost_function_als( end """ +$(SIGNATURES) + Solve the equations `Rx x = Sx` (x = a, b) with initial guess `x0` ``` ┌---------------------------┐ diff --git a/src/algorithms/contractions/ctmrg_contractions.jl b/src/algorithms/contractions/ctmrg_contractions.jl index c031f6849..e08c681d1 100644 --- a/src/algorithms/contractions/ctmrg_contractions.jl +++ b/src/algorithms/contractions/ctmrg_contractions.jl @@ -7,8 +7,7 @@ const CTMRGCornerTensor{T,S} = AbstractTensorMap{T,S,1,1} # ---------------------------- """ - enlarge_northwest_corner((row, col), env, network::InfiniteSquareNetwork{O}) - enlarge_northwest_corner(E_west, C_northwest, E_north, A::O) +$(SIGNATURES) Contract the enlarged northwest corner of the CTMRG environment, either by specifying the coordinates, environments and network, or by directly providing the tensors. @@ -44,8 +43,7 @@ function enlarge_northwest_corner( end """ - enlarge_northeast_corner((row, col), env, network::InfiniteSquareNetwork{O}) - enlarge_northeast_corner(E_north, C_northeast, E_east, A::O) +$(SIGNATURES) Contract the enlarged northeast corner of the CTMRG environment, either by specifying the coordinates, environments and network, or by directly providing the tensors. @@ -81,8 +79,7 @@ function enlarge_northeast_corner( end """ - enlarge_southeast_corner((row, col), env, network::InfiniteSquareNetwork{O}) - enlarge_southeast_corner(E_east, C_southeast, E_south, A::O) +$(SIGNATURES) Contract the enlarged southeast corner of the CTMRG environment, either by specifying the coordinates, environments and network, or by directly providing the tensors. @@ -118,8 +115,7 @@ function enlarge_southeast_corner( end """ - enlarge_southwest_corner((row, col), env, network::InfiniteSquareNetwork{O}) - enlarge_southwest_corner(E_south, C_southwest, E_west, A::O) +$(SIGNATURES) Contract the enlarged southwest corner of the CTMRG environment, either by specifying the coordinates, environments and network, or by directly providing the tensors. @@ -158,7 +154,7 @@ end # ---------------------- """ - left_projector(E_1, C, E_2, V, isqS, A) +$(SIGNATURES) Contract the CTMRG left projector with the higher-dimensional subspace facing to the left. @@ -190,7 +186,7 @@ function left_projector(E_1, C, E_2, V, isqS, A::PFTensor) end """ - right_projector(E_1, C, E_2, U, isqS, A) +$(SIGNATURES) Contract the CTMRG right projector with the higher-dimensional subspace facing to the right. @@ -222,7 +218,7 @@ function right_projector(E_1, C, E_2, U, isqS, A::PFTensor) end """ - contract_projectors(U, S, V, Q, Q_next) +$(SIGNATURES) Compute projectors based on a SVD of `Q * Q_next`, where the inverse square root `isqS` of the singular values is computed. @@ -249,10 +245,10 @@ function contract_projectors(U, S, V, Q, Q_next) end """ - half_infinite_environment(quadrant1::AbstractTensorMap{T,S,3,3}, quadrant2::AbstractTensorMap{T,S,N,N}) - half_infinite_environment(C_1, C_2, E_1, E_2, E_3, E_4, A_1::P, A_2::P) - half_infinite_environment(C_1, C_2, E_1, E_2, E_3, E_4, x, A_1::P, A_2::P) - half_infinite_environment(x, C_1, C_2, E_1, E_2, E_3, E_4, A_1::P, A_2::P) + half_infinite_environment(quadrant1, quadrant2) + half_infinite_environment(C_1, C_2, E_1, E_2, E_3, E_4, A_1, A_2) + half_infinite_environment(C_1, C_2, E_1, E_2, E_3, E_4, x, A_1, A_2) + half_infinite_environment(x, C_1, C_2, E_1, E_2, E_3, E_4, A_1, A_2) Contract two quadrants (enlarged corners) to form a half-infinite environment. @@ -380,18 +376,11 @@ function half_infinite_environment( end """ - full_infinite_environment( - quadrant1::T, quadrant2::T, quadrant3::T, quadrant4::T - ) where {T<:AbstractTensorMap{<:Number,<:ElementarySpace,N,N}} - function full_infinite_environment( - half1::T, half2::T - ) where {T<:AbstractTensorMap{<:Number,<:ElementarySpace,N,N}} - full_infinite_environment(C_1, C_2, C_3, C_4, E_1, E_2, E_3, E_4, E_5, E_6, E_7, E_8, - A_1::P, A_2::P, A_3::P, A_4::P) - full_infinite_environment(C_1, C_2, E_1, E_2, E_3, E_4, x, - A_1::P, A_2::P, A_3::P, A_4::P) - full_infinite_environment(x, C_1, C_2, E_1, E_2, E_3, E_4, - A_1::P, A_2::P, A_3::P, A_4::P) + full_infinite_environment(quadrant1, quadrant2, quadrant3, quadrant4) + full_infinite_environment(half1, half2) + full_infinite_environment(C_1, C_2, C_3, C_4, E_1, E_2, E_3, E_4, E_5, E_6, E_7, E_8, A_1, A_2, A_3, A_4) + full_infinite_environment(C_1, C_2, E_1, E_2, E_3, E_4, x, A_1, A_2, A_3, A_4) + full_infinite_environment(x, C_1, C_2, E_1, E_2, E_3, E_4, A_1, A_2, A_3, A_4) Contract four quadrants (enlarged corners) to form a full-infinite environment. @@ -740,8 +729,7 @@ end # corners """ - renormalize_corner(quadrant::AbstractTensorMap{T,S,3,3}, P_left, P_right) - renormalize_corner(quadrant::AbstractTensorMap{T,S,2,2}, P_left, P_right) +$(SIGNATURES) Apply projectors to each side of a quadrant. @@ -782,11 +770,21 @@ Apply projectors to each side of a quadrant. end """ - renormalize_northwest_corner((row, col), enlarged_env::CTMRGEnv, P_left, P_right) - renormalize_northwest_corner(quadrant::AbstractTensorMap{T,S,N,N}, P_left, P_right) where {T,S,N} - renormalize_northwest_corner(E_west, C_northwest, E_north, P_left, P_right, A::O) + renormalize_northwest_corner((row, col), enlarged_env, P_left, P_right) + renormalize_northwest_corner(quadrant, P_left, P_right) + renormalize_northwest_corner(E_west, C_northwest, E_north, P_left, P_right, A) Apply `renormalize_corner` to the enlarged northwest corner. + +``` + |~~~~~~~~| -- |~~~~~~| + |quadrant| |P_left| -- + |~~~~~~~~| -- |~~~~~~| + | | + [P_right] + | +``` + Alternatively, provide the constituent tensors and perform the complete contraction. ``` @@ -835,11 +833,21 @@ function renormalize_northwest_corner( end """ - renormalize_northeast_corner((row, col), enlarged_env::CTMRGEnv, P_left, P_right) - renormalize_northeast_corner(quadrant::AbstractTensorMap{T,S,N,N}, P_left, P_right) where {T,S,N} - renormalize_northeast_corner(E_north, C_northeast, E_east, P_left, P_right, A::O) + renormalize_northeast_corner((row, col), enlarged_env, P_left, P_right) + renormalize_northeast_corner(quadrant, P_left, P_right) + renormalize_northeast_corner(E_north, C_northeast, E_east, P_left, P_right, A) Apply `renormalize_corner` to the enlarged northeast corner. + +``` + |~~~~~~~| -- |~~~~~~~~| + -- |P_right| |quadrant| + |~~~~~~~| -- |~~~~~~~~| + | | + [P_left] + | +``` + Alternatively, provide the constituent tensors and perform the complete contraction. ``` @@ -890,11 +898,21 @@ function renormalize_northeast_corner( end """ - renormalize_southeast_corner((row, col), enlarged_env::CTMRGEnv, P_left, P_right) - renormalize_southeast_corner(quadrant::AbstractTensorMap{T,S,N,N}, P_left, P_right) where {T,S,N} - renormalize_southeast_corner(E_east, C_southeast, E_south, P_left, P_right, A::O) + renormalize_southeast_corner((row, col), enlarged_env, P_left, P_right) + renormalize_southeast_corner(quadrant, P_left, P_right) + renormalize_southeast_corner(E_east, C_southeast, E_south, P_left, P_right, A) Apply `renormalize_corner` to the enlarged southeast corner. + +``` + | + [P_right] + | | + |~~~~~~| -- |~~~~~~~~| + -- |P_left| |quadrant| + |~~~~~~| -- |~~~~~~~~| +``` + Alternatively, provide the constituent tensors and perform the complete contraction. ``` @@ -943,20 +961,30 @@ function renormalize_southeast_corner( end """ - renormalize_southwest_corner((row, col), enlarged_env::CTMRGEnv, P_left, P_right) - renormalize_southwest_corner(quadrant::AbstractTensorMap{T,S,N,N}, P_left, P_right) where {T,S,N} - renormalize_southwest_corner(E_south, C_southwest, E_west, P_left, P_right, A::O) + renormalize_southwest_corner((row, col), enlarged_env, P_left, P_right) + renormalize_southwest_corner(quadrant, P_left, P_right) + renormalize_southwest_corner(E_south, C_southwest, E_west, P_left, P_right, A) Apply `renormalize_corner` to the enlarged southwest corner. + +``` + | + [P_left] + | | + |~~~~~~~~| -- |~~~~~~| + |quadrant| |P_left| -- + |~~~~~~~~| -- |~~~~~~| +``` + Alternatively, provide the constituent tensors and perform the complete contraction. ``` | - [~~~~P_right~~~~~] + [~~~~~P_left~~~~~] | | - E_west -- A -- |~~~~~~| - | | |P_left| -- - C_southwest -- E_south -- |~~~~~~| + E_west -- A -- |~~~~~~~| + | | |P_right| -- + C_southwest -- E_south -- |~~~~~~~| ``` """ function renormalize_southwest_corner((row, col), enlarged_env, P_left, P_right) @@ -997,6 +1025,7 @@ end """ renormalize_bottom_corner((r, c), env, projectors) + renormalize_bottom_corner(C_southwest, E_south, P_bottom) Apply bottom projector to southwest corner and south edge. ``` @@ -1036,7 +1065,8 @@ end end """ - renormalize_top_corner((row, col), env::CTMRGEnv, projectors) + renormalize_top_corner((row, col), env, projectors) + renormalize_top_corner(C_northwest, E_north, P_top) Apply top projector to northwest corner and north edge. ``` @@ -1260,7 +1290,7 @@ end # corners """ - fix_gauge_corner(corner, σ_in, σ_out) +$(SIGNATURES) Multiply corner tensor with incoming and outgoing gauge signs. @@ -1279,7 +1309,7 @@ function fix_gauge_corner( end """ - fix_gauge_northwest_corner((row, col), env, signs) +$(SIGNATURES) Apply `fix_gauge_corner` to the northwest corner with appropriate row and column indices. """ @@ -1292,7 +1322,7 @@ function fix_gauge_northwest_corner((row, col), env::CTMRGEnv, signs) end """ - fix_gauge_northeast_corner((row, col), env, signs) +$(SIGNATURES) Apply `fix_gauge_corner` to the northeast corner with appropriate row and column indices. """ @@ -1305,7 +1335,7 @@ function fix_gauge_northeast_corner((row, col), env::CTMRGEnv, signs) end """ - fix_gauge_southeast_corner((row, col), env, signs) +$(SIGNATURES) Apply `fix_gauge_corner` to the southeast corner with appropriate row and column indices. """ @@ -1318,7 +1348,7 @@ function fix_gauge_southeast_corner((row, col), env::CTMRGEnv, signs) end """ - fix_gauge_southwest_corner((row, col), env, signs) +$(SIGNATURES) Apply `fix_gauge_corner` to the southwest corner with appropriate row and column indices. """ @@ -1333,7 +1363,7 @@ end # edges """ - fix_gauge_edge(edge, σ_in, σ_out) +$(SIGNATURES) Multiply edge tensor with incoming and outgoing gauge signs. @@ -1361,7 +1391,7 @@ Multiply edge tensor with incoming and outgoing gauge signs. end """ - fix_gauge_north_edge((row, col), env, signs) +$(SIGNATURES) Apply `fix_gauge_edge` to the north edge with appropriate row and column indices. """ @@ -1374,7 +1404,7 @@ function fix_gauge_north_edge((row, col), env::CTMRGEnv, signs) end """ - fix_gauge_east_edge((row, col), env, signs) +$(SIGNATURES) Apply `fix_gauge_edge` to the east edge with appropriate row and column indices. """ @@ -1385,7 +1415,7 @@ function fix_gauge_east_edge((row, col), env::CTMRGEnv, signs) end """ - fix_gauge_south_edge((row, col), env, signs) +$(SIGNATURES) Apply `fix_gauge_edge` to the south edge with appropriate row and column indices. """ @@ -1398,7 +1428,7 @@ function fix_gauge_south_edge((row, col), env::CTMRGEnv, signs) end """ - fix_gauge_south_edge((row, col), env, signs) +$(SIGNATURES) Apply `fix_gauge_edge` to the west edge with appropriate row and column indices. """ @@ -1411,7 +1441,7 @@ end # left singular vectors """ - fix_gauge_north_left_vecs((row, col), U, signs) +$(SIGNATURES) Multiply north left singular vectors with gauge signs from the right. """ @@ -1420,7 +1450,7 @@ function fix_gauge_north_left_vecs((row, col), U, signs) end """ - fix_gauge_east_left_vecs((row, col), U, signs) +$(SIGNATURES) Multiply east left singular vectors with gauge signs from the right. """ @@ -1429,7 +1459,7 @@ function fix_gauge_east_left_vecs((row, col), U, signs) end """ - fix_gauge_south_left_vecs((row, col), U, signs) +$(SIGNATURES) Multiply south left singular vectors with gauge signs from the right. """ @@ -1438,7 +1468,7 @@ function fix_gauge_south_left_vecs((row, col), U, signs) end """ - fix_gauge_west_left_vecs((row, col), U, signs) +$(SIGNATURES) Multiply west left singular vectors with gauge signs from the right. """ @@ -1449,7 +1479,7 @@ end # right singular vectors """ - fix_gauge_north_right_vecs((row, col), V, signs) +$(SIGNATURES) Multiply north right singular vectors with gauge signs from the left. """ @@ -1458,7 +1488,7 @@ function fix_gauge_north_right_vecs((row, col), V, signs) end """ - fix_gauge_east_right_vecs((row, col), V, signs) +$(SIGNATURES) Multiply east right singular vectors with gauge signs from the left. """ @@ -1467,7 +1497,7 @@ function fix_gauge_east_right_vecs((row, col), V, signs) end """ - fix_gauge_south_right_vecs((row, col), V, signs) +$(SIGNATURES) Multiply south right singular vectors with gauge signs from the left. """ @@ -1476,7 +1506,7 @@ function fix_gauge_south_right_vecs((row, col), V, signs) end """ - fix_gauge_west((row, col), V, signs) +$(SIGNATURES) Multiply west right singular vectors with gauge signs from the left. """ @@ -1675,7 +1705,7 @@ function _pepo_domain_projector_expr( end # -# Contractions +# PEPO Contractions # ## Site contraction diff --git a/src/algorithms/contractions/localoperator.jl b/src/algorithms/contractions/localoperator.jl index 3889819d6..c04cb8443 100644 --- a/src/algorithms/contractions/localoperator.jl +++ b/src/algorithms/contractions/localoperator.jl @@ -19,9 +19,13 @@ virtuallabel(args...) = tensorlabel(:D, args...) physicallabel(args...) = tensorlabel(:d, args...) """ - contract_local_operator(inds, O, peps, env) +$(SIGNATURES) Contract a local operator `O` on the PEPS `peps` at the indices `inds` using the environment `env`. + +This works by generating the appropriate contraction on a rectangular patch with its corners +specified by `inds`. The `peps` is contracted with `O` from above and below, and the PEPS-operator +sandwich is surrounded with the appropriate environment tensors. """ function contract_local_operator( inds::NTuple{N,CartesianIndex{2}}, @@ -214,9 +218,13 @@ end end """ - contract_local_norm(inds, peps, env) +$(SIGNATURES) Contract a local norm of the PEPS `peps` around indices `inds`. + +This works analogously to [`contract_local_operator`](@ref) by generating the contraction +on a rectangular patch based on `inds` but replacing the operator with an identity such +that the PEPS norm is computed. (Note that this is not the physical norm of the state.) """ function contract_local_norm( inds::NTuple{N,CartesianIndex{2}}, ket::InfinitePEPS, bra::InfinitePEPS, env::CTMRGEnv diff --git a/src/algorithms/ctmrg/ctmrg.jl b/src/algorithms/ctmrg/ctmrg.jl index 8fcceff10..0c688f3be 100644 --- a/src/algorithms/ctmrg/ctmrg.jl +++ b/src/algorithms/ctmrg/ctmrg.jl @@ -1,5 +1,5 @@ """ - CTMRGAlgorithm +$(TYPEDEF) Abstract super type for the corner transfer matrix renormalization group (CTMRG) algorithm for contracting infinite PEPS. @@ -8,6 +8,11 @@ abstract type CTMRGAlgorithm end const CTMRG_SYMBOLS = IdDict{Symbol,Type{<:CTMRGAlgorithm}}() +""" + CTMRGAlgorithm(; kwargs...) + +Keyword argument parser returning the appropriate `CTMRGAlgorithm` algorithm struct. +""" function CTMRGAlgorithm(; alg=Defaults.ctmrg_alg, tol=Defaults.ctmrg_tol, @@ -39,7 +44,7 @@ Perform a single CTMRG iteration in which all directions are being grown and ren function ctmrg_iteration(network, env, alg::CTMRGAlgorithm) end """ - leading_boundary(env₀, network; kwargs...) + leading_boundary(env₀, network; kwargs...) -> env, info # expert version: leading_boundary(env₀, network, alg::CTMRGAlgorithm) @@ -74,8 +79,29 @@ supplied via the keyword arguments or directly as an [`CTMRGAlgorithm`](@ref) st - `:truncbelow` : Additionally supply singular value cutoff `η`; truncate such that every retained singular value is larger than `η` * `svd_alg::Union{<:SVDAdjoint,NamedTuple}` : SVD algorithm for computing projectors. See also [`SVDAdjoint`](@ref). By default, a reverse-rule tolerance of `tol=1e1tol` where the `krylovdim` is adapted to the `env₀` environment dimension. * `projector_alg::Symbol=:$(Defaults.projector_alg)` : Variant of the projector algorithm. See also [`ProjectorAlgorithm`](@ref). - - `halfinfinite`: Projection via SVDs of half-infinite (two enlarged corners) CTMRG environments. - - `fullinfinite`: Projection via SVDs of full-infinite (all four enlarged corners) CTMRG environments. + - `:halfinfinite` : Projection via SVDs of half-infinite (two enlarged corners) CTMRG environments. + - `:fullinfinite` : Projection via SVDs of full-infinite (all four enlarged corners) CTMRG environments. + +## Return values + +The CTMRG routine returns the final CTMRG environment as well as an information `NamedTuple` +containing the following fields: + +* `truncation_error` : Last (maximal) SVD truncation error of the CTMRG projectors. +* `condition_number` : Last (maximal) condition number of the enlarged CTMRG environment. + +In case the `alg` is a `SimultaneousCTMRG`, the last SVD will also be returned: + +* `U` : Last unit cell of left singular vectors. +* `S` : Last unit cell of singular values. +* `V` : Last unit cell of right singular vectors. + +If, in addition, the specified SVD algorithm computes the full, untruncated SVD, the full +set of vectors and values will be returned as well: + +* `U_full` : Last unit cell of all left singular vectors. +* `S_full` : Last unit cell of all singular values. +* `V_full` : Last unit cell of all right singular vectors. """ function leading_boundary(env₀::CTMRGEnv, network::InfiniteSquareNetwork; kwargs...) alg = select_algorithm(leading_boundary, env₀; kwargs...) @@ -134,13 +160,15 @@ end @non_differentiable ctmrg_logfinish!(args...) @non_differentiable ctmrg_logcancel!(args...) -#= -In order to compute an error measure, we compare the singular values of the current iteration with the previous one. -However, when the virtual spaces change, this comparison is not directly possible. -Instead, we project both tensors into the smaller space and then compare the difference. +# TODO: we might want to consider embedding the smaller tensor into the larger space and then compute the difference +""" + _singular_value_distance((S₁, S₂)) -TODO: we might want to consider embedding the smaller tensor into the larger space and then compute the difference -=# +Compute the singular value distance as an error measure, e.g. for CTMRG iterations. +To that end, the singular values of the current iteration `S₁` are compared with the +previous one `S₂`. When the virtual spaces change, this comparison is not directly possible +such that both tensors are projected into the smaller space and then subtracted. +""" function _singular_value_distance((S₁, S₂)) V₁ = space(S₁, 1) V₂ = space(S₂, 1) @@ -156,7 +184,7 @@ end """ calc_convergence(env, CS_old, TS_old) - calc_convergence(env_new::CTMRGEnv, env_old::CTMRGEnv) + calc_convergence(env_new, env_old) Given a new environment `env`, compute the maximal singular value distance. This determined either from the previous corner and edge singular values diff --git a/src/algorithms/ctmrg/gaugefix.jl b/src/algorithms/ctmrg/gaugefix.jl index 96d783393..af9eec742 100644 --- a/src/algorithms/ctmrg/gaugefix.jl +++ b/src/algorithms/ctmrg/gaugefix.jl @@ -1,5 +1,5 @@ """ - gauge_fix(envprev::CTMRGEnv{C,T}, envfinal::CTMRGEnv{C,T}) where {C,T} +$(SIGNATURES) Fix the gauge of `envfinal` based on the previous environment `envprev`. This assumes that the `envfinal` is the result of one CTMRG iteration on `envprev`. @@ -139,7 +139,13 @@ function fix_relative_phases( return U_fixed, V_fixed end -# Fix global phases of corners and edges via dot product (to ensure compatibility with symm. tensors) +""" +$(SIGNATURES) + +Fix global multiplicative phase of the environment tensors. To that end, the dot products +between all corners and all edges are computed to obtain the global phase which is then +divided out. +""" function fix_global_phases(envprev::CTMRGEnv, envfix::CTMRGEnv) cornersgfix = map(zip(envprev.corners, envfix.corners)) do (Cprev, Cfix) φ = dot(Cprev, Cfix) @@ -156,7 +162,7 @@ end calc_elementwise_convergence(envfinal, envfix; atol=1e-6) Check if the element-wise difference of the corner and edge tensors of the final and fixed -CTMRG environments are below some tolerance. +CTMRG environments are below `atol` and return the maximal difference. """ function calc_elementwise_convergence(envfinal::CTMRGEnv, envfix::CTMRGEnv; atol::Real=1e-6) ΔC = envfinal.corners .- envfix.corners diff --git a/src/algorithms/ctmrg/projectors.jl b/src/algorithms/ctmrg/projectors.jl index d74acf733..d9a6836a1 100644 --- a/src/algorithms/ctmrg/projectors.jl +++ b/src/algorithms/ctmrg/projectors.jl @@ -1,5 +1,5 @@ """ - ProjectorAlgorithm +$(TYPEDEF) Abstract super type for all CTMRG projector algorithms. """ @@ -7,6 +7,11 @@ abstract type ProjectorAlgorithm end const PROJECTOR_SYMBOLS = IdDict{Symbol,Type{<:ProjectorAlgorithm}}() +""" + ProjectorAlgorithm(; kwargs...) + +Keyword argument parser returning the appropriate `ProjectorAlgorithm` algorithm struct. +""" function ProjectorAlgorithm(; alg=Defaults.projector_alg, svd_alg=(;), @@ -70,15 +75,21 @@ function truncation_scheme(alg::ProjectorAlgorithm, edge) end """ - struct HalfInfiniteProjector{S,T} <: ProjectorAlgorithm - HalfInfiniteProjector(; kwargs...) - +$(TYPEDEF) Projector algorithm implementing projectors from SVDing the half-infinite CTMRG environment. -## Keyword arguments +## Fields + +$(TYPEDFIELDS) + +## Constructors + + HalfInfiniteProjector(; kwargs...) -* `svd_alg::Union{<:SVDAdjoint,NamedTuple}=SVDAdjoint()` : SVD algorithm including the reverse rule. See ['SVDAdjoint'](@ref). +Construct the half-infinite projector algorithm based on the following keyword arguments: + +* `svd_alg::Union{<:SVDAdjoint,NamedTuple}=SVDAdjoint()` : SVD algorithm including the reverse rule. See [`SVDAdjoint`](@ref). * `trscheme::Union{TruncationScheme,NamedTuple}=(; alg::Symbol=:$(Defaults.trscheme))` : Truncation scheme for the projector computation, which controls the resulting virtual spaces. Here, `alg` can be one of the following: - `:fixedspace` : Keep virtual spaces fixed during projection - `:notrunc` : No singular values are truncated and the performed SVDs are exact @@ -102,14 +113,21 @@ end PROJECTOR_SYMBOLS[:halfinfinite] = HalfInfiniteProjector """ - struct FullInfiniteProjector{S,T} <: ProjectorAlgorithm - FullInfiniteProjector(; kwargs...) +$(TYPEDEF) Projector algorithm implementing projectors from SVDing the full 4x4 CTMRG environment. -## Keyword arguments +## Fields + +$(TYPEDFIELDS) + +## Constructors + + FullInfiniteProjector(; kwargs...) + +Construct the full-infinite projector algorithm based on the following keyword arguments: -* `svd_alg::Union{<:SVDAdjoint,NamedTuple}=SVDAdjoint()` : SVD algorithm including the reverse rule. See ['SVDAdjoint'](@ref). +* `svd_alg::Union{<:SVDAdjoint,NamedTuple}=SVDAdjoint()` : SVD algorithm including the reverse rule. See [`SVDAdjoint`](@ref). * `trscheme::Union{TruncationScheme,NamedTuple}=(; alg::Symbol=:$(Defaults.trscheme))` : Truncation scheme for the projector computation, which controls the resulting virtual spaces. Here, `alg` can be one of the following: - `:fixedspace` : Keep virtual spaces fixed during projection - `:notrunc` : No singular values are truncated and the performed SVDs are exact diff --git a/src/algorithms/ctmrg/sequential.jl b/src/algorithms/ctmrg/sequential.jl index 649dffb89..6925192f4 100644 --- a/src/algorithms/ctmrg/sequential.jl +++ b/src/algorithms/ctmrg/sequential.jl @@ -1,13 +1,19 @@ """ struct SequentialCTMRG <: CTMRGAlgorithm - SequentialCTMRG(; kwargs...) CTMRG algorithm where the expansions and renormalization is performed sequentially column-wise. This is implemented as a growing and projecting step to the left, followed by a clockwise rotation (performed four times). -## Keyword arguments +## Fields + +$(TYPEDFIELDS) + +## Constructors + + SequentialCTMRG(; kwargs...) +Construct a sequential CTMRG algorithm struct based on keyword arguments. For a full description, see [`leading_boundary`](@ref). The supported keywords are: * `tol::Real=$(Defaults.ctmrg_tol)` diff --git a/src/algorithms/ctmrg/simultaneous.jl b/src/algorithms/ctmrg/simultaneous.jl index 6bb0e64ea..76a37bffe 100644 --- a/src/algorithms/ctmrg/simultaneous.jl +++ b/src/algorithms/ctmrg/simultaneous.jl @@ -1,12 +1,18 @@ """ - struct SimultaneousCTMRG <: CTMRGAlgorithm - SimultaneousCTMRG(; kwargs...) +$(TYPEDEF) CTMRG algorithm where all sides are grown and renormalized at the same time. In particular, the projectors are applied to the corners from two sides simultaneously. -## Keyword arguments +## Fields + +$(TYPEDFIELDS) + +## Constructors + + SimultaneousCTMRG(; kwargs...) +Construct a simultaneous CTMRG algorithm struct based on keyword arguments. For a full description, see [`leading_boundary`](@ref). The supported keywords are: * `tol::Real=$(Defaults.ctmrg_tol)` @@ -99,7 +105,7 @@ function simultaneous_projectors( end """ - renormalize_simultaneously(enlarged_corners, projectors, network, env) +$(SIGNATURES) Renormalize all enlarged corners and edges simultaneously. """ diff --git a/src/algorithms/ctmrg/sparse_environments.jl b/src/algorithms/ctmrg/sparse_environments.jl index bf0f8d70f..2601aaec2 100644 --- a/src/algorithms/ctmrg/sparse_environments.jl +++ b/src/algorithms/ctmrg/sparse_environments.jl @@ -3,9 +3,17 @@ # -------------------------------------------------------- """ - struct EnlargedCorner{TC,TE,TA} +$(TYPEDEF) Enlarged CTMRG corner tensor storage. + +## Constructors + + EnlargedCorner(network::InfiniteSquareNetwork, env, coordinates) + +Construct an enlarged corner with the correct row and column indices based on the given +`coordinates` which are of the form `(dir, row, col)`. + """ struct EnlargedCorner{TC,TE,TA} C::TC @@ -13,13 +21,6 @@ struct EnlargedCorner{TC,TE,TA} E_2::TE A::TA end - -""" - EnlargedCorner(network::InfiniteSquareNetwork, env, coordinates) - -Construct an enlarged corner with the correct row and column indices based on the given -`coordinates` which are of the form `(dir, row, col)`. -""" function EnlargedCorner(network::InfiniteSquareNetwork, env, coordinates) dir, r, c = coordinates if dir == NORTHWEST @@ -54,7 +55,7 @@ function EnlargedCorner(network::InfiniteSquareNetwork, env, coordinates) end """ - TensorKit.TensorMap(Q::EnlargedCorner, dir::Int) + TensorMap(Q::EnlargedCorner, dir::Int) Instantiate enlarged corner as `TensorMap` where `dir` selects the correct contraction direction, i.e. the way the environment and PEPS tensors connect. @@ -92,10 +93,8 @@ end # Compute left and right projectors sparsely without constructing enlarged corners explicitly function left_and_right_projector(U, S, V, Q::EnlargedCorner, Q_next::EnlargedCorner) isqS = sdiag_pow(S, -0.5) - P_left = left_projector(Q.E_1, Q.C, Q.E_2, V, isqS, Q.ket, Q.bra) - P_right = right_projector( - Q_next.E_1, Q_next.C, Q_next.E_2, U, isqS, Q_next.ket, Q_next.bra - ) + P_left = left_projector(Q.E_1, Q.C, Q.E_2, V, isqS, Q.A) + P_right = right_projector(Q_next.E_1, Q_next.C, Q_next.E_2, U, isqS, Q_next.A) return P_left, P_right end @@ -104,9 +103,19 @@ end # -------------------------------- """ - struct HalfInfiniteEnv{C,E,A,A′} +$(TYPEDEF) Half-infinite CTMRG environment tensor storage. + +## Fields + +$(FIELDS) + +## Constructors + + HalfInfiniteEnv(quadrant1::EnlargedCorner, quadrant2::EnlargedCorner) + +Construct sparse half-infinite environment based on two sparse enlarged corners (quadrants). """ struct HalfInfiniteEnv{TC,TE,TA} # TODO: subtype as AbstractTensorMap once TensorKit is updated C_1::TC @@ -118,8 +127,6 @@ struct HalfInfiniteEnv{TC,TE,TA} # TODO: subtype as AbstractTensorMap once Tens A_1::TA A_2::TA end - -# Construct environment from two enlarged corners function HalfInfiniteEnv(quadrant1::EnlargedCorner, quadrant2::EnlargedCorner) return HalfInfiniteEnv( quadrant1.C, @@ -134,7 +141,7 @@ function HalfInfiniteEnv(quadrant1::EnlargedCorner, quadrant2::EnlargedCorner) end """ - TensorKit.TensorMap(env::HalfInfiniteEnv) + TensorMap(env::HalfInfiniteEnv) Instantiate half-infinite environment as `TensorMap` explicitly. """ @@ -183,9 +190,20 @@ end # -------------------------------- """ - struct FullInfiniteEnv{TC,TE,TA} +$(TYPEDEF) Full-infinite CTMRG environment tensor storage. + +## Fields + +$(FIELDS) + +## Constructors + FullInfiniteEnv( + quadrant1::E, quadrant2::E, quadrant3::E, quadrant4::E + ) where {E<:EnlargedCorner} + +Construct sparse full-infinite environment based on four sparse enlarged corners (quadrants). """ struct FullInfiniteEnv{TC,TE,TA} # TODO: subtype as AbstractTensorMap once TensorKit is updated C_1::TC @@ -205,8 +223,6 @@ struct FullInfiniteEnv{TC,TE,TA} # TODO: subtype as AbstractTensorMap once Tens A_3::TA A_4::TA end - -# Construct environment from two enlarged corners function FullInfiniteEnv( quadrant1::E, quadrant2::E, quadrant3::E, quadrant4::E ) where {E<:EnlargedCorner} @@ -231,7 +247,7 @@ function FullInfiniteEnv( end """ - TensorKit.TensorMap(env::FullInfiniteEnv) + TensorMap(env::FullInfiniteEnv) Instantiate full-infinite environment as `TensorMap` explicitly. """ diff --git a/src/algorithms/optimization/fixed_point_differentiation.jl b/src/algorithms/optimization/fixed_point_differentiation.jl index 2d3802c23..d83fdbf13 100644 --- a/src/algorithms/optimization/fixed_point_differentiation.jl +++ b/src/algorithms/optimization/fixed_point_differentiation.jl @@ -8,6 +8,11 @@ const EIGSOLVER_SOLVER_SYMBOLS = IdDict{Symbol,Type{<:KrylovKit.KrylovAlgorithm} :arnoldi => Arnoldi ) +""" + GradMode(; kwargs...) + +Keyword argument parser returning the appropriate `GradMode` algorithm struct. +""" function GradMode(; alg=Defaults.gradient_alg, tol=Defaults.gradient_tol, @@ -63,12 +68,19 @@ end iterscheme(::GradMode{F}) where {F} = F """ - struct GeomSum <: GradMode{iterscheme} - GeomSum(; kwargs...) +$(TYPEDEF) Gradient mode for CTMRG using explicit evaluation of the geometric sum. -## Keyword arguments +## Fields + +$(TYPEDFIELDS) + +## Constructors + + GeomSum(; kwargs...) + +Construct the `GeomSum` algorithm struct based on the following keyword arguments: * `tol::Real=$(Defaults.gradient_tol)` : Convergence tolerance for the difference of norms of two consecutive summands in the geometric sum. * `maxiter::Int=$(Defaults.gradient_maxiter)` : Maximal number of gradient iterations. @@ -90,12 +102,19 @@ GeomSum(; kwargs...) = GradMode(; alg=:geomsum, kwargs...) GRADIENT_MODE_SYMBOLS[:geomsum] = GeomSum """ - struct ManualIter <: GradMode{iterscheme} - ManualIter(; kwargs...) +$(TYPEDEF) Gradient mode for CTMRG using manual iteration to solve the linear problem. -## Keyword arguments +## Fields + +$(TYPEDFIELDS) + +## Constructors + + ManualIter(; kwargs...) + +Construct the `ManualIter` algorithm struct based on the following keyword arguments: * `tol::Real=$(Defaults.gradient_tol)` : Convergence tolerance for the norm difference of two consecutive `dx` contributions. * `maxiter::Int=$(Defaults.gradient_maxiter)` : Maximal number of gradient iterations. @@ -117,13 +136,20 @@ ManualIter(; kwargs...) = GradMode(; alg=:manualiter, kwargs...) GRADIENT_MODE_SYMBOLS[:manualiter] = ManualIter """ - struct LinSolver <: GradMode{iterscheme} - LinSolver(; kwargs...) +$(TYPEDEF) Gradient mode wrapper around `KrylovKit.LinearSolver` for solving the gradient linear problem using iterative solvers. -## Keyword arguments +## Fields + +$(TYPEDFIELDS) + +## Constructors + + LinSolver(; kwargs...) + +Construct the `LinSolver` algorithm struct based on the following keyword arguments: * `tol::Real=$(Defaults.gradient_tol)` : Convergence tolerance of the linear solver. * `maxiter::Int=$(Defaults.gradient_maxiter)` : Maximal number of solver iterations. @@ -132,8 +158,8 @@ problem using iterative solvers. - `:fixed` : the differentiated CTMRG iteration uses a pre-computed SVD with a fixed set of gauges - `:diffgauge` : the differentiated iteration consists of a CTMRG iteration and a subsequent gauge-fixing step such that the gauge-fixing procedure is differentiated as well * `solver_alg::Union{KrylovKit.LinearSolver,NamedTuple}=(; alg::Symbol=:$(Defaults.gradient_linsolver)` : Linear solver algorithm which, if supplied directly as a `KrylovKit.LinearSolver` overrides the above specified `tol`, `maxiter` and `verbosity`. Alternatively, it can be supplied via a `NamedTuple` where `alg` can be one of the following: - - `:gmres` : GMRES iterative linear solver, see the [KrylovKit docs](https://jutho.github.io/KrylovKit.jl/stable/man/algorithms/#KrylovKit.GMRES) for details - - `:bicgstab` : BiCGStab iterative linear solver, see the [KrylovKit docs](https://jutho.github.io/KrylovKit.jl/stable/man/algorithms/#KrylovKit.BiCGStab) for details + - `:gmres` : GMRES iterative linear solver, see [`KrylovKit.GMRES`](@extref) for details + - `:bicgstab` : BiCGStab iterative linear solver, see [`KrylovKit.BiCGStab`](@extref) for details """ struct LinSolver{F} <: GradMode{F} solver_alg::KrylovKit.LinearSolver @@ -143,13 +169,20 @@ LinSolver(; kwargs...) = GradMode(; alg=:linsolver, kwargs...) GRADIENT_MODE_SYMBOLS[:linsolver] = LinSolver """ - struct EigSolver <: GradMode{iterscheme} - EigSolver(; kwargs...) +$(TYPEDEF) Gradient mode wrapper around `KrylovKit.KrylovAlgorithm` for solving the gradient linear problem as an eigenvalue problem. -## Keyword arguments +## Fields + +$(TYPEDFIELDS) + +## Constructors + + EigSolver(; kwargs...) + +Construct the `EigSolver` algorithm struct based on the following keyword arguments: * `tol::Real=$(Defaults.gradient_tol)` : Convergence tolerance of the eigen solver. * `maxiter::Int=$(Defaults.gradient_maxiter)` : Maximal number of solver iterations. @@ -158,7 +191,7 @@ problem as an eigenvalue problem. - `:fixed` : the differentiated CTMRG iteration uses a pre-computed SVD with a fixed set of gauges - `:diffgauge` : the differentiated iteration consists of a CTMRG iteration and a subsequent gauge-fixing step such that the gauge-fixing procedure is differentiated as well * `solver_alg::Union{KrylovKit.KrylovAlgorithm,NamedTuple}=(; alg=:$(Defaults.gradient_eigsolver)` : Eigen solver algorithm which, if supplied directly as a `KrylovKit.KrylovAlgorithm` overrides the above specified `tol`, `maxiter` and `verbosity`. Alternatively, it can be supplied via a `NamedTuple` where `alg` can be one of the following: - - `:arnoldi` : Arnoldi Krylov algorithm, see the [KrylovKit docs](https://jutho.github.io/KrylovKit.jl/stable/man/algorithms/#KrylovKit.Arnoldi) for details + - `:arnoldi` : Arnoldi Krylov algorithm, see [`KrylovKit.Arnoldi`](@extref) for details """ struct EigSolver{F} <: GradMode{F} solver_alg::KrylovKit.KrylovAlgorithm @@ -277,7 +310,7 @@ end @doc """ fpgrad(∂F∂x, ∂f∂x, ∂f∂A, y0, alg) -Compute the gradient of the cost function for CTMRG by solving the following equation: +Compute the gradient of the CTMRG fixed point by solving the following equation: dx = ∑ₙ (∂f∂x)ⁿ ∂f∂A dA = (1 - ∂f∂x)⁻¹ ∂f∂A dA diff --git a/src/algorithms/optimization/peps_optimization.jl b/src/algorithms/optimization/peps_optimization.jl index 0110e0398..299d3c526 100644 --- a/src/algorithms/optimization/peps_optimization.jl +++ b/src/algorithms/optimization/peps_optimization.jl @@ -1,16 +1,24 @@ """ - struct PEPSOptimize{G} - PEPSOptimize(; kwargs...) +$(TYPEDEF) Algorithm struct for PEPS ground-state optimization using AD. See [`fixedpoint`](@ref) for details. -## Keyword arguments +## Fields -* `boundary_alg::Union{NamedTuple,<:CTMRGAlgorithm}` : Supply boundary algorithm parameters using either a `NamedTuple` of keyword arguments or a `CTMRGAlgorithm` directly. See [`leading_boundary`](@ref) for a description of all possible keyword arguments. -* `gradient_alg::Union{NamedTuple,Nothing,<:GradMode}` : Supply gradient algorithm parameters using either a `NamedTuple` of keyword arguments, `nothing`, or a `GradMode` directly. See [`fixedpoint`](@ref) for a description of all possible keyword arguments. -* `optimizer_alg::Union{NamedTuple,<:OptimKit.OptimizationAlgorithm}` : Supply optimizer algorithm parameters using either a `NamedTuple` of keyword arguments, or a `OptimKit.OptimizationAlgorithm` directly. See [`fixedpoint`](@ref) for a description of all possible keyword arguments. -* `reuse_env::Bool=$(Defaults.reuse_env)` : If `true`, the current optimization step is initialized on the previous environment, otherwise a random environment is used. -* `symmetrization::Union{Nothing,SymmetrizationStyle}=nothing` : Accepts `nothing` or a `SymmetrizationStyle`, in which case the PEPS and PEPS gradient are symmetrized after each optimization iteration. +$(TYPEDFIELDS) + +## Constructors + + PEPSOptimize(; kwargs...) + +Construct a PEPS optimization algorithm struct based on keyword arguments. +For a full description, see [`fixedpoint`](@ref). The supported keywords are: + +* `boundary_alg::Union{NamedTuple,<:CTMRGAlgorithm}` +* `gradient_alg::Union{NamedTuple,Nothing,<:GradMode}` +* `optimizer_alg::Union{NamedTuple,<:OptimKit.OptimizationAlgorithm}` +* `reuse_env::Bool=$(Defaults.reuse_env)` +* `symmetrization::Union{Nothing,SymmetrizationStyle}=nothing` """ struct PEPSOptimize{G} boundary_alg::CTMRGAlgorithm @@ -93,10 +101,9 @@ function _OptimizationAlgorithm(; end """ - fixedpoint(operator, peps₀::InfinitePEPS, env₀::CTMRGEnv; kwargs...) + fixedpoint(operator, peps₀::InfinitePEPS, env₀::CTMRGEnv; kwargs...) -> peps_final, env_final, cost_final, info # expert version: - fixedpoint(operator, peps₀::InfinitePEPS, env₀::CTMRGEnv, alg::PEPSOptimize; - finalize!=OptimKit._finalize!) + fixedpoint(operator, peps₀::InfinitePEPS, env₀::CTMRGEnv, alg::PEPSOptimize; finalize!=OptimKit._finalize!) Find the fixed point of `operator` (i.e. the ground state) starting from `peps₀` according to the supplied optimization parameters. The initial environment `env₀` serves as an @@ -139,8 +146,8 @@ keyword arguments are: * `maxiter::Int=$(Defaults.gradient_maxiter)` : Maximal number of gradient problem iterations. * `alg::Symbol=:$(Defaults.gradient_alg)` : Gradient algorithm variant, can be one of the following: - `:geomsum` : Compute gradient directly from the geometric sum, see [`GeomSum`](@ref) - - `:manualiter` : Iterate gradient geometric sum manually, see ['ManualIter'](@ref) - - `:linsolver` : Solve fixed-point gradient linear problem using iterative solver, see ['LinSolver'](@ref) + - `:manualiter` : Iterate gradient geometric sum manually, see [`ManualIter`](@ref) + - `:linsolver` : Solve fixed-point gradient linear problem using iterative solver, see [`LinSolver`](@ref) - `:eigsolver` : Determine gradient via eigenvalue formulation of its Sylvester equation, see [`EigSolver`](@ref) * `verbosity::Int` : Gradient output verbosity, ≤0 by default to disable too verbose printing. Should only be >0 for debug purposes. * `iterscheme::Symbol=:$(Defaults.gradient_iterscheme)` : CTMRG iteration scheme determining mode of differentiation. This can be: @@ -225,7 +232,7 @@ function fixedpoint( peps₀ = peps_normalize(peps₀) # optimize operator cost function - (peps_final, env_final), cost, ∂cost, numfg, convergence_history = optimize( + (peps_final, env_final), cost_final, ∂cost, numfg, convergence_history = optimize( (peps₀, env₀), alg.optimizer_alg; retract, @@ -265,7 +272,7 @@ function fixedpoint( gradnorms_unitcell, times, ) - return peps_final, env_final, cost, info + return peps_final, env_final, cost_final, info end """ @@ -280,7 +287,7 @@ function peps_normalize(A::InfinitePEPS) end """ - peps_retract(x, η, α) +$(SIGNATURES) Performs a norm-preserving retraction of an infinite PEPS `A = x[1]` along `η` with step size `α`, giving a new PEPS `A´`, @@ -305,7 +312,7 @@ function peps_retract(x, η, α) end """ - peps_transport!(ξ, x, η, α, x′) +$(SIGNATURES) Transports a direction at `A = x[1]` to a valid direction at `A´ = x´[1]` corresponding to the norm-preserving retraction of `A` along `η` with step size `α`. In particular, starting @@ -334,7 +341,7 @@ end real_inner(_, η₁, η₂) = real(dot(η₁, η₂)) """ - symmetrize_retract_and_finalize!(symm::SymmetrizationStyle) + symmetrize_retract_and_finalize!(symm::SymmetrizationStyle, [retract, finalize!]) Return the `retract` and `finalize!` function for symmetrizing the `peps` and `grad` tensors. """ diff --git a/src/algorithms/time_evolution/evoltools.jl b/src/algorithms/time_evolution/evoltools.jl index 6ff3e8487..18bad74fe 100644 --- a/src/algorithms/time_evolution/evoltools.jl +++ b/src/algorithms/time_evolution/evoltools.jl @@ -8,12 +8,17 @@ function get_gate(dt::Float64, H::LocalOperator) numin(op) == 2 && norm(Tuple(terms[2] - terms[1])) == 1.0 for (terms, op) in H.terms ]) "Only nearest-neighbour terms allowed" return LocalOperator( - H.lattice, Tuple(sites => exp(-dt * op) for (sites, op) in H.terms)... + physicalspace(H), Tuple(sites => exp(-dt * op) for (sites, op) in H.terms)... ) end """ - is_equivalent(bond1::NTuple{2,CartesianIndex{2}}, bond2::NTuple{2,CartesianIndex{2}}, (Nrow, Ncol)::NTuple{2,Int}) + is_equivalent( + bond1::NTuple{2,CartesianIndex{2}}, + bond2::NTuple{2,CartesianIndex{2}}, + (Nrow, Ncol)::NTuple{2,Int}, + ) + Check if two 2-site bonds are related by a (periodic) lattice translation. """ @@ -51,6 +56,8 @@ function get_gateterm(gate::LocalOperator, bond::NTuple{2,CartesianIndex{2}}) end """ +$(SIGNATURES) + Use QR decomposition on two tensors connected by a bond to get the reduced tensors ``` @@ -81,6 +88,8 @@ function _qr_bond(A::PEPSTensor, B::PEPSTensor) end """ +$(SIGNATURES) + Reconstruct the tensors connected by a bond from their QR results obtained from `_qr_bond` ``` @@ -98,6 +107,8 @@ function _qr_bond_undo(X::PEPSOrth, a::AbstractTensorMap, b::AbstractTensorMap, end """ +$(SIGNATURES) + Apply 2-site `gate` on the reduced matrices `a`, `b` ``` -1← a -← 3 -← b ← -4 diff --git a/src/algorithms/time_evolution/simpleupdate.jl b/src/algorithms/time_evolution/simpleupdate.jl index ebf05a843..4b03952ee 100644 --- a/src/algorithms/time_evolution/simpleupdate.jl +++ b/src/algorithms/time_evolution/simpleupdate.jl @@ -1,8 +1,12 @@ """ - struct SimpleUpdate +$(TYPEDEF) Algorithm struct for simple update (SU) of infinite PEPS with bond weights. Each SU run is converged when the singular value difference becomes smaller than `tol`. + +## Fields + +$(TYPEDFIELDS) """ struct SimpleUpdate dt::Float64 @@ -13,8 +17,7 @@ end # TODO: add kwarg constructor and SU Defaults """ -_su_bondx!(row::Int, col::Int, gate::AbstractTensorMap{T,S,2,2}, - peps::InfiniteWeightPEPS, alg::SimpleUpdate) where {S<:ElementarySpace} +$(SIGNATURES) Simple update of the x-bond `peps.weights[1,r,c]`. @@ -119,10 +122,10 @@ function su_iter( end """ - simpleupdate(peps::InfiniteWeightPEPS, ham::LocalOperator, alg::SimpleUpdate; + simpleupdate(peps::InfiniteWeightPEPS, H::LocalOperator, alg::SimpleUpdate; bipartite::Bool=false, check_interval::Int=500) -Perform simple update with nearest neighbor Hamiltonian `ham`, where the evolution +Perform simple update with nearest neighbor Hamiltonian `H`, where the evolution information is printed every `check_interval` steps. If `bipartite == true` (for square lattice), a unit cell size of `(2, 2)` is assumed, @@ -131,14 +134,14 @@ as well as tensors and x/y weights which are the same across the diagonals, i.e. """ function simpleupdate( peps::InfiniteWeightPEPS, - ham::LocalOperator, + H::LocalOperator, alg::SimpleUpdate; bipartite::Bool=false, check_interval::Int=500, ) time_start = time() # exponentiating the 2-site Hamiltonian gate - gate = get_gate(alg.dt, ham) + gate = get_gate(alg.dt, H) wtdiff = 1.0 wts0 = deepcopy(peps.weights) for count in 1:(alg.maxiter) diff --git a/src/algorithms/truncation/bond_truncation.jl b/src/algorithms/truncation/bond_truncation.jl index 0d32973c9..40f413074 100644 --- a/src/algorithms/truncation/bond_truncation.jl +++ b/src/algorithms/truncation/bond_truncation.jl @@ -1,12 +1,22 @@ """ - ALSTruncation +$(TYPEDEF) Algorithm struct for the alternating least square (ALS) optimization of a bond. -- `trscheme::Bool`: SVD truncation scheme when initilizing the truncated tensors connected by the bond. -- `maxiter::Int`: Maximal number of ALS iterations. -- `tol::Float64`: ALS converges when fidelity change between two FET iterations is smaller than `tol`. -- `check_interval::Int`: Set number of iterations to print information. Output is suppressed when `check_interval <= 0`. +## Fields + +$(TYPEDFIELDS) + +## Constructors + + ALSTruncation(; kwargs...) + +The truncation algorithm can be constructed from the following keyword arguments: + +* `trscheme::TensorKit.TruncationScheme`: SVD truncation scheme when initilizing the truncated tensors connected by the bond. +* `maxiter::Int=50` : Maximal number of ALS iterations. +* `tol::Float64=1e-15` : ALS converges when fidelity change between two FET iterations is smaller than `tol`. +* `check_interval::Int=0` : Set number of iterations to print information. Output is suppressed when `check_interval <= 0`. """ @kwdef struct ALSTruncation trscheme::TensorKit.TruncationScheme @@ -29,7 +39,7 @@ function _als_message( end """ - bond_truncate(a::AbstractTensorMap{T,S,2,1}, b::AbstractTensorMap{T,S,1,2}, benv::BondEnv{T,S}, alg) where {T<:Number,S<:ElementarySpace} + bond_truncate(a::AbstractTensorMap{T,S,2,1}, b::AbstractTensorMap{T,S,1,2}, benv::BondEnv{T,S}, alg) -> U, S, V, info After time-evolving the reduced tensors `a` and `b` connected by a bond, truncate the bond dimension using the bond environment tensor `benv`. diff --git a/src/algorithms/truncation/fullenv_truncation.jl b/src/algorithms/truncation/fullenv_truncation.jl index 6c107bbab..7856f499b 100644 --- a/src/algorithms/truncation/fullenv_truncation.jl +++ b/src/algorithms/truncation/fullenv_truncation.jl @@ -1,13 +1,27 @@ """ - FullEnvTruncation +$(TYPEDEF) Algorithm struct for the full environment truncation (FET). -- `trscheme::Bool`: SVD truncation scheme when optimizing the new bond matrix. -- `maxiter::Int`: Maximal number of FET iterations. -- `tol::Float64`: FET converges when fidelity change between two FET iterations is smaller than `tol`. -- `trunc_init::Bool`: Controls whether the initialization of the new bond matrix is obtained from truncated SVD of the old bond matrix. -- `check_interval::Int`: Set number of iterations to print information. Output is suppressed when `check_interval <= 0`. +## Fields + +$(TYPEDFIELDS) + +## Constructors + + FullEnvTruncation(; kwargs...) + +The truncation algorithm can be constructed from the following keyword arguments: + +* `trscheme::TensorKit.TruncationScheme` : SVD truncation scheme when optimizing the new bond matrix. +* `maxiter::Int=50` : Maximal number of FET iterations. +* `tol::Float64=1e-15` : FET converges when fidelity change between two FET iterations is smaller than `tol`. +* `trunc_init::Bool=true` : Controls whether the initialization of the new bond matrix is obtained from truncated SVD of the old bond matrix. +* `check_interval::Int=0` : Set number of iterations to print information. Output is suppressed when `check_interval <= 0`. + +## References + +* [Glen Evenbly, Phys. Rev. B 98, 085155 (2018)](@cite evenbly_gauge_2018). """ @kwdef struct FullEnvTruncation trscheme::TensorKit.TruncationScheme @@ -18,6 +32,8 @@ Algorithm struct for the full environment truncation (FET). end """ +$(SIGNATURES) + Given the bond environment `benv`, calculate the inner product between two states specified by the bond matrices `b1`, `b2` ``` @@ -38,6 +54,8 @@ function inner_prod( end """ +$(SIGNATURES) + Given the bond environment `benv`, calculate the fidelity between two states specified by the bond matrices `b1`, `b2` ``` @@ -52,6 +70,8 @@ function fidelity( end """ +$(SIGNATURES) + Apply a twist to domain or codomain indices that correspond to dual spaces """ function _linearmap_twist!(t::AbstractTensorMap) @@ -72,9 +92,10 @@ function _fet_message( end """ - fullenv_truncate(benv::BondEnv{T,S}, b0::AbstractTensorMap{T,S,1,1}, alg::FullEnvTruncation) where {T<:Number,S<:ElementarySpace} + fullenv_truncate(benv::BondEnv{T,S}, b0::AbstractTensorMap{T,S,1,1}, alg::FullEnvTruncation) -> U, S, V, info -The full environment truncation algorithm (Physical Review B 98, 085155 (2018)). +Perform full environment truncation algorithm from +[Phys. Rev. B 98, 085155 (2018)](@cite evenbly_gauge_2018) on `benv`. Given a fixed state `|b0⟩` with bond matrix `b0` and the corresponding positive-definite bond environment `benv`, @@ -190,9 +211,14 @@ Then the bond matrix `u s v†` is updated by SVD: - l ← v† - ==> - u ← s ← v† - ``` -## Returns +## Return values + +Returns the SVD result of the new bond matrix `U`, `S`, `V`, as well as an information +`NamedTuple` containing the following fields: -The SVD result of the new bond matrix `u`, `s`, `vh`. +* `fid` : Last fidelity. +* `Δfid` : Last fidelity difference. +* `Δs` : Last singular value difference. """ function fullenv_truncate( b0::AbstractTensorMap{T,S,1,1}, benv::BondEnv{T,S}, alg::FullEnvTruncation diff --git a/src/algorithms/truncation/truncationschemes.jl b/src/algorithms/truncation/truncationschemes.jl index dfd7a3400..9039d925e 100644 --- a/src/algorithms/truncation/truncationschemes.jl +++ b/src/algorithms/truncation/truncationschemes.jl @@ -1,5 +1,5 @@ """ - struct FixedSpaceTruncation <: TensorKit.TruncationScheme +$(TYPEDEF) CTMRG specific truncation scheme for `tsvd` which keeps the bond space on which the SVD is performed fixed. Since different environment directions and unit cell entries might @@ -15,7 +15,8 @@ const TRUNCATION_SCHEME_SYMBOLS = IdDict{Symbol,Type{<:TruncationScheme}}( :truncspace => TensorKit.TruncationSpace, :truncbelow => TensorKit.TruncationCutoff, ) -# Should be TruncationScheme but piracy + +# Should be TruncationScheme but rename to avoid type piracy function _TruncationScheme(; alg=Defaults.trscheme, η=nothing) # replace Symbol with TruncationScheme type haskey(TRUNCATION_SCHEME_SYMBOLS, alg) || diff --git a/src/environments/ctmrg_environments.jl b/src/environments/ctmrg_environments.jl index 4200c938a..571edf98d 100644 --- a/src/environments/ctmrg_environments.jl +++ b/src/environments/ctmrg_environments.jl @@ -1,5 +1,5 @@ """ - struct CTMRGEnv{C,T} +$(TYPEDEF) Corner transfer-matrix environment containing unit-cell arrays of corner and edge tensors. The last two indices of the arrays correspond to the row and column indices of the unit @@ -20,12 +20,14 @@ Here `P` represents an effective local constituent tensor. This can either be a rank-4 tensor, a pair of PEPS tensors, or a stack of PEPS-PEPO-PEPS tensors depending on the network being contracted. -# Fields -- `corners::Array{C,3}`: Array of corner tensors. -- `edges::Array{T,3}`: Array of edge tensors. +## Fields + +$(TYPEDFIELDS) """ struct CTMRGEnv{C,T} + "4 x rows x cols array of corner tensors, where the first dimension specifies the spatial direction" corners::Array{C,3} + "4 x rows x cols array of edge tensors, where the first dimension specifies the spatial direction" edges::Array{T,3} end @@ -58,7 +60,7 @@ end """ CTMRGEnv( - [f=randn, ComplexF64], Ds_north::A, Ds_east::A, chis_north::B, [chis_east::B], [chis_south::B], [chis_west::B] + [f=randn, T=ComplexF64], Ds_north::A, Ds_east::A, chis_north::B, [chis_east::B], [chis_south::B], [chis_west::B] ) where {A<:AbstractMatrix{<:SpaceLike}, B<:AbstractMatrix{<:ElementarySpaceLike}} Construct a CTMRG environment by specifying matrices of north and east virtual spaces of the @@ -158,7 +160,8 @@ end """ CTMRGEnv( - [f=randn, ComplexF64], D_north::P, D_east::P, chi_north::S, [chi_east::S], [chi_south::S], [chi_west::S]; unitcell::Tuple{Int,Int}=(1, 1), + [f=randn, T=ComplexF64], D_north::P, D_east::P, chi_north::S, [chi_east::S], [chi_south::S], [chi_west::S]; + unitcell::Tuple{Int,Int}=(1, 1), ) where {P<:ProductSpaceLike,S<:ElementarySpaceLike} Construct a CTMRG environment by specifying the north and east virtual spaces of the @@ -178,7 +181,7 @@ function CTMRGEnv( chi_south::S=chi_north, chi_west::S=chi_north; unitcell::Tuple{Int,Int}=(1, 1), -) where {P<:ProductSpaceLike,S<:Union{Int,ElementarySpace}} +) where {P<:ProductSpaceLike,S<:ElementarySpaceLike} return CTMRGEnv( randn, ComplexF64, @@ -200,7 +203,7 @@ function CTMRGEnv( chi_south::S=chi_north, chi_west::S=chi_north; unitcell::Tuple{Int,Int}=(1, 1), -) where {P<:ProductSpaceLike,S<:Union{Int,ElementarySpace}} +) where {P<:ProductSpaceLike,S<:ElementarySpaceLike} return CTMRGEnv( f, T, @@ -283,7 +286,7 @@ end """ CTMRGEnv( - peps::InfiniteSquareNetwork, chi_north::S, [chi_east::S], [chi_south::S], [chi_west::S], + [f=randn, T=ComplexF64,] network::InfiniteSquareNetwork, chi_north::S, [chi_east::S], [chi_south::S], [chi_west::S], ) where {S<:ElementarySpaceLike} Construct a CTMRG environment by specifying a corresponding diff --git a/src/networks/infinitesquarenetwork.jl b/src/networks/infinitesquarenetwork.jl index 503688eac..ac7f14312 100644 --- a/src/networks/infinitesquarenetwork.jl +++ b/src/networks/infinitesquarenetwork.jl @@ -1,7 +1,11 @@ """ - InfiniteSquareNetwork{O} +$(TYPEDEF) Contractible square network. Wraps a matrix of 'rank-4-tensor-like' objects. + +## Fields + +$(TYPEDFIELDS) """ struct InfiniteSquareNetwork{O} A::Matrix{O} diff --git a/src/networks/tensors.jl b/src/networks/tensors.jl index 7a597dfba..813cc5303 100644 --- a/src/networks/tensors.jl +++ b/src/networks/tensors.jl @@ -57,9 +57,7 @@ const PEPSTensor{S<:ElementarySpace} = AbstractTensorMap{<:Any,S,1,4} """ PEPSTensor(f, ::Type{T}, Pspace::S, Nspace::S, - [Espace::S], [Sspace::S], [Wspace::S]) where {T,S<:ElementarySpace} - PEPSTensor(f, ::Type{T}, Pspace::Int, Nspace::Int, - [Espace::Int], [Sspace::Int], [Wspace::Int]) where {T} + [Espace::S], [Sspace::S], [Wspace::S]) where {T,S<:Union{Int,ElementarySpace}} Construct a PEPS tensor based on the physical, north, east, west and south spaces. Alternatively, only the space dimensions can be provided and ℂ is assumed as the field. diff --git a/src/operators/infinitepepo.jl b/src/operators/infinitepepo.jl index d8cb2766b..054d4cfdf 100644 --- a/src/operators/infinitepepo.jl +++ b/src/operators/infinitepepo.jl @@ -2,6 +2,10 @@ struct InfinitePEPO{T<:PEPOTensor} Represents an infinite projected entangled-pair operator (PEPO) on a 3D cubic lattice. + +## Fields + +$(TYPEDFIELDS) """ struct InfinitePEPO{T<:PEPOTensor} A::Array{T,3} @@ -39,7 +43,7 @@ function InfinitePEPO(A::AbstractArray{T,3}) where {T<:PEPOTensor} end """ - InfinitePEPO(f=randn, T=ComplexF64, Pspaces, Nspaces, Espaces) + InfinitePEPO([f=randn, T=ComplexF64,] Pspaces, Nspaces, Espaces) Allow users to pass in arrays of spaces. """ @@ -79,16 +83,16 @@ function InfinitePEPO( end """ - InfinitePEPO(A; unitcell=(1, 1, 1)) + InfinitePEPO(A::PEPOTensor; unitcell=(1, 1, 1)) -Create an InfinitePEPO by specifying a tensor and unit cell. +Create an InfinitePEPO by specifying a tensor which is repeated across the unit cell. """ function InfinitePEPO(A::T; unitcell::Tuple{Int,Int,Int}=(1, 1, 1)) where {T<:PEPOTensor} return InfinitePEPO(fill(A, unitcell)) end """ - InfinitePEPO(f=randn, T=ComplexF64, Pspace, Nspace, [Espace]; unitcell=(1,1,1)) + InfinitePEPO([f=randn, T=ComplexF64,] Pspace::S, Nspace::S, [Espace::S]; unitcell=(1,1,1)) where {S<:ElementarySpace} Create an InfinitePEPO by specifying its spaces and unit cell. """ diff --git a/src/operators/lattices/squarelattice.jl b/src/operators/lattices/squarelattice.jl index a2f3d9b5e..e72b63b49 100644 --- a/src/operators/lattices/squarelattice.jl +++ b/src/operators/lattices/squarelattice.jl @@ -1,7 +1,17 @@ """ - InfiniteSquare(Nrows::Integer=1, Ncols::Integer=1) +$(TYPEDEF) Infinite square lattice with a unit cell of size `(Nrows, Ncols)`. + +## Fields + +$(TYPEDFIELDS) + +## Constructor + + InfiniteSquare([Nrows=1, Ncols=1]) + +By default, an infinite square with a (1, 1)-unitcell is constructed. """ struct InfiniteSquare <: AbstractLattice{2} Nrows::Int diff --git a/src/operators/localoperator.jl b/src/operators/localoperator.jl index 7071af31e..c5535ee98 100644 --- a/src/operators/localoperator.jl +++ b/src/operators/localoperator.jl @@ -1,22 +1,22 @@ # Hamiltonian consisting of local terms # ------------------------------------- """ - struct LocalOperator{T<:Tuple,S} +$(TYPEDEF) A sum of local operators acting on a lattice. The lattice is stored as a matrix of vector spaces, and the terms are stored as a tuple of pairs of indices and operators. -# Fields +## Fields - `lattice::Matrix{S}`: The lattice on which the operator acts. - `terms::T`: The terms of the operator, stored as a tuple of pairs of indices and operators. -# Constructors +## Constructors LocalOperator(lattice::Matrix{S}, terms::Pair...) LocalOperator{T,S}(lattice::Matrix{S}, terms::T) where {T,S} -# Examples +## Examples ```julia lattice = fill(ℂ^2, 1, 1) # single-site unitcell @@ -96,6 +96,15 @@ function Base.repeat(O::LocalOperator, m::Int, n::Int) return LocalOperator(lattice, terms...) end +""" + physicalspace(O::LocalOperator) + +Return lattice of physical spaces on which the `LocalOperator` is defined. +""" +function physicalspace(O::LocalOperator) + return O.lattice +end + # Real and imaginary part # ----------------------- function Base.real(O::LocalOperator) @@ -128,9 +137,7 @@ Base.:-(O1::LocalOperator, O2::LocalOperator) = O1 + (-O2) # ---------------------- """ - _mirror_antidiag_site( - site::S, (Nrow, Ncol)::NTuple{2,Int} - ) where {S<:Union{CartesianIndex{2},NTuple{2,Int}}} +$(SIGNATURES) Get the position of `site` after reflection about the anti-diagonal line. """ @@ -142,9 +149,7 @@ function _mirror_antidiag_site( end """ - _rotr90_site( - site::S, (Nrow, Ncol)::NTuple{2,Int} - ) where {S<:Union{CartesianIndex{2},NTuple{2,Int}}} +$(SIGNATURES) Get the position of `site` after clockwise (right) rotation by 90 degrees. """ @@ -156,9 +161,7 @@ function _rotr90_site( end """ - _rotl90_site( - site::S, (Nrow, Ncol)::NTuple{2,Int} - ) where {S<:Union{CartesianIndex{2},NTuple{2,Int}}} +$(SIGNATURES) Get the position of `site` after counter-clockwise (left) rotation by 90 degrees. """ @@ -170,9 +173,7 @@ function _rotl90_site( end """ - _rot180_site( - site::S, (Nrow, Ncol)::NTuple{2,Int} - ) where {S<:Union{CartesianIndex{2},NTuple{2,Int}}} +$(SIGNATURES) Get the position of `site` after rotation by 180 degrees. """ @@ -189,36 +190,38 @@ end Mirror a `LocalOperator` across the anti-diagonal axis of its lattice. """ function mirror_antidiag(H::LocalOperator) - lattice2 = mirror_antidiag(H.lattice) + lattice2 = mirror_antidiag(physicalspace(H)) terms2 = ( - (Tuple(_mirror_antidiag_site(site, size(H.lattice)) for site in sites) => op) for - (sites, op) in H.terms + ( + Tuple(_mirror_antidiag_site(site, size(physicalspace(H))) for site in sites) => + op + ) for (sites, op) in H.terms ) return LocalOperator(lattice2, terms2...) end function Base.rotr90(H::LocalOperator) - lattice2 = rotr90(H.lattice) + lattice2 = rotr90(physicalspace(H)) terms2 = ( - (Tuple(_rotr90_site(site, size(H.lattice)) for site in sites) => op) for + (Tuple(_rotr90_site(site, size(physicalspace(H))) for site in sites) => op) for (sites, op) in H.terms ) return LocalOperator(lattice2, terms2...) end function Base.rotl90(H::LocalOperator) - lattice2 = rotl90(H.lattice) + lattice2 = rotl90(physicalspace(H)) terms2 = ( - (Tuple(_rotl90_site(site, size(H.lattice)) for site in sites) => op) for + (Tuple(_rotl90_site(site, size(physicalspace(H))) for site in sites) => op) for (sites, op) in H.terms ) return LocalOperator(lattice2, terms2...) end function Base.rot180(H::LocalOperator) - lattice2 = rot180(H.lattice) + lattice2 = rot180(physicalspace(H)) terms2 = ( - (Tuple(_rot180_site(site, size(H.lattice)) for site in sites) => op) for + (Tuple(_rot180_site(site, size(physicalspace(H))) for site in sites) => op) for (sites, op) in H.terms ) return LocalOperator(lattice2, terms2...) @@ -250,6 +253,8 @@ TensorKit.sectortype(::Type{<:LocalOperator{T,S}}) where {T,S} = sectortype(S) end """ +$(SIGNATURES) + Fuse identities on auxiliary physical spaces into a given operator. """ function _fuse_ids(op::AbstractTensorMap{T,S,N,N}, Ps::NTuple{N,S}) where {T,S,N} @@ -262,13 +267,13 @@ function _fuse_ids(op::AbstractTensorMap{T,S,N,N}, Ps::NTuple{N,S}) where {T,S,N end """ - MPSKit.add_physical_charge(H::LocalOperator, charges::AbstractMatrix{<:Sector}) where {S} + add_physical_charge(H::LocalOperator, charges::AbstractMatrix{<:Sector}) Change the spaces of a `LocalOperator` by fusing in an auxiliary charge into the domain of the operator on every site, according to a given matrix of 'auxiliary' physical charges. """ function MPSKit.add_physical_charge(H::LocalOperator, charges::AbstractMatrix{<:Sector}) - size(H.lattice) == size(charges) || + size(physicalspace(H)) == size(charges) || throw(ArgumentError("Incompatible lattice and auxiliary charge sizes")) sectortype(H) === eltype(charges) || throw(SectorMismatch("Incompatible lattice and auxiliary charge sizes")) @@ -279,7 +284,7 @@ function MPSKit.add_physical_charge(H::LocalOperator, charges::AbstractMatrix{<: Paux = PeriodicArray(map(c -> Vect[typeof(c)](c => 1)', charges)) # new physical spaces - Pspaces = map(fuse, H.lattice, Paux) + Pspaces = map(fuse, physicalspace(H), Paux) new_terms = map(H.terms) do (sites, op) Paux_slice = map(Base.Fix1(getindex, Paux), sites) diff --git a/src/operators/models.jl b/src/operators/models.jl index c739aee6c..ac28b7a2e 100644 --- a/src/operators/models.jl +++ b/src/operators/models.jl @@ -1,9 +1,10 @@ -## Model Hamiltonians -# ------------------- + +# +## Tools for defining Hamiltonians +# + """ - nearest_neighbour_hamiltonian( - lattice::Matrix{S}, h::AbstractTensorMap{T,S,2,2} - ) where {S,T} + nearest_neighbour_hamiltonian(lattice::Matrix{S}, h::AbstractTensorMap{T,S,2,2}) where {S,T} Create a nearest neighbor `LocalOperator` by specifying the 2-site interaction term `h` which acts both in horizontal and vertical direction. @@ -21,6 +22,10 @@ function nearest_neighbour_hamiltonian( return LocalOperator(lattice, terms...) end +# +## MPSKitModels.jl re-exports +# + function MPSKitModels.transverse_field_ising( T::Type{<:Number}, S::Union{Type{Trivial},Type{Z2Irrep}}, @@ -75,77 +80,6 @@ function MPSKitModels.heisenberg_XXZ( ) end -""" - j1_j2([elt::Type{T}], [symm::Type{S}], [lattice::InfiniteSquare]; - J1=1.0, J2=1.0, spin=1//2, sublattice=true) - -Square lattice J₁-J₂ model. The `sublattice` kwarg enables a single site unit cell via a -sublattice rotation. -""" -function j1_j2(lattice::InfiniteSquare; kwargs...) - return j1_j2(ComplexF64, Trivial, lattice; kwargs...) -end -function j1_j2( - T::Type{<:Number}, - S::Type{<:Sector}, - lattice::InfiniteSquare; - J1=1.0, - J2=1.0, - spin=1//2, - sublattice=true, -) - term_AA = S_xx(T, S; spin) + S_yy(T, S; spin) + S_zz(T, S; spin) - term_AB = if sublattice - -S_xx(T, S; spin) + S_yy(T, S; spin) - S_zz(T, S; spin) # Apply sublattice rotation - else - term_AA - end - spaces = fill(domain(term_AA)[1], (lattice.Nrows, lattice.Ncols)) - return LocalOperator( - spaces, - (neighbor => J1 * term_AB for neighbor in nearest_neighbours(lattice))..., - (neighbor => J2 * term_AA for neighbor in next_nearest_neighbours(lattice))..., - ) -end - -""" - pwave_superconductor(::Type{T}=ComplexF64; t=1, μ=2, Δ=1, unitcell=(1, 1)) - -Square lattice p-wave superconductor model. -""" -function pwave_superconductor(lattice::InfiniteSquare; kwargs...) - return pwave_superconductor(ComplexF64, lattice; kwargs...) -end -function pwave_superconductor( - T::Type{<:Number}, lattice::InfiniteSquare; t::Number=1, μ::Number=2, Δ::Number=1 -) - physical_space = Vect[FermionParity](0 => 1, 1 => 1) - spaces = fill(physical_space, (lattice.Nrows, lattice.Ncols)) - - # on-site - h0 = zeros(T, physical_space ← physical_space) - block(h0, FermionParity(1)) .= -μ - - # two-site (x-direction) - hx = zeros(T, physical_space^2 ← physical_space^2) - block(hx, FermionParity(0)) .= [0 -Δ; -Δ 0] - block(hx, FermionParity(1)) .= [0 -t; -t 0] - - # two-site (y-direction) - hy = zeros(T, physical_space^2 ← physical_space^2) - block(hy, FermionParity(0)) .= [0 Δ*im; -Δ*im 0] - block(hy, FermionParity(1)) .= [0 -t; -t 0] - - x_neighbors = filter(n -> n[2].I[2] > n[1].I[2], nearest_neighbours(lattice)) - y_neighbors = filter(n -> n[2].I[1] > n[1].I[1], nearest_neighbours(lattice)) - return LocalOperator( - spaces, - ((idx,) => h0 for idx in vertices(lattice))..., - (neighbor => hx for neighbor in x_neighbors)..., - (neighbor => hy for neighbor in y_neighbors)..., - ) -end - function MPSKitModels.hubbard_model( T::Type{<:Number}, particle_symmetry::Type{<:Sector}, @@ -231,3 +165,95 @@ function MPSKitModels.tj_model( end return nearest_neighbour_hamiltonian(fill(pspace, size(lattice)), h) end + +# +## Models not yet defined in MPSKitModels +# + +""" + j1_j2_model([elt::Type{T}, symm::Type{S},] lattice::InfiniteSquare; + J1=1.0, J2=1.0, spin=1//2, sublattice=true) + +Square lattice ``J_1\\text{-}J_2`` model, defined by the Hamiltonian + +```math +H = J_1 \\sum_{\\langle i,j \\rangle} \\vec{S}_i \\cdot \\vec{S}_j ++ J_2 \\sum_{\\langle\\langle i,j \\rangle\\rangle} \\vec{S}_i \\cdot \\vec{S}_j, +``` + +where ``\\vec{S}_i = (S_i^x, S_i^y, S_i^z)``. We denote the nearest and next-nearest neighbor +terms using ``\\langle i,j \\rangle`` and ``\\langle\\langle i,j \\rangle\\rangle``, +respectively. The `sublattice` kwarg enables a single-site unit cell ground state via a +unitary sublattice rotation. +""" +function j1_j2_model(lattice::InfiniteSquare; kwargs...) + return j1_j2_model(ComplexF64, Trivial, lattice; kwargs...) +end +function j1_j2_model( + T::Type{<:Number}, + S::Type{<:Sector}, + lattice::InfiniteSquare; + J1=1.0, + J2=1.0, + spin=1//2, + sublattice=true, +) + term_AA = S_xx(T, S; spin) + S_yy(T, S; spin) + S_zz(T, S; spin) + term_AB = if sublattice + -S_xx(T, S; spin) + S_yy(T, S; spin) - S_zz(T, S; spin) # Apply sublattice rotation + else + term_AA + end + spaces = fill(domain(term_AA)[1], (lattice.Nrows, lattice.Ncols)) + return LocalOperator( + spaces, + (neighbor => J1 * term_AB for neighbor in nearest_neighbours(lattice))..., + (neighbor => J2 * term_AA for neighbor in next_nearest_neighbours(lattice))..., + ) +end + +""" + pwave_superconductor([T=ComplexF64,] lattice::InfiniteSquare; t=1, μ=2, Δ=1) + +Square lattice ``p``-wave superconductor model, defined by the Hamiltonian + +```math + H = -\\sum_{\\langle i,j \\rangle} \\left( t c_i^\\dagger c_j + + \\Delta c_i c_j + \\text{h.c.} \\right) - \\mu \\sum_i n_i, +``` + +where ``t`` is the hopping amplitude, ``\\Delta`` specifies the superconducting gap, ``\\mu`` +is the chemical potential, and ``n_i = c_i^\\dagger c_i`` is the fermionic number operator. +""" +function pwave_superconductor(lattice::InfiniteSquare; kwargs...) + return pwave_superconductor(ComplexF64, lattice; kwargs...) +end +function pwave_superconductor( + T::Type{<:Number}, lattice::InfiniteSquare; t::Number=1, μ::Number=2, Δ::Number=1 +) + physical_space = Vect[FermionParity](0 => 1, 1 => 1) + spaces = fill(physical_space, (lattice.Nrows, lattice.Ncols)) + + # on-site + h0 = zeros(T, physical_space ← physical_space) + block(h0, FermionParity(1)) .= -μ + + # two-site (x-direction) + hx = zeros(T, physical_space^2 ← physical_space^2) + block(hx, FermionParity(0)) .= [0 -Δ; -Δ 0] + block(hx, FermionParity(1)) .= [0 -t; -t 0] + + # two-site (y-direction) + hy = zeros(T, physical_space^2 ← physical_space^2) + block(hy, FermionParity(0)) .= [0 Δ*im; -Δ*im 0] + block(hy, FermionParity(1)) .= [0 -t; -t 0] + + x_neighbors = filter(n -> n[2].I[2] > n[1].I[2], nearest_neighbours(lattice)) + y_neighbors = filter(n -> n[2].I[1] > n[1].I[1], nearest_neighbours(lattice)) + return LocalOperator( + spaces, + ((idx,) => h0 for idx in vertices(lattice))..., + (neighbor => hx for neighbor in x_neighbors)..., + (neighbor => hy for neighbor in y_neighbors)..., + ) +end diff --git a/src/states/infinitepartitionfunction.jl b/src/states/infinitepartitionfunction.jl index c5ca3ff3d..2c281b0d1 100644 --- a/src/states/infinitepartitionfunction.jl +++ b/src/states/infinitepartitionfunction.jl @@ -2,6 +2,10 @@ struct InfinitePartitionFunction{T<:PartitionFunctionTensor} Represents an infinite partition function on a 2D square lattice. + +## Fields + +$(TYPEDFIELDS) """ struct InfinitePartitionFunction{T<:PartitionFunctionTensor} A::Matrix{T} @@ -38,7 +42,7 @@ end """ InfinitePartitionFunction( - f=randn, T=ComplexF64, Pspaces::A, Nspaces::A, [Espaces::A] + [f=randn, T=ComplexF64,] Pspaces::A, Nspaces::A, [Espaces::A] ) where {A<:AbstractMatrix{<:Union{Int,ElementarySpace}}} Create an `InfinitePartitionFunction` by specifying the physical, north virtual and east virtual spaces @@ -93,7 +97,9 @@ function InfinitePartitionFunction( end """ - InfinitePartitionFunction(f=randn, T=ComplexF64, Pspace, Nspace, [Espace]; unitcell=(1,1)) + InfinitePartitionFunction( + [f=randn, T=ComplexF64,] Pspace::S, Nspace::S, [Espace::S]; unitcell=(1,1) + ) where {S<:ElementarySpaceLike} Create an InfinitePartitionFunction by specifying its physical, north and east spaces and unit cell. Spaces can be specified either via `Int` or via `ElementarySpace`. diff --git a/src/states/infinitepeps.jl b/src/states/infinitepeps.jl index ce95de962..5c992582a 100644 --- a/src/states/infinitepeps.jl +++ b/src/states/infinitepeps.jl @@ -2,6 +2,10 @@ struct InfinitePEPS{T<:PEPSTensor} Represents an infinite projected entangled-pair state on a 2D square lattice. + +## Fields + +$(TYPEDFIELDS) """ struct InfinitePEPS{T<:PEPSTensor} A::Matrix{T} @@ -35,9 +39,7 @@ function InfinitePEPS(A::AbstractMatrix{<:PEPSTensor}) end """ - InfinitePEPS( - f=randn, T=ComplexF64, Pspaces::A, Nspaces::A, [Espaces::A] - ) where {A<:AbstractMatrix{<:Union{Int,ElementarySpace}}} + InfinitePEPS([f=randn, T=ComplexF64,] Pspaces::A, Nspaces::A, [Espaces::A]) where {A<:AbstractMatrix{<:Union{Int,ElementarySpace}}} Create an `InfinitePEPS` by specifying the physical, north virtual and east virtual spaces of the PEPS tensor at each site in the unit cell as a matrix. Each individual space can be @@ -65,7 +67,7 @@ function InfinitePEPS( end """ - InfinitePEPS(A; unitcell=(1, 1)) + InfinitePEPS(A::PEPSTensor; unitcell=(1, 1)) Create an `InfinitePEPS` by specifying a tensor and unit cell. @@ -89,7 +91,7 @@ function InfinitePEPS(A::T; unitcell::Tuple{Int,Int}=(1, 1)) where {T<:PEPSTenso end """ - InfinitePEPS(f=randn, T=ComplexF64, Pspace, Nspace, [Espace]; unitcell=(1,1)) + InfinitePEPS([f=randn, T=ComplexF64,] Pspace, Nspace, [Espace]; unitcell=(1,1)) Create an InfinitePEPS by specifying its physical, north and east spaces and unit cell. Spaces can be specified either via `Int` or via `ElementarySpace`. @@ -201,6 +203,12 @@ end ## FiniteDifferences vectorization +""" + to_vec(A::InfinitePEPS) -> vec, state_from_vec + +Vectorize an `InfinitePEPS` into a vector of real numbers. A vectorized infinite PEPS can +retrieved again as an `InfinitePEPS` by application of the `state_from_vec` map. +""" function FiniteDifferences.to_vec(A::InfinitePEPS) vec, back = FiniteDifferences.to_vec(unitcell(A)) function state_from_vec(vec) diff --git a/src/states/infiniteweightpeps.jl b/src/states/infiniteweightpeps.jl index 58b6c5598..63cce8c82 100644 --- a/src/states/infiniteweightpeps.jl +++ b/src/states/infiniteweightpeps.jl @@ -10,8 +10,15 @@ const PEPSWeight{T,S} = AbstractTensorMap{T,S,1,1} """ struct SUWeight{E<:PEPSWeight} -Schmidt bond weights used in simple/cluster update. -Weight elements are always real. +Schmidt bond weights used in simple/cluster update. Weight elements are always real. + +## Fields + +$(TYPEDFIELDS) + +## Constructors + + SUWeight(wts_mats::AbstractMatrix{E}...) where {E<:PEPSWeight} """ struct SUWeight{E<:PEPSWeight} data::Array{E,3} @@ -65,6 +72,16 @@ end Represents an infinite projected entangled-pair state on a 2D square lattice consisting of vertex tensors and bond weights. + +## Fields + +$(TYPEDFIELDS) + +## Constructors + + InfiniteWeightPEPS(vertices::Matrix{T}, weight_mats::Matrix{E}...) where {T<:PEPSTensor,E<:PEPSWeight} + InfiniteWeightPEPS([f=randn, T=ComplexF64,] Pspaces::M, Nspaces::M, [Espaces::M]) where {M<:AbstractMatrix{<:Union{Int,ElementarySpace}}} + InfiniteWeightPEPS([f=randn, T=ComplexF64,] Pspace::S, Nspace::S, Espace::S=Nspace; unitcell::Tuple{Int,Int}=(1, 1)) where {S<:ElementarySpace} """ struct InfiniteWeightPEPS{T<:PEPSTensor,E<:PEPSWeight} vertices::Matrix{T} @@ -92,9 +109,7 @@ struct InfiniteWeightPEPS{T<:PEPSTensor,E<:PEPSWeight} end """ - InfiniteWeightPEPS( - vertices::Matrix{T}, weight_mats::Matrix{E}... - ) where {T<:PEPSTensor,E<:PEPSWeight} + InfiniteWeightPEPS(vertices::Matrix{T}, weight_mats::Matrix{E}...) where {T<:PEPSTensor,E<:PEPSWeight} Create an InfiniteWeightPEPS from matrices of vertex tensors, and separate matrices of weights on each type of bond at all locations in the unit cell. @@ -106,9 +121,7 @@ function InfiniteWeightPEPS( end """ - InfiniteWeightPEPS( - f=randn, T=ComplexF64, Pspaces::M, Nspaces::M, [Espaces::M] - ) where {M<:AbstractMatrix{<:Union{Int,ElementarySpace}}} + InfiniteWeightPEPS([f=randn, T=ComplexF64,] Pspaces::M, Nspaces::M, [Espaces::M]) where {M<:AbstractMatrix{<:Union{Int,ElementarySpace}}} Create an InfiniteWeightPEPS by specifying the physical, north virtual and east virtual spaces of the PEPS vertex tensor at each site in the unit cell as a matrix. @@ -133,14 +146,15 @@ function InfiniteWeightPEPS( end """ - InfiniteWeightPEPS( - f, T, Pspace::S, Nspace::S, Espace::S=Nspace; unitcell::Tuple{Int,Int}=(1, 1) - ) where {S<:ElementarySpace} + InfiniteWeightPEPS([f=randn, T=ComplexF64,] Pspace::S, Nspace::S, Espace::S=Nspace; unitcell::Tuple{Int,Int}=(1, 1)) where {S<:ElementarySpace} Create an InfiniteWeightPEPS by specifying its physical, north and east spaces (as `ElementarySpace`s) and unit cell size. Use `T` to specify the element type of the vertex tensors. Bond weights are initialized as identity matrices of element type `Float64`. """ +function InfiniteWeightPEPS(Pspaces::S, Nspaces::S, Espaces::S) where {S<:ElementarySpace} + return InfiniteWeightPEPS(randn, ComplexF64, Pspaces, Nspaces, Espaces) +end function InfiniteWeightPEPS( f, T, Pspace::S, Nspace::S, Espace::S=Nspace; unitcell::Tuple{Int,Int}=(1, 1) ) where {S<:ElementarySpace} @@ -198,12 +212,10 @@ function _absorb_weights( end """ - absorb_weight(t::PEPSTensor, row::Int, col::Int, ax::Int, weights::SUWeight; - sqrtwt::Bool=false, invwt::Bool=false) + absorb_weight(t::PEPSTensor, row::Int, col::Int, ax::Int, weights::SUWeight; sqrtwt::Bool=false, invwt::Bool=false) -Absorb or remove environment weight on an axis of vertex tensor `t` -known to be located at position (`row`, `col`) in the unit cell. -Weights around the tensor at `(row, col)` are +Absorb or remove environment weight on an axis of vertex tensor `t` known to be located at +position (`row`, `col`) in the unit cell. Weights around the tensor at `(row, col)` are ``` ↓ [2,r,c] @@ -214,19 +226,21 @@ Weights around the tensor at `(row, col)` are ↓ ``` -# Arguments -- `t::T`: The vertex tensor to which the weight will be absorbed. The first axis of `t` should be the physical axis. -- `row::Int`: The row index specifying the position in the tensor network. -- `col::Int`: The column index specifying the position in the tensor network. -- `ax::Int`: The axis into which the weight is absorbed, taking values from 1 to 4, standing for north, east, south, west respectively. -- `weights::SUWeight`: The weight object to absorb into the tensor. -- `sqrtwt::Bool=false` (optional): If `true`, the square root of the weight is absorbed. -- `invwt::Bool=false` (optional): If `true`, the inverse of the weight is absorbed. +## Arguments + +- `t::T` : The vertex tensor to which the weight will be absorbed. The first axis of `t` should be the physical axis. +- `row::Int` : The row index specifying the position in the tensor network. +- `col::Int` : The column index specifying the position in the tensor network. +- `ax::Int` : The axis into which the weight is absorbed, taking values from 1 to 4, standing for north, east, south, west respectively. +- `weights::SUWeight` : The weight object to absorb into the tensor. + +## Keyword arguments + +- `sqrtwt::Bool=false` : If `true`, the square root of the weight is absorbed. +- `invwt::Bool=false` : If `true`, the inverse of the weight is absorbed. -# Details -The optional kwargs `sqrtwt` and `invwt` allow taking the square root or the inverse of the weight before absorption. +## Examples -# Examples ```julia # Absorb the weight into the north axis of tensor at position (2, 3) absorb_weight(t, 2, 3, 1, weights) diff --git a/src/utility/diffable_threads.jl b/src/utility/diffable_threads.jl index c03937d98..b01375482 100644 --- a/src/utility/diffable_threads.jl +++ b/src/utility/diffable_threads.jl @@ -1,7 +1,7 @@ """ dtmap(args...; kwargs...) -Differentiable wrapper around `OhMyThreads.tmap`. +Differentiable wrapper around [OhMyThreads.tmap]. All calls of `dtmap` inside of PEPSKit use the threading scheduler stored inside `Defaults.scheduler` which can be modified using `set_scheduler!`. diff --git a/src/utility/retractions.jl b/src/utility/retractions.jl index 67286a285..fe4aee991 100644 --- a/src/utility/retractions.jl +++ b/src/utility/retractions.jl @@ -3,7 +3,7 @@ Utilities for preserving the norm of (VectorInterface-compliant) vectors during =# """ - norm_preserving_retract(A, η, α) +$(SIGNATURES) Performs a norm-preserving retraction of vector `A` along the direction `η` with step size `α`, giving a new vector `A´`, @@ -33,7 +33,7 @@ function norm_preserving_retract(A, η, α) end """ - norm_preserving_transport!(ξ, A, η, α, A′) +$(SIGNATURES) Transports a direction `ξ` at `A` to a valid direction at `A´` corresponding to the norm-preserving retraction of `A` along `η` with step size `α`. In particular, starting diff --git a/src/utility/rotations.jl b/src/utility/rotations.jl index d83d43568..57f0e0393 100644 --- a/src/utility/rotations.jl +++ b/src/utility/rotations.jl @@ -9,7 +9,7 @@ const SOUTHEAST = 3 const SOUTHWEST = 4 """ - rotate_north(t, dir) +$(SIGNATURES) Rotate the `dir` direction of `t` to face north by successive applications of `rotl90`. """ diff --git a/src/utility/svd.jl b/src/utility/svd.jl index d4504d0fc..01184961b 100644 --- a/src/utility/svd.jl +++ b/src/utility/svd.jl @@ -12,16 +12,23 @@ using TensorKit: const KrylovKitCRCExt = Base.get_extension(KrylovKit, :KrylovKitChainRulesCoreExt) """ - struct FullSVDReverseRule - FullSVDReverseRule(; kwargs...) +$(TYPEDEF) SVD reverse-rule algorithm which uses a modified version of TensorKit's `tsvd!` reverse-rule allowing for Lorentzian broadening and output verbosity control. -## Keyword arguments +## Fields + +$(TYPEDFIELDS) + +## Constructors + + FullSVDReverseRule(; kwargs...) + +Construct a `FullSVDReverseRule` algorithm struct from the following keyword arguments: -* `broadening::Float64=$(Defaults.svd_rrule_broadening)`: Lorentzian broadening amplitude for smoothing divergent term in SVD derivative in case of (pseudo) degenerate singular values. -* `verbosity::Int=0`: Suppresses all output if `≤0`, print gauge dependency warnings if `1`, and always print gauge dependency if `≥2`. +* `broadening::Float64=$(Defaults.svd_rrule_broadening)` : Lorentzian broadening amplitude for smoothing divergent term in SVD derivative in case of (pseudo) degenerate singular values. +* `verbosity::Int=0` : Suppresses all output if `≤0`, prints gauge dependency warnings if `1`, and always prints gauge dependency if `≥2`. """ @kwdef struct FullSVDReverseRule broadening::Float64 = Defaults.svd_rrule_broadening @@ -29,18 +36,25 @@ allowing for Lorentzian broadening and output verbosity control. end """ - struct SVDAdjoint - SVDAdjoint(; kwargs...) +$(TYPEDEF) Wrapper for a SVD algorithm `fwd_alg` with a defined reverse rule `rrule_alg`. If `isnothing(rrule_alg)`, Zygote differentiates the forward call automatically. -## Keyword arguments +## Fields + +$(TYPEDFIELDS) + +## Constructors + + SVDAdjoint(; kwargs...) + +Construct a `SVDAdjoint` algorithm struct based on the following keyword arguments: * `fwd_alg::Union{Algorithm,NamedTuple}=(; alg::Symbol=$(Defaults.svd_fwd_alg))`: SVD algorithm of the forward pass which can either be passed as an `Algorithm` instance or a `NamedTuple` where `alg` is one of the following: - - `:sdd`: TensorKit's wrapper for LAPACK's `_gesdd` - - `:svd`: TensorKit's wrapper for LAPACK's `_gesvd` - - `:iterative`: Iterative SVD only computing the specifed number of singular values and vectors, see ['IterSVD'](@ref) + - `:sdd` : TensorKit's wrapper for LAPACK's `_gesdd` + - `:svd` : TensorKit's wrapper for LAPACK's `_gesvd` + - `:iterative` : Iterative SVD only computing the specifed number of singular values and vectors, see [`IterSVD`](@ref) * `rrule_alg::Union{Algorithm,NamedTuple}=(; alg::Symbol=$(Defaults.svd_rrule_alg))`: Reverse-rule algorithm for differentiating the SVD. Can be supplied by an `Algorithm` instance directly or as a `NamedTuple` where `alg` is one of the following: - `:full`: Uses a modified version of TensorKit's reverse-rule for `tsvd` which doesn't solve any linear problem and instead requires access to the full SVD, see [`FullSVDReverseRule`](@ref). - `:gmres`: GMRES iterative linear solver, see the [KrylovKit docs](https://jutho.github.io/KrylovKit.jl/stable/man/algorithms/#KrylovKit.GMRES) for details @@ -112,7 +126,7 @@ function SVDAdjoint(; fwd_alg=(;), rrule_alg=(;)) end """ - PEPSKit.tsvd(t, alg; trunc=notrunc(), p=2) + PEPSKit.tsvd(t, alg::SVDAdjoint; trunc=notrunc(), p=2) Wrapper around `TensorKit.tsvd` which dispatches on the `alg` argument. This is needed since a custom adjoint for `PEPSKit.tsvd` may be defined, @@ -132,7 +146,6 @@ end ## Forward algorithms -# TODO: add `LinearAlgebra.cond` to TensorKit # Compute condition number smax / smin for diagonal singular value TensorMap function _condition_number(S::AbstractTensorMap) smax = maximum(first ∘ last, blocks(S)) @@ -171,12 +184,16 @@ function _tsvd!( end """ - struct FixedSVD +$(TYPEDEF) SVD struct containing a pre-computed decomposition or even multiple ones. Additionally, it can contain the untruncated full decomposition as well. The call to `tsvd` just returns the pre-computed U, S and V. In the reverse pass, the SVD adjoint is computed with these exact U, S, and V and, potentially, the full decompositions if the adjoints needs access to them. + +## Fields + +$(TYPEDFIELDS) """ struct FixedSVD{Ut,St,Vt,Utf,Stf,Vtf} U::Ut @@ -209,8 +226,7 @@ function _tsvd!(_, alg::FixedSVD, ::TruncationScheme, ::Real) end """ - struct IterSVD(; alg=KrylovKit.GKL(), fallback_threshold = Inf, start_vector=random_start_vector) - IterSVD(; kwargs...) +$(TYPEDEF) Iterative SVD solver based on KrylovKit's GKL algorithm, adapted to (symmetric) tensors. The number of targeted singular values is set via the `TruncationSpace` in `ProjectorAlg`. @@ -218,11 +234,19 @@ In particular, this make it possible to specify the targeted singular values blo In case the symmetry block is too small as compared to the number of singular values, or the iterative SVD didn't converge, the algorithm falls back to a dense SVD. -## Keyword arguments +## Fields + +$(TYPEDFIELDS) + +## Constructors + + IterSVD(; kwargs...) + +Construct an `IterSVD` algorithm struct based on the following keyword arguments: -* `alg::KrlovKit.GKL=KrylovKit.GKL(; tol=1e-14, krylovdim=25)`: GKL algorithm struct for block-wise iterative SVD. -* `fallback_threshold::Float64=Inf`: Threshold for `howmany / minimum(size(block))` above which (if the block is too small) the algorithm falls back to TensorKit's dense SVD. -* `start_vector=random_start_vector`: Function providing the initial vector for the iterative SVD algorithm. +* `alg::KrylovKit.GKL=KrylovKit.GKL(; tol=1e-14, krylovdim=25)` : GKL algorithm struct for block-wise iterative SVD. +* `fallback_threshold::Float64=Inf` : Threshold for `howmany / minimum(size(block))` above which (if the block is too small) the algorithm falls back to TensorKit's dense SVD. +* `start_vector=random_start_vector` : Function providing the initial vector for the iterative SVD algorithm. """ @kwdef struct IterSVD alg::KrylovKit.GKL = KrylovKit.GKL(; tol=1e-14, krylovdim=25) diff --git a/src/utility/symmetrization.jl b/src/utility/symmetrization.jl index 1f53f9303..5bde3ea92 100644 --- a/src/utility/symmetrization.jl +++ b/src/utility/symmetrization.jl @@ -1,28 +1,28 @@ abstract type SymmetrizationStyle end """ - struct ReflectDepth <: SymmetrizationStyle +$(TYPEDEF) Reflection symmmetrization along the horizontal axis, such that north and south are mirrored. """ struct ReflectDepth <: SymmetrizationStyle end """ - struct ReflectWidth <: SymmetrizationStyle +$(TYPEDEF) Reflection symmmetrization along the vertical axis, such that east and west are mirrored. """ struct ReflectWidth <: SymmetrizationStyle end """ - struct Rotate <: SymmetrizationStyle +$(TYPEDEF) Rotation symmmetrization leaving the object invariant under π/2 rotations. """ struct Rotate <: SymmetrizationStyle end """ - struct RotateReflect <: SymmetrizationStyle +$(TYPEDEF) Full reflection and rotation symmmetrization, such that reflection along the horizontal and vertical axis as well as π/2 rotations leave the object invariant. diff --git a/src/utility/util.jl b/src/utility/util.jl index f57865a73..da66f66b0 100644 --- a/src/utility/util.jl +++ b/src/utility/util.jl @@ -28,7 +28,7 @@ end # iterator over each coordinates """ - eachcoordinate(x, dirs=1:4) + eachcoordinate(x, [dirs=1:4]) Enumerate all (dir, row, col) pairs. """ @@ -89,17 +89,17 @@ function ChainRulesCore.rrule( end """ - absorb_s(u::AbstractTensorMap, s::DiagonalTensorMap, vh::AbstractTensorMap) + absorb_s(U::AbstractTensorMap, S::DiagonalTensorMap, V::AbstractTensorMap) -Given `tsvd` result `u`, `s` and `vh`, absorb singular values `s` into `u` and `vh` by: +Given `tsvd` result `U`, `S` and `V`, absorb singular values `S` into `U` and `V` by: ``` - u -> u * sqrt(s), vh -> sqrt(s) * vh + U -> U * sqrt(S), V -> sqrt(S) * V ``` """ -function absorb_s(u::AbstractTensorMap, s::DiagonalTensorMap, vh::AbstractTensorMap) - @assert !isdual(space(s, 1)) - sqrt_s = sdiag_pow(s, 0.5) - return u * sqrt_s, sqrt_s * vh +function absorb_s(U::AbstractTensorMap, S::DiagonalTensorMap, V::AbstractTensorMap) + @assert !isdual(space(S, 1)) + sqrt_S = sdiag_pow(S, 0.5) + return U * sqrt_S, sqrt_S * V end """ @@ -202,12 +202,13 @@ function ChainRulesCore.rrule(::typeof(_setindex), a::AbstractArray, tv, args... return t, _setindex_pullback end +# TODO: link to Zygote.showgrad once they update documenter.jl """ @showtypeofgrad(x) Macro utility to show to type of the gradient that is about to accumulate for `x`. -See also [`Zygote.@showgrad`](@ref). +See also `Zygote.@showgrad`. """ macro showtypeofgrad(x) return :( diff --git a/test/examples/bose_hubbard.jl b/test/examples/bose_hubbard.jl index 948ec033e..be363fef9 100644 --- a/test/examples/bose_hubbard.jl +++ b/test/examples/bose_hubbard.jl @@ -33,7 +33,7 @@ reuse_env = true # Hamiltonian H = bose_hubbard_model(ComplexF64, symmetry, lattice; cutoff, t, U, n) -Pspaces = H.lattice +Pspaces = physicalspace(H) # initialize state Nspaces = fill(Vpeps, size(lattice)...) diff --git a/test/examples/j1j2_model.jl b/test/examples/j1j2_model.jl index bd886bb19..9f6511680 100644 --- a/test/examples/j1j2_model.jl +++ b/test/examples/j1j2_model.jl @@ -11,7 +11,7 @@ using OptimKit # initialize states Random.seed!(91283219347) -H = j1_j2(InfiniteSquare(); J2=0.25) +H = j1_j2_model(InfiniteSquare(); J2=0.25) peps₀ = product_peps(2, χbond; noise_amp=1e-1) peps₀ = symmetrize!(peps₀, RotateReflect()) env₀, = leading_boundary(CTMRGEnv(peps₀, ComplexSpace(χenv)), peps₀) diff --git a/test/examples/tf_ising.jl b/test/examples/tf_ising.jl index fa3b730ca..cc6758db5 100644 --- a/test/examples/tf_ising.jl +++ b/test/examples/tf_ising.jl @@ -33,7 +33,7 @@ peps, env, E, = fixedpoint(H, peps₀, env₀; tol=gradtol) # compute magnetization σx = TensorMap(scalartype(peps₀)[0 1; 1 0], ℂ^2, ℂ^2) -M = LocalOperator(H.lattice, (CartesianIndex(1, 1),) => σx) +M = LocalOperator(physicalspace(H), (CartesianIndex(1, 1),) => σx) magn = expectation_value(peps, M, env) @test E ≈ e atol = 1e-2