diff --git a/.gitignore b/.gitignore index d649038..9428ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ Manifest.toml # Vi/Vim backup files .*.swp + +.DS_Store diff --git a/Project.toml b/Project.toml index 193f0c0..1ddd8f5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,17 +1,53 @@ name = "ITensorMPS" uuid = "0d1a4710-d33b-49a5-8f18-73bdf49b47e2" authors = ["Matthew Fishman ", "Miles Stoudenmire "] -version = "0.2.6" +version = "0.3.0" [deps] -ITensorTDVP = "25707e16-a4db-4a07-99d9-4d67b7af0342" +Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" ITensors = "9136182c-28ba-11e9-034c-db9fb085ebd5" -Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +IsApprox = "28f27b66-4bd8-47e7-9110-e2746eb8bed7" +KrylovKit = "0b1a1467-8014-51b9-945f-bf0ae24f4b77" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +NDTensors = "23ae76d9-e61a-49c4-8f12-3f1a16adf9cf" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SerializedElementArrays = "d3ce8812-9567-47e9-a7b5-65a6d70a3065" +TupleTools = "9d95972d-f1c8-5527-a6e0-b4b365fa01f6" + +[weakdeps] +ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" +Observers = "338f10d5-c7f1-4033-a7d1-f9dec39bcaa0" +PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" +ZygoteRules = "700de1a5-db45-46bc-99cf-38207098b444" + +[extensions] +ITensorMPSChainRulesCoreExt = "ChainRulesCore" +ITensorMPSHDF5Ext = "HDF5" +ITensorMPSObserversExt = "Observers" +ITensorMPSPackageCompilerExt = "PackageCompiler" +ITensorMPSZygoteRulesExt = ["ChainRulesCore", "ZygoteRules"] [compat] -ITensorTDVP = "0.4.1" -ITensors = "0.6.7" -Reexport = "1" +Adapt = "4.1.0" +ChainRulesCore = "1.10" +Compat = "4.16.0" +HDF5 = "0.14, 0.15, 0.16, 0.17" +ITensors = "0.7" +IsApprox = "2.0.0" +KrylovKit = "0.8.1" +LinearAlgebra = "1.10" +NDTensors = "0.3.46" +Observers = "0.2" +PackageCompiler = "1, 2" +Printf = "1.10" +Random = "1.10" +SerializedElementArrays = "0.1.0" +Test = "1.10" +TupleTools = "1.5.0" +ZygoteRules = "0.2.2" julia = "1.10" [extras] diff --git a/README.md b/README.md index 3e9b09b..4bdba9c 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,20 @@ # ITensorMPS.jl -[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://itensor.github.io/ITensorMPS.jl/stable/) -[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://itensor.github.io/ITensorMPS.jl/dev/) +[![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://itensor.github.io/ITensors.jl/dev/) [![Build Status](https://github.com/ITensor/ITensorMPS.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/ITensor/ITensorMPS.jl/actions/workflows/CI.yml?query=branch%3Amain) -[![Coverage](https://codecov.io/gh/ITensor/ITensorMPS.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/ITensor/ITensorMPS.jl) [![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle) -Finite MPS and MPO methods based on ITensor (ITensors.jl). +Finite MPS and MPO methods based on the Julia version of [ITensor](https://www.itensor.org) ([ITensors.jl](https://github.com/ITensor/ITensors.jl)). See the [ITensors.jl documentation](https://itensor.github.io/ITensors.jl/dev/) for more details. -This package currently re-exports the MPS and MPO functionality of the [ITensors.jl](https://github.com/ITensor/ITensors.jl), including functionality like DMRG, applying MPO to MPS, applying gates to MPS and MPO, etc. See the [ITensor documentation](https://itensor.github.io/ITensors.jl/dev) for guides and examples on using this package. +## News -Additionally, it re-exports the functionality of the [ITensorTDVP.jl](https://github.com/ITensor/ITensorTDVP.jl) package, which provides other DMRG-like MPS solvers such as TDVP and MPS linear equation solving. +### ITensorMPS.jl v0.3 release notes -## Upgrade guide +All MPS/MPO code from [ITensors.jl](https://github.com/ITensor/ITensors.jl) and [ITensorTDVP.jl](https://github.com/ITensor/ITensorTDVP.jl) has been moved into this repository and this repository now relies on ITensors.jl v0.7 and above. All of the MPS/MPO functionality that was previously in ITensors.jl and ITensorTDVP.jl will be developed here from now on. For users of this repository, this change should not break any code, though please let us know if you have any issues. -The goal will be to move the MPS and MPO code from the ITensors.jl package, along with all of the code from the [ITensorTDVP.jl](https://github.com/ITensor/ITensorTDVP.jl) package, into this repository. If you are using any MPS/MPO functionality of ITensors.jl, such as the `MPS` and `MPO` types or constructors thereof (like `randomMPS`), `OpSum`, `siteinds`, `dmrg`, `apply`, etc. you should install the ITensorMPS.jl package with `import Pkg; Pkg.add("ITensorMPS")` and add `using ITensorMPS` to your code. Additionally, if you are currently using [ITensorTDVP.jl](https://github.com/ITensor/ITensorTDVP.jl), you should replace `using ITensorTDVP` with `using ITensorMPS` in your codes. +#### Upgrade guide -## News +If you are using any MPS/MPO functionality of ITensors.jl, such as the `MPS` and `MPO` types or constructors thereof (like `random_mps`), `OpSum`, `siteinds`, `dmrg`, `apply`, etc. you should install the ITensorMPS.jl package with `import Pkg; Pkg.add("ITensorMPS")` and add `using ITensorMPS` to your code. Additionally, if you are currently using [ITensorTDVP.jl](https://github.com/ITensor/ITensorTDVP.jl), you should replace `using ITensorTDVP` with `using ITensorMPS` in your code. ### ITensorMPS.jl v0.2.1 release notes diff --git a/examples/autodiff/circuit_optimization/op.jl b/examples/autodiff/circuit_optimization/op.jl new file mode 100644 index 0000000..bdb40e6 --- /dev/null +++ b/examples/autodiff/circuit_optimization/op.jl @@ -0,0 +1,52 @@ +using ITensors, ITensorMPS +using Zygote + +s = siteind("Qubit") + +f(x) = op("Ry", s; θ=x)[1, 1] + +x = 0.2 +@show f(x), cos(x / 2) +@show f'(x), -sin(x / 2) / 2 + +# Simple gate optimization +ψ0 = state(s, "0") +ψp = state(s, "+") + +function loss(x) + U = op("Ry", s; θ=x) + Uψ0 = replaceprime(U * ψ0, 1 => 0) + return -(dag(ψp) * Uψ0)[] +end + +# Extremely simple gradient descent implementation, +# where gradients are computing with automatic differentiation +# using Zygote. +function gradient_descent(f, x0; γ, nsteps, grad_tol) + @show γ, nsteps + x = x0 + f_x = f(x) + ∇f_x = f'(x) + step = 0 + @show step, x, f_x, ∇f_x + for step in 1:nsteps + x -= γ * ∇f_x + f_x = f(x) + ∇f_x = f'(x) + @show step, x, f_x, ∇f_x + if norm(∇f_x) ≤ grad_tol + break + end + end + return x, f_x, ∇f_x +end + +x0 = 0 +γ = 2.0 # Learning rate +nsteps = 30 # Number of steps of gradient descent +grad_tol = 1e-4 # Stop if gradient falls below this value +x, loss_x, ∇loss_x = gradient_descent(loss, x0; γ=γ, nsteps=nsteps, grad_tol=grad_tol) + +@show x0, loss(x0) +@show x, loss(x) +@show π / 2, loss(π / 2) diff --git a/examples/autodiff/circuit_optimization/state_preparation.jl b/examples/autodiff/circuit_optimization/state_preparation.jl new file mode 100644 index 0000000..bb09325 --- /dev/null +++ b/examples/autodiff/circuit_optimization/state_preparation.jl @@ -0,0 +1,65 @@ +using ITensors, ITensorMPS +using OptimKit +using Random +using Zygote + +nsites = 20 # Number of sites +nlayers = 3 # Layers of gates in the ansatz +gradtol = 1e-4 # Tolerance for stopping gradient descent + +# A layer of the circuit we want to optimize +function layer(nsites, θ⃗) + RY_layer = [("Ry", (n,), (θ=θ⃗[n],)) for n in 1:nsites] + CX_layer = [("CX", (n, n + 1)) for n in 1:2:(nsites - 1)] + return [RY_layer; CX_layer] +end + +# The variational circuit we want to optimize +function variational_circuit(nsites, nlayers, θ⃗) + range = 1:nsites + circuit = layer(nsites, θ⃗[range]) + for n in 1:(nlayers - 1) + circuit = [circuit; layer(nsites, θ⃗[range .+ n * nsites])] + end + return circuit +end + +Random.seed!(1234) + +θ⃗ᵗᵃʳᵍᵉᵗ = 2π * rand(nsites * nlayers) +𝒰ᵗᵃʳᵍᵉᵗ = variational_circuit(nsites, nlayers, θ⃗ᵗᵃʳᵍᵉᵗ) + +s = siteinds("Qubit", nsites) +Uᵗᵃʳᵍᵉᵗ = ops(𝒰ᵗᵃʳᵍᵉᵗ, s) + +ψ0 = MPS(s, "0") + +# Create the random target state +ψᵗᵃʳᵍᵉᵗ = apply(Uᵗᵃʳᵍᵉᵗ, ψ0; cutoff=1e-8) + +# +# The loss function, a function of the gate parameters +# and implicitly depending on the target state: +# +# loss(θ⃗) = -|⟨θ⃗ᵗᵃʳᵍᵉᵗ|U(θ⃗)|0⟩|² = -|⟨θ⃗ᵗᵃʳᵍᵉᵗ|θ⃗⟩|² +# +function loss(θ⃗) + nsites = length(ψ0) + s = siteinds(ψ0) + 𝒰θ⃗ = variational_circuit(nsites, nlayers, θ⃗) + Uθ⃗ = ops(𝒰θ⃗, s) + ψθ⃗ = apply(Uθ⃗, ψ0) + return -abs(inner(ψᵗᵃʳᵍᵉᵗ, ψθ⃗))^2 +end + +θ⃗₀ = randn!(copy(θ⃗ᵗᵃʳᵍᵉᵗ)) + +@show loss(θ⃗₀), loss(θ⃗ᵗᵃʳᵍᵉᵗ) + +loss_∇loss(x) = (loss(x), convert(Vector, loss'(x))) +algorithm = LBFGS(; gradtol=gradtol, verbosity=2) +θ⃗ₒₚₜ, lossₒₚₜ, ∇lossₒₚₜ, numfg, normgradhistory = optimize(loss_∇loss, θ⃗₀, algorithm) + +@show loss(θ⃗ₒₚₜ), loss(θ⃗ᵗᵃʳᵍᵉᵗ) + +nothing diff --git a/examples/autodiff/circuit_optimization/vqe.jl b/examples/autodiff/circuit_optimization/vqe.jl new file mode 100644 index 0000000..edaeeae --- /dev/null +++ b/examples/autodiff/circuit_optimization/vqe.jl @@ -0,0 +1,81 @@ +using ITensors, ITensorMPS +using OptimKit +using Random +using Zygote + +nsites = 4 # Number of sites +nlayers = 2 # Layers of gates in the ansatz +gradtol = 1e-4 # Tolerance for stopping gradient descent + +# The Hamiltonian we are minimizing +function ising_hamiltonian(nsites; h) + ℋ = OpSum() + for j in 1:(nsites - 1) + ℋ -= 1, "Z", j, "Z", j + 1 + end + for j in 1:nsites + ℋ += h, "X", j + end + return ℋ +end + +# A layer of the circuit we want to optimize +function layer(nsites, θ⃗) + RY_layer = [("Ry", (n,), (θ=θ⃗[n],)) for n in 1:nsites] + CX_layer = [("CX", (n, n + 1)) for n in 1:2:(nsites - 1)] + return [RY_layer; CX_layer] +end + +# The variational circuit we want to optimize +function variational_circuit(nsites, nlayers, θ⃗) + range = 1:nsites + circuit = layer(nsites, θ⃗[range]) + for n in 1:(nlayers - 1) + circuit = [circuit; layer(nsites, θ⃗[range .+ n * nsites])] + end + return circuit +end + +s = siteinds("Qubit", nsites) + +h = 1.3 +ℋ = ising_hamiltonian(nsites; h=h) +H = MPO(ℋ, s) +ψ0 = MPS(s, "0") + +# +# The loss function, a function of the gate parameters +# and implicitly depending on the Hamiltonian and state: +# +# loss(θ⃗) = ⟨0|U(θ⃗)† H U(θ⃗)|0⟩ = ⟨θ⃗|H|θ⃗⟩ +# +function loss(θ⃗) + nsites = length(ψ0) + s = siteinds(ψ0) + 𝒰θ⃗ = variational_circuit(nsites, nlayers, θ⃗) + Uθ⃗ = ops(𝒰θ⃗, s) + ψθ⃗ = apply(Uθ⃗, ψ0; cutoff=1e-8) + return inner(ψθ⃗, H, ψθ⃗; cutoff=1e-8) +end + +Random.seed!(1234) +θ⃗₀ = 2π * rand(nsites * nlayers) + +@show loss(θ⃗₀) + +println("\nOptimize circuit with gradient optimization") + +loss_∇loss(x) = (loss(x), convert(Vector, loss'(x))) +algorithm = LBFGS(; gradtol=1e-3, verbosity=2) +θ⃗ₒₚₜ, lossₒₚₜ, ∇lossₒₚₜ, numfg, normgradhistory = optimize(loss_∇loss, θ⃗₀, algorithm) + +@show loss(θ⃗ₒₚₜ) + +println("\nRun DMRG as a comparison") + +e_dmrg, ψ_dmrg = dmrg(H, ψ0; nsweeps=5, maxdim=10) + +println("\nCompare variational circuit energy to DMRG energy") +@show loss(θ⃗ₒₚₜ), e_dmrg + +nothing diff --git a/examples/autodiff/mps_autodiff.jl b/examples/autodiff/mps_autodiff.jl new file mode 100644 index 0000000..d1194e2 --- /dev/null +++ b/examples/autodiff/mps_autodiff.jl @@ -0,0 +1,49 @@ +using ITensors, ITensorMPS +using OptimKit +using Zygote + +function ising(n; J, h) + os = OpSum() + for j in 1:(n - 1) + os -= J, "Z", j, "Z", j + 1 + end + for j in 1:n + os -= h, "X", j + end + return os +end + +function loss(H, ψ) + n = length(ψ) + ψHψ = ITensor(1.0) + ψψ = ITensor(1.0) + for j in 1:n + ψHψ = ψHψ * dag(ψ[j]') * H[j] * ψ[j] + ψψ = ψψ * replaceinds(dag(ψ[j]'), s[j]' => s[j]) * ψ[j] + end + return ψHψ[] / ψψ[] +end + +n = 10 +s = siteinds("S=1/2", n) +J = 1.0 +h = 0.5 + +# Loss function only works with `Vector{ITensor}`, +# extract with `ITensors.data`. +ψ0 = ITensors.data(random_mps(s; linkdims=10)) +H = ITensors.data(MPO(ising(n; J, h), s)) + +loss(ψ) = loss(H, ψ) + +optimizer = LBFGS(; maxiter=25, verbosity=2) +function loss_and_grad(x) + y, (∇,) = withgradient(loss, x) + return y, ∇ +end +ψ, fs, gs, niter, normgradhistory = optimize(loss_and_grad, ψ0, optimizer) +Edmrg, ψdmrg = dmrg(MPO(H), MPS(ψ0); nsweeps=10, cutoff=1e-8) + +@show loss(ψ0), norm(loss'(ψ0)) +@show loss(ψ), norm(loss'(ψ)) +@show loss(ITensors.data(ψdmrg)), norm(loss'(ITensors.data(ψdmrg))) diff --git a/examples/autodiff/ops/ops_ad.jl b/examples/autodiff/ops/ops_ad.jl new file mode 100644 index 0000000..c509388 --- /dev/null +++ b/examples/autodiff/ops/ops_ad.jl @@ -0,0 +1,154 @@ +using ITensors, ITensorMPS +using ITensors.Ops +using Zygote + +s = siteinds("S=1/2", 4) + +function f1(x) + y = ITensor(Op("Ry", 1; θ=x), s) + return y[1, 1] +end + +@show x = 2.0 + +@show f1(x) +@show f1'(x) + +function f2(x) + y = exp(ITensor(Op("Ry", 1; θ=x), s)) + return y[1, 1] +end + +@show x = 2.0 + +@show f2(x) +@show f2'(x) + +function f3(x) + y = Op("Ry", 1; θ=x) + Op("Ry", 1; θ=x) + return y[1].params.θ +end + +@show x = 2.0 + +@show f3(x) +@show f3'(x) + +function f4(x) + y = ITensor(Op("Ry", 1; θ=x) + Op("Ry", 1; θ=x), s) + return y[1, 1] +end + +@show x = 2.0 + +@show f4(x) +@show f4'(x) + +function f5(x) + y = exp(ITensor(Op("Ry", 1; θ=x) + Op("Ry", 1; θ=x), s)) + return y[1, 1] +end + +@show x = 2.0 + +@show f5(x) +@show f5'(x) + +function f6(x) + y = ITensor(exp(Op("Ry", 1; θ=x) + Op("Ry", 1; θ=x)), s) + return y[1, 1] +end + +@show x = 2.0 + +@show f6(x) +@show f6'(x) + +function f7(x) + y = ITensor(2 * Op("Ry", 1; θ=x), s) + return y[1, 1] +end + +@show x = 2.0 + +@show f7(x) +@show f7'(x) + +function f8(x) + y = ITensor(2 * (Op("Ry", 1; θ=x) + Op("Ry", 1; θ=x)), s) + return y[1, 1] +end + +@show x = 2.0 + +@show f8(x) +@show f8'(x) + +function f9(x) + y = ITensor(Op("Ry", 1; θ=x) * Op("Ry", 2; θ=x), s) + return y[1, 1] +end + +@show x = 2.0 + +@show f9(x) +@show f9'(x) + +function f10(x) + y = ITensor(exp(-x * Op("X", 1) * Op("X", 2)), s) + return norm(y) +end + +@show x = 2.0 + +@show f10(x) +@show f10'(x) + +V = random_itensor(s[1], s[2]) + +function f11(x) + y = exp(-x * Op("X", 1) * Op("X", 2)) + y *= exp(-x * Op("X", 1) * Op("X", 2)) + U = Prod{ITensor}(y, s) + return norm(U(V)) +end + +@show x = 2.0 + +@show f11(x) +@show f11'(x) + +function f12(x) + y = exp(-x * (Op("X", 1) + Op("Z", 1) + Op("Z", 1)); alg=Trotter{1}(1)) + U = Prod{ITensor}(y, s) + return norm(U(V)) +end + +@show x = 2.0 + +@show f12(x) +@show f12'(x) + +## ## XXX: Error in vcat! +## function f13(x) +## y = -x * (Op("X", 1) * Op("X", 2) + Op("Z", 1) * Op("Z", 2)) +## U = ITensor(y, s) +## return norm(U * V) +## end +## +## @show x = 2.0 +## +## @show f13(x) +## @show f13'(x) +## +## ## XXX: Error in vcat! +## function f14(x) +## y = exp(-x * (Op("X", 1) * Op("X", 2) + Op("Z", 1) * Op("Z", 2)); alg=Trotter{1}(1)) +## U = ITensor(y, s) +## return norm(U * V) +## end +## +## @show x = 2.0 +## +## @show f14(x) +## @show f14'(x) diff --git a/examples/autodiff/ops/trotter_ad_1.jl b/examples/autodiff/ops/trotter_ad_1.jl new file mode 100644 index 0000000..c6c8fc2 --- /dev/null +++ b/examples/autodiff/ops/trotter_ad_1.jl @@ -0,0 +1,51 @@ +using ITensors, ITensorMPS +using Zygote +using OptimKit + +function ising(n; h) + ℋ = Sum{Op}() + for j in 1:(n - 1) + ℋ -= "Z", j, "Z", j + 1 + end + for j in 1:n + ℋ += h, "X", j + end + return ℋ +end + +n = 4 +s = siteinds("S=1/2", n) + +h = 1.1 + +ℋ = ising(n; h) +βᶠ = 1.0 +𝒰 = exp(-βᶠ * ℋ; alg=Trotter{1}(5)) + +U = Prod{ITensor}(𝒰, s) + +ψ = prod(MPS(s, "0")) + +# Target state +Uψ = U(ψ) + +function loss(β) + 𝒰ᵝ = exp(-β[1] * ℋ; alg=Trotter{1}(5)) + Uᵝ = Prod{ITensor}(𝒰ᵝ, s) + Uᵝψ = Uᵝ(ψ) + return -abs(inner(Uψ, Uᵝψ))^2 / (norm(Uψ) * norm(Uᵝψ))^2 +end + +β⁰ = [0.0] +@show loss(β⁰) +@show loss'(β⁰) + +@show loss(βᶠ) +@show loss'(βᶠ) + +loss_∇loss(β) = (loss(β), convert(Vector, loss'(β))) +algorithm = LBFGS(; gradtol=1e-3, verbosity=2) +βᵒᵖᵗ, _ = optimize(loss_∇loss, β⁰, algorithm) + +@show loss(βᵒᵖᵗ) +@show loss'(βᵒᵖᵗ) diff --git a/examples/autodiff/ops/trotter_ad_2.jl b/examples/autodiff/ops/trotter_ad_2.jl new file mode 100644 index 0000000..163dcc7 --- /dev/null +++ b/examples/autodiff/ops/trotter_ad_2.jl @@ -0,0 +1,52 @@ +using ITensors, ITensorMPS +using Zygote +using OptimKit + +function ising(n; h) + ℋ = Sum{Op}() + for j in 1:(n - 1) + ℋ -= "Z", j, "Z", j + 1 + end + for j in 1:n + ℋ += h, "X", j + end + return ℋ +end + +n = 4 +s = siteinds("S=1/2", n) + +β = 1.0 +hᶠ = 1.1 + +ℋᶠ = ising(n; h=hᶠ) +𝒰ᶠ = exp(-β * ℋᶠ; alg=Trotter{1}(5)) + +Uᶠ = Prod{ITensor}(𝒰ᶠ, s) + +ψ = prod(MPS(s, "0")) + +# Target state +Uᶠψ = Uᶠ(ψ) + +function loss(h) + ℋ = ising(n; h=h[1]) + 𝒰ʰ = exp(-β * ℋ; alg=Trotter{1}(5)) + Uʰ = Prod{ITensor}(𝒰ʰ, s) + Uʰψ = Uʰ(ψ) + return -abs(inner(Uᶠψ, Uʰψ))^2 / (norm(Uᶠψ) * norm(Uʰψ))^2 +end + +h⁰ = [0.0] +@show loss(h⁰) +@show loss'(h⁰) + +@show loss(hᶠ) +@show loss'(hᶠ) + +loss_∇loss(h) = (loss(h), convert(Vector, loss'(h))) +algorithm = LBFGS(; gradtol=1e-3, verbosity=2) +hᵒᵖᵗ, _ = optimize(loss_∇loss, h⁰, algorithm) + +@show loss(hᵒᵖᵗ) +@show loss'(hᵒᵖᵗ) diff --git a/examples/autodiff/ops/vqe.jl b/examples/autodiff/ops/vqe.jl new file mode 100644 index 0000000..6757a8d --- /dev/null +++ b/examples/autodiff/ops/vqe.jl @@ -0,0 +1,103 @@ +using ITensors, ITensorMPS +using OptimKit +using Random +using Zygote + +nsites = 4 # Number of sites +nlayers = 2 # Layers of gates in the ansatz +gradtol = 1e-4 # Tolerance for stopping gradient descent + +# The Hamiltonian we are minimizing +function ising_hamiltonian(nsites; h) + ℋ = Sum{Op}() + for j in 1:(nsites - 1) + ℋ -= "Z", j, "Z", j + 1 + end + for j in 1:nsites + ℋ += h, "X", j + end + return ℋ +end + +## # A layer of the circuit we want to optimize +## function layer(nsites, θ⃗) +## RY_layer = [("Ry", (n,), (θ=θ⃗[n],)) for n in 1:nsites] +## CX_layer = [("CX", (n, n + 1)) for n in 1:2:(nsites - 1)] +## return [RY_layer; CX_layer] +## end +## +## # The variational circuit we want to optimize +## function variational_circuit(nsites, nlayers, θ⃗) +## range = 1:nsites +## circuit = layer(nsites, θ⃗[range]) +## for n in 1:(nlayers - 1) +## circuit = [circuit; layer(nsites, θ⃗[range .+ n * nsites])] +## end +## return Prod{Op}(circuit) +## end + +# A layer of the circuit we want to optimize +function layer(nsites, θ⃗) + l = Prod{Op}() + for n in 1:nsites + l = Op("Ry", n; θ=θ⃗[n]) * l + end + for n in 1:2:(nsites - 1) + l = Op("CX", n, n + 1) * l + end + return l +end + +# The variational circuit we want to optimize +function variational_circuit(nsites, nlayers, θ⃗) + range = 1:nsites + circuit = layer(nsites, θ⃗[range]) + for n in 1:(nlayers - 1) + circuit = layer(nsites, θ⃗[range .+ n * nsites]) * circuit + end + return circuit +end + +s = siteinds("Qubit", nsites) + +h = 1.3 +ℋ = ising_hamiltonian(nsites; h=h) +H = MPO(ℋ, s) +ψ0 = MPS(s, "0") + +# +# The loss function, a function of the gate parameters +# and implicitly depending on the Hamiltonian and state: +# +# loss(θ⃗) = ⟨0|U(θ⃗)† H U(θ⃗)|0⟩ = ⟨θ⃗|H|θ⃗⟩ +# +function loss(θ⃗) + nsites = length(ψ0) + s = siteinds(ψ0) + 𝒰θ⃗ = variational_circuit(nsites, nlayers, θ⃗) + Uθ⃗ = Prod{ITensor}(𝒰θ⃗, s) + ψθ⃗ = apply(Uθ⃗, ψ0; cutoff=1e-8) + return inner(ψθ⃗', H, ψθ⃗; cutoff=1e-8) +end + +Random.seed!(1234) +θ⃗₀ = 2π * rand(nsites * nlayers) + +@show loss(θ⃗₀) + +println("\nOptimize circuit with gradient optimization") + +loss_∇loss(x) = (loss(x), convert(Vector, loss'(x))) +algorithm = LBFGS(; gradtol=1e-3, verbosity=2) +θ⃗ₒₚₜ, lossₒₚₜ, ∇lossₒₚₜ, numfg, normgradhistory = optimize(loss_∇loss, θ⃗₀, algorithm) + +@show loss(θ⃗ₒₚₜ) + +println("\nRun DMRG as a comparison") + +e_dmrg, ψ_dmrg = dmrg(H, ψ0; nsweeps=5, maxdim=10) + +println("\nCompare variational circuit energy to DMRG energy") +@show loss(θ⃗ₒₚₜ), e_dmrg + +nothing diff --git a/examples/dmrg/1d_heisenberg.jl b/examples/dmrg/1d_heisenberg.jl new file mode 100644 index 0000000..4718721 --- /dev/null +++ b/examples/dmrg/1d_heisenberg.jl @@ -0,0 +1,38 @@ +using ITensors, ITensorMPS +using Printf +using Random + +Random.seed!(1234) + +let + N = 100 + + # Create N spin-one degrees of freedom + sites = siteinds("S=1", N) + # Alternatively can make spin-half sites instead + #sites = siteinds("S=1/2", N) + + # Input operator terms which define a Hamiltonian + os = OpSum() + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + end + # Convert these terms to an MPO tensor network + H = MPO(os, sites) + + # Create an initial random matrix product state + psi0 = random_mps(sites; linkdims=10) + + # Plan to do 5 DMRG sweeps: + nsweeps = 5 + # Set maximum MPS bond dimensions for each sweep + maxdim = [10, 20, 100, 100, 200] + # Set maximum truncation error allowed when adapting bond dimensions + cutoff = [1E-11] + + # Run the DMRG algorithm, returning energy and optimized MPS + energy, psi = dmrg(H, psi0; nsweeps, maxdim, cutoff) + @printf("Final energy = %.12f\n", energy) +end diff --git a/examples/dmrg/1d_heisenberg_conserve_spin.jl b/examples/dmrg/1d_heisenberg_conserve_spin.jl new file mode 100644 index 0000000..f8ec1fb --- /dev/null +++ b/examples/dmrg/1d_heisenberg_conserve_spin.jl @@ -0,0 +1,39 @@ +using ITensors, ITensorMPS +using LinearAlgebra +using Printf +using Random +using Strided + +Random.seed!(1234) +BLAS.set_num_threads(1) +Strided.set_num_threads(1) +ITensors.enable_threaded_blocksparse() +#ITensors.disable_threaded_blocksparse() + +let + N = 100 + + sites = siteinds("S=1", N; conserve_qns=true) + + os = OpSum() + for j in 1:(N - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + H = MPO(os, sites) + + state = [isodd(n) ? "Up" : "Dn" for n in 1:N] + psi0 = random_mps(sites, state; linkdims=10) + + # Plan to do 5 DMRG sweeps: + nsweeps = 5 + # Set maximum MPS bond dimensions for each sweep + maxdim = [10, 20, 100, 100, 200] + # Set maximum truncation error allowed when adapting bond dimensions + cutoff = [1E-11] + + # Run the DMRG algorithm, returning energy and optimized MPS + energy, psi = dmrg(H, psi0; nsweeps, maxdim, cutoff) + @printf("Final energy = %.12f\n", energy) +end diff --git a/examples/dmrg/1d_hubbard_extended.jl b/examples/dmrg/1d_hubbard_extended.jl new file mode 100644 index 0000000..6e10091 --- /dev/null +++ b/examples/dmrg/1d_hubbard_extended.jl @@ -0,0 +1,93 @@ +using ITensors, ITensorMPS + +# +# DMRG calculation of the extended Hubbard model +# ground state wavefunction, and spin densities +# + +let + N = 20 + Npart = 10 + t1 = 1.0 + t2 = 0.2 + U = 1.0 + V1 = 0.5 + + sites = siteinds("Electron", N; conserve_qns=true) + + os = OpSum() + for b in 1:(N - 1) + os -= t1, "Cdagup", b, "Cup", b + 1 + os -= t1, "Cdagup", b + 1, "Cup", b + os -= t1, "Cdagdn", b, "Cdn", b + 1 + os -= t1, "Cdagdn", b + 1, "Cdn", b + os += V1, "Ntot", b, "Ntot", b + 1 + end + for b in 1:(N - 2) + os -= t2, "Cdagup", b, "Cup", b + 2 + os -= t2, "Cdagup", b + 2, "Cup", b + os -= t2, "Cdagdn", b, "Cdn", b + 2 + os -= t2, "Cdagdn", b + 2, "Cdn", b + end + for i in 1:N + os += U, "Nupdn", i + end + H = MPO(os, sites) + + nsweeps = 6 + maxdim = [50, 100, 200, 400, 800, 800] + cutoff = [1E-12] + + state = ["Emp" for n in 1:N] + p = Npart + for i in N:-1:1 + if p > i + println("Doubly occupying site $i") + state[i] = "UpDn" + p -= 2 + elseif p > 0 + println("Singly occupying site $i") + state[i] = (isodd(i) ? "Up" : "Dn") + p -= 1 + end + end + # Initialize wavefunction to be bond + # dimension 10 random MPS with number + # of particles the same as `state` + psi0 = random_mps(sites, state; linkdims=10) + + # Check total number of particles: + @show flux(psi0) + + # Start DMRG calculation: + energy, psi = dmrg(H, psi0; nsweeps, maxdim, cutoff) + + upd = fill(0.0, N) + dnd = fill(0.0, N) + for j in 1:N + orthogonalize!(psi, j) + psidag_j = dag(prime(psi[j], "Site")) + upd[j] = scalar(psidag_j * op(sites, "Nup", j) * psi[j]) + dnd[j] = scalar(psidag_j * op(sites, "Ndn", j) * psi[j]) + end + + println("Up Density:") + for j in 1:N + println("$j $(upd[j])") + end + println() + + println("Dn Density:") + for j in 1:N + println("$j $(dnd[j])") + end + println() + + println("Total Density:") + for j in 1:N + println("$j $(upd[j]+dnd[j])") + end + println() + + println("\nGround State Energy = $energy") +end diff --git a/examples/dmrg/1d_ising_with_observer.jl b/examples/dmrg/1d_ising_with_observer.jl new file mode 100644 index 0000000..76194be --- /dev/null +++ b/examples/dmrg/1d_ising_with_observer.jl @@ -0,0 +1,83 @@ +# In this example we show how to pass a DMRGObserver to +# the dmrg function which allows tracking energy convergence and +# convergence of local operators. +using ITensors, ITensorMPS + +""" + Get MPO of transverse field Ising model Hamiltonian with field strength h +""" +function tfimMPO(sites, h::Float64) + # Input operator terms which define a Hamiltonian + N = length(sites) + os = OpSum() + for j in 1:(N - 1) + os -= 1, "Z", j, "Z", j + 1 + end + for j in 1:N + os += h, "X", j + end + # Convert these terms to an MPO tensor network + return MPO(os, sites) +end + +let + N = 100 + sites = siteinds("S=1/2", N) + psi0 = random_mps(sites; linkdims=10) + + # define parameters for DMRG sweeps + nsweeps = 15 + maxdim = [10, 20, 100, 100, 200] + cutoff = [1E-10] + + #= + create observer which will measure Sᶻ at each + site during the dmrg sweeps and track energies after each sweep. + in addition it will stop the computation if energy converges within + 1E-7 tolerance + =# + let + Sz_observer = DMRGObserver(["Sz"], sites; energy_tol=1E-7) + + # we will now run DMRG calculation for different values + # of the transverse field and check how local observables + # converge to their ground state values + + println("Running DMRG for TFIM with h=0.1") + println("================================") + H = tfimMPO(sites, 0.1) + energy, psi = dmrg(H, psi0; nsweeps, maxdim, cutoff, observer=Sz_observer) + + for (i, Szs) in enumerate(measurements(Sz_observer)["Sz"]) + println("<Σ Sz> after sweep $i = ", sum(Szs) / N) + end + end + + let + println("\nRunning DMRG for TFIM with h=1.0 (critical point)") + println("================================") + Sz_observer = DMRGObserver(["Sz"], sites; energy_tol=1E-7) + H = tfimMPO(sites, 1.0) + energy, psi = dmrg(H, psi0; nsweeps, maxdim, cutoff, observer=Sz_observer) + + for (i, Szs) in enumerate(measurements(Sz_observer)["Sz"]) + println("<Σ Sz> after sweep $i = ", sum(Szs) / N) + end + end + + let + println("\nRunning DMRG for TFIM with h=5.") + println("================================") + Sz_Sx_observer = DMRGObserver(["Sz", "Sx"], sites; energy_tol=1E-7) + H = tfimMPO(sites, 5.0) + energy, psi = dmrg(H, psi0; nsweeps, maxdim, cutoff, observer=Sz_Sx_observer) + + for (i, Szs) in enumerate(measurements(Sz_Sx_observer)["Sz"]) + println("<Σ Sz> after sweep $i = ", sum(Szs) / N) + end + println() + for (i, Sxs) in enumerate(measurements(Sz_Sx_observer)["Sx"]) + println("<Σ Sx> after sweep $i = ", sum(Sxs) / N) + end + end +end diff --git a/examples/dmrg/2d_heisenberg_conserve_spin.jl b/examples/dmrg/2d_heisenberg_conserve_spin.jl new file mode 100644 index 0000000..8788784 --- /dev/null +++ b/examples/dmrg/2d_heisenberg_conserve_spin.jl @@ -0,0 +1,34 @@ +using ITensors, ITensorMPS + +let + Ny = 6 + Nx = 12 + + N = Nx * Ny + + sites = siteinds("S=1/2", N; conserve_qns=true) + + lattice = square_lattice(Nx, Ny; yperiodic=false) + + os = OpSum() + for b in lattice + os += 0.5, "S+", b.s1, "S-", b.s2 + os += 0.5, "S-", b.s1, "S+", b.s2 + os += "Sz", b.s1, "Sz", b.s2 + end + H = MPO(os, sites) + + state = [isodd(n) ? "Up" : "Dn" for n in 1:N] + # Initialize wavefunction to a random MPS + # of bond-dimension 10 with same quantum + # numbers as `state` + psi0 = random_mps(sites, state; linkdims=20) + + nsweeps = 10 + maxdim = [20, 60, 100, 100, 200, 400, 800] + cutoff = [1E-8] + + energy, psi = dmrg(H, psi0; nsweeps, maxdim, cutoff) + + return nothing +end diff --git a/examples/dmrg/2d_hubbard_conserve_momentum.jl b/examples/dmrg/2d_hubbard_conserve_momentum.jl new file mode 100644 index 0000000..27c8535 --- /dev/null +++ b/examples/dmrg/2d_hubbard_conserve_momentum.jl @@ -0,0 +1,95 @@ +using ITensors, ITensorMPS +using LinearAlgebra +using Random +using Strided + +include(joinpath(@__DIR__, "..", "src", "electronk.jl")) +include(joinpath(@__DIR__, "..", "src", "hubbard.jl")) + +""" +Usage: +```julia +energy, H, psi = main(; Nx=8, Ny=4, U=4.0, t=1.0, nsweeps=10, maxdim=3000, threaded_blocksparse=false); +energy, H, psi = main(; Nx=8, Ny=4, U=4.0, t=1.0, nsweeps=10, maxdim=3000, threaded_blocksparse=true); +energy, H, psi = main(; Nx=8, Ny=4, U=4.0, t=1.0, nsweeps=10, maxdim=3000, random_init=false, threaded_blocksparse=false); +energy, H, psi = main(; Nx=8, Ny=4, U=4.0, t=1.0, nsweeps=10, maxdim=3000, random_init=false, threaded_blocksparse=true); + +using HDF5 +h5open("2d_hubbard_conserve_momentum.h5", "w") do fid + fid["energy"] = energy + fid["H"] = H + fid["psi"] = psi +end; + +energy, H, psi = h5open("2d_hubbard_conserve_momentum.h5") do fid + energy = read(fid, "energy") + H = read(fid, "H", MPO) + psi = read(fid, "psi", MPS) + return energy, H, psi +end; +``` +""" +function main(; + Nx::Int=8, + Ny::Int=4, + U::Float64=4.0, + t::Float64=1.0, + maxdim::Int=3000, + conserve_ky=true, + threaded_blocksparse=false, + nsweeps=10, + random_init=true, + seed=1234, +) + # Helps make results reproducible when comparing + # sequential vs. threaded. + itensor_rng = Xoshiro() + Random.seed!(itensor_rng, seed) + + @show Threads.nthreads() + + # Disable other threading + BLAS.set_num_threads(1) + Strided.set_num_threads(1) + + ITensors.enable_threaded_blocksparse(threaded_blocksparse) + @show ITensors.using_threaded_blocksparse() + + N = Nx * Ny + + maxdim = min.([100, 200, 400, 800, 2000, 3000, maxdim], maxdim) + cutoff = [1e-6] + noise = [1e-6, 1e-7, 1e-8, 0.0] + + sites = siteinds("ElecK", N; conserve_qns=true, conserve_ky, modulus_ky=Ny) + + os = hubbard(; Nx, Ny, t, U, ky=true) + H = MPO(os, sites) + + # Number of structural nonzero elements in a bulk + # Hamiltonian MPO tensor + @show nnz(H[end ÷ 2]) + @show nnzblocks(H[end ÷ 2]) + + # Create starting state with checkerboard + # pattern + state = map(CartesianIndices((Ny, Nx))) do I + return iseven(I[1]) ⊻ iseven(I[2]) ? "↓" : "↑" + end + display(state) + + psi0 = if random_init + random_mps(itensor_rng, sites, state; linkdims=2) + else + MPS(sites, state) + end + @time @show inner(psi0', H, psi0) + + energy, psi = @time dmrg(H, psi0; nsweeps, maxdim, cutoff, noise) + @show Nx, Ny + @show t, U + @show flux(psi) + @show maxlinkdim(psi) + @show energy + return energy, H, psi +end diff --git a/examples/dmrg/2d_hubbard_conserve_particles.jl b/examples/dmrg/2d_hubbard_conserve_particles.jl new file mode 100644 index 0000000..2c1b400 --- /dev/null +++ b/examples/dmrg/2d_hubbard_conserve_particles.jl @@ -0,0 +1,44 @@ +using ITensors, ITensorMPS + +function main(; Nx=6, Ny=3, U=4.0, t=1.0) + N = Nx * Ny + + nsweeps = 10 + maxdim = [100, 200, 400, 800, 1600] + cutoff = [1E-6] + noise = [1E-6, 1E-7, 1E-8, 0.0] + + sites = siteinds("Electron", N; conserve_qns=true) + + lattice = square_lattice(Nx, Ny; yperiodic=true) + + os = OpSum() + for b in lattice + os -= t, "Cdagup", b.s1, "Cup", b.s2 + os -= t, "Cdagup", b.s2, "Cup", b.s1 + os -= t, "Cdagdn", b.s1, "Cdn", b.s2 + os -= t, "Cdagdn", b.s2, "Cdn", b.s1 + end + for n in 1:N + os += U, "Nupdn", n + end + H = MPO(os, sites) + + # Half filling + state = [isodd(n) ? "Up" : "Dn" for n in 1:N] + + # Initialize wavefunction to a random MPS + # of bond-dimension 10 with same quantum + # numbers as `state` + psi0 = random_mps(sites, state) + + energy, psi = dmrg(H, psi0; nsweeps, maxdim, cutoff, noise) + @show t, U + @show flux(psi) + @show maxlinkdim(psi) + @show energy + + return nothing +end + +main() diff --git a/examples/dmrg/Project.toml b/examples/dmrg/Project.toml new file mode 100644 index 0000000..e6b9177 --- /dev/null +++ b/examples/dmrg/Project.toml @@ -0,0 +1,6 @@ +[deps] +ITensorMPS = "0d1a4710-d33b-49a5-8f18-73bdf49b47e2" +ITensors = "9136182c-28ba-11e9-034c-db9fb085ebd5" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Strided = "5e0ebb24-38b0-5f93-81fe-25c709ecae67" diff --git a/examples/dmrg/threaded_blocksparse/2d_hubbard_conserve_momentum.jl b/examples/dmrg/threaded_blocksparse/2d_hubbard_conserve_momentum.jl new file mode 100644 index 0000000..9905e96 --- /dev/null +++ b/examples/dmrg/threaded_blocksparse/2d_hubbard_conserve_momentum.jl @@ -0,0 +1,94 @@ +using ITensors, ITensorMPS +using LinearAlgebra +using Random + +include(joinpath(@__DIR__, "..", "..", "src", "electronk.jl")) +include(joinpath(@__DIR__, "..", "..", "src", "hubbard.jl")) + +function main(; + Nx::Int=6, + Ny::Int=3, + U::Float64=4.0, + t::Float64=1.0, + maxdim::Int=3000, + conserve_ky=true, + nsweeps=10, + blas_num_threads=1, + strided_num_threads=1, + threaded_blocksparse=false, + outputlevel=1, + seed=1234, +) + Random.seed!(seed) + ITensors.Strided.set_num_threads(strided_num_threads) + BLAS.set_num_threads(blas_num_threads) + ITensors.enable_threaded_blocksparse(threaded_blocksparse) + + if outputlevel > 0 + @show Threads.nthreads() + @show Sys.CPU_THREADS + @show BLAS.get_num_threads() + @show ITensors.Strided.get_num_threads() + @show ITensors.using_threaded_blocksparse() + println() + end + + N = Nx * Ny + + maxdim = min.([100, 200, 400, 800, 2000, 3000, maxdim], maxdim) + cutoff = [1E-6] + noise = [1E-6, 1E-7, 1E-8, 0.0] + + sites = siteinds("ElecK", N; conserve_qns=true, conserve_ky, modulus_ky=Ny) + + os = hubbard(; Nx, Ny, t, U, ky=true) + H = MPO(os, sites) + + # Number of structural nonzero elements in a bulk + # Hamiltonian MPO tensor + if outputlevel > 0 + @show nnz(H[end ÷ 2]) + @show nnzblocks(H[end ÷ 2]) + end + + # Create starting state with checkerboard + # pattern + state = map(CartesianIndices((Ny, Nx))) do I + return iseven(I[1]) ⊻ iseven(I[2]) ? "↓" : "↑" + end + display(state) + + psi0 = random_mps(sites, state; linkdims=10) + + energy, psi = @time dmrg(H, psi0; nsweeps, maxdim, cutoff, noise, outputlevel) + + if outputlevel > 0 + @show Nx, Ny + @show t, U + @show flux(psi) + @show maxlinkdim(psi) + @show energy + end + return nothing +end + +println("################################") +println("Compilation") +println("################################") +println("Without threaded block sparse:\n") +main(; nsweeps=2, threaded_blocksparse=false, outputlevel=0) +println() +println("With threaded block sparse:\n") +main(; nsweeps=2, threaded_blocksparse=true, outputlevel=0) +println() + +println("################################") +println("Runtime") +println("################################") +println() +println("Without threaded block sparse:\n") +main(; nsweeps=10, threaded_blocksparse=false) +println() +println("With threaded block sparse:\n") +main(; nsweeps=10, threaded_blocksparse=true) +println() diff --git a/examples/dmrg/threaded_blocksparse/Project.toml b/examples/dmrg/threaded_blocksparse/Project.toml new file mode 100644 index 0000000..d163dd0 --- /dev/null +++ b/examples/dmrg/threaded_blocksparse/Project.toml @@ -0,0 +1,4 @@ +[deps] +ITensors = "9136182c-28ba-11e9-034c-db9fb085ebd5" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" diff --git a/examples/dmrg/write_to_disk/1d_heisenberg.jl b/examples/dmrg/write_to_disk/1d_heisenberg.jl new file mode 100644 index 0000000..6391e86 --- /dev/null +++ b/examples/dmrg/write_to_disk/1d_heisenberg.jl @@ -0,0 +1,36 @@ +using ITensors, ITensorMPS +using LinearAlgebra +using Printf +using Random +using Strided + +Random.seed!(1234) +Strided.set_num_threads(1) + +let + N = 100 + + sites = siteinds("S=1", N; conserve_qns=true) + + os = OpSum() + for j in 1:(N - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + H = MPO(os, sites) + + state = [isodd(n) ? "Up" : "Dn" for n in 1:N] + psi0 = random_mps(sites, state; linkdims=10) + + # Plan to do 5 DMRG sweeps: + nsweeps = 5 + # Set maximum MPS bond dimensions for each sweep + maxdim = [10, 20, 100, 100, 200] + # Set maximum truncation error allowed when adapting bond dimensions + cutoff = 1E-10 + + # Run the DMRG algorithm, returning energy and optimized MPS + energy, psi = dmrg(H, psi0; nsweeps, cutoff, maxdim, write_when_maxdim_exceeds=25) + @printf("Final energy = %.12f\n", energy) +end diff --git a/examples/dmrg/write_to_disk/Project.toml b/examples/dmrg/write_to_disk/Project.toml new file mode 100644 index 0000000..abb011c --- /dev/null +++ b/examples/dmrg/write_to_disk/Project.toml @@ -0,0 +1,7 @@ +[deps] +ITensorMPS = "0d1a4710-d33b-49a5-8f18-73bdf49b47e2" +ITensors = "9136182c-28ba-11e9-034c-db9fb085ebd5" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Strided = "5e0ebb24-38b0-5f93-81fe-25c709ecae67" diff --git a/examples/exact_diagonalization/Project.toml b/examples/exact_diagonalization/Project.toml new file mode 100644 index 0000000..226adde --- /dev/null +++ b/examples/exact_diagonalization/Project.toml @@ -0,0 +1,7 @@ +[deps] +ITensorMPS = "0d1a4710-d33b-49a5-8f18-73bdf49b47e2" +ITensors = "9136182c-28ba-11e9-034c-db9fb085ebd5" +KrylovKit = "0b1a1467-8014-51b9-945f-bf0ae24f4b77" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MKL = "33e6dc65-8f57-5167-99aa-e5a354878fb2" +Strided = "5e0ebb24-38b0-5f93-81fe-25c709ecae67" diff --git a/examples/exact_diagonalization/exact_diagonalization.jl b/examples/exact_diagonalization/exact_diagonalization.jl new file mode 100644 index 0000000..2016ab8 --- /dev/null +++ b/examples/exact_diagonalization/exact_diagonalization.jl @@ -0,0 +1,64 @@ +using ITensors, ITensorMPS +using KrylovKit +using LinearAlgebra +using MKL +using Strided + +include("fuse_inds.jl") + +Strided.disable_threads() +ITensors.disable_threaded_blocksparse() + +function heisenberg(n) + os = OpSum() + for j in 1:(n - 1) + os += 1 / 2, "S+", j, "S-", j + 1 + os += 1 / 2, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + return os +end + +function main(n; blas_num_threads=Sys.CPU_THREADS, fuse=true, binary=true) + if n > 16 + @warn "System size of $n is likely too large for exact diagonalization." + end + + BLAS.set_num_threads(blas_num_threads) + + # Hilbert space + s = siteinds("S=1/2", n; conserve_qns=true) + H = MPO(heisenberg(n), s) + initstate(j) = isodd(j) ? "↑" : "↓" + ψ0 = random_mps(s, initstate; linkdims=10) + + edmrg, ψdmrg = dmrg(H, ψ0; nsweeps=10, cutoff=1e-6) + + if fuse + if binary + println("Fuse the indices using a binary tree") + T = fusion_tree_binary(s) + H_full = @time fuse_inds_binary(H, T) + ψ0_full = @time fuse_inds_binary(ψ0, T) + else + println("Fuse the indices using an unbalances tree") + T = fusion_tree(s) + H_full = @time fuse_inds(H, T) + ψ0_full = @time fuse_inds(ψ0, T) + end + else + println("Don't fuse the indices") + @disable_warn_order begin + H_full = @time contract(H) + ψ0_full = @time contract(ψ0) + end + end + + vals, vecs, info = @time eigsolve( + H_full, ψ0_full, 1, :SR; ishermitian=true, tol=1e-6, krylovdim=30, eager=true + ) + + @show edmrg, vals[1] +end + +main(14) diff --git a/examples/exact_diagonalization/fuse_inds.jl b/examples/exact_diagonalization/fuse_inds.jl new file mode 100644 index 0000000..82f90e9 --- /dev/null +++ b/examples/exact_diagonalization/fuse_inds.jl @@ -0,0 +1,91 @@ +using ITensors, ITensorMPS + +function fusion_tree(s::Vector{<:Index}) + n = length(s) + Cs = Vector{ITensor}(undef, n - 1) + cj = s[1] + for j in 1:(n - 1) + fuse_inds = (cj, s[j + 1]) + Cj = combiner(fuse_inds...) + Cs[j] = Cj + cj = uniqueind(Cj, fuse_inds) + end + return Cs +end + +function fuse_inds(A::MPS, fusion_tree::Vector{ITensor}) + n = length(A) + A_fused = A[1] + for j in 2:n + A_fused = A_fused * A[j] * fusion_tree[j - 1] + end + return A_fused +end + +function fuse_inds(A::MPO, fusion_tree::Vector{ITensor}) + n = length(A) + A_fused = A[1] + for j in 2:n + A_fused = A_fused * A[j] * dag(fusion_tree[j - 1]) * fusion_tree[j - 1]' + end + return A_fused +end + +function fusion_tree_binary_layer(s::Vector{IndexT}; layer=1) where {IndexT<:Index} + n = length(s) + Cs = ITensor[] + cs = IndexT[] + for j in 1:2:(n - 1) + fuse_inds = (s[j], s[j + 1]) + Cj = combiner(fuse_inds...; tags="n=$(j)⊗$(j + 1),l=$(layer)") + push!(Cs, Cj) + cj = uniqueind(Cj, fuse_inds) + push!(cs, cj) + end + if isodd(n) + push!(cs, last(s)) + end + return Cs, cs +end + +function fusion_tree_binary(s::Vector{<:Index}; depth=ceil(Int, log2(length(s)))) + Cs = Vector{ITensor}[] + c_layer = s + for layer in 1:depth + C_layer, c_layer = fusion_tree_binary_layer(c_layer; layer) + push!(Cs, C_layer) + end + return Cs +end + +function fuse_tensors(A::MPS, fusion_tree_layer::Vector{ITensor}, j::Int) + return A[j] * A[j + 1] * fusion_tree_layer[(j + 1) ÷ 2] +end + +function fuse_tensors(A::MPO, fusion_tree_layer::Vector{ITensor}, j::Int) + return A[j] * + A[j + 1] * + dag(fusion_tree_layer[(j + 1) ÷ 2]) * + fusion_tree_layer[(j + 1) ÷ 2]' +end + +function fuse_inds_binary_layer(A::Union{MPS,MPO}, fusion_tree_layer::Vector{ITensor}) + n = length(fusion_tree_layer) + A_fused = ITensor[] + for j in 1:2:(2n) + push!(A_fused, fuse_tensors(A, fusion_tree_layer, j)) + end + if isodd(length(A)) + push!(A_fused, A[end]) + end + return typeof(A)(A_fused) +end + +function fuse_inds_binary(A::Union{MPS,MPO}, fusion_tree::Vector{Vector{ITensor}}) + depth = length(fusion_tree) + A_fused = A + for layer in 1:depth + A_fused = fuse_inds_binary_layer(A_fused, fusion_tree[layer]) + end + return only(A_fused) +end diff --git a/examples/finite_temperature/metts.jl b/examples/finite_temperature/metts.jl new file mode 100644 index 0000000..355fcc3 --- /dev/null +++ b/examples/finite_temperature/metts.jl @@ -0,0 +1,115 @@ +using ITensors, ITensorMPS +using Printf + +#= + +This example code implements the minimally entangled typical thermal state (METTS). +For more information on METTS, see the following references: +- "Minimally entangled typical quantum states at finite temperature", Steven R. White, + Phys. Rev. Lett. 102, 190601 (2009) + and arxiv:0902.4475 (https://arxiv.org/abs/0902.4475) +- "Minimally entangled typical thermal state algorithms", E M Stoudenmire and Steven R White, + New Journal of Physics 12, 055026 (2010) https://doi.org/10.1088/1367-2630/12/5/055026 + +=# + +function ITensors.op(::OpName"expτSS", ::SiteType"S=1/2", s1::Index, s2::Index; τ) + h = + 1 / 2 * op("S+", s1) * op("S-", s2) + + 1 / 2 * op("S-", s1) * op("S+", s2) + + op("Sz", s1) * op("Sz", s2) + return exp(τ * h) +end + +""" +Given a Vector of numbers, returns +the average and the standard error +(= the width of distribution of the numbers) +""" +function avg_err(v::Vector) + N = length(v) + avg = v[1] / N + avg2 = v[1]^2 / N + for j in 2:N + avg += v[j] / N + avg2 += v[j]^2 / N + end + return avg, √((avg2 - avg^2) / N) +end + +function main(; N=10, cutoff=1E-8, δτ=0.1, beta=2.0, NMETTS=3000, Nwarm=10) + + # Make an array of 'site' indices + s = siteinds("S=1/2", N) + + # Make gates (1,2),(2,3),(3,4),... + gates = ops([("expτSS", (n, n + 1), (τ=-δτ / 2,)) for n in 1:(N - 1)], s) + # Include gates in reverse order to complete Trotter formula + append!(gates, reverse(gates)) + + # Make y-rotation gates to use in METTS collapses + Ry_gates = ops([("Ry", n, (θ=π / 2,)) for n in 1:N], s) + + # Arbitrary initial state + psi = random_mps(s) + + # Make H for measuring the energy + terms = OpSum() + for j in 1:(N - 1) + terms += 1 / 2, "S+", j, "S-", j + 1 + terms += 1 / 2, "S-", j, "S+", j + 1 + terms += "Sz", j, "Sz", j + 1 + end + H = MPO(terms, s) + + # Make τ_range and check δτ is commensurate + τ_range = δτ:δτ:(beta / 2) + if norm(length(τ_range) * δτ - beta / 2) > 1E-10 + error("Time step δτ=$δτ not commensurate with beta/2=$(beta/2)") + end + + energies = Float64[] + + for step in 1:(Nwarm + NMETTS) + if step <= Nwarm + println("Making warmup METTS number $step") + else + println("Making METTS number $(step-Nwarm)") + end + + # Do the time evolution by applying the gates + for τ in τ_range + psi = apply(gates, psi; cutoff) + normalize!(psi) + end + + # Measure properties after >= Nwarm + # METTS have been made + if step > Nwarm + energy = inner(psi', H, psi) + push!(energies, energy) + @printf(" Energy of METTS %d = %.4f\n", step - Nwarm, energy) + a_E, err_E = avg_err(energies) + @printf( + " Estimated Energy = %.4f +- %.4f [%.4f,%.4f]\n", + a_E, + err_E, + a_E - err_E, + a_E + err_E + ) + end + + # Measure in X or Z basis on alternating steps + if step % 2 == 1 + psi = apply(Ry_gates, psi) + samp = sample!(psi) + new_state = [samp[j] == 1 ? "X+" : "X-" for j in 1:N] + else + samp = sample!(psi) + new_state = [samp[j] == 1 ? "Z+" : "Z-" for j in 1:N] + end + psi = MPS(s, new_state) + end + + return nothing +end diff --git a/examples/finite_temperature/purification.jl b/examples/finite_temperature/purification.jl new file mode 100644 index 0000000..fd374e4 --- /dev/null +++ b/examples/finite_temperature/purification.jl @@ -0,0 +1,56 @@ +using ITensors, ITensorMPS +using Printf + +#= + +This example code implements the purification or "ancilla" method for +finite temperature quantum systems. + +For more information see the following references: +- "Finite-temperature density matrix renormalization using an enlarged Hilbert space", + Adrian E. Feiguin and Steven R. White, Phys. Rev. B 72, 220401(R) + and arxiv:cond-mat/0510124 (https://arxiv.org/abs/cond-mat/0510124) + +=# + +function ITensors.op(::OpName"expτSS", ::SiteType"S=1/2", s1::Index, s2::Index; τ) + h = + 1 / 2 * op("S+", s1) * op("S-", s2) + + 1 / 2 * op("S-", s1) * op("S+", s2) + + op("Sz", s1) * op("Sz", s2) + return exp(τ * h) +end + +function main(; N=10, cutoff=1E-8, δτ=0.1, beta_max=2.0) + + # Make an array of 'site' indices + s = siteinds("S=1/2", N; conserve_qns=true) + + # Make gates (1,2),(2,3),(3,4),... + gates = ops([("expτSS", (n, n + 1), (τ=-δτ / 2,)) for n in 1:(N - 1)], s) + # Include gates in reverse order to complete Trotter formula + append!(gates, reverse(gates)) + + # Initial state is infinite-temperature mixed state + rho = MPO(s, "Id") ./ √2 + + # Make H for measuring the energy + terms = OpSum() + for j in 1:(N - 1) + terms += 1 / 2, "S+", j, "S-", j + 1 + terms += 1 / 2, "S-", j, "S+", j + 1 + terms += "Sz", j, "Sz", j + 1 + end + H = MPO(terms, s) + + # Do the time evolution by applying the gates + # for Nsteps steps + for β in 0:δτ:beta_max + energy = inner(rho, H) + @printf("β = %.2f energy = %.8f\n", β, energy) + rho = apply(gates, rho; cutoff) + rho = rho / tr(rho) + end + + return nothing +end diff --git a/examples/gate_evolution/mpo_gate_evolution.jl b/examples/gate_evolution/mpo_gate_evolution.jl new file mode 100644 index 0000000..b64f170 --- /dev/null +++ b/examples/gate_evolution/mpo_gate_evolution.jl @@ -0,0 +1,65 @@ +using ITensors, ITensorMPS + +function ITensors.op(::OpName"expτSS", ::SiteType"S=1/2", s1::Index, s2::Index; τ) + h = + 1 / 2 * op("S+", s1) * op("S-", s2) + + 1 / 2 * op("S-", s1) * op("S+", s2) + + op("Sz", s1) * op("Sz", s2) + return exp(τ * h) +end + +function main(; N=10, cutoff=1E-8, δt=0.1, ttotal=5.0) + # Compute the number of steps to do + Nsteps = Int(ttotal / δt) + + # Make an array of 'site' indices + s = siteinds("S=1/2", N; conserve_qns=true) + + # Make gates (1,2),(2,3),(3,4),... + gates = ops([("expτSS", (n, n + 1), (τ=-δt * im / 2,)) for n in 1:(N - 1)], s) + + # Include gates in reverse order too + # (N,N-1),(N-1,N-2),... + append!(gates, reverse(gates)) + + # Function that measures on site n + function measure_Sz(psi::MPS, n) + psi = orthogonalize(psi, n) + sn = siteind(psi, n) + Sz = scalar(dag(prime(psi[n], "Site")) * op("Sz", sn) * psi[n]) + return real(Sz) + end + + # Initialize psi to be a product state (alternating up and down) + psi0 = MPS(s, n -> isodd(n) ? "Up" : "Dn") + + c = div(N, 2) + + # Compute and print initial value + t = 0.0 + Sz = measure_Sz(psi0, c) + println("$t $Sz") + + # Do the time evolution by applying the gates + # for Nsteps steps + psi = psi0 + for step in 1:Nsteps + psi = apply(gates, psi; cutoff) + t += δt + Sz = measure_Sz(psi, c) + println("$t $Sz") + end + + # Now do the same evolution with an MPO + rho0 = MPO(psi0) + rho = rho0 + for step in 1:Nsteps + rho = apply(gates, rho; cutoff, apply_dag=true) + t += δt + end + @show inner(psi, rho, psi) + @show inner(psi, psi) + @show tr(rho) + + return nothing +end diff --git a/examples/gate_evolution/quantum_simulator.jl b/examples/gate_evolution/quantum_simulator.jl new file mode 100644 index 0000000..ca4ddc3 --- /dev/null +++ b/examples/gate_evolution/quantum_simulator.jl @@ -0,0 +1,26 @@ +using ITensors, ITensorMPS + +N = 10 +s = siteinds("Qubit", N) + +# Make all of the operators +X = ops(s, [("X", n) for n in 1:N]) +H = ops(s, [("H", n) for n in 1:N]) +CX = ops(s, [("CX", n, m) for n in 1:N, m in 1:N]) + +# Start with the state |0000...⟩ +ψ0 = MPS(s, "0") + +# Change to the state |1010...⟩ +gates = [X[n] for n in 1:2:N] +ψ = apply(gates, ψ0; cutoff=1e-15) +@assert inner(ψ, MPS(s, n -> isodd(n) ? "1" : "0")) ≈ 1 + +# Change to the state |10111011...⟩ +append!(gates, [CX[n, n + 3] for n in 1:4:(N - 3)]) +ψ = apply(gates, ψ0; cutoff=1e-15) +@assert inner(ψ, MPS(s, ["1", "0", "1", "1", "1", "0", "1", "1", "1", "0"])) ≈ 1 + +# Change the state |10111011...⟩ to the (|+⟩, |-⟩) basis +append!(gates, [H[n] for n in 1:N]) +ψ = apply(gates, ψ0; cutoff=1e-15) diff --git a/examples/gate_evolution/trotter_suzuki_decomposition.jl b/examples/gate_evolution/trotter_suzuki_decomposition.jl new file mode 100644 index 0000000..6222470 --- /dev/null +++ b/examples/gate_evolution/trotter_suzuki_decomposition.jl @@ -0,0 +1,32 @@ +using ITensors, ITensorMPS + +function heisenberg(N) + os = Sum{Op}() + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + end + return os +end + +function main(N; nsteps, order) + ℋ = heisenberg(N) + s = siteinds("S=1/2", N) + ψ₀ = MPS(s, n -> isodd(n) ? "↑" : "↓") + t = 1.0 + 𝒰 = exp(im * t * ℋ; alg=Trotter{order}(nsteps)) + U = Prod{ITensor}(𝒰, s) + H = ITensor(ℋ, s) + 𝒰ʳᵉᶠ = exp(im * t * ℋ) + Uʳᵉᶠ = ITensor(𝒰ʳᵉᶠ, s) + Uʳᵉᶠψ₀ = replaceprime(Uʳᵉᶠ * prod(ψ₀), 1 => 0) + return norm(prod(U(ψ₀)) - Uʳᵉᶠψ₀) +end + +@show main(4; nsteps=10, order=1) +@show main(4; nsteps=10, order=2) +@show main(4; nsteps=10, order=4) +@show main(4; nsteps=100, order=1) +@show main(4; nsteps=100, order=2) +@show main(4; nsteps=100, order=4) diff --git a/examples/mps_mpo_algebra/mps_density_matrix.jl b/examples/mps_mpo_algebra/mps_density_matrix.jl new file mode 100644 index 0000000..49abf39 --- /dev/null +++ b/examples/mps_mpo_algebra/mps_density_matrix.jl @@ -0,0 +1,14 @@ +using ITensors, ITensorMPS + +N = 4 +nmps = 3 +cutoff = 1e-8 +s = siteinds("S=1/2", N) +ψs = [random_mps(s; linkdims=2) for _ in 1:nmps] +ρs = [outer(ψ, ψ; cutoff) for ψ in ψs] +ρ = sum(ρs; cutoff) + +ψs_full = prod.(ψs) +ρs_full = [ψ'dag(ψ) for ψ in ψs_full] +ρ_full = sum(ρs_full) +@show norm(ρ_full - prod(ρ)) diff --git a/examples/01_tdvp.jl b/examples/solvers/01_tdvp.jl similarity index 100% rename from examples/01_tdvp.jl rename to examples/solvers/01_tdvp.jl diff --git a/examples/02_dmrg-x.jl b/examples/solvers/02_dmrg-x.jl similarity index 100% rename from examples/02_dmrg-x.jl rename to examples/solvers/02_dmrg-x.jl diff --git a/examples/03_models.jl b/examples/solvers/03_models.jl similarity index 100% rename from examples/03_models.jl rename to examples/solvers/03_models.jl diff --git a/examples/03_tdvp_time_dependent.jl b/examples/solvers/03_tdvp_time_dependent.jl similarity index 100% rename from examples/03_tdvp_time_dependent.jl rename to examples/solvers/03_tdvp_time_dependent.jl diff --git a/examples/03_updaters.jl b/examples/solvers/03_updaters.jl similarity index 100% rename from examples/03_updaters.jl rename to examples/solvers/03_updaters.jl diff --git a/examples/04_tdvp_observers.jl b/examples/solvers/04_tdvp_observers.jl similarity index 100% rename from examples/04_tdvp_observers.jl rename to examples/solvers/04_tdvp_observers.jl diff --git a/examples/Project.toml b/examples/solvers/Project.toml similarity index 92% rename from examples/Project.toml rename to examples/solvers/Project.toml index 4d3b745..ae4dddd 100644 --- a/examples/Project.toml +++ b/examples/solvers/Project.toml @@ -6,6 +6,3 @@ KrylovKit = "0b1a1467-8014-51b9-945f-bf0ae24f4b77" Observers = "338f10d5-c7f1-4033-a7d1-f9dec39bcaa0" OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" - -[compat] -ITensors = "0.6.7" diff --git a/examples/src/electronk.jl b/examples/src/electronk.jl new file mode 100644 index 0000000..22a73f1 --- /dev/null +++ b/examples/src/electronk.jl @@ -0,0 +1,198 @@ +using ITensors, ITensorMPS + +function ITensors.space( + ::SiteType"ElecK", + n::Int; + conserve_qns=false, + conserve_sz=conserve_qns, + conserve_nf=conserve_qns, + conserve_nfparity=conserve_qns, + conserve_ky=false, + qnname_sz="Sz", + qnname_nf="Nf", + qnname_nfparity="NfParity", + qnname_ky="Ky", + modulus_ky=nothing, + # Deprecated + conserve_parity=nothing, +) + if !isnothing(conserve_parity) + conserve_nfparity = conserve_parity + end + if conserve_ky && conserve_sz && conserve_nf + mod = (n - 1) % modulus_ky + mod2 = (2 * mod) % modulus_ky + return [ + QN((qnname_nf, 0, -1), (qnname_sz, 0), (qnname_ky, 0, modulus_ky)) => 1 + QN((qnname_nf, 1, -1), (qnname_sz, 1), (qnname_ky, mod, modulus_ky)) => 1 + QN((qnname_nf, 1, -1), (qnname_sz, -1), (qnname_ky, mod, modulus_ky)) => 1 + QN((qnname_nf, 2, -1), (qnname_sz, 0), (qnname_ky, mod2, modulus_ky)) => 1 + ] + elseif conserve_ky + error("Cannot conserve ky without conserving sz and nf") + elseif conserve_sz && conserve_nf + return [ + QN((qnname_nf, 0, -1), (qnname_sz, 0)) => 1 + QN((qnname_nf, 1, -1), (qnname_sz, +1)) => 1 + QN((qnname_nf, 1, -1), (qnname_sz, -1)) => 1 + QN((qnname_nf, 2, -1), (qnname_sz, 0)) => 1 + ] + elseif conserve_nf + return [ + QN(qnname_nf, 0, -1) => 1 + QN(qnname_nf, 1, -1) => 2 + QN(qnname_nf, 2, -1) => 1 + ] + elseif conserve_sz + return [ + QN((qnname_sz, 0), (qnname_nfparity, 0, -2)) => 1 + QN((qnname_sz, +1), (qnname_nfparity, 1, -2)) => 1 + QN((qnname_sz, -1), (qnname_nfparity, 1, -2)) => 1 + QN((qnname_sz, 0), (qnname_nfparity, 0, -2)) => 1 + ] + elseif conserve_nfparity + return [ + QN(qnname_nfparity, 0, -2) => 1 + QN(qnname_nfparity, 1, -2) => 2 + QN(qnname_nfparity, 0, -2) => 1 + ] + end + return 4 +end + +ITensors.state(::StateName"Emp", ::SiteType"ElecK") = [1.0 0.0 0.0 0.0] +ITensors.state(::StateName"Up", ::SiteType"ElecK") = [0.0 1.0 0.0 0.0] +ITensors.state(::StateName"Dn", ::SiteType"ElecK") = [0.0 0.0 1.0 0.0] +ITensors.state(::StateName"UpDn", ::SiteType"ElecK") = [0.0 0.0 0.0 1.0] +ITensors.state(::StateName"0", st::SiteType"ElecK") = state(StateName("Emp"), st) +ITensors.state(::StateName"↑", st::SiteType"ElecK") = state(StateName("Up"), st) +ITensors.state(::StateName"↓", st::SiteType"ElecK") = state(StateName("Dn"), st) +ITensors.state(::StateName"↑↓", st::SiteType"ElecK") = state(StateName("UpDn"), st) + +function ITensors.op!(Op::ITensor, ::OpName"Nup", ::SiteType"ElecK", s::Index) + Op[s' => 2, s => 2] = 1.0 + return Op[s' => 4, s => 4] = 1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Ndn", ::SiteType"ElecK", s::Index) + Op[s' => 3, s => 3] = 1.0 + return Op[s' => 4, s => 4] = 1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Nupdn", ::SiteType"ElecK", s::Index) + return Op[s' => 4, s => 4] = 1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Ntot", ::SiteType"ElecK", s::Index) + Op[s' => 2, s => 2] = 1.0 + Op[s' => 3, s => 3] = 1.0 + return Op[s' => 4, s => 4] = 2.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Cup", ::SiteType"ElecK", s::Index) + Op[s' => 1, s => 2] = 1.0 + return Op[s' => 3, s => 4] = 1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Cdagup", ::SiteType"ElecK", s::Index) + Op[s' => 2, s => 1] = 1.0 + return Op[s' => 4, s => 3] = 1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Cdn", ::SiteType"ElecK", s::Index) + Op[s' => 1, s => 3] = 1.0 + return Op[s' => 2, s => 4] = -1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Cdagdn", ::SiteType"ElecK", s::Index) + Op[s' => 3, s => 1] = 1.0 + return Op[s' => 4, s => 2] = -1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Aup", ::SiteType"ElecK", s::Index) + Op[s' => 1, s => 2] = 1.0 + return Op[s' => 3, s => 4] = 1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Adagup", ::SiteType"ElecK", s::Index) + Op[s' => 2, s => 1] = 1.0 + return Op[s' => 4, s => 3] = 1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Adn", ::SiteType"ElecK", s::Index) + Op[s' => 1, s => 3] = 1.0 + return Op[s' => 2, s => 4] = 1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Adagdn", ::SiteType"ElecK", s::Index) + Op[s' => 3, s => 1] = 1.0 + return Op[s' => 2, s => 4] = 1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"F", ::SiteType"ElecK", s::Index) + Op[s' => 1, s => 1] = +1.0 + Op[s' => 2, s => 2] = -1.0 + Op[s' => 3, s => 3] = -1.0 + return Op[s' => 4, s => 4] = +1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Fup", ::SiteType"ElecK", s::Index) + Op[s' => 1, s => 1] = +1.0 + Op[s' => 2, s => 2] = -1.0 + Op[s' => 3, s => 3] = +1.0 + return Op[s' => 4, s => 4] = -1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Fdn", ::SiteType"ElecK", s::Index) + Op[s' => 1, s => 1] = +1.0 + Op[s' => 2, s => 2] = +1.0 + Op[s' => 3, s => 3] = -1.0 + return Op[s' => 4, s => 4] = -1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"Sz", ::SiteType"ElecK", s::Index) + Op[s' => 2, s => 2] = +0.5 + return Op[s' => 3, s => 3] = -0.5 +end + +function ITensors.op!(Op::ITensor, ::OpName"Sᶻ", st::SiteType"ElecK", s::Index) + return op!(Op, OpName("Sz"), st, s) +end + +function ITensors.op!(Op::ITensor, ::OpName"Sx", ::SiteType"ElecK", s::Index) + Op[s' => 2, s => 3] = 0.5 + return Op[s' => 3, s => 2] = 0.5 +end + +function ITensors.op!(Op::ITensor, ::OpName"Sˣ", st::SiteType"ElecK", s::Index) + return op!(Op, OpName("Sx"), st, s) +end + +function ITensors.op!(Op::ITensor, ::OpName"S+", ::SiteType"ElecK", s::Index) + return Op[s' => 2, s => 3] = 1.0 +end + +op!(Op::ITensor, ::OpName"S⁺", st::SiteType"ElecK", s::Index) = op!(Op, OpName("S+"), st, s) +op!(Op::ITensor, ::OpName"Sp", st::SiteType"ElecK", s::Index) = op!(Op, OpName("S+"), st, s) +function op!(Op::ITensor, ::OpName"Splus", st::SiteType"ElecK", s::Index) + return op!(Op, OpName("S+"), st, s) +end + +function op!(Op::ITensor, ::OpName"S-", ::SiteType"ElecK", s::Index) + return Op[s' => 3, s => 2] = 1.0 +end + +function ITensors.op!(Op::ITensor, ::OpName"S⁻", st::SiteType"ElecK", s::Index) + return op!(Op, OpName("S-"), st, s) +end +function ITensors.op!(Op::ITensor, ::OpName"Sm", st::SiteType"ElecK", s::Index) + return op!(Op, OpName("S-"), st, s) +end +function ITensors.op!(Op::ITensor, ::OpName"Sminus", st::SiteType"ElecK", s::Index) + return op!(Op, OpName("S-"), st, s) +end + +ITensors.has_fermion_string(::OpName"Cup", ::SiteType"ElecK") = true +ITensors.has_fermion_string(::OpName"Cdagup", ::SiteType"ElecK") = true +ITensors.has_fermion_string(::OpName"Cdn", ::SiteType"ElecK") = true +ITensors.has_fermion_string(::OpName"Cdagdn", ::SiteType"ElecK") = true diff --git a/examples/src/hubbard.jl b/examples/src/hubbard.jl new file mode 100644 index 0000000..30bf2ef --- /dev/null +++ b/examples/src/hubbard.jl @@ -0,0 +1,84 @@ + +function hubbard_1d(; N::Int, t=1.0, U=0.0) + opsum = OpSum() + for b in 1:(N - 1) + opsum -= t, "Cdagup", b, "Cup", b + 1 + opsum -= t, "Cdagup", b + 1, "Cup", b + opsum -= t, "Cdagdn", b, "Cdn", b + 1 + opsum -= t, "Cdagdn", b + 1, "Cdn", b + end + if U ≠ 0 + for n in 1:N + opsum += U, "Nupdn", n + end + end + return opsum +end + +function hubbard_2d(; Nx::Int, Ny::Int, t=1.0, U=0.0, yperiodic::Bool=true) + N = Nx * Ny + lattice = square_lattice(Nx, Ny; yperiodic=yperiodic) + opsum = OpSum() + for b in lattice + opsum -= t, "Cdagup", b.s1, "Cup", b.s2 + opsum -= t, "Cdagup", b.s2, "Cup", b.s1 + opsum -= t, "Cdagdn", b.s1, "Cdn", b.s2 + opsum -= t, "Cdagdn", b.s2, "Cdn", b.s1 + end + if U ≠ 0 + for n in 1:N + opsum += U, "Nupdn", n + end + end + return opsum +end + +function hubbard_2d_ky(; Nx::Int, Ny::Int, t=1.0, U=0.0) + opsum = OpSum() + for x in 0:(Nx - 1) + for ky in 0:(Ny - 1) + s = x * Ny + ky + 1 + disp = -2 * t * cos((2 * π / Ny) * ky) + if abs(disp) > 1e-12 + opsum += disp, "Nup", s + opsum += disp, "Ndn", s + end + end + end + for x in 0:(Nx - 2) + for ky in 0:(Ny - 1) + s1 = x * Ny + ky + 1 + s2 = (x + 1) * Ny + ky + 1 + opsum -= t, "Cdagup", s1, "Cup", s2 + opsum -= t, "Cdagup", s2, "Cup", s1 + opsum -= t, "Cdagdn", s1, "Cdn", s2 + opsum -= t, "Cdagdn", s2, "Cdn", s1 + end + end + if U ≠ 0 + for x in 0:(Nx - 1) + for ky in 0:(Ny - 1) + for py in 0:(Ny - 1) + for qy in 0:(Ny - 1) + s1 = x * Ny + (ky + qy + Ny) % Ny + 1 + s2 = x * Ny + (py - qy + Ny) % Ny + 1 + s3 = x * Ny + py + 1 + s4 = x * Ny + ky + 1 + opsum += (U / Ny), "Cdagdn", s1, "Cdagup", s2, "Cup", s3, "Cdn", s4 + end + end + end + end + end + return opsum +end + +function hubbard(; Nx::Int, Ny::Int=1, t=1.0, U=0.0, yperiodic::Bool=true, ky::Bool=false) + return opsum = if Ny == 1 + hubbard_1d(; N=Nx, t, U) + elseif ky + hubbard_2d_ky(; Nx, Ny, t, U) + else + hubbard_2d(; Nx, Ny, yperiodic, t, U) + end +end diff --git a/ext/ITensorMPSChainRulesCoreExt/ITensorMPSChainRulesCoreExt.jl b/ext/ITensorMPSChainRulesCoreExt/ITensorMPSChainRulesCoreExt.jl new file mode 100644 index 0000000..318e8a4 --- /dev/null +++ b/ext/ITensorMPSChainRulesCoreExt/ITensorMPSChainRulesCoreExt.jl @@ -0,0 +1,6 @@ +module ITensorMPSChainRulesCoreExt +include("indexset.jl") +include("abstractmps.jl") +include("mps.jl") +include("mpo.jl") +end diff --git a/ext/ITensorMPSChainRulesCoreExt/abstractmps.jl b/ext/ITensorMPSChainRulesCoreExt/abstractmps.jl new file mode 100644 index 0000000..53c6980 --- /dev/null +++ b/ext/ITensorMPSChainRulesCoreExt/abstractmps.jl @@ -0,0 +1,192 @@ +using Adapt: adapt +using ChainRulesCore: ChainRulesCore, HasReverseMode, NoTangent, RuleConfig, rrule_via_ad +using ITensors: + ITensors, ITensor, dag, hassameinds, inds, itensor, mapprime, replaceprime, swapprime +using ITensorMPS: ITensorMPS, MPO, MPS, apply, inner, siteinds +using NDTensors: datatype + +function ChainRulesCore.rrule( + ::Type{T}, x::Vector{<:ITensor}; kwargs... +) where {T<:Union{MPS,MPO}} + y = T(x; kwargs...) + function T_pullback(ȳ) + ȳtensors = ȳ.data + n = length(ȳtensors) + envL = [ȳtensors[1] * dag(x[1])] + envR = [ȳtensors[n] * dag(x[n])] + for j in 2:(n - 1) + push!(envL, envL[j - 1] * ȳtensors[j] * dag(x[j])) + push!(envR, envR[j - 1] * ȳtensors[n + 1 - j] * dag(x[n + 1 - j])) + end + + x̄ = ITensor[] + push!(x̄, ȳtensors[1] * envR[n - 1]) + for j in 2:(n - 1) + push!(x̄, envL[j - 1] * ȳtensors[j] * envR[n - j]) + end + push!(x̄, envL[n - 1] * ȳtensors[n]) + return (NoTangent(), x̄) + end + return y, T_pullback +end + +function ChainRulesCore.rrule( + ::typeof(inner), x1::T, x2::T; kwargs... +) where {T<:Union{MPS,MPO}} + if !hassameinds(siteinds, x1, x2) + error( + "Taking gradients of `inner(::MPS, ::MPS)` is not supported if the site indices of the input MPS don't match. If you input `inner(x, Ay)` where `Ay` is the result of something like `contract(A::MPO, y::MPS)`, try `inner(x', Ay)` or `inner(x, replaceprime(Ay, 1 => 0))`instead.", + ) + end + y = inner(x1, x2) + function inner_pullback(ȳ) + x̄1 = dag(ȳ) * x2 + # `dag` of `x1` gets reversed by `inner` + x̄2 = x1 * ȳ + return (NoTangent(), x̄1, x̄2) + end + return y, inner_pullback +end + +# TODO: Define a more general version in ITensors.jl +function _contract(::Type{ITensor}, ψ::Union{MPS,MPO}, ϕ::Union{MPS,MPO}; kwargs...) + n = length(ψ) + @assert length(ϕ) == length(ψ) + + jcenter = findfirst(j -> !hassameinds(siteinds(ψ, j), siteinds(ϕ, j)), 1:n) + + Tᴸ = adapt(datatype(ψ[1]), ITensor(1)) + for j in 1:jcenter + Tᴸ = Tᴸ * ψ[j] * ϕ[j] + end + Tᴿ = adapt(datatype(ψ[end]), ITensor(1)) + for j in reverse((jcenter + 1):length(ψ)) + Tᴿ = Tᴿ * ψ[j] * ϕ[j] + end + return Tᴸ * Tᴿ +end + +function _contract(::Type{MPO}, ψ::MPS, ϕ::MPS; kwargs...) + ψmat = convert(MPO, ψ) + ϕmat = convert(MPO, ϕ) + return contract(ψmat, ϕmat; kwargs...) +end + +# quick and dirty +# is this sufficient always? +function _is_mps_or_hermitian_mpo(x::MPO; kwargs...) + s = siteinds(x) + return all(eachindex(x)) do i + isapprox(x[i], swapprime(dag(x[i]), 0 => 1; inds=s[i]); kwargs...) + end +end +_is_mps_or_hermitian_mpo(x::MPS; kwargs...) = true + +function ChainRulesCore.rrule( + ::typeof(apply), x1::Vector{ITensor}, x2::Union{MPS,MPO}; apply_dag=false, kwargs... +) + #if apply_dag && !_is_mps_or_hermitian_mpo(x2) + #error( + #"For now, we only support taking derivatives of MPO gate application with `apply_dag=true` for Hermitian MPOs. As an alternative, you can manually apply the gate once on each side of the MPO.", + #) + #end + + N = length(x1) + 1 + + # Apply circuit and store intermediates in the forward direction + x1x2 = Vector{typeof(x2)}(undef, N) + x1x2[1] = x2 + for n in 2:N + x1x2[n] = apply(x1[n - 1], x1x2[n - 1]; move_sites_back=true, apply_dag, kwargs...) + end + y = x1x2[end] + + function apply_pullback(ȳ) + x1x2dag = dag.(x1x2) + x1dag = [swapprime(dag(x), 0 => 1) for x in x1] + + # Apply circuit and store intermediates in the reverse direction + x1dag_ȳ = Vector{typeof(x2)}(undef, N) + x1dag_ȳ[end] = ȳ + for n in (N - 1):-1:1 + x1dag_ȳ[n] = apply( + x1dag[n], x1dag_ȳ[n + 1]; move_sites_back=true, apply_dag, kwargs... + ) + end + + x̄1 = similar(x1) + for n in 1:length(x1) + # check if it's not a noisy gate (rank-3 tensor) + if iseven(length(inds(x1[n]))) + gateinds = inds(x1[n]; plev=0) + if x2 isa MPS + ξ̃ = prime(x1dag_ȳ[n + 1], gateinds) + ϕ̃ = x1x2dag[n] + x̄1[n] = _contract(ITensor, ξ̃, ϕ̃; kwargs...) + else + # apply U on one side of the MPO + if apply_dag + ishermitian = _is_mps_or_hermitian_mpo(x2) + + if ishermitian + ϕ̃ = swapprime(x1x2dag[n], 0 => 1) + ϕ̃ = apply(x1[n], ϕ̃; move_sites_back=true, apply_dag=false, kwargs...) + ϕ̃ = mapprime(ϕ̃, 1 => 2, 0 => 1) + ϕ̃ = replaceprime(ϕ̃, 1 => 0; inds=gateinds') + + ξ̃ = 2 * dag(x1dag_ȳ[n + 1])' + x̄1[n] = _contract(ITensor, ξ̃, ϕ̃; kwargs...) + else + # prepare contribution from taking the derivative w.r.t. Q + # M = x1Q†, QM -> M†W̄ = Qx1†W̄ + ϕ̃ = swapprime(x1x2dag[n], 0 => 1) + ϕ̃ = apply(x1[n], ϕ̃; move_sites_back=true, apply_dag=false, kwargs...) + ϕ̃ = mapprime(ϕ̃, 1 => 2, 0 => 1) + ϕ̃ = replaceprime(ϕ̃, 1 => 0; inds=gateinds') + ξ̃ = mapprime(x1dag_ȳ[n + 1], 0 => 2) + x̄1[n] = _contract(ITensor, ξ̃, ϕ̃; kwargs...) + + # prepare contribution from taking the derivative w.r.t. Q† + # M = Qx1, MQ† -> W̄†M = W̄†Qx1 + ϕ̃ = apply(x1[n], x1x2[n]; move_sites_back=true, apply_dag=false, kwargs...) + ϕ̃ = mapprime(ϕ̃, 1 => 2, 0 => 1) + ϕ̃ = replaceprime(ϕ̃, 1 => 0; inds=gateinds') + ξ̃ = dag(x1dag_ȳ[n + 1])' + x̄1[n] += _contract(ITensor, ξ̃, ϕ̃; kwargs...) + end + else + ϕ̃ = mapprime(x1x2dag[n], 0 => 2) + ϕ̃ = replaceprime(ϕ̃, 1 => 0; inds=gateinds') + ξ̃ = mapprime(x1dag_ȳ[n + 1], 0 => 2) + x̄1[n] = _contract(ITensor, ξ̃, ϕ̃; kwargs...) + end + end + else + s = inds(x1[n]) + x̄1[n] = itensor(zeros(dim.(s)), s...) + end + end + x̄2 = x1dag_ȳ[1] + return (NoTangent(), x̄1, x̄2) + end + return y, apply_pullback +end + +function ChainRulesCore.rrule( + config::RuleConfig{>:HasReverseMode}, + ::typeof(map), + f, + x::Union{MPS,MPO}; + set_limits::Bool=true, +) + y_data, pullback_data = rrule_via_ad(config, map, f, ITensors.data(x)) + function map_pullback(ȳ) + dmap, df, dx_data = pullback_data(ȳ) + return dmap, df, MPS(dx_data) + end + y = typeof(x)(y_data) + if !set_limits + y = ITensorMPS.set_ortho_lims(y, ITensorMPS.ortho_lims(x)) + end + return y, map_pullback +end diff --git a/ext/ITensorMPSChainRulesCoreExt/indexset.jl b/ext/ITensorMPSChainRulesCoreExt/indexset.jl new file mode 100644 index 0000000..154f3f3 --- /dev/null +++ b/ext/ITensorMPSChainRulesCoreExt/indexset.jl @@ -0,0 +1,34 @@ +using ChainRulesCore: ChainRulesCore, unthunk +using Compat: Returns +using ITensors: + addtags, + noprime, + prime, + removetags, + replaceinds, + replaceprime, + replacetags, + setprime, + settags +using ITensorMPS: MPO, MPS + +for fname in ( + :prime, :setprime, :noprime, :replaceprime, :addtags, :removetags, :replacetags, :settags +) + @eval begin + function ChainRulesCore.rrule(f::typeof($fname), x::Union{MPS,MPO}, a...; kwargs...) + y = f(x, a...; kwargs...) + function f_pullback(ȳ) + x̄ = copy(unthunk(ȳ)) + for j in eachindex(x̄) + x̄[j] = replaceinds(ȳ[j], inds(y[j]) => inds(x[j])) + end + ā = map(Returns(NoTangent()), a) + return (NoTangent(), x̄, ā...) + end + return y, f_pullback + end + end +end + +ChainRulesCore.rrule(::typeof(adjoint), x::Union{MPS,MPO}) = ChainRulesCore.rrule(prime, x) diff --git a/ext/ITensorMPSChainRulesCoreExt/mpo.jl b/ext/ITensorMPSChainRulesCoreExt/mpo.jl new file mode 100644 index 0000000..3da69ac --- /dev/null +++ b/ext/ITensorMPSChainRulesCoreExt/mpo.jl @@ -0,0 +1,84 @@ +using ChainRulesCore: ChainRulesCore, NoTangent +using ITensors: Algorithm, contract, hassameinds, inner, mapprime +using ITensorMPS: MPO, MPS, firstsiteinds, siteinds +using LinearAlgebra: tr + +function ChainRulesCore.rrule( + ::typeof(contract), alg::Algorithm, x1::MPO, x2::MPO; kwargs... +) + y = contract(alg, x1, x2; kwargs...) + function contract_pullback(ȳ) + x̄1 = contract(alg, ȳ, dag(x2); kwargs...) + x̄2 = contract(alg, dag(x1), ȳ; kwargs...) + return (NoTangent(), NoTangent(), x̄1, x̄2) + end + return y, contract_pullback +end + +function ChainRulesCore.rrule( + ::typeof(contract), alg::Algorithm, x1::MPO, x2::MPS; kwargs... +) + y = contract(alg, x1, x2; kwargs...) + function contract_pullback(ȳ) + x̄1 = _contract(MPO, ȳ, dag(x2); kwargs...) + x̄2 = contract(alg, dag(x1), ȳ; kwargs...) + return (NoTangent(), NoTangent(), x̄1, x̄2) + end + return y, contract_pullback +end + +## function ChainRulesCore.rrule(::typeof(*), x1::MPO, x2::MPO; alg, kwargs...) +## return ChainRulesCore.rrule(contract, alg, x1, x2; kwargs...) +## end + +function ChainRulesCore.rrule(::typeof(+), x1::MPO, x2::MPO; kwargs...) + y = +(x1, x2; kwargs...) + function add_pullback(ȳ) + return (NoTangent(), ȳ, ȳ) + end + return y, add_pullback +end + +function ChainRulesCore.rrule(::typeof(-), x1::MPO, x2::MPO; kwargs...) + y = -(x1, x2; kwargs...) + function subtract_pullback(ȳ) + return (NoTangent(), ȳ, -ȳ) + end + return y, subtract_pullback +end + +function ChainRulesCore.rrule(::typeof(tr), x::MPO; plev=(0 => 1), kwargs...) + y = tr(x; plev, kwargs...) + function tr_pullback(ȳ) + s = noprime(firstsiteinds(x)) + n = length(s) + x̄ = MPO(s, "Id") + for j in 1:n + x̄[j] = mapprime(x̄[j], 0 => first(plev), 1 => last(plev)) + end + return (NoTangent(), ȳ * x̄) + end + return y, tr_pullback +end + +function ChainRulesCore.rrule(::typeof(inner), x1::MPS, x2::MPO, x3::MPS; kwargs...) + if !hassameinds(siteinds, x1, (x2, x3)) || !hassameinds(siteinds, x3, (x2, x1)) + error( + "Taking gradients of `inner(x::MPS, A::MPO, y::MPS)` is not supported if the site indices of the input MPS and MPO don't match. Try using if you input `inner(x, A, y), try `inner(x', A, y)` instead.", + ) + end + + y = inner(x1, x2, x3; kwargs...) + function inner_pullback(ȳ) + x̄1 = dag(ȳ) * contract(x2, x3; kwargs...) + x̄2 = ȳ * dag(_contract(MPO, dag(x1), x3; kwargs...)) + x̄3 = contract(dag(x2), x1; kwargs...) * ȳ + + @assert siteinds(x1) == siteinds(x̄1) + @assert hassameinds(siteinds, x2, x̄2) + @assert siteinds(x3) == siteinds(x̄3) + + return (NoTangent(), x̄1, x̄2, x̄3) + end + return y, inner_pullback +end diff --git a/ext/ITensorMPSChainRulesCoreExt/mps.jl b/ext/ITensorMPSChainRulesCoreExt/mps.jl new file mode 100644 index 0000000..e0647d4 --- /dev/null +++ b/ext/ITensorMPSChainRulesCoreExt/mps.jl @@ -0,0 +1,4 @@ +using ChainRulesCore: @non_differentiable +using ITensors: Index +using ITensorMPS: MPS +@non_differentiable MPS(::Type{<:Number}, sites::Vector{<:Index}, states_) diff --git a/ext/ITensorMPSHDF5Ext/ITensorMPSHDF5Ext.jl b/ext/ITensorMPSHDF5Ext/ITensorMPSHDF5Ext.jl new file mode 100644 index 0000000..db65136 --- /dev/null +++ b/ext/ITensorMPSHDF5Ext/ITensorMPSHDF5Ext.jl @@ -0,0 +1,4 @@ +module ITensorMPSHDF5Ext +include("mps.jl") +include("mpo.jl") +end diff --git a/ext/ITensorMPSHDF5Ext/mpo.jl b/ext/ITensorMPSHDF5Ext/mpo.jl new file mode 100644 index 0000000..90a53a0 --- /dev/null +++ b/ext/ITensorMPSHDF5Ext/mpo.jl @@ -0,0 +1,28 @@ +using HDF5: HDF5, attributes, create_group, open_group, read, write +using ITensors: ITensor +using ITensorMPS: MPO + +function HDF5.write(parent::Union{HDF5.File,HDF5.Group}, name::AbstractString, M::MPO) + g = create_group(parent, name) + attributes(g)["type"] = "MPO" + attributes(g)["version"] = 1 + N = length(M) + write(g, "rlim", M.rlim) + write(g, "llim", M.llim) + write(g, "length", N) + for n in 1:N + write(g, "MPO[$(n)]", M[n]) + end +end + +function HDF5.read(parent::Union{HDF5.File,HDF5.Group}, name::AbstractString, ::Type{MPO}) + g = open_group(parent, name) + if read(attributes(g)["type"]) != "MPO" + error("HDF5 group or file does not contain MPO data") + end + N = read(g, "length") + rlim = read(g, "rlim") + llim = read(g, "llim") + v = [read(g, "MPO[$(i)]", ITensor) for i in 1:N] + return MPO(v, llim, rlim) +end diff --git a/ext/ITensorMPSHDF5Ext/mps.jl b/ext/ITensorMPSHDF5Ext/mps.jl new file mode 100644 index 0000000..619a381 --- /dev/null +++ b/ext/ITensorMPSHDF5Ext/mps.jl @@ -0,0 +1,28 @@ +using HDF5: HDF5, attributes, create_group, open_group, read, write +using ITensors: ITensor +using ITensorMPS: MPS + +function HDF5.write(parent::Union{HDF5.File,HDF5.Group}, name::AbstractString, M::MPS) + g = create_group(parent, name) + attributes(g)["type"] = "MPS" + attributes(g)["version"] = 1 + N = length(M) + write(g, "length", N) + write(g, "rlim", M.rlim) + write(g, "llim", M.llim) + for n in 1:N + write(g, "MPS[$(n)]", M[n]) + end +end + +function HDF5.read(parent::Union{HDF5.File,HDF5.Group}, name::AbstractString, ::Type{MPS}) + g = open_group(parent, name) + if read(attributes(g)["type"]) != "MPS" + error("HDF5 group or file does not contain MPS data") + end + N = read(g, "length") + rlim = read(g, "rlim") + llim = read(g, "llim") + v = [read(g, "MPS[$(i)]", ITensor) for i in 1:N] + return MPS(v, llim, rlim) +end diff --git a/ext/ITensorMPSObserversExt/ITensorMPSObserversExt.jl b/ext/ITensorMPSObserversExt/ITensorMPSObserversExt.jl new file mode 100644 index 0000000..f353b99 --- /dev/null +++ b/ext/ITensorMPSObserversExt/ITensorMPSObserversExt.jl @@ -0,0 +1,9 @@ +module ITensorMPSObserversExt +using Observers: Observers +using Observers.DataFrames: AbstractDataFrame +using ITensorMPS: ITensorMPS + +function ITensorMPS.update_observer!(observer::AbstractDataFrame; kwargs...) + return Observers.update!(observer; kwargs...) +end +end diff --git a/ext/ITensorMPSPackageCompilerExt/ITensorMPSPackageCompilerExt.jl b/ext/ITensorMPSPackageCompilerExt/ITensorMPSPackageCompilerExt.jl new file mode 100644 index 0000000..0310dc7 --- /dev/null +++ b/ext/ITensorMPSPackageCompilerExt/ITensorMPSPackageCompilerExt.jl @@ -0,0 +1,3 @@ +module ITensorMPSPackageCompilerExt +include("compile.jl") +end diff --git a/ext/ITensorMPSPackageCompilerExt/compile.jl b/ext/ITensorMPSPackageCompilerExt/compile.jl new file mode 100644 index 0000000..c14d0e9 --- /dev/null +++ b/ext/ITensorMPSPackageCompilerExt/compile.jl @@ -0,0 +1,26 @@ +using NDTensors: @Algorithm_str +using ITensors: ITensors +using PackageCompiler: PackageCompiler + +function ITensors.compile( + ::Algorithm"PackageCompiler"; + dir::AbstractString=ITensors.default_compile_dir(), + filename::AbstractString=ITensors.default_compile_filename(), +) + if !isdir(dir) + println("""The directory "$dir" doesn't exist yet, creating it now.""") + println() + mkdir(dir) + end + path = joinpath(dir, filename) + println( + """Creating the system image "$path" containing the compiled version of ITensorMPS. This may take a few minutes.""", + ) + PackageCompiler.create_sysimage( + :ITensorMPS; + sysimage_path=path, + precompile_execution_file=joinpath(@__DIR__, "precompile_itensormps.jl"), + ) + println(ITensors.compile_note(; dir, filename)) + return path +end diff --git a/ext/ITensorMPSPackageCompilerExt/precompile_itensormps.jl b/ext/ITensorMPSPackageCompilerExt/precompile_itensormps.jl new file mode 100644 index 0000000..ebcadde --- /dev/null +++ b/ext/ITensorMPSPackageCompilerExt/precompile_itensormps.jl @@ -0,0 +1,28 @@ +using ITensorMPS: MPO, OpSum, dmrg, random_mps, siteinds + +# TODO: This uses all of the tests to make +# precompile statements, but takes a long time +# (e.g. 700 seconds). +# Try again with later versions of PackageCompiler +# +# include(joinpath(joinpath(dirname(dirname(@__DIR__)), +# test"), +# "runtests.jl")) + +function main(; N, dmrg_kwargs) + opsum = OpSum() + for j in 1:(N - 1) + opsum += 0.5, "S+", j, "S-", j + 1 + opsum += 0.5, "S-", j, "S+", j + 1 + opsum += "Sz", j, "Sz", j + 1 + end + for conserve_qns in (false, true) + sites = siteinds("S=1", N; conserve_qns) + H = MPO(opsum, sites) + ψ0 = random_mps(sites, j -> isodd(j) ? "↑" : "↓"; linkdims=2) + dmrg(H, ψ0; outputlevel=0, dmrg_kwargs...) + end + return nothing +end + +main(; N=6, dmrg_kwargs=(; nsweeps=3, maxdim=10, cutoff=1e-13)) diff --git a/ext/ITensorMPSZygoteRulesExt/ITensorMPSZygoteRulesExt.jl b/ext/ITensorMPSZygoteRulesExt/ITensorMPSZygoteRulesExt.jl new file mode 100644 index 0000000..f79322b --- /dev/null +++ b/ext/ITensorMPSZygoteRulesExt/ITensorMPSZygoteRulesExt.jl @@ -0,0 +1,14 @@ +module ITensorMPSZygoteRulesExt +using ChainRulesCore: ChainRulesCore +using ITensorMPS: MPO, MPS +using ZygoteRules: @adjoint + +# Needed for defining the rule for `adjoint(A::ITensor)` +# which currently doesn't work by overloading `ChainRulesCore.rrule` +# since it is defined in `Zygote`, which takes precedent. +@adjoint function Base.adjoint(x::Union{MPS,MPO}) + y, adjoint_rrule_pullback = ChainRulesCore.rrule(adjoint, x) + adjoint_pullback(ȳ) = Base.tail(adjoint_rrule_pullback(ȳ)) + return y, adjoint_pullback +end +end diff --git a/src/Deprecated.jl b/src/Deprecated.jl index d56b368..35c775b 100644 --- a/src/Deprecated.jl +++ b/src/Deprecated.jl @@ -1,3 +1,106 @@ -module Deprecated -using ITensors.ITensorMPS: dmrg +# mps/abstractmps.jl +@deprecate orthoCenter(args...; kwargs...) orthocenter(args...; kwargs...) +@deprecate store(m::AbstractMPS) data(m) false +@deprecate replacesites!(args...; kwargs...) ITensors.replace_siteinds!(args...; kwargs...) +@deprecate applyMPO(args...; kwargs...) contract(args...; kwargs...) +@deprecate applympo(args...; kwargs...) contract(args...; kwargs...) +@deprecate errorMPOprod(args...; kwargs...) error_contract(args...; kwargs...) +@deprecate error_mpoprod(args...; kwargs...) error_contract(args...; kwargs...) +@deprecate error_mul(args...; kwargs...) error_contract(args...; kwargs...) +@deprecate multMPO(args...; kwargs...) contract(args...; kwargs...) +@deprecate sum(A::AbstractMPS, B::AbstractMPS; kwargs...) add(A, B; kwargs...) +@deprecate multmpo(args...; kwargs...) contract(args...; kwargs...) +@deprecate set_leftlim!(args...; kwargs...) ITensors.setleftlim!(args...; kwargs...) +@deprecate set_rightlim!(args...; kwargs...) ITensors.setrightlim!(args...; kwargs...) +@deprecate tensors(args...; kwargs...) ITensors.data(args...; kwargs...) +@deprecate primelinks!(args...; kwargs...) ITensors.prime_linkinds!(args...; kwargs...) +@deprecate simlinks!(args...; kwargs...) ITensors.sim_linkinds!(args...; kwargs...) +@deprecate mul(A::AbstractMPS, B::AbstractMPS; kwargs...) contract(A, B; kwargs...) + +# mps/mpo.jl +@deprecate toMPO(args...; kwargs...) MPO(args...; kwargs...) +@deprecate MPO(A::MPS; kwargs...) outer(A', A; kwargs...) +@deprecate randomMPO(args...; kwargs...) random_mpo(args...; kwargs...) + +# mps/mps.jl +@deprecate randomMPS(args...; kwargs...) random_mps(args...; kwargs...) + +# Deprecated syntax for specifying link dimensions. +@deprecate randomMPS(elt::Type{<:Number}, sites::Vector{<:Index}, state, linkdims::Integer) random_mps( + elt, sites, state; linkdims +) +@deprecate randomMPS(elt::Type{<:Number}, sites::Vector{<:Index}, linkdims::Integer) random_mps( + elt, sites; linkdims +) +@deprecate randomMPS(sites::Vector{<:Index}, state, linkdims::Integer) random_mps( + sites, state; linkdims +) +@deprecate randomMPS(sites::Vector{<:Index}, linkdims::Integer) random_mps(sites; linkdims) + +# Pass throughs of old name to new name: + +unique_siteind(A::AbstractMPS, B::AbstractMPS, j::Integer) = siteinds(uniqueind, A, B, j) + +unique_siteinds(A::AbstractMPS, B::AbstractMPS, j::Integer) = siteinds(uniqueinds, A, B, j) + +unique_siteinds(A::AbstractMPS, B::AbstractMPS) = siteinds(uniqueind, A, B) + +common_siteind(A::AbstractMPS, B::AbstractMPS, j::Integer) = siteinds(commonind, A, B, j) + +common_siteinds(A::AbstractMPS, B::AbstractMPS, j::Integer) = siteinds(commoninds, A, B, j) + +common_siteinds(A::AbstractMPS, B::AbstractMPS) = siteinds(commoninds, A, B) + +firstsiteind(M::AbstractMPS, j::Integer; kwargs...) = siteind(first, M, j; kwargs...) + +map_linkinds!(f::Function, M::AbstractMPS) = map!(f, linkinds, M) + +map_linkinds(f::Function, M::AbstractMPS) = map(f, linkinds, M) + +function map_common_siteinds!(f::Function, M1::AbstractMPS, M2::AbstractMPS) + return map!(f, siteinds, commoninds, M1, M2) +end + +function map_common_siteinds(f::Function, M1::AbstractMPS, M2::AbstractMPS) + return map(f, siteinds, commoninds, M1, M2) +end + +function map_unique_siteinds!(f::Function, M1::AbstractMPS, M2::AbstractMPS) + return map!(f, siteinds, uniqueinds, M1, M2) +end + +function map_unique_siteinds(f::Function, M1::AbstractMPS, M2::AbstractMPS) + return map(f, siteinds, uniqueinds, M1, M2) +end + +for fname in + (:sim, :prime, :setprime, :noprime, :addtags, :removetags, :replacetags, :settags) + @eval begin + function $(Symbol(fname, :_linkinds))(M::AbstractMPS, args...; kwargs...) + return map(i -> $fname(i, args...; kwargs...), linkinds, M) + end + function $(Symbol(fname, :_linkinds!))(M::AbstractMPS, args...; kwargs...) + return map!(i -> $fname(i, args...; kwargs...), linkinds, M) + end + function $(Symbol(fname, :_common_siteinds))( + M1::AbstractMPS, M2::AbstractMPS, args...; kwargs... + ) + return map(i -> $fname(i, args...; kwargs...), siteinds, commoninds, M1, M2) + end + function $(Symbol(fname, :_common_siteinds!))( + M1::AbstractMPS, M2::AbstractMPS, args...; kwargs... + ) + return map!(i -> $fname(i, args...; kwargs...), siteinds, commoninds, M1, M2) + end + function $(Symbol(fname, :_unique_siteinds))( + M1::AbstractMPS, M2::AbstractMPS, args...; kwargs... + ) + return map(i -> $fname(i, args...; kwargs...), siteinds, uniqueinds, M1, M2) + end + function $(Symbol(fname, :_unique_siteinds!))( + M1::AbstractMPS, M2::AbstractMPS, args...; kwargs... + ) + return map!(i -> $fname(i, args...; kwargs...), siteinds, uniqueinds, M1, M2) + end + end end diff --git a/src/ITensorMPS.jl b/src/ITensorMPS.jl index b0a7e13..e90ec72 100644 --- a/src/ITensorMPS.jl +++ b/src/ITensorMPS.jl @@ -1,178 +1,47 @@ module ITensorMPS -using Reexport: @reexport -@reexport using ITensorTDVP: TimeDependentSum, dmrg_x, expand, linsolve, tdvp, to_vec -# Not re-exported, but this makes these types and functions accessible -# as `ITensorMPS.x`. -using ITensors.ITensorMPS: - AbstractProjMPO, AbstractSum, ProjMPS, makeL!, makeR!, set_terms, sortmergeterms, terms -include("Experimental.jl") -using .Experimental: Experimental -include("Deprecated.jl") -using .Deprecated: Deprecated, dmrg -export dmrg -# `ops` is defined in `ITensors.SiteTypes`. -# TODO: Maybe reexport from there. -@reexport using ITensors: contract, ops -@reexport using ITensors.ITensorMPS: - @OpName_str, - @SiteType_str, - @StateName_str, - @TagType_str, - @ValName_str, - @preserve_ortho, - @visualize, - @visualize!, - @visualize_noeval, - @visualize_noeval!, - @visualize_sequence, - @visualize_sequence_noeval, - AbstractMPS, - AbstractObserver, - Apply, - AutoMPO, - DMRGMeasurement, - DMRGObserver, - Lattice, - LatticeBond, - MPO, - MPS, - NoObserver, - Op, - OpName, - OpSum, - Ops, - Prod, - ProjMPO, - ProjMPOSum, - ProjMPO_MPS, - Scaled, - SiteType, - Spectrum, - StateName, - Sum, - Sweeps, - TagType, # deprecate - Trotter, - ValName, - add, - add!, - apply, - applyMPO, - applympo, - argsdict, - checkdone!, # remove export - coefficient, - common_siteind, - common_siteinds, - convert_leaf_eltype, # remove export - correlation_matrix, - cutoff, - cutoff!, # deprecate - disk, - dot, # remove export - eigs, # deprecate - energies, # deprecate - entropy, # deprecate - errorMPOprod, # deprecate - error_contract, - error_mpoprod, # deprecate - error_mul, # deprecate - expect, - findfirstsiteind, # deprecate - findfirstsiteinds, # deprecate - findsite, # deprecate - findsites, # deprecate - firstsiteind, # deprecate - firstsiteinds, # deprecate - get_cutoffs, # deprecate - get_maxdims, # deprecate - get_mindims, # deprecate - get_noises, # deprecate - has_fermion_string, # remove export - hassameinds, - inner, - isortho, - linkdim, - linkdims, - linkind, - linkindex, - linkinds, - logdot, - loginner, - lognorm, - lproj, - maxdim, - maxdim!, - maxlinkdim, - measure!, - measurements, - mindim, - mindim!, - movesite, - movesites, - mul, # deprecate - multMPO, - multmpo, - noise, - noise!, - noiseterm, - nsite, - nsweep, - op, - orthoCenter, - ortho_lims, - orthocenter, - orthogonalize, - orthogonalize!, - outer, - position!, - product, - primelinks!, - productMPS, - projector, - promote_itensor_eltype, - randomMPO, - randomMPS, - random_mpo, - random_mps, - replace_siteinds, - replace_siteinds!, - replacebond, - replacebond!, - replaceprime, - replacesites!, - reset_ortho_lims!, - rproj, - sample, - sample!, - set_leftlim!, - set_ortho_lims!, - set_rightlim!, - setcutoff!, - setmaxdim!, - setmindim!, - setnoise!, - simlinks!, - siteind, - siteindex, - siteinds, - splitblocks, - square_lattice, - state, - sum, - swapbondsites, - sweepnext, - tensors, - toMPO, - totalqn, - tr, - triangular_lattice, - truncate, - truncate!, - truncerror, - truncerrors, - unique_siteind, - unique_siteinds, - val, - ⋅ +using ITensors +include("imports.jl") +include("exports.jl") +include("abstractmps.jl") +include("mps.jl") +include("mpo.jl") +include("sweeps.jl") +include("abstractprojmpo/abstractprojmpo.jl") +include("abstractprojmpo/projmpo.jl") +include("abstractprojmpo/diskprojmpo.jl") +include("abstractprojmpo/projmposum.jl") +include("abstractprojmpo/projmps.jl") +include("abstractprojmpo/projmpo_mps.jl") +include("observer.jl") +include("dmrg.jl") +include("adapt.jl") +include("opsum_to_mpo/matelem.jl") +include("opsum_to_mpo/qnmatelem.jl") +include("opsum_to_mpo/opsum_to_mpo_generic.jl") +include("opsum_to_mpo/opsum_to_mpo.jl") +include("opsum_to_mpo/opsum_to_mpo_qn.jl") +include("deprecated.jl") +include("defaults.jl") +include("update_observer.jl") +include("lattices/lattices.jl") +include("solvers/ITensorsExtensions.jl") +using .ITensorsExtensions: to_vec +include("solvers/applyexp.jl") +include("solvers/defaults.jl") +include("solvers/update_observer.jl") +include("solvers/timedependentsum.jl") +include("solvers/tdvporder.jl") +include("solvers/sweep_update.jl") +include("solvers/alternating_update.jl") +include("solvers/tdvp.jl") +include("solvers/dmrg.jl") +include("solvers/dmrg_x.jl") +include("solvers/reducedcontractproblem.jl") +include("solvers/contract.jl") +include("solvers/reducedconstantterm.jl") +include("solvers/reducedlinearproblem.jl") +include("solvers/linsolve.jl") +include("solvers/expand.jl") +include("lib/Experimental/src/Experimental.jl") +include("lib/ITensorMPSNamedDimsArraysExt/src/ITensorMPSNamedDimsArraysExt.jl") end diff --git a/src/abstractmps.jl b/src/abstractmps.jl new file mode 100644 index 0000000..4670be5 --- /dev/null +++ b/src/abstractmps.jl @@ -0,0 +1,2415 @@ +using IsApprox: Approx, IsApprox +using ITensors: ITensors +using NDTensors: NDTensors, using_auto_fermion, scalartype, tensor +using ITensors.Ops: Prod +using ITensors.QuantumNumbers: QuantumNumbers, removeqn +using ITensors.SiteTypes: SiteTypes, siteinds +using ITensors.TagSets: TagSets + +abstract type AbstractMPS end + +""" + length(::MPS/MPO) + +The number of sites of an MPS/MPO. +""" +Base.length(m::AbstractMPS) = length(data(m)) + +""" + size(::MPS/MPO) + +The number of sites of an MPS/MPO, in a tuple. +""" +Base.size(m::AbstractMPS) = size(data(m)) + +Base.ndims(m::AbstractMPS) = ndims(data(m)) + +function promote_itensor_eltype(m::Vector{ITensor}) + T = isassigned(m, 1) ? eltype(m[1]) : Number + for n in 2:length(m) + Tn = isassigned(m, n) ? eltype(m[n]) : Number + T = promote_type(T, Tn) + end + return T +end + +function LinearAlgebra.promote_leaf_eltypes(m::Vector{ITensor}) + return promote_itensor_eltype(m) +end + +function LinearAlgebra.promote_leaf_eltypes(m::AbstractMPS) + return LinearAlgebra.promote_leaf_eltypes(data(m)) +end + +""" + promote_itensor_eltype(m::MPS) + promote_itensor_eltype(m::MPO) + +Return the promotion of the elements type of the +tensors in the MPS or MPO. For example, +if all tensors have type `Float64` then +return `Float64`. But if one or more tensors +have type `ComplexF64`, return `ComplexF64`. +""" +promote_itensor_eltype(m::AbstractMPS) = LinearAlgebra.promote_leaf_eltypes(m) + +NDTensors.scalartype(m::AbstractMPS) = LinearAlgebra.promote_leaf_eltypes(m) +NDTensors.scalartype(m::Array{ITensor}) = LinearAlgebra.promote_leaf_eltypes(m) +NDTensors.scalartype(m::Array{<:Array{ITensor}}) = LinearAlgebra.promote_leaf_eltypes(m) + +""" + eltype(m::MPS) + eltype(m::MPO) + +The element type of the MPS/MPO. Always returns `ITensor`. + +For the element type of the ITensors of the MPS/MPO, +use `promote_itensor_eltype`. +""" +Base.eltype(::AbstractMPS) = ITensor + +Base.complex(ψ::AbstractMPS) = complex.(ψ) +Base.real(ψ::AbstractMPS) = real.(ψ) +Base.imag(ψ::AbstractMPS) = imag.(ψ) +Base.conj(ψ::AbstractMPS) = conj.(ψ) + +function convert_leaf_eltype(eltype::Type, ψ::AbstractMPS) + return map(ψᵢ -> convert_leaf_eltype(eltype, ψᵢ), ψ; set_limits=false) +end + +""" + ITensors.data(::MPS/MPO) + +Returns a view of the Vector storage of an MPS/MPO. + +This is not exported and mostly for internal usage, please let us +know if there is functionality not available for MPS/MPO you would like. +""" +data(m::AbstractMPS) = m.data + +ITensors.contract(ψ::AbstractMPS) = contract(data(ψ)) + +leftlim(m::AbstractMPS) = m.llim + +rightlim(m::AbstractMPS) = m.rlim + +function setleftlim!(m::AbstractMPS, new_ll::Integer) + return m.llim = new_ll +end + +function setrightlim!(m::AbstractMPS, new_rl::Integer) + return m.rlim = new_rl +end + +""" + ortho_lims(::MPS/MPO) + +Returns the range of sites of the orthogonality center of the MPS/MPO. + +# Examples + +```julia +s = siteinds("S=½", 5) +ψ = random_mps(s) +ψ = orthogonalize(ψ, 3) + +# ortho_lims(ψ) = 3:3 +@show ortho_lims(ψ) + +ψ[2] = random_itensor(inds(ψ[2])) + +# ortho_lims(ψ) = 2:3 +@show ortho_lims(ψ) +``` +""" +function ortho_lims(ψ::AbstractMPS) + return (leftlim(ψ) + 1):(rightlim(ψ) - 1) +end + +""" + ITensors.set_ortho_lims!(::MPS/MPO, r::UnitRange{Int}) + +Sets the range of sites of the orthogonality center of the MPS/MPO. + +This is not exported and is an advanced feature that should be used with +care. Setting the orthogonality limits wrong can lead to incorrect results +when using ITensor MPS/MPO functions. + +If you are modifying an MPS/MPO and want the orthogonality limits to be +preserved, please see the `@preserve_ortho` macro. +""" +function set_ortho_lims!(ψ::AbstractMPS, r::UnitRange{Int}) + setleftlim!(ψ, first(r) - 1) + setrightlim!(ψ, last(r) + 1) + return ψ +end + +function set_ortho_lims(ψ::AbstractMPS, r::UnitRange{Int}) + return set_ortho_lims!(copy(ψ), r) +end + +reset_ortho_lims!(ψ::AbstractMPS) = set_ortho_lims!(ψ, 1:length(ψ)) + +isortho(m::AbstractMPS) = leftlim(m) + 1 == rightlim(m) - 1 + +# Could also define as `only(ortho_lims)` +function orthocenter(m::AbstractMPS) + !isortho(m) && error( + "$(typeof(m)) has no well-defined orthogonality center, orthogonality center is on the range $(ortho_lims(m)).", + ) + return leftlim(m) + 1 +end + +getindex(M::AbstractMPS, n) = getindex(data(M), n) + +isassigned(M::AbstractMPS, n) = isassigned(data(M), n) + +lastindex(M::AbstractMPS) = lastindex(data(M)) + +""" + @preserve_ortho + +Specify that a block of code preserves the orthogonality limits of +an MPS/MPO that is being modified in-place. The first input is either +a single MPS/MPO or a tuple of the MPS/MPO whose orthogonality limits +should be preserved. + +# Examples + +```julia +s = siteinds("S=1/2", 4) + +# Make random MPS with bond dimension 2 +ψ₁ = random_mps(s, "↑"; linkdims=2) +ψ₂ = random_mps(s, "↑"; linkdims=2) +ψ₁ = orthogonalize(ψ₁, 1) +ψ₂ = orthogonalize(ψ₂, 1) + +# ortho_lims(ψ₁) = 1:1 +@show ortho_lims(ψ₁) + +# ortho_lims(ψ₂) = 1:1 +@show ortho_lims(ψ₂) + +@preserve_ortho (ψ₁, ψ₂) begin + ψ₁ .= addtags.(ψ₁, "x₁"; tags = "Link") + ψ₂ .= addtags.(ψ₂, "x₂"; tags = "Link") +end + +# ortho_lims(ψ₁) = 1:1 +@show ortho_lims(ψ₁) + +# ortho_lims(ψ₂) = 1:1 +@show ortho_lims(ψ₂) +``` +""" +macro preserve_ortho(ψ, block) + quote + if $(esc(ψ)) isa AbstractMPS + local ortho_limsψ = ortho_lims($(esc(ψ))) + else + local ortho_limsψ = ortho_lims.($(esc(ψ))) + end + r = $(esc(block)) + if $(esc(ψ)) isa AbstractMPS + set_ortho_lims!($(esc(ψ)), ortho_limsψ) + else + set_ortho_lims!.($(esc(ψ)), ortho_limsψ) + end + r + end +end + +function setindex!(M::AbstractMPS, T::ITensor, n::Integer; set_limits::Bool=true) + if set_limits + (n <= leftlim(M)) && setleftlim!(M, n - 1) + (n >= rightlim(M)) && setrightlim!(M, n + 1) + end + data(M)[n] = T + return M +end + +function setindex!(M::MPST, v::MPST, ::Colon) where {MPST<:AbstractMPS} + setleftlim!(M, leftlim(v)) + setrightlim!(M, rightlim(v)) + data(M)[:] = data(v) + return M +end + +setindex!(M::AbstractMPS, v::Vector{<:ITensor}, ::Colon) = setindex!(M, MPS(v), :) + +""" + copy(::MPS) + copy(::MPO) + +Make a shallow copy of an MPS or MPO. By shallow copy, it means that a new MPS/MPO +is returned, but the data of the tensors are still shared between the returned MPS/MPO +and the original MPS/MPO. + +Therefore, replacing an entire tensor of the returned MPS/MPO will not modify the input MPS/MPO, +but modifying the data of the returned MPS/MPO will modify the input MPS/MPO. + +Use [`deepcopy`](@ref) for an alternative that copies the ITensors as well. + +# Examples +```julia +julia> using ITensors, ITensorMPS + +julia> s = siteinds("S=1/2", 3); + +julia> M1 = random_mps(s; linkdims=3); + +julia> norm(M1) +0.9999999999999999 + +julia> M2 = copy(M1); + +julia> M2[1] *= 2; + +julia> norm(M1) +0.9999999999999999 + +julia> norm(M2) +1.9999999999999998 + +julia> M3 = copy(M1); + +julia> M3[1] .*= 3; # Modifies the tensor data + +julia> norm(M1) +3.0000000000000004 + +julia> norm(M3) +3.0000000000000004 +``` +""" +Base.copy(m::AbstractMPS) = typeof(m)(copy(data(m)), leftlim(m), rightlim(m)) + +Base.similar(m::AbstractMPS) = typeof(m)(similar(data(m)), 0, length(m)) + +""" + deepcopy(::MPS) + deepcopy(::MPO) + +Make a deep copy of an MPS or MPO. By deep copy, it means that a new MPS/MPO +is returned that doesn't share any data with the input MPS/MPO. + +Therefore, modifying the resulting MPS/MPO will note modify the original MPS/MPO. + +Use [`copy`](@ref) for an alternative that performs a shallow copy that avoids +copying the ITensor data. + +# Examples +```julia +julia> using ITensors, ITensorMPS + +julia> s = siteinds("S=1/2", 3); + +julia> M1 = random_mps(s; linkdims=3); + +julia> norm(M1) +1.0 + +julia> M2 = deepcopy(M1); + +julia> M2[1] .*= 2; # Modifies the tensor data + +julia> norm(M1) +1.0 + +julia> norm(M2) +2.0 + +julia> M3 = copy(M1); + +julia> M3[1] .*= 3; # Modifies the tensor data + +julia> norm(M1) +3.0 + +julia> norm(M3) +3.0 +``` +""" +deepcopy(m::AbstractMPS) = typeof(m)(copy.(data(m)), leftlim(m), rightlim(m)) + +eachindex(m::AbstractMPS) = 1:length(m) + +iterate(M::AbstractMPS) = iterate(data(M)) + +iterate(M::AbstractMPS, state) = iterate(data(M), state) + +""" + linkind(M::MPS, j::Integer) + linkind(M::MPO, j::Integer) + +Get the link or bond Index connecting the +MPS or MPO tensor on site j to site j+1. + +If there is no link Index, return `nothing`. +""" +function linkind(M::AbstractMPS, j::Integer) + N = length(M) + (j ≥ length(M) || j < 1) && return nothing + return commonind(M[j], M[j + 1]) +end + +""" + linkinds(M::MPS, j::Integer) + linkinds(M::MPO, j::Integer) + +Get all of the link or bond Indices connecting the +MPS or MPO tensor on site j to site j+1. +""" +function linkinds(M::AbstractMPS, j::Integer) + N = length(M) + (j ≥ length(M) || j < 1) && return IndexSet() + return commoninds(M[j], M[j + 1]) +end + +linkinds(ψ::AbstractMPS) = [linkind(ψ, b) for b in 1:(length(ψ) - 1)] + +function linkinds(::typeof(all), ψ::AbstractMPS) + return IndexSet[linkinds(ψ, b) for b in 1:(length(ψ) - 1)] +end + +# +# Internal tools for checking for default link tags +# + +""" + ITensors.defaultlinktags(b::Integer) + +Default link tags for link index connecting sites `b` to `b+1`. +""" +defaultlinktags(b::Integer) = TagSet("Link,l=$b") + +""" + ITensors.hasdefaultlinktags(ψ::MPS/MPO) + +Return true if the MPS/MPO has default link tags. +""" +function hasdefaultlinktags(ψ::AbstractMPS) + ls = linkinds(all, ψ) + for (b, lb) in enumerate(ls) + if length(lb) ≠ 1 || tags(only(lb)) ≠ defaultlinktags(b) + return false + end + end + return true +end + +""" + ITensors.eachlinkinds(ψ::MPS/MPO) + +Return an iterator over each of the sets of link indices of the MPS/MPO. +""" +eachlinkinds(ψ::AbstractMPS) = (linkinds(ψ, n) for n in eachindex(ψ)[1:(end - 1)]) + +""" + ITensors.eachsiteinds(ψ::MPS/MPO) + +Return an iterator over each of the sets of site indices of the MPS/MPO. +""" +eachsiteinds(ψ::AbstractMPS) = (siteinds(ψ, n) for n in eachindex(ψ)) + +""" + ITensors.hasnolinkinds(ψ::MPS/MPO) + +Return true if the MPS/MPO has no link indices. +""" +function hasnolinkinds(ψ::AbstractMPS) + for l in eachlinkinds(ψ) + if length(l) > 0 + return false + end + end + return true +end + +""" + ITensors.insertlinkinds(ψ::MPS/MPO) + +If any link indices are missing, insert default ones. +""" +function insertlinkinds(ψ::AbstractMPS) + ψ = copy(ψ) + space = hasqns(ψ) ? [QN() => 1] : 1 + linkind(b::Integer) = Index(space; tags=defaultlinktags(b)) + for b in 1:(length(ψ) - 1) + if length(linkinds(ψ, b)) == 0 + lb = ITensor(1, linkind(b)) + @preserve_ortho ψ begin + ψ[b] = ψ[b] * lb + ψ[b + 1] = ψ[b + 1] * dag(lb) + end + end + end + return ψ +end + +""" + dense(::MPS/MPO) + +Given an MPS (or MPO), return a new MPS (or MPO) +having called `dense` on each ITensor to convert each +tensor to use dense storage and remove any QN or other +sparse structure information, if it is not dense already. +""" +function dense(ψ::AbstractMPS) + ψ = copy(ψ) + @preserve_ortho ψ ψ .= dense.(ψ) + return ψ +end + +""" + siteinds(uniqueinds, A::MPO, B::MPS, j::Integer; kwargs...) + siteinds(uniqueind, A::MPO, B::MPS, j::Integer; kwargs...) + +Get the site index (or indices) of MPO `A` that is unique to `A` (not shared with MPS/MPO `B`). +""" +function SiteTypes.siteinds( + f::Union{typeof(uniqueinds),typeof(uniqueind)}, + A::AbstractMPS, + B::AbstractMPS, + j::Integer; + kwargs..., +) + N = length(A) + N == 1 && return f(A[j], B[j]; kwargs...) + j == 1 && return f(A[j], A[j + 1], B[j]; kwargs...) + j == N && return f(A[j], A[j - 1], B[j]; kwargs...) + return f(A[j], A[j - 1], A[j + 1], B[j]; kwargs...) +end + +""" + siteinds(uniqueinds, A::MPO, B::MPS) + siteinds(uniqueind, A::MPO, B::MPO) + +Get the site indices of MPO `A` that are unique to `A` (not shared with MPS/MPO `B`), as a `Vector{<:Index}`. +""" +function SiteTypes.siteinds( + f::Union{typeof(uniqueinds),typeof(uniqueind)}, A::AbstractMPS, B::AbstractMPS; kwargs... +) + return [siteinds(f, A, B, j; kwargs...) for j in eachindex(A)] +end + +""" + siteinds(commoninds, A::MPO, B::MPS, j::Integer; kwargs...) + siteinds(commonind, A::MPO, B::MPO, j::Integer; kwargs...) + +Get the site index (or indices) of the `j`th MPO tensor of `A` that is shared with MPS/MPO `B`. +""" +function SiteTypes.siteinds( + f::Union{typeof(commoninds),typeof(commonind)}, + A::AbstractMPS, + B::AbstractMPS, + j::Integer; + kwargs..., +) + return f(A[j], B[j]; kwargs...) +end + +""" + siteinds(commoninds, A::MPO, B::MPS; kwargs...) + siteinds(commonind, A::MPO, B::MPO; kwargs...) + +Get a vector of the site index (or indices) of MPO `A` that is shared with MPS/MPO `B`. +""" +function SiteTypes.siteinds( + f::Union{typeof(commoninds),typeof(commonind)}, A::AbstractMPS, B::AbstractMPS; kwargs... +) + return [siteinds(f, A, B, j) for j in eachindex(A)] +end + +keys(ψ::AbstractMPS) = keys(data(ψ)) + +# +# Find sites of an MPS or MPO +# + +# TODO: accept a keyword argument sitedict that +# is a dictionary from the site indices to the site. +""" + findsite(M::Union{MPS, MPO}, is) + +Return the first site of the MPS or MPO that has at least one +Index in common with the Index or collection of indices `is`. + +To find all sites with common indices with `is`, use the +`findsites` function. + +# Examples +```julia +s = siteinds("S=1/2", 5) +ψ = random_mps(s) +findsite(ψ, s[3]) == 3 +findsite(ψ, (s[3], s[4])) == 3 + +M = MPO(s) +findsite(M, s[4]) == 4 +findsite(M, s[4]') == 4 +findsite(M, (s[4]', s[4])) == 4 +findsite(M, (s[4]', s[3])) == 3 +``` +""" +findsite(ψ::AbstractMPS, is) = findfirst(hascommoninds(is), ψ) + +findsite(ψ::AbstractMPS, s::Index) = findsite(ψ, IndexSet(s)) + +# +# TODO: Maybe make: +# findall(f::Function, siteindsM::Tuple{typeof(siteinds), ::AbstractMPS}) +# findall(siteindsM::Tuple{typeof(siteinds), <:AbstractMPS}, is) = +# findall(hassameinds(is), siteindsM) +# +""" + findsites(M::Union{MPS, MPO}, is) + +Return the sites of the MPS or MPO that have +indices in common with the collection of site indices +`is`. + +# Examples +```julia +s = siteinds("S=1/2", 5) +ψ = random_mps(s) +findsites(ψ, s[3]) == [3] +findsites(ψ, (s[4], s[1])) == [1, 4] + +M = MPO(s) +findsites(M, s[4]) == [4] +findsites(M, s[4]') == [4] +findsites(M, (s[4]', s[4])) == [4] +findsites(M, (s[4]', s[3])) == [3, 4] +``` +""" +findsites(ψ::AbstractMPS, is) = findall(hascommoninds(is), ψ) + +findsites(ψ::AbstractMPS, s::Index) = findsites(ψ, IndexSet(s)) + +# +# TODO: Maybe make: +# findfirst(f::Function, siteindsM::Tuple{typeof(siteinds), ::AbstractMPS}) +# findfirst(siteindsM::Tuple{typeof(siteinds), <:AbstractMPS}, is) = +# findfirst(hassameinds(is), siteindsM) +# +""" + findfirstsiteind(M::MPS, i::Index) + findfirstsiteind(M::MPO, i::Index) + +Return the first site of the MPS or MPO that has the +site index `i`. +""" +findfirstsiteind(ψ::AbstractMPS, s::Index) = findfirst(hasind(s), ψ) + +# TODO: depracate in favor of findsite. +""" + findfirstsiteinds(M::MPS, is) + findfirstsiteinds(M::MPO, is) + +Return the first site of the MPS or MPO that has the +site indices `is`. +""" +findfirstsiteinds(ψ::AbstractMPS, s) = findfirst(hasinds(s), ψ) + +""" + siteind(::typeof(first), M::Union{MPS,MPO}, j::Integer; kwargs...) + +Return the first site Index found on the MPS or MPO +(the first Index unique to the `j`th MPS/MPO tensor). + +You can choose different filters, like prime level +and tags, with the `kwargs`. +""" +function SiteTypes.siteind(::typeof(first), M::AbstractMPS, j::Integer; kwargs...) + N = length(M) + (N == 1) && return firstind(M[1]; kwargs...) + if j == 1 + si = uniqueind(M[j], M[j + 1]; kwargs...) + elseif j == N + si = uniqueind(M[j], M[j - 1]; kwargs...) + else + si = uniqueind(M[j], M[j - 1], M[j + 1]; kwargs...) + end + return si +end + +""" + siteinds(M::Union{MPS, MPO}}, j::Integer; kwargs...) + +Return the site Indices found of the MPO or MPO +at the site `j` as an IndexSet. + +Optionally filter prime tags and prime levels with +keyword arguments like `plev` and `tags`. +""" +function SiteTypes.siteinds(M::AbstractMPS, j::Integer; kwargs...) + N = length(M) + (N == 1) && return inds(M[1]; kwargs...) + if j == 1 + si = uniqueinds(M[j], M[j + 1]; kwargs...) + elseif j == N + si = uniqueinds(M[j], M[j - 1]; kwargs...) + else + si = uniqueinds(M[j], M[j - 1], M[j + 1]; kwargs...) + end + return si +end + +function SiteTypes.siteinds(::typeof(all), ψ::AbstractMPS, n::Integer; kwargs...) + return siteinds(ψ, n; kwargs...) +end + +function SiteTypes.siteinds(::typeof(first), ψ::AbstractMPS; kwargs...) + return [siteind(first, ψ, j; kwargs...) for j in 1:length(ψ)] +end + +function SiteTypes.siteinds(::typeof(only), ψ::AbstractMPS; kwargs...) + return [siteind(only, ψ, j; kwargs...) for j in 1:length(ψ)] +end + +function SiteTypes.siteinds(::typeof(all), ψ::AbstractMPS; kwargs...) + return [siteinds(ψ, j; kwargs...) for j in 1:length(ψ)] +end + +function replaceinds!(::typeof(linkinds), M::AbstractMPS, l̃s::Vector{<:Index}) + for i in eachindex(M)[1:(end - 1)] + l = linkind(M, i) + l̃ = l̃s[i] + if !isnothing(l) + @preserve_ortho M begin + M[i] = replaceinds(M[i], l => l̃) + M[i + 1] = replaceinds(M[i + 1], l => l̃) + end + end + end + return M +end + +function replaceinds(::typeof(linkinds), M::AbstractMPS, l̃s::Vector{<:Index}) + return replaceinds!(linkinds, copy(M), l̃s) +end + +# TODO: change kwarg from `set_limits` to `preserve_ortho` +function map!(f::Function, M::AbstractMPS; set_limits::Bool=true) + for i in eachindex(M) + M[i, set_limits=set_limits] = f(M[i]) + end + return M +end + +# TODO: change kwarg from `set_limits` to `preserve_ortho` +function map(f::Function, M::AbstractMPS; set_limits::Bool=true) + return map!(f, copy(M); set_limits=set_limits) +end + +for (fname, fname!) in [ + (:(ITensors.dag), :(dag!)), + (:(ITensors.prime), :(ITensors.prime!)), + (:(ITensors.setprime), :(ITensors.setprime!)), + (:(ITensors.noprime), :(ITensors.noprime!)), + (:(ITensors.swapprime), :(ITensors.swapprime!)), + (:(ITensors.replaceprime), :(ITensors.replaceprime!)), + (:(TagSets.addtags), :(ITensors.addtags!)), + (:(TagSets.removetags), :(ITensors.removetags!)), + (:(TagSets.replacetags), :(ITensors.replacetags!)), + (:(ITensors.settags), :(ITensors.settags!)), +] + @eval begin + """ + $($fname)[!](M::MPS, args...; kwargs...) + $($fname)[!](M::MPO, args...; kwargs...) + + Apply $($fname) to all ITensors of an MPS/MPO, returning a new MPS/MPO. + + The ITensors of the MPS/MPO will be a view of the storage of the original ITensors. Alternatively apply the function in-place. + """ + function $fname(M::AbstractMPS, args...; set_limits::Bool=false, kwargs...) + return map(m -> $fname(m, args...; kwargs...), M; set_limits=set_limits) + end + + function $(fname!)(M::AbstractMPS, args...; set_limits::Bool=false, kwargs...) + return map!(m -> $fname(m, args...; kwargs...), M; set_limits=set_limits) + end + end +end + +adjoint(M::AbstractMPS) = prime(M) + +function hascommoninds(::typeof(siteinds), A::AbstractMPS, B::AbstractMPS) + N = length(A) + for n in 1:N + !hascommoninds(siteinds(A, n), siteinds(B, n)) && return false + end + return true +end + +function check_hascommoninds(::typeof(siteinds), A::AbstractMPS, B::AbstractMPS) + N = length(A) + if length(B) ≠ N + throw( + DimensionMismatch( + "$(typeof(A)) and $(typeof(B)) have mismatched lengths $N and $(length(B))." + ), + ) + end + for n in 1:N + !hascommoninds(siteinds(A, n), siteinds(B, n)) && error( + "$(typeof(A)) A and $(typeof(B)) B must share site indices. On site $n, A has site indices $(siteinds(A, n)) while B has site indices $(siteinds(B, n)).", + ) + end + return nothing +end + +function map!(f::Function, ::typeof(linkinds), M::AbstractMPS) + for i in eachindex(M)[1:(end - 1)] + l = linkinds(M, i) + if !isempty(l) + l̃ = f(l) + @preserve_ortho M begin + M[i] = replaceinds(M[i], l, l̃) + M[i + 1] = replaceinds(M[i + 1], l, l̃) + end + end + end + return M +end + +map(f::Function, ::typeof(linkinds), M::AbstractMPS) = map!(f, linkinds, copy(M)) + +function map!(f::Function, ::typeof(siteinds), M::AbstractMPS) + for i in eachindex(M) + s = siteinds(M, i) + if !isempty(s) + @preserve_ortho M begin + M[i] = replaceinds(M[i], s, f(s)) + end + end + end + return M +end + +map(f::Function, ::typeof(siteinds), M::AbstractMPS) = map!(f, siteinds, copy(M)) + +function map!( + f::Function, ::typeof(siteinds), ::typeof(commoninds), M1::AbstractMPS, M2::AbstractMPS +) + length(M1) != length(M2) && error("MPOs/MPSs must be the same length") + for i in eachindex(M1) + s = siteinds(commoninds, M1, M2, i) + if !isempty(s) + s̃ = f(s) + @preserve_ortho (M1, M2) begin + M1[i] = replaceinds(M1[i], s .=> s̃) + M2[i] = replaceinds(M2[i], s .=> s̃) + end + end + end + return M1, M2 +end + +function map!( + f::Function, ::typeof(siteinds), ::typeof(uniqueinds), M1::AbstractMPS, M2::AbstractMPS +) + length(M1) != length(M2) && error("MPOs/MPSs must be the same length") + for i in eachindex(M1) + s = siteinds(uniqueinds, M1, M2, i) + if !isempty(s) + @preserve_ortho M1 begin + M1[i] = replaceinds(M1[i], s .=> f(s)) + end + end + end + return M1 +end + +function map( + f::Function, + ffilter::typeof(siteinds), + fsubset::Union{typeof(commoninds),typeof(uniqueinds)}, + M1::AbstractMPS, + M2::AbstractMPS, +) + return map!(f, ffilter, fsubset, copy(M1), copy(M2)) +end + +function hassameinds(::typeof(siteinds), M1::AbstractMPS, M2::AbstractMPS) + length(M1) ≠ length(M2) && return false + for n in 1:length(M1) + !hassameinds(siteinds(all, M1, n), siteinds(all, M2, n)) && return false + end + return true +end + +function hassamenuminds(::typeof(siteinds), M1::AbstractMPS, M2::AbstractMPS) + length(M1) ≠ length(M2) && return false + for n in 1:length(M1) + length(siteinds(M1, n)) ≠ length(siteinds(M2, n)) && return false + end + return true +end + +for (fname, fname!) in [ + (:(NDTensors.sim), :(sim!)), + (:(ITensors.prime), :(ITensors.prime!)), + (:(ITensors.setprime), :(ITensors.setprime!)), + (:(ITensors.noprime), :(ITensors.noprime!)), + (:(TagSets.addtags), :(ITensors.addtags!)), + (:(TagSets.removetags), :(ITensors.removetags!)), + (:(TagSets.replacetags), :(ITensors.replacetags!)), + (:(ITensors.settags), :(ITensors.settags!)), +] + @eval begin + """ + $($fname)[!](linkinds, M::MPS, args...; kwargs...) + $($fname)[!](linkinds, M::MPO, args...; kwargs...) + + Apply $($fname) to all link indices of an MPS/MPO, returning a new MPS/MPO. + + The ITensors of the MPS/MPO will be a view of the storage of the original ITensors. + """ + function $fname(ffilter::typeof(linkinds), M::AbstractMPS, args...; kwargs...) + return map(i -> $fname(i, args...; kwargs...), ffilter, M) + end + + function $(fname!)(ffilter::typeof(linkinds), M::AbstractMPS, args...; kwargs...) + return map!(i -> $fname(i, args...; kwargs...), ffilter, M) + end + + """ + $($fname)[!](siteinds, M::MPS, args...; kwargs...) + $($fname)[!](siteinds, M::MPO, args...; kwargs...) + + Apply $($fname) to all site indices of an MPS/MPO, returning a new MPS/MPO. + + The ITensors of the MPS/MPO will be a view of the storage of the original ITensors. + """ + function $fname(ffilter::typeof(siteinds), M::AbstractMPS, args...; kwargs...) + return map(i -> $fname(i, args...; kwargs...), ffilter, M) + end + + function $(fname!)(ffilter::typeof(siteinds), M::AbstractMPS, args...; kwargs...) + return map!(i -> $fname(i, args...; kwargs...), ffilter, M) + end + + """ + $($fname)[!](siteinds, commoninds, M1::MPO, M2::MPS, args...; kwargs...) + $($fname)[!](siteinds, commoninds, M1::MPO, M2::MPO, args...; kwargs...) + + Apply $($fname) to the site indices that are shared by `M1` and `M2`. + + Returns new MPSs/MPOs. The ITensors of the MPSs/MPOs will be a view of the storage of the original ITensors. + """ + function $fname( + ffilter::typeof(siteinds), + fsubset::typeof(commoninds), + M1::AbstractMPS, + M2::AbstractMPS, + args...; + kwargs..., + ) + return map(i -> $fname(i, args...; kwargs...), ffilter, fsubset, M1, M2) + end + + function $(fname!)( + ffilter::typeof(siteinds), + fsubset::typeof(commoninds), + M1::AbstractMPS, + M2::AbstractMPS, + args...; + kwargs..., + ) + return map!(i -> $fname(i, args...; kwargs...), ffilter, fsubset, M1, M2) + end + + """ + $($fname)[!](siteinds, uniqueinds, M1::MPO, M2::MPS, args...; kwargs...) + + Apply $($fname) to the site indices of `M1` that are not shared with `M2`. Returns new MPSs/MPOs. + + The ITensors of the MPSs/MPOs will be a view of the storage of the original ITensors. + """ + function $fname( + ffilter::typeof(siteinds), + fsubset::typeof(uniqueinds), + M1::AbstractMPS, + M2::AbstractMPS, + args...; + kwargs..., + ) + return map(i -> $fname(i, args...; kwargs...), ffilter, fsubset, M1, M2) + end + + function $(fname!)( + ffilter::typeof(siteinds), + fsubset::typeof(uniqueinds), + M1::AbstractMPS, + M2::AbstractMPS, + args...; + kwargs..., + ) + return map!(i -> $fname(i, args...; kwargs...), ffilter, fsubset, M1, M2) + end + end +end + +""" + maxlinkdim(M::MPS) + maxlinkdim(M::MPO) + +Get the maximum link dimension of the MPS or MPO. + +The minimum this will return is `1`, even if there +are no link indices. +""" +function maxlinkdim(M::AbstractMPS) + md = 1 + for b in eachindex(M)[1:(end - 1)] + l = linkind(M, b) + linkdim = isnothing(l) ? 1 : dim(l) + md = max(md, linkdim) + end + return md +end + +""" + linkdim(M::MPS, j::Integer) + linkdim(M::MPO, j::Integer) + +Get the dimension of the link or bond connecting the +MPS or MPO tensor on site j to site j+1. + +If there is no link Index, return `nothing`. +""" +function linkdim(ψ::AbstractMPS, b::Integer) + l = linkind(ψ, b) + isnothing(l) && return nothing + return dim(l) +end + +linkdims(ψ::AbstractMPS) = [linkdim(ψ, b) for b in 1:(length(ψ) - 1)] + +function inner_mps_mps_deprecation_warning() + return """ + Calling `inner(x::MPS, y::MPS)` where the site indices of the `MPS` `x` and `y` + don't match is deprecated as of ITensor v0.3 and will result in an error in ITensor +v0.4. Likely you are attempting to take the inner product of MPS that have site indices +with mismatched prime levels. The most common cause of this is something like the following: + + ```julia + s = siteinds("S=1/2") + psi = random_mps(s) + H = MPO(s, "Id") + Hpsi = contract(H, psi; cutoff=1e-8) # or `Hpsi = *(H, psi; cutoff=1e-8)` + inner(psi, Hpsi) + ``` + + `psi` has the Index structure `-s-(psi)` and `H` has the Index structure + `-s'-(H)-s-`, so the contraction follows as: `-s'-(H)-s-(psi) ≈ -s'-(Hpsi)`. + Then, the prime levels of `Hpsi` and `psi` don't match in `inner(psi, Hpsi)`. + + There are a few ways to fix this. You can simply change: + + ```julia + inner(psi, Hpsi) + ``` + + to: + + ```julia + inner(psi', Hpsi) + ``` + + in which case both `psi'` and `Hpsi` have primed site indices. Alternatively, + you can use the `apply` function instead of the `contract` function, which + calls `contract` and unprimes the resulting MPS: + + ```julia + Hpsi = apply(H, psi; cutoff=1e-8) # or `Hpsi = H(psi; cutoff=1e-8)` + inner(psi, Hpsi) + ``` + + Finally, if you only compute `Hpsi` to pass to the `inner` function, consider using: + + ```julia + inner(psi', H, psi) + ``` + + directly which is calculated exactly and is more efficient. Alternatively, you can use: + + ```julia + inner(psi, Apply(H, psi)) + ``` + + in which case `Apply(H, psi)` represents the "lazy" evaluation of + `apply(H, psi)` and internally calls something equivalent to `inner(psi', H, psi)`. + + Although the new behavior seems less convenient, it makes it easier to + generalize `inner(::MPS, ::MPS)` to other types of inputs, like `MPS` with + different tag and prime conventions, multiple sites per tensor, `ITensor` inputs, etc. + """ +end + +# Implement below, define here so it can be used in `deprecate_make_inds_match!`. +function _log_or_not_dot end + +function deprecate_make_inds_match!( + ::typeof(_log_or_not_dot), + M1dag::MPST, + M2::MPST, + loginner::Bool; + make_inds_match::Bool=true, +) where {MPST<:AbstractMPS} + siteindsM1dag = siteinds(all, M1dag) + siteindsM2 = siteinds(all, M2) + N = length(M2) + if any(n -> length(n) > 1, siteindsM1dag) || + any(n -> length(n) > 1, siteindsM2) || + !hassamenuminds(siteinds, M1dag, M2) + # If the MPS have more than one site Indices on any site or they don't have + # the same number of site indices on each site, don't try to make the + # indices match + if !hassameinds(siteinds, M1dag, M2) + n = findfirst(n -> !hassameinds(siteinds(M1dag, n), siteinds(M2, n)), 1:N) + error( + """Calling `dot(ϕ::MPS/MPO, ψ::MPS/MPO)` with multiple site indices per + MPS/MPO tensor but the site indices don't match. Even with `make_inds_match = true`, + the case of multiple site indices per MPS/MPO is not handled automatically. + The sites with unmatched site indices are: + + inds(ϕ[$n]) = $(inds(M1dag[n])) + + inds(ψ[$n]) = $(inds(M2[n])) + + Make sure the site indices of your MPO/MPS match. You may need to prime + one of the MPS, such as `dot(ϕ', ψ)`.""" + ) + end + make_inds_match = false + end + if !hassameinds(siteinds, M1dag, M2) && make_inds_match + ITensors.warn_once(inner_mps_mpo_mps_deprecation_warning(), :inner_mps_mps) + replace_siteinds!(M1dag, siteindsM2) + end + return M1dag, M2 +end + +function _log_or_not_dot( + M1::MPST, M2::MPST, loginner::Bool; make_inds_match::Bool=true +)::Number where {MPST<:AbstractMPS} + N = length(M1) + if length(M2) != N + throw(DimensionMismatch("inner: mismatched lengths $N and $(length(M2))")) + end + M1dag = dag(M1) + sim!(linkinds, M1dag) + M1dag, M2 = deprecate_make_inds_match!( + _log_or_not_dot, M1dag, M2, loginner; make_inds_match + ) + check_hascommoninds(siteinds, M1dag, M2) + O = M1dag[1] * M2[1] + + if loginner + normO = norm(O) + log_inner_tot = log(normO) + O ./= normO + end + + for j in eachindex(M1)[2:end] + O = (O * M1dag[j]) * M2[j] + + if loginner + normO = norm(O) + log_inner_tot += log(normO) + O ./= normO + end + end + + if loginner + if !isreal(O[]) || real(O[]) < 0 + log_inner_tot += log(complex(O[])) + end + return log_inner_tot + end + + dot_M1_M2 = O[] + + if !isfinite(dot_M1_M2) + @warn "The inner product (or norm²) you are computing is very large " * + "($dot_M1_M2). You should consider using `lognorm` or `loginner` instead, " * + "which will help avoid floating point errors. For example if you are trying " * + "to normalize your MPS/MPO `A`, the normalized MPS/MPO `B` would be given by " * + "`B = A ./ z` where `z = exp(lognorm(A) / length(A))`." + end + + return dot_M1_M2 +end + +""" + dot(A::MPS, B::MPS) + dot(A::MPO, B::MPO) + +Same as [`inner`](@ref). + +See also [`loginner`](@ref), [`logdot`](@ref). +""" +function LinearAlgebra.dot(M1::MPST, M2::MPST; kwargs...) where {MPST<:AbstractMPS} + return _log_or_not_dot(M1, M2, false; kwargs...) +end + +""" + logdot(A::MPS, B::MPS) + logdot(A::MPO, B::MPO) + +Same as [`loginner`](@ref). + +See also [`inner`](@ref), [`dot`](@ref). +""" +function logdot(M1::MPST, M2::MPST; kwargs...) where {MPST<:AbstractMPS} + return _log_or_not_dot(M1, M2, true; kwargs...) +end + +function make_inds_match_docstring_warning() + return """ + !!! compat "ITensors 0.3" + Before ITensors 0.3, `inner` had a keyword argument `make_inds_match` that default to `true`. + When true, the function attempted to make the site indices match before contracting. So for example, the + inputs could have different site indices, as long as they have the same dimensions or QN blocks. + This behavior was fragile since it only worked for MPS with single site indices per tensor, + and as of ITensors 0.3 has been deprecated. As of ITensors 0.3 you will need to make sure + the MPS or MPO you input have compatible site indices to contract over, such as by making + sure the prime levels match properly. + """ +end + +""" + inner(A::MPS, B::MPS) + inner(A::MPO, B::MPO) + +Compute the inner product `⟨A|B⟩`. If `A` and `B` are MPOs, computes the Frobenius inner product. + +Use [`loginner`](@ref) to avoid underflow/overflow for taking overlaps of large MPS or MPO. + +$(make_inds_match_docstring_warning()) + +Same as [`dot`](@ref). + +See also [`loginner`](@ref), [`logdot`](@ref). +""" +inner(M1::MPST, M2::MPST; kwargs...) where {MPST<:AbstractMPS} = dot(M1, M2; kwargs...) + +""" + loginner(A::MPS, B::MPS) + loginner(A::MPO, B::MPO) + +Compute the logarithm of the inner product `⟨A|B⟩`. If `A` and `B` are MPOs, computes the logarithm of the Frobenius inner product. + +This is useful for larger MPS/MPO, where in the limit of large numbers of sites the inner product can diverge or approach zero. + +$(make_inds_match_docstring_warning()) + +Same as [`logdot`](@ref). + +See also [`inner`](@ref), [`dot`](@ref). +""" +function loginner(M1::MPST, M2::MPST; kwargs...) where {MPST<:AbstractMPS} + return logdot(M1, M2; kwargs...) +end + +""" + norm(A::MPS) + norm(A::MPO) + +Compute the norm of the MPS or MPO. + +If the MPS or MPO has a well defined orthogonality center, this reduces to the +norm of the orthogonality center tensor. Otherwise, it computes the norm with +the full inner product of the MPS/MPO with itself. + +See also [`lognorm`](@ref). +""" +function norm(M::AbstractMPS) + if isortho(M) + return norm(M[orthocenter(M)]) + end + norm2_M = dot(M, M) + rtol = eps(real(scalartype(M))) * 10 + atol = rtol + if !IsApprox.isreal(norm2_M, Approx(; rtol=rtol, atol=atol)) + @warn "norm² is $norm2_M, which is not real up to a relative tolerance of " * + "$rtol and an absolute tolerance of $atol. Taking the real part, which may not be accurate." + end + return sqrt(real(norm2_M)) +end + +""" + lognorm(A::MPS) + lognorm(A::MPO) + +Compute the logarithm of the norm of the MPS or MPO. + +This is useful for larger MPS/MPO that are not gauged, where in the limit of +large numbers of sites the norm can diverge or approach zero. + +See also [`norm`](@ref), [`logdot`](@ref). +""" +function lognorm(M::AbstractMPS) + if isortho(M) + return log(norm(M[orthocenter(M)])) + end + lognorm2_M = logdot(M, M) + rtol = eps(real(scalartype(M))) * 10 + atol = rtol + if !IsApprox.isreal(lognorm2_M, Approx(; rtol=rtol, atol=atol)) + @warn "log(norm²) is $lognorm2_M, which is not real up to a relative tolerance " * + "of $rtol and an absolute tolerance of $atol. Taking the real part, which may not be accurate." + end + return real(lognorm2_M) / 2 +end + +function isapprox( + x::AbstractMPS, + y::AbstractMPS; + atol::Real=0, + rtol::Real=Base.rtoldefault( + LinearAlgebra.promote_leaf_eltypes(x), LinearAlgebra.promote_leaf_eltypes(y), atol + ), +) + d = norm(x - y) + if isfinite(d) + return d <= max(atol, rtol * max(norm(x), norm(y))) + else + error("In `isapprox(x::MPS, y::MPS)`, `norm(x - y)` is not finite") + end +end + +# copy an MPS/MPO, but do a deep copy of the tensors in the +# range of the orthogonality center. +function deepcopy_ortho_center(M::AbstractMPS) + M = copy(M) + c = ortho_lims(M) + # TODO: define `getindex(::AbstractMPS, I)` to return `AbstractMPS` + M[c] = deepcopy(typeof(M)(M[c])) + return M +end + +""" + normalize(A::MPS; (lognorm!)=[]) + normalize(A::MPO; (lognorm!)=[]) + +Return a new MPS or MPO `A` that is the same as the original MPS or MPO but with `norm(A) ≈ 1`. + +In practice, this evenly spreads `lognorm(A)` over the tensors within the range +of the orthogonality center to avoid numerical overflow in the case of diverging norms. + +See also [`normalize!`](@ref), [`norm`](@ref), [`lognorm`](@ref). +""" +function normalize(M::AbstractMPS; (lognorm!)=[]) + return normalize!(deepcopy_ortho_center(M); (lognorm!)=lognorm!) +end + +""" + normalize!(A::MPS; (lognorm!)=[]) + normalize!(A::MPO; (lognorm!)=[]) + +Change the MPS or MPO `A` in-place such that `norm(A) ≈ 1`. This modifies the +data of the tensors within the orthogonality center. + +In practice, this evenly spreads `lognorm(A)` over the tensors within the range +of the orthogonality center to avoid numerical overflow in the case of diverging norms. + +If the norm of the input MPS or MPO is 0, normalizing is ill-defined. In this +case, we just return the original MPS or MPO. You can check for this case as follows: + +```julia +s = siteinds("S=1/2", 4) +ψ = 0 * random_mps(s) +lognorm_ψ = [] +normalize!(ψ; (lognorm!)=lognorm_ψ) +lognorm_ψ[1] == -Inf # There was an infinite norm +``` + +See also [`normalize`](@ref), [`norm`](@ref), [`lognorm`](@ref). +""" +function LinearAlgebra.normalize!(M::AbstractMPS; (lognorm!)=[]) + c = ortho_lims(M) + lognorm_M = lognorm(M) + push!(lognorm!, lognorm_M) + if lognorm_M == -Inf + return M + end + z = exp(lognorm_M / length(c)) + # XXX: this is not modifying `M` in-place. + # M[c] ./= z + for n in c + M[n] ./= z + end + return M +end + +""" + dist(A::MPS, B::MPS) + dist(A::MPO, B::MPO) + +Compute the Euclidean distance between to MPS/MPO. Equivalent to `norm(A - B)` +but done more efficiently as: + +`sqrt(abs(inner(A, A) + inner(B, B) - 2 * real(inner(A, B))))`. + +Note that if the MPS/MPO are not normalized, the normalizations may diverge and + this may not be accurate. For those cases, likely it is best to use `norm(A - B)` + directly (or `lognorm(A - B)` if you expect the result may be very large). +""" +function dist(A::AbstractMPS, B::AbstractMPS) + return sqrt(abs(inner(A, A) + inner(B, B) - 2 * real(inner(A, B)))) +end + +function site_combiners(ψ::AbstractMPS) + N = length(ψ) + Cs = Vector{ITensor}(undef, N) + for n in 1:N + s = siteinds(all, ψ, n) + Cs[n] = combiner(s; tags=commontags(s)) + end + return Cs +end + +# The maximum link dimensions when adding MPS/MPO +function _add_maxlinkdims(ψ⃗::AbstractMPS...) + N = length(ψ⃗[1]) + maxdims = Vector{Int}(undef, N - 1) + for b in 1:(N - 1) + maxdims[b] = sum(ψ -> linkdim(ψ, b), ψ⃗) + end + return maxdims +end + +function +( + ::Algorithm"densitymatrix", ψ⃗::MPST...; cutoff=1e-15, kwargs... +) where {MPST<:AbstractMPS} + if !all(ψ -> hassameinds(siteinds, first(ψ⃗), ψ), ψ⃗) + error("In `+(::MPS/MPO...)`, the input `MPS` or `MPO` do not have the same site + indices. For example, the site indices of the first site are $(siteinds.(ψ⃗, 1))") + end + + Nₘₚₛ = length(ψ⃗) + + @assert all(ψᵢ -> length(ψ⃗[1]) == length(ψᵢ), ψ⃗) + + N = length(ψ⃗[1]) + + ψ⃗ = copy.(ψ⃗) + + X⃗ = site_combiners(ψ⃗[1]) + + for ψᵢ in ψ⃗ + @preserve_ortho ψᵢ ψᵢ .*= X⃗ + end + + ψ⃗ = convert.(MPS, ψ⃗) + + s = siteinds(ψ⃗[1]) + + ψ⃗ = orthogonalize.(ψ⃗, N) + + ψ = MPS(N) + + ρ⃗ₙ = [prime(ψᵢ[N], s[N]) * dag(ψᵢ[N]) for ψᵢ in ψ⃗] + ρₙ = sum(ρ⃗ₙ) + + # Maximum theoretical link dimensions + add_maxlinkdims = _add_maxlinkdims(ψ⃗...) + + C⃗ₙ = last.(ψ⃗) + for n in reverse(2:N) + Dₙ, Vₙ, spec = eigen( + ρₙ; + ishermitian=true, + tags=tags(linkind(ψ⃗[1], n - 1)), + cutoff=cutoff, + maxdim=add_maxlinkdims[n - 1], + kwargs..., + ) + lₙ₋₁ = commonind(Dₙ, Vₙ) + + # Update the total state + ψ[n] = Vₙ + + # Compute the new density matrix + C⃗ₙ₋₁ = [ψ⃗[i][n - 1] * C⃗ₙ[i] * dag(Vₙ) for i in 1:Nₘₚₛ] + C⃗ₙ₋₁′ = [prime(Cₙ₋₁, (s[n - 1], lₙ₋₁)) for Cₙ₋₁ in C⃗ₙ₋₁] + ρ⃗ₙ₋₁ = C⃗ₙ₋₁′ .* dag.(C⃗ₙ₋₁) + ρₙ₋₁ = sum(ρ⃗ₙ₋₁) + + C⃗ₙ = C⃗ₙ₋₁ + ρₙ = ρₙ₋₁ + end + + ψ[1] = sum(C⃗ₙ) + ψ .*= dag.(X⃗) + + set_ortho_lims!(ψ, 1:1) + + return convert(MPST, ψ) +end + +function +(::Algorithm"directsum", ψ⃗::MPST...) where {MPST<:AbstractMPS} + n = length(first(ψ⃗)) + @assert all(ψᵢ -> length(first(ψ⃗)) == length(ψᵢ), ψ⃗) + + # Output tensor + ϕ = MPST(n) + + # Direct sum first tensor + j = 1 + l⃗j = map(ψᵢ -> linkind(ψᵢ, j), ψ⃗) + ϕj, (lj,) = directsum( + (ψ⃗[i][j] => (l⃗j[i],) for i in 1:length(ψ⃗))...; tags=[tags(first(l⃗j))] + ) + ljm_prev = lj + ϕ[j] = ϕj + for j in 2:(n - 1) + l⃗jm = map(ψᵢ -> linkind(ψᵢ, j - 1), ψ⃗) + l⃗j = map(ψᵢ -> linkind(ψᵢ, j), ψ⃗) + ϕj, (ljm, lj) = directsum( + (ψ⃗[i][j] => (l⃗jm[i], l⃗j[i]) for i in 1:length(ψ⃗))...; + tags=[tags(first(l⃗jm)), tags(first(l⃗j))], + ) + ϕj = replaceind(ϕj, ljm => dag(ljm_prev)) + ljm_prev = lj + ϕ[j] = ϕj + end + j = n + l⃗jm = map(ψᵢ -> linkind(ψᵢ, j - 1), ψ⃗) + ϕj, (ljm,) = directsum( + (ψ⃗[i][j] => (l⃗jm[i],) for i in 1:length(ψ⃗))...; tags=[tags(first(l⃗jm))] + ) + ϕj = replaceind(ϕj, ljm => dag(ljm_prev)) + ϕ[j] = ϕj + return ϕ +end + +""" + +(A::MPS/MPO...; kwargs...) + add(A::MPS/MPO...; kwargs...) + +Add arbitrary numbers of MPS/MPO with each other, optionally truncating the results. + +A cutoff of 1e-15 is used by default, and in general users should set their own +cutoff for their particular application. + +# Keywords + +- `cutoff::Real`: singular value truncation cutoff +- `maxdim::Int`: maximum MPS/MPO bond dimension +- `alg = "densitymatrix"`: `"densitymatrix"` or `"directsum"`. `"densitymatrix"` adds the MPS/MPO + by adding up and diagoanlizing local density matrices site by site in a single + sweep through the system, truncating the density matrix with `cutoff` and `maxdim`. + `"directsum"` performs a direct sum of each tensors on each site of the input + MPS/MPO being summed. It doesn't perform any truncation, and therefore ignores + `cutoff` and `maxdim`. The bond dimension of the output is the sum of the bond + dimensions of the inputs. You can truncate the resulting MPS/MPO with the `truncate!` function. + +# Examples + +```julia +N = 10 + +s = siteinds("S=1/2", N; conserve_qns = true) + +state = n -> isodd(n) ? "↑" : "↓" +ψ₁ = random_mps(s, state; linkdims=2) +ψ₂ = random_mps(s, state; linkdims=2) +ψ₃ = random_mps(s, state; linkdims=2) + +ψ = +(ψ₁, ψ₂; cutoff = 1e-8) + +# Can use: +# +# ψ = ψ₁ + ψ₂ +# +# but generally you want to set a custom `cutoff` and `maxdim`. + +println() +@show inner(ψ, ψ) +@show inner(ψ₁, ψ₂) + inner(ψ₁, ψ₂) + inner(ψ₂, ψ₁) + inner(ψ₂, ψ₂) + +# Computes ψ₁ + 2ψ₂ +ψ = ψ₁ + 2ψ₂ + +println() +@show inner(ψ, ψ) +@show inner(ψ₁, ψ₁) + 2 * inner(ψ₁, ψ₂) + 2 * inner(ψ₂, ψ₁) + 4 * inner(ψ₂, ψ₂) + +# Computes ψ₁ + 2ψ₂ + ψ₃ +ψ = ψ₁ + 2ψ₂ + ψ₃ + +println() +@show inner(ψ, ψ) +@show inner(ψ₁, ψ₁) + 2 * inner(ψ₁, ψ₂) + inner(ψ₁, ψ₃) + + 2 * inner(ψ₂, ψ₁) + 4 * inner(ψ₂, ψ₂) + 2 * inner(ψ₂, ψ₃) + + inner(ψ₃, ψ₁) + 2 * inner(ψ₃, ψ₂) + inner(ψ₃, ψ₃) +``` +""" +function +(ψ⃗::AbstractMPS...; alg=Algorithm"densitymatrix"(), kwargs...) + return +(Algorithm(alg), ψ⃗...; kwargs...) +end + ++(ψ::AbstractMPS) = ψ + +add(ψ⃗::AbstractMPS...; kwargs...) = +(ψ⃗...; kwargs...) + +-(ψ₁::AbstractMPS, ψ₂::AbstractMPS; kwargs...) = +(ψ₁, -ψ₂; kwargs...) + +add(A::T, B::T; kwargs...) where {T<:AbstractMPS} = +(A, B; kwargs...) + +""" + sum(A::Vector{MPS}; kwargs...) + + sum(A::Vector{MPO}; kwargs...) + +Add multiple MPS/MPO with each other, with some optional +truncation. + +# Keywords + +- `cutoff::Real`: singular value truncation cutoff +- `maxdim::Int`: maximum MPS/MPO bond dimension +""" +function sum(ψ⃗::Vector{T}; kwargs...) where {T<:AbstractMPS} + iszero(length(ψ⃗)) && return T() + isone(length(ψ⃗)) && return copy(only(ψ⃗)) + return +(ψ⃗...; kwargs...) +end + +""" + orthogonalize!(M::MPS, j::Int; kwargs...) + orthogonalize(M::MPS, j::Int; kwargs...) + + orthogonalize!(M::MPO, j::Int; kwargs...) + orthogonalize(M::MPO, j::Int; kwargs...) + +Move the orthogonality center of the MPS +to site `j`. No observable property of the +MPS will be changed, and no truncation of the +bond indices is performed. Afterward, tensors +`1,2,...,j-1` will be left-orthogonal and tensors +`j+1,j+2,...,N` will be right-orthogonal. + +Either modify in-place with `orthogonalize!` or +out-of-place with `orthogonalize`. +""" +function orthogonalize!(M::AbstractMPS, j::Int; maxdim=nothing, normalize=nothing) + # TODO: Delete `maxdim` and `normalize` keyword arguments. + @debug_check begin + if !(1 <= j <= length(M)) + error("Input j=$j to `orthogonalize!` out of range (valid range = 1:$(length(M)))") + end + end + while leftlim(M) < (j - 1) + (leftlim(M) < 0) && setleftlim!(M, 0) + b = leftlim(M) + 1 + linds = uniqueinds(M[b], M[b + 1]) + lb = linkind(M, b) + if !isnothing(lb) + ltags = tags(lb) + else + ltags = TagSet("Link,l=$b") + end + L, R = factorize(M[b], linds; tags=ltags, maxdim) + M[b] = L + M[b + 1] *= R + setleftlim!(M, b) + if rightlim(M) < leftlim(M) + 2 + setrightlim!(M, leftlim(M) + 2) + end + end + + N = length(M) + + while rightlim(M) > (j + 1) + (rightlim(M) > (N + 1)) && setrightlim!(M, N + 1) + b = rightlim(M) - 2 + rinds = uniqueinds(M[b + 1], M[b]) + lb = linkind(M, b) + if !isnothing(lb) + ltags = tags(lb) + else + ltags = TagSet("Link,l=$b") + end + L, R = factorize(M[b + 1], rinds; tags=ltags, maxdim) + M[b + 1] = L + M[b] *= R + + setrightlim!(M, b + 1) + if leftlim(M) > rightlim(M) - 2 + setleftlim!(M, rightlim(M) - 2) + end + end + return M +end + +# Allows overloading `orthogonalize!` based on the projected +# MPO type. By default just calls `orthogonalize!` on the MPS. +function orthogonalize!(PH, M::AbstractMPS, j::Int; kwargs...) + return orthogonalize!(M, j; kwargs...) +end + +function orthogonalize(ψ0::AbstractMPS, args...; kwargs...) + ψ = copy(ψ0) + orthogonalize!(ψ, args...; kwargs...) + return ψ +end + +""" + truncate!(M::MPS; kwargs...) + truncate!(M::MPO; kwargs...) + +Perform a truncation of all bonds of an MPS/MPO, +using the truncation parameters (cutoff,maxdim, etc.) +provided as keyword arguments. + +Keyword arguments: +* `site_range`=1:N - only truncate the MPS bonds between these sites +""" +function truncate!(M::AbstractMPS; alg="frobenius", kwargs...) + return truncate!(Algorithm(alg), M; kwargs...) +end + +function truncate!( + ::Algorithm"frobenius", M::AbstractMPS; site_range=1:length(M), kwargs... +) + N = length(M) + + # Left-orthogonalize all tensors to make + # truncations controlled + orthogonalize!(M, last(site_range)) + + # Perform truncations in a right-to-left sweep + for j in reverse((first(site_range) + 1):last(site_range)) + rinds = uniqueinds(M[j], M[j - 1]) + ltags = tags(commonind(M[j], M[j - 1])) + U, S, V = svd(M[j], rinds; lefttags=ltags, kwargs...) + M[j] = U + M[j - 1] *= (S * V) + setrightlim!(M, j) + end + return M +end + +function truncate(ψ0::AbstractMPS; kwargs...) + ψ = copy(ψ0) + truncate!(ψ; kwargs...) + return ψ +end + +# Make `*` an alias for `contract` of two `AbstractMPS` +*(A::AbstractMPS, B::AbstractMPS; kwargs...) = contract(A, B; kwargs...) + +function _apply_to_orthocenter!(f, ψ::AbstractMPS, x) + limsψ = ortho_lims(ψ) + n = first(limsψ) + ψ[n] = f(ψ[n], x) + return ψ +end + +function _apply_to_orthocenter(f, ψ::AbstractMPS, x) + return _apply_to_orthocenter!(f, copy(ψ), x) +end + +""" + ψ::MPS/MPO * α::Number + +Scales the MPS or MPO by the provided number. + +Currently, this works by scaling one of the sites within the orthogonality limits. +""" +(ψ::AbstractMPS * α::Number) = _apply_to_orthocenter(*, ψ, α) + +""" + α::Number * ψ::MPS/MPO + +Scales the MPS or MPO by the provided number. + +Currently, this works by scaling one of the sites within the orthogonality limits. +""" +(α::Number * ψ::AbstractMPS) = ψ * α + +(ψ::AbstractMPS / α::Number) = _apply_to_orthocenter(/, ψ, α) + +-(ψ::AbstractMPS) = -1 * ψ + +LinearAlgebra.rmul!(ψ::AbstractMPS, α::Number) = _apply_to_orthocenter!(*, ψ, α) + +""" + setindex!(::Union{MPS, MPO}, ::Union{MPS, MPO}, + r::UnitRange{Int64}) + +Sets a contiguous range of MPS/MPO tensors +""" +function setindex!(ψ::MPST, ϕ::MPST, r::UnitRange{Int64}) where {MPST<:AbstractMPS} + @assert length(r) == length(ϕ) + # TODO: accept r::Union{AbstractRange{Int}, Vector{Int}} + # if r isa AbstractRange + # @assert step(r) = 1 + # else + # all(==(1), diff(r)) + # end + llim = leftlim(ψ) + rlim = rightlim(ψ) + for (j, n) in enumerate(r) + ψ[n] = ϕ[j] + end + if llim + 1 ≥ r[1] + setleftlim!(ψ, leftlim(ϕ) + r[1] - 1) + end + if rlim - 1 ≤ r[end] + setrightlim!(ψ, rightlim(ϕ) + r[1] - 1) + end + return ψ +end + +_isodd_fermionic_parity(s::Index, ::Integer) = false + +function _isodd_fermionic_parity(s::QNIndex, n::Integer) + qn_n = qn(space(s)[n]) + fermionic_qn_pos = findfirst(q -> isfermionic(q), qn_n) + isnothing(fermionic_qn_pos) && return false + return isodd(val(qn_n[fermionic_qn_pos])) +end + +function _fermionic_swap(s1::Index, s2::Index) + T = ITensor(QN(), s1', s2', dag(s1), dag(s2)) + for b in nzblocks(T) + dval = 1.0 + # Must be a diagonal block + ((b[1] ≠ b[3]) || (b[2] ≠ b[4])) && continue + n1, n2 = b[1], b[2] + if _isodd_fermionic_parity(s1, n1) && _isodd_fermionic_parity(s2, n2) + dval = -1.0 + end + Tb = ITensors.blockview(tensor(T), b) + mat_dim = prod(dims(Tb)[1:2]) + Tbr = reshape(Tb, mat_dim, mat_dim) + for i in diagind(Tbr) + NDTensors.setdiagindex!(Tbr, dval, i) + end + end + return T +end + +# TODO: add a version that determines the sites +# from common site indices of ψ and A +""" + setindex!(ψ::Union{MPS, MPO}, A::ITensor, r::UnitRange{Int}; + orthocenter::Int = last(r), perm = nothing, kwargs...) + replacesites!([...]) + replacesites([...]) + +Replace the sites in the range `r` with tensors made +from decomposing `A` into an MPS or MPO. + +The MPS or MPO must be orthogonalized such that +``` +firstsite ≤ ITensors.orthocenter(ψ) ≤ lastsite +``` + +Choose the new orthogonality center with `orthocenter`, which +should be within `r`. + +Optionally, permute the order of the sites with `perm`. +""" +function setindex!( + ψ::MPST, + A::ITensor, + r::UnitRange{Int}; + orthocenter::Integer=last(r), + perm=nothing, + kwargs..., +) where {MPST<:AbstractMPS} + # Replace the sites of ITensor ψ + # with the tensor A, splitting up A + # into MPS tensors + firstsite = first(r) + lastsite = last(r) + @assert firstsite ≤ ITensorMPS.orthocenter(ψ) ≤ lastsite + @assert firstsite ≤ leftlim(ψ) + 1 + @assert rightlim(ψ) - 1 ≤ lastsite + + # TODO: allow orthocenter outside of this + # range, and orthogonalize/truncate as needed + @assert firstsite ≤ orthocenter ≤ lastsite + + # Check that A has the proper common + # indices with ψ + lind = linkind(ψ, firstsite - 1) + rind = linkind(ψ, lastsite) + + sites = [siteinds(ψ, j) for j in firstsite:lastsite] + + #s = collect(Iterators.flatten(sites)) + indsA = filter(x -> !isnothing(x), [lind, Iterators.flatten(sites)..., rind]) + @assert hassameinds(A, indsA) + + # For MPO case, restrict to 0 prime level + #sites = filter(hasplev(0), sites) + + if !isnothing(perm) + sites0 = sites + sites = sites0[[perm...]] + # Check if the site indices + # are fermionic + if !using_auto_fermion() && any(ITensors.anyfermionic, sites) + if length(sites) == 2 && ψ isa MPS + if all(ITensors.allfermionic, sites) + s0 = Index.(sites0) + + # TODO: the Fermionic swap is could be diagonal, + # if we combine the site indices + #C = combiner(s0[1], s0[2]) + #c = combinedind(C) + #AC = A * C + #AC = noprime(AC * _fermionic_swap(c)) + #A = AC * dag(C) + + FSWAP = _fermionic_swap(s0[1], s0[2]) + A = noprime(A * FSWAP) + end + elseif ψ isa MPO + @warn "In setindex!(MPO, ::ITensor, ::UnitRange), " * + "fermionic signs are only not handled properly for non-trivial " * + "permutations of sites. Please inform the developers of ITensors " * + "if you require this feature (otherwise, fermionic signs can be " * + "put in manually with fermionic swap gates)." + else + @warn "In setindex!(::Union{MPS, MPO}, ::ITensor, ::UnitRange), " * + "fermionic signs are only handled properly for permutations involving 2 sites. " * + "The original sites are $sites0, with a permutation $perm. " * + "To have the fermion sign handled correctly, we recommend performing your permutation " * + "pairwise." + end + end + end + + ψA = MPST(A, sites; leftinds=lind, orthocenter=orthocenter - first(r) + 1, kwargs...) + #@assert prod(ψA) ≈ A + + ψ[firstsite:lastsite] = ψA + + return ψ +end + +function setindex!( + ψ::MPST, A::ITensor, r::UnitRange{Int}, args::Pair{Symbol}...; kwargs... +) where {MPST<:AbstractMPS} + return setindex!(ψ, A, r; args..., kwargs...) +end + +replacesites!(ψ::AbstractMPS, args...; kwargs...) = setindex!(ψ, args...; kwargs...) + +replacesites(ψ::AbstractMPS, args...; kwargs...) = setindex!(copy(ψ), args...; kwargs...) + +_number_inds(s::Index) = 1 +_number_inds(s::IndexSet) = length(s) +_number_inds(sites) = sum(_number_inds(s) for s in sites) + +""" + MPS(A::ITensor, sites; kwargs...) + MPO(A::ITensor, sites; kwargs...) + +Construct an MPS/MPO from an ITensor `A` by decomposing it site +by site according to the site indices `sites`. + +# Keywords + +- `leftinds = nothing`: optional left dangling indices. Indices that are not + in `sites` and `leftinds` will be dangling off of the right side of the MPS/MPO. +- `orthocenter::Integer = length(sites)`: the desired final orthogonality + center of the output MPS/MPO. +- `cutoff`: the desired truncation error at each link. +- `maxdim`: the maximum link dimension. +""" +function (::Type{MPST})( + A::ITensor, sites; leftinds=nothing, orthocenter::Integer=length(sites), kwargs... +) where {MPST<:AbstractMPS} + N = length(sites) + for s in sites + @assert hasinds(A, s) + end + @assert isnothing(leftinds) || hasinds(A, leftinds) + + @assert 1 ≤ orthocenter ≤ N + + ψ = Vector{ITensor}(undef, N) + Ã = A + l = leftinds + # TODO: To minimize work, loop from + # 1:orthocenter and reverse(orthocenter:N) + # so the orthogonality center is set correctly. + for n in 1:(N - 1) + Lis = IndexSet(sites[n]) + if !isnothing(l) + Lis = unioninds(Lis, l) + end + L, R = factorize(Ã, Lis; kwargs..., tags="Link,n=$n", ortho="left") + l = commonind(L, R) + ψ[n] = L + Ã = R + end + ψ[N] = Ã + M = MPST(ψ) + setleftlim!(M, N - 1) + setrightlim!(M, N + 1) + M = orthogonalize(M, orthocenter) + return M +end + +function (::Type{MPST})(A::AbstractArray, sites; kwargs...) where {MPST<:AbstractMPS} + return MPST(itensor(A, sites...), sites; kwargs...) +end + +""" + swapbondsites(ψ::Union{MPS, MPO}, b::Integer; kwargs...) + +Swap the sites `b` and `b+1`. +""" +function swapbondsites(ψ::AbstractMPS, b::Integer; ortho="right", kwargs...) + ψ = copy(ψ) + if ortho == "left" + orthocenter = b + 1 + elseif ortho == "right" + orthocenter = b + end + if leftlim(ψ) < b - 1 + ψ = orthogonalize(ψ, b) + elseif rightlim(ψ) > b + 2 + ψ = orthogonalize(ψ, b + 1) + end + ψ[b:(b + 1), orthocenter=orthocenter, perm=[2, 1], kwargs...] = ψ[b] * ψ[b + 1] + return ψ +end + +""" + movesite(::Union{MPS, MPO}, n1n2::Pair{Int, Int}) + +Create a new MPS/MPO where the site at `n1` is moved to `n2`, +for a pair `n1n2 = n1 => n2`. + +This is done with a series a pairwise swaps, and can introduce +a lot of entanglement into your state, so use with caution. +""" +function movesite( + ψ::AbstractMPS, n1n2::Pair{Int,Int}; orthocenter::Integer=last(n1n2), kwargs... +) + n1, n2 = n1n2 + n1 == n2 && return copy(ψ) + ψ = orthogonalize(ψ, n2) + r = n1:(n2 - 1) + ortho = "left" + if n1 > n2 + r = reverse(n2:(n1 - 1)) + ortho = "right" + end + for n in r + ψ = swapbondsites(ψ, n; ortho=ortho, kwargs...) + end + ψ = orthogonalize(ψ, orthocenter) + return ψ +end + +# Helper function for permuting a vector for the +# movesites function. +function _movesite(ns::Vector{Int}, n1n2::Pair{Int,Int}) + n1, n2 = n1n2 + n1 == n2 && return copy(ns) + r = n1:(n2 - 1) + if n1 > n2 + r = reverse(n2:(n1 - 1)) + end + for n in r + ns = replace(ns, n => n + 1, n + 1 => n) + end + return ns +end + +function _movesites(ψ::AbstractMPS, ns::Vector{Int}, ns′::Vector{Int}; kwargs...) + ψ = copy(ψ) + N = length(ns) + @assert N == length(ns′) + for i in 1:N + ψ = movesite(ψ, ns[i] => ns′[i]; kwargs...) + ns = _movesite(ns, ns[i] => ns′[i]) + end + return ψ, ns +end + +# TODO: make a permutesites(::MPS/MPO, perm) +# function that takes a permutation of the sites +# p(1:N) for N sites +function movesites(ψ::AbstractMPS, nsns′::Vector{Pair{Int,Int}}; kwargs...) + ns = first.(nsns′) + ns′ = last.(nsns′) + ψ = copy(ψ) + N = length(ns) + @assert N == length(ns′) + p = sortperm(ns′) + ns = ns[p] + ns′ = ns′[p] + ns = collect(ns) + while ns ≠ ns′ + ψ, ns = _movesites(ψ, ns, ns′; kwargs...) + end + return ψ +end + +# TODO: call the Vector{Pair{Int, Int}} version +function movesites(ψ::AbstractMPS, ns, ns′; kwargs...) + ψ = copy(ψ) + N = length(ns) + @assert N == length(ns′) + p = sortperm(ns′) + ns = ns[p] + ns′ = ns′[p] + ns = collect(ns) + for i in 1:N + ψ = movesite(ψ, ns[i] => ns′[i]; kwargs...) + ns = _movesite(ns, ns[i] => ns′[i]) + end + return ψ +end + +""" + apply(o::ITensor, ψ::Union{MPS, MPO}, [ns::Vector{Int}]; kwargs...) + product([...]) + +Get the product of the operator `o` with the MPS/MPO `ψ`, +where the operator is applied to the sites `ns`. If `ns` +are not specified, the sites are determined by the common indices +between `o` and the site indices of `ψ`. + +If `ns` are non-contiguous, the sites of the MPS are +moved to be contiguous. By default, the sites are moved +back to their original locations. You can leave them where +they are by setting the keyword argument `move_sites_back` +to false. + +# Keywords + +- `cutoff::Real`: singular value truncation cutoff. +- `maxdim::Int`: maximum MPS/MPO dimension. +- `apply_dag::Bool = false`: apply the gate and the dagger of the gate (only + relevant for MPO evolution). +- `move_sites_back::Bool = true`: after the ITensors are applied to the MPS or + MPO, move the sites of the MPS or MPO back to their original locations. +""" +function product( + o::ITensor, + ψ::AbstractMPS, + ns=findsites(ψ, o); + move_sites_back::Bool=true, + apply_dag::Bool=false, + kwargs..., +) + N = length(ns) + ns = sort(ns) + + # TODO: make this smarter by minimizing + # distance to orthogonalization. + # For example, if ITensors.orthocenter(ψ) > ns[end], + # set to ns[end]. + ψ = orthogonalize(ψ, ns[1]) + diff_ns = diff(ns) + ns′ = ns + if any(!=(1), diff_ns) + ns′ = [ns[1] + n - 1 for n in 1:N] + ψ = movesites(ψ, ns .=> ns′; kwargs...) + end + ϕ = ψ[ns′[1]] + for n in 2:N + ϕ *= ψ[ns′[n]] + end + ϕ = product(o, ϕ; apply_dag=apply_dag) + ψ[ns′[1]:ns′[end], kwargs...] = ϕ + if move_sites_back + # Move the sites back to their original positions + ψ = movesites(ψ, ns′ .=> ns; kwargs...) + end + return ψ +end + +""" + apply(As::Vector{<:ITensor}, M::Union{MPS, MPO}; kwargs...) + product([...]) + +Apply the ITensors `As` to the MPS or MPO `M`, treating them as gates or +matrices from pairs of prime or unprimed indices. + +# Keywords + +- `cutoff::Real`: singular value truncation cutoff. +- `maxdim::Int`: maximum MPS/MPO dimension. +- `apply_dag::Bool = false`: apply the gate and the dagger of the gate + (only relevant for MPO evolution). +- `move_sites_back::Bool = true`: after the ITensor is applied to the MPS or + MPO, move the sites of the MPS or MPO back to their original locations. + +# Examples + +Apply one-site gates to an MPS: + +```julia +N = 3 + +ITensors.op(::OpName"σx", ::SiteType"S=1/2", s::Index) = + 2*op("Sx", s) + +ITensors.op(::OpName"σz", ::SiteType"S=1/2", s::Index) = + 2*op("Sz", s) + +# Make the operator list. +os = [("σx", n) for n in 1:N] +append!(os, [("σz", n) for n in 1:N]) + +@show os + +s = siteinds("S=1/2", N) +gates = ops(os, s) + +# Starting state |↑↑↑⟩ +ψ0 = MPS(s, "↑") + +# Apply the gates. +ψ = apply(gates, ψ0; cutoff = 1e-15) + +# Test against exact (full) wavefunction +prodψ = apply(gates, prod(ψ0)) +@show prod(ψ) ≈ prodψ + +# The result is: +# σz₃ σz₂ σz₁ σx₃ σx₂ σx₁ |↑↑↑⟩ = -|↓↓↓⟩ +@show inner(ψ, MPS(s, "↓")) == -1 +``` + +Apply nonlocal two-site gates and one-site gates to an MPS: + +```julia +# 2-site gate +function ITensors.op(::OpName"CX", ::SiteType"S=1/2", s1::Index, s2::Index) + mat = [1 0 0 0 + 0 1 0 0 + 0 0 0 1 + 0 0 1 0] + return itensor(mat, s2', s1', s2, s1) +end + +os = [("CX", 1, 3), ("σz", 3)] + +@show os + +# Start with the state |↓↑↑⟩ +ψ0 = MPS(s, n -> n == 1 ? "↓" : "↑") + +# The result is: +# σz₃ CX₁₃ |↓↑↑⟩ = -|↓↑↓⟩ +ψ = apply(ops(os, s), ψ0; cutoff = 1e-15) +@show inner(ψ, MPS(s, n -> n == 1 || n == 3 ? "↓" : "↑")) == -1 +``` + +Perform TEBD-like time evolution: + +```julia +# Define the nearest neighbor term `S⋅S` for the Heisenberg model +function ITensors.op(::OpName"expS⋅S", ::SiteType"S=1/2", + s1::Index, s2::Index; τ::Number) + O = 0.5 * op("S+", s1) * op("S-", s2) + + 0.5 * op("S-", s1) * op("S+", s2) + + op("Sz", s1) * op("Sz", s2) + return exp(τ * O) +end + +τ = -0.1im +os = [("expS⋅S", (1, 2), (τ = τ,)), + ("expS⋅S", (2, 3), (τ = τ,))] +ψ0 = MPS(s, n -> n == 1 ? "↓" : "↑") +expτH = ops(os, s) +ψτ = apply(expτH, ψ0) +``` +""" +function product( + As::Vector{ITensor}, + ψ::AbstractMPS; + move_sites_back_between_gates::Bool=true, + move_sites_back::Bool=true, + kwargs..., +) + Aψ = ψ + for A in As + Aψ = product(A, Aψ; move_sites_back=move_sites_back_between_gates, kwargs...) + end + if !move_sites_back_between_gates && move_sites_back + s = siteinds(Aψ) + ns = 1:length(ψ) + ñs = [findsite(ψ, i) for i in s] + Aψ = movesites(Aψ, ns .=> ñs; kwargs...) + end + return Aψ +end + +# Apply in the reverse order for proper order of operations +# For example: +# +# s = siteinds("Qubit", 1) +# ψ = random_mps(s) +# +# # U = Z₁X₁ +# U = Prod{Op}() +# U = ("X", 1) * U +# U = ("Z", 1) * U +# +# # U|ψ⟩ = Z₁X₁|ψ⟩ +# apply(U, +function product(o::Prod{ITensor}, ψ::AbstractMPS; kwargs...) + return product(reverse(terms(o)), ψ; kwargs...) +end + +function (o::Prod{ITensor})(ψ::AbstractMPS; kwargs...) + return apply(o, ψ; kwargs...) +end + +# +# QN functions +# + +""" + hasqns(M::MPS) + + hasqns(M::MPO) + +Return true if the MPS or MPO has +tensors which carry quantum numbers. +""" +hasqns(M::AbstractMPS) = any(hasqns, data(M)) + +# Trait type version of hasqns +# Note this is not inferrable, so hasqns would be preferred +symmetrystyle(M::AbstractMPS) = symmetrystyle(data(M)) + +""" + flux(M::MPS) + + flux(M::MPO) + + totalqn(M::MPS) + + totalqn(M::MPO) + +For an MPS or MPO which conserves quantum +numbers, compute the total QN flux. For +a tensor network such as an MPS or MPO, +the flux is the sum of fluxes of each of +the tensors in the network. The name +`totalqn` is an alias for `flux`. +""" +function flux(M::AbstractMPS) + hasqns(M) || return nothing + q = QN() + for j in (M.llim + 1):(M.rlim - 1) + q += flux(M[j]) + end + return q +end + +totalqn(M::AbstractMPS) = flux(M) + +function checkflux(M::AbstractMPS) + for m in M + checkflux(m) + end + return nothing +end + +""" + splitblocks[!](::typeof(linkinds), M::AbstractMPS; tol = 0) + +Split the QN blocks of the links of the MPS or MPO into dimension 1 blocks. +Then, only keep the blocks with `norm(b) > tol`. + +This can make the ITensors of the MPS/MPO more sparse, and is particularly +helpful as a preprocessing step on a local Hamiltonian MPO for DMRG. +""" +function splitblocks!(::typeof(linkinds), M::AbstractMPS; tol=0) + for i in eachindex(M)[1:(end - 1)] + l = linkind(M, i) + if !isnothing(l) + @preserve_ortho M begin + M[i] = splitblocks(M[i], l) + M[i + 1] = splitblocks(M[i + 1], l) + end + end + end + return M +end + +function splitblocks(::typeof(linkinds), M::AbstractMPS; tol=0) + return splitblocks!(linkinds, copy(M); tol=0) +end + +removeqns(M::AbstractMPS) = map(removeqns, M; set_limits=false) +function QuantumNumbers.removeqn(M::AbstractMPS, qn_name::String) + return map(m -> removeqn(m, qn_name), M; set_limits=false) +end + +# +# Broadcasting +# + +BroadcastStyle(MPST::Type{<:AbstractMPS}) = Style{MPST}() +function BroadcastStyle(::Style{MPST}, ::DefaultArrayStyle{N}) where {N,MPST<:AbstractMPS} + return Style{MPST}() +end + +broadcastable(ψ::AbstractMPS) = ψ +function copyto!(ψ::AbstractMPS, b::Broadcasted) + copyto!(data(ψ), b) + # In general, we assume the broadcast operation + # will mess up the orthogonality + # TODO: special case for `prime`, `settags`, etc. + reset_ortho_lims!(ψ) + return ψ +end + +function Base.similar(bc::Broadcasted{Style{MPST}}, ElType::Type) where {MPST<:AbstractMPS} + return similar(Array{ElType}, axes(bc)) +end + +function Base.similar( + bc::Broadcasted{Style{MPST}}, ::Type{ITensor} +) where {MPST<:AbstractMPS} + # In general, we assume the broadcast operation + # will mess up the orthogonality so we use + # a generic constructor where we don't specify + # the orthogonality limits. + return MPST(similar(Array{ITensor}, axes(bc))) +end + +# +# Printing functions +# + +function Base.show(io::IO, M::AbstractMPS) + print(io, "$(typeof(M))") + (length(M) > 0) && print(io, "\n") + for i in eachindex(M) + if !isassigned(M, i) + println(io, "#undef") + else + A = M[i] + if order(A) != 0 + println(io, "[$i] $(inds(A))") + else + println(io, "[$i] ITensor()") + end + end + end +end diff --git a/src/abstractprojmpo/abstractprojmpo.jl b/src/abstractprojmpo/abstractprojmpo.jl new file mode 100644 index 0000000..05aae2d --- /dev/null +++ b/src/abstractprojmpo/abstractprojmpo.jl @@ -0,0 +1,251 @@ +abstract type AbstractProjMPO end + +copy(::AbstractProjMPO) = error("Not implemented") + +""" + nsite(P::ProjMPO) + +Retrieve the number of unprojected (open) +site indices of the ProjMPO object `P` +""" +nsite(P::AbstractProjMPO) = P.nsite + +set_nsite!(::AbstractProjMPO, nsite) = error("Not implemented") + +# The range of center sites +site_range(P::AbstractProjMPO) = (P.lpos + 1):(P.rpos - 1) + +""" + length(P::ProjMPO) + +The length of a ProjMPO is the same as +the length of the MPO used to construct it +""" +Base.length(P::AbstractProjMPO) = length(P.H) + +function lproj(P::AbstractProjMPO)::Union{ITensor,OneITensor} + (P.lpos <= 0) && return OneITensor() + return P.LR[P.lpos] +end + +function rproj(P::AbstractProjMPO)::Union{ITensor,OneITensor} + (P.rpos >= length(P) + 1) && return OneITensor() + return P.LR[P.rpos] +end + +function ITensors.contract(P::AbstractProjMPO, v::ITensor)::ITensor + itensor_map = Union{ITensor,OneITensor}[lproj(P)] + append!(itensor_map, P.H[site_range(P)]) + push!(itensor_map, rproj(P)) + + # Reverse the contraction order of the map if + # the first tensor is a scalar (for example we + # are at the left edge of the system) + if dim(first(itensor_map)) == 1 + reverse!(itensor_map) + end + + # Apply the map + Hv = v + for it in itensor_map + Hv *= it + end + return Hv +end + +""" + product(P::ProjMPO,v::ITensor)::ITensor + + (P::ProjMPO)(v::ITensor) + +Efficiently multiply the ProjMPO `P` +by an ITensor `v` in the sense that the +ProjMPO is a generalized square matrix +or linear operator and `v` is a generalized +vector in the space where it acts. The +returned ITensor will have the same indices +as `v`. The operator overload `P(v)` is +shorthand for `product(P,v)`. +""" +function product(P::AbstractProjMPO, v::ITensor)::ITensor + Pv = contract(P, v) + if order(Pv) != order(v) + error( + string( + "The order of the ProjMPO-ITensor product P*v is not equal to the order of the ITensor v, ", + "this is probably due to an index mismatch.\nCommon reasons for this error: \n", + "(1) You are trying to multiply the ProjMPO with the $(nsite(P))-site wave-function at the wrong position.\n", + "(2) `orthogonalize!` was called, changing the MPS without updating the ProjMPO.\n\n", + "P*v inds: $(inds(Pv)) \n\n", + "v inds: $(inds(v))", + ), + ) + end + return noprime(Pv) +end + +(P::AbstractProjMPO)(v::ITensor) = product(P, v) + +""" + eltype(P::ProjMPO) + +Deduce the element type (such as Float64 +or ComplexF64) of the tensors in the ProjMPO +`P`. +""" +function Base.eltype(P::AbstractProjMPO)::Type + ElType = eltype(lproj(P)) + for j in site_range(P) + ElType = promote_type(ElType, eltype(P.H[j])) + end + return promote_type(ElType, eltype(rproj(P))) +end + +""" + size(P::ProjMPO) + +The size of a ProjMPO are its dimensions +`(d,d)` when viewed as a matrix or linear operator +acting on a space of dimension `d`. + +For example, if a ProjMPO maps from a space with +indices `(a,s1,s2,b)` to the space `(a',s1',s2',b')` +then the size is `(d,d)` where +`d = dim(a)*dim(s1)*dim(s1)*dim(b)` +""" +function Base.size(P::AbstractProjMPO)::Tuple{Int,Int} + d = 1 + for i in inds(lproj(P)) + plev(i) > 0 && (d *= dim(i)) + end + for j in site_range(P) + for i in inds(P.H[j]) + plev(i) > 0 && (d *= dim(i)) + end + end + for i in inds(rproj(P)) + plev(i) > 0 && (d *= dim(i)) + end + return (d, d) +end + +function _makeL!(P::AbstractProjMPO, psi::MPS, k::Int)::Union{ITensor,Nothing} + # Save the last `L` that is made to help with caching + # for DiskProjMPO + ll = P.lpos + if ll ≥ k + # Special case when nothing has to be done. + # Still need to change the position if lproj is + # being moved backward. + P.lpos = k + return nothing + end + # Make sure ll is at least 0 for the generic logic below + ll = max(ll, 0) + L = lproj(P) + while ll < k + L = L * psi[ll + 1] * P.H[ll + 1] * dag(prime(psi[ll + 1])) + P.LR[ll + 1] = L + ll += 1 + end + # Needed when moving lproj backward. + P.lpos = k + return L +end + +function makeL!(P::AbstractProjMPO, psi::MPS, k::Int) + _makeL!(P, psi, k) + return P +end + +function _makeR!(P::AbstractProjMPO, psi::MPS, k::Int)::Union{ITensor,Nothing} + # Save the last `R` that is made to help with caching + # for DiskProjMPO + rl = P.rpos + if rl ≤ k + # Special case when nothing has to be done. + # Still need to change the position if rproj is + # being moved backward. + P.rpos = k + return nothing + end + N = length(P.H) + # Make sure rl is no bigger than `N + 1` for the generic logic below + rl = min(rl, N + 1) + R = rproj(P) + while rl > k + R = R * psi[rl - 1] * P.H[rl - 1] * dag(prime(psi[rl - 1])) + P.LR[rl - 1] = R + rl -= 1 + end + P.rpos = k + return R +end + +function makeR!(P::AbstractProjMPO, psi::MPS, k::Int) + _makeR!(P, psi, k) + return P +end + +""" + position!(P::ProjMPO, psi::MPS, pos::Int) + +Given an MPS `psi`, shift the projection of the +MPO represented by the ProjMPO `P` such that +the set of unprojected sites begins with site `pos`. +This operation efficiently reuses previous projections +of the MPO on sites that have already been projected. +The MPS `psi` must have compatible bond indices with +the previous projected MPO tensors for this +operation to succeed. +""" +function position!(P::AbstractProjMPO, psi::MPS, pos::Int) + makeL!(P, psi, pos - 1) + makeR!(P, psi, pos + nsite(P)) + return P +end + +""" + noiseterm(P::ProjMPO, + phi::ITensor, + ortho::String) + +Return a "noise term" or density matrix perturbation +ITensor as proposed in Phys. Rev. B 72, 180403 for aiding +convergence of DMRG calculations. The ITensor `phi` +is the contracted product of MPS tensors acted on by the +ProjMPO `P`, and `ortho` is a String which can take +the values `"left"` or `"right"` depending on the +sweeping direction of the DMRG calculation. +""" +function noiseterm(P::AbstractProjMPO, phi::ITensor, ortho::String)::ITensor + if nsite(P) != 2 + error("noise term only defined for 2-site ProjMPO") + end + + site_range_P = site_range(P) + if ortho == "left" + AL = P.H[first(site_range_P)] + AL = lproj(P) * AL + nt = AL * phi + elseif ortho == "right" + AR = P.H[last(site_range_P)] + AR = AR * rproj(P) + nt = phi * AR + else + error("In noiseterm, got ortho = $ortho, only supports `left` and `right`") + end + nt = nt * dag(noprime(nt)) + + return nt +end + +function checkflux(P::AbstractProjMPO) + checkflux(P.H) + for n in length(P.LR) + if isassigned(P.LR, n) + checkflux(P.LR[n]) + end + end + return nothing +end diff --git a/src/abstractprojmpo/diskprojmpo.jl b/src/abstractprojmpo/diskprojmpo.jl new file mode 100644 index 0000000..dd9d1df --- /dev/null +++ b/src/abstractprojmpo/diskprojmpo.jl @@ -0,0 +1,128 @@ + +""" +A DiskProjMPO computes and stores the projection of an +MPO into a basis defined by an MPS, leaving a +certain number of site indices of the MPO unprojected. +Which sites are unprojected can be shifted by calling +the `position!` method. + +Drawing of the network represented by a ProjMPO `P(H)`, +showing the case of `nsite(P)==2` and `position!(P,psi,4)` +for an MPS `psi`: + +``` +o--o--o- -o--o--o--o--o--o +``` + +The environment tensors are stored on disk, which is helpful +for large bond dimensions if they cannot fit in memory. +""" +mutable struct DiskProjMPO <: AbstractProjMPO + lpos::Int + rpos::Int + nsite::Int + H::MPO + LR::DiskVector{ITensor} + Lcache::Union{ITensor,OneITensor} + lposcache::Union{Int,Nothing} + Rcache::Union{ITensor,OneITensor} + rposcache::Union{Int,Nothing} +end + +function copy(P::DiskProjMPO) + return DiskProjMPO( + P.lpos, + P.rpos, + P.nsite, + copy(P.H), + copy(P.LR), + P.Lcache, + P.lposcache, + P.Rcache, + P.rposcache, + ) +end + +function set_nsite!(P::DiskProjMPO, nsite) + P.nsite = nsite + return P +end + +function DiskProjMPO(H::MPO) + return new( + 0, + length(H) + 1, + 2, + H, + disk(Vector{ITensor}(undef, length(H))), + OneITensor, + nothing, + OneITensor, + nothing, + ) +end + +function disk(pm::ProjMPO; kwargs...) + return DiskProjMPO( + pm.lpos, + pm.rpos, + pm.nsite, + pm.H, + disk(pm.LR; kwargs...), + lproj(pm), + pm.lpos, + rproj(pm), + pm.rpos, + ) +end +disk(pm::DiskProjMPO; kwargs...) = pm + +# Special overload of lproj which uses the cached +# version of the left projected MPO, and if the +# cache doesn't exist it loads it from disk. +function lproj(P::DiskProjMPO)::Union{ITensor,OneITensor} + (P.lpos <= 0) && return OneITensor() + if (P.lpos ≠ P.lposcache) || (P.lpos == 1) + # Need to update the cache + P.Lcache = P.LR[P.lpos] + P.lposcache = P.lpos + end + return P.Lcache +end + +# Special overload of rproj which uses the cached +# version of the right projected MPO, and if the +# cache doesn't exist it loads it from disk. +function rproj(P::DiskProjMPO)::Union{ITensor,OneITensor} + (P.rpos >= length(P) + 1) && return OneITensor() + if (P.rpos ≠ P.rposcache) || (P.rpos == length(P)) + # Need to update the cache + P.Rcache = P.LR[P.rpos] + P.rposcache = P.rpos + end + return P.Rcache +end + +function makeL!(P::DiskProjMPO, psi::MPS, k::Int) + L = _makeL!(P, psi, k) + if !isnothing(L) + # Cache the result + P.Lcache = L + P.lposcache = P.lpos + end + return P +end + +function makeR!(P::DiskProjMPO, psi::MPS, k::Int) + R = _makeR!(P, psi, k) + if !isnothing(R) + # Cache the result + P.Rcache = R + P.rposcache = P.rpos + end + return P +end diff --git a/src/abstractprojmpo/projmpo.jl b/src/abstractprojmpo/projmpo.jl new file mode 100644 index 0000000..d8c2a84 --- /dev/null +++ b/src/abstractprojmpo/projmpo.jl @@ -0,0 +1,35 @@ + +""" +A ProjMPO computes and stores the projection of an +MPO into a basis defined by an MPS, leaving a +certain number of site indices of the MPO unprojected. +Which sites are unprojected can be shifted by calling +the `position!` method. + +Drawing of the network represented by a ProjMPO `P(H)`, +showing the case of `nsite(P)==2` and `position!(P,psi,4)` +for an MPS `psi`: + +``` +o--o--o- -o--o--o--o--o--o +``` +""" +mutable struct ProjMPO <: AbstractProjMPO + lpos::Int + rpos::Int + nsite::Int + H::MPO + LR::Vector{ITensor} +end +ProjMPO(H::MPO) = ProjMPO(0, length(H) + 1, 2, H, Vector{ITensor}(undef, length(H))) + +copy(P::ProjMPO) = ProjMPO(P.lpos, P.rpos, P.nsite, copy(P.H), copy(P.LR)) + +function set_nsite!(P::ProjMPO, nsite) + P.nsite = nsite + return P +end diff --git a/src/abstractprojmpo/projmpo_mps.jl b/src/abstractprojmpo/projmpo_mps.jl new file mode 100644 index 0000000..c4c0ca7 --- /dev/null +++ b/src/abstractprojmpo/projmpo_mps.jl @@ -0,0 +1,67 @@ +mutable struct ProjMPO_MPS + PH::ProjMPO + pm::Vector{ProjMPS} + weight::Float64 +end + +copy(P::ProjMPO_MPS) = ProjMPO_MPS(copy(P.PH), copy.(P.pm), P.weight) + +function ProjMPO_MPS(H::MPO, mpsv::Vector{MPS}; weight=1.0) + return ProjMPO_MPS(ProjMPO(H), [ProjMPS(m) for m in mpsv], weight) +end + +ProjMPO_MPS(H::MPO, Ms::MPS...; weight=1.0) = ProjMPO_MPS(H, [Ms...], weight) + +nsite(P::ProjMPO_MPS) = nsite(P.PH) + +function set_nsite!(Ps::ProjMPO_MPS, nsite) + set_nsite!(Ps.PH, nsite) + for P in Ps.pm + set_nsite!(P, nsite) + end + return Ps +end + +Base.length(P::ProjMPO_MPS) = length(P.PH) + +function site_range(P::ProjMPO_MPS) + r = site_range(P.PH) + @assert all(m -> site_range(m) == r, P.pm) + return r +end + +function product(P::ProjMPO_MPS, v::ITensor)::ITensor + Pv = product(P.PH, v) + for p in P.pm + Pv += P.weight * product(p, v) + end + return Pv +end + +function Base.eltype(P::ProjMPO_MPS) + elT = eltype(P.PH) + for p in P.pm + elT = promote_type(elT, eltype(p)) + end + return elT +end + +(P::ProjMPO_MPS)(v::ITensor) = product(P, v) + +Base.size(P::ProjMPO_MPS) = size(P.H) + +function position!(P::ProjMPO_MPS, psi::MPS, pos::Int) + position!(P.PH, psi, pos) + for p in P.pm + position!(p, psi, pos) + end + return P +end + +noiseterm(P::ProjMPO_MPS, phi::ITensor, dir::String) = noiseterm(P.PH, phi, dir) + +function checkflux(P::ProjMPO_MPS) + checkflux(P.PH) + foreach(checkflux, P.pm) + return nothing +end diff --git a/src/abstractprojmpo/projmposum.jl b/src/abstractprojmpo/projmposum.jl new file mode 100644 index 0000000..3d5d5d9 --- /dev/null +++ b/src/abstractprojmpo/projmposum.jl @@ -0,0 +1,167 @@ +using Compat: allequal + +abstract type AbstractSum end + +terms(sum::AbstractSum) = sum.terms + +function set_terms(sum::AbstractSum, terms) + return error("Please implement `set_terms` for the `AbstractSum` type `$(typeof(sum))`.") +end + +copy(P::AbstractSum) = typeof(P)(copy.(terms(P))) + +function nsite(P::AbstractSum) + @assert allequal(nsite.(terms(P))) + return nsite(first(terms(P))) +end + +function set_nsite!(A::AbstractSum, nsite) + return set_terms(A, map(term -> set_nsite!(term, nsite), terms(A))) +end + +function length(A::AbstractSum) + @assert allequal(length.(terms(A))) + return length(first(terms(A))) +end + +function site_range(A::AbstractSum) + @assert allequal(Iterators.map(site_range, terms(A))) + return site_range(first(terms(A))) +end + +""" + product(P::ProjMPOSum,v::ITensor) + + (P::ProjMPOSum)(v::ITensor) + +Efficiently multiply the ProjMPOSum `P` +by an ITensor `v` in the sense that the +ProjMPOSum is a generalized square matrix +or linear operator and `v` is a generalized +vector in the space where it acts. The +returned ITensor will have the same indices +as `v`. The operator overload `P(v)` is +shorthand for `product(P,v)`. +""" +product(A::AbstractSum, v::ITensor) = sum(t -> product(t, v), terms(A)) + +ITensors.contract(A::AbstractSum, v::ITensor) = sum(t -> contract(t, v), terms(A)) + +""" + eltype(P::ProjMPOSum) + +Deduce the element type (such as Float64 +or ComplexF64) of the tensors in the ProjMPOSum +`P`. +""" +eltype(A::AbstractSum) = mapreduce(eltype, promote_type, terms(A)) + +(A::AbstractSum)(v::ITensor) = product(A, v) + +""" + size(P::ProjMPOSum) + +The size of a ProjMPOSum are its dimensions +`(d,d)` when viewed as a matrix or linear operator +acting on a space of dimension `d`. + +For example, if a ProjMPOSum maps from a space with +indices `(a,s1,s2,b)` to the space `(a',s1',s2',b')` +then the size is `(d,d)` where +`d = dim(a)*dim(s1)*dim(s1)*dim(b)` +""" +function size(A::AbstractSum) + @assert allequal(size.(terms(A))) + return size(first(terms(A))) +end + +""" + position!(P::ProjMPOSum, psi::MPS, pos::Int) + +Given an MPS `psi`, shift the projection of the +MPO represented by the ProjMPOSum `P` such that +the set of unprojected sites begins with site `pos`. +This operation efficiently reuses previous projections +of the MPOs on sites that have already been projected. +The MPS `psi` must have compatible bond indices with +the previous projected MPO tensors for this +operation to succeed. +""" +function position!(A::AbstractSum, psi::MPS, pos::Int) + new_terms = map(term -> position!(term, psi, pos), terms(A)) + return set_terms(A, new_terms) +end + +""" + noiseterm(P::ProjMPOSum, + phi::ITensor, + ortho::String) + +Return a "noise term" or density matrix perturbation +ITensor as proposed in Phys. Rev. B 72, 180403 for aiding +convergence of DMRG calculations. The ITensor `phi` +is the contracted product of MPS tensors acted on by the +ProjMPOSum `P`, and `ortho` is a String which can take +the values `"left"` or `"right"` depending on the +sweeping direction of the DMRG calculation. +""" +function noiseterm(A::AbstractSum, phi::ITensor, dir::String) + return sum(t -> noiseterm(t, phi, dir), terms(A)) +end + +""" + disk(ps::AbstractSum; kwargs...) + +Call `disk` on each term of an AbstractSum, to enable +saving of cached data to hard disk. +""" +function disk(sum::AbstractSum; disk_kwargs...) + return set_terms(sum, map(t -> disk(t; disk_kwargs...), terms(sum))) +end + +function checkflux(sum::AbstractSum) + foreach(checkflux, terms(sum)) + return nothing +end + +# +# Definition of concrete, generic SequentialSum type +# + +struct SequentialSum{T} <: AbstractSum + terms::Vector{T} +end + +function SequentialSum{T}(mpos::Vector{MPO}) where {T<:AbstractProjMPO} + return SequentialSum([T(M) for M in mpos]) +end + +SequentialSum{T}(Ms::MPO...) where {T<:AbstractProjMPO} = SequentialSum{T}([Ms...]) + +set_terms(sum::SequentialSum, terms) = SequentialSum(terms) + +""" +A ProjMPOSum computes and stores the projection of an +implied sum of MPOs into a basis defined by an MPS, +leaving a certain number of site indices of each MPO +unprojected. Which sites are unprojected can be shifted +by calling the `position!` method. The MPOs used as +input to a ProjMPOSum are *not* added together beforehand; +instead when the `product` method of a ProjMPOSum is invoked, +each projected MPO in the set of MPOs is multiplied by +the input tensor one-by-one in an efficient way. + +Drawing of the network represented by a ProjMPOSum +`P([H1,H2,...])`, showing the case of `nsite(P)==2` +and `position!(P,psi,4)` for an MPS `psi` (note the +sum Σⱼ on the left): + +``` + o--o--o- -o--o--o--o--o--o +``` +""" +const ProjMPOSum = SequentialSum{ProjMPO} diff --git a/src/abstractprojmpo/projmps.jl b/src/abstractprojmpo/projmps.jl new file mode 100644 index 0000000..0ad60a5 --- /dev/null +++ b/src/abstractprojmpo/projmps.jl @@ -0,0 +1,130 @@ + +mutable struct ProjMPS + lpos::Int + rpos::Int + nsite::Int + M::MPS + LR::Vector{ITensor} +end +ProjMPS(M::MPS) = ProjMPS(0, length(M) + 1, 2, M, Vector{ITensor}(undef, length(M))) + +copy(P::ProjMPS) = ProjMPS(P.lpos, P.rpos, P.nsite, copy(P.M), copy(P.LR)) + +nsite(P::ProjMPS) = P.nsite + +# The range of center sites +# TODO: Use the `AbstractProjMPO` version. +site_range(P::ProjMPS) = (P.lpos + 1):(P.rpos - 1) + +function set_nsite!(P::ProjMPS, nsite) + P.nsite = nsite + return P +end + +Base.length(P::ProjMPS) = length(P.M) + +function lproj(P::ProjMPS) + (P.lpos <= 0) && return nothing + return P.LR[P.lpos] +end + +function rproj(P::ProjMPS) + (P.rpos >= length(P) + 1) && return nothing + return P.LR[P.rpos] +end + +function product(P::ProjMPS, v::ITensor)::ITensor + if nsite(P) != 2 + error("Only two-site ProjMPS currently supported") + end + + Lpm = dag(prime(P.M[P.lpos + 1], "Link")) + !isnothing(lproj(P)) && (Lpm *= lproj(P)) + + Rpm = dag(prime(P.M[P.rpos - 1], "Link")) + !isnothing(rproj(P)) && (Rpm *= rproj(P)) + + pm = Lpm * Rpm + + pv = scalar(pm * v) + + Mv = pv * dag(pm) + + return noprime(Mv) +end + +#function Base.eltype(P::ProjMPS) +# elT = eltype(P.M[P.lpos+1]) +# for j = P.lpos+2:P.rpos-1 +# elT = promote_type(elT,eltype(P.M[j])) +# end +# if !isnull(lproj(P)) +# elT = promote_type(elT,eltype(lproj(P))) +# end +# if !isnull(rproj(P)) +# elT = promote_type(elT,eltype(rproj(P))) +# end +# return elT +#end + +(P::ProjMPS)(v::ITensor) = product(P, v) + +#function Base.size(P::ProjMPS)::Tuple{Int,Int} +# d = 1 +# if P.lpos > 0 +# d *= dim(linkind(M,P.lpos)) +# end +# for j=P.lpos+1:P.rpos-1 +# d *= dim(siteind(P.M,j)) +# end +# if P.rpos-1 < N +# d *= dim(linkind(M,P.rpos-1)) +# end +# return (d,d) +#end + +function makeL!(P::ProjMPS, psi::MPS, k::Int) + while P.lpos < k + ll = P.lpos + if ll <= 0 + P.LR[1] = psi[1] * dag(prime(P.M[1], "Link")) + P.lpos = 1 + else + P.LR[ll + 1] = P.LR[ll] * psi[ll + 1] * dag(prime(P.M[ll + 1], "Link")) + P.lpos += 1 + end + end +end + +function makeR!(P::ProjMPS, psi::MPS, k::Int) + N = length(P.M) + while P.rpos > k + rl = P.rpos + if rl >= N + 1 + P.LR[N] = psi[N] * dag(prime(P.M[N], "Link")) + P.rpos = N + else + P.LR[rl - 1] = P.LR[rl] * psi[rl - 1] * dag(prime(P.M[rl - 1], "Link")) + P.rpos -= 1 + end + end +end + +function position!(P::ProjMPS, psi::MPS, pos::Int) + makeL!(P, psi, pos - 1) + makeR!(P, psi, pos + nsite(P)) + + #These next two lines are needed + #when moving lproj and rproj backward + P.lpos = pos - 1 + P.rpos = pos + nsite(P) + return P +end + +function checkflux(P::ProjMPS) + checkflux(P.M) + foreach(eachindex(P.LR)) do i + isassigned(P.LR, i) && checkflux(P.LR[i]) + end + return nothing +end diff --git a/src/adapt.jl b/src/adapt.jl new file mode 100644 index 0000000..b73ab27 --- /dev/null +++ b/src/adapt.jl @@ -0,0 +1,2 @@ +using Adapt: Adapt +Adapt.adapt_structure(to, x::Union{MPS,MPO}) = map(xᵢ -> adapt(to, xᵢ), x) diff --git a/src/defaults.jl b/src/defaults.jl new file mode 100644 index 0000000..992a7fc --- /dev/null +++ b/src/defaults.jl @@ -0,0 +1,4 @@ +default_maxdim() = typemax(Int) +default_mindim() = 1 +default_cutoff(type::Type{<:Number}) = eps(real(type)) +default_noise() = false diff --git a/src/dmrg.jl b/src/dmrg.jl new file mode 100644 index 0000000..39ca7a2 --- /dev/null +++ b/src/dmrg.jl @@ -0,0 +1,389 @@ +using Adapt: adapt +using KrylovKit: eigsolve +using NDTensors: scalartype, timer +using Printf: @printf +using TupleTools: TupleTools + +function permute( + M::AbstractMPS, ::Tuple{typeof(linkind),typeof(siteinds),typeof(linkind)} +)::typeof(M) + M̃ = typeof(M)(length(M)) + for n in 1:length(M) + lₙ₋₁ = linkind(M, n - 1) + lₙ = linkind(M, n) + s⃗ₙ = TupleTools.sort(Tuple(siteinds(M, n)); by=plev) + M̃[n] = ITensors.permute(M[n], filter(!isnothing, (lₙ₋₁, s⃗ₙ..., lₙ))) + end + set_ortho_lims!(M̃, ortho_lims(M)) + return M̃ +end + +function dmrg(H::MPO, psi0::MPS, sweeps::Sweeps; kwargs...) + check_hascommoninds(siteinds, H, psi0) + check_hascommoninds(siteinds, H, psi0') + # Permute the indices to have a better memory layout + # and minimize permutations + H = permute(H, (linkind, siteinds, linkind)) + PH = ProjMPO(H) + return dmrg(PH, psi0, sweeps; kwargs...) +end + +function dmrg(Hs::Vector{MPO}, psi0::MPS, sweeps::Sweeps; kwargs...) + for H in Hs + check_hascommoninds(siteinds, H, psi0) + check_hascommoninds(siteinds, H, psi0') + end + Hs .= permute.(Hs, Ref((linkind, siteinds, linkind))) + PHS = ProjMPOSum(Hs) + return dmrg(PHS, psi0, sweeps; kwargs...) +end + +function dmrg(H::MPO, Ms::Vector{MPS}, psi0::MPS, sweeps::Sweeps; weight=true, kwargs...) + check_hascommoninds(siteinds, H, psi0) + check_hascommoninds(siteinds, H, psi0') + for M in Ms + check_hascommoninds(siteinds, M, psi0) + end + H = permute(H, (linkind, siteinds, linkind)) + Ms .= permute.(Ms, Ref((linkind, siteinds, linkind))) + if weight <= 0 + error( + "weight parameter should be > 0.0 in call to excited-state dmrg (value passed was weight=$weight)", + ) + end + PMM = ProjMPO_MPS(H, Ms; weight) + return dmrg(PMM, psi0, sweeps; kwargs...) +end + +using NDTensors.TypeParameterAccessors: unwrap_array_type +""" + dmrg(H::MPO, psi0::MPS; kwargs...) + dmrg(H::MPO, psi0::MPS, sweeps::Sweeps; kwargs...) + +Use the density matrix renormalization group (DMRG) algorithm +to optimize a matrix product state (MPS) such that it is the +eigenvector of lowest eigenvalue of a Hermitian matrix `H`, +represented as a matrix product operator (MPO). + + dmrg(Hs::Vector{MPO}, psi0::MPS; kwargs...) + dmrg(Hs::Vector{MPO}, psi0::MPS, sweeps::Sweeps; kwargs...) + +Use the density matrix renormalization group (DMRG) algorithm +to optimize a matrix product state (MPS) such that it is the +eigenvector of lowest eigenvalue of a Hermitian matrix `H`. +This version of `dmrg` accepts a representation of H as a +Vector of MPOs, `Hs = [H1, H2, H3, ...]` such that `H` is defined +`as H = H1 + H2 + H3 + ...` +Note that this sum of MPOs is not actually computed; rather +the set of MPOs `[H1,H2,H3,..]` is efficiently looped over at +each step of the DMRG algorithm when optimizing the MPS. + + dmrg(H::MPO, Ms::Vector{MPS}, psi0::MPS; weight=1.0, kwargs...) + dmrg(H::MPO, Ms::Vector{MPS}, psi0::MPS, sweeps::Sweeps; weight=1.0, kwargs...) + +Use the density matrix renormalization group (DMRG) algorithm +to optimize a matrix product state (MPS) such that it is the +eigenvector of lowest eigenvalue of a Hermitian matrix `H`, +subject to the constraint that the MPS is orthogonal to each +of the MPS provided in the Vector `Ms`. The orthogonality +constraint is approximately enforced by adding to `H` terms of +the form `w|M1> write_when_maxdim_exceeds) || + (maxdim(sweeps, 1) > write_when_maxdim_exceeds) + PH = disk(PH; path=write_path) + end + end + PH = position!(PH, psi, 1) + energy = 0.0 + + for sw in 1:nsweep(sweeps) + sw_time = @elapsed begin + maxtruncerr = 0.0 + + if !isnothing(write_when_maxdim_exceeds) && + maxdim(sweeps, sw) > write_when_maxdim_exceeds + if outputlevel >= 2 + println( + "\nWriting environment tensors do disk (write_when_maxdim_exceeds = $write_when_maxdim_exceeds and maxdim(sweeps, sw) = $(maxdim(sweeps, sw))).\nFiles located at path=$write_path\n", + ) + end + PH = disk(PH; path=write_path) + end + + for (b, ha) in sweepnext(N) + @debug_check begin + checkflux(psi) + checkflux(PH) + end + + @timeit_debug timer "dmrg: position!" begin + PH = position!(PH, psi, b) + end + + @debug_check begin + checkflux(psi) + checkflux(PH) + end + + @timeit_debug timer "dmrg: psi[b]*psi[b+1]" begin + phi = psi[b] * psi[b + 1] + end + + @timeit_debug timer "dmrg: eigsolve" begin + vals, vecs = eigsolve( + PH, + phi, + 1, + eigsolve_which_eigenvalue; + ishermitian, + tol=eigsolve_tol, + krylovdim=eigsolve_krylovdim, + maxiter=eigsolve_maxiter, + ) + end + + energy = vals[1] + ## Right now there is a conversion problem in CUDA.jl where `UnifiedMemory` Arrays are being converted + ## into `DeviceMemory`. This conversion line is here temporarily to fix that problem when it arises + ## Adapt is only called when using CUDA backend. CPU will work as implemented previously. + ## TODO this might be the only place we really need iscu if its not fixed. + phi = if NDTensors.iscu(phi) && NDTensors.iscu(vecs[1]) + adapt(ITensors.set_eltype(unwrap_array_type(phi), eltype(vecs[1])), vecs[1]) + else + vecs[1] + end + + ortho = ha == 1 ? "left" : "right" + + drho = nothing + if noise(sweeps, sw) > 0 + @timeit_debug timer "dmrg: noiseterm" begin + # Use noise term when determining new MPS basis. + # This is used to preserve the element type of the MPS. + elt = real(scalartype(psi)) + drho = elt(noise(sweeps, sw)) * noiseterm(PH, phi, ortho) + end + end + + @debug_check begin + checkflux(phi) + end + + @timeit_debug timer "dmrg: replacebond!" begin + spec = replacebond!( + PH, + psi, + b, + phi; + maxdim=maxdim(sweeps, sw), + mindim=mindim(sweeps, sw), + cutoff=cutoff(sweeps, sw), + eigen_perturbation=drho, + ortho, + normalize=true, + which_decomp, + svd_alg, + ) + end + maxtruncerr = max(maxtruncerr, spec.truncerr) + + @debug_check begin + checkflux(psi) + checkflux(PH) + end + + if outputlevel >= 2 + @printf("Sweep %d, half %d, bond (%d,%d) energy=%s\n", sw, ha, b, b + 1, energy) + @printf( + " Truncated using cutoff=%.1E maxdim=%d mindim=%d\n", + cutoff(sweeps, sw), + maxdim(sweeps, sw), + mindim(sweeps, sw) + ) + @printf( + " Trunc. err=%.2E, bond dimension %d\n", spec.truncerr, dim(linkind(psi, b)) + ) + flush(stdout) + end + + sweep_is_done = (b == 1 && ha == 2) + measure!( + observer; + energy, + psi, + projected_operator=PH, + bond=b, + sweep=sw, + half_sweep=ha, + spec, + outputlevel, + sweep_is_done, + ) + end + end + if outputlevel >= 1 + @printf( + "After sweep %d energy=%s maxlinkdim=%d maxerr=%.2E time=%.3f\n", + sw, + energy, + maxlinkdim(psi), + maxtruncerr, + sw_time + ) + flush(stdout) + end + isdone = checkdone!(observer; energy, psi, sweep=sw, outputlevel) + isdone && break + end + return (energy, psi) +end + +function _dmrg_sweeps(; + nsweeps, + maxdim=default_maxdim(), + mindim=default_mindim(), + cutoff=default_cutoff(Float64), + noise=default_noise(), +) + sweeps = Sweeps(nsweeps) + setmaxdim!(sweeps, maxdim...) + setmindim!(sweeps, mindim...) + setcutoff!(sweeps, cutoff...) + setnoise!(sweeps, noise...) + return sweeps +end + +function dmrg( + x1, + x2, + psi0::MPS; + nsweeps, + maxdim=default_maxdim(), + mindim=default_mindim(), + cutoff=default_cutoff(Float64), + noise=default_noise(), + kwargs..., +) + return dmrg( + x1, x2, psi0, _dmrg_sweeps(; nsweeps, maxdim, mindim, cutoff, noise); kwargs... + ) +end + +function dmrg( + x1, + psi0::MPS; + nsweeps, + maxdim=default_maxdim(), + mindim=default_mindim(), + cutoff=default_cutoff(Float64), + noise=default_noise(), + kwargs..., +) + return dmrg(x1, psi0, _dmrg_sweeps(; nsweeps, maxdim, mindim, cutoff, noise); kwargs...) +end diff --git a/src/exports.jl b/src/exports.jl new file mode 100644 index 0000000..749a73c --- /dev/null +++ b/src/exports.jl @@ -0,0 +1,174 @@ +using LinearAlgebra: ⋅ +export + # Exports that were removed from ITensors.jl + # when ITensors.ITensorMPS was moved to ITensorMPS.jl. + @OpName_str, + @SiteType_str, + @StateName_str, + @TagType_str, + @ValName_str, + Apply, + Op, + OpName, + Ops, + Prod, + Scaled, + SiteType, + Spectrum, + StateName, + Sum, + TagType, + Trotter, + ValName, + apply, + argsdict, + coefficient, + contract, + convert_leaf_eltype, + eigs, + entropy, + has_fermion_string, + hassameinds, + linkindex, + ops, + replaceprime, + siteindex, + splitblocks, + tr, + truncerror, + val, + + # lattices.jl + Lattice, + LatticeBond, + square_lattice, + triangular_lattice, + + # solvers + TimeDependentSum, + dmrg_x, + expand, + linsolve, + tdvp, + to_vec, + + # dmrg.jl + dmrg, + # abstractmps.jl + # Macros + @preserve_ortho, + # Methods + AbstractMPS, + add, + common_siteind, + common_siteinds, + findfirstsiteind, + findfirstsiteinds, + findsite, + findsites, + firstsiteind, + firstsiteinds, + logdot, + loginner, + lognorm, + movesite, + movesites, + ortho_lims, + orthocenter, + promote_itensor_eltype, + reset_ortho_lims!, + set_ortho_lims!, + siteinds, + sim!, + # autompo/ + AutoMPO, + OpSum, + add!, + # mpo.jl + # Types + MPO, + # Methods + error_contract, + maxlinkdim, + orthogonalize, + orthogonalize!, + outer, + projector, + random_mpo, + truncate, + truncate!, + unique_siteind, + unique_siteinds, + # mps.jl + # Types + MPS, + # Methods + ⋅, + dot, + correlation_matrix, + expect, + inner, + isortho, + linkdim, + linkdims, + linkind, + linkinds, + op, + productMPS, + random_mps, + replacebond, + replacebond!, + sample, + sample!, + siteind, + siteinds, + state, + replace_siteinds!, + replace_siteinds, + swapbondsites, + totalqn, + # observer.jl + # Types + AbstractObserver, + DMRGObserver, + DMRGMeasurement, + NoObserver, + # Methods + checkdone!, + energies, + measure!, + measurements, + truncerrors, + # projmpo.jl + disk, + ProjMPO, + lproj, + product, + rproj, + noiseterm, + nsite, + position!, + # projmposum.jl + ProjMPOSum, + # projmpo_mps.jl + ProjMPO_MPS, + # sweeps.jl + Sweeps, + cutoff, + cutoff!, + get_cutoffs, + get_maxdims, + get_mindims, + get_noises, + maxdim, + maxdim!, + mindim, + mindim!, + noise, + noise!, + nsweep, + setmaxdim!, + setmindim!, + setcutoff!, + setnoise!, + sweepnext diff --git a/src/imports.jl b/src/imports.jl new file mode 100644 index 0000000..d3a4b93 --- /dev/null +++ b/src/imports.jl @@ -0,0 +1,174 @@ +# Primarily used to import names into the `ITensorMPS` +# module from submodules or from `ITensors` so they can +# be reexported. +using ITensors.SiteTypes: + @OpName_str, + @SiteType_str, + @StateName_str, + @TagType_str, + @ValName_str, + OpName, + SiteType, + StateName, + TagType, + ValName, + ops +using ITensors.Ops: Trotter + +import Base: + # types + Array, + CartesianIndices, + Vector, + NTuple, + Tuple, + # symbols + +, + -, + *, + ^, + /, + ==, + <, + >, + !, + # functions + adjoint, + allunique, + axes, + complex, + conj, + convert, + copy, + copyto!, + deepcopy, + deleteat!, + eachindex, + eltype, + fill!, + filter, + filter!, + findall, + findfirst, + getindex, + hash, + imag, + intersect, + intersect!, + isapprox, + isassigned, + isempty, + isless, + isreal, + iszero, + iterate, + keys, + lastindex, + length, + map, + map!, + ndims, + print, + promote_rule, + push!, + real, + resize!, + setdiff, + setdiff!, + setindex!, + show, + similar, + size, + summary, + truncate, + zero, + # macros + @propagate_inbounds + +import Base.Broadcast: + # types + AbstractArrayStyle, + Broadcasted, + BroadcastStyle, + DefaultArrayStyle, + Style, + # functions + _broadcast_getindex, + broadcasted, + broadcastable, + instantiate + +import ..ITensors.NDTensors: + Algorithm, + @Algorithm_str, + EmptyNumber, + _Tuple, + _NTuple, + blas_get_num_threads, + datatype, + dense, + diagind, + disable_auto_fermion, + double_precision, + eachblock, + eachdiagblock, + enable_auto_fermion, + fill!!, + randn!!, + permutedims, + permutedims! + +import ..ITensors: + AbstractRNG, + Apply, + apply, + argument, + Broadcasted, + @Algorithm_str, + checkflux, + convert_leaf_eltype, + commontags, + @debug_check, + dag, + data, + DefaultArrayStyle, + DiskVector, + flux, + hascommoninds, + hasqns, + hassameinds, + inner, + isfermionic, + maxdim, + mindim, + noprime, + noprime!, + norm, + normalize, + outer, + OneITensor, + permute, + prime, + prime!, + product, + QNIndex, + replaceinds, + replaceprime, + replacetags, + setprime, + sim, + site, + splitblocks, + store, + Style, + sum, + swapprime, + symmetrystyle, + terms, + @timeit_debug, + truncate!, + which_op + +import ..ITensors.Ops: params + +import SerializedElementArrays: disk diff --git a/src/lattices/lattices.jl b/src/lattices/lattices.jl new file mode 100644 index 0000000..8e1c973 --- /dev/null +++ b/src/lattices/lattices.jl @@ -0,0 +1,140 @@ + +""" +A LatticeBond is a struct which represents +a single bond in a geometrical lattice or +else on interaction graph defining a physical +model such as a quantum Hamiltonian. + +LatticeBond has the following data fields: + + - s1::Int -- number of site 1 + - s2::Int -- number of site 2 + - x1::Float64 -- x coordinate of site 1 + - y1::Float64 -- y coordinate of site 1 + - x2::Float64 -- x coordinate of site 2 + - y2::Float64 -- y coordinate of site 2 + - type::String -- optional description of bond type +""" +struct LatticeBond + s1::Int + s2::Int + x1::Float64 + y1::Float64 + x2::Float64 + y2::Float64 + type::String +end + +""" + LatticeBond(s1::Int,s2::Int) + + LatticeBond(s1::Int,s2::Int, + x1::Real,y1::Real, + x2::Real,y2::Real, + type::String="") + +Construct a LatticeBond struct by +specifying just the numbers of sites +1 and 2, or additional details including +the (x,y) coordinates of the two sites and +an optional type string. +""" +function LatticeBond(s1::Int, s2::Int) + return LatticeBond(s1, s2, 0.0, 0.0, 0.0, 0.0, "") +end + +function LatticeBond( + s1::Int, s2::Int, x1::Real, y1::Real, x2::Real, y2::Real, bondtype::String="" +) + cf(x) = convert(Float64, x) + return LatticeBond(s1, s2, cf(x1), cf(y1), cf(x2), cf(y2), bondtype) +end + +""" +Lattice is an alias for Vector{LatticeBond} +""" +const Lattice = Vector{LatticeBond} + +""" + square_lattice(Nx::Int, + Ny::Int; + kwargs...)::Lattice + +Return a Lattice (array of LatticeBond +objects) corresponding to the two-dimensional +square lattice of dimensions (Nx,Ny). +By default the lattice has open boundaries, +but can be made periodic in the y direction +by specifying the keyword argument +`yperiodic=true`. +""" +function square_lattice(Nx::Int, Ny::Int; yperiodic=false)::Lattice + yperiodic = yperiodic && (Ny > 2) + N = Nx * Ny + Nbond = 2N - Ny + (yperiodic ? 0 : -Nx) + latt = Lattice(undef, Nbond) + b = 0 + for n in 1:N + x = div(n - 1, Ny) + 1 + y = mod(n - 1, Ny) + 1 + if x < Nx + latt[b += 1] = LatticeBond(n, n + Ny, x, y, x + 1, y) + end + if Ny > 1 + if y < Ny + latt[b += 1] = LatticeBond(n, n + 1, x, y, x, y + 1) + end + if yperiodic && y == 1 + latt[b += 1] = LatticeBond(n, n + Ny - 1, x, y, x, y + Ny - 1) + end + end + end + return latt +end + +""" + triangular_lattice(Nx::Int, + Ny::Int; + kwargs...)::Lattice + +Return a Lattice (array of LatticeBond +objects) corresponding to the two-dimensional +triangular lattice of dimensions (Nx,Ny). +By default the lattice has open boundaries, +but can be made periodic in the y direction +by specifying the keyword argument +`yperiodic=true`. +""" +function triangular_lattice(Nx::Int, Ny::Int; yperiodic=false)::Lattice + yperiodic = yperiodic && (Ny > 2) + N = Nx * Ny + Nbond = 3N - 2Ny + (yperiodic ? 0 : -2Nx + 1) + latt = Lattice(undef, Nbond) + b = 0 + for n in 1:N + x = div(n - 1, Ny) + 1 + y = mod(n - 1, Ny) + 1 + + # x-direction bonds + if x < Nx + latt[b += 1] = LatticeBond(n, n + Ny) + end + + # 2d bonds + if Ny > 1 + # vertical / y-periodic diagonal bond + if (n + 1 <= N) && ((y < Ny) || yperiodic) + latt[b += 1] = LatticeBond(n, n + 1) + end + # periodic vertical bond + if yperiodic && y == 1 + latt[b += 1] = LatticeBond(n, n + Ny - 1) + end + # diagonal bonds + if x < Nx && y < Ny + latt[b += 1] = LatticeBond(n, n + Ny + 1) + end + end + end + return latt +end diff --git a/src/Experimental.jl b/src/lib/Experimental/src/Experimental.jl similarity index 50% rename from src/Experimental.jl rename to src/lib/Experimental/src/Experimental.jl index 42be6dc..4e6fa93 100644 --- a/src/Experimental.jl +++ b/src/lib/Experimental/src/Experimental.jl @@ -1,3 +1,3 @@ module Experimental -using ITensorTDVP: dmrg +include("dmrg.jl") end diff --git a/src/lib/Experimental/src/dmrg.jl b/src/lib/Experimental/src/dmrg.jl new file mode 100644 index 0000000..8796936 --- /dev/null +++ b/src/lib/Experimental/src/dmrg.jl @@ -0,0 +1,17 @@ +using ..ITensorMPS: + MPS, + alternating_update, + compose_observers, + default_observer, + eigsolve_updater, + values_observer + +function dmrg( + operator, init::MPS; updater=eigsolve_updater, (observer!)=default_observer(), kwargs... +) + info_ref! = Ref{Any}() + info_observer! = values_observer(; info=info_ref!) + observer! = compose_observers(observer!, info_observer!) + state = alternating_update(operator, init; updater, observer!, kwargs...) + return info_ref![].eigval, state +end diff --git a/src/lib/ITensorMPSNamedDimsArraysExt/src/ITensorMPSNamedDimsArraysExt.jl b/src/lib/ITensorMPSNamedDimsArraysExt/src/ITensorMPSNamedDimsArraysExt.jl new file mode 100644 index 0000000..97229dd --- /dev/null +++ b/src/lib/ITensorMPSNamedDimsArraysExt/src/ITensorMPSNamedDimsArraysExt.jl @@ -0,0 +1,3 @@ +module ITensorMPSNamedDimsArraysExt +include("to_nameddimsarray.jl") +end diff --git a/src/lib/ITensorMPSNamedDimsArraysExt/src/to_nameddimsarray.jl b/src/lib/ITensorMPSNamedDimsArraysExt/src/to_nameddimsarray.jl new file mode 100644 index 0000000..f6596cb --- /dev/null +++ b/src/lib/ITensorMPSNamedDimsArraysExt/src/to_nameddimsarray.jl @@ -0,0 +1,6 @@ +using ..ITensorMPS: AbstractMPS +using ITensors.ITensorsNamedDimsArraysExt: ITensorsNamedDimsArraysExt, to_nameddimsarray + +function ITensorsNamedDimsArraysExt.to_nameddimsarray(x::AbstractMPS) + return to_nameddimsarray.(x) +end diff --git a/src/mpo.jl b/src/mpo.jl new file mode 100644 index 0000000..9acd561 --- /dev/null +++ b/src/mpo.jl @@ -0,0 +1,1050 @@ +using Adapt: adapt +using LinearAlgebra: dot +using Random: Random +using ITensors.Ops: OpSum +using ITensors.SiteTypes: SiteTypes, siteind, siteinds + +""" + MPO + +A finite size matrix product operator type. +Keeps track of the orthogonality center. +""" +mutable struct MPO <: AbstractMPS + data::Vector{ITensor} + llim::Int + rlim::Int +end + +function MPO(A::Vector{<:ITensor}; ortho_lims::UnitRange=1:length(A)) + return MPO(A, first(ortho_lims) - 1, last(ortho_lims) + 1) +end + +set_data(A::MPO, data::Vector{ITensor}) = MPO(data, A.llim, A.rlim) + +MPO() = MPO(ITensor[], 0, 0) + +function convert(::Type{MPS}, M::MPO) + return MPS(data(M); ortho_lims=ortho_lims(M)) +end + +function convert(::Type{MPO}, M::MPS) + return MPO(data(M); ortho_lims=ortho_lims(M)) +end + +function MPO(::Type{ElT}, sites::Vector{<:Index}) where {ElT<:Number} + N = length(sites) + v = Vector{ITensor}(undef, N) + if N == 0 + return MPO() + elseif N == 1 + v[1] = ITensor(ElT, dag(sites[1]), sites[1]') + return MPO(v) + end + space_ii = all(hasqns, sites) ? [QN() => 1] : 1 + l = [Index(space_ii, "Link,l=$ii") for ii in 1:(N - 1)] + for ii in eachindex(sites) + s = sites[ii] + if ii == 1 + v[ii] = ITensor(ElT, dag(s), s', l[ii]) + elseif ii == N + v[ii] = ITensor(ElT, dag(l[ii - 1]), dag(s), s') + else + v[ii] = ITensor(ElT, dag(l[ii - 1]), dag(s), s', l[ii]) + end + end + return MPO(v) +end + +MPO(sites::Vector{<:Index}) = MPO(Float64, sites) + +""" + MPO(N::Int) + +Make an MPO of length `N` filled with default ITensors. +""" +MPO(N::Int) = MPO(Vector{ITensor}(undef, N)) + +""" + MPO([::Type{ElT} = Float64}, ]sites, ops::Vector{String}) + +Make an MPO with pairs of sites `s[i]` and `s[i]'` +and operators `ops` on each site. +""" +function MPO(::Type{ElT}, sites::Vector{<:Index}, ops::Vector) where {ElT<:Number} + N = length(sites) + os = Prod{Op}() + for n in 1:N + os *= Op(ops[n], n) + end + M = MPO(ElT, os, sites) + + # Currently, OpSum does not output the optimally truncated + # MPO (see https://github.com/ITensor/ITensors.jl/issues/526) + # So here, we need to first normalize, then truncate, then + # return the normalization. + lognormM = lognorm(M) + M ./= exp(lognormM / N) + truncate!(M; cutoff=1e-15) + M .*= exp(lognormM / N) + return M +end + +function MPO(::Type{ElT}, sites::Vector{<:Index}, fops::Function) where {ElT<:Number} + ops = [fops(n) for n in 1:length(sites)] + return MPO(ElT, sites, ops) +end + +MPO(sites::Vector{<:Index}, ops) = MPO(Float64, sites, ops) + +function MPO(sites::Vector{<:Index}, os::OpSum) + return error( + "To construct an MPO from an OpSum `opsum` and a set of indices `sites`, you must use MPO(opsum, sites)", + ) +end + +""" + MPO([::Type{ElT} = Float64, ]sites, op::String) + +Make an MPO with pairs of sites `s[i]` and `s[i]'` +and operator `op` on every site. +""" +function MPO(::Type{ElT}, sites::Vector{<:Index}, op::String) where {ElT<:Number} + return MPO(ElT, sites, fill(op, length(sites))) +end + +MPO(sites::Vector{<:Index}, op::String) = MPO(Float64, sites, op) + +function MPO(::Type{ElT}, sites::Vector{<:Index}, op::Matrix{<:Number}) where {ElT<:Number} + # return MPO(ElT, sites, fill(op, length(sites))) + return error( + "Not defined on purpose because of potential ambiguity with `MPO(A::Array, sites::Vector)`. Pass the on-site matrices as functions like `MPO(sites, n -> [1 0; 0 1])` instead.", + ) +end + +MPO(sites::Vector{<:Index}, op::Matrix{ElT}) where {ElT<:Number} = MPO(ElT, sites, op) + +function random_mpo(sites::Vector{<:Index}, m::Int=1) + return random_mpo(Random.default_rng(), sites, m) +end + +function random_mpo(rng::AbstractRNG, sites::Vector{<:Index}, m::Int=1) + M = MPO(sites, "Id") + for i in eachindex(sites) + randn!(rng, M[i]) + normalize!(M[i]) + end + m > 1 && throw(ArgumentError("random_mpo: currently only m==1 supported")) + return M +end + +function MPO(A::ITensor, sites::Vector{<:Index}; kwargs...) + return MPO(A, IndexSet.(prime.(sites), dag.(sites)); kwargs...) +end + +function outer_mps_mps_deprecation_warning() + return "Calling `outer(ψ::MPS, ϕ::MPS)` for MPS `ψ` and `ϕ` with shared indices is deprecated. Currently, we automatically prime `ψ` to make sure the site indices don't clash, but that will no longer be the case in ITensors v0.4. To upgrade your code, call `outer(ψ', ϕ)`. Although the new interface seems less convenient, it will allow `outer` to accept more general outer products going forward, such as outer products where some indices are shared (a batched outer product) or outer products of MPS between site indices that aren't just related by a single prime level." +end + +function deprecate_make_inds_unmatch(::typeof(outer), ψ::MPS, ϕ::MPS; kw...) + if hassameinds(siteinds, ψ, ϕ) + ITensors.warn_once(outer_mps_mps_deprecation_warning(), :outer_mps_mps) + ψ = ψ' + end + return ψ, ϕ +end + +""" + outer(x::MPS, y::MPS; ) -> MPO + +Compute the outer product of `MPS` `x` and `MPS` `y`, +returning an `MPO` approximation. Note that `y` will be conjugated. + +In Dirac notation, this is the operation `|x⟩⟨y|`. + +If you want an outer product of an MPS with itself, you should +call `outer(x', x; kwargs...)` so that the resulting MPO +has site indices with indices coming in pairs of prime levels +of 1 and 0. If not, the site indices won't be unique which would +not be an outer product. + +For example: + +```julia +s = siteinds("S=1/2", 5) +x = random_mps(s) +y = random_mps(s) +outer(x, y) # Incorrect! Site indices must be unique. +outer(x', y) # Results in an MPO with pairs of primed and unprimed indices. +``` + +This allows for more general outer products, such as more general +MPO outputs which don't have pairs of primed and unprimed indices, +or outer products where the input MPS are vectorizations of MPOs. + +For example: + +```julia +s = siteinds("S=1/2", 5) +X = MPO(s, "Id") +Y = MPO(s, "Id") +x = convert(MPS, X) +y = convert(MPS, Y) +outer(x, y) # Incorrect! Site indices must be unique. +outer(x', y) # Incorrect! Site indices must be unique. +outer(addtags(x, "Out"), addtags(y, "In")) # This performs a proper outer product. +``` + +The keyword arguments determine the truncation, and accept +the same arguments as `contract(::MPO, ::MPO; kwargs...)`. + +See also [`apply`](@ref), [`contract`](@ref). +""" +function outer(ψ::MPS, ϕ::MPS; kw...) + ψ, ϕ = deprecate_make_inds_unmatch(outer, ψ, ϕ; kw...) + + ψmat = convert(MPO, ψ) + ϕmat = convert(MPO, dag(ϕ)) + return contract(ψmat, ϕmat; kw...) +end + +""" + projector(x::MPS; ) -> MPO + +Computes the projector onto the state `x`. In Dirac notation, this is the operation `|x⟩⟨x|/|⟨x|x⟩|²`. + +Use keyword arguments to control the level of truncation, which are +the same as those accepted by `contract(::MPO, ::MPO; kw...)`. + +# Keywords + + - `normalize::Bool=true`: whether or not to normalize the input MPS before + forming the projector. If `normalize==false` and the input MPS is not + already normalized, this function will not output a proper project, and + simply outputs `outer(x, x) = |x⟩⟨x|`, i.e. the projector scaled by `norm(x)^2`. + - truncation keyword arguments accepted by `contract(::MPO, ::MPO; kw...)`. + +See also [`outer`](@ref), [`contract`](@ref). +""" +function projector(ψ::MPS; normalize::Bool=true, kw...) + ψψᴴ = outer(ψ', ψ; kw...) + if normalize + normalize!(ψψᴴ[orthocenter(ψψᴴ)]) + end + return ψψᴴ +end + +# XXX: rename originalsiteind? +""" + siteind(M::MPO, j::Int; plev = 0, kwargs...) + +Get the first site Index of the MPO found, by +default with prime level 0. +""" +SiteTypes.siteind(M::MPO, j::Int; kwargs...) = siteind(first, M, j; plev=0, kwargs...) + +# TODO: make this return the site indices that would have +# been used to create the MPO? I.e.: +# [dag(siteinds(M, j; plev = 0, kwargs...)) for j in 1:length(M)] +""" + siteinds(M::MPO; kwargs...) + +Get a Vector of IndexSets of all the site indices of M. +""" +SiteTypes.siteinds(M::MPO; kwargs...) = siteinds(all, M; kwargs...) + +function SiteTypes.siteinds(Mψ::Tuple{MPO,MPS}, n::Int; kwargs...) + return siteinds(uniqueinds, Mψ[1], Mψ[2], n; kwargs...) +end + +function nsites(Mψ::Tuple{MPO,MPS}) + M, ψ = Mψ + N = length(M) + @assert N == length(ψ) + return N +end + +function SiteTypes.siteinds(Mψ::Tuple{MPO,MPS}; kwargs...) + return [siteinds(Mψ, n; kwargs...) for n in 1:nsites(Mψ)] +end + +# XXX: rename originalsiteinds? +""" + firstsiteinds(M::MPO; kwargs...) + +Get a Vector of the first site Index found on each site of M. + +By default, it finds the first site Index with prime level 0. +""" +firstsiteinds(M::MPO; kwargs...) = siteinds(first, M; plev=0, kwargs...) + +function hassameinds(::typeof(siteinds), ψ::MPS, Hϕ::Tuple{MPO,MPS}) + N = length(ψ) + @assert N == length(Hϕ[1]) == length(Hϕ[1]) + for n in 1:N + !hassameinds(siteinds(Hϕ, n), siteinds(ψ, n)) && return false + end + return true +end + +function inner_mps_mpo_mps_deprecation_warning() + return """ + Calling `inner(x::MPS, A::MPO, y::MPS)` where the site indices of the `MPS` + `x` and the `MPS` resulting from contracting `MPO` `A` with `MPS` `y` don't + match is deprecated as of ITensors v0.3 and will result in an error in ITensors + v0.4. The most common cause of this is something like the following: + + ```julia + s = siteinds("S=1/2") + psi = random_mps(s) + H = MPO(s, "Id") + inner(psi, H, psi) + ``` + + `psi` has the Index structure `-s-(psi)` and `H` has the Index structure + `-s'-(H)-s-`, so the Index structure of would be `(dag(psi)-s- -s'-(H)-s-(psi)` + unless the prime levels were fixed. Previously we tried fixing the prime level + in situations like this, but we will no longer be doing that going forward. + + There are a few ways to fix this. You can simply change: + + ```julia + inner(psi, H, psi) + ``` + + to: + + ```julia + inner(psi', H, psi) + ``` + + in which case the Index structure will be `(dag(psi)-s'-(H)-s-(psi)`. + + Alternatively, you can use the `Apply` function: + + ```julia + + inner(psi, Apply(H, psi)) + ``` + + In this case, `Apply(H, psi)` represents the "lazy" evaluation of + `apply(H, psi)`. The function `apply(H, psi)` performs the contraction of + `H` with `psi` and then unprimes the results, so this versions ensures that + the prime levels of the inner product will match. + + Although the new behavior seems less convenient, it makes it easier to + generalize `inner(::MPS, ::MPO, ::MPS)` to other types of inputs, like `MPS` + and `MPO` with different tag and prime conventions, multiple sites per tensor, + `ITensor` inputs, etc. + """ +end + +function deprecate_make_inds_match!( + ::typeof(dot), ydag::MPS, A::MPO, x::MPS; make_inds_match::Bool=true +) + N = length(x) + if !hassameinds(siteinds, ydag, (A, x)) + sAx = siteinds((A, x)) + if any(s -> length(s) > 1, sAx) + n = findfirst(n -> !hassameinds(siteinds(ydag, n), siteinds((A, x), n)), 1:N) + error( + """Calling `dot(ϕ::MPS, H::MPO, ψ::MPS)` with multiple site indices per MPO/MPS tensor but the site indices don't match. Even with `make_inds_match = true`, the case of multiple site indices per MPO/MPS is not handled automatically. The sites with unmatched site indices are: + + inds(ϕ[$n]) = $(inds(ydag[n])) + + inds(H[$n]) = $(inds(A[n])) + + inds(ψ[$n]) = $(inds(x[n])) + + Make sure the site indices of your MPO/MPS match. You may need to prime one of the MPS, such as `dot(ϕ', H, ψ)`.""", + ) + end + if !hassameinds(siteinds, ydag, (A, x)) && make_inds_match + ITensors.warn_once(inner_mps_mpo_mps_deprecation_warning(), :inner_mps_mpo_mps) + replace_siteinds!(ydag, sAx) + end + end + return ydag, A, x +end + +function _log_or_not_dot( + y::MPS, A::MPO, x::MPS, loginner::Bool; make_inds_match::Bool=true, kwargs... +)::Number + N = length(A) + check_hascommoninds(siteinds, A, x) + ydag = dag(y) + sim!(linkinds, ydag) + ydag, A, x = deprecate_make_inds_match!(dot, ydag, A, x; make_inds_match) + check_hascommoninds(siteinds, A, y) + O = ydag[1] * A[1] * x[1] + if loginner + normO = norm(O) + log_inner_tot = log(normO) + O ./= normO + end + for j in 2:N + O = O * ydag[j] * A[j] * x[j] + if loginner + normO = norm(O) + log_inner_tot += log(normO) + O ./= normO + end + end + if loginner + if !isreal(O[]) || real(O[]) < 0 + log_inner_tot += log(complex(O[])) + end + return log_inner_tot + else + return O[] + end +end + +""" + dot(y::MPS, A::MPO, x::MPS) + +Same as [`inner`](@ref). +""" +function LinearAlgebra.dot(y::MPS, A::MPO, x::MPS; make_inds_match::Bool=true, kwargs...) + return _log_or_not_dot(y, A, x, false; make_inds_match=make_inds_match, kwargs...) +end + +""" + logdot(B::MPO, y::MPS, A::MPO, x::MPS) + Compute the logarithm of the inner product `⟨y|A|x⟩` efficiently and exactly. + This is useful for larger MPS/MPO, where in the limit of large numbers of sites the inner product can diverge or approach zero. + Same as [`loginner`](@ref). +""" +function logdot(y::MPS, A::MPO, x::MPS; make_inds_match::Bool=true, kwargs...) + return _log_or_not_dot(y, A, x, true; make_inds_match=make_inds_match, kwargs...) +end + +""" + inner(y::MPS, A::MPO, x::MPS) + +Compute `⟨y|A|x⟩ = ⟨y|Ax⟩` efficiently and exactly without making any intermediate +MPOs. In general it is more efficient and accurate than `inner(y, apply(A, x))`. + +This is helpful for computing the expectation value of an operator `A`, which would be: + +```julia +inner(x', A, x) +``` + +assuming `x` is normalized. + +If you want to compute `⟨By|Ax⟩` you can use `inner(B::MPO, y::MPS, A::MPO, x::MPS)`. + +This is helpful for computing the variance of an operator `A`, which would be: + +```julia +inner(A, x, A, x) - inner(x', A, x) ^ 2 +``` + +assuming `x` is normalized. + +$(make_inds_match_docstring_warning()) + +Same as [`dot`](@ref). +""" +inner(y::MPS, A::MPO, x::MPS; kwargs...) = dot(y, A, x; kwargs...) + +function inner(y::MPS, Ax::Apply{Tuple{MPO,MPS}}) + return inner(y', Ax.args[1], Ax.args[2]) +end + +""" + loginner(y::MPS, A::MPO, x::MPS) + Same as [`logdot`](@ref). +""" +loginner(y::MPS, A::MPO, x::MPS; kwargs...) = logdot(y, A, x; kwargs...) + +""" + dot(B::MPO, y::MPS, A::MPO, x::MPS) + +Same as [`inner`](@ref). +""" +function LinearAlgebra.dot( + B::MPO, y::MPS, A::MPO, x::MPS; make_inds_match::Bool=true, kwargs... +)::Number + !make_inds_match && error( + "make_inds_match = false not currently supported in dot(::MPO, ::MPS, ::MPO, ::MPS)" + ) + N = length(B) + if length(y) != N || length(x) != N || length(A) != N + throw( + DimensionMismatch( + "inner: mismatched lengths $N and $(length(x)) or $(length(y)) or $(length(A))" + ), + ) + end + check_hascommoninds(siteinds, A, x) + check_hascommoninds(siteinds, B, y) + for j in eachindex(B) + !hascommoninds( + uniqueinds(siteinds(A, j), siteinds(x, j)), uniqueinds(siteinds(B, j), siteinds(y, j)) + ) && error( + "$(typeof(x)) Ax and $(typeof(y)) By must share site indices. On site $j, Ax has site indices $(uniqueinds(siteinds(A, j), (siteinds(x, j)))) while By has site indices $(uniqueinds(siteinds(B, j), siteinds(y, j))).", + ) + end + ydag = dag(y) + Bdag = dag(B) + sim!(linkinds, ydag) + sim!(linkinds, Bdag) + yB = ydag[1] * Bdag[1] + Ax = A[1] * x[1] + O = yB * Ax + for j in 2:N + yB = ydag[j] * Bdag[j] + Ax = A[j] * x[j] + yB *= O + O = yB * Ax + end + return O[] +end + +# TODO: maybe make these into tuple inputs? +# Also can generalize to: +# inner((β, B, y), (α, A, x)) +""" + inner(B::MPO, y::MPS, A::MPO, x::MPS) + +Compute `⟨By|A|x⟩ = ⟨By|Ax⟩` efficiently and exactly without making any intermediate +MPOs. In general it is more efficient and accurate than `inner(apply(B, y), apply(A, x))`. + +This is helpful for computing the variance of an operator `A`, which would be: + +```julia +inner(A, x, A, x) - inner(x, A, x) ^ 2 +``` + +$(make_inds_match_docstring_warning()) + +Same as [`dot`](@ref). +""" +inner(B::MPO, y::MPS, A::MPO, x::MPS) = dot(B, y, A, x) + +function LinearAlgebra.dot(M1::MPO, M2::MPO; make_inds_match::Bool=false, kwargs...) + if make_inds_match + error("In dot(::MPO, ::MPO), make_inds_match is not currently supported") + end + return _log_or_not_dot(M1, M2, false; make_inds_match=make_inds_match) +end + +# TODO: implement by combining the MPO indices and converting +# to MPS +function logdot(M1::MPO, M2::MPO; make_inds_match::Bool=false, kwargs...) + if make_inds_match + error("In dot(::MPO, ::MPO), make_inds_match is not currently supported") + end + return _log_or_not_dot(M1, M2, true; make_inds_match=make_inds_match) +end + +function LinearAlgebra.tr(M::MPO; plev::Pair{Int,Int}=0 => 1, tags::Pair=ts"" => ts"") + N = length(M) + # + # TODO: choose whether to contract or trace + # first depending on the bond dimension. The scaling is: + # + # 1. Trace last: O(χ²d²) + O(χd²) + # 2. Trace first: O(χ²d²) + O(χ²) + # + # So tracing first is better if d > √χ. + # + L = tr(M[1]; plev=plev, tags=tags) + for j in 2:N + L *= M[j] + L = tr(L; plev=plev, tags=tags) + end + return L +end + +""" + error_contract(y::MPS, A::MPO, x::MPS; + make_inds_match::Bool = true) + error_contract(y::MPS, x::MPS, A::MPO; + make_inds_match::Bool = true) + +Compute the distance between A|x> and an approximation MPS y: +`| |y> - A|x> |/| A|x> | = √(1 + ( - 2*real())/)`. + +If `make_inds_match = true`, the function attempts match the site +indices of `y` with the site indices of `A` that are not common +with `x`. +""" +function error_contract(y::MPS, A::MPO, x::MPS; kwargs...) + N = length(A) + if length(y) != N || length(x) != N + throw( + DimensionMismatch("inner: mismatched lengths $N and $(length(x)) or $(length(y))") + ) + end + iyy = dot(y, y; kwargs...) + iyax = dot(y', A, x; kwargs...) + iaxax = dot(A, x, A, x; kwargs...) + return sqrt(abs(1.0 + (iyy - 2 * real(iyax)) / iaxax)) +end + +error_contract(y::MPS, x::MPS, A::MPO) = error_contract(y, A, x) + +""" + apply(A::MPO, x::MPS; kwargs...) + +Contract the `MPO` `A` with the `MPS` `x` and then map the prime level of the resulting +MPS back to 0. + +Equivalent to `replaceprime(contract(A, x; kwargs...), 2 => 1)`. + +See also [`contract`](@ref) for details about the arguments available. +""" +function apply(A::MPO, ψ::MPS; alg=Algorithm"densitymatrix"(), kwargs...) + return apply(Algorithm(alg), A, ψ; kwargs...) +end + +function apply(alg::Algorithm, A::MPO, ψ::MPS; kwargs...) + Aψ = contract(alg, A, ψ; kwargs...) + return replaceprime(Aψ, 1 => 0) +end + +(A::MPO)(ψ::MPS; kwargs...) = apply(A, ψ; kwargs...) + +function Apply(A::MPO, ψ::MPS; kwargs...) + return ITensors.LazyApply.Applied(apply, (A, ψ), NamedTuple(kwargs)) +end + +function ITensors.contract(A::MPO, ψ::MPS; alg=nothing, method=alg, kwargs...) + # TODO: Delete `method` since it is deprecated. + alg = NDTensors.replace_nothing(method, "densitymatrix") + + # Keyword argument deprecations + # TODO: Delete these. + if alg == "DensityMatrix" + @warn "In contract, method DensityMatrix is deprecated in favor of densitymatrix" + alg = "densitymatrix" + end + if alg == "Naive" + @warn "In contract, `alg=\"Naive\"` is deprecated in favor of `alg=\"naive\"`" + alg = "naive" + end + + return contract(Algorithm(alg), A, ψ; kwargs...) +end + +contract_mpo_mps_doc = """ + contract(ψ::MPS, A::MPO; kwargs...) -> MPS + *(::MPS, ::MPO; kwargs...) -> MPS + + contract(A::MPO, ψ::MPS; kwargs...) -> MPS + *(::MPO, ::MPS; kwargs...) -> MPS + +Contract the `MPO` `A` with the `MPS` `ψ`, returning an `MPS` with the unique +site indices of the `MPO`. + +For example, for an MPO with site indices with prime levels of 1 and 0, such as +`-s'-A-s-`, and an MPS with site indices with prime levels of 0, such as +`-s-x`, the result is an MPS `y` with site indices with prime levels of 1, +`-s'-y = -s'-A-s-x`. + +Since it is common to contract an MPO with prime levels of 1 and 0 with an MPS with +prime level of 0 and want a resulting MPS with prime levels of 0, we provide a +convenience function `apply`: +```julia +apply(A, x; kwargs...) = replaceprime(contract(A, x; kwargs...), 2 => 1)`. +``` + +Choose the method with the `method` keyword, for example +`"densitymatrix"` and `"naive"`. + +# Keywords +- `cutoff::Float64=1e-13`: the cutoff value for truncating the density matrix + eigenvalues. Note that the default is somewhat arbitrary and subject to + change, in general you should set a `cutoff` value. +- `maxdim::Int=maxlinkdim(A) * maxlinkdim(ψ))`: the maximal bond dimension of the results MPS. +- `mindim::Int=1`: the minimal bond dimension of the resulting MPS. +- `normalize::Bool=false`: whether or not to normalize the resulting MPS. +- `method::String="densitymatrix"`: the algorithm to use for the contraction. + Currently the options are "densitymatrix", where the network formed by the + MPO and MPS is squared and contracted down to a density matrix which is + diagonalized iteratively at each site, and "naive", where the MPO and MPS + tensor are contracted exactly at each site and then a truncation of the + resulting MPS is performed. + +See also [`apply`](@ref). +""" + +@doc """ +$contract_mpo_mps_doc +""" ITensors.contract(::MPO, ::MPS) + +ITensors.contract(ψ::MPS, A::MPO; kwargs...) = contract(A, ψ; kwargs...) + +*(A::MPO, B::MPS; kwargs...) = contract(A, B; kwargs...) +*(A::MPS, B::MPO; kwargs...) = contract(A, B; kwargs...) + +# TODO: try this to copy the docstring +# Causing an error in Revise +#@doc """ +#$contract_mpo_mps_doc +#""" *(::MPO, ::MPS) + +#@doc (@doc contract(::MPO, ::MPS)) *(::MPO, ::MPS) + +function ITensors.contract( + ::Algorithm"densitymatrix", + A::MPO, + ψ::MPS; + cutoff=1e-13, + maxdim=maxlinkdim(A) * maxlinkdim(ψ), + mindim=1, + normalize=false, + kwargs..., +)::MPS + n = length(A) + n != length(ψ) && + throw(DimensionMismatch("lengths of MPO ($n) and MPS ($(length(ψ))) do not match")) + if n == 1 + return MPS([A[1] * ψ[1]]) + end + mindim = max(mindim, 1) + requested_maxdim = maxdim + ψ_out = similar(ψ) + + any(i -> isempty(i), siteinds(commoninds, A, ψ)) && + error("In `contract(A::MPO, x::MPS)`, `A` and `x` must share a set of site indices") + + # In case A and ψ have the same link indices + A = sim(linkinds, A) + + ψ_c = dag(ψ) + A_c = dag(A) + + # To not clash with the link indices of A and ψ + sim!(linkinds, A_c) + sim!(linkinds, ψ_c) + sim!(siteinds, commoninds, A_c, ψ_c) + + # A version helpful for making the density matrix + simA_c = sim(siteinds, uniqueinds, A_c, ψ_c) + + # Store the left environment tensors + E = Vector{ITensor}(undef, n - 1) + + E[1] = ψ[1] * A[1] * A_c[1] * ψ_c[1] + for j in 2:(n - 1) + E[j] = E[j - 1] * ψ[j] * A[j] * A_c[j] * ψ_c[j] + end + R = ψ[n] * A[n] + simR_c = ψ_c[n] * simA_c[n] + ρ = E[n - 1] * R * simR_c + l = linkind(ψ, n - 1) + ts = isnothing(l) ? "" : tags(l) + Lis = siteinds(uniqueinds, A, ψ, n) + Ris = siteinds(uniqueinds, simA_c, ψ_c, n) + F = eigen(ρ, Lis, Ris; ishermitian=true, tags=ts, cutoff, maxdim, mindim, kwargs...) + D, U, Ut = F.D, F.V, F.Vt + l_renorm, r_renorm = F.l, F.r + ψ_out[n] = Ut + R = R * dag(Ut) * ψ[n - 1] * A[n - 1] + simR_c = simR_c * U * ψ_c[n - 1] * simA_c[n - 1] + for j in reverse(2:(n - 1)) + # Determine smallest maxdim to use + cip = commoninds(ψ[j], E[j - 1]) + ciA = commoninds(A[j], E[j - 1]) + prod_dims = dim(cip) * dim(ciA) + maxdim = min(prod_dims, requested_maxdim) + + s = siteinds(uniqueinds, A, ψ, j) + s̃ = siteinds(uniqueinds, simA_c, ψ_c, j) + ρ = E[j - 1] * R * simR_c + l = linkind(ψ, j - 1) + ts = isnothing(l) ? "" : tags(l) + Lis = IndexSet(s..., l_renorm) + Ris = IndexSet(s̃..., r_renorm) + F = eigen(ρ, Lis, Ris; ishermitian=true, tags=ts, cutoff, maxdim, mindim, kwargs...) + D, U, Ut = F.D, F.V, F.Vt + l_renorm, r_renorm = F.l, F.r + ψ_out[j] = Ut + R = R * dag(Ut) * ψ[j - 1] * A[j - 1] + simR_c = simR_c * U * ψ_c[j - 1] * simA_c[j - 1] + end + if normalize + R ./= norm(R) + end + ψ_out[1] = R + setleftlim!(ψ_out, 0) + setrightlim!(ψ_out, 2) + return ψ_out +end + +function _contract(::Algorithm"naive", A, ψ; truncate=true, kwargs...) + A = sim(linkinds, A) + ψ = sim(linkinds, ψ) + + N = length(A) + if N != length(ψ) + throw(DimensionMismatch("lengths of MPO ($N) and MPS ($(length(ψ))) do not match")) + end + + ψ_out = typeof(ψ)(N) + for j in 1:N + ψ_out[j] = A[j] * ψ[j] + end + + for b in 1:(N - 1) + Al = commoninds(A[b], A[b + 1]) + ψl = commoninds(ψ[b], ψ[b + 1]) + l = [Al..., ψl...] + if !isempty(l) + C = combiner(l) + ψ_out[b] *= C + ψ_out[b + 1] *= dag(C) + end + end + + if truncate + truncate!(ψ_out; kwargs...) + end + + return ψ_out +end + +function ITensors.contract(alg::Algorithm"naive", A::MPO, ψ::MPS; kwargs...) + return _contract(alg, A, ψ; kwargs...) +end + +function ITensors.contract(A::MPO, B::MPO; alg="zipup", kwargs...) + return contract(Algorithm(alg), A, B; kwargs...) +end + +function ITensors.contract(alg::Algorithm"naive", A::MPO, B::MPO; kwargs...) + return _contract(alg, A, B; kwargs...) +end + +function ITensors.contract( + ::Algorithm"zipup", + A::MPO, + B::MPO; + cutoff=1e-14, + maxdim=maxlinkdim(A) * maxlinkdim(B), + mindim=1, + kwargs..., +) + if hassameinds(siteinds, A, B) + error( + "In `contract(A::MPO, B::MPO)`, MPOs A and B have the same site indices. The indices of the MPOs in the contraction are taken literally, and therefore they should only share one site index per site so the contraction results in an MPO. You may want to use `replaceprime(contract(A', B), 2 => 1)` or `apply(A, B)` which automatically adjusts the prime levels assuming the input MPOs have pairs of primed and unprimed indices.", + ) + end + N = length(A) + N != length(B) && + throw(DimensionMismatch("lengths of MPOs A ($N) and B ($(length(B))) do not match")) + # Special case for a single site + N == 1 && return MPO([A[1] * B[1]]) + A = orthogonalize(A, 1) + B = orthogonalize(B, 1) + A = sim(linkinds, A) + sA = siteinds(uniqueinds, A, B) + sB = siteinds(uniqueinds, B, A) + C = MPO(N) + lCᵢ = Index[] + R = ITensor(true) + for i in 1:(N - 2) + RABᵢ = R * A[i] * B[i] + left_inds = [sA[i]..., sB[i]..., lCᵢ...] + C[i], R = factorize( + RABᵢ, + left_inds; + ortho="left", + tags=commontags(linkinds(A, i)), + cutoff, + maxdim, + mindim, + kwargs..., + ) + lCᵢ = dag(commoninds(C[i], R)) + end + i = N - 1 + RABᵢ = R * A[i] * B[i] * A[i + 1] * B[i + 1] + left_inds = [sA[i]..., sB[i]..., lCᵢ...] + C[N - 1], C[N] = factorize( + RABᵢ, + left_inds; + ortho="right", + tags=commontags(linkinds(A, i)), + cutoff, + maxdim, + mindim, + kwargs..., + ) + truncate!(C; kwargs...) + return C +end + +""" + apply(A::MPO, B::MPO; kwargs...) + +Contract the `MPO` `A'` with the `MPO` `B` and then map the prime level of the resulting +MPO back to having pairs of indices with prime levels of 1 and 0. + +Equivalent to `replaceprime(contract(A', B; kwargs...), 2 => 1)`. + +See also [`contract`](@ref) for details about the arguments available. +""" +function apply(A::MPO, B::MPO; kwargs...) + AB = contract(A', B; kwargs...) + return replaceprime(AB, 2 => 1) +end + +function apply(A1::MPO, A2::MPO, A3::MPO, As::MPO...; kwargs...) + return apply(apply(A1, A2; kwargs...), A3, As...; kwargs...) +end + +(A::MPO)(B::MPO; kwargs...) = apply(A, B; kwargs...) + +contract_mpo_mpo_doc = """ + contract(A::MPO, B::MPO; kwargs...) -> MPO + *(::MPO, ::MPO; kwargs...) -> MPO + +Contract the `MPO` `A` with the `MPO` `B`, returning an `MPO` with the +site indices that are not shared between `A` and `B`. + +If you are contracting two MPOs with the same sets of indices, likely you +want to call something like: + +```julia +C = contract(A', B; cutoff=1e-12) +C = replaceprime(C, 2 => 1) +``` + +That is because if MPO `A` has the index structure `-s'-A-s-` and MPO `B` +has the Index structure `-s'-B-s-`, if we only want to contract over +on set of the indices, we would do `(-s'-A-s-)'-s'-B-s- = -s''-A-s'-s'-B-s- = -s''-C-s-`, +and then map the prime levels back to pairs of primed and unprimed indices with: +`replaceprime(-s''-C-s-, 2 => 1) = -s'-C-s-`. + +Since this is a common use case, you can use the convenience function: + +```julia +C = apply(A, B; cutoff=1e-12) +``` + +which is the same as the code above. + +If you are contracting MPOs that have diverging norms, such as MPOs representing sums of local +operators, the truncation can become numerically unstable (see https://arxiv.org/abs/1909.06341 for +a more numerically stable alternative). For now, you can use the following options to contract +MPOs like that: + +```julia +C = contract(A, B; alg="naive", truncate=false) +# Bring the indices back to pairs of primed and unprimed +C = apply(A, B; alg="naive", truncate=false) +``` + +# Keywords +- `cutoff::Float64=1e-14`: the cutoff value for truncating the density matrix + eigenvalues. Note that the default is somewhat arbitrary and subject to change, + in general you should set a `cutoff` value. +- `maxdim::Int=maxlinkdim(A) * maxlinkdim(B))`: the maximal bond dimension of the results MPS. +- `mindim::Int=1`: the minimal bond dimension of the resulting MPS. +- `alg="zipup"`: Either `"zipup"` or `"naive"`. `"zipup"` contracts pairs of + site tensors and truncates with SVDs in a sweep across the sites, while `"naive"` + first contracts pairs of tensor exactly and then truncates at the end if `truncate=true`. +- `truncate=true`: Enable or disable truncation. If `truncate=false`, ignore + other truncation parameters like `cutoff` and `maxdim`. This is most relevant + for the `"naive"` version, if you just want to contract the tensors pairwise + exactly. This can be useful if you are contracting MPOs that have diverging + norms, such as MPOs originating from sums of local operators. + +See also [`apply`](@ref) for details about the arguments available. +""" + +@doc """ +$contract_mpo_mpo_doc +""" ITensors.contract(::MPO, ::MPO) + +*(A::MPO, B::MPO; kwargs...) = contract(A, B; kwargs...) + +# TODO: try this to copy the docstring +# Causing an error in Revise +#@doc """ +#$contract_mpo_mpo_doc +#""" *(::MPO, ::MPO) + +#@doc (@doc contract(::MPO, ::MPO)) *(::MPO, ::MPO) + +""" + sample(M::MPO) + +Given a normalized MPO `M`, +returns a `Vector{Int}` of `length(M)` +corresponding to one sample of the +probability distribution defined by the MPO, +treating the MPO as a density matrix. + +The MPO `M` should have an (approximately) +positive spectrum. +""" +function sample(M::MPO) + return sample(Random.default_rng(), M) +end + +function sample(rng::AbstractRNG, M::MPO) + N = length(M) + s = siteinds(M) + R = Vector{ITensor}(undef, N) + R[N] = M[N] * δ(dag(s[N])) + for n in reverse(1:(N - 1)) + R[n] = M[n] * δ(dag(s[n])) * R[n + 1] + end + + if abs(1.0 - R[1][]) > 1E-8 + error("sample: MPO is not normalized, norm=$(norm(M[1]))") + end + + result = zeros(Int, N) + ρj = M[1] * R[2] + Lj = ITensor() + + for j in 1:N + s = siteind(M, j) + d = dim(s) + # Compute the probability of each state + # one-by-one and stop when the random + # number r is below the total prob so far + pdisc = 0.0 + r = rand(rng) + # Will need n, An, and pn below + n = 1 + projn = ITensor() + pn = 0.0 + while n <= d + projn = ITensor(s) + projn[s => n] = 1.0 + pnc = (ρj * projn * prime(projn))[] + if imag(pnc) > 1e-8 + @warn "In sample, probability $pnc is complex." + end + pn = real(pnc) + pdisc += pn + (r < pdisc) && break + n += 1 + end + result[j] = n + if j < N + if j == 1 + Lj = M[j] * projn * prime(projn) + elseif j > 1 + Lj = Lj * M[j] * projn * prime(projn) + end + if j == N - 1 + ρj = Lj * M[j + 1] + else + ρj = Lj * M[j + 1] * R[j + 2] + end + s = siteind(M, j + 1) + normj = (ρj * δ(s', s))[] + ρj ./= normj + end + end + return result +end diff --git a/src/mps.jl b/src/mps.jl new file mode 100644 index 0000000..567319d --- /dev/null +++ b/src/mps.jl @@ -0,0 +1,1034 @@ +using Adapt: adapt +using NDTensors: using_auto_fermion +using Random: Random +using ITensors.SiteTypes: SiteTypes, siteind, siteinds, state + +""" + MPS + +A finite size matrix product state type. +Keeps track of the orthogonality center. +""" +mutable struct MPS <: AbstractMPS + data::Vector{ITensor} + llim::Int + rlim::Int +end + +function MPS(A::Vector{<:ITensor}; ortho_lims::UnitRange=1:length(A)) + return MPS(A, first(ortho_lims) - 1, last(ortho_lims) + 1) +end + +set_data(A::MPS, data::Vector{ITensor}) = MPS(data, A.llim, A.rlim) + +@doc """ + MPS(v::Vector{<:ITensor}) + +Construct an MPS from a Vector of ITensors. +""" MPS(v::Vector{<:ITensor}) + +""" + MPS() + +Construct an empty MPS with zero sites. +""" +MPS() = MPS(ITensor[], 0, 0) + +""" + MPS(N::Int) + +Construct an MPS with N sites with default constructed +ITensors. +""" +function MPS(N::Int; ortho_lims::UnitRange=1:N) + return MPS(Vector{ITensor}(undef, N); ortho_lims=ortho_lims) +end + +""" + MPS([::Type{ElT} = Float64, ]sites; linkdims=1) + +Construct an MPS filled with Empty ITensors of type `ElT` from a collection of indices. + +Optionally specify the link dimension with the keyword argument `linkdims`, which by default is 1. + +In the future we may generalize `linkdims` to allow specifying each individual link dimension as a vector, +and additionally allow specifying quantum numbers. +""" +function MPS( + ::Type{T}, sites::Vector{<:Index}; linkdims::Union{Integer,Vector{<:Integer}}=1 +) where {T<:Number} + _linkdims = _fill_linkdims(linkdims, sites) + N = length(sites) + v = Vector{ITensor}(undef, N) + if N == 1 + v[1] = ITensor(T, sites[1]) + return MPS(v) + end + + spaces = if hasqns(sites) + [[QN() => _linkdims[j]] for j in 1:(N - 1)] + else + [_linkdims[j] for j in 1:(N - 1)] + end + + l = [Index(spaces[ii], "Link,l=$ii") for ii in 1:(N - 1)] + for ii in eachindex(sites) + s = sites[ii] + if ii == 1 + v[ii] = ITensor(T, l[ii], s) + elseif ii == N + v[ii] = ITensor(T, dag(l[ii - 1]), s) + else + v[ii] = ITensor(T, dag(l[ii - 1]), s, l[ii]) + end + end + return MPS(v) +end + +MPS(sites::Vector{<:Index}, args...; kwargs...) = MPS(Float64, sites, args...; kwargs...) + +function randomU(eltype::Type{<:Number}, s1::Index, s2::Index) + return randomU(Random.default_rng(), eltype, s1, s2) +end + +function randomU(rng::AbstractRNG, eltype::Type{<:Number}, s1::Index, s2::Index) + if !hasqns(s1) && !hasqns(s2) + mdim = dim(s1) * dim(s2) + RM = randn(rng, eltype, mdim, mdim) + Q, _ = NDTensors.qr_positive(RM) + G = itensor(Q, dag(s1), dag(s2), s1', s2') + else + M = random_itensor(rng, eltype, QN(), s1', s2', dag(s1), dag(s2)) + U, S, V = svd(M, (s1', s2')) + u = commonind(U, S) + v = commonind(S, V) + replaceind!(U, u, v) + G = U * V + end + return G +end + +function randomizeMPS!(eltype::Type{<:Number}, M::MPS, sites::Vector{<:Index}, linkdims=1) + return randomizeMPS!(Random.default_rng(), eltype, M, sites, linkdims) +end + +function randomizeMPS!( + rng::AbstractRNG, eltype::Type{<:Number}, M::MPS, sites::Vector{<:Index}, linkdims=1 +) + _linkdims = _fill_linkdims(linkdims, sites) + if isone(length(sites)) + randn!(rng, M[1]) + normalize!(M) + return M + end + N = length(sites) + c = div(N, 2) + max_pass = 100 + for pass in 1:max_pass, half in 1:2 + if half == 1 + (db, brange) = (+1, 1:1:(N - 1)) + else + (db, brange) = (-1, N:-1:2) + end + for b in brange + s1 = sites[b] + s2 = sites[b + db] + G = randomU(rng, eltype, s1, s2) + T = noprime(G * M[b] * M[b + db]) + rinds = uniqueinds(M[b], M[b + db]) + + b_dim = half == 1 ? b : b + db + U, S, V = svd(T, rinds; maxdim=_linkdims[b_dim], utags="Link,l=$(b-1)") + M[b] = U + M[b + db] = S * V + M[b + db] /= norm(M[b + db]) + end + if half == 2 && dim(commonind(M[c], M[c + 1])) >= _linkdims[c] + break + end + end + setleftlim!(M, 0) + setrightlim!(M, 2) + if dim(commonind(M[c], M[c + 1])) < _linkdims[c] + @warn "MPS center bond dimension is less than requested (you requested $(_linkdims[c]), but in practice it is $(dim(commonind(M[c], M[c + 1]))). This is likely due to technicalities of truncating quantum number sectors." + end +end + +function randomCircuitMPS( + eltype::Type{<:Number}, sites::Vector{<:Index}, linkdims::Vector{<:Integer}; kwargs... +) + return randomCircuitMPS(Random.default_rng(), eltype, sites, linkdims; kwargs...) +end + +function randomCircuitMPS( + rng::AbstractRNG, + eltype::Type{<:Number}, + sites::Vector{<:Index}, + linkdims::Vector{<:Integer}; + kwargs..., +) + N = length(sites) + M = MPS(N) + + if N == 1 + M[1] = ITensor(randn(rng, eltype, dim(sites[1])), sites[1]) + M[1] /= norm(M[1]) + return M + end + + l = Vector{Index}(undef, N) + + d = dim(sites[N]) + chi = min(linkdims[N - 1], d) + l[N - 1] = Index(chi, "Link,l=$(N-1)") + O = NDTensors.random_unitary(rng, eltype, chi, d) + M[N] = itensor(O, l[N - 1], sites[N]) + + for j in (N - 1):-1:2 + chi *= dim(sites[j]) + chi = min(linkdims[j - 1], chi) + l[j - 1] = Index(chi, "Link,l=$(j-1)") + O = NDTensors.random_unitary(rng, eltype, chi, dim(sites[j]) * dim(l[j])) + T = reshape(O, (chi, dim(sites[j]), dim(l[j]))) + M[j] = itensor(T, l[j - 1], sites[j], l[j]) + end + + O = NDTensors.random_unitary(rng, eltype, 1, dim(sites[1]) * dim(l[1])) + l0 = Index(1, "Link,l=0") + T = reshape(O, (1, dim(sites[1]), dim(l[1]))) + M[1] = itensor(T, l0, sites[1], l[1]) + M[1] *= onehot(eltype, l0 => 1) + + M.llim = 0 + M.rlim = 2 + + return M +end + +function randomCircuitMPS(sites::Vector{<:Index}, linkdims::Vector{<:Integer}; kwargs...) + return randomCircuitMPS(Random.default_rng(), sites, linkdims; kwargs...) +end + +function randomCircuitMPS( + rng::AbstractRNG, sites::Vector{<:Index}, linkdims::Vector{<:Integer}; kwargs... +) + return randomCircuitMPS(rng, Float64, sites, linkdims; kwargs...) +end + +function _fill_linkdims(linkdims::Vector{<:Integer}, sites::Vector{<:Index}) + @assert length(linkdims) == length(sites) - 1 + return linkdims +end + +function _fill_linkdims(linkdims::Integer, sites::Vector{<:Index}) + return fill(linkdims, length(sites) - 1) +end + +""" + random_mps(eltype::Type{<:Number}, sites::Vector{<:Index}; linkdims=1) + +Construct a random MPS with link dimension `linkdims` of +type `eltype`. + +`linkdims` can also accept a `Vector{Int}` with +`length(linkdims) == length(sites) - 1` for constructing an +MPS with non-uniform bond dimension. +""" +function random_mps( + ::Type{ElT}, sites::Vector{<:Index}; linkdims::Union{Integer,Vector{<:Integer}}=1 +) where {ElT<:Number} + return random_mps(Random.default_rng(), ElT, sites; linkdims) +end + +function random_mps( + rng::AbstractRNG, + ::Type{ElT}, + sites::Vector{<:Index}; + linkdims::Union{Integer,Vector{<:Integer}}=1, +) where {ElT<:Number} + _linkdims = _fill_linkdims(linkdims, sites) + if any(hasqns, sites) + error("initial state required to use random_mps with QNs") + end + + # For non-QN-conserving MPS, instantiate + # the random MPS directly as a circuit: + return randomCircuitMPS(rng, ElT, sites, _linkdims) +end + +""" + random_mps(sites::Vector{<:Index}; linkdims=1) + random_mps(eltype::Type{<:Number}, sites::Vector{<:Index}; linkdims=1) + +Construct a random MPS with link dimension `linkdims` which by +default has element type `Float64`. + +`linkdims` can also accept a `Vector{Int}` with +`length(linkdims) == length(sites) - 1` for constructing an +MPS with non-uniform bond dimension. +""" +function random_mps(sites::Vector{<:Index}; linkdims::Union{Integer,Vector{<:Integer}}=1) + return random_mps(Random.default_rng(), sites; linkdims) +end + +function random_mps( + rng::AbstractRNG, sites::Vector{<:Index}; linkdims::Union{Integer,Vector{<:Integer}}=1 +) + return random_mps(rng, Float64, sites; linkdims) +end + +function random_mps( + sites::Vector{<:Index}, state; linkdims::Union{Integer,Vector{<:Integer}}=1 +) + return random_mps(Random.default_rng(), sites, state; linkdims) +end + +function random_mps( + rng::AbstractRNG, + sites::Vector{<:Index}, + state; + linkdims::Union{Integer,Vector{<:Integer}}=1, +) + return random_mps(rng, Float64, sites, state; linkdims) +end + +function random_mps( + eltype::Type{<:Number}, + sites::Vector{<:Index}, + state; + linkdims::Union{Integer,Vector{<:Integer}}=1, +) + return random_mps(Random.default_rng(), eltype, sites, state; linkdims) +end + +function random_mps( + rng::AbstractRNG, + eltype::Type{<:Number}, + sites::Vector{<:Index}, + state; + linkdims::Union{Integer,Vector{<:Integer}}=1, +)::MPS + M = MPS(eltype, sites, state) + if any(>(1), linkdims) + randomizeMPS!(rng, eltype, M, sites, linkdims) + end + return M +end + +@doc """ + random_mps(sites::Vector{<:Index}, state; linkdims=1) + +Construct a real, random MPS with link dimension `linkdims`, +made by randomizing an initial product state specified by +`state`. This version of `random_mps` is necessary when creating +QN-conserving random MPS (consisting of QNITensors). The initial +`state` array provided determines the total QN of the resulting +random MPS. +""" random_mps(::Vector{<:Index}, ::Any) + +""" + MPS(::Type{T<:Number}, ivals::Vector{<:Pair{<:Index}}) + +Construct a product state MPS with element type `T` and +nonzero values determined from the input IndexVals. +""" +function MPS(::Type{T}, ivals::Vector{<:Pair{<:Index}}) where {T<:Number} + N = length(ivals) + M = MPS(N) + + if N == 1 + M[1] = ITensor(T, ind(ivals[1])) + M[1][ivals[1]] = one(T) + return M + end + + if hasqns(ind(ivals[1])) + lflux = QN() + for j in 1:(N - 1) + lflux += qn(ivals[j]) + end + links = Vector{QNIndex}(undef, N - 1) + for j in (N - 1):-1:1 + links[j] = dag(Index(lflux => 1; tags="Link,l=$j")) + lflux -= qn(ivals[j]) + end + else + links = [Index(1, "Link,l=$n") for n in 1:(N - 1)] + end + + M[1] = ITensor(T, ind(ivals[1]), links[1]) + M[1][ivals[1], links[1] => 1] = one(T) + for n in 2:(N - 1) + s = ind(ivals[n]) + M[n] = ITensor(T, dag(links[n - 1]), s, links[n]) + M[n][links[n - 1] => 1, ivals[n], links[n] => 1] = one(T) + end + M[N] = ITensor(T, dag(links[N - 1]), ind(ivals[N])) + M[N][links[N - 1] => 1, ivals[N]] = one(T) + + return M +end + +# For backwards compatibility +const productMPS = MPS + +""" + MPS(ivals::Vector{<:Pair{<:Index}}) + +Construct a product state MPS with element type `Float64` and +nonzero values determined from the input IndexVals. +""" +MPS(ivals::Vector{<:Pair{<:Index}}) = MPS(Float64, ivals) + +""" + MPS(::Type{T}, + sites::Vector{<:Index}, + states::Union{Vector{String}, + Vector{Int}, + String, + Int}) + +Construct a product state MPS of element type `T`, having +site indices `sites`, and which corresponds to the initial +state given by the array `states`. The input `states` may +be an array of strings or an array of ints recognized by the +`state` function defined for the relevant Index tag type. +In addition, a single string or int can be input to create +a uniform state. + +# Examples + +```julia +N = 10 +sites = siteinds("S=1/2", N) +states = [isodd(n) ? "Up" : "Dn" for n in 1:N] +psi = MPS(ComplexF64, sites, states) +phi = MPS(sites, "Up") +``` +""" +function MPS(eltype::Type{<:Number}, sites::Vector{<:Index}, states_) + if length(sites) != length(states_) + throw(DimensionMismatch("Number of sites and and initial vals don't match")) + end + N = length(states_) + M = MPS(N) + + if N == 1 + M[1] = state(sites[1], states_[1]) + return convert_leaf_eltype(eltype, M) + end + + states = [state(sites[j], states_[j]) for j in 1:N] + + if hasqns(states[1]) + lflux = QN() + for j in 1:(N - 1) + lflux += flux(states[j]) + end + links = Vector{QNIndex}(undef, N - 1) + for j in (N - 1):-1:1 + links[j] = dag(Index(lflux => 1; tags="Link,l=$j")) + lflux -= flux(states[j]) + end + else + links = [Index(1; tags="Link,l=$n") for n in 1:N] + end + + M[1] = ITensor(sites[1], links[1]) + M[1] += states[1] * state(links[1], 1) + for n in 2:(N - 1) + M[n] = ITensor(dag(links[n - 1]), sites[n], links[n]) + M[n] += state(dag(links[n - 1]), 1) * states[n] * state(links[n], 1) + end + M[N] = ITensor(dag(links[N - 1]), sites[N]) + M[N] += state(dag(links[N - 1]), 1) * states[N] + + return convert_leaf_eltype(eltype, M) +end + +function MPS( + ::Type{T}, sites::Vector{<:Index}, state::Union{String,Integer} +) where {T<:Number} + return MPS(T, sites, fill(state, length(sites))) +end + +function MPS(::Type{T}, sites::Vector{<:Index}, states::Function) where {T<:Number} + states_vec = [states(n) for n in 1:length(sites)] + return MPS(T, sites, states_vec) +end + +""" + MPS(sites::Vector{<:Index},states) + +Construct a product state MPS having +site indices `sites`, and which corresponds to the initial +state given by the array `states`. The `states` array may +consist of either an array of integers or strings, as +recognized by the `state` function defined for the relevant +Index tag type. + +# Examples + +```julia +N = 10 +sites = siteinds("S=1/2", N) +states = [isodd(n) ? "Up" : "Dn" for n in 1:N] +psi = MPS(sites, states) +``` +""" +MPS(sites::Vector{<:Index}, states) = MPS(Float64, sites, states) + +""" + siteind(M::MPS, j::Int; kwargs...) + +Get the first site Index of the MPS. Return `nothing` if none is found. +""" +SiteTypes.siteind(M::MPS, j::Int; kwargs...) = siteind(first, M, j; kwargs...) + +""" + siteind(::typeof(only), M::MPS, j::Int; kwargs...) + +Get the only site Index of the MPS. Return `nothing` if none is found. +""" +function SiteTypes.siteind(::typeof(only), M::MPS, j::Int; kwargs...) + is = siteinds(M, j; kwargs...) + if isempty(is) + return nothing + end + return only(is) +end + +""" + siteinds(M::MPS) + siteinds(::typeof(first), M::MPS) + +Get a vector of the first site Index found on each tensor of the MPS. + + siteinds(::typeof(only), M::MPS) + +Get a vector of the only site Index found on each tensor of the MPS. Errors if more than one is found. + + siteinds(::typeof(all), M::MPS) + +Get a vector of the all site Indices found on each tensor of the MPS. Returns a Vector of IndexSets. +""" +SiteTypes.siteinds(M::MPS; kwargs...) = siteinds(first, M; kwargs...) + +function replace_siteinds!(M::MPS, sites) + for j in eachindex(M) + sj = only(siteinds(M, j)) + M[j] = replaceinds(M[j], sj => sites[j]) + end + return M +end + +replace_siteinds(M::MPS, sites) = replace_siteinds!(copy(M), sites) + +""" + replacebond!(M::MPS, b::Int, phi::ITensor; kwargs...) + +Factorize the ITensor `phi` and replace the ITensors +`b` and `b+1` of MPS `M` with the factors. Choose +the orthogonality with `ortho="left"/"right"`. +""" +function replacebond!( + M::MPS, + b::Int, + phi::ITensor; + normalize=nothing, + swapsites=nothing, + ortho=nothing, + # Decomposition kwargs + which_decomp=nothing, + mindim=nothing, + maxdim=nothing, + cutoff=nothing, + eigen_perturbation=nothing, + # svd kwargs + svd_alg=nothing, + use_absolute_cutoff=nothing, + use_relative_cutoff=nothing, + min_blockdim=nothing, +) + normalize = NDTensors.replace_nothing(normalize, false) + swapsites = NDTensors.replace_nothing(swapsites, false) + ortho = NDTensors.replace_nothing(ortho, "left") + + indsMb = inds(M[b]) + if swapsites + sb = siteind(M, b) + sbp1 = siteind(M, b + 1) + indsMb = replaceind(indsMb, sb, sbp1) + end + L, R, spec = factorize( + phi, + indsMb; + mindim, + maxdim, + cutoff, + ortho, + which_decomp, + eigen_perturbation, + svd_alg, + tags=tags(linkind(M, b)), + use_absolute_cutoff, + use_relative_cutoff, + min_blockdim, + ) + M[b] = L + M[b + 1] = R + if ortho == "left" + leftlim(M) == b - 1 && setleftlim!(M, leftlim(M) + 1) + rightlim(M) == b + 1 && setrightlim!(M, rightlim(M) + 1) + normalize && (M[b + 1] ./= norm(M[b + 1])) + elseif ortho == "right" + leftlim(M) == b && setleftlim!(M, leftlim(M) - 1) + rightlim(M) == b + 2 && setrightlim!(M, rightlim(M) - 1) + normalize && (M[b] ./= norm(M[b])) + else + error( + "In replacebond!, got ortho = $ortho, only currently supports `left` and `right`." + ) + end + return spec +end + +""" + replacebond(M::MPS, b::Int, phi::ITensor; kwargs...) + +Like `replacebond!`, but returns the new MPS. +""" +function replacebond(M0::MPS, b::Int, phi::ITensor; kwargs...) + M = copy(M0) + replacebond!(M, b, phi; kwargs...) + return M +end + +# Allows overloading `replacebond!` based on the projected +# MPO type. By default just calls `replacebond!` on the MPS. +function replacebond!(PH, M::MPS, b::Int, phi::ITensor; kwargs...) + return replacebond!(M, b, phi; kwargs...) +end + +""" + sample!(m::MPS) + +Given a normalized MPS m, returns a `Vector{Int}` +of `length(m)` corresponding to one sample +of the probability distribution defined by +squaring the components of the tensor +that the MPS represents. If the MPS does +not have an orthogonality center, +orthogonalize!(m,1) will be called before +computing the sample. +""" +function sample!(m::MPS) + return sample!(Random.default_rng(), m) +end + +function sample!(rng::AbstractRNG, m::MPS) + orthogonalize!(m, 1) + return sample(rng, m) +end + +""" + sample(m::MPS) + +Given a normalized MPS m with `orthocenter(m)==1`, +returns a `Vector{Int}` of `length(m)` +corresponding to one sample of the +probability distribution defined by +squaring the components of the tensor +that the MPS represents +""" +function sample(m::MPS) + return sample(Random.default_rng(), m) +end + +function sample(rng::AbstractRNG, m::MPS) + N = length(m) + + if orthocenter(m) != 1 + error("sample: MPS m must have orthocenter(m)==1") + end + if abs(1.0 - norm(m[1])) > 1E-8 + error("sample: MPS is not normalized, norm=$(norm(m[1]))") + end + + result = zeros(Int, N) + A = m[1] + + for j in 1:N + s = siteind(m, j) + d = dim(s) + # Compute the probability of each state + # one-by-one and stop when the random + # number r is below the total prob so far + pdisc = 0.0 + r = rand(rng) + # Will need n,An, and pn below + n = 1 + An = ITensor() + pn = 0.0 + while n <= d + projn = ITensor(s) + projn[s => n] = 1.0 + An = A * dag(projn) + pn = real(scalar(dag(An) * An)) + pdisc += pn + (r < pdisc) && break + n += 1 + end + result[j] = n + if j < N + A = m[j + 1] * An + A *= (1.0 / sqrt(pn)) + end + end + return result +end + +_op_prod(o1::AbstractString, o2::AbstractString) = "$o1 * $o2" +_op_prod(o1::Matrix{<:Number}, o2::Matrix{<:Number}) = o1 * o2 + +""" + correlation_matrix(psi::MPS, + Op1::AbstractString, + Op2::AbstractString; + kwargs...) + + correlation_matrix(psi::MPS, + Op1::Matrix{<:Number}, + Op2::Matrix{<:Number}; + kwargs...) + +Given an MPS psi and two strings denoting +operators (as recognized by the `op` function), +computes the two-point correlation function matrix +C[i,j] = +using efficient MPS techniques. Returns the matrix C. + +# Optional Keyword Arguments + + - `sites = 1:length(psi)`: compute correlations only + for sites in the given range + - `ishermitian = false` : if `false`, force independent calculations of the + matrix elements above and below the diagonal, while if `true` assume they are complex conjugates. + +For a correlation matrix of size NxN and an MPS of typical +bond dimension m, the scaling of this algorithm is N^2*m^3. + +# Examples + +```julia +N = 30 +m = 4 + +s = siteinds("S=1/2", N) +psi = random_mps(s; linkdims=m) +Czz = correlation_matrix(psi, "Sz", "Sz") +Czz = correlation_matrix(psi, [1/2 0; 0 -1/2], [1/2 0; 0 -1/2]) # same as above + +s = siteinds("Electron", N; conserve_qns=true) +psi = random_mps(s, n -> isodd(n) ? "Up" : "Dn"; linkdims=m) +Cuu = correlation_matrix(psi, "Cdagup", "Cup"; sites=2:8) +``` +""" +function correlation_matrix( + psi::MPS, _Op1, _Op2; sites=1:length(psi), site_range=nothing, ishermitian=nothing +) + if !isnothing(site_range) + @warn "The `site_range` keyword arg. to `correlation_matrix` is deprecated: use the keyword `sites` instead" + sites = site_range + end + if !(sites isa AbstractRange) + sites = collect(sites) + end + + start_site = first(sites) + end_site = last(sites) + + N = length(psi) + ElT = promote_itensor_eltype(psi) + s = siteinds(psi) + + Op1 = _Op1 #make copies into which we can insert "F" string operators, and then restore. + Op2 = _Op2 + onsiteOp = _op_prod(Op1, Op2) + fermionic1 = has_fermion_string(Op1, s[start_site]) + fermionic2 = has_fermion_string(Op2, s[end_site]) + if fermionic1 != fermionic2 + error( + "correlation_matrix: Mixed fermionic and bosonic operators are not supported yet." + ) + end + + # Decide if we need to calculate a non-hermitian corr. matrix, which is roughly double the work. + is_cm_hermitian = ishermitian + if isnothing(is_cm_hermitian) + # Assume correlation matrix is non-hermitian + is_cm_hermitian = false + O1 = op(Op1, s, start_site) + O2 = op(Op2, s, start_site) + O1 /= norm(O1) + O2 /= norm(O2) + #We need to decide if O1 ∝ O2 or O1 ∝ O2^dagger allowing for some round off errors. + eps = 1e-10 + is_op_proportional = norm(O1 - O2) < eps + is_op_hermitian = norm(O1 - dag(swapprime(O2, 0, 1))) < eps + if is_op_proportional || is_op_hermitian + is_cm_hermitian = true + end + # finally if they are both fermionic and proportional then the corr matrix will + # be anti symmetric insterad of Hermitian. Handle things like + # at this point we know fermionic2=fermionic1, but we put them both in the if + # to clarify the meaning of what we are doing. + if is_op_proportional && fermionic1 && fermionic2 + is_cm_hermitian = false + end + end + + psi = orthogonalize(psi, start_site) + norm2_psi = norm(psi[start_site])^2 + + # Nb = size of block of correlation matrix + Nb = length(sites) + + C = zeros(ElT, Nb, Nb) + + if start_site == 1 + L = ITensor(1.0) + else + lind = commonind(psi[start_site], psi[start_site - 1]) + L = delta(dag(lind), lind') + end + pL = start_site - 1 + + for (ni, i) in enumerate(sites[1:(end - 1)]) + while pL < i - 1 + pL += 1 + sᵢ = siteind(psi, pL) + L = (L * psi[pL]) * prime(dag(psi[pL]), !sᵢ) + end + + Li = L * psi[i] + + # Get j == i diagonal correlations + rind = commonind(psi[i], psi[i + 1]) + oᵢ = adapt(datatype(Li), op(onsiteOp, s, i)) + C[ni, ni] = ((Li * oᵢ) * prime(dag(psi[i]), !rind))[] / norm2_psi + + # Get j > i correlations + if !using_auto_fermion() && fermionic2 + Op1 = "$Op1 * F" + end + + oᵢ = adapt(datatype(Li), op(Op1, s, i)) + + Li12 = (dag(psi[i])' * oᵢ) * Li + pL12 = i + + for (n, j) in enumerate(sites[(ni + 1):end]) + nj = ni + n + + while pL12 < j - 1 + pL12 += 1 + if !using_auto_fermion() && fermionic2 + oᵢ = adapt(datatype(psi[pL12]), op("F", s[pL12])) + Li12 *= (oᵢ * dag(psi[pL12])') + else + sᵢ = siteind(psi, pL12) + Li12 *= prime(dag(psi[pL12]), !sᵢ) + end + Li12 *= psi[pL12] + end + + lind = commonind(psi[j], Li12) + Li12 *= psi[j] + + oⱼ = adapt(datatype(Li12), op(Op2, s, j)) + sⱼ = siteind(psi, j) + val = (Li12 * oⱼ) * prime(dag(psi[j]), (sⱼ, lind)) + + # XXX: This gives a different fermion sign with + # ITensors.enable_auto_fermion() + # val = prime(dag(psi[j]), (sⱼ, lind)) * (oⱼ * Li12) + + C[ni, nj] = scalar(val) / norm2_psi + if is_cm_hermitian + C[nj, ni] = conj(C[ni, nj]) + end + + pL12 += 1 + if !using_auto_fermion() && fermionic2 + oᵢ = adapt(datatype(psi[pL12]), op("F", s[pL12])) + Li12 *= (oᵢ * dag(psi[pL12])') + else + sᵢ = siteind(psi, pL12) + Li12 *= prime(dag(psi[pL12]), !sᵢ) + end + @assert pL12 == j + end #for j + Op1 = _Op1 #"Restore Op1 with no Fs" + + if !is_cm_hermitian #If isHermitian=false the we must calculate the below diag elements explicitly. + + # Get j < i correlations by swapping the operators + if !using_auto_fermion() && fermionic1 + Op2 = "$Op2 * F" + end + oᵢ = adapt(datatype(psi[i]), op(Op2, s, i)) + Li21 = (Li * oᵢ) * dag(psi[i])' + pL21 = i + if !using_auto_fermion() && fermionic1 + Li21 = -Li21 #Required because we swapped fermionic ops, instead of sweeping right to left. + end + + for (n, j) in enumerate(sites[(ni + 1):end]) + nj = ni + n + + while pL21 < j - 1 + pL21 += 1 + if !using_auto_fermion() && fermionic1 + oᵢ = adapt(datatype(psi[pL21]), op("F", s[pL21])) + Li21 *= oᵢ * dag(psi[pL21])' + else + sᵢ = siteind(psi, pL21) + Li21 *= prime(dag(si[pL21]), !sᵢ) + end + Li21 *= psi[pL21] + end + + lind = commonind(psi[j], Li21) + Li21 *= psi[j] + + oⱼ = adapt(datatype(psi[j]), op(Op1, s, j)) + sⱼ = siteind(psi, j) + val = (prime(dag(psi[j]), (sⱼ, lind)) * (oⱼ * Li21))[] + C[nj, ni] = val / norm2_psi + + pL21 += 1 + if !using_auto_fermion() && fermionic1 + oᵢ = adapt(datatype(psi[pL21]), op("F", s[pL21])) + Li21 *= (oᵢ * dag(psi[pL21])') + else + sᵢ = siteind(psi, pL21) + Li21 *= prime(dag(psi[pL21]), !sᵢ) + end + @assert pL21 == j + end #for j + Op2 = _Op2 #"Restore Op2 with no Fs" + end #if is_cm_hermitian + + pL += 1 + sᵢ = siteind(psi, i) + L = Li * prime(dag(psi[i]), !sᵢ) + end #for i + + # Get last diagonal element of C + i = end_site + while pL < i - 1 + pL += 1 + sᵢ = siteind(psi, pL) + L = L * psi[pL] * prime(dag(psi[pL]), !sᵢ) + end + lind = commonind(psi[i], psi[i - 1]) + oᵢ = adapt(datatype(psi[i]), op(onsiteOp, s, i)) + sᵢ = siteind(psi, i) + val = (L * (oᵢ * psi[i]) * prime(dag(psi[i]), (sᵢ, lind)))[] + C[Nb, Nb] = val / norm2_psi + + return C +end + +""" + expect(psi::MPS, op::AbstractString...; kwargs...) + expect(psi::MPS, op::Matrix{<:Number}...; kwargs...) + expect(psi::MPS, ops; kwargs...) + +Given an MPS `psi` and a single operator name, returns +a vector of the expected value of the operator on +each site of the MPS. + +If multiple operator names are provided, returns a tuple +of expectation value vectors. + +If a container of operator names is provided, returns the +same type of container with names replaced by vectors +of expectation values. + +# Optional Keyword Arguments + + - `sites = 1:length(psi)`: compute expected values only for sites in the given range + +# Examples + +```julia +N = 10 + +s = siteinds("S=1/2", N) +psi = random_mps(s; linkdims=8) +Z = expect(psi, "Sz") # compute for all sites +Z = expect(psi, "Sz"; sites=2:4) # compute for sites 2,3,4 +Z3 = expect(psi, "Sz"; sites=3) # compute for site 3 only (output will be a scalar) +XZ = expect(psi, ["Sx", "Sz"]) # compute Sx and Sz for all sites +Z = expect(psi, [1/2 0; 0 -1/2]) # same as expect(psi,"Sz") + +s = siteinds("Electron", N) +psi = random_mps(s; linkdims=8) +dens = expect(psi, "Ntot") +updens, dndens = expect(psi, "Nup", "Ndn") # pass more than one operator +``` +""" +function expect(psi::MPS, ops; sites=1:length(psi), site_range=nothing) + psi = copy(psi) + N = length(psi) + ElT = promote_itensor_eltype(psi) + s = siteinds(psi) + + if !isnothing(site_range) + @warn "The `site_range` keyword arg. to `expect` is deprecated: use the keyword `sites` instead" + sites = site_range + end + + site_range = (sites isa AbstractRange) ? sites : collect(sites) + Ns = length(site_range) + start_site = first(site_range) + + el_types = map(o -> ishermitian(op(o, s[start_site])) ? real(ElT) : ElT, ops) + + psi = orthogonalize(psi, start_site) + norm2_psi = norm(psi)^2 + iszero(norm2_psi) && error("MPS has zero norm in function `expect`") + + ex = map((o, el_t) -> zeros(el_t, Ns), ops, el_types) + for (entry, j) in enumerate(site_range) + psi = orthogonalize(psi, j) + for (n, opname) in enumerate(ops) + oⱼ = adapt(datatype(psi[j]), op(opname, s[j])) + val = inner(psi[j], apply(oⱼ, psi[j])) / norm2_psi + ex[n][entry] = (el_types[n] <: Real) ? real(val) : val + end + end + + if sites isa Number + return map(arr -> arr[1], ex) + end + return ex +end + +function expect(psi::MPS, op::AbstractString; kwargs...) + return first(expect(psi, (op,); kwargs...)) +end + +function expect(psi::MPS, op::Matrix{<:Number}; kwargs...) + return first(expect(psi, (op,); kwargs...)) +end + +function expect(psi::MPS, op1::AbstractString, ops::AbstractString...; kwargs...) + return expect(psi, (op1, ops...); kwargs...) +end + +function expect(psi::MPS, op1::Matrix{<:Number}, ops::Matrix{<:Number}...; kwargs...) + return expect(psi, (op1, ops...); kwargs...) +end diff --git a/src/observer.jl b/src/observer.jl new file mode 100644 index 0000000..4436d36 --- /dev/null +++ b/src/observer.jl @@ -0,0 +1,190 @@ + +abstract type AbstractObserver end + +measure!(o::AbstractObserver; kwargs...) = nothing +checkdone!(o::AbstractObserver; kwargs...) = false + +""" +NoObserver is a trivial implementation of an +observer type which can be used as a default +argument for DMRG routines taking an AbstractObserver +""" +struct NoObserver <: AbstractObserver end + +""" +A DMRGMeasurement object is an alias for `Vector{Vector{Float64}}`, +in other words an array of arrays of real numbers. + +Given a DMRGMeasurement `M`,the result for the +measurement on sweep `n` and site `i` as `M[n][i]`. +""" +const DMRGMeasurement = Vector{Vector{Float64}} + +""" +DMRGObserver is an implementation of an +observer object (<:AbstractObserver) which +implements custom measurements and allows +the `dmrg` function to return early if an +energy convergence criterion is met. +""" +struct DMRGObserver{T} <: AbstractObserver + ops::Vector{String} + sites::Vector{<:Index} + measurements::Dict{String,DMRGMeasurement} + energies::Vector{T} + truncerrs::Vector{Float64} + etol::Float64 + minsweeps::Int64 +end + +""" + DMRGObserver(;energy_tol=0.0, + minsweeps=2, + energy_type=Float64) + +Construct a DMRGObserver by providing the energy +tolerance used for early stopping, and minimum number +of sweeps that must be done. + +Optional keyword arguments: + + - energy_tol: if the energy from one sweep to the + next no longer changes by more than this amount, + stop after the current sweep + - minsweeps: do at least this many sweeps + - energy_type: type to use when storing energies at each step +""" +function DMRGObserver(; energy_tol=0.0, minsweeps=2, energy_type=Float64) + return DMRGObserver( + String[], + Index[], + Dict{String,DMRGMeasurement}(), + energy_type[], + Float64[], + energy_tol, + minsweeps, + ) +end + +""" + DMRGObserver(ops::Vector{String}, + sites::Vector{<:Index}; + energy_tol=0.0, + minsweeps=2, + energy_type=Float64) + +Construct a DMRGObserver, provide an array +of `ops` of operator names which are strings +recognized by the `op` function. Each of +these operators will be measured on every site +during every step of DMRG and the results +recorded inside the DMRGOberver for later +analysis. The array `sites` is the basis +of sites used to define the MPS and MPO for +the DMRG calculation. + +Optionally, one can provide an energy +tolerance used for early stopping, and minimum number +of sweeps that must be done. + +Optional keyword arguments: + + - energy_tol: if the energy from one sweep to the + next no longer changes by more than this amount, + stop after the current sweep + - minsweeps: do at least this many sweeps + - energy_type: type to use when storing energies at each step +""" +function DMRGObserver( + ops::Vector{String}, + sites::Vector{<:Index}; + energy_tol=0.0, + minsweeps=2, + energy_type=Float64, +) + measurements = Dict(o => DMRGMeasurement() for o in ops) + return DMRGObserver{energy_type}( + ops, sites, measurements, energy_type[], Float64[], energy_tol, minsweeps + ) +end + +""" + measurements(o::DMRGObserver) + +After using a DMRGObserver object `o` within +a DMRG calculation, retrieve a dictionary +of measurement results, with the keys being +operator names and values being DMRGMeasurement +objects. +""" +measurements(o::DMRGObserver) = o.measurements + +""" + energies(o::DMRGObserver) + +After using a DMRGObserver object `o` within +a DMRG calculation, retrieve an array of the +energy after each sweep. +""" +energies(o::DMRGObserver) = o.energies + +observer_sites(obs::DMRGObserver) = obs.sites + +observer_ops(obs::DMRGObserver) = obs.ops + +truncerrors(obs::DMRGObserver) = obs.truncerrs + +function measurelocalops!(obs::DMRGObserver, wf::ITensor, i::Int) + for o in observer_ops(obs) + # Moves to GPU if needed + oⱼ = adapt(datatype(wf), op(observer_sites(obs), o, i)) + m = dot(wf, apply(oⱼ, wf)) + imag(m) > 1e-8 && (@warn "encountered finite imaginary part when measuring $o") + measurements(obs)[o][end][i] = real(m) + end +end + +function measure!(obs::DMRGObserver; kwargs...) + half_sweep = kwargs[:half_sweep] + b = kwargs[:bond] + energy = kwargs[:energy] + psi = kwargs[:psi] + truncerr = truncerror(kwargs[:spec]) + + if half_sweep == 2 + N = length(psi) + + if b == (N - 1) + for o in observer_ops(obs) + push!(measurements(obs)[o], zeros(N)) + end + push!(truncerrors(obs), 0.0) + end + + # when sweeping left the orthogonality center is located + # at site n=b after the bond update. + # We want to measure at n=b+1 because there the tensor has been + # already fully updated (by the right and left pass of the sweep). + wf = psi[b] * psi[b + 1] + measurelocalops!(obs, wf, b + 1) + + if b == 1 + push!(energies(obs), energy) + measurelocalops!(obs, wf, b) + end + truncerr > truncerrors(obs)[end] && (truncerrors(obs)[end] = truncerr) + end +end + +function checkdone!( + o::DMRGObserver; outputlevel=false, energy=nothing, psi=nothing, sweep=nothing +) + if ( + length(real(energies(o))) > o.minsweeps && + abs(real(energies(o))[end] - real(energies(o))[end - 1]) < o.etol + ) + outputlevel > 0 && println("Energy difference less than $(o.etol), stopping DMRG") + return true + end + return false +end diff --git a/src/opsum_to_mpo/matelem.jl b/src/opsum_to_mpo/matelem.jl new file mode 100644 index 0000000..5baa776 --- /dev/null +++ b/src/opsum_to_mpo/matelem.jl @@ -0,0 +1,40 @@ +################################## +# MatElem (simple sparse matrix) # +################################## + +struct MatElem{T} + row::Int + col::Int + val::T +end + +#function Base.show(io::IO,m::MatElem) +# print(io,"($(m.row),$(m.col),$(m.val))") +#end + +function toMatrix(els::Vector{MatElem{T}})::Matrix{T} where {T} + nr = 0 + nc = 0 + for el in els + nr = max(nr, el.row) + nc = max(nc, el.col) + end + M = zeros(T, nr, nc) + for el in els + M[el.row, el.col] = el.val + end + return M +end + +function Base.:(==)(m1::MatElem{T}, m2::MatElem{T})::Bool where {T} + return (m1.row == m2.row && m1.col == m2.col && m1.val == m2.val) +end + +function Base.isless(m1::MatElem{T}, m2::MatElem{T})::Bool where {T} + if m1.row != m2.row + return m1.row < m2.row + elseif m1.col != m2.col + return m1.col < m2.col + end + return m1.val < m2.val +end diff --git a/src/opsum_to_mpo/opsum_to_mpo.jl b/src/opsum_to_mpo/opsum_to_mpo.jl new file mode 100644 index 0000000..114eec3 --- /dev/null +++ b/src/opsum_to_mpo/opsum_to_mpo.jl @@ -0,0 +1,148 @@ +using NDTensors: using_auto_fermion + +# `ValType::Type{<:Number}` is used instead of `ValType::Type` for efficiency, possibly due to increased method specialization. +# See https://github.com/ITensor/ITensors.jl/pull/1183. +function svdMPO( + ValType::Type{<:Number}, os::OpSum{C}, sites; mindim=1, maxdim=typemax(Int), cutoff=1e-15 +)::MPO where {C} + N = length(sites) + + # Specifying the element type with `Matrix{ValType}[...]` improves type inference and therefore efficiency. + # See https://github.com/ITensor/ITensors.jl/pull/1183. + Vs = Matrix{ValType}[Matrix{ValType}(undef, 1, 1) for n in 1:N] + tempMPO = [MatElem{Scaled{C,Prod{Op}}}[] for n in 1:N] + + function crosses_bond(t::Scaled{C,Prod{Op}}, n::Int) where {C} + return (only(site(t[1])) <= n <= only(site(t[end]))) + end + + rightmaps = [Dict{Vector{Op},Int}() for _ in 1:N] + + for n in 1:N + leftbond_coefs = MatElem{ValType}[] + + leftmap = Dict{Vector{Op},Int}() + for term in os + crosses_bond(term, n) || continue + + left = filter(t -> (only(site(t)) < n), terms(term)) + onsite = filter(t -> (only(site(t)) == n), terms(term)) + right = filter(t -> (only(site(t)) > n), terms(term)) + + bond_row = -1 + bond_col = -1 + if !isempty(left) + bond_row = posInLink!(leftmap, left) + bond_col = posInLink!(rightmaps[n - 1], vcat(onsite, right)) + bond_coef = convert(ValType, coefficient(term)) + push!(leftbond_coefs, MatElem(bond_row, bond_col, bond_coef)) + end + + A_row = bond_col + A_col = posInLink!(rightmaps[n], right) + site_coef = one(C) + if A_row == -1 + site_coef = coefficient(term) + end + if isempty(onsite) + if !using_auto_fermion() && isfermionic(right, sites) + push!(onsite, Op("F", n)) + else + push!(onsite, Op("Id", n)) + end + end + el = MatElem(A_row, A_col, site_coef * Prod(onsite)) + push!(tempMPO[n], el) + end + remove_dups!(tempMPO[n]) + if n > 1 && !isempty(leftbond_coefs) + M = toMatrix(leftbond_coefs) + U, S, V = svd(M) + P = S .^ 2 + truncate!(P; maxdim=maxdim, cutoff=cutoff, mindim=mindim) + tdim = length(P) + nc = size(M, 2) + Vs[n - 1] = Matrix{ValType}(V[1:nc, 1:tdim]) + end + end + + llinks = Vector{Index{Int}}(undef, N + 1) + llinks[1] = Index(2, "Link,l=0") + + H = MPO(sites) + + for n in 1:N + VL = Matrix{ValType}(undef, 1, 1) + if n > 1 + VL = Vs[n - 1] + end + VR = Vs[n] + tdim = isempty(rightmaps[n]) ? 0 : size(VR, 2) + + llinks[n + 1] = Index(2 + tdim, "Link,l=$n") + + ll = llinks[n] + rl = llinks[n + 1] + + H[n] = ITensor() + + for el in tempMPO[n] + A_row = el.row + A_col = el.col + t = el.val + (abs(coefficient(t)) > eps()) || continue + + M = zeros(ValType, dim(ll), dim(rl)) + + ct = convert(ValType, coefficient(t)) + if A_row == -1 && A_col == -1 #onsite term + M[end, 1] += ct + elseif A_row == -1 #term starting on site n + for c in 1:size(VR, 2) + z = ct * VR[A_col, c] + M[end, 1 + c] += z + end + elseif A_col == -1 #term ending on site n + for r in 1:size(VL, 2) + z = ct * conj(VL[A_row, r]) + M[1 + r, 1] += z + end + else + for r in 1:size(VL, 2), c in 1:size(VR, 2) + z = ct * conj(VL[A_row, r]) * VR[A_col, c] + M[1 + r, 1 + c] += z + end + end + + T = itensor(M, ll, rl) + H[n] += T * computeSiteProd(sites, argument(t)) + end + + # + # Special handling of starting and + # ending identity operators: + # + idM = zeros(ValType, dim(ll), dim(rl)) + idM[1, 1] = 1.0 + idM[end, end] = 1.0 + T = itensor(idM, ll, rl) + H[n] += T * computeSiteProd(sites, Prod([Op("Id", n)])) + end + + L = ITensor(llinks[1]) + L[end] = 1.0 + + R = ITensor(llinks[N + 1]) + R[1] = 1.0 + + H[1] *= L + H[N] *= R + + return H +end #svdMPO + +function svdMPO(os::OpSum{C}, sites; kwargs...)::MPO where {C} + # Function barrier to improve type stability + ValType = determineValType(terms(os)) + return svdMPO(ValType, os, sites; kwargs...) +end diff --git a/src/opsum_to_mpo/opsum_to_mpo_generic.jl b/src/opsum_to_mpo/opsum_to_mpo_generic.jl new file mode 100644 index 0000000..915fd0e --- /dev/null +++ b/src/opsum_to_mpo/opsum_to_mpo_generic.jl @@ -0,0 +1,349 @@ +using NDTensors: using_auto_fermion +using ITensors.Ops: Ops, Op, OpSum, Scaled, Sum, coefficient +using ITensors.SiteTypes: has_fermion_string, op + +# TODO: Deprecate. +const AutoMPO = OpSum + +""" + add!(opsum::OpSum, + op1::String, i1::Int) + + add!(opsum::OpSum, + coef::Number, + op1::String, i1::Int) + + add!(opsum::OpSum, + op1::String, i1::Int, + op2::String, i2::Int, + ops...) + + add!(opsum::OpSum, + coef::Number, + op1::String, i1::Int, + op2::String, i2::Int, + ops...) + + +(opsum:OpSum, term::Tuple) + +Add a single- or multi-site operator +term to the OpSum `opsum`. Each operator +is specified by a name (String) and a +site number (Int). The second version +accepts a real or complex coefficient. + +The `+` operator version of this function +accepts a tuple with entries either +(String,Int,String,Int,...) or +(Number,String,Int,String,Int,...) +where these tuple values are the same +as valid inputs to the `add!` function. +For inputting a very large number of +terms (tuples) to an OpSum, consider +using the broadcasted operator `.+=` +which avoids reallocating the OpSum +after each addition. + +# Examples +```julia +opsum = OpSum() + +add!(opsum,"Sz",2,"Sz",3) + +opsum += ("Sz",3,"Sz",4) + +opsum += (0.5,"S+",4,"S-",5) + +opsum .+= (0.5,"S+",5,"S-",6) +``` +""" +function add!(os::OpSum, o::Scaled{C,Prod{Op}}) where {C} + push!(terms(os), o) + return os +end +add!(os::OpSum, o::Op) = add!(os, Prod{Op}() * o) +add!(os::OpSum, o::Scaled{C,Op}) where {C} = add!(os, Prod{Op}() * o) +add!(os::OpSum, o::Prod{Op}) = add!(os, one(Float64) * o) +add!(os::OpSum, o::Tuple) = add!(os, Ops.op_term(o)) +add!(os::OpSum, a1::String, args...) = add!(os, (a1, args...)) +add!(os::OpSum, a1::Number, args...) = add!(os, (a1, args...)) +subtract!(os::OpSum, o::Tuple) = add!(os, -Ops.op_term(o)) + +function isfermionic(t::Vector{Op}, sites) + p = +1 + for op in t + if has_fermion_string(ITensors.name(op), sites[site(op)]) + p *= -1 + end + end + return (p == -1) +end + +# +# Abuse broadcasting syntax for in-place addition: +# +# os .+= ("Sz",1) +# os .-= ("Sz",1) +# +# TODO: Deprecate this syntax? +# + +struct OpSumStyle <: Broadcast.BroadcastStyle end +Base.BroadcastStyle(::Type{<:OpSum}) = OpSumStyle() + +struct OpSumAddTermStyle <: Broadcast.BroadcastStyle end + +Base.broadcastable(os::OpSum) = os + +Base.BroadcastStyle(::OpSumStyle, ::Broadcast.Style{Tuple}) = OpSumAddTermStyle() + +Broadcast.instantiate(bc::Broadcast.Broadcasted{OpSumAddTermStyle}) = bc + +function Base.copyto!(os, bc::Broadcast.Broadcasted{OpSumAddTermStyle,<:Any,typeof(+)}) + add!(os, bc.args[2]) + return os +end + +function Base.copyto!(os, bc::Broadcast.Broadcasted{OpSumAddTermStyle,<:Any,typeof(-)}) + subtract!(os, bc.args[2]) + return os +end + +# XXX: Create a new function name for this. +isempty(op_qn::Pair{Vector{Op},QN}) = isempty(op_qn.first) + +# the key type is Prod{Op} for the dense case +# and is Pair{Prod{Op},QN} for the QN conserving case +function posInLink!(linkmap::Dict{K,Int}, k::K)::Int where {K} + isempty(k) && return -1 + pos = get(linkmap, k, -1) + if pos == -1 + pos = length(linkmap) + 1 + linkmap[k] = pos + end + return pos +end + +# TODO: Define as `C`. Rename `coefficient_type`. +function determineValType(terms::Vector{Scaled{C,Prod{Op}}}) where {C} + for t in terms + (!isreal(coefficient(t))) && return ComplexF64 + end + return Float64 +end + +function computeSiteProd(sites, ops::Prod{Op})::ITensor + i = only(site(ops[1])) + T = op(sites[i], which_op(ops[1]); params(ops[1])...) + for j in 2:length(ops) + (only(site(ops[j])) != i) && error("Mismatch of site number in computeSiteProd") + opj = op(sites[i], which_op(ops[j]); params(ops[j])...) + T = product(T, opj) + end + return T +end + +function remove_dups!(v::Vector{T}) where {T} + N = length(v) + (N == 0) && return nothing + sort!(v) + n = 1 + u = 2 + while u <= N + while u < N && v[u] == v[n] + u += 1 + end + if v[u] != v[n] + v[n + 1] = v[u] + n += 1 + end + u += 1 + end + resize!(v, n) + return nothing +end #remove_dups! + +function sorteachterm(os::OpSum, sites) + os = copy(os) + + for (j, t) in enumerate(os) + if maximum(ITensors.sites(t)) > length(sites) + error( + "The OpSum contains a term $t that extends beyond the number of sites $(length(sites)).", + ) + end + + # Sort operators in t by site order and + # save the permutation used, "perm", for analysis below + Nt = length(t) + perm = Vector{Int}(undef, Nt) + sortperm!(perm, terms(t); alg=InsertionSort, lt=(o1, o2) -> (site(o1) < site(o2))) + # Apply permutation: + t = coefficient(t) * Prod(terms(t)[perm]) + + # prevsite keeps track of whether we are switching + # to a new site to make sure F string + # is only placed at most once for each site + prevsite = typemax(Int) + t_parity = +1 + for n in reverse(1:Nt) + site_n = only(site(t[n])) + if !using_auto_fermion() && (t_parity == -1) && (site_n < prevsite) + # Insert local piece of Jordan-Wigner string emanating + # from fermionic operators to the right + # (Remaining F operators will be put in by svdMPO) + terms(t)[n] = Op("$(which_op(t[n])) * F", site_n) + end + prevsite = site_n + + if has_fermion_string(which_op(t[n]), sites[site_n]) + t_parity = -t_parity + else + # Ignore bosonic operators in perm + # by zeroing corresponding entries + perm[n] = 0 + end + end + + (t_parity == -1) && + error("Parity-odd fermionic terms not yet supported by OpSum to MPO conversion") + + # Keep only fermionic op positions (non-zero entries) + filter!(!iszero, perm) + # and account for anti-commuting, fermionic operators + # during above sort; put resulting sign into coef + t *= ITensors.parity_sign(perm) + terms(os)[j] = t + end + + return os +end + +function sortmergeterms(os::OpSum{C}) where {C} + os_sorted_terms = sort(terms(os)) + os = Sum(os_sorted_terms) + # Merge (add) terms with same operators + merge_os_data = Scaled{C,Prod{Op}}[] + last_term = copy(os[1]) + last_term_coef = coefficient(last_term) + for n in 2:length(os) + if argument(os[n]) == argument(last_term) + last_term_coef += coefficient(os[n]) + last_term = last_term_coef * argument(last_term) + else + push!(merge_os_data, last_term) + last_term = os[n] + last_term_coef = coefficient(last_term) + end + end + push!(merge_os_data, last_term) + os = Sum(merge_os_data) + return os +end + +""" + MPO(os::OpSum, sites::Vector{<:Index}; splitblocks=true, kwargs...) + MPO(eltype::Type{<:Number}, os::OpSum, sites::Vector{<:Index}; splitblocks=true, kwargs...) + +Convert an OpSum object `os` to an +MPO, with indices given by `sites`. The +resulting MPO will have the indices +`sites[1], sites[1]', sites[2], sites[2]'` +etc. The conversion is done by an algorithm +that compresses the MPO resulting from adding +the OpSum terms together, often achieving +the minimum possible bond dimension. + +Optionally specify the desired element type +of the output MPO by passing the type +as the first argument. + +The keyword argument `splitblocks` controls +the sparsity of the resulting MPO. +With the default `splitblocks=true`, the link +indices of the MPO are split into blocks of +dimension 1, potentially making the MPO more sparse. + +With the `splitblocks=false`, the blocks +of the link dimensions are packed as much as +possible according to common quantum numbers, +making larger blocks. Before ITensors 0.3.19, +this was the default output, but we have found +that in general MPOs output with `splitblocks=true` +lead to better performance in algorithms like +DMRG. + +# Examples + +```julia +os = OpSum() +os += "Sz",1,"Sz",2 +os += "Sz",2,"Sz",3 +os += "Sz",3,"Sz",4 + +sites = siteinds("S=1/2",4) +H = MPO(os,sites) +H = MPO(Float32,os,sites) +H = MPO(os,sites; splitblocks=false) +``` +""" +function MPO(os::OpSum, sites::Vector{<:Index}; splitblocks=true, kwargs...)::MPO + length(terms(os)) == 0 && error("OpSum has no terms") + + os = deepcopy(os) + os = sorteachterm(os, sites) + os = sortmergeterms(os) + + if hasqns(sites[1]) + return qn_svdMPO(os, sites; kwargs...) + end + M = svdMPO(os, sites; kwargs...) + if splitblocks + M = ITensors.splitblocks(linkinds, M) + end + return M +end + +function MPO(elt::Type{<:Number}, os::OpSum, sites::Vector{<:Index}; kwargs...) + return NDTensors.convert_scalartype(elt, MPO(os, sites; kwargs...)) +end + +# Conversion from other formats +function MPO(eltype::Type{<:Number}, o::Op, s::Vector{<:Index}; kwargs...) + return MPO(eltype, OpSum{Float64}() + o, s; kwargs...) +end + +function MPO( + eltype::Type{<:Number}, o::Scaled{C,Op}, s::Vector{<:Index}; kwargs... +) where {C} + return MPO(eltype, OpSum{C}() + o, s; kwargs...) +end + +function MPO(eltype::Type{<:Number}, o::Sum{Op}, s::Vector{<:Index}; kwargs...) + return MPO(eltype, OpSum{Float64}() + o, s; kwargs...) +end + +function MPO(eltype::Type{<:Number}, o::Prod{Op}, s::Vector{<:Index}; kwargs...) + return MPO(eltype, OpSum{Float64}() + o, s; kwargs...) +end + +function MPO( + eltype::Type{<:Number}, o::Scaled{C,Prod{Op}}, s::Vector{<:Index}; kwargs... +) where {C} + return MPO(eltype, OpSum{C}() + o, s; kwargs...) +end + +function MPO( + eltype::Type{<:Number}, o::Sum{Scaled{C,Op}}, s::Vector{<:Index}; kwargs... +) where {C} + return MPO(eltype, OpSum{C}() + o, s; kwargs...) +end + +# Like `Ops.OpSumLike` but without `OpSum` included. +const OpSumLikeWithoutOpSum{C} = Union{ + Op,Scaled{C,Op},Sum{Op},Prod{Op},Scaled{C,Prod{Op}},Sum{Scaled{C,Op}} +} + +function MPO(o::OpSumLikeWithoutOpSum, s::Vector{<:Index}; kwargs...) + return MPO(Float64, o, s; kwargs...) +end diff --git a/src/opsum_to_mpo/opsum_to_mpo_qn.jl b/src/opsum_to_mpo/opsum_to_mpo_qn.jl new file mode 100644 index 0000000..25f75b3 --- /dev/null +++ b/src/opsum_to_mpo/opsum_to_mpo_qn.jl @@ -0,0 +1,261 @@ +using NDTensors: using_auto_fermion + +# `ValType::Type{<:Number}` is used instead of `ValType::Type` for efficiency, possibly due to increased method specialization. +# See https://github.com/ITensor/ITensors.jl/pull/1183. +function qn_svdMPO( + ValType::Type{<:Number}, os::OpSum{C}, sites; mindim=1, maxdim=typemax(Int), cutoff=1e-15 +)::MPO where {C} + N = length(sites) + + # Specifying the element type with `Dict{QN,Matrix{ValType}}[...]` improves type inference and therefore efficiency. + # See https://github.com/ITensor/ITensors.jl/pull/1183. + Vs = Dict{QN,Matrix{ValType}}[Dict{QN,Matrix{ValType}}() for n in 1:(N + 1)] + sparse_MPO = [QNMatElem{Scaled{C,Prod{Op}}}[] for n in 1:N] + + function crosses_bond(t::Scaled{C,Prod{Op}}, n::Int) + return (only(site(t[1])) <= n <= only(site(t[end]))) + end + + # A cache of the ITensor operators on a certain site + # of a certain type + op_cache = Dict{Pair{String,Int},ITensor}() + function calcQN(term::Vector{Op}) + q = QN() + for st in term + op_tensor = get(op_cache, which_op(st) => only(site(st)), nothing) + if op_tensor === nothing + op_tensor = op(sites[only(site(st))], which_op(st); params(st)...) + op_cache[which_op(st) => only(site(st))] = op_tensor + end + q -= flux(op_tensor) + end + return q + end + + Hflux = -calcQN(terms(first(terms(os)))) + + rightmap = Dict{Pair{Vector{Op},QN},Int}() + next_rightmap = Dict{Pair{Vector{Op},QN},Int}() + + for n in 1:N + h_sparse = Dict{QN,Vector{MatElem{ValType}}}() + + leftmap = Dict{Pair{Vector{Op},QN},Int}() + for term in os + crosses_bond(term, n) || continue + + left = filter(t -> (only(site(t)) < n), terms(term)) + onsite = filter(t -> (only(site(t)) == n), terms(term)) + right = filter(t -> (only(site(t)) > n), terms(term)) + + lqn = calcQN(left) + sqn = calcQN(onsite) + + bond_row = -1 + bond_col = -1 + if !isempty(left) + bond_row = posInLink!(leftmap, left => lqn) + bond_col = posInLink!(rightmap, vcat(onsite, right) => lqn) + bond_coef = convert(ValType, coefficient(term)) + q_h_sparse = get!(h_sparse, lqn, MatElem{ValType}[]) + push!(q_h_sparse, MatElem(bond_row, bond_col, bond_coef)) + end + + rqn = sqn + lqn + A_row = bond_col + A_col = posInLink!(next_rightmap, right => rqn) + site_coef = one(C) + if A_row == -1 + site_coef = coefficient(term) + end + if isempty(onsite) + if !using_auto_fermion() && isfermionic(right, sites) + push!(onsite, Op("F", n)) + else + push!(onsite, Op("Id", n)) + end + end + el = QNMatElem(lqn, rqn, A_row, A_col, site_coef * Prod(onsite)) + push!(sparse_MPO[n], el) + end + remove_dups!(sparse_MPO[n]) + + if n > 1 && !isempty(h_sparse) + for (q, mat) in h_sparse + h = toMatrix(mat) + U, S, V = svd(h) + P = S .^ 2 + truncate!(P; maxdim, cutoff, mindim) + tdim = length(P) + Vs[n][q] = Matrix{ValType}(V[:, 1:tdim]) + end + end + + rightmap = next_rightmap + next_rightmap = Dict{Pair{Vector{Op},QN},Int}() + end + + # + # Make MPO link indices + # + llinks = Vector{QNIndex}(undef, N + 1) + # Set dir=In for fermionic ordering, avoid arrow sign + # : + linkdir = using_auto_fermion() ? ITensors.In : ITensors.Out + llinks[1] = Index([QN() => 1, Hflux => 1]; tags="Link,l=0", dir=linkdir) + for n in 1:N + qi = Vector{Pair{QN,Int}}() + push!(qi, QN() => 1) + for (q, Vq) in Vs[n + 1] + cols = size(Vq, 2) + if using_auto_fermion() # + push!(qi, (-q) => cols) + else + push!(qi, q => cols) + end + end + push!(qi, Hflux => 1) + llinks[n + 1] = Index(qi...; tags="Link,l=$n", dir=linkdir) + end + + H = MPO(N) + + # Find location where block of Index i + # matches QN q, but *not* 1 or dim(i) + # which are special ending/starting states + function qnblock(i::Index, q::QN) + for b in 2:(nblocks(i) - 1) + flux(i, Block(b)) == q && return b + end + return error("Could not find block of QNIndex with matching QN") + end + qnblockdim(i::Index, q::QN) = blockdim(i, qnblock(i, q)) + + for n in 1:N + ll = llinks[n] + rl = llinks[n + 1] + + begin_block = Dict{Tuple{QN,Vector{Op}},Matrix{ValType}}() + cont_block = Dict{Tuple{QN,Vector{Op}},Matrix{ValType}}() + end_block = Dict{Tuple{QN,Vector{Op}},Matrix{ValType}}() + onsite_block = Dict{Tuple{QN,Vector{Op}},Matrix{ValType}}() + + for el in sparse_MPO[n] + t = el.val + (abs(coefficient(t)) > eps()) || continue + A_row = el.row + A_col = el.col + ct = convert(ValType, coefficient(t)) + + ldim = (A_row == -1) ? 1 : qnblockdim(ll, el.rowqn) + rdim = (A_col == -1) ? 1 : qnblockdim(rl, el.colqn) + zero_mat() = zeros(ValType, ldim, rdim) + + if A_row == -1 && A_col == -1 + # Onsite term + M = get!(onsite_block, (el.rowqn, terms(t)), zeros(ValType, 1, 1)) + M[1, 1] += ct + elseif A_row == -1 + # Operator beginning a term on site n + M = get!(begin_block, (el.rowqn, terms(t)), zero_mat()) + VR = Vs[n + 1][el.colqn] + for c in 1:size(VR, 2) + M[1, c] += ct * VR[A_col, c] + end + elseif A_col == -1 + # Operator ending a term on site n + M = get!(end_block, (el.rowqn, terms(t)), zero_mat()) + VL = Vs[n][el.rowqn] + for r in 1:size(VL, 2) + M[r, 1] += ct * conj(VL[A_row, r]) + end + else + # Operator continuing a term on site n + M = get!(cont_block, (el.rowqn, terms(t)), zero_mat()) + VL = Vs[n][el.rowqn] + VR = Vs[n + 1][el.colqn] + for r in 1:size(VL, 2), c in 1:size(VR, 2) + M[r, c] += ct * conj(VL[A_row, r]) * VR[A_col, c] + end + end + end + + H[n] = ITensor() + + # Helper functions to compute block locations + # of various blocks within the onsite blocks, + # begin blocks, etc. + loc_onsite(rq, cq) = Block(nblocks(ll), 1) + loc_begin(rq, cq) = Block(nblocks(ll), qnblock(rl, cq)) + loc_cont(rq, cq) = Block(qnblock(ll, rq), qnblock(rl, cq)) + loc_end(rq, cq) = Block(qnblock(ll, rq), 1) + + for (loc, block) in ( + (loc_onsite, onsite_block), + (loc_begin, begin_block), + (loc_end, end_block), + (loc_cont, cont_block), + ) + for (q_op, M) in block + op_prod = q_op[2] + Op = computeSiteProd(sites, Prod(op_prod)) + (nnzblocks(Op) == 0) && continue + + rq = q_op[1] + sq = flux(Op) + cq = rq + if !isnothing(sq) + # By convention, if `Op` has no blocks it has a flux + # of `nothing`, catch this case + cq -= sq + + if using_auto_fermion() + # : + # MPO is defined with Index order + # of (rl,s[n]',s[n],cl) where rl = row link, cl = col link + # so compute sign that would result by permuting cl from + # second position to last position: + if fparity(sq) == 1 && fparity(cq) == 1 + Op .*= -1 + end + end + end + + b = loc(rq, cq) + T = ITensors.NDTensors.BlockSparseTensor(ValType, [b], (dag(ll), rl)) + T[b] .= M + + H[n] += (itensor(T) * Op) + end + end + + # Put in ending identity operator + Id = op("Id", sites[n]) + b = Block(1, 1) + T = ITensors.NDTensors.BlockSparseTensor(ValType, [b], (dag(ll), rl)) + T[b] = 1 + H[n] += (itensor(T) * Id) + + # Put in starting identity operator + b = Block(nblocks(ll), nblocks(rl)) + T = ITensors.NDTensors.BlockSparseTensor(ValType, [b], (dag(ll), rl)) + T[b] = 1 + H[n] += (itensor(T) * Id) + end # for n in 1:N + + L = ITensor(llinks[1]) + L[llinks[1] => end] = 1.0 + H[1] *= L + + R = ITensor(dag(llinks[N + 1])) + R[dag(llinks[N + 1]) => 1] = 1.0 + H[N] *= R + + return H +end #qn_svdMPO + +function qn_svdMPO(os::OpSum{C}, sites; kwargs...)::MPO where {C} + # Function barrier to improve type stability + ValType = determineValType(terms(os)) + return qn_svdMPO(ValType, os, sites; kwargs...) +end diff --git a/src/opsum_to_mpo/qnmatelem.jl b/src/opsum_to_mpo/qnmatelem.jl new file mode 100644 index 0000000..7ec55c4 --- /dev/null +++ b/src/opsum_to_mpo/qnmatelem.jl @@ -0,0 +1,30 @@ +struct QNMatElem{T} + rowqn::QN + colqn::QN + row::Int + col::Int + val::T +end + +function Base.:(==)(m1::QNMatElem{T}, m2::QNMatElem{T})::Bool where {T} + return ( + m1.row == m2.row && + m1.col == m2.col && + m1.val == m2.val && + m1.rowqn == m2.rowqn && + m1.colqn == m2.colqn + ) +end + +function Base.isless(m1::QNMatElem{T}, m2::QNMatElem{T})::Bool where {T} + if m1.rowqn != m2.rowqn + return m1.rowqn < m2.rowqn + elseif m1.colqn != m2.colqn + return m1.colqn < m2.colqn + elseif m1.row != m2.row + return m1.row < m2.row + elseif m1.col != m2.col + return m1.col < m2.col + end + return m1.val < m2.val +end diff --git a/src/solvers/ITensorsExtensions.jl b/src/solvers/ITensorsExtensions.jl new file mode 100644 index 0000000..90ed8b1 --- /dev/null +++ b/src/solvers/ITensorsExtensions.jl @@ -0,0 +1,9 @@ +module ITensorsExtensions +using ITensors: ITensor, array, inds, itensor +function to_vec(x::ITensor) + function to_itensor(x_vec) + return itensor(x_vec, inds(x)) + end + return vec(array(x)), to_itensor +end +end diff --git a/src/solvers/alternating_update.jl b/src/solvers/alternating_update.jl new file mode 100644 index 0000000..75ecc4b --- /dev/null +++ b/src/solvers/alternating_update.jl @@ -0,0 +1,117 @@ +using ITensors: ITensors, permute + +function _extend_sweeps_param(param, nsweeps) + if param isa Number + eparam = fill(param, nsweeps) + else + length(param) == nsweeps && return param + eparam = Vector(undef, nsweeps) + eparam[1:length(param)] = param + eparam[(length(param) + 1):end] .= param[end] + end + return eparam +end + +function process_sweeps(; nsweeps, maxdim, mindim, cutoff, noise) + maxdim = _extend_sweeps_param(maxdim, nsweeps) + mindim = _extend_sweeps_param(mindim, nsweeps) + cutoff = _extend_sweeps_param(cutoff, nsweeps) + noise = _extend_sweeps_param(noise, nsweeps) + return (; maxdim, mindim, cutoff, noise) +end + +function alternating_update( + operator, + init::MPS; + updater, + updater_kwargs=(;), + nsweeps=default_nsweeps(), + checkdone=default_checkdone(), + write_when_maxdim_exceeds=default_write_when_maxdim_exceeds(), + nsite=default_nsite(), + reverse_step=default_reverse_step(), + time_start=default_time_start(), + time_step=default_time_step(), + order=default_order(), + (observer!)=default_observer(), + (sweep_observer!)=default_sweep_observer(), + outputlevel=default_outputlevel(), + normalize=default_normalize(), + maxdim=default_maxdim(), + mindim=default_mindim(), + cutoff=default_cutoff(ITensors.scalartype(init)), + noise=default_noise(), +) + reduced_operator = ITensorMPS.reduced_operator(operator) + if isnothing(nsweeps) + return error("Must specify `nsweeps`.") + end + maxdim, mindim, cutoff, noise = process_sweeps(; nsweeps, maxdim, mindim, cutoff, noise) + forward_order = TDVPOrder(order, Base.Forward) + state = copy(init) + # Keep track of the start of the current time step. + # Helpful for tracking the total time, for example + # when using time-dependent updaters. + # This will be passed as a keyword argument to the + # `updater`. + current_time = time_start + info = nothing + for sweep in 1:nsweeps + if !isnothing(write_when_maxdim_exceeds) && maxdim[sweep] > write_when_maxdim_exceeds + if outputlevel >= 2 + println( + "write_when_maxdim_exceeds = $write_when_maxdim_exceeds and maxdim(sweeps, sw) = $(maxdim(sweeps, sweep)), writing environment tensors to disk", + ) + end + reduced_operator = disk(reduced_operator) + end + sweep_elapsed_time = @elapsed begin + state, reduced_operator, info = sweep_update( + forward_order, + reduced_operator, + state; + updater, + updater_kwargs, + nsite, + current_time, + time_step, + reverse_step, + sweep, + observer!, + normalize, + outputlevel, + maxdim=maxdim[sweep], + mindim=mindim[sweep], + cutoff=cutoff[sweep], + noise=noise[sweep], + ) + end + if !isnothing(time_step) + current_time += time_step + end + update_observer!( + sweep_observer!; state, reduced_operator, sweep, outputlevel, current_time + ) + if outputlevel >= 1 + print("After sweep ", sweep, ":") + print(" maxlinkdim=", maxlinkdim(state)) + @printf(" maxerr=%.2E", info.maxtruncerr) + if !isnothing(current_time) + print(" current_time=", round(current_time; digits=3)) + end + print(" time=", round(sweep_elapsed_time; digits=3)) + println() + flush(stdout) + end + isdone = checkdone(; + state, sweep, outputlevel, observer=observer!, sweep_observer=sweep_observer! + ) + isdone && break + end + return state +end + +# Assume it is already in a reduced basis. +reduced_operator(operator) = operator +reduced_operator(operators::Vector{MPO}) = ProjMPOSum(operators) +reduced_operator(operator::MPO) = ProjMPO(operator) diff --git a/src/solvers/applyexp.jl b/src/solvers/applyexp.jl new file mode 100644 index 0000000..92121b8 --- /dev/null +++ b/src/solvers/applyexp.jl @@ -0,0 +1,131 @@ +using LinearAlgebra: dot +using Printf: @printf + +# +# To Do: +# - implement assembleLanczosVectors +# - check slice ranges - change end value by 1? +# + +function assemble_lanczos_vecs(lanczos_vectors, linear_comb, norm) + #if length(lanczos_vectors) != length(linear_comb) + # @show length(lanczos_vectors) + # @show length(linear_comb) + #end + xt = norm * linear_comb[1] * lanczos_vectors[1] + for i in 2:length(lanczos_vectors) + xt += norm * linear_comb[i] * lanczos_vectors[i] + end + return xt +end + +struct ApplyExpInfo + numops::Int + converged::Int +end + +function applyexp(H, tau::Number, x0; maxiter=30, tol=1e-12, outputlevel=0, normcutoff=1e-7) + # Initialize Lanczos vectors + v1 = copy(x0) + nrm = norm(v1) + v1 /= nrm + lanczos_vectors = [v1] + + ElT = promote_type(typeof(tau), eltype(x0)) + + bigTmat = zeros(ElT, maxiter + 3, maxiter + 3) + + nmatvec = 0 + + v0 = nothing + beta = 0.0 + for iter in 1:maxiter + tmat_size = iter + 1 + + # Matrix-vector multiplication + w = H(v1) + nmatvec += 1 + + avnorm = norm(w) + alpha = dot(w, v1) + + bigTmat[iter, iter] = alpha + + w -= alpha * v1 + if iter > 1 + w -= beta * v0 + end + v0 = copy(v1) + + beta = norm(w) + + # check for Lanczos sequence exhaustion + if abs(beta) < normcutoff + # Assemble the time evolved state + tmat = bigTmat[1:tmat_size, 1:tmat_size] + tmat_exp = exp(tau * tmat) + linear_comb = tmat_exp[:, 1] + xt = assemble_lanczos_vecs(lanczos_vectors, linear_comb, nrm) + return xt, ApplyExpInfo(nmatvec, 1) + end + + # update next lanczos vector + v1 = copy(w) + v1 /= beta + push!(lanczos_vectors, v1) + bigTmat[iter + 1, iter] = beta + bigTmat[iter, iter + 1] = beta + + # Convergence check + if iter > 0 + # Prepare extended T-matrix for exponentiation + tmat_ext_size = tmat_size + 2 + tmat_ext = bigTmat[1:tmat_ext_size, 1:tmat_ext_size] + + tmat_ext[tmat_size - 1, tmat_size] = 0.0 + tmat_ext[tmat_size + 1, tmat_size] = 1.0 + + # Exponentiate extended T-matrix + tmat_ext_exp = exp(tau * tmat_ext) + + ϕ1 = abs(nrm * tmat_ext_exp[tmat_size, 1]) + ϕ2 = abs(nrm * tmat_ext_exp[tmat_size + 1, 1] * avnorm) + + if ϕ1 > 10 * ϕ2 + error = ϕ2 + elseif (ϕ1 > ϕ2) + error = (ϕ1 * ϕ2) / (ϕ1 - ϕ2) + else + error = ϕ1 + end + + if outputlevel >= 3 + @printf(" Iteration: %d, Error: %.2E\n", iter, error) + end + + if ((error < tol) || (iter == maxiter)) + converged = 1 + if (iter == maxiter) + println("warning: applyexp not converged in $maxiter steps") + converged = 0 + end + + # Assemble the time evolved state + linear_comb = tmat_ext_exp[:, 1] + xt = assemble_lanczos_vecs(lanczos_vectors, linear_comb, nrm) + + if outputlevel >= 3 + println(" Number of iterations: $iter") + end + + return xt, ApplyExpInfo(nmatvec, converged) + end + end # end convergence test + end # iter + + if outputlevel >= 0 + println("In applyexp, number of matrix-vector multiplies: ", nmatvec) + end + + return x0 +end diff --git a/src/solvers/contract.jl b/src/solvers/contract.jl new file mode 100644 index 0000000..1919fdd --- /dev/null +++ b/src/solvers/contract.jl @@ -0,0 +1,41 @@ +using ITensors: ITensors, Index, ITensor, @Algorithm_str, commoninds, contract, hasind, sim + +function contract_operator_state_updater(operator, init; internal_kwargs) + # TODO: Use `contract(operator)`. + state = ITensor(true) + for j in (operator.lpos + 1):(operator.rpos - 1) + state *= operator.input_state[j] + end + state = contract(operator, state) + return state, (;) +end + +function default_contract_init(operator::MPO, input_state::MPS) + input_state = deepcopy(input_state) + s = only.(siteinds(uniqueinds, operator, input_state)) + # TODO: Fix issue with `replace_siteinds`, seems to be modifying in-place. + return replace_siteinds(deepcopy(input_state), s) +end + +function ITensors.contract( + ::Algorithm"fit", + operator::MPO, + input_state::MPS; + init=default_contract_init(operator, input_state), + kwargs..., +) + # Fix siteinds of `init` if needed. + # This is needed to work around an issue that `apply` + # can't be customized right now, and just uses the same `init` + # as that of `contract`. + # TODO: Allow customization of `apply` and remove this. + s = only.(siteinds(uniqueinds, operator, input_state)) + if !all(p -> p[1] == [2], zip(s, siteinds(init))) + # TODO: Fix issue with `replace_siteinds`, seems to be modifying in-place. + init = replace_siteinds(deepcopy(init), s) + end + reduced_operator = ReducedContractProblem(input_state, operator) + return alternating_update( + reduced_operator, init; updater=contract_operator_state_updater, kwargs... + ) +end diff --git a/src/solvers/defaults.jl b/src/solvers/defaults.jl new file mode 100644 index 0000000..6211125 --- /dev/null +++ b/src/solvers/defaults.jl @@ -0,0 +1,15 @@ +using Compat: Returns + +default_nsweeps() = nothing +default_checkdone() = Returns(false) +default_write_when_maxdim_exceeds() = nothing +default_nsite() = 2 +default_reverse_step() = false +default_time_start() = nothing +default_time_step() = nothing +default_order() = 2 +default_observer() = EmptyObserver() +default_sweep_observer() = EmptyObserver() +default_outputlevel() = 0 +default_normalize() = false +default_sweep() = 1 diff --git a/src/solvers/dmrg.jl b/src/solvers/dmrg.jl new file mode 100644 index 0000000..90fc9df --- /dev/null +++ b/src/solvers/dmrg.jl @@ -0,0 +1,25 @@ +using ITensors: ITensors +using KrylovKit: eigsolve + +function eigsolve_updater( + operator, + init; + internal_kwargs, + which_eigval=:SR, + ishermitian=true, + tol=10^2 * eps(real(ITensors.scalartype(init))), + krylovdim=3, + maxiter=1, + verbosity=0, + eager=false, +) + howmany = 1 + eigvals, eigvecs, info = eigsolve( + operator, init, howmany, which_eigval; ishermitian, tol, krylovdim, maxiter, verbosity + ) + return eigvecs[1], (; info, eigval=eigvals[1]) +end + +# A version of `dmrg` based on `alternating_update` and `eigsolve_updater` +# is defined in `src/lib/Experimental` as `ITensorMPS.Experimental.dmrg` to not conflict +# with `ITensorMPS.dmrg`. diff --git a/src/solvers/dmrg_x.jl b/src/solvers/dmrg_x.jl new file mode 100644 index 0000000..48201e6 --- /dev/null +++ b/src/solvers/dmrg_x.jl @@ -0,0 +1,23 @@ +using ITensors: array, contract, dag, uniqueind, onehot +using LinearAlgebra: eigen + +function eigen_updater(operator, state; internal_kwargs) + contracted_operator = contract(operator, ITensor(true)) + d, u = eigen(contracted_operator; ishermitian=true) + u_ind = uniqueind(u, contracted_operator) + u′_ind = uniqueind(d, u) + max_overlap, max_index = findmax(abs, array(state * dag(u))) + u_max = u * dag(onehot(eltype(u), u_ind => max_index)) + d_max = d[u′_ind => max_index, u_ind => max_index] + return u_max, (; eigval=d_max) +end + +function dmrg_x( + operator, state::MPS; updater=eigen_updater, (observer!)=default_observer(), kwargs... +) + info_ref = Ref{Any}() + info_observer = values_observer(; info=info_ref) + observer = compose_observers(observer!, info_observer) + eigvec = alternating_update(operator, state; updater, (observer!)=observer, kwargs...) + return info_ref[].eigval, eigvec +end diff --git a/src/solvers/expand.jl b/src/solvers/expand.jl new file mode 100644 index 0000000..c970e45 --- /dev/null +++ b/src/solvers/expand.jl @@ -0,0 +1,161 @@ +using Adapt: adapt +using ITensors: + ITensors, + Algorithm, + Index, + ITensor, + @Algorithm_str, + δ, + commonind, + dag, + denseblocks, + directsum, + hasqns, + prime, + scalartype, + uniqueinds +using LinearAlgebra: normalize, svd, tr +using NDTensors: unwrap_array_type + +# Possible improvements: +# - Allow a maxdim argument to be passed to `expand`. +# - Current behavior is letting bond dimension get too +# big when used in imaginary time evolution. +# - Consider switching the default to variational/fit apply +# when building Krylov vectors. +# - Use (1-tau*operator)|state> to generate Krylov vectors +# instead of operator|state>. Is that needed? + +function expand(state, reference; alg, kwargs...) + return expand(Algorithm(alg), state, reference; kwargs...) +end + +function expand_cutoff_doctring() + return """ + The cutoff is used to control the truncation of the expanded + basis and defaults to half the precision of the scalar type + of the input state, i.e. ~1e-8 for `Float64`. + """ +end + +function expand_warning_doctring() + return """ + !!! warning + Users are not given many customization options just yet as we + gain more experience on the right balance between efficacy of the + expansion and performance in various scenarios, and default values + and keyword arguments are subject to change as we learn more about + how to best use the method. + """ +end + +function expand_citation_docstring() + return """ + [^global_expansion]: Time Dependent Variational Principle with Ancillary Krylov Subspace. Mingru Yang, Steven R. White, [arXiv:2005.06104](https://arxiv.org/abs/2005.06104) + """ +end + +""" + expand(state::MPS, references::Vector{MPS}; alg="orthogonalize", cutoff) + +Given an MPS `state` and a collection of MPS `references`, +returns an MPS which is equal to `state` +(has fidelity 1 with `state`) but whose MPS basis +is expanded to contain a portion of the basis of +the `references` that is orthogonal to the MPS basis of `state`. +See [^global_expansion] for more details. + +$(expand_cutoff_doctring()) + +$(expand_warning_doctring()) + +$(expand_citation_docstring()) +""" +function expand( + ::Algorithm"orthogonalize", + state::MPS, + references::Vector{MPS}; + cutoff=(√(eps(real(scalartype(state))))), +) + n = length(state) + state = orthogonalize(state, n) + references = map(reference -> orthogonalize(reference, n), references) + s = siteinds(state) + for j in reverse(2:n) + # SVD state[j] to compute basisⱼ + linds = [s[j - 1]; linkinds(state, j - 1)] + _, λⱼ, basisⱼ = svd(state[j], linds; righttags="bψ_$j,Link") + rinds = uniqueinds(basisⱼ, λⱼ) + # Make projectorⱼ + idⱼ = prod(rinds) do r + return adapt(unwrap_array_type(basisⱼ), denseblocks(δ(scalartype(state), r', dag(r)))) + end + projectorⱼ = idⱼ - prime(basisⱼ, rinds) * dag(basisⱼ) + # Sum reference density matrices + ρⱼ = sum(reference -> prime(reference[j], rinds) * dag(reference[j]), references) + ρⱼ /= tr(ρⱼ) + # Apply projectorⱼ + ρⱼ_projected = apply(apply(projectorⱼ, ρⱼ), projectorⱼ) + expanded_basisⱼ = basisⱼ + if norm(ρⱼ_projected) > 10^3 * eps(real(scalartype(state))) + # Diagonalize projected density matrix ρⱼ_projected + # to compute reference_basisⱼ, which spans part of right basis + # of references which is orthogonal to right basis of state + dⱼ, reference_basisⱼ = eigen( + ρⱼ_projected; cutoff, ishermitian=true, righttags="bϕ_$j,Link" + ) + state_indⱼ = only(commoninds(basisⱼ, λⱼ)) + reference_indⱼ = only(commoninds(reference_basisⱼ, dⱼ)) + expanded_basisⱼ, expanded_indⱼ = directsum( + basisⱼ => state_indⱼ, reference_basisⱼ => reference_indⱼ + ) + end + # Shift ortho center one site left using dag(expanded_basisⱼ) + # and replace tensor at site j with expanded_basisⱼ + state[j - 1] = state[j - 1] * (state[j] * dag(expanded_basisⱼ)) + state[j] = expanded_basisⱼ + for reference in references + reference[j - 1] = reference[j - 1] * (reference[j] * dag(expanded_basisⱼ)) + reference[j] = expanded_basisⱼ + end + end + return state +end + +""" + expand(state::MPS, reference::MPO; alg="global_krylov", krylovdim=2, cutoff) + +Given an MPS `state` and an MPO `reference`, +returns an MPS which is equal to `state` +(has fidelity 1 with state) but whose MPS basis +is expanded to contain a portion of the basis of +the Krylov space built by repeated applications of +`reference` to `state` that is orthogonal +to the MPS basis of `state`. +The `reference` operator is applied to `state` `krylovdim` +number of times, with a default of 2 which should give +a good balance between reliability and performance. +See [^global_expansion] for more details. + +$(expand_cutoff_doctring()) + +$(expand_warning_doctring()) + +$(expand_citation_docstring()) +""" +function expand( + ::Algorithm"global_krylov", + state::MPS, + operator::MPO; + krylovdim=2, + cutoff=(√(eps(real(scalartype(state))))), + apply_kwargs=(; maxdim=maxlinkdim(state) + 1), +) + # TODO: Try replacing this logic with `Base.accumulate`. + references = Vector{MPS}(undef, krylovdim) + for k in 1:krylovdim + previous_reference = get(references, k - 1, state) + references[k] = normalize(apply(operator, previous_reference; apply_kwargs...)) + end + return expand(state, references; alg="orthogonalize", cutoff) +end diff --git a/src/solvers/linsolve.jl b/src/solvers/linsolve.jl new file mode 100644 index 0000000..9ed68f3 --- /dev/null +++ b/src/solvers/linsolve.jl @@ -0,0 +1,50 @@ +using KrylovKit: KrylovKit, linsolve + +function linsolve_updater(problem, init; internal_kwargs, coefficients, kwargs...) + x, info = linsolve( + operator(problem), + constant_term(problem), + init, + coefficients[1], + coefficients[2]; + kwargs..., + ) + return x, (; info) +end + +""" +Compute a solution x to the linear system: + +(a₀ + a₁ * A)*x = b + +using starting guess x₀. Leaving a₀, a₁ +set to their default values solves the +system A*x = b. + +To adjust the balance between accuracy of solution +and speed of the algorithm, it is recommed to first try +adjusting the updater keyword arguments as descibed below. + +Keyword arguments: + - `nsweeps`, `cutoff`, `maxdim`, etc. (like for other MPO/MPS updaters). + - `updater_kwargs=(;)` - a `NamedTuple` containing keyword arguments that will get forwarded to the local updater, + in this case `KrylovKit.linsolve` which is a GMRES linear updater. For example: + ```juli + linsolve(A, b, x; maxdim=100, cutoff=1e-8, nsweeps=10, updater_kwargs=(; ishermitian=true, tol=1e-6, maxiter=20, krylovdim=30)) + ``` + See `KrylovKit.jl` documentation for more details on available keyword arguments. +""" +function KrylovKit.linsolve( + operator, + constant_term::MPS, + init::MPS, + coefficient1::Number=false, + coefficient2::Number=true; + updater=linsolve_updater, + updater_kwargs=(;), + kwargs..., +) + reduced_problem = ReducedLinearProblem(operator, constant_term) + updater_kwargs = (; coefficients=(coefficient1, coefficient2), updater_kwargs...) + return alternating_update(reduced_problem, init; updater, updater_kwargs, kwargs...) +end diff --git a/src/solvers/reducedconstantterm.jl b/src/solvers/reducedconstantterm.jl new file mode 100644 index 0000000..52c882b --- /dev/null +++ b/src/solvers/reducedconstantterm.jl @@ -0,0 +1,152 @@ +using ITensors: ITensors, ITensor, dag, dim, prime + +""" +Holds the following data where basis +is the MPS being optimized and state is the +MPS held constant by the ProjMPS. +``` + o--o--o--o--o--o--o--o--o--o--o +``` +""" +mutable struct ReducedConstantTerm <: AbstractProjMPO + lpos::Int + rpos::Int + nsite::Int + state::MPS + environments::Vector{ITensor} +end + +function ReducedConstantTerm(state::MPS) + lpos = 0 + rpos = length(state) + 1 + nsite = 2 + environments = Vector{ITensor}(undef, length(state)) + return ReducedConstantTerm(lpos, rpos, nsite, state, environments) +end + +function Base.getproperty(reduced_state::ReducedConstantTerm, sym::Symbol) + # This is for compatibility with `AbstractProjMPO`. + # TODO: Don't use `reduced_state.H`, `reduced_state.LR`, etc. + # in `AbstractProjMPO`. + if sym == :LR + return getfield(reduced_state, :environments) + end + return getfield(reduced_state, sym) +end + +Base.length(reduced_state::ReducedConstantTerm) = length(reduced_state.state) + +function Base.copy(reduced_state::ReducedConstantTerm) + return ReducedConstantTerm( + reduced_state.lpos, + reduced_state.rpos, + reduced_state.nsite, + copy(reduced_state.state), + copy(reduced_state.environments), + ) +end + +function ITensorMPS.set_nsite!(reduced_state::ReducedConstantTerm, nsite) + reduced_state.nsite = nsite + return reduced_state +end + +function ITensorMPS.makeL!(reduced_state::ReducedConstantTerm, basis::MPS, position::Int) + # Save the last `L` that is made to help with caching + # for DiskProjMPO + ll = reduced_state.lpos + if ll ≥ position + # Special case when nothing has to be done. + # Still need to change the position if lproj is + # being moved backward. + reduced_state.lpos = position + return nothing + end + # Make sure ll is at least 0 for the generic logic below + ll = max(ll, 0) + L = lproj(reduced_state) + while ll < position + L = L * basis[ll + 1] * dag(prime(reduced_state.state[ll + 1], "Link")) + reduced_state.environments[ll + 1] = L + ll += 1 + end + # Needed when moving lproj backward. + reduced_state.lpos = position + return reduced_state +end + +function ITensorMPS.makeR!(reduced_state::ReducedConstantTerm, basis::MPS, position::Int) + # Save the last `R` that is made to help with caching + # for DiskProjMPO + environment_position = reduced_state.rpos + if environment_position ≤ position + # Special case when nothing has to be done. + # Still need to change the position if rproj is + # being moved backward. + reduced_state.rpos = position + return nothing + end + N = length(reduced_state.state) + # Make sure environment_position is no bigger than `N + 1` for the generic logic below + environment_position = min(environment_position, N + 1) + right_environment = rproj(reduced_state) + while environment_position > position + right_environment = + right_environment * + basis[environment_position - 1] * + dag(prime(reduced_state.state[environment_position - 1], "Link")) + reduced_state.environments[environment_position - 1] = right_environment + environment_position -= 1 + end + reduced_state.rpos = position + return reduced_state +end + +function ITensors.contract(reduced_state::ReducedConstantTerm, v::ITensor) + reduced_state_tensors = Union{ITensor,OneITensor}[lproj(reduced_state)] + append!( + reduced_state_tensors, + [prime(t, "Link") for t in reduced_state.state[site_range(reduced_state)]], + ) + push!(reduced_state_tensors, rproj(reduced_state)) + + # Reverse the contraction order of the map if + # the first tensor is a scalar (for example we + # are at the left edge of the system) + if dim(first(reduced_state_tensors)) == 1 + reverse!(reduced_state_tensors) + end + + # Apply the map + inner = v + for t in reduced_state_tensors + inner *= t + end + return inner +end + +# Contract the reduced constant term down to a since ITensor. +function ITensors.contract(reduced_state::ReducedConstantTerm) + reduced_state_tensors = Union{ITensor,OneITensor}[lproj(reduced_state)] + append!( + reduced_state_tensors, + [dag(prime(t, "Link")) for t in reduced_state.state[site_range(reduced_state)]], + ) + push!(reduced_state_tensors, rproj(reduced_state)) + + # Reverse the contraction order of the map if + # the first tensor is a scalar (for example we + # are at the left edge of the system) + if dim(first(reduced_state_tensors)) == 1 + reverse!(reduced_state_tensors) + end + + # Apply the map + contracted_reduced_state = ITensor(true) + for t in reduced_state_tensors + contracted_reduced_state *= t + end + return contracted_reduced_state +end diff --git a/src/solvers/reducedcontractproblem.jl b/src/solvers/reducedcontractproblem.jl new file mode 100644 index 0000000..916d60c --- /dev/null +++ b/src/solvers/reducedcontractproblem.jl @@ -0,0 +1,122 @@ +using ITensors: ITensor + +""" +A ReducedContractProblem represents the application of an +MPO `operator` onto an MPS `input_state` but "projected" by +the basis of a different MPS `state` (which +could be an approximation to operator|state>). + +As an implementation of the AbstractProjMPO +type, it supports multiple `nsite` values for +one- and two-site algorithms. + +``` + *--*--*- -*--*--*--*--*--* +``` +""" +mutable struct ReducedContractProblem <: AbstractProjMPO + lpos::Int + rpos::Int + nsite::Int + input_state::MPS + operator::MPO + environments::Vector{ITensor} +end + +function ReducedContractProblem(input_state::MPS, operator::MPO) + lpos = 0 + rpos = length(operator) + 1 + nsite = 2 + environments = Vector{ITensor}(undef, length(operator)) + return ReducedContractProblem(lpos, rpos, nsite, input_state, operator, environments) +end + +function Base.getproperty(reduced_operator::ReducedContractProblem, sym::Symbol) + # This is for compatibility with `AbstractProjMPO`. + # TODO: Don't use `reduced_operator.H`, `reduced_operator.LR`, etc. + # in `AbstractProjMPO`. + if sym === :H + return getfield(reduced_operator, :operator) + elseif sym == :LR + return getfield(reduced_operator, :environments) + end + return getfield(reduced_operator, sym) +end + +function Base.copy(reduced_operator::ReducedContractProblem) + return ReducedContractProblem( + reduced_operator.lpos, + reduced_operator.rpos, + reduced_operator.nsite, + copy(reduced_operator.input_state), + copy(reduced_operator.operator), + copy(reduced_operator.environments), + ) +end + +Base.length(reduced_operator::ReducedContractProblem) = length(reduced_operator.operator) + +function ITensorMPS.set_nsite!(reduced_operator::ReducedContractProblem, nsite) + reduced_operator.nsite = nsite + return reduced_operator +end + +function ITensorMPS.makeL!(reduced_operator::ReducedContractProblem, state::MPS, k::Int) + # Save the last `L` that is made to help with caching + # for DiskProjMPO + ll = reduced_operator.lpos + if ll ≥ k + # Special case when nothing has to be done. + # Still need to change the position if lproj is + # being moved backward. + reduced_operator.lpos = k + return nothing + end + # Make sure ll is at least 0 for the generic logic below + ll = max(ll, 0) + L = lproj(reduced_operator) + while ll < k + L = + L * + reduced_operator.input_state[ll + 1] * + reduced_operator.operator[ll + 1] * + dag(state[ll + 1]) + reduced_operator.environments[ll + 1] = L + ll += 1 + end + # Needed when moving lproj backward. + reduced_operator.lpos = k + return reduced_operator +end + +function ITensorMPS.makeR!(reduced_operator::ReducedContractProblem, state::MPS, k::Int) + # Save the last `R` that is made to help with caching + # for DiskProjMPO + rl = reduced_operator.rpos + if rl ≤ k + # Special case when nothing has to be done. + # Still need to change the position if rproj is + # being moved backward. + reduced_operator.rpos = k + return nothing + end + N = length(reduced_operator.operator) + # Make sure rl is no bigger than `N + 1` for the generic logic below + rl = min(rl, N + 1) + R = rproj(reduced_operator) + while rl > k + R = + R * + reduced_operator.input_state[rl - 1] * + reduced_operator.operator[rl - 1] * + dag(state[rl - 1]) + reduced_operator.environments[rl - 1] = R + rl -= 1 + end + reduced_operator.rpos = k + return reduced_operator +end diff --git a/src/solvers/reducedlinearproblem.jl b/src/solvers/reducedlinearproblem.jl new file mode 100644 index 0000000..283fdfc --- /dev/null +++ b/src/solvers/reducedlinearproblem.jl @@ -0,0 +1,61 @@ +using ITensors: contract + +mutable struct ReducedLinearProblem <: AbstractProjMPO + reduced_operator::ProjMPO + reduced_constant_terms::Vector{ReducedConstantTerm} +end + +# Linear problem updater interface. +operator(reduced_problem::ReducedLinearProblem) = reduced_problem.reduced_operator +function constant_term(reduced_problem::ReducedLinearProblem) + constant_terms = map(reduced_problem.reduced_constant_terms) do reduced_constant_term + return contract(reduced_constant_term) + end + return dag(only(constant_terms)) +end + +function ReducedLinearProblem(operator::MPO, constant_term::MPS) + return ReducedLinearProblem(ProjMPO(operator), [ReducedConstantTerm(constant_term)]) +end + +function ReducedLinearProblem(operator::MPO, constant_terms::Vector{MPS}) + return ReducedLinearProblem(ProjMPO(operator), ReducedConstantTerm.(constant_terms)) +end + +function Base.copy(reduced_problem::ReducedLinearProblem) + return ReducedLinearProblem( + copy(reduced_problem.reduced_operator), copy(reduced_problem.reduced_constant_terms) + ) +end + +function ITensorMPS.nsite(reduced_problem::ReducedLinearProblem) + return nsite(reduced_problem.reduced_operator) +end + +function ITensorMPS.set_nsite!(reduced_problem::ReducedLinearProblem, nsite) + set_nsite!(reduced_problem.reduced_operator, nsite) + for m in reduced_problem.reduced_constant_terms + set_nsite!(m, nsite) + end + return reduced_problem +end + +function ITensorMPS.makeL!(reduced_problem::ReducedLinearProblem, state::MPS, position::Int) + makeL!(reduced_problem.reduced_operator, state, position) + for reduced_constant_term in reduced_problem.reduced_constant_terms + makeL!(reduced_constant_term, state, position) + end + return reduced_problem +end + +function ITensorMPS.makeR!(reduced_problem::ReducedLinearProblem, state::MPS, position::Int) + makeR!(reduced_problem.reduced_operator, state, position) + for reduced_constant_term in reduced_problem.reduced_constant_terms + makeR!(reduced_constant_term, state, position) + end + return reduced_problem +end + +function ITensors.contract(reduced_problem::ReducedLinearProblem, v::ITensor) + return contract(reduced_problem.reduced_operator, v) +end diff --git a/src/solvers/sweep_update.jl b/src/solvers/sweep_update.jl new file mode 100644 index 0000000..46ffa3e --- /dev/null +++ b/src/solvers/sweep_update.jl @@ -0,0 +1,476 @@ +using ITensors: ITensors, uniqueinds +using LinearAlgebra: norm, normalize!, svd +using Printf: @printf + +function sweep_update( + order::TDVPOrder, + reduced_operator, + state::MPS; + current_time=nothing, + time_step=nothing, + kwargs..., +) + order_orderings = orderings(order) + order_sub_time_steps = sub_time_steps(order) + if !isnothing(time_step) + order_sub_time_steps = eltype(time_step).(order_sub_time_steps) + order_sub_time_steps *= time_step + end + info = nothing + sub_time_step = nothing + for substep in 1:length(order_sub_time_steps) + if !isnothing(time_step) + sub_time_step = order_sub_time_steps[substep] + end + state, reduced_operator, info = sub_sweep_update( + order_orderings[substep], + reduced_operator, + state; + current_time, + time_step=sub_time_step, + kwargs..., + ) + if !isnothing(time_step) + current_time += sub_time_step + end + end + return state, reduced_operator, info +end + +isforward(direction::Base.ForwardOrdering) = true +isforward(direction::Base.ReverseOrdering) = false +isreverse(direction) = !isforward(direction) + +function sweep_bonds(direction::Base.ForwardOrdering, n::Int; ncenter::Int) + return 1:(n - ncenter + 1) +end + +function sweep_bonds(direction::Base.ReverseOrdering, n::Int; ncenter::Int) + return reverse(sweep_bonds(Base.Forward, n; ncenter)) +end + +is_forward_done(direction::Base.ForwardOrdering, b, n; ncenter) = (b + ncenter - 1 == n) +is_forward_done(direction::Base.ReverseOrdering, b, n; ncenter) = false +is_reverse_done(direction::Base.ForwardOrdering, b, n; ncenter) = false +is_reverse_done(direction::Base.ReverseOrdering, b, n; ncenter) = (b == 1) +function is_half_sweep_done(direction, b, n; ncenter) + return is_forward_done(direction, b, n; ncenter) || + is_reverse_done(direction, b, n; ncenter) +end + +function sub_sweep_update( + direction::Base.Ordering, + reduced_operator, + state::MPS; + updater, + updater_kwargs, + which_decomp=nothing, + svd_alg=nothing, + sweep=default_sweep(), + current_time=nothing, + time_step=nothing, + nsite=default_nsite(), + reverse_step=default_reverse_step(), + normalize=default_normalize(), + (observer!)=default_observer(), + outputlevel=default_outputlevel(), + maxdim=default_maxdim(), + mindim=default_mindim(), + cutoff=default_cutoff(ITensors.scalartype(state)), + noise=default_noise(), +) + reduced_operator = copy(reduced_operator) + state = copy(state) + if length(state) == 1 + error( + "`tdvp`, `dmrg`, `linsolve`, etc. currently does not support system sizes of 1. You can diagonalize the MPO tensor directly with tools like `LinearAlgebra.eigen`, `KrylovKit.exponentiate`, etc.", + ) + end + N = length(state) + set_nsite!(reduced_operator, nsite) + if isforward(direction) + if !isortho(state) || orthocenter(state) != 1 + orthogonalize!(state, 1) + end + @assert isortho(state) && orthocenter(state) == 1 + position!(reduced_operator, state, 1) + elseif isreverse(direction) + if !isortho(state) || orthocenter(state) != N - nsite + 1 + orthogonalize!(state, N - nsite + 1) + end + @assert(isortho(state) && (orthocenter(state) == N - nsite + 1)) + position!(reduced_operator, state, N - nsite + 1) + end + maxtruncerr = 0.0 + info = nothing + for b in sweep_bonds(direction, N; ncenter=nsite) + current_time, maxtruncerr, spec, info = region_update!( + reduced_operator, + state, + b; + updater, + updater_kwargs, + nsite, + reverse_step, + current_time, + outputlevel, + time_step, + normalize, + direction, + noise, + which_decomp, + svd_alg, + cutoff, + maxdim, + mindim, + maxtruncerr, + ) + if outputlevel >= 2 + if nsite == 1 + @printf("Sweep %d, direction %s, bond (%d,) \n", sweep, direction, b) + elseif nsite == 2 + @printf("Sweep %d, direction %s, bond (%d,%d) \n", sweep, direction, b, b + 1) + end + print(" Truncated using") + @printf(" cutoff=%.1E", cutoff) + @printf(" maxdim=%.1E", maxdim) + print(" mindim=", mindim) + print(" current_time=", round(current_time; digits=3)) + println() + if spec != nothing + @printf( + " Trunc. err=%.2E, bond dimension %d\n", spec.truncerr, dim(linkind(state, b)) + ) + end + flush(stdout) + end + update_observer!( + observer!; + state, + reduced_operator, + bond=b, + sweep, + half_sweep=isforward(direction) ? 1 : 2, + spec, + outputlevel, + half_sweep_is_done=is_half_sweep_done(direction, b, N; ncenter=nsite), + current_time, + info, + ) + end + # Just to be sure: + normalize && normalize!(state) + return state, reduced_operator, (; maxtruncerr) +end + +function region_update!( + reduced_operator, + state, + b; + updater, + updater_kwargs, + nsite, + reverse_step, + current_time, + outputlevel, + time_step, + normalize, + direction, + noise, + which_decomp, + svd_alg, + cutoff, + maxdim, + mindim, + maxtruncerr, +) + return region_update!( + Val(nsite), + Val(reverse_step), + reduced_operator, + state, + b; + updater, + updater_kwargs, + current_time, + outputlevel, + time_step, + normalize, + direction, + noise, + which_decomp, + svd_alg, + cutoff, + maxdim, + mindim, + maxtruncerr, + ) +end + +function region_update!( + nsite_val::Val{1}, + reverse_step_val::Val{false}, + reduced_operator, + state, + b; + updater, + updater_kwargs, + current_time, + outputlevel, + time_step, + normalize, + direction, + noise, + which_decomp, + svd_alg, + cutoff, + maxdim, + mindim, + maxtruncerr, +) + N = length(state) + nsite = 1 + # Do 'forwards' evolution step + set_nsite!(reduced_operator, nsite) + position!(reduced_operator, state, b) + reduced_state = state[b] + internal_kwargs = (; current_time, time_step, outputlevel) + reduced_state, info = updater( + reduced_operator, reduced_state; internal_kwargs, updater_kwargs... + ) + if !isnothing(time_step) + current_time += time_step + end + normalize && (reduced_state /= norm(reduced_state)) + spec = nothing + state[b] = reduced_state + if !is_half_sweep_done(direction, b, N; ncenter=nsite) + # Move ortho center + Δ = (isforward(direction) ? +1 : -1) + orthogonalize!(state, b + Δ) + end + return current_time, maxtruncerr, spec, info +end + +function region_update!( + nsite_val::Val{1}, + reverse_step_val::Val{true}, + reduced_operator, + state, + b; + updater, + updater_kwargs, + current_time, + outputlevel, + time_step, + normalize, + direction, + noise, + which_decomp, + svd_alg, + cutoff, + maxdim, + mindim, + maxtruncerr, +) + N = length(state) + nsite = 1 + # Do 'forwards' evolution step + set_nsite!(reduced_operator, nsite) + position!(reduced_operator, state, b) + reduced_state = state[b] + internal_kwargs = (; current_time, time_step, outputlevel) + reduced_state, info = updater( + reduced_operator, reduced_state; internal_kwargs, updater_kwargs... + ) + current_time += time_step + normalize && (reduced_state /= norm(reduced_state)) + spec = nothing + state[b] = reduced_state + if !is_half_sweep_done(direction, b, N; ncenter=nsite) + # Do backwards evolution step + b1 = (isforward(direction) ? b + 1 : b) + Δ = (isforward(direction) ? +1 : -1) + uinds = uniqueinds(reduced_state, state[b + Δ]) + U, S, V = svd(reduced_state, uinds) + state[b] = U + bond_reduced_state = S * V + if isforward(direction) + ITensorMPS.setleftlim!(state, b) + elseif isreverse(direction) + ITensorMPS.setrightlim!(state, b) + end + set_nsite!(reduced_operator, nsite - 1) + position!(reduced_operator, state, b1) + internal_kwargs = (; current_time, time_step=-time_step, outputlevel) + bond_reduced_state, info = updater( + reduced_operator, bond_reduced_state; internal_kwargs, updater_kwargs... + ) + current_time -= time_step + normalize && (bond_reduced_state ./= norm(bond_reduced_state)) + state[b + Δ] = bond_reduced_state * state[b + Δ] + if isforward(direction) + ITensorMPS.setrightlim!(state, b + Δ + 1) + elseif isreverse(direction) + ITensorMPS.setleftlim!(state, b + Δ - 1) + end + set_nsite!(reduced_operator, nsite) + end + return current_time, maxtruncerr, spec, info +end + +function region_update!( + nsite_val::Val{2}, + reverse_step_val::Val{false}, + reduced_operator, + state, + b; + updater, + updater_kwargs, + current_time, + time_step, + outputlevel, + normalize, + direction, + noise, + which_decomp, + svd_alg, + cutoff, + maxdim, + mindim, + maxtruncerr, +) + N = length(state) + nsite = 2 + # Do 'forwards' evolution step + set_nsite!(reduced_operator, nsite) + position!(reduced_operator, state, b) + reduced_state = state[b] * state[b + 1] + internal_kwargs = (; current_time, time_step, outputlevel) + reduced_state, info = updater( + reduced_operator, reduced_state; internal_kwargs, updater_kwargs... + ) + if !isnothing(time_step) + current_time += time_step + end + normalize && (reduced_state /= norm(reduced_state)) + spec = nothing + ortho = isforward(direction) ? "left" : "right" + drho = nothing + if noise > 0.0 && isforward(direction) + drho = noise * noiseterm(reduced_operator, reduced_state, ortho) + end + spec = replacebond!( + state, + b, + reduced_state; + maxdim, + mindim, + cutoff, + eigen_perturbation=drho, + ortho=ortho, + normalize, + which_decomp, + svd_alg, + ) + maxtruncerr = max(maxtruncerr, spec.truncerr) + return current_time, maxtruncerr, spec, info +end + +function region_update!( + nsite_val::Val{2}, + reverse_step_val::Val{true}, + reduced_operator, + state, + b; + updater, + updater_kwargs, + current_time, + time_step, + outputlevel, + normalize, + direction, + noise, + which_decomp, + svd_alg, + cutoff, + maxdim, + mindim, + maxtruncerr, +) + N = length(state) + nsite = 2 + # Do 'forwards' evolution step + set_nsite!(reduced_operator, nsite) + position!(reduced_operator, state, b) + reduced_state = state[b] * state[b + 1] + internal_kwargs = (; current_time, time_step, outputlevel) + reduced_state, info = updater( + reduced_operator, reduced_state; internal_kwargs, updater_kwargs... + ) + current_time += time_step + normalize && (reduced_state /= norm(reduced_state)) + spec = nothing + ortho = isforward(direction) ? "left" : "right" + drho = nothing + if noise > 0.0 && isforward(direction) + drho = noise * noiseterm(reduced_operator, phi, ortho) + end + spec = replacebond!( + state, + b, + reduced_state; + maxdim, + mindim, + cutoff, + eigen_perturbation=drho, + ortho=ortho, + normalize, + which_decomp, + svd_alg, + ) + maxtruncerr = max(maxtruncerr, spec.truncerr) + if !is_half_sweep_done(direction, b, N; ncenter=nsite) + # Do backwards evolution step + b1 = (isforward(direction) ? b + 1 : b) + Δ = (isforward(direction) ? +1 : -1) + bond_reduced_state = state[b1] + set_nsite!(reduced_operator, nsite - 1) + position!(reduced_operator, state, b1) + internal_kwargs = (; current_time, time_step=-time_step, outputlevel) + bond_reduced_state, info = updater( + reduced_operator, bond_reduced_state; internal_kwargs, updater_kwargs... + ) + current_time -= time_step + normalize && (bond_reduced_state /= norm(bond_reduced_state)) + state[b1] = bond_reduced_state + set_nsite!(reduced_operator, nsite) + end + return current_time, maxtruncerr, spec, info +end + +function region_!( + ::Val{nsite}, + ::Val{reverse_step}, + reduced_operator, + state, + b; + updater, + updater_kwargs, + current_time, + outputlevel, + time_step, + normalize, + direction, + noise, + which_decomp, + svd_alg, + cutoff, + maxdim, + mindim, + maxtruncerr, +) where {nsite,reverse_step} + return error( + "`tdvp`, `dmrg`, `linsolve`, etc. with `nsite=$nsite` and `reverse_step=$reverse_step` not implemented.", + ) +end diff --git a/src/solvers/tdvp.jl b/src/solvers/tdvp.jl new file mode 100644 index 0000000..651bd34 --- /dev/null +++ b/src/solvers/tdvp.jl @@ -0,0 +1,92 @@ +using ITensors: Algorithm, @Algorithm_str +using KrylovKit: exponentiate + +function exponentiate_updater(operator, init; internal_kwargs, kwargs...) + state, info = exponentiate(operator, internal_kwargs.time_step, init; kwargs...) + return state, (; info) +end + +function applyexp_updater(operator, init; internal_kwargs, kwargs...) + state, info = applyexp(operator, internal_kwargs.time_step, init; kwargs...) + return state, (; info) +end + +tdvp_updater(updater_backend::String) = tdvp_updater(Algorithm(updater_backend)) +tdvp_updater(::Algorithm"exponentiate") = exponentiate_updater +tdvp_updater(::Algorithm"applyexp") = applyexp_updater +function tdvp_updater(updater_backend::Algorithm) + return error("`updater_backend=$(String(updater_backend))` not recognized.") +end + +function time_step_and_nsteps(t, time_step::Nothing, nsteps::Nothing) + # Default to 1 step. + nsteps = 1 + return time_step_and_nsteps(t, time_step, nsteps) +end + +function time_step_and_nsteps(t, time_step::Nothing, nsteps) + return t / nsteps, nsteps +end + +function time_step_and_nsteps(t, time_step, nsteps::Nothing) + nsteps_float = t / time_step + nsteps_rounded = round(nsteps_float) + if abs(nsteps_float - nsteps_rounded) ≉ 0 + return error("`t / time_step = $t / $time_step = $(t / time_step)` must be an integer.") + end + return time_step, Int(nsteps_rounded) +end + +function time_step_and_nsteps(t, time_step, nsteps) + if time_step * nsteps ≠ t + return error( + "Calling `tdvp(operator, t, state; time_step, nsteps, kwargs...)` with `t = $t`, `time_step = $time_step`, and `nsteps = $nsteps` must satisfy `time_step * nsteps == t`, while `time_step * nsteps = $time_step * $nsteps = $(time_step * nsteps)`.", + ) + end + return time_step, nsteps +end + +""" + tdvp(operator, t::Number, init::MPS; time_step, nsteps, kwargs...) + +Use the time dependent variational principle (TDVP) algorithm +to compute `exp(t * operator) * init` using an efficient algorithm based +on alternating optimization of the MPS tensors and local Krylov +exponentiation of `operator`. + +Specify one of `time_step` or `nsteps`. If they are both specified, they +must satisfy `time_step * nsteps == t`. If neither are specified, the +default is `nsteps=1`, which means that `time_step == t`. + +Returns: +* `state::MPS` - time-evolved MPS + +""" +function tdvp( + operator, + t::Number, + init::MPS; + updater_backend="exponentiate", + updater=tdvp_updater(updater_backend), + reverse_step=true, + time_step=nothing, + time_start=zero(t), + nsweeps=nothing, + nsteps=nsweeps, + (step_observer!)=default_sweep_observer(), + (sweep_observer!)=step_observer!, + kwargs..., +) + time_step, nsteps = time_step_and_nsteps(t, time_step, nsteps) + return alternating_update( + operator, + init; + updater, + reverse_step, + nsweeps=nsteps, + time_start, + time_step, + sweep_observer!, + kwargs..., + ) +end diff --git a/src/solvers/tdvporder.jl b/src/solvers/tdvporder.jl new file mode 100644 index 0000000..e37e00e --- /dev/null +++ b/src/solvers/tdvporder.jl @@ -0,0 +1,24 @@ +struct TDVPOrder{order,direction} end + +TDVPOrder(order::Int, direction::Base.Ordering) = TDVPOrder{order,direction}() + +orderings(::TDVPOrder) = error("Not implemented") +sub_time_steps(::TDVPOrder) = error("Not implemented") + +function orderings(::TDVPOrder{1,direction}) where {direction} + return [direction, Base.ReverseOrdering(direction)] +end +sub_time_steps(::TDVPOrder{1}) = [1, 0] + +function orderings(::TDVPOrder{2,direction}) where {direction} + return [direction, Base.ReverseOrdering(direction)] +end +sub_time_steps(::TDVPOrder{2}) = [1 / 2, 1 / 2] + +function orderings(::TDVPOrder{4,direction}) where {direction} + return [direction, Base.ReverseOrdering(direction)] +end +function sub_time_steps(::TDVPOrder{4}) + s = 1 / (2 - 2^(1 / 3)) + return [s / 2, s / 2, (1 - 2 * s) / 2, (1 - 2 * s) / 2, s / 2, s / 2] +end diff --git a/src/solvers/timedependentsum.jl b/src/solvers/timedependentsum.jl new file mode 100644 index 0000000..f2998de --- /dev/null +++ b/src/solvers/timedependentsum.jl @@ -0,0 +1,68 @@ +using ITensors: ITensor, inds, permute + +# Represents a time-dependent sum of terms: +# +# expr(t) = coefficients(expr)[1](t) * terms(expr)[1] + coefficients(expr)[2](t) * terms(expr)[2] + … +# +struct TimeDependentSum{Coefficients,Terms} + coefficients::Coefficients + terms::Terms +end + +coefficients(expr::TimeDependentSum) = expr.coefficients +terms(expr::TimeDependentSum) = expr.terms +function Base.copy(expr::TimeDependentSum) + return TimeDependentSum(coefficients(expr), copy.(terms(expr))) +end + +function Base.:*(c::Number, expr::TimeDependentSum) + scaled_coefficients = map(coefficient -> (t -> c * coefficient(t)), coefficients(expr)) + return TimeDependentSum(scaled_coefficients, terms(expr)) +end +Base.:*(expr::TimeDependentSum, c::Number) = c * expr + +# Evaluating a `TimeDependentSum` at a certain time +# returns a `ScaledSum` at that time. +function (expr::TimeDependentSum)(t::Number) + coefficients_t = map(coefficient -> coefficient(t), coefficients(expr)) + return ScaledSum(coefficients_t, terms(expr)) +end + +# alternating_update inteface +function reduced_operator(operator::TimeDependentSum) + return TimeDependentSum(coefficients(operator), reduced_operator.(terms(operator))) +end +function ITensorMPS.set_nsite!(operator::TimeDependentSum, nsite) + foreach(t -> set_nsite!(t, nsite), terms(operator)) + return operator +end +function ITensorMPS.position!(operator::TimeDependentSum, state, position) + foreach(t -> position!(t, state, position), terms(operator)) + return operator +end + +# Represents the sum of scaled terms: +# +# H = coefficient[1] * H[1] + coefficient * H[2] + … +# +struct ScaledSum{Coefficients,Terms} + coefficients::Coefficients + terms::Terms +end + +coefficients(expr::ScaledSum) = expr.coefficients +terms(expr::ScaledSum) = expr.terms + +# Apply the scaled sum of terms: +# +# expr(x) = coefficients(expr)[1] * terms(expr)[1](x) + coefficients(expr)[2] * terms(expr)[2](x) + … +# +# onto x. +function scaledsum_apply(expr, x) + return mapreduce(+, zip(coefficients(expr), terms(expr))) do coefficient_and_term + coefficient, term = coefficient_and_term + return coefficient * term(x) + end +end +(expr::ScaledSum)(x) = scaledsum_apply(expr, x) +(expr::ScaledSum)(x::ITensor) = permute(scaledsum_apply(expr, x), inds(x)) diff --git a/src/solvers/update_observer.jl b/src/solvers/update_observer.jl new file mode 100644 index 0000000..3d388ab --- /dev/null +++ b/src/solvers/update_observer.jl @@ -0,0 +1,24 @@ +struct EmptyObserver end +update_observer!(observer::EmptyObserver; kwargs...) = observer + +struct ValuesObserver{Values<:NamedTuple} + values::Values +end +function update_observer!(observer::ValuesObserver; kwargs...) + for key in keys(observer.values) + observer.values[key][] = kwargs[key] + end + return observer +end +values_observer(; kwargs...) = ValuesObserver(NamedTuple(kwargs)) + +struct ComposedObservers{Observers<:Tuple} + observers::Observers +end +compose_observers(observers...) = ComposedObservers(observers) +function update_observer!(observer::ComposedObservers; kwargs...) + for observerᵢ in observer.observers + update_observer!(observerᵢ; kwargs...) + end + return observer +end diff --git a/src/sweeps.jl b/src/sweeps.jl new file mode 100644 index 0000000..1f56633 --- /dev/null +++ b/src/sweeps.jl @@ -0,0 +1,266 @@ +using Printf: @printf + +""" +A Sweeps objects holds information +about the various parameters controlling +a density matrix renormalization group (DMRG) +or similar matrix product state (MPS) calculation. + +For a Sweeps object `sw` the available +parameters are: + + - `nsweep(sw)` -- the number of sweeps to do + - `maxdim(sw,n)` -- maximum MPS bond dimension for sweep n + - `mindim(sw,n)` -- minimum MPS bond dimension for sweep n + - `cutoff(sw,n)` -- truncation error cutoff for sweep n + - `noise(sw,n)` -- noise term coefficient for sweep n +""" +mutable struct Sweeps + nsweep::Int + maxdim::Vector{Int} + cutoff::Vector{Float64} + mindim::Vector{Int} + noise::Vector{Float64} + + function Sweeps(nsw::Int; maxdim=typemax(Int), cutoff=1E-16, mindim=1, noise=0.0) + sw = new(nsw, fill(typemax(Int), nsw), fill(1E-16, nsw), fill(1, nsw), fill(0.0, nsw)) + setmaxdim!(sw, maxdim...) + setmindim!(sw, mindim...) + setcutoff!(sw, cutoff...) + setnoise!(sw, noise...) + return sw + end +end + +Sweeps() = Sweeps(0) + +""" + Sweeps(d::AbstractMatrix) + + Sweeps(nsweep::Int, d::AbstractMatrix) + +Make a sweeps object from a matrix of input values. +The first row should be strings that define which +variables are being set ("maxdim", "cutoff", "mindim", +and "noise"). + +If the number of sweeps are not specified, they +are determined from the size of the input matrix. + +# Examples + +```julia +julia > Sweeps( + [ + "maxdim" "mindim" "cutoff" "noise" + 50 10 1e-12 1E-7 + 100 20 1e-12 1E-8 + 200 20 1e-12 1E-10 + 400 20 1e-12 0 + 800 20 1e-12 1E-11 + 800 20 1e-12 0 + ], +) +Sweeps +1cutoff = 1.0E-12, maxdim = 50, mindim = 10, noise = 1.0E-07 +2cutoff = 1.0E-12, maxdim = 100, mindim = 20, noise = 1.0E-08 +3cutoff = 1.0E-12, maxdim = 200, mindim = 20, noise = 1.0E-10 +4cutoff = 1.0E-12, maxdim = 400, mindim = 20, noise = 0.0E+00 +5cutoff = 1.0E-12, maxdim = 800, mindim = 20, noise = 1.0E-11 +6cutoff = 1.0E-12, maxdim = 800, mindim = 20, noise = 0.0E+00 +``` +""" +function Sweeps(nsw::Int, d::AbstractMatrix) + sw = Sweeps(nsw) + vars = d[1, :] + for (n, var) in enumerate(vars) + inputs = d[2:end, n] + if var == "maxdim" + maxdim!(sw, inputs...) + elseif var == "cutoff" + cutoff!(sw, inputs...) + elseif var == "mindim" + mindim!(sw, inputs...) + elseif var == "noise" + noise!(sw, float.(inputs)...) + else + error("Sweeps object does not have the field $var") + end + end + return sw +end + +Sweeps(d::AbstractMatrix) = Sweeps(size(d, 1) - 1, d) + +""" + nsweep(sw::Sweeps) + length(sw::Sweeps) + +Obtain the number of sweeps parameterized +by this sweeps object. +""" +nsweep(sw::Sweeps)::Int = sw.nsweep + +Base.length(sw::Sweeps)::Int = sw.nsweep + +Base.isempty(sw::Sweeps)::Bool = (sw.nsweep == 0) + +""" + maxdim(sw::Sweeps,n::Int) + +Maximum MPS bond dimension allowed by the +Sweeps object `sw` during sweep `n` +""" +maxdim(sw::Sweeps, n::Int)::Int = sw.maxdim[n] + +""" + mindim(sw::Sweeps,n::Int) + +Minimum MPS bond dimension allowed by the +Sweeps object `sw` during sweep `n` +""" +mindim(sw::Sweeps, n::Int)::Int = sw.mindim[n] + +""" + cutoff(sw::Sweeps,n::Int) + +Truncation error cutoff setting of the +Sweeps object `sw` during sweep `n` +""" +cutoff(sw::Sweeps, n::Int)::Float64 = sw.cutoff[n] + +""" + noise(sw::Sweeps,n::Int) + +Noise term coefficient setting of the +Sweeps object `sw` during sweep `n` +""" +noise(sw::Sweeps, n::Int)::Float64 = sw.noise[n] + +get_maxdims(sw::Sweeps) = sw.maxdim +get_mindims(sw::Sweeps) = sw.mindim +get_cutoffs(sw::Sweeps) = sw.cutoff +get_noises(sw::Sweeps) = sw.noise + +""" + maxdim!(sw::Sweeps,maxdims::Int...) + +Set the maximum MPS bond dimension for each +sweep by providing up to `nsweep(sw)` values. +If fewer values are provided, the last value +is repeated for the remaining sweeps. +""" +function setmaxdim!(sw::Sweeps, maxdims::Int...)::Nothing + mdims = collect(maxdims) + for i in 1:nsweep(sw) + sw.maxdim[i] = get(mdims, i, maxdims[end]) + end +end +maxdim!(sw::Sweeps, maxdims::Int...) = setmaxdim!(sw, maxdims...) + +""" + mindim!(sw::Sweeps,maxdims::Int...) + +Set the minimum MPS bond dimension for each +sweep by providing up to `nsweep(sw)` values. +If fewer values are provided, the last value +is repeated for the remaining sweeps. +""" +function setmindim!(sw::Sweeps, mindims::Int...)::Nothing + mdims = collect(mindims) + for i in 1:nsweep(sw) + sw.mindim[i] = get(mdims, i, mindims[end]) + end +end +mindim!(sw::Sweeps, mindims::Int...) = setmindim!(sw, mindims...) + +""" + cutoff!(sw::Sweeps,maxdims::Int...) + +Set the MPS truncation error used for each +sweep by providing up to `nsweep(sw)` values. +If fewer values are provided, the last value +is repeated for the remaining sweeps. +""" +function setcutoff!(sw::Sweeps, cutoffs::Real...)::Nothing + cuts = collect(cutoffs) + for i in 1:nsweep(sw) + sw.cutoff[i] = get(cuts, i, cutoffs[end]) + end +end +cutoff!(sw::Sweeps, cutoffs::Real...) = setcutoff!(sw, cutoffs...) + +""" + noise!(sw::Sweeps,maxdims::Int...) + +Set the noise-term coefficient used for each +sweep by providing up to `nsweep(sw)` values. +If fewer values are provided, the last value +is repeated for the remaining sweeps. +""" +function setnoise!(sw::Sweeps, noises::Real...)::Nothing + nvals = collect(noises) + for i in 1:nsweep(sw) + sw.noise[i] = get(nvals, i, noises[end]) + end +end +noise!(sw::Sweeps, noises::Real...) = setnoise!(sw, noises...) + +function Base.show(io::IO, sw::Sweeps) + println(io, "Sweeps") + for n in 1:nsweep(sw) + @printf( + io, + "%d cutoff=%.1E, maxdim=%d, mindim=%d, noise=%.1E\n", + n, + cutoff(sw, n), + maxdim(sw, n), + mindim(sw, n), + noise(sw, n) + ) + end +end + +struct SweepNext + N::Int + ncenter::Int +end + +""" + sweepnext(N::Int; ncenter::Int=2) + +Returns an iterable object that evaluates +to tuples of the form `(b,ha)` where `b` +is the bond number and `ha` is the half-sweep +number. Takes an optional named argument +`ncenter` for use with an n-site MPS or DMRG +algorithm, with a default of 2-site. +""" +function sweepnext(N::Int; ncenter::Int=2)::SweepNext + if ncenter < 0 + error("ncenter must be non-negative") + end + return SweepNext(N, ncenter) +end + +function Base.iterate(sn::SweepNext, state=(0, 1)) + b, ha = state + if ha == 1 + inc = 1 + bstop = sn.N - sn.ncenter + 2 + else + inc = -1 + bstop = 0 + end + new_b = b + inc + new_ha = ha + done = false + if new_b == bstop + new_b -= inc + new_ha += 1 + if ha == 2 + return nothing + end + end + return ((new_b, new_ha), (new_b, new_ha)) +end diff --git a/src/tdvp_sweeps.jl b/src/tdvp_sweeps.jl new file mode 100644 index 0000000..d39ea92 --- /dev/null +++ b/src/tdvp_sweeps.jl @@ -0,0 +1,17 @@ +function process_sweeps(s::Sweeps) + return (; + nsweeps=s.nsweep, maxdim=s.maxdim, mindim=s.mindim, cutoff=s.cutoff, noise=s.noise + ) +end + +function tdvp(H, t::Number, psi0::MPS, sweeps::Sweeps; kwargs...) + return tdvp(H, t, psi0; process_sweeps(sweeps)..., kwargs...) +end + +function tdvp(solver, H, t::Number, psi0::MPS, sweeps::Sweeps; kwargs...) + return tdvp(solver, H, t, psi0; process_sweeps(sweeps)..., kwargs...) +end + +function dmrg(H, psi0::MPS, sweeps::Sweeps; kwargs...) + return dmrg(H, psi0; process_sweeps(sweeps)..., kwargs...) +end diff --git a/src/update_observer.jl b/src/update_observer.jl new file mode 100644 index 0000000..43be20a --- /dev/null +++ b/src/update_observer.jl @@ -0,0 +1,7 @@ +function update_observer!(observer; kwargs...) + return error("Not implemented") +end + +function update_observer!(observer::AbstractObserver; kwargs...) + return measure!(observer; kwargs...) +end diff --git a/test/Ops/Project.toml b/test/Ops/Project.toml new file mode 100644 index 0000000..a355267 --- /dev/null +++ b/test/Ops/Project.toml @@ -0,0 +1,3 @@ +[deps] +ITensorMPS = "0d1a4710-d33b-49a5-8f18-73bdf49b47e2" +ITensors = "9136182c-28ba-11e9-034c-db9fb085ebd5" diff --git a/test/Ops/runtests.jl b/test/Ops/runtests.jl new file mode 100644 index 0000000..36cbca6 --- /dev/null +++ b/test/Ops/runtests.jl @@ -0,0 +1,18 @@ +@eval module $(gensym()) +using ITensors +using Test + +ITensors.Strided.disable_threads() +ITensors.BLAS.set_num_threads(1) +ITensors.disable_threaded_blocksparse() + +@testset "$(@__DIR__)" begin + filenames = filter(readdir(@__DIR__)) do f + startswith("test_")(f) && endswith(".jl")(f) + end + @testset "Test $(@__DIR__)/$filename" for filename in filenames + println("Running $(@__DIR__)/$filename") + @time include(filename) + end +end +end diff --git a/test/Ops/test_ops_mpo.jl b/test/Ops/test_ops_mpo.jl new file mode 100644 index 0000000..c04faea --- /dev/null +++ b/test/Ops/test_ops_mpo.jl @@ -0,0 +1,99 @@ +@eval module $(gensym()) +using ITensors: ITensor, contract, op, replaceprime +using ITensors.Ops: Op, OpSum, Prod, Scaled, Sum, expand +using ITensorMPS: ITensorMPS, MPO, siteinds +using LinearAlgebra: I, norm +using Test: @test, @testset + +@testset "Ops to MPO" begin + ∑H = Sum{Op}() + ∑H += 1.2, "X", 1, "X", 2 + ∑H += 2, "Z", 1 + ∑H += 2, "Z", 2 + + @test ∑H isa Sum{Scaled{Float64,Prod{Op}}} + + s = siteinds("Qubit", 2) + H = MPO(∑H, s) + + Id(n) = Op(I, n) + X(n) = Op("X", n) + Z(n) = Op("Z", n) + T(o) = ITensor(o, s) + Hfull = 1.2 * T(X(1)) * T(X(2)) + 2 * T(Z(1)) * T(Id(2)) + 2 * T(Id(1)) * T(Z(2)) + + @test prod(H) ≈ Hfull + + @test prod(MPO(X(1), s)) ≈ T(X(1)) * T(Id(2)) + @test prod(MPO(2X(1), s)) ≈ 2T(X(1)) * T(Id(2)) + @test prod(MPO(X(1) * Z(2), s)) ≈ T(X(1)) * T(Z(2)) + @test prod(MPO(3.5X(1) * Z(2), s)) ≈ 3.5T(X(1)) * T(Z(2)) + @test prod(MPO(X(1) + Z(2), s)) ≈ T(X(1)) * T(Id(2)) + T(Id(1)) * T(Z(2)) + @test prod(MPO(X(1) + 3.3Z(2), s)) ≈ T(X(1)) * T(Id(2)) + 3.3T(Id(1)) * T(Z(2)) + @test prod(MPO((X(1) + Z(2)) / 2, s)) ≈ 0.5T(X(1)) * T(Id(2)) + 0.5T(Id(1)) * T(Z(2)) + + @testset "OpSum to MPO with repeated terms" begin + ℋ = OpSum() + ℋ += "Z", 1 + ℋ += "Z", 1 + ℋ += "X", 2 + ℋ += "Z", 1 + ℋ += "Z", 1 + ℋ += "X", 2 + ℋ += "X", 2 + ℋ_merged = OpSum() + ℋ_merged += (4, "Z", 1) + ℋ_merged += (3, "X", 2) + @test ITensorMPS.sortmergeterms(ℋ) == ℋ_merged + + # Test with repeated terms + s = siteinds("S=1/2", 1) + ℋ = OpSum() + ("Z", 1) + ("Z", 1) + H = MPO(ℋ, s) + @test contract(H) ≈ 2 * op("Z", s, 1) + end +end + +function heisenberg_old(N) + os = OpSum() + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + end + return os +end + +function heisenberg(N) + os = Sum{Op}() + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + end + return os +end + +@testset "OpSum comparison" begin + N = 4 + s = siteinds("S=1/2", N) + os_old = heisenberg_old(N) + os_new = heisenberg(N) + @test os_old isa OpSum + @test os_new isa Sum{Scaled{Float64,Prod{Op}}} + Hold = MPO(os_old, s) + Hnew = MPO(os_new, s) + @test prod(Hold) ≈ prod(Hnew) +end + +@testset "Square Hamiltonian" begin + N = 4 + ℋ = heisenberg(N) + ℋ² = expand(ℋ^2) + s = siteinds("S=1/2", N) + H = MPO(ℋ, s) + H² = MPO(ℋ², s) + @test norm(replaceprime(H' * H, 2 => 1) - H²) ≈ 0 atol = 1e-14 + @test norm(H(H) - H²) ≈ 0 atol = 1e-14 +end +end diff --git a/test/Ops/test_trotter.jl b/test/Ops/test_trotter.jl new file mode 100644 index 0000000..f03ea7d --- /dev/null +++ b/test/Ops/test_trotter.jl @@ -0,0 +1,41 @@ +@eval module $(gensym()) +using Test: @test, @testset +using ITensorMPS: MPO, MPS, siteinds +using ITensors: ITensor, apply, contract, replaceprime +using ITensors.Ops: Op, Prod, Sum, Trotter + +function heisenberg(N) + os = Sum{Op}() + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + end + return os +end + +@testset "Heisenberg Trotter" begin + N = 4 + ℋ = heisenberg(N) + s = siteinds("S=1/2", N) + ψ₀ = MPS(s, n -> isodd(n) ? "↑" : "↓") + t = 1.0 + for nsteps in [10, 100] + for order in [1, 2] #, 4] + 𝒰 = exp(im * t * ℋ; alg=Trotter{order}(nsteps)) + U = Prod{ITensor}(𝒰, s) + ∑H = Sum{ITensor}(ℋ, s) + # XXX: Define this, filling out identities. + # ITensor(ℋ, s) + I = contract(MPO(s, "Id")) + H = 0.0 * contract(MPO(s, "Id")) + for h in ∑H + H += apply(h, I) + end + Uʳᵉᶠψ₀ = replaceprime(exp(im * t * H) * prod(ψ₀), 1 => 0) + atol = max(1e-6, 1 / nsteps^order) + @test prod(U(ψ₀)) ≈ Uʳᵉᶠψ₀ atol = atol + end + end +end +end diff --git a/test/Project.toml b/test/Project.toml index 824fb9a..ca4d25b 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,12 +1,16 @@ [deps] +ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" +HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" ITensorMPS = "0d1a4710-d33b-49a5-8f18-73bdf49b47e2" -ITensorTDVP = "25707e16-a4db-4a07-99d9-4d67b7af0342" ITensors = "9136182c-28ba-11e9-034c-db9fb085ebd5" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" KrylovKit = "0b1a1467-8014-51b9-945f-bf0ae24f4b77" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +NDTensors = "23ae76d9-e61a-49c4-8f12-3f1a16adf9cf" Observers = "338f10d5-c7f1-4033-a7d1-f9dec39bcaa0" +OptimKit = "77e91f04-9b3b-57a6-a776-40b61faaebe0" OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" diff --git a/test/base/Project.toml b/test/base/Project.toml new file mode 100644 index 0000000..e84dc5a --- /dev/null +++ b/test/base/Project.toml @@ -0,0 +1,9 @@ +[deps] +Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" +HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" +ITensorMPS = "0d1a4710-d33b-49a5-8f18-73bdf49b47e2" +ITensors = "9136182c-28ba-11e9-034c-db9fb085ebd5" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +NDTensors = "23ae76d9-e61a-49c4-8f12-3f1a16adf9cf" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" diff --git a/test/base/backup/test_arraystorage.jl b/test/base/backup/test_arraystorage.jl new file mode 100644 index 0000000..d9c25b5 --- /dev/null +++ b/test/base/backup/test_arraystorage.jl @@ -0,0 +1,22 @@ +using ITensors +using Test + +@testset "Test ArrayStorage DMRG QN $conserve_qns" for conserve_qns in (false,) # true) + n = 4 + s = siteinds("S=1/2", n; conserve_qns) + heisenberg_opsum = function (n) + os = OpSum() + for j in 1:(n - 1) + os += "Sz", j, "Sz", j + 1 + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + end + return os + end + H = MPO(heisenberg_opsum(n), s) + ψ = random_mps(s, j -> isodd(j) ? "↑" : "↓"; linkdims=4) + dmrg_kwargs = (; nsweeps=2, cutoff=[1e-4, 1e-12], maxdim=10, outputlevel=0) + e1, ψ1 = dmrg(NDTensors.to_arraystorage.((H, ψ))...; dmrg_kwargs...) + e2, ψ2 = dmrg(H, ψ; dmrg_kwargs...) + @test e1 ≈ e2 +end diff --git a/test/base/runtests.jl b/test/base/runtests.jl new file mode 100644 index 0000000..84bca80 --- /dev/null +++ b/test/base/runtests.jl @@ -0,0 +1,22 @@ +using ITensors: ITensors +using Test: @testset + +ITensors.Strided.disable_threads() +ITensors.BLAS.set_num_threads(1) +ITensors.disable_threaded_blocksparse() + +@testset "$(@__DIR__)" begin + filenames = filter(readdir(@__DIR__)) do file + return startswith("test_")(file) && endswith(".jl")(file) + end + @testset "Test $(@__DIR__)/$filename" for filename in filenames + println("Running $(@__DIR__)/$filename") + @time include(filename) + end + + test_dirs = ["test_solvers"] + @testset "Test $(@__DIR__)/$test_dir" for test_dir in test_dirs + println("Running $(@__DIR__)/$test_dir/runtests.jl") + @time include(joinpath(@__DIR__, test_dir, "runtests.jl")) + end +end diff --git a/test/base/test_abstractprojmpo.jl b/test/base/test_abstractprojmpo.jl new file mode 100644 index 0000000..07134de --- /dev/null +++ b/test/base/test_abstractprojmpo.jl @@ -0,0 +1,80 @@ +using ITensors +using Random +using Test +using ITensorMPS: ITensorMPS + +@testset "AbstractProjMPO (eltype=$elt, conserve_qns=$conserve_qns)" for elt in ( + Float32, Float64, Complex{Float32}, Complex{Float64} + ), + conserve_qns in [false, true] + + n = 4 + s = siteinds("S=1/2", n; conserve_qns) + o = MPO(elt, s, "I") + x = MPS(elt, s, j -> isodd(j) ? "↑" : "↓") + pmpo = ProjMPO(o) + position!(pmpo, x, 2) + @testset "ProjMPO (storage=$storage)" for storage in (identity, ITensors.disk) + po = storage(pmpo) + + # `AbstractProjMPO` interface. + @test ITensorMPS.nsite(po) == 2 + @test ITensorMPS.site_range(po) == 2:3 + @test eltype(po) == elt + @test isnothing(ITensorMPS.checkflux(po)) + po_contracted = contract(po, ITensor(one(Bool))) + @test po_contracted isa ITensor + @test ndims(po_contracted) == 8 + @test eltype(po_contracted) == elt + + # Specific to `ProjMPO`. + @test lproj(po) isa ITensor + @test ndims(lproj(po)) == 3 + @test eltype(lproj(po)) == elt + @test rproj(po) isa ITensor + @test ndims(rproj(po)) == 3 + @test eltype(rproj(po)) == elt + end + @testset "ProjMPOSum (storage=$storage)" for storage in (identity, ITensors.disk) + po = storage(ProjMPOSum([pmpo, pmpo])) + + # `AbstractProjMPO` interface. + @test ITensorMPS.nsite(po) == 2 + @test ITensorMPS.site_range(po) == 2:3 + @test eltype(po) == elt + @test isnothing(ITensorMPS.checkflux(po)) + po_contracted = contract(po, ITensor(one(Bool))) + @test po_contracted isa ITensor + @test ndims(po_contracted) == 8 + @test eltype(po_contracted) == elt + + # Specific to `ProjMPOSum`. + @test length(ITensorMPS.terms(po)) == 2 + end + @testset "ITensorMPS.ProjMPS" begin + # TODO: Replace with `ProjOuter`, make it into + # a proper `AbstractProjMPO`. + px = ITensorMPS.ProjMPS(x) + position!(px, x, 2) + + # `AbstractProjMPO` interface. + @test ITensorMPS.nsite(px) == 2 + @test ITensorMPS.site_range(px) == 2:3 + @test_broken eltype(px) == elt + @test isnothing(ITensorMPS.checkflux(px)) + @test_broken contract(px, ITensor(one(Bool))) + end + @testset "ITensorMPS.ProjMPO_MPS" begin + # TODO: Replace with `ProjOuter`, make it into + # a proper `AbstractProjMPO`. + po = ITensorMPS.ProjMPO_MPS(o, [x]) + position!(po, x, 2) + + # `AbstractProjMPO` interface. + @test ITensorMPS.nsite(po) == 2 + @test ITensorMPS.site_range(po) == 2:3 + @test_broken eltype(po) == elt + @test isnothing(ITensorMPS.checkflux(po)) + @test_broken contract(po, ITensor(one(Bool))) + end +end diff --git a/test/base/test_algorithm.jl b/test/base/test_algorithm.jl new file mode 100644 index 0000000..eb0f2e1 --- /dev/null +++ b/test/base/test_algorithm.jl @@ -0,0 +1,39 @@ +using ITensors +using Test + +@testset "Algorithm" begin + alg = ITensors.Algorithm("X") + + @test alg isa ITensors.Algorithm"X" + @test alg == ITensors.Algorithm"X"() + + s = siteinds("S=1/2", 4) + A = MPO(s, "Id") + ψ = random_mps(s) + + @test_throws MethodError contract(alg, A, ψ) + @test_throws MethodError contract(A, ψ; method="X") + @test_throws MethodError contract(A, ψ; alg="X") + @test contract(ITensors.Algorithm("densitymatrix"), A, ψ) ≈ A * ψ + @test contract(ITensors.Algorithm("naive"), A, ψ) ≈ A * ψ + @test contract(A, ψ; alg="densitymatrix") ≈ A * ψ + @test contract(A, ψ; method="densitymatrix") ≈ A * ψ + @test contract(A, ψ; alg="naive") ≈ A * ψ + @test contract(A, ψ; method="naive") ≈ A * ψ + + B = copy(A) + truncate!(ITensors.Algorithm("frobenius"), B) + @test A ≈ B + + B = copy(A) + truncate!(B; alg="frobenius") + @test A ≈ B + + # Custom algorithm + function ITensors.truncate!(::ITensors.Algorithm"my_new_algorithm", A::MPO; cutoff=1e-15) + return "my_new_algorithm was called with cutoff $cutoff" + end + cutoff = 1e-5 + res = truncate!(A; alg="my_new_algorithm", cutoff=cutoff) + @test res == "my_new_algorithm was called with cutoff $cutoff" +end diff --git a/test/base/test_autompo.jl b/test/base/test_autompo.jl new file mode 100644 index 0000000..c4ebeec --- /dev/null +++ b/test/base/test_autompo.jl @@ -0,0 +1,1253 @@ +@eval module $(gensym()) +using ITensorMPS, ITensors, Test, Random, JLD2 +using NDTensors: scalartype + +include(joinpath(@__DIR__, "utils", "util.jl")) + +function components_to_opsum(comps, n; reverse::Bool=true) + opsum = OpSum() + for (factor, operators, sites) in comps + # reverse ordering for compatibility + sites = reverse ? (n + 1) .- sites : sites + sites_and_ops = [[Matrix(operator), site] for (operator, site) in zip(operators, sites)] + sites_and_ops = [vcat(sites_and_ops...)...] + opsum += factor, sites_and_ops... + end + return opsum +end + +function isingMPO(sites)::MPO + H = MPO(sites) + N = length(H) + link = Vector{Index}(undef, N + 1) + for n in 1:(N + 1) + link[n] = Index(3, "Link,Ising,l=$(n-1)") + end + for n in 1:N + s = sites[n] + ll = link[n] + rl = link[n + 1] + H[n] = ITensor(dag(ll), dag(s), s', rl) + H[n] += setelt(ll => 1) * setelt(rl => 1) * op(sites, "Id", n) + H[n] += setelt(ll => 3) * setelt(rl => 3) * op(sites, "Id", n) + H[n] += setelt(ll => 2) * setelt(rl => 1) * op(sites, "Sz", n) + H[n] += setelt(ll => 3) * setelt(rl => 2) * op(sites, "Sz", n) + end + LE = ITensor(link[1]) + LE[3] = 1.0 + RE = ITensor(dag(link[N + 1])) + RE[1] = 1.0 + H[1] *= LE + H[N] *= RE + return H +end + +function heisenbergMPO(sites, h::Vector{Float64}, onsite::String="Sz")::MPO + H = MPO(sites) + N = length(H) + link = Vector{Index}(undef, N + 1) + for n in 1:(N + 1) + link[n] = Index(5, "Link,Heis,l=$(n-1)") + end + for n in 1:N + s = sites[n] + ll = link[n] + rl = link[n + 1] + H[n] = ITensor(ll, s, s', rl) + H[n] += setelt(ll => 1) * setelt(rl => 1) * op(sites, "Id", n) + H[n] += setelt(ll => 5) * setelt(rl => 5) * op(sites, "Id", n) + H[n] += setelt(ll => 2) * setelt(rl => 1) * op(sites, "S+", n) + H[n] += setelt(ll => 3) * setelt(rl => 1) * op(sites, "S-", n) + H[n] += setelt(ll => 4) * setelt(rl => 1) * op(sites, "Sz", n) + H[n] += setelt(ll => 5) * setelt(rl => 2) * op(sites, "S-", n) * 0.5 + H[n] += setelt(ll => 5) * setelt(rl => 3) * op(sites, "S+", n) * 0.5 + H[n] += setelt(ll => 5) * setelt(rl => 4) * op(sites, "Sz", n) + H[n] += setelt(ll => 5) * setelt(rl => 1) * op(sites, onsite, n) * h[n] + end + H[1] *= setelt(link[1] => 5) + H[N] *= setelt(link[N + 1] => 1) + return H +end + +function NNheisenbergMPO(sites, J1::Float64, J2::Float64)::MPO + H = MPO(sites) + N = length(H) + link = Vector{Index}(undef, N + 1) + if hasqns(sites[1]) + for n in 1:(N + 1) + link[n] = Index( + [ + QN() => 1, + QN("Sz", -2) => 1, + QN("Sz", +2) => 1, + QN() => 1, + QN("Sz", -2) => 1, + QN("Sz", +2) => 1, + QN() => 2, + ], + "Link,H,l=$(n-1)", + ) + end + else + for n in 1:(N + 1) + link[n] = Index(8, "Link,H,l=$(n-1)") + end + end + for n in 1:N + s = sites[n] + ll = dag(link[n]) + rl = link[n + 1] + H[n] = ITensor(ll, dag(s), s', rl) + H[n] += onehot(ll => 1) * onehot(rl => 1) * op(sites, "Id", n) + H[n] += onehot(ll => 8) * onehot(rl => 8) * op(sites, "Id", n) + + H[n] += onehot(ll => 2) * onehot(rl => 1) * op(sites, "S-", n) + H[n] += onehot(ll => 5) * onehot(rl => 2) * op(sites, "Id", n) + H[n] += onehot(ll => 8) * onehot(rl => 2) * op(sites, "S+", n) * J1 / 2 + H[n] += onehot(ll => 8) * onehot(rl => 5) * op(sites, "S+", n) * J2 / 2 + + H[n] += onehot(ll => 3) * onehot(rl => 1) * op(sites, "S+", n) + H[n] += onehot(ll => 6) * onehot(rl => 3) * op(sites, "Id", n) + H[n] += onehot(ll => 8) * onehot(rl => 3) * op(sites, "S-", n) * J1 / 2 + H[n] += onehot(ll => 8) * onehot(rl => 6) * op(sites, "S-", n) * J2 / 2 + + H[n] += onehot(ll => 4) * onehot(rl => 1) * op(sites, "Sz", n) + H[n] += onehot(ll => 7) * onehot(rl => 4) * op(sites, "Id", n) + H[n] += onehot(ll => 8) * onehot(rl => 4) * op(sites, "Sz", n) * J1 + H[n] += onehot(ll => 8) * onehot(rl => 7) * op(sites, "Sz", n) * J2 + end + H[1] *= onehot(link[1] => 8) + H[N] *= onehot(dag(link[N + 1]) => 1) + return H +end + +function threeSiteIsingMPO(sites, h::Vector{Float64})::MPO + H = MPO(sites) + N = length(H) + link = Vector{Index}(undef, N + 1) + for n in 1:(N + 1) + link[n] = Index(4, "Link,l=$(n-1)") + end + for n in 1:N + s = sites[n] + ll = link[n] + rl = link[n + 1] + H[n] = ITensor(ll, s, s', rl) + H[n] += setelt(ll => 1) * setelt(rl => 1) * op(sites, "Id", n) + H[n] += setelt(ll => 4) * setelt(rl => 4) * op(sites, "Id", n) + H[n] += setelt(ll => 2) * setelt(rl => 1) * op(sites, "Sz", n) + H[n] += setelt(ll => 3) * setelt(rl => 2) * op(sites, "Sz", n) + H[n] += setelt(ll => 4) * setelt(rl => 3) * op(sites, "Sz", n) + H[n] += setelt(ll => 4) * setelt(rl => 1) * op(sites, "Sx", n) * h[n] + end + H[1] *= setelt(link[1] => 4) + H[N] *= setelt(link[N + 1] => 1) + return H +end + +function fourSiteIsingMPO(sites)::MPO + H = MPO(sites) + N = length(H) + link = Vector{Index}(undef, N + 1) + for n in 1:(N + 1) + link[n] = Index(5, "Link,l=$(n-1)") + end + for n in 1:N + s = sites[n] + ll = link[n] + rl = link[n + 1] + H[n] = ITensor(ll, s, s', rl) + H[n] += setelt(ll => 1) * setelt(rl => 1) * op(sites, "Id", n) + H[n] += setelt(ll => 5) * setelt(rl => 5) * op(sites, "Id", n) + H[n] += setelt(ll => 2) * setelt(rl => 1) * op(sites, "Sz", n) + H[n] += setelt(ll => 3) * setelt(rl => 2) * op(sites, "Sz", n) + H[n] += setelt(ll => 4) * setelt(rl => 3) * op(sites, "Sz", n) + H[n] += setelt(ll => 5) * setelt(rl => 4) * op(sites, "Sz", n) + end + H[1] *= setelt(link[1] => 5) + H[N] *= setelt(link[N + 1] => 1) + return H +end + +@testset "OpSum" begin + N = 10 + + @test !ITensors.using_auto_fermion() + + @testset "Show MPOTerm" begin + os = OpSum() + add!(os, "Sz", 1, "Sz", 2) + @test length(sprint(show, os[1])) > 1 + end + + @testset "Multisite operator" begin + os = OpSum() + os += ("CX", 1, 2) + os += (2.3, "R", 3, 4, "S", 2) + os += ("X", 3) + @test length(os) == 3 + @test coefficient(os[1]) == 1 + @test length(os[1]) == 1 + @test ITensors.which_op(os[1][1]) == "CX" + @test ITensors.sites(os[1][1]) == (1, 2) + @test coefficient(os[2]) == 2.3 + @test length(os[2]) == 2 + @test ITensors.which_op(os[2][1]) == "R" + @test ITensors.sites(os[2][1]) == (3, 4) + @test ITensors.which_op(os[2][2]) == "S" + @test ITensors.sites(os[2][2]) == (2,) + @test coefficient(os[3]) == 1 + @test length(os[3]) == 1 + @test ITensors.which_op(os[3][1]) == "X" + @test ITensors.sites(os[3][1]) == (3,) + + os = OpSum() + ("CX", 1, 2) + @test length(os) == 1 + @test coefficient(os[1]) == 1 + @test length(os[1]) == 1 + @test ITensors.which_op(os[1][1]) == "CX" + @test ITensors.sites(os[1][1]) == (1, 2) + + # Coordinate + os = OpSum() + ("X", (1, 2)) + @test length(os) == 1 + @test coefficient(os[1]) == 1 + @test length(os[1]) == 1 + @test ITensors.which_op(os[1][1]) == "X" + @test ITensors.sites(os[1][1]) == ((1, 2),) + + os = OpSum() + ("CX", 1, 2, (ϕ=π / 3,)) + @test length(os) == 1 + @test coefficient(os[1]) == 1 + @test length(os[1]) == 1 + @test ITensors.which_op(os[1][1]) == "CX" + @test ITensors.sites(os[1][1]) == (1, 2) + @test ITensors.params(os[1][1]) == (ϕ=π / 3,) + + os = OpSum() + ("CX", 1, 2, (ϕ=π / 3,), "CZ", 3, 4, (θ=π / 2,)) + @test length(os) == 1 + @test coefficient(os[1]) == 1 + @test length(os[1]) == 2 + @test ITensors.which_op(os[1][1]) == "CX" + @test ITensors.sites(os[1][1]) == (1, 2) + @test ITensors.params(os[1][1]) == (ϕ=π / 3,) + @test ITensors.which_op(os[1][2]) == "CZ" + @test ITensors.sites(os[1][2]) == (3, 4) + @test ITensors.params(os[1][2]) == (θ=π / 2,) + + os = OpSum() + ("CX", (ϕ=π / 3,), 1, 2, "CZ", (θ=π / 2,), 3, 4) + @test length(os) == 1 + @test coefficient(os[1]) == 1 + @test length(os[1]) == 2 + @test ITensors.which_op(os[1][1]) == "CX" + @test ITensors.sites(os[1][1]) == (1, 2) + @test ITensors.params(os[1][1]) == (ϕ=π / 3,) + @test ITensors.which_op(os[1][2]) == "CZ" + @test ITensors.sites(os[1][2]) == (3, 4) + @test ITensors.params(os[1][2]) == (θ=π / 2,) + + os = OpSum() + ("CX", 1, 2, (ϕ=π / 3,)) + @test length(os) == 1 + @test coefficient(os[1]) == 1 + @test length(os[1]) == 1 + @test ITensors.which_op(os[1][1]) == "CX" + @test ITensors.sites(os[1][1]) == (1, 2) + @test ITensors.params(os[1][1]) == (ϕ=π / 3,) + + os = OpSum() + (1 + 2im, "CRz", (ϕ=π / 3,), 1, 2) + @test length(os) == 1 + @test coefficient(os[1]) == 1 + 2im + @test length(os[1]) == 1 + @test ITensors.which_op(os[1][1]) == "CRz" + @test ITensors.sites(os[1][1]) == (1, 2) + @test ITensors.params(os[1][1]) == (ϕ=π / 3,) + + os = OpSum() + ("CRz", (ϕ=π / 3,), 1, 2) + @test length(os) == 1 + @test coefficient(os[1]) == 1 + @test length(os[1]) == 1 + @test ITensors.which_op(os[1][1]) == "CRz" + @test ITensors.sites(os[1][1]) == (1, 2) + @test ITensors.params(os[1][1]) == (ϕ=π / 3,) + end + + @testset "Show OpSum" begin + os = OpSum() + add!(os, "Sz", 1, "Sz", 2) + add!(os, "Sz", 2, "Sz", 3) + @test length(sprint(show, os)) > 1 + end + + @testset "OpSum algebra (eltype=$elt)" for elt in ( + Float32, Float64, Complex{Float32}, Complex{Float64} + ) + n = 5 + sites = siteinds("S=1/2", n) + O1 = OpSum() + for j in 1:(n - 1) + O1 += "Sz", j, "Sz", j + 1 + end + O2 = OpSum() + for j in 1:n + O2 += "Sx", j + end + O = O1 + 2 * O2 + @test length(O) == 2 * n - 1 + H1 = MPO(elt, O1, sites) + H2 = MPO(elt, O2, sites) + H = H1 + 2 * H2 + @test scalartype(H1) == elt + @test scalartype(H2) == elt + @test scalartype(H) == elt + @test prod(MPO(O, sites)) ≈ prod(H) + + @test scalartype(MPO(elt, Op("Sz", 1), sites)) == elt + @test scalartype(MPO(elt, Op("Sz", 1) + Op("Sz", 2), sites)) == elt + @test scalartype(MPO(elt, 2 * Op("Sz", 1) + 3 * Op("Sz", 2), sites)) == elt + @test scalartype(MPO(elt, 2 * Op("Sz", 1), sites)) == elt + @test scalartype(MPO(elt, Op("Sz", 1) * Op("Sz", 2), sites)) == elt + @test scalartype(MPO(elt, 2 * Op("Sz", 1) * Op("Sz", 2), sites)) == elt + + O = O1 - 2 * O2 + @test length(O) == 2 * n - 1 + H1 = MPO(elt, O1, sites) + H2 = MPO(elt, O2, sites) + H = H1 - 2 * H2 + @test scalartype(H1) == elt + @test scalartype(H2) == elt + @test scalartype(H) == elt + @test prod(MPO(O, sites)) ≈ prod(H) + + O = O1 - O2 / 2 + @test length(O) == 2 * n - 1 + H1 = MPO(elt, O1, sites) + H2 = MPO(elt, O2, sites) + H = H1 - H2 / 2 + @test scalartype(H1) == elt + @test scalartype(H2) == elt + @test scalartype(H) == elt + @test prod(MPO(O, sites)) ≈ prod(H) + end + + @testset "Single creation op" begin + os = OpSum() + add!(os, "Adagup", 3) + sites = siteinds("Electron", N) + W = MPO(os, sites) + psi = makeRandomMPS(sites) + cdu_psi = copy(psi) + cdu_psi[3] = noprime(cdu_psi[3] * op(sites, "Adagup", 3)) + @test inner(psi', W, psi) ≈ inner(cdu_psi, psi) + end + + @testset "Ising" begin + os = OpSum() + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + @test scalartype(Ha) <: Float64 + He = isingMPO(sites) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + + H_complex = MPO(ComplexF64, os, sites) + @test scalartype(H_complex) <: ComplexF64 + @test H_complex ≈ Ha + end + + @testset "Ising" begin + os = OpSum() + for j in 1:(N - 1) + os -= "Sz", j, "Sz", j + 1 + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = -isingMPO(sites) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Ising-Different Order" begin + os = OpSum() + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = isingMPO(sites) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Heisenberg" begin + os = OpSum() + h = rand(N) #random magnetic fields + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + end + for j in 1:N + os += h[j], "Sz", j + end + + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = heisenbergMPO(sites, h) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Multiple Onsite Ops" begin + sites = siteinds("S=1", N) + os1 = OpSum() + for j in 1:(N - 1) + os1 += "Sz", j, "Sz", j + 1 + os1 += 0.5, "S+", j, "S-", j + 1 + os1 += 0.5, "S-", j, "S+", j + 1 + end + for j in 1:N + os1 += "Sz * Sz", j + end + Ha1 = MPO(os1, sites) + + os2 = OpSum() + for j in 1:(N - 1) + os2 += "Sz", j, "Sz", j + 1 + os2 += 0.5, "S+", j, "S-", j + 1 + os2 += 0.5, "S-", j, "S+", j + 1 + end + for j in 1:N + os2 += "Sz", j, "Sz", j + end + Ha2 = MPO(os2, sites) + + He = heisenbergMPO(sites, ones(N), "Sz * Sz") + psi = makeRandomMPS(sites) + Oe = inner(psi', He, psi) + Oa1 = inner(psi', Ha1, psi) + @test Oa1 ≈ Oe + Oa2 = inner(psi', Ha2, psi) + @test Oa2 ≈ Oe + end + + @testset "Three-site ops" begin + os = OpSum() + # To test version of add! taking a coefficient + add!(os, 1.0, "Sz", 1, "Sz", 2, "Sz", 3) + @test length(os) == 1 + for j in 2:(N - 2) + add!(os, "Sz", j, "Sz", j + 1, "Sz", j + 2) + end + h = ones(N) + for j in 1:N + add!(os, h[j], "Sx", j) + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = threeSiteIsingMPO(sites, h) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Four-site ops" begin + os = OpSum() + for j in 1:(N - 3) + add!(os, "Sz", j, "Sz", j + 1, "Sz", j + 2, "Sz", j + 3) + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = fourSiteIsingMPO(sites) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Next-neighbor Heisenberg" begin + os = OpSum() + J1 = 1.0 + J2 = 0.5 + for j in 1:(N - 1) + add!(os, J1, "Sz", j, "Sz", j + 1) + add!(os, J1 * 0.5, "S+", j, "S-", j + 1) + add!(os, J1 * 0.5, "S-", j, "S+", j + 1) + end + for j in 1:(N - 2) + add!(os, J2, "Sz", j, "Sz", j + 2) + add!(os, J2 * 0.5, "S+", j, "S-", j + 2) + add!(os, J2 * 0.5, "S-", j, "S+", j + 2) + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + + He = NNheisenbergMPO(sites, J1, J2) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + #@test maxlinkdim(Ha) == 8 + end + + @testset "Onsite Regression Test" begin + sites = siteinds("S=1", 4) + os = OpSum() + add!(os, 0.5, "Sx", 1) + add!(os, 0.5, "Sy", 1) + H = MPO(os, sites) + l = commonind(H[1], H[2]) + T = setelt(l => 1) * H[1] + O = op(sites[1], "Sx") + op(sites[1], "Sy") + @test norm(T - 0.5 * O) < 1E-8 + + sites = siteinds("S=1", 2) + os = OpSum() + add!(os, 0.5im, "Sx", 1) + add!(os, 0.5, "Sy", 1) + H = MPO(os, sites) + T = H[1] * H[2] + O = + im * op(sites[1], "Sx") * op(sites[2], "Id") + op(sites[1], "Sy") * op(sites[2], "Id") + @test norm(T - 0.5 * O) < 1E-8 + end + + @testset "+ syntax" begin + @testset "Single creation op" begin + os = OpSum() + os += "Adagup", 3 + sites = siteinds("Electron", N) + W = MPO(os, sites) + psi = makeRandomMPS(sites) + cdu_psi = copy(psi) + cdu_psi[3] = noprime(cdu_psi[3] * op(sites, "Adagup", 3)) + @test inner(psi', W, psi) ≈ inner(cdu_psi, psi) + end + + @testset "Ising" begin + os = OpSum() + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = isingMPO(sites) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Ising-Different Order" begin + os = OpSum() + for j in 1:(N - 1) + os += "Sz", j + 1, "Sz", j + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = isingMPO(sites) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Heisenberg" begin + os = OpSum() + h = rand(N) #random magnetic fields + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + end + for j in 1:N + os += h[j], "Sz", j + end + + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = heisenbergMPO(sites, h) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Multiple Onsite Ops" begin + sites = siteinds("S=1", N) + os1 = OpSum() + for j in 1:(N - 1) + os1 += "Sz", j, "Sz", j + 1 + os1 += 0.5, "S+", j, "S-", j + 1 + os1 += 0.5, "S-", j, "S+", j + 1 + end + for j in 1:N + os1 += "Sz * Sz", j + end + Ha1 = MPO(os1, sites) + + os2 = OpSum() + for j in 1:(N - 1) + os2 += "Sz", j, "Sz", j + 1 + os2 += 0.5, "S+", j, "S-", j + 1 + os2 += 0.5, "S-", j, "S+", j + 1 + end + for j in 1:N + os2 += "Sz", j, "Sz", j + end + Ha2 = MPO(os2, sites) + + He = heisenbergMPO(sites, ones(N), "Sz * Sz") + psi = makeRandomMPS(sites) + Oe = inner(psi', He, psi) + Oa1 = inner(psi', Ha1, psi) + @test Oa1 ≈ Oe + Oa2 = inner(psi', Ha2, psi) + @test Oa2 ≈ Oe + end + + @testset "Three-site ops" begin + os = OpSum() + # To test version of add! taking a coefficient + os += 1.0, "Sz", 1, "Sz", 2, "Sz", 3 + @test length(os) == 1 + for j in 2:(N - 2) + os += "Sz", j, "Sz", j + 1, "Sz", j + 2 + end + h = ones(N) + for j in 1:N + os += h[j], "Sx", j + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = threeSiteIsingMPO(sites, h) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Four-site ops" begin + os = OpSum() + for j in 1:(N - 3) + os += "Sz", j, "Sz", j + 1, "Sz", j + 2, "Sz", j + 3 + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = fourSiteIsingMPO(sites) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Next-neighbor Heisenberg" begin + os = OpSum() + J1 = 1.0 + J2 = 0.5 + for j in 1:(N - 1) + os += J1, "Sz", j, "Sz", j + 1 + os += J1 * 0.5, "S+", j, "S-", j + 1 + os += J1 * 0.5, "S-", j, "S+", j + 1 + end + for j in 1:(N - 2) + os += J2, "Sz", j, "Sz", j + 2 + os += J2 * 0.5, "S+", j, "S-", j + 2 + os += J2 * 0.5, "S-", j, "S+", j + 2 + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + + He = NNheisenbergMPO(sites, J1, J2) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + #@test maxlinkdim(Ha) == 8 + end + + #@testset "-= syntax" begin + # os = OpSum() + # os += (-1,"Sz",1,"Sz",2) + # os2 = OpSum() + # os2 -= ("Sz",1,"Sz",2) + # @test os == os2 + #end + + @testset "Onsite Regression Test" begin + sites = siteinds("S=1", 4) + os = OpSum() + os += 0.5, "Sx", 1 + os += 0.5, "Sy", 1 + H = MPO(os, sites) + l = commonind(H[1], H[2]) + T = setelt(l => 1) * H[1] + O = op(sites[1], "Sx") + op(sites[1], "Sy") + @test norm(T - 0.5 * O) < 1E-8 + + sites = siteinds("S=1", 2) + os = OpSum() + os += 0.5im, "Sx", 1 + os += 0.5, "Sy", 1 + H = MPO(os, sites) + T = H[1] * H[2] + O = + im * op(sites[1], "Sx") * op(sites[2], "Id") + + op(sites[1], "Sy") * op(sites[2], "Id") + @test norm(T - 0.5 * O) < 1E-8 + end + end + + @testset ".+= and .-= syntax" begin + + #@testset ".-= syntax" begin + # os = OpSum() + # os .+= (-1,"Sz",1,"Sz",2) + # os2 = OpSum() + # os2 .-= ("Sz",1,"Sz",2) + # @test os == os2 + #end + + @testset "Single creation op" begin + os = OpSum() + os .+= "Adagup", 3 + sites = siteinds("Electron", N) + W = MPO(os, sites) + psi = makeRandomMPS(sites) + cdu_psi = copy(psi) + cdu_psi[3] = noprime(cdu_psi[3] * op(sites, "Adagup", 3)) + @test inner(psi', W, psi) ≈ inner(cdu_psi, psi) + end + + @testset "Ising" begin + os = OpSum() + for j in 1:(N - 1) + os .+= "Sz", j, "Sz", j + 1 + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = isingMPO(sites) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Ising-Different Order" begin + os = OpSum() + for j in 1:(N - 1) + os .+= "Sz", j + 1, "Sz", j + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = isingMPO(sites) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Heisenberg" begin + os = OpSum() + h = rand(N) #random magnetic fields + for j in 1:(N - 1) + os .+= "Sz", j, "Sz", j + 1 + os .+= 0.5, "S+", j, "S-", j + 1 + os .+= 0.5, "S-", j, "S+", j + 1 + end + for j in 1:N + os .+= h[j], "Sz", j + end + + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = heisenbergMPO(sites, h) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Multiple Onsite Ops" begin + sites = siteinds("S=1", N) + os1 = OpSum() + for j in 1:(N - 1) + os1 .+= "Sz", j, "Sz", j + 1 + os1 .+= 0.5, "S+", j, "S-", j + 1 + os1 .+= 0.5, "S-", j, "S+", j + 1 + end + for j in 1:N + os1 .+= "Sz * Sz", j + end + Ha1 = MPO(os1, sites) + + os2 = OpSum() + for j in 1:(N - 1) + os2 .+= "Sz", j, "Sz", j + 1 + os2 .+= 0.5, "S+", j, "S-", j + 1 + os2 .+= 0.5, "S-", j, "S+", j + 1 + end + for j in 1:N + os2 .+= "Sz", j, "Sz", j + end + Ha2 = MPO(os2, sites) + + He = heisenbergMPO(sites, ones(N), "Sz * Sz") + psi = makeRandomMPS(sites) + Oe = inner(psi', He, psi) + Oa1 = inner(psi', Ha1, psi) + @test Oa1 ≈ Oe + Oa2 = inner(psi', Ha2, psi) + @test Oa2 ≈ Oe + end + + @testset "Three-site ops" begin + os = OpSum() + # To test version of add! taking a coefficient + os .+= 1.0, "Sz", 1, "Sz", 2, "Sz", 3 + @test length(os) == 1 + for j in 2:(N - 2) + os .+= "Sz", j, "Sz", j + 1, "Sz", j + 2 + end + h = ones(N) + for j in 1:N + os .+= h[j], "Sx", j + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = threeSiteIsingMPO(sites, h) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Four-site ops" begin + os = OpSum() + for j in 1:(N - 3) + os .+= "Sz", j, "Sz", j + 1, "Sz", j + 2, "Sz", j + 3 + end + sites = siteinds("S=1/2", N) + Ha = MPO(os, sites) + He = fourSiteIsingMPO(sites) + psi = makeRandomMPS(sites) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + end + + @testset "Next-neighbor Heisenberg" begin + os = OpSum() + J1 = 1.0 + J2 = 0.5 + for j in 1:(N - 1) + os .+= J1, "Sz", j, "Sz", j + 1 + os .+= J1 * 0.5, "S+", j, "S-", j + 1 + os .+= J1 * 0.5, "S-", j, "S+", j + 1 + end + for j in 1:(N - 2) + os .+= J2, "Sz", j, "Sz", j + 2 + os .+= J2 * 0.5, "S+", j, "S-", j + 2 + os .+= J2 * 0.5, "S-", j, "S+", j + 2 + end + sites = siteinds("S=1/2", N; conserve_qns=true) + Ha = MPO(os, sites) + + He = NNheisenbergMPO(sites, J1, J2) + psi = random_mps(sites, [isodd(n) ? "Up" : "Dn" for n in 1:N]) + Oa = inner(psi', Ha, psi) + Oe = inner(psi', He, psi) + @test Oa ≈ Oe + #@test maxlinkdim(Ha) == 8 + end + + @testset "Onsite Regression Test" begin + sites = siteinds("S=1", 4) + os = OpSum() + os .+= 0.5, "Sx", 1 + os .+= 0.5, "Sy", 1 + H = MPO(os, sites) + l = commonind(H[1], H[2]) + T = setelt(l => 1) * H[1] + O = op(sites[1], "Sx") + op(sites[1], "Sy") + @test norm(T - 0.5 * O) < 1E-8 + + sites = siteinds("S=1", 2) + os = OpSum() + os .+= 0.5im, "Sx", 1 + os .+= 0.5, "Sy", 1 + H = MPO(os, sites) + T = H[1] * H[2] + O = + im * op(sites[1], "Sx") * op(sites[2], "Id") + + op(sites[1], "Sy") * op(sites[2], "Id") + @test norm(T - 0.5 * O) < 1E-8 + end + end + + @testset "Fermionic Operators" begin + N = 5 + s = siteinds("Fermion", N) + + a1 = OpSum() + a1 += "Cdag", 1, "C", 3 + M1 = MPO(a1, s) + + a2 = OpSum() + a2 -= 1, "C", 3, "Cdag", 1 + M2 = MPO(a2, s) + + a3 = OpSum() + a3 += "Cdag", 1, "N", 2, "C", 3 + M3 = MPO(a3, s) + + p011 = MPS(s, [1, 2, 2, 1, 1]) + p110 = MPS(s, [2, 2, 1, 1, 1]) + + @test inner(p110', M1, p011) ≈ -1.0 + @test inner(p110', M2, p011) ≈ -1.0 + @test inner(p110', M3, p011) ≈ -1.0 + + p001 = MPS(s, [1, 1, 2, 1, 1]) + p100 = MPS(s, [2, 1, 1, 1, 1]) + + @test inner(p100', M1, p001) ≈ +1.0 + @test inner(p100', M2, p001) ≈ +1.0 + @test inner(p100', M3, p001) ≈ 0.0 + + # + # Repeat similar test but + # with Electron sites + # + + s = siteinds("Electron", N; conserve_qns=true) + + a1 = OpSum() + a1 += "Cdagup", 1, "Cup", 3 + M1 = MPO(a1, s) + + a2 = OpSum() + a2 -= 1, "Cdn", 3, "Cdagdn", 1 + M2 = MPO(a2, s) + + p0uu = MPS(s, [1, 2, 2, 1, 1]) + puu0 = MPS(s, [2, 2, 1, 1, 1]) + p0ud = MPS(s, [1, 2, 3, 1, 1]) + pdu0 = MPS(s, [3, 2, 1, 1, 1]) + p00u = MPS(s, [1, 1, 2, 1, 1]) + pu00 = MPS(s, [2, 1, 1, 1, 1]) + p00d = MPS(s, [1, 1, 3, 1, 1]) + pd00 = MPS(s, [3, 1, 1, 1, 1]) + + @test inner(puu0', M1, p0uu) ≈ -1.0 + @test inner(pdu0', M2, p0ud) ≈ -1.0 + @test inner(pu00', M1, p00u) ≈ +1.0 + @test inner(pd00', M2, p00d) ≈ +1.0 + end + + @testset "Chemical Hamiltonian Test" begin + for auto_fermion in [false, true] + if auto_fermion + ITensors.enable_auto_fermion() + else + ITensors.disable_auto_fermion() + end + N = 6 + t = randn(N, N) + V = randn(N, N, N, N) + s = siteinds("Electron", N; conserve_qns=true) + + ost = OpSum() + for i in 1:N, j in 1:N + ost += t[i, j], "Cdagup", i, "Cup", j + ost += t[i, j], "Cdagdn", i, "Cdn", j + end + Ht = MPO(ost, s) + + osV = OpSum() + for i in 1:N, j in 1:N, k in 1:N, l in 1:N + osV += V[i, j, k, l], "Cdagup", i, "Cdagup", j, "Cup", k, "Cup", l + osV += V[i, j, k, l], "Cdagup", i, "Cdagdn", j, "Cdn", k, "Cup", l + osV += V[i, j, k, l], "Cdagdn", i, "Cdagup", j, "Cup", k, "Cdn", l + osV += V[i, j, k, l], "Cdagdn", i, "Cdagdn", j, "Cdn", k, "Cdn", l + end + HV = MPO(osV, s) + + for i in 1:N, j in 1:N + stᵢ = fill("0", N) + stⱼ = fill("0", N) + stᵢ[i] = "Up" + stⱼ[j] = "Up" + psiᵢ = MPS(s, stᵢ) + psiⱼ = MPS(s, stⱼ) + @test abs(inner(psiᵢ', Ht, psiⱼ) - t[i, j]) < 1E-10 + end + + for i in 1:N, j in 1:N, k in 1:N, l in 1:N + ((i == j) || (k == l)) && continue + + stᵢⱼ = fill("0", N) + stᵢⱼ[i] = "Up" + stᵢⱼ[j] = "Up" + psiᵢⱼ = MPS(s, stᵢⱼ) + + stₖₗ = fill("0", N) + stₖₗ[k] = "Up" + stₖₗ[l] = "Up" + psiₖₗ = MPS(s, stₖₗ) + + mpo_val = inner(psiᵢⱼ', HV, psiₖₗ) + exact_val = 0.0 + for m in 1:N, n in 1:N, p in 1:N, q in 1:N + if m == i && n == j && p == l && q == k + exact_val += V[i, j, l, k] + elseif m == i && n == j && p == k && q == l + exact_val -= V[i, j, k, l] + elseif m == j && n == i && p == l && q == k + exact_val -= V[j, i, l, k] + elseif m == j && n == i && p == k && q == l + exact_val += V[j, i, k, l] + end + end + (k > l) && (exact_val *= -1) + (i > j) && (exact_val *= -1) + @test abs(mpo_val - exact_val) < 1E-10 + end + end + ITensors.disable_auto_fermion() + end + + @testset "Complex OpSum Coefs" begin + N = 4 + + for use_qn in [false, true] + sites = siteinds("S=1/2", N; conserve_qns=use_qn) + os = OpSum() + for i in 1:(N - 1) + os += +1im, "S+", i, "S-", i + 1 + os -= 1im, "S-", i, "S+", i + 1 + end + H = MPO(os, sites) + psiud = MPS(sites, [1, 2, 1, 2]) + psidu = MPS(sites, [2, 1, 1, 2]) + @test inner(psiud', H, psidu) ≈ +1im + @test inner(psidu', H, psiud) ≈ -1im + end + end + + @testset "Non-zero QN MPO" begin + N = 4 + s = siteinds("Boson", N; conserve_qns=true) + + j = 3 + terms = OpSum() + terms += "Adag", j + W = MPO(terms, s) + + function op_mpo(sites, which_op, j) + N = length(sites) + ops = [n < j ? "Id" : (n > j ? "Id" : which_op) for n in 1:N] + M = MPO([op(ops[n], sites[n]) for n in 1:length(sites)]) + q = flux(op(which_op, sites[j])) + links = [Index([n < j ? q => 1 : QN() => 1], "Link,l=$n") for n in 1:N] + for n in 1:(N - 1) + M[n] *= onehot(links[n] => 1) + M[n + 1] *= onehot(dag(links[n]) => 1) + end + return M + end + M = op_mpo(s, "Adag", j) + + @test norm(prod(W) - prod(M)) < 1E-10 + + psi = random_mps(s, [isodd(n) ? "1" : "0" for n in 1:length(s)]; linkdims=4) + Mpsi = apply(M, psi; alg="naive") + Wpsi = apply(M, psi; alg="naive") + @test abs(inner(Mpsi, Wpsi) / inner(Mpsi, Mpsi) - 1.0) < 1E-10 + end + + @testset "Fermion OpSum Issue 514 Regression Test" begin + N = 4 + s = siteinds("Electron", N; conserve_qns=true) + os1 = OpSum() + os2 = OpSum() + + os1 += "Nup", 1 + os2 += "Cdagup", 1, "Cup", 1 + + M1 = MPO(os1, s) + M2 = MPO(os2, s) + + H1 = M1[1] * M1[2] * M1[3] * M1[4] + H2 = M2[1] * M2[2] * M2[3] * M2[4] + + @test norm(H1 - H2) ≈ 0.0 + end + + @testset "OpSum in-place modification regression test" begin + N = 2 + t = 1.0 + os = OpSum() + for n in 1:(N - 1) + os .-= t, "Cdag", n, "C", n + 1 + os .-= t, "Cdag", n + 1, "C", n + end + s = siteinds("Fermion", N; conserve_qns=true) + os_original = deepcopy(os) + for i in 1:4 + MPO(os, s) + @test os == os_original + end + end + + @testset "Accuracy Regression Test (Issue 725)" begin + ITensors.space(::SiteType"HardCore") = 2 + + ITensors.state(::StateName"0", ::SiteType"HardCore") = [1.0, 0.0] + ITensors.state(::StateName"1", ::SiteType"HardCore") = [0.0, 1.0] + + function ITensors.op!(Op::ITensor, ::OpName"N", ::SiteType"HardCore", s::Index) + return Op[s' => 2, s => 2] = 1 + end + + function ITensors.op!(Op::ITensor, ::OpName"Adag", ::SiteType"HardCore", s::Index) + return Op[s' => 1, s => 2] = 1 + end + + function ITensors.op!(Op::ITensor, ::OpName"A", ::SiteType"HardCore", s::Index) + return Op[s' => 2, s => 1] = 1 + end + + t = 1.0 + V1 = 1E-3 + V2 = 2E-5 + + N = 20 + sites = siteinds("HardCore", N) + + os = OpSum() + for j in 1:(N - 1) + os -= t, "Adag", j, "A", j + 1 + os -= t, "A", j, "Adag", j + 1 + os += V1, "N", j, "N", j + 1 + end + for j in 1:(N - 2) + os += V2, "N", j, "N", j + 2 + end + H = MPO(os, sites) + psi0 = MPS(sites, n -> isodd(n) ? "0" : "1") + @test abs(inner(psi0', H, psi0) - 0.00018) < 1E-10 + end + + @testset "Matrix operator representation" begin + dim = 4 + op = rand(dim, dim) + opt = op' + s = [Index(dim), Index(dim)] + a = OpSum() + a += 1.0, op + opt, 1 + a += 1.0, op + opt, 2 + mpoa = MPO(a, s) + b = OpSum() + b += 1.0, op, 1 + b += 1.0, opt, 1 + b += 1.0, op, 2 + b += 1.0, opt, 2 + mpob = MPO(b, s) + @test mpoa ≈ mpob + end + + @testset "Matrix operator representation - hashing bug" begin + n = 4 + dim = 4 + s = siteinds(dim, n) + o = rand(dim, dim) + os = OpSum() + for j in 1:(n - 1) + os += copy(o), j, copy(o), j + 1 + end + H1 = MPO(os, s) + H2 = ITensor() + H2 += op(o, s[1]) * op(o, s[2]) * op("I", s[3]) * op("I", s[4]) + H2 += op("I", s[1]) * op(o, s[2]) * op(o, s[3]) * op("I", s[4]) + H2 += op("I", s[1]) * op("I", s[2]) * op(o, s[3]) * op(o, s[4]) + @test contract(H1) ≈ H2 + end + + @testset "Matrix operator representation - hashing bug" begin + file_path = joinpath(@__DIR__, "utils", "opsum_hash_bug.jld2") + comps, n, dims = load(file_path, "comps", "n", "dims") + s = [Index(d) for d in dims] + for _ in 1:100 + os = components_to_opsum(comps, n) + # Before defining `hash(::Op, h::UInt)`, this + # would randomly throw an error due to + # some hashing issue in `MPO(::OpSum, ...)` + MPO(os, s) + end + end + + @testset "Operator with empty blocks - issue #963" begin + sites = siteinds("Fermion", 2; conserve_qns=true) + opsum1 = OpSum() + for p in 1:2, q in 1:2, r in 1:2, s in 1:2 + opsum1 += "c†", p, "c†", q, "c", r, "c", s + end + H1 = MPO(opsum1, sites) + opsum2 = OpSum() + for p in 1:2, q in 1:2, r in 1:2, s in 1:2 + if !(p == q == r == s) + opsum2 += "c†", p, "c†", q, "c", r, "c", s + end + end + H2 = MPO(opsum2, sites) + @test H1 ≈ H2 + end + + @testset "One-site ops bond dimension test" begin + sites = siteinds("S=1/2", N) + + # one-site operator on every site + os = OpSum() + for j in 1:N + os += "Z", j + end + H = MPO(os, sites) + @test all(linkdims(H) .== 2) + + # one-site operator on a single site + os = OpSum() + os += "Z", rand(1:N) + H = MPO(os, sites) + @test all(linkdims(H) .<= 2) + @test_broken all(linkdims(H) .== 1) + end + + @testset "Regression test (Issue 1150): Zero blocks operator" begin + N = 4 + sites = siteinds("Fermion", N; conserve_qns=true) + os = OpSum() + os += (1.111, "Cdag", 3, "Cdag", 4, "C", 2, "C", 1) + os += (2.222, "Cdag", 4, "Cdag", 1, "C", 3, "C", 2) + os += (3.333, "Cdag", 1, "Cdag", 4, "C", 4, "C", 1) + os += (4.444, "Cdag", 2, "Cdag", 3, "C", 1, "C", 4) + # The following operator has C on site 2 twice, resulting + # in a local operator with no blocks (exactly zero), + # causing a certain logical step in working out the column qn + # to fail: + os += (5.555, "Cdag", 4, "Cdag", 4, "C", 2, "C", 2) + @test_nowarn H = MPO(os, sites) + end +end +end diff --git a/test/base/test_deprecated.jl b/test/base/test_deprecated.jl new file mode 100644 index 0000000..a44bb03 --- /dev/null +++ b/test/base/test_deprecated.jl @@ -0,0 +1,27 @@ +@eval module $(gensym()) +using ITensorMPS: MPS, maxlinkdim, randomMPS, siteinds +using LinearAlgebra: norm +using Test: @test, @testset +@testset "randomMPS" begin + sites = siteinds("S=1/2", 4) + state = j -> isodd(j) ? "↑" : "↓" + linkdims = 2 + # Deprecated linkdims syntax + for mps in [ + randomMPS(Float64, sites, state; linkdims), + randomMPS(Float64, sites; linkdims), + randomMPS(sites, state; linkdims), + randomMPS(sites, linkdims), + # Deprecated linkdims syntax + randomMPS(Float64, sites, state, linkdims), + randomMPS(Float64, sites, linkdims), + randomMPS(sites, state, linkdims), + randomMPS(sites, linkdims), + ] + @test mps isa MPS + @test length(mps) == 4 + @test maxlinkdim(mps) == 2 + @test norm(mps) > 0 + end +end +end diff --git a/test/base/test_dmrg.jl b/test/base/test_dmrg.jl new file mode 100644 index 0000000..28affb7 --- /dev/null +++ b/test/base/test_dmrg.jl @@ -0,0 +1,468 @@ +using ITensors, Test, Random +using ITensorMPS: dmrg, nsite, set_nsite!, siteinds, site_range + +@testset "Basic DMRG" begin + @testset "Spin-one Heisenberg" begin + N = 10 + sites = siteinds("S=1", N) + + os = OpSum() + for j in 1:(N - 1) + add!(os, "Sz", j, "Sz", j + 1) + add!(os, 0.5, "S+", j, "S-", j + 1) + add!(os, 0.5, "S-", j, "S+", j + 1) + end + H = MPO(os, sites) + + psi = random_mps(sites) + + sweeps = Sweeps(3) + @test length(sweeps) == 3 + maxdim!(sweeps, 10, 20, 40) + mindim!(sweeps, 1, 10) + cutoff!(sweeps, 1E-11) + noise!(sweeps, 1E-10) + str = split(sprint(show, sweeps), '\n') + @test length(str) > 1 + energy, psi = dmrg(H, psi, sweeps; outputlevel=0) + @test energy < -12.0 + end + + @testset "QN-conserving Spin-one Heisenberg" begin + N = 10 + sites = siteinds("S=1", N; conserve_qns=true) + + os = OpSum() + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + end + H = MPO(os, sites) + + state = [isodd(n) ? "Up" : "Dn" for n in 1:N] + psi = random_mps(sites, state; linkdims=4) + + sweeps = Sweeps(3) + @test length(sweeps) == 3 + maxdim!(sweeps, 10, 20, 40) + mindim!(sweeps, 1, 10) + cutoff!(sweeps, 1E-11) + noise!(sweeps, 1E-10) + str = split(sprint(show, sweeps), '\n') + @test length(str) > 1 + energy, psi = dmrg(H, psi, sweeps; outputlevel=0) + @test energy < -12.0 + end + + @testset "QN-conserving Spin-one Heisenberg with disk caching" begin + N = 10 + sites = siteinds("S=1", N; conserve_qns=true) + + os = OpSum() + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + end + H = MPO(os, sites) + + state = [isodd(n) ? "Up" : "Dn" for n in 1:N] + psi = random_mps(sites, state; linkdims=4) + + sweeps = Sweeps(3) + @test length(sweeps) == 3 + maxdim!(sweeps, 10, 20, 40) + mindim!(sweeps, 1, 10) + cutoff!(sweeps, 1E-11) + noise!(sweeps, 1E-10) + str = split(sprint(show, sweeps), '\n') + @test length(str) > 1 + energy, psi = dmrg(H, psi, sweeps; outputlevel=0, write_when_maxdim_exceeds=15) + @test energy < -12.0 + end + + @testset "ProjMPO with disk caching" begin + N = 10 + sites = siteinds("S=1", N; conserve_qns=true) + + os = OpSum() + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + end + H = MPO(os, sites) + + state = [isodd(n) ? "Up" : "Dn" for n in 1:N] + psi = random_mps(sites, state; linkdims=4) + PH = ProjMPO(H) + + PHc = copy(PH) + + n = 4 + orthogonalize!(psi, n) + position!(PH, psi, n) + PHdisk = ITensors.disk(PH) + + @test length(PH) == N + @test length(PHdisk) == N + @test site_range(PH) == n:(n + 1) + @test eltype(PH) == Float64 + ## TODO sometimes random_mps gives a linkdim value of 3 + ## which causes an error in `calculated_dim = 3^2 * 4^2` + calculated_dim = + linkdim(psi, n - 1) * + linkdim(psi, n + 1) * + dim(siteind(psi, n)) * + dim(siteind(psi, n + 1)) + @test size(PH) == (calculated_dim, calculated_dim) + @test PH.lpos == n - 1 + @test PH.rpos == n + 2 + @test PHc.lpos == 0 + @test PHc.rpos == N + 1 + @test rproj(PH) ≈ rproj(PHdisk) + @test PHdisk.LR isa ITensors.DiskVector{ITensor} + @test PHdisk.LR[PHdisk.rpos] ≈ PHdisk.Rcache + position!(PH, psi, N) + @test PH.lpos == N - 1 + end + + @testset "ProjMPOSum DMRG with disk caching" begin + N = 10 + sites = siteinds("S=1", N; conserve_qns=true) + + osA = OpSum() + for j in 1:(N - 1) + osA += "Sz", j, "Sz", j + 1 + end + HA = MPO(osA, sites) + + osB = OpSum() + for j in 1:(N - 1) + osB += 0.5, "S+", j, "S-", j + 1 + osB += 0.5, "S-", j, "S+", j + 1 + end + HB = MPO(osB, sites) + + state = [isodd(n) ? "Up" : "Dn" for n in 1:N] + psi = random_mps(sites, state; linkdims=4) + + energy, psi = dmrg( + [HA, HB], psi; nsweeps=3, maxdim=[10, 20, 30], write_when_maxdim_exceeds=10 + ) + @test energy < -12.0 + end + + @testset "ProjMPO: nsite" begin + N = 10 + sites = siteinds("S=1", N) + + os1 = OpSum() + for j in 1:(N - 1) + os1 += 0.5, "S+", j, "S-", j + 1 + os1 += 0.5, "S-", j, "S+", j + 1 + end + os2 = OpSum() + for j in 1:(N - 1) + os2 += "Sz", j, "Sz", j + 1 + end + H1 = MPO(os1, sites) + H2 = MPO(os2, sites) + + state = [isodd(n) ? "Up" : "Dn" for n in 1:N] + psi = random_mps(sites, state; linkdims=4) + PH1 = ProjMPO(H1) + PH = ProjMPOSum([H1, H2]) + PH1c = copy(PH1) + PHc = copy(PH) + @test nsite(PH1) == 2 + @test nsite(PH) == 2 + @test nsite(PH1c) == 2 + @test nsite(PHc) == 2 + + set_nsite!(PH1, 3) + @test nsite(PH1) == 3 + @test nsite(PH1c) == 2 + @test nsite(PHc) == 2 + + set_nsite!(PH, 4) + @test nsite(PH) == 4 + @test nsite(PH1c) == 2 + @test nsite(PHc) == 2 + end + + @testset "Transverse field Ising" begin + N = 32 + sites = siteinds("S=1/2", N) + Random.seed!(432) + psi0 = random_mps(sites) + + os = OpSum() + for j in 1:N + j < N && add!(os, -1.0, "Z", j, "Z", j + 1) + add!(os, -1.0, "X", j) + end + H = MPO(os, sites) + + sweeps = Sweeps(5) + maxdim!(sweeps, 10, 20) + cutoff!(sweeps, 1E-12) + noise!(sweeps, 1E-10) + energy, psi = dmrg(H, psi0, sweeps; outputlevel=0) + + # Exact energy for transverse field Ising model + # with open boundary conditions at criticality + energy_exact = 1.0 - 1.0 / sin(π / (4 * N + 2)) + @test abs((energy - energy_exact) / energy_exact) < 1e-4 + end + + @testset "Compact Sweeps syntax" begin + N = 32 + sites = siteinds("S=1/2", N) + Random.seed!(432) + psi0 = random_mps(sites) + + function ising(N; h=1.0) + os = OpSum() + for j in 1:N + j < N && (os -= ("Z", j, "Z", j + 1)) + os -= h, "X", j + end + return os + end + + h = 1.0 + H = MPO(ising(N; h=h), sites) + energy, psi = dmrg( + H, psi0; nsweeps=5, maxdim=[10, 20], cutoff=1e-12, noise=1e-10, outputlevel=0 + ) + + energy_exact = 1.0 - 1.0 / sin(π / (4 * N + 2)) + @test abs((energy - energy_exact) / energy_exact) < 1e-4 + end + + @testset "Transverse field Ising, conserve Sz parity" begin + N = 32 + sites = siteinds("S=1/2", N; conserve_szparity=true) + Random.seed!(432) + + state = [isodd(j) ? "↑" : "↓" for j in 1:N] + psi0 = random_mps(sites, state) + + os = OpSum() + for j in 1:N + j < N && add!(os, -1.0, "X", j, "X", j + 1) + add!(os, -1.0, "Z", j) + end + H = MPO(os, sites) + + sweeps = Sweeps(5) + maxdim!(sweeps, 10, 20) + cutoff!(sweeps, 1E-12) + noise!(sweeps, 1E-10) + energy, psi = dmrg(H, psi0, sweeps; outputlevel=0) + + # Exact energy for transverse field Ising model + # with open boundary conditions at criticality + energy_exact = 1.0 - 1.0 / sin(π / (4 * N + 2)) + @test abs((energy - energy_exact) / energy_exact) < 1e-4 + end + + @testset "DMRGObserver" begin + + # Test that basic constructors work + observer = DMRGObserver() + observer = DMRGObserver(; minsweeps=2, energy_tol=1E-4) + + # Test in a DMRG calculation + N = 10 + sites = siteinds("S=1/2", N) + Random.seed!(42) + psi0 = random_mps(sites) + + os = OpSum() + for j in 1:(N - 1) + os -= 1, "Sz", j, "Sz", j + 1 + end + for j in 1:N + os -= 0.2, "Sx", j + end + H = MPO(os, sites) + + sweeps = Sweeps(3) + maxdim!(sweeps, 10) + cutoff!(sweeps, 1E-12) + + observer = DMRGObserver(["Sz", "Sx"], sites) + + E, psi = dmrg(H, psi0, sweeps; observer=observer, outputlevel=0) + @test length(measurements(observer)["Sz"]) == 3 + @test length(measurements(observer)["Sx"]) == 3 + @test all(length.(measurements(observer)["Sz"]) .== N) + @test all(length.(measurements(observer)["Sx"]) .== N) + @test length(energies(observer)) == 3 + @test length(truncerrors(observer)) == 3 + @test energies(observer)[end] == E + @test all(truncerrors(observer) .< 1E-9) + + orthogonalize!(psi, 1) + m = scalar(dag(psi[1]) * noprime(op(sites, "Sz", 1) * psi[1])) + @test measurements(observer)["Sz"][end][1] ≈ m + end + + @testset "Sum of MPOs (ProjMPOSum)" begin + N = 10 + sites = siteinds("S=1", N) + + osZ = OpSum() + for j in 1:(N - 1) + osZ += "Sz", j, "Sz", j + 1 + end + HZ = MPO(osZ, sites) + + osXY = OpSum() + for j in 1:(N - 1) + osXY += 0.5, "S+", j, "S-", j + 1 + osXY += 0.5, "S-", j, "S+", j + 1 + end + HXY = MPO(osXY, sites) + + psi = random_mps(sites) + + sweeps = Sweeps(3) + maxdim!(sweeps, 10, 20, 40) + mindim!(sweeps, 1, 10, 10) + cutoff!(sweeps, 1E-11) + noise!(sweeps, 1E-10) + energy, psi = dmrg([HZ, HXY], psi, sweeps; outputlevel=0) + @test energy < -12.0 + end + + @testset "Excited-state DMRG" begin + N = 10 + weight = 15.0 + + sites = siteinds("S=1", N) + sites[1] = Index(2, "S=1/2,n=1,Site") + sites[N] = Index(2, "S=1/2,n=$N,Site") + + os = OpSum() + for j in 1:(N - 1) + os += "Sz", j, "Sz", j + 1 + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + end + H = MPO(os, sites) + + psi0i = random_mps(sites; linkdims=10) + + sweeps = Sweeps(4) + maxdim!(sweeps, 10, 20, 100, 100) + cutoff!(sweeps, 1E-11) + noise!(sweeps, 1E-10) + + energy0, psi0 = dmrg(H, psi0i, sweeps; outputlevel=0) + @test energy0 < -11.5 + + psi1i = random_mps(sites; linkdims=10) + energy1, psi1 = dmrg(H, [psi0], psi1i, sweeps; outputlevel=0, weight=weight) + + @test energy1 > energy0 + @test energy1 < -11.1 + + @test inner(psi1, psi0) < 1E-5 + end + + @testset "Fermionic Hamiltonian" begin + N = 10 + t1 = 1.0 + t2 = 0.5 + V = 0.2 + s = siteinds("Fermion", N; conserve_qns=true) + + state = fill(1, N) + state[1] = 2 + state[3] = 2 + state[5] = 2 + state[7] = 2 + psi0 = MPS(s, state) + + os = OpSum() + for j in 1:(N - 1) + os -= t1, "Cdag", j, "C", j + 1 + os -= t1, "Cdag", j + 1, "C", j + os += V, "N", j, "N", j + 1 + end + for j in 1:(N - 2) + os -= t2, "Cdag", j, "C", j + 2 + os -= t2, "Cdag", j + 2, "C", j + end + H = MPO(os, s) + + sweeps = Sweeps(5) + maxdim!(sweeps, 10, 20, 100, 100, 200) + cutoff!(sweeps, 1E-8) + noise!(sweeps, 1E-10) + + energy, psi = dmrg(H, psi0, sweeps; outputlevel=0) + @test (-6.5 < energy < -6.4) + end + + @testset "Hubbard model" begin + N = 10 + Npart = 8 + t1 = 1.0 + U = 1.0 + V1 = 0.5 + sites = siteinds("Electron", N; conserve_qns=true) + os = OpSum() + for i in 1:N + os += (U, "Nupdn", i) + end + for b in 1:(N - 1) + os -= t1, "Cdagup", b, "Cup", b + 1 + os -= t1, "Cdagup", b + 1, "Cup", b + os -= t1, "Cdagdn", b, "Cdn", b + 1 + os -= t1, "Cdagdn", b + 1, "Cdn", b + os += V1, "Ntot", b, "Ntot", b + 1 + end + H = MPO(os, sites) + sweeps = Sweeps(6) + maxdim!(sweeps, 50, 100, 200, 400, 800, 800) + cutoff!(sweeps, 1E-10) + state = ["Up", "Dn", "Dn", "Up", "Emp", "Up", "Up", "Emp", "Dn", "Dn"] + psi0 = random_mps(sites, state; linkdims=10) + energy, psi = dmrg(H, psi0, sweeps; outputlevel=0) + @test (-8.02 < energy < -8.01) + end + + @testset "Input Without Ortho Center or Not at 1" begin + N = 6 + sites = siteinds("S=1", N) + + os = OpSum() + for j in 1:(N - 1) + add!(os, "Sz", j, "Sz", j + 1) + add!(os, 0.5, "S+", j, "S-", j + 1) + add!(os, 0.5, "S-", j, "S+", j + 1) + end + H = MPO(os, sites) + + sweeps = Sweeps(1) + maxdim!(sweeps, 10) + cutoff!(sweeps, 1E-11) + + psi0 = random_mps(sites; linkdims=4) + + # Test that input works with wrong ortho center: + orthogonalize!(psi0, 5) + energy, psi = dmrg(H, psi0, sweeps; outputlevel=0) + + # Test that input works with no ortho center: + for j in 1:N + psi0[j] = random_itensor(inds(psi0[j])) + end + energy, psi = dmrg(H, psi0, sweeps; outputlevel=0) + end +end diff --git a/test/base/test_examples.jl b/test/base/test_examples.jl new file mode 100644 index 0000000..ead536e --- /dev/null +++ b/test/base/test_examples.jl @@ -0,0 +1,30 @@ +@eval module $(gensym()) +using ITensorMPS: ITensorMPS +using Suppressor: @capture_out +using Test: @test_nowarn, @testset +@testset "Example Codes" begin + @testset "DMRG with Observer" begin + @test_nowarn begin + @capture_out begin + include( + joinpath(pkgdir(ITensorMPS), "examples", "dmrg", "1d_ising_with_observer.jl") + ) + end + end + end + @testset "Package Compile Code" begin + @test_nowarn begin + @capture_out begin + include( + joinpath( + pkgdir(ITensorMPS), + "ext", + "ITensorMPSPackageCompilerExt", + "precompile_itensormps.jl", + ), + ) + end + end + end +end +end diff --git a/test/base/test_exports.jl b/test/base/test_exports.jl new file mode 100644 index 0000000..429b021 --- /dev/null +++ b/test/base/test_exports.jl @@ -0,0 +1,26 @@ +@eval module $(gensym()) +using ITensorMPS: ITensorMPS +include("utils/TestITensorMPSExportedNames.jl") +using Test: @test, @test_broken, @testset +@testset "Exports and aliases" begin + @testset "Exports" begin + # @show setdiff(names(ITensorMPS), TestITensorMPSExportedNames.ITENSORMPS_EXPORTED_NAMES) + # @show setdiff(TestITensorMPSExportedNames.ITENSORMPS_EXPORTED_NAMES, names(ITensorMPS)) + @test issetequal( + names(ITensorMPS), TestITensorMPSExportedNames.ITENSORMPS_EXPORTED_NAMES + ) + # Test the names are actually defined, if not we might need to import them + # from ITensors.jl. + for name in TestITensorMPSExportedNames.ITENSORMPS_EXPORTED_NAMES + @test isdefined(ITensorMPS, name) + end + end + @testset "Not exported" begin + for name in + [:AbstractProjMPO, :ProjMPS, :makeL!, :makeR!, :set_terms, :sortmergeterms, :terms] + @test isdefined(ITensorMPS, name) + @test !Base.isexported(ITensorMPS, name) + end + end +end +end diff --git a/test/base/test_fermions.jl b/test/base/test_fermions.jl new file mode 100644 index 0000000..05eed7c --- /dev/null +++ b/test/base/test_fermions.jl @@ -0,0 +1,290 @@ +@eval module $(gensym()) +using ITensorMPS +using ITensors +using Test + +@testset "AutoFermion MPS, MPO, and OpSum" begin + ITensors.enable_auto_fermion() + + @testset "MPS Tests" begin + @testset "Product MPS consistency checks" begin + s = siteinds("Fermion", 3; conserve_qns=true) + + pA = MPS(s, [2, 1, 2]) + TA = ITensor(s[1], s[2], s[3]) + TA[s[1] => 2, s[2] => 1, s[3] => 2] = 1.0 + A = pA[1] * pA[2] * pA[3] + @test norm(A - TA) < 1E-8 + + pB = MPS(s, [1, 2, 2]) + TB = ITensor(s[1], s[2], s[3]) + TB[s[1] => 1, s[2] => 2, s[3] => 2] = 1.0 + B = pB[1] * pB[2] * pB[3] + @test norm(B - TB) < 1E-8 + end + @testset "MPS inner regression test" begin + sites = siteinds("Fermion", 3; conserve_qns=true) + psi = MPS(sites, [2, 2, 1]) + @test inner(psi, psi) ≈ 1.0 + end + @testset "Orthogonalize of Product MPS" begin + N = 3 + + sites = siteinds("Fermion", N; conserve_qns=true) + + state = [1 for n in 1:N] + state[1] = 2 + state[2] = 2 + psi = MPS(sites, state) + psi_fluxes = [flux(psi[n]) for n in 1:N] + + psi_orig = copy(psi) + orthogonalize!(psi, 1) + @test inner(psi_orig, psi) ≈ 1.0 + @test inner(psi, psi_orig) ≈ 1.0 + end + end + + @testset "Fermionic OpSum Tests" begin + @testset "Spinless Fermion Hamiltonian" begin + N = 2 + sites = siteinds("Fermion", N; conserve_qns=true) + t1 = 1.0 + os = OpSum() + for b in 1:(N - 1) + os -= t1, "Cdag", b, "C", b + 1 + os -= t1, "Cdag", b + 1, "C", b + end + H = MPO(os, sites) + + HH = H[1] + for n in 2:N + HH *= H[n] + end + HHc = dag(swapprime(HH, 0, 1)) + @test norm(HHc - HH) < 1E-8 + end + + @testset "Fermion Hamiltonian Matrix Elements" begin + N = 10 + t1 = 0.654 + V1 = 1.23 + + sites = siteinds("Fermion", N; conserve_qns=true) + + os = OpSum() + for b in 1:(N - 1) + os -= t1, "Cdag", b, "C", b + 1 + os -= t1, "Cdag", b + 1, "C", b + os += V1, "N", b, "N", b + 1 + end + H = MPO(os, sites) + + for j in 1:(N - 2) + stateA = [1 for n in 1:N] + stateA[j] = 2 + stateA[N] = 2 # to make MPS bosonic + + stateB = [1 for n in 1:N] + stateB[j + 1] = 2 + stateB[N] = 2 # to make MPS bosonic + + psiA = MPS(sites, stateA) + psiB = MPS(sites, stateB) + + @test inner(psiA', H, psiB) ≈ -t1 + @test inner(psiB', H, psiA) ≈ -t1 + end + + for j in 1:(N - 1) + state = [1 for n in 1:N] + state[j] = 2 + state[j + 1] = 2 + psi = MPS(sites, state) + @test inner(psi', H, psi) ≈ V1 + end + end + + @testset "Fermion Second Neighbor Hopping" begin + N = 4 + t1 = 1.79 + t2 = 0.427 + s = siteinds("Fermion", N; conserve_qns=true) + os = OpSum() + for n in 1:(N - 1) + os -= t1, "Cdag", n, "C", n + 1 + os -= t1, "Cdag", n + 1, "C", n + end + for n in 1:(N - 2) + os -= t2, "Cdag", n, "C", n + 2 + os -= t2, "Cdag", n + 2, "C", n + end + H = MPO(os, s) + + state1 = [1 for n in 1:N] + state1[1] = 2 + state1[4] = 2 + psi1 = MPS(s, state1) + + state2 = [1 for n in 1:N] + state2[2] = 2 + state2[4] = 2 + psi2 = MPS(s, state2) + + state3 = [1 for n in 1:N] + state3[3] = 2 + state3[4] = 2 + psi3 = MPS(s, state3) + + @test inner(psi1', H, psi2) ≈ -t1 + @test inner(psi2', H, psi1) ≈ -t1 + @test inner(psi2', H, psi3) ≈ -t1 + @test inner(psi3', H, psi2) ≈ -t1 + + @test inner(psi1', H, psi3) ≈ -t2 + @test inner(psi3', H, psi1) ≈ -t2 + + # Add stationary particle to site 2, + # hopping over should change sign: + state1[2] = 2 + psi1 = MPS(s, state1) + state3[2] = 2 + psi3 = MPS(s, state3) + @test inner(psi1', H, psi3) ≈ +t2 + @test inner(psi3', H, psi1) ≈ +t2 + end + + @testset "OpSum Regression Test" begin + N = 3 + s = siteinds("Fermion", N; conserve_qns=true) + + os = OpSum() + os += "Cdag", 1, "C", 3 + @test_nowarn H = MPO(os, s) + end + end + + @testset "DMRG Tests" begin + @testset "Nearest Neighbor Fermions" begin + N = 8 + t1 = 1.0 + V1 = 4.0 + + s = siteinds("Fermion", N; conserve_qns=true) + + ost = OpSum() + osV = OpSum() + for b in 1:(N - 1) + ost -= t1, "Cdag", b, "C", b + 1 + ost -= t1, "Cdag", b + 1, "C", b + osV += V1, "N", b, "N", b + 1 + end + Ht = MPO(ost, s) + HV = MPO(osV, s) + + state = ["Emp" for n in 1:N] + for i in 1:2:N + state[i] = "Occ" + end + psi0 = MPS(s, state) + + sweeps = Sweeps(3) + maxdim!(sweeps, 20, 20, 40, 80, 200) + cutoff!(sweeps, 1E-6) + + correct_energy = -2.859778 + + energy, psi = dmrg([Ht, HV], psi0, sweeps; outputlevel=0) + @test abs(energy - correct_energy) < 1E-4 + + # Test using SVD within DMRG too: + energy, psi = dmrg([Ht, HV], psi0, sweeps; outputlevel=0, which_decomp="svd") + @test abs(energy - correct_energy) < 1E-4 + + # Test using only eigen decomp: + energy, psi = dmrg([Ht, HV], psi0, sweeps; outputlevel=0, which_decomp="eigen") + @test abs(energy - correct_energy) < 1E-4 + end + + @testset "Further Neighbor and Correlations" begin + N = 8 + t1 = 1.0 + t2 = 0.2 + + s = siteinds("Fermion", N; conserve_qns=true) + + ost = OpSum() + for b in 1:(N - 1) + ost -= t1, "Cdag", b, "C", b + 1 + ost -= t1, "Cdag", b + 1, "C", b + end + for b in 1:(N - 2) + ost -= t2, "Cdag", b, "C", b + 2 + ost -= t2, "Cdag", b + 2, "C", b + end + Ht = MPO(ost, s) + + state = ["Emp" for n in 1:N] + for i in 1:2:N + state[i] = "Occ" + end + psi0 = MPS(s, state) + + sweeps = Sweeps(3) + maxdim!(sweeps, 20, 20, 40, 80, 200) + cutoff!(sweeps, 1E-6) + + energy, psi = dmrg(Ht, psi0, sweeps; outputlevel=0) + + energy_inner = inner(psi', Ht, psi) + + C = correlation_matrix(psi, "Cdag", "C") + C_energy = + sum(j -> -2t1 * C[j, j + 1], 1:(N - 1)) + sum(j -> -2t2 * C[j, j + 2], 1:(N - 2)) + + @test energy_inner ≈ energy + @test C_energy ≈ energy + end + end + + @testset "MPS gate system" begin + @testset "Fermion sites" begin + N = 3 + + s = siteinds("Fermion", N; conserve_qns=true) + + # Ground state |000⟩ + ψ000 = MPS(s, "0") + + # Start state |011⟩ + ψ011 = MPS(s, n -> n == 2 || n == 3 ? "1" : "0") + + # Reference state |110⟩ + ψ110 = MPS(s, n -> n == 1 || n == 2 ? "1" : "0") + + function ITensors.op(::OpName"CdagC3", ::SiteType, s1::Index, s2::Index) + return op("Cdag", s1) * op("C", s2) + end + + os = [("CdagC3", 1, 3)] + Os = ops(os, s) + + # Results in -|110⟩ + ψ1 = product(Os, ψ011; cutoff=1e-15) + + @test inner(ψ1, ψ110) == -1 + + os = OpSum() + os += "Cdag", 1, "C", 3 + H = MPO(os, s) + + # Results in -|110⟩ + ψ2 = noprime(contract(H, ψ011; cutoff=1e-15)) + + @test inner(ψ2, ψ110) == -1 + end + end + + ITensors.disable_auto_fermion() +end +end diff --git a/test/base/test_inference.jl b/test/base/test_inference.jl new file mode 100644 index 0000000..c408db0 --- /dev/null +++ b/test/base/test_inference.jl @@ -0,0 +1,21 @@ +using ITensors +using ITensors.NDTensors +using Test + +@testset "dmrg" begin + N = 10 + sites = siteinds("S=1", N) + opsum = OpSum() + for j in 1:(N - 1) + opsum += "Sz", j, "Sz", j + 1 + opsum += 0.5, "S+", j, "S-", j + 1 + opsum += 0.5, "S-", j, "S+", j + 1 + end + H = MPO(opsum, sites) + psi0 = random_mps(sites; linkdims=10) + sweeps = Sweeps(5) + setmaxdim!(sweeps, 10, 20, 100, 100, 200) + setcutoff!(sweeps, 1E-11) + @test @inferred(Tuple{Any,MPS}, dmrg(H, psi0, sweeps; outputlevel=0)) isa + Tuple{Float64,MPS} +end diff --git a/test/base/test_lattices.jl b/test/base/test_lattices.jl new file mode 100644 index 0000000..0a86ae7 --- /dev/null +++ b/test/base/test_lattices.jl @@ -0,0 +1,14 @@ +using ITensors, Test + +@test LatticeBond(1, 2) == LatticeBond(1, 2, 0.0, 0.0, 0.0, 0.0, "") +@testset "Square lattice" begin + sL = square_lattice(3, 4) + @test length(sL) == 17 +end + +@testset "Triangular lattice" begin + tL = triangular_lattice(3, 4) + @test length(tL) == 23 + tL = triangular_lattice(3, 4; yperiodic=true) + @test length(tL) == 28 # inc. periodic vertical bonds +end diff --git a/test/base/test_mpo.jl b/test/base/test_mpo.jl new file mode 100644 index 0000000..047e738 --- /dev/null +++ b/test/base/test_mpo.jl @@ -0,0 +1,863 @@ +@eval module $(gensym()) +using Combinatorics +using ITensorMPS +using ITensors +using NDTensors: scalartype +using StableRNGs: StableRNG +using Test + +include(joinpath(@__DIR__, "utils", "util.jl")) + +function basicRandomMPO(sites; dim=4) + M = MPO(sites) + N = length(M) + links = [Index(dim, "n=$(n-1),Link") for n in 1:(N + 1)] + for n in 1:N + M[n] = random_itensor(links[n], sites[n], sites[n]', links[n + 1]) + end + M[1] *= delta(links[1]) + M[N] *= delta(links[N + 1]) + return M +end + +@testset "[first]siteinds(::MPO)" begin + N = 5 + s = siteinds("S=1/2", N) + M = random_mpo(s) + v = siteinds(M) + for n in 1:N + @test hassameinds(v[n], (s[n], s[n]')) + end + @test firstsiteinds(M) == s +end + +@testset "MPO Basics" begin + N = 6 + sites = [Index(2, "Site,n=$n") for n in 1:N] + @test length(MPO()) == 0 + @test length(MPO(Index{Int}[])) == 0 + O = MPO(sites) + @test length(O) == N + + str = split(sprint(show, O), '\n') + @test endswith(str[1], "MPO") + @test length(str) == length(O) + 2 + + O[1] = ITensor(sites[1], prime(sites[1])) + @test hasind(O[1], sites[1]) + @test hasind(O[1], prime(sites[1])) + P = copy(O) + @test hasind(P[1], sites[1]) + @test hasind(P[1], prime(sites[1])) + # test constructor from Vector{ITensor} + K = random_mpo(sites) + @test ITensors.data(MPO(copy(ITensors.data(K)))) == ITensors.data(K) + + @testset "orthogonalize!" begin + phi = random_mps(sites) + K = random_mpo(sites) + orthogonalize!(phi, 1) + orthogonalize!(K, 1) + orig_inner = ⋅(phi', K, phi) + orthogonalize!(phi, div(N, 2)) + orthogonalize!(K, div(N, 2)) + @test ⋅(phi', K, phi) ≈ orig_inner + end + + @testset "norm MPO" begin + A = random_mpo(sites) + Adag = sim(linkinds, dag(A)) + A² = ITensor(1) + for j in 1:N + A² *= Adag[j] * A[j] + end + @test A²[] ≈ inner(A, A) + @test sqrt(A²[]) ≈ norm(A) + for j in 1:N + A[j] ./= j + end + reset_ortho_lims!(A) + @test norm(A) ≈ 1 / factorial(N) + end + + @testset "lognorm MPO" begin + A = random_mpo(sites) + for j in 1:N + A[j] .*= j + end + reset_ortho_lims!(A) + Adag = sim(linkinds, dag(A)) + A² = ITensor(1) + for j in 1:N + A² *= Adag[j] * A[j] + end + @test A²[] ≈ A ⋅ A + @test 0.5 * log(A²[]) ≈ lognorm(A) + @test lognorm(A) ≈ log(factorial(N)) + end + + @testset "inner " begin + phi = random_mps(sites) + K = random_mpo(sites) + @test maxlinkdim(K) == 1 + psi = random_mps(sites) + phidag = dag(phi) + prime!(phidag) + phiKpsi = phidag[1] * K[1] * psi[1] + for j in 2:N + phiKpsi *= phidag[j] * K[j] * psi[j] + end + @test phiKpsi[] ≈ inner(phi', K, psi) + + badsites = [Index(2, "Site") for n in 1:(N + 1)] + badpsi = random_mps(badsites) + @test_throws DimensionMismatch inner(phi', K, badpsi) + + # make bigger random MPO... + for link_dim in 2:5 + mpo_tensors = ITensor[ITensor() for ii in 1:N] + mps_tensors = ITensor[ITensor() for ii in 1:N] + mps_tensors2 = ITensor[ITensor() for ii in 1:N] + mpo_link_inds = [Index(link_dim, "r$ii,Link") for ii in 1:(N - 1)] + mps_link_inds = [Index(link_dim, "r$ii,Link") for ii in 1:(N - 1)] + mpo_tensors[1] = random_itensor(mpo_link_inds[1], sites[1], sites[1]') + mps_tensors[1] = random_itensor(mps_link_inds[1], sites[1]) + mps_tensors2[1] = random_itensor(mps_link_inds[1], sites[1]) + for ii in 2:(N - 1) + mpo_tensors[ii] = random_itensor( + mpo_link_inds[ii], mpo_link_inds[ii - 1], sites[ii], sites[ii]' + ) + mps_tensors[ii] = random_itensor( + mps_link_inds[ii], mps_link_inds[ii - 1], sites[ii] + ) + mps_tensors2[ii] = random_itensor( + mps_link_inds[ii], mps_link_inds[ii - 1], sites[ii] + ) + end + mpo_tensors[N] = random_itensor(mpo_link_inds[N - 1], sites[N], sites[N]') + mps_tensors[N] = random_itensor(mps_link_inds[N - 1], sites[N]) + mps_tensors2[N] = random_itensor(mps_link_inds[N - 1], sites[N]) + K = MPO(mpo_tensors, 0, N + 1) + psi = MPS(mps_tensors, 0, N + 1) + phi = MPS(mps_tensors2, 0, N + 1) + orthogonalize!(psi, 1; maxdim=link_dim) + orthogonalize!(K, 1; maxdim=link_dim) + orthogonalize!(phi, 1; normalize=true, maxdim=link_dim) + phidag = dag(phi) + prime!(phidag) + phiKpsi = phidag[1] * K[1] * psi[1] + for j in 2:N + phiKpsi *= phidag[j] * K[j] * psi[j] + end + @test scalar(phiKpsi) ≈ inner(phi', K, psi) + end + end + + @testset "loginner " begin + n = 4 + c = 2 + + s = siteinds("S=1/2", n) + ψ = c .* random_mps(s; linkdims=4) + Φ = c .* random_mps(s; linkdims=4) + K = random_mpo(s) + + @test log(complex(inner(ψ', K, Φ))) ≈ loginner(ψ', K, Φ) + end + + @testset "inner " begin + phi = makeRandomMPS(sites) + + K = makeRandomMPO(sites; chi=2) + J = makeRandomMPO(sites; chi=2) + + psi = makeRandomMPS(sites) + phidag = dag(phi) + prime!(phidag, 2) + Jdag = dag(J) + prime!(Jdag) + for j in eachindex(Jdag) + swapprime!(Jdag[j], 2, 3) + swapprime!(Jdag[j], 1, 2) + swapprime!(Jdag[j], 3, 1) + end + + phiJdagKpsi = phidag[1] * Jdag[1] * K[1] * psi[1] + for j in eachindex(psi)[2:end] + phiJdagKpsi = phiJdagKpsi * phidag[j] * Jdag[j] * K[j] * psi[j] + end + + @test phiJdagKpsi[] ≈ inner(J, phi, K, psi) + + badsites = [Index(2, "Site") for n in 1:(N + 1)] + badpsi = random_mps(badsites) + @test_throws DimensionMismatch inner(J, phi, K, badpsi) + + # generic tags and prime levels + Kgen = replacetags(K, "Site" => "OpOut"; plev=1) + noprime!(replacetags!(Kgen, "Site" => "Kpsi"; plev=0)) + ITensorMPS.sim!(siteinds, Kgen) + + Jgen = replacetags(J, "Site" => "OpOut"; plev=1) + noprime!(replacetags!(Jgen, "Site" => "Jphi"; plev=0)) + ITensorMPS.sim!(siteinds, Jgen) + # make sure operators share site indices + replaceinds!.(Jgen, siteinds(Jgen; tags="OpOut"), siteinds(Kgen; tags="OpOut")) + + # make sure states share site indices with operators + psigen = replace_siteinds(psi, siteinds(Kgen; tags="Kpsi")) + phigen = replace_siteinds(phi, siteinds(Jgen; tags="Jphi")) + + @test phiJdagKpsi[] ≈ inner(Jgen, phigen, Kgen, psigen) + + badpsigen = replacetags(psigen, "Kpsi" => "notKpsi") + badphigen = sim(siteinds, phigen) + badKgen = replacetags(Kgen, "OpOut" => "notOpOut") + badJgen = sim(siteinds, Jgen) + @test_throws ErrorException inner(Jgen, phigen, Kgen, badpsigen) + @test_throws ErrorException inner(Jgen, badphigen, Kgen, psigen) + @test_throws ErrorException inner(Jgen, phigen, badKgen, psigen) + @test_throws ErrorException inner(badJgen, phigen, Kgen, psigen) + end + + @testset "error_contract" begin + phi = makeRandomMPS(sites) + K = makeRandomMPO(sites; chi=2) + + psi = makeRandomMPS(sites) + + dist = sqrt( + abs(1 + (inner(phi, phi) - 2 * real(inner(phi', K, psi))) / inner(K, psi, K, psi)) + ) + @test dist ≈ error_contract(phi, K, psi) + + badsites = [Index(2, "Site") for n in 1:(N + 1)] + badpsi = random_mps(badsites) + # Apply K to phi and check that error_contract is close to 0. + Kphi = contract(K, phi; method="naive", cutoff=1E-8) + @test error_contract(noprime(Kphi), K, phi) ≈ 0.0 atol = 1e-4 + @test error_contract(noprime(Kphi), phi, K) ≈ 0.0 atol = 1e-4 + + @test_throws DimensionMismatch contract(K, badpsi; method="naive", cutoff=1E-8) + @test_throws DimensionMismatch error_contract(phi, K, badpsi) + end + + @testset "contract" begin + phi = random_mps(sites) + K = random_mpo(sites) + @test maxlinkdim(K) == 1 + psi = random_mps(sites) + psi_out = contract(K, psi; maxdim=1) + @test inner(phi', psi_out) ≈ inner(phi', K, psi) + psi_out = contract(psi, K; maxdim=1) + @test inner(phi', psi_out) ≈ inner(phi', K, psi) + psi_out = psi * K + @test inner(phi', psi_out) ≈ inner(phi', K, psi) + @test_throws MethodError contract(K, psi; method="fakemethod") + + badsites = [Index(2, "Site") for n in 1:(N + 1)] + badpsi = random_mps(badsites) + @test_throws DimensionMismatch contract(K, badpsi) + + # make bigger random MPO... + for link_dim in 2:5 + mpo_tensors = ITensor[ITensor() for ii in 1:N] + mps_tensors = ITensor[ITensor() for ii in 1:N] + mps_tensors2 = ITensor[ITensor() for ii in 1:N] + mpo_link_inds = [Index(link_dim, "r$ii,Link") for ii in 1:(N - 1)] + mps_link_inds = [Index(link_dim, "r$ii,Link") for ii in 1:(N - 1)] + mpo_tensors[1] = random_itensor(mpo_link_inds[1], sites[1], sites[1]') + mps_tensors[1] = random_itensor(mps_link_inds[1], sites[1]) + mps_tensors2[1] = random_itensor(mps_link_inds[1], sites[1]) + for ii in 2:(N - 1) + mpo_tensors[ii] = random_itensor( + mpo_link_inds[ii], mpo_link_inds[ii - 1], sites[ii], sites[ii]' + ) + mps_tensors[ii] = random_itensor( + mps_link_inds[ii], mps_link_inds[ii - 1], sites[ii] + ) + mps_tensors2[ii] = random_itensor( + mps_link_inds[ii], mps_link_inds[ii - 1], sites[ii] + ) + end + mpo_tensors[N] = random_itensor(mpo_link_inds[N - 1], sites[N], sites[N]') + mps_tensors[N] = random_itensor(mps_link_inds[N - 1], sites[N]) + mps_tensors2[N] = random_itensor(mps_link_inds[N - 1], sites[N]) + K = MPO(mpo_tensors, 0, N + 1) + psi = MPS(mps_tensors, 0, N + 1) + phi = MPS(mps_tensors2, 0, N + 1) + orthogonalize!(psi, 1; maxdim=link_dim) + orthogonalize!(K, 1; maxdim=link_dim) + orthogonalize!(phi, 1; normalize=true, maxdim=link_dim) + psi_out = contract(deepcopy(K), deepcopy(psi); maxdim=10 * link_dim, cutoff=0.0) + @test inner(phi', psi_out) ≈ inner(phi', K, psi) + end + end + + @testset "add(::MPO, ::MPO)" begin + shsites = siteinds("S=1/2", N) + K = random_mpo(shsites) + L = random_mpo(shsites) + M = add(K, L) + @test length(M) == N + psi = random_mps(shsites) + k_psi = contract(K, psi; maxdim=1) + l_psi = contract(L, psi; maxdim=1) + @test inner(psi', k_psi + l_psi) ≈ ⋅(psi', M, psi) atol = 5e-3 + @test inner(psi', sum([k_psi, l_psi])) ≈ dot(psi', M, psi) atol = 5e-3 + for dim in 2:4 + shsites = siteinds("S=1/2", N) + K = basicRandomMPO(shsites; dim=dim) + L = basicRandomMPO(shsites; dim=dim) + M = K + L + @test length(M) == N + psi = random_mps(shsites) + k_psi = contract(K, psi) + l_psi = contract(L, psi) + @test inner(psi', k_psi + l_psi) ≈ dot(psi', M, psi) atol = 5e-3 + @test inner(psi', sum([k_psi, l_psi])) ≈ inner(psi', M, psi) atol = 5e-3 + psi = random_mps(shsites) + M = add(K, L; cutoff=1E-9) + k_psi = contract(K, psi) + l_psi = contract(L, psi) + @test inner(psi', k_psi + l_psi) ≈ inner(psi', M, psi) atol = 5e-3 + end + end + + @testset "+(::MPO, ::MPO)" begin + conserve_qns = true + s = siteinds("S=1/2", N; conserve_qns=conserve_qns) + + ops = n -> isodd(n) ? "Sz" : "Id" + H₁ = MPO(s, ops) + H₂ = MPO(s, ops) + + H = H₁ + H₂ + + @test inner(H, H) ≈ inner_add(H₁, H₂) + @test maxlinkdim(H) ≤ maxlinkdim(H₁) + maxlinkdim(H₂) + + α₁ = 2.2 + α₂ = 3.4 + 1.2im + + H = α₁ * H₁ + H₂ + + @test inner(H, H) ≈ inner_add((α₁, H₁), H₂) + @test maxlinkdim(H) ≤ maxlinkdim(H₁) + maxlinkdim(H₂) + + H = H₁ - H₂ + + @test inner(H, H) ≈ inner_add(H₁, (-1, H₂)) + @test maxlinkdim(H) ≤ maxlinkdim(H₁) + maxlinkdim(H₂) + + H = α₁ * H₁ - α₂ * H₂ + + @test inner(H, H) ≈ inner_add((α₁, H₁), (-α₂, H₂)) + @test maxlinkdim(H) ≤ maxlinkdim(H₁) + maxlinkdim(H₂) + end + + @testset "contract(::MPO, ::MPO)" begin + psi = random_mps(sites) + K = random_mpo(sites) + L = random_mpo(sites) + @test maxlinkdim(K) == 1 + @test maxlinkdim(L) == 1 + KL = contract(prime(K), L; maxdim=1) + psi_kl_out = contract(prime(K), contract(L, psi; maxdim=1); maxdim=1) + @test inner(psi'', KL, psi) ≈ inner(psi'', psi_kl_out) atol = 5e-3 + + # where both K and L have differently labelled sites + othersitesk = [Index(2, "Site,aaa") for n in 1:N] + othersitesl = [Index(2, "Site,bbb") for n in 1:N] + K = random_mpo(sites) + L = random_mpo(sites) + for ii in 1:N + replaceind!(K[ii], sites[ii]', othersitesk[ii]) + replaceind!(L[ii], sites[ii]', othersitesl[ii]) + end + KL = contract(K, L; maxdim=1) + psik = random_mps(othersitesk) + psil = random_mps(othersitesl) + psi_kl_out = contract(K, contract(L, psil; maxdim=1); maxdim=1) + @test inner(psik, KL, psil) ≈ inner(psik, psi_kl_out) atol = 5e-3 + + badsites = [Index(2, "Site") for n in 1:(N + 1)] + badL = random_mpo(badsites) + @test_throws DimensionMismatch contract(K, badL) + end + + @testset "*(::MPO, ::MPO)" begin + psi = random_mps(sites) + K = random_mpo(sites) + L = random_mpo(sites) + @test maxlinkdim(K) == 1 + @test maxlinkdim(L) == 1 + KL = *(prime(K), L; maxdim=1) + psi_kl_out = *(prime(K), *(L, psi; maxdim=1); maxdim=1) + @test ⋅(psi'', KL, psi) ≈ dot(psi'', psi_kl_out) atol = 5e-3 + + @test_throws ErrorException K * L + @test_throws ErrorException contract(K, L) + + @test replaceprime(KL, 2 => 1) ≈ apply(K, L; maxdim=1) + @test replaceprime(KL, 2 => 1) ≈ K(L; maxdim=1) + + # where both K and L have differently labelled sites + othersitesk = [Index(2, "Site,aaa") for n in 1:N] + othersitesl = [Index(2, "Site,bbb") for n in 1:N] + K = random_mpo(sites) + L = random_mpo(sites) + for ii in 1:N + replaceind!(K[ii], sites[ii]', othersitesk[ii]) + replaceind!(L[ii], sites[ii]', othersitesl[ii]) + end + KL = *(K, L; maxdim=1) + psik = random_mps(othersitesk) + psil = random_mps(othersitesl) + psi_kl_out = *(K, *(L, psil; maxdim=1); maxdim=1) + @test dot(psik, KL, psil) ≈ psik ⋅ psi_kl_out atol = 5e-3 + + badsites = [Index(2, "Site") for n in 1:(N + 1)] + badL = random_mpo(badsites) + @test_throws DimensionMismatch K * badL + end + + @testset "Multi-arg apply(::MPO...)" begin + ρ1 = (x -> outer(x', x; maxdim=4))(random_mps(sites; linkdims=2)) + ρ2 = (x -> outer(x', x; maxdim=4))(random_mps(sites; linkdims=2)) + ρ3 = (x -> outer(x', x; maxdim=4))(random_mps(sites; linkdims=2)) + @test apply(ρ1, ρ2, ρ3; cutoff=1e-8) ≈ + apply(apply(ρ1, ρ2; cutoff=1e-8), ρ3; cutoff=1e-8) + end + + sites = siteinds("S=1/2", N) + O = MPO(sites, "Sz") + @test length(O) == N # just make sure this works + + @test_throws ArgumentError random_mpo(sites, 2) + @test isnothing(linkind(MPO(fill(ITensor(), N), 0, N + 1), 1)) + + @testset "movesites $N sites" for N in 1:7 + s0 = siteinds("S=1/2", N) + ψ0 = MPO(s0, "Id") + for perm in permutations(1:N) + s = s0[perm] + ψ = random_mpo(s) + ns′ = [findsite(ψ0, i) for i in s] + @test ns′ == perm + ψ′ = movesites(ψ, 1:N .=> ns′) + for n in 1:N + @test hassameinds(siteinds(ψ0, n), siteinds(ψ′, n)) + end + @test @set_warn_order 15 prod(ψ) ≈ prod(ψ′) + end + end + + @testset "Construct MPO from ITensor" begin + N = 5 + s = siteinds("S=1/2", N) + l = [Index(3, "left_$n") for n in 1:2] + r = [Index(3, "right_$n") for n in 1:2] + + sis = [[sₙ', sₙ] for sₙ in s] + + A = random_itensor(s..., prime.(s)...) + ψ = MPO(A, sis; orthocenter=4) + ls = linkinds(ψ) + @test hassameinds(ψ[1], (s[1], s[1]', ls[1])) + @test hassameinds(ψ[N], (s[N], s[N]', ls[N - 1])) + @test prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == 4 + @test maxlinkdim(ψ) == 16 + + A = random_itensor(s..., prime.(s)...) + ψ = MPO(A, s; orthocenter=4) + ls = linkinds(ψ) + @test hassameinds(ψ[1], (s[1], s[1]', ls[1])) + @test hassameinds(ψ[N], (s[N], s[N]', ls[N - 1])) + @test prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == 4 + @test maxlinkdim(ψ) == 16 + + ψ0 = MPO(s, "Id") + A = prod(ψ0) + ψ = MPO(A, sis; cutoff=1e-15, orthocenter=3) + ls = linkinds(ψ) + @test hassameinds(ψ[1], (s[1], s[1]', ls[1])) + @test hassameinds(ψ[N], (s[N], s[N]', ls[N - 1])) + @test prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == 3 + @test maxlinkdim(ψ) == 1 + + # Use matrix + @test_throws ErrorException MPO(s, [1/2 0; 0 1/2]) + @test MPO(s, _ -> [1/2 0; 0 1/2]) ≈ MPO(s, "Id") ./ 2 + + ψ0 = MPO(s, "Id") + A = prod(ψ0) + ψ = MPO(A, s; cutoff=1e-15, orthocenter=3) + ls = linkinds(ψ) + @test hassameinds(ψ[1], (s[1], s[1]', ls[1])) + @test hassameinds(ψ[N], (s[N], s[N]', ls[N - 1])) + @test prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == 3 + @test maxlinkdim(ψ) == 1 + + A = random_itensor(s..., prime.(s)..., l[1], r[1]) + ψ = MPO(A, sis; leftinds=l[1]) + ls = linkinds(ψ) + @test hassameinds(ψ[1], (l[1], s[1], s[1]', ls[1])) + @test hassameinds(ψ[N], (r[1], s[N], s[N]', ls[N - 1])) + @test prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == N + @test maxlinkdim(ψ) == 48 + + A = random_itensor(s..., prime.(s)..., l[1], r[1]) + ψ = MPO(A, s; leftinds=l[1]) + ls = linkinds(ψ) + @test hassameinds(ψ[1], (l[1], s[1], s[1]', ls[1])) + @test hassameinds(ψ[N], (r[1], s[N], s[N]', ls[N - 1])) + @test prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == N + @test maxlinkdim(ψ) == 48 + + A = random_itensor(s..., prime.(s)..., l..., r...) + ψ = MPO(A, sis; leftinds=l, orthocenter=2) + ls = linkinds(ψ) + @test hassameinds(ψ[1], (l..., s[1], s[1]', ls[1])) + @test hassameinds(ψ[N], (r..., s[N], s[N]', ls[N - 1])) + @test @set_warn_order 15 prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == 2 + @test maxlinkdim(ψ) == 144 + + A = random_itensor(s..., prime.(s)..., l..., r...) + ψ = MPO(A, s; leftinds=l, orthocenter=2) + ls = linkinds(ψ) + @test hassameinds(ψ[1], (l..., s[1], s[1]', ls[1])) + @test hassameinds(ψ[N], (r..., s[N], s[N]', ls[N - 1])) + @test @set_warn_order 15 prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == 2 + @test maxlinkdim(ψ) == 144 + end + + @testset "Set range of MPO tensors" begin + N = 5 + s = siteinds("S=1/2", N) + ψ0 = random_mpo(s) + + ψ = orthogonalize(ψ0, 2) + A = prod(ITensors.data(ψ)[2:(N - 1)]) + randn!(A) + ϕ = MPO(A, s[2:(N - 1)]; orthocenter=1) + ψ[2:(N - 1)] = ϕ + @test prod(ψ) ≈ ψ[1] * A * ψ[N] + @test maxlinkdim(ψ) == 4 + @test ITensorMPS.orthocenter(ψ) == 2 + + ψ = orthogonalize(ψ0, 1) + A = prod(ITensors.data(ψ)[2:(N - 1)]) + randn!(A) + @test_throws AssertionError ψ[2:(N - 1)] = A + + ψ = orthogonalize(ψ0, 2) + A = prod(ITensors.data(ψ)[2:(N - 1)]) + randn!(A) + ψ[2:(N - 1), orthocenter=3] = A + @test prod(ψ) ≈ ψ[1] * A * ψ[N] + @test maxlinkdim(ψ) == 4 + @test ITensorMPS.orthocenter(ψ) == 3 + end + + @testset "swapbondsites MPO" begin + N = 5 + sites = siteinds("S=1/2", N) + ψ0 = random_mpo(sites) + + # TODO: implement this? + #ψ = replacebond(ψ0, 3, ψ0[3] * ψ0[4]; + # swapsites = true, + # cutoff = 1e-15) + #@test siteind(ψ, 1) == siteind(ψ0, 1) + #@test siteind(ψ, 2) == siteind(ψ0, 2) + #@test siteind(ψ, 4) == siteind(ψ0, 3) + #@test siteind(ψ, 3) == siteind(ψ0, 4) + #@test siteind(ψ, 5) == siteind(ψ0, 5) + #@test prod(ψ) ≈ prod(ψ0) + #@test maxlinkdim(ψ) == 1 + + ψ = swapbondsites(ψ0, 4; cutoff=1e-15) + @test siteind(ψ, 1) == siteind(ψ0, 1) + @test siteind(ψ, 2) == siteind(ψ0, 2) + @test siteind(ψ, 3) == siteind(ψ0, 3) + @test siteind(ψ, 5) == siteind(ψ0, 4) + @test siteind(ψ, 4) == siteind(ψ0, 5) + @test prod(ψ) ≈ prod(ψ0) + @test maxlinkdim(ψ) == 1 + end + + @testset "MPO(::MPS)" begin + i = Index(QN(0, 2) => 1, QN(1, 2) => 1; tags="i") + j = settags(i, "j") + A = random_itensor(ComplexF64, i, j) + M = A' * dag(A) + ψ = MPS(A, [i, j]) + @test prod(ψ) ≈ A + ρ = outer(ψ', ψ) + @test prod(ρ) ≈ M + ρ = projector(ψ; normalize=false) + @test prod(ρ) ≈ M + # Deprecated syntax + ρ = @test_deprecated MPO(ψ) + @test prod(ρ) ≈ M + end + + @testset "outer(::MPS, ::MPS) and projector(::MPS)" begin + N = 40 + s = siteinds("S=1/2", N; conserve_qns=true) + state(n) = isodd(n) ? "Up" : "Dn" + χψ = 3 + ψ = random_mps(ComplexF64, s, state; linkdims=χψ) + χϕ = 4 + ϕ = random_mps(ComplexF64, s, state; linkdims=χϕ) + + ψ[only(ortho_lims(ψ))] *= 2 + + Pψ = projector(ψ; normalize=false, cutoff=1e-8) + Pψᴴ = swapprime(dag(Pψ), 0 => 1) + @test maxlinkdim(Pψ) == χψ^2 + @test sqrt(inner(Pψ, Pψ) + inner(ψ, ψ)^2 - inner(ψ', Pψ, ψ) - inner(ψ', Pψᴴ, ψ)) / + abs(inner(ψ, ψ)) ≈ 0 atol = 1e-5 * N + + normψ = norm(ψ) + Pψ = projector(ψ; cutoff=1e-8) + Pψᴴ = swapprime(dag(Pψ), 0 => 1) + @test maxlinkdim(Pψ) == χψ^2 + @test sqrt( + inner(Pψ, Pψ) * normψ^4 + inner(ψ, ψ)^2 - inner(ψ', Pψ, ψ) * normψ^2 - + inner(ψ', Pψᴴ, ψ) * normψ^2, + ) / abs(inner(ψ, ψ)) ≈ 0 atol = 1e-5 * N + + ψϕ = outer(ψ', ϕ; cutoff=1e-8) + ϕψ = swapprime(dag(ψϕ), 0 => 1) + @test maxlinkdim(ψϕ) == χψ * χϕ + @test sqrt( + inner(ψϕ, ψϕ) + inner(ψ, ψ) * inner(ϕ, ϕ) - inner(ψ', ψϕ, ϕ) - inner(ϕ', ϕψ, ψ) + ) / sqrt(inner(ψ, ψ) * inner(ϕ, ϕ)) ≈ 0 atol = 1e-5 * N + end + + @testset "tr(::MPO)" begin + N = 5 + s = siteinds("S=1/2", N) + H = MPO(s, "Id") + d = dim(s[1]) + @test tr(H) ≈ d^N + end + + @testset "tr(::MPO) multiple site indices" begin + N = 6 + s = siteinds("S=1/2", N) + H = MPO(s, "Id") + H2 = MPO([H[j] * H[j + 1] for j in 1:2:(N - 1)]) + d = dim(s[1]) + @test tr(H) ≈ d^N + @test tr(H2) ≈ d^N + end + + @testset "check_hascommonsiteinds checks in DMRG, inner, dot" begin + N = 4 + s1 = siteinds("S=1/2", N) + s2 = siteinds("S=1/2", N) + psi1 = random_mps(s1) + psi2 = random_mps(s2) + H1 = MPO(OpSum() + ("Id", 1), s1) + H2 = MPO(OpSum() + ("Id", 1), s2) + + @test_throws ErrorException inner(psi1, H2, psi1) + @test_throws ErrorException inner(psi1, H2, psi2; make_inds_match=false) + + sweeps = Sweeps(1) + maxdim!(sweeps, 10) + + @test_throws ErrorException dmrg(H2, psi1, sweeps) + @test_throws ErrorException dmrg(H1, [psi2], psi1, sweeps) + @test_throws ErrorException dmrg([H1, H2], psi1, sweeps) + end + + @testset "unsupported kwarg in dot, logdot" begin + N = 6 + sites = [Index(2, "Site,n=$n") for n in 1:N] + K = random_mpo(sites) + L = random_mpo(sites) + @test_throws ErrorException dot(K, L, make_inds_match=true) + @test_throws ErrorException logdot(K, L, make_inds_match=true) + end + + @testset "MPO*MPO contraction with multiple site indices" begin + N = 8 + s = siteinds("S=1/2", N) + a = OpSum() + for j in 1:(N - 1) + a .+= 0.5, "S+", j, "S-", j + 1 + a .+= 0.5, "S-", j, "S+", j + 1 + a .+= "Sz", j, "Sz", j + 1 + end + H = MPO(a, s) + # Create MPO/MPS with pairs of sites merged + H2 = MPO([H[b] * H[b + 1] for b in 1:2:N]) + @test @disable_warn_order prod(H) ≈ prod(H2) + HH = H' * H + H2H2 = H2' * H2 + @test @disable_warn_order prod(HH) ≈ prod(H2H2) + end + + @testset "MPO*MPO contraction with multiple and combined site indices" begin + N = 8 + s = siteinds("S=1/2", N) + a = OpSum() + for j in 1:(N - 1) + a .+= 0.5, "S+", j, "S-", j + 1 + a .+= 0.5, "S-", j, "S+", j + 1 + a .+= "Sz", j, "Sz", j + 1 + end + H = MPO(a, s) + HH = setprime(H' * H, 1; plev=2) + + # Create MPO/MPS with pairs of sites merged + H2 = MPO([H[b] * H[b + 1] for b in 1:2:N]) + @test @disable_warn_order prod(H) ≈ prod(H2) + s = siteinds(H2; plev=1) + C = combiner.(s; tags="X") + H2 .*= C + H2H2 = prime(H2; tags=!ts"X") * dag(H2) + @test @disable_warn_order prod(HH) ≈ prod(H2H2) + end + + @testset "Bond dimensions of MPO*MPS in default contract" begin + N = 8 + chi1 = 6 + chi2 = 2 + s = siteinds(2, N) + + A = begin + l = [Index(chi1, "n=$n,Link") for n in 1:N] + M = MPO(N) + M[1] = random_itensor(dag(s[1]), l[1], s'[1]) + for n in 2:(N - 1) + M[n] = random_itensor(dag(s[n]), dag(l[n - 1]), l[n], s'[n]) + end + M[N] = random_itensor(dag(s[N]), dag(l[N - 1]), s'[N]) + nrm = inner(M, M) + for n in 1:N + M[n] ./= (nrm)^(1 / (2N)) + end + truncate!(M; cutoff=1E-10) + M + end + + psi = random_mps(s; linkdims=chi2) + + Apsi = contract(A, psi) + + dims = linkdims(Apsi) + for d in dims + @test d <= chi1 * chi2 + end + + @test apply(A, psi) ≈ noprime(Apsi) + @test ITensors.materialize(Apply(A, psi)) ≈ noprime(Apsi) + @test A(psi) ≈ noprime(Apsi) + @test inner(noprime(Apsi), Apply(A, psi)) ≈ inner(Apsi, Apsi) + end + + @testset "Other MPO contract algorithms" begin + # Regression test - ensure that output of "naive" algorithm is an + # MPO not an MPS + N = 8 + s = siteinds(2, N) + A = random_mpo(s) + B = random_mpo(s) + C = apply(A, B; alg="naive") + @test C isa MPO + end + + @testset "MPO with no link indices" for conserve_qns in [false, true] + s = siteinds("S=1/2", 4; conserve_qns) + H = MPO([op("Id", sn) for sn in s]) + @test linkinds(H) == fill(nothing, length(s) - 1) + @test norm(H) == √(2^length(s)) + + Hortho = orthogonalize(H, 1) + @test Hortho ≈ H + @test linkdims(Hortho) == fill(1, length(s) - 1) + + Htrunc = truncate(H; cutoff=1e-8) + @test Htrunc ≈ H + @test linkdims(Htrunc) == fill(1, length(s) - 1) + + H² = apply(H, H; cutoff=1e-8) + H̃² = MPO([apply(H[n], H[n]) for n in 1:length(s)]) + @test linkdims(H²) == fill(1, length(s) - 1) + @test H² ≈ H̃² + + e, ψ = dmrg(H, random_mps(s, n -> isodd(n) ? "↑" : "↓"); nsweeps=2, outputlevel=0) + @test e ≈ 1 + end + + @testset "consistent precision of apply" for T in + (Float32, Float64, ComplexF32, ComplexF64) + sites = siteinds("S=1/2", 4) + A = randn(T) * convert_leaf_eltype(T, random_mpo(sites)) + B = randn(T) * convert_leaf_eltype(T, random_mpo(sites)) + @test scalartype(apply(A, B)) == T + end + @testset "sample" begin + N = 6 + sites = [Index(2, "Site,n=$n") for n in 1:N] + seed = 623 + rng = StableRNG(seed) + K = random_mps(rng, sites) + L = MPO(K) + result = sample(rng, L) + @test result ≈ [1, 1, 2, 1, 1, 1] + end + + @testset "MPO+MPO sum (directsum)" begin + N = 3 + conserve_qns = true + s = siteinds("S=1/2", N; conserve_qns=conserve_qns) + + ops = n -> isodd(n) ? "Sz" : "Id" + H₁ = MPO(s, ops) + H₂ = MPO(s, ops) + + H = +(H₁, H₂; alg="directsum") + H_ref = +(H₁, H₂; alg="densitymatrix") + + @test typeof(H) == typeof(H₁) + @test H_ref ≈ H + @test inner(H, H) ≈ inner_add(H₁, H₂) + @test maxlinkdim(H) ≤ maxlinkdim(H₁) + maxlinkdim(H₂) + + α₁ = 2.2 + α₂ = 3.4 + 1.2im + + H = +(α₁ * H₁, H₂; alg="directsum") + + @test typeof(H) == typeof(H₁) + @test inner(H, H) ≈ inner_add((α₁, H₁), H₂) + @test maxlinkdim(H) ≤ maxlinkdim(H₁) + maxlinkdim(H₂) + + H = +(H₁, -H₂; alg="directsum") + + @test typeof(H) == typeof(H₁) + @test inner(H, H) ≈ inner_add(H₁, (-1, H₂)) + @test maxlinkdim(H) ≤ maxlinkdim(H₁) + maxlinkdim(H₂) + + H = +(α₁ * H₁, -α₂ * H₂; alg="directsum") + + @test typeof(H) == typeof(H₁) + @test inner(H, H) ≈ inner_add((α₁, H₁), (-α₂, H₂)) + @test maxlinkdim(H) ≤ maxlinkdim(H₁) + maxlinkdim(H₂) + end +end +end diff --git a/test/base/test_mps.jl b/test/base/test_mps.jl new file mode 100644 index 0000000..65eed0e --- /dev/null +++ b/test/base/test_mps.jl @@ -0,0 +1,2021 @@ +@eval module $(gensym()) +using Combinatorics +using ITensorMPS +using ITensors +using LinearAlgebra: diag +using Random +using Test + +Random.seed!(1234) + +include(joinpath(@__DIR__, "utils", "util.jl")) + +@testset "MPS Basics" begin + sites = [Index(2, "Site") for n in 1:10] + psi = MPS(sites) + @test length(psi) == length(psi) + @test length(MPS()) == 0 + @test linkdims(psi) == fill(1, length(psi) - 1) + @test isnothing(flux(psi)) + + psi = MPS(sites; linkdims=3) + @test length(psi) == length(psi) + @test length(MPS()) == 0 + @test linkdims(psi) == fill(3, length(psi) - 1) + @test isnothing(flux(psi)) + + str = split(sprint(show, psi), '\n') + @test endswith(str[1], "MPS") + @test length(str) == length(psi) + 2 + + @test siteind(psi, 2) == sites[2] + @test findfirstsiteind(psi, sites[2]) == 2 + @test findfirstsiteind(psi, sites[4]) == 4 + @test findfirstsiteinds(psi, IndexSet(sites[5])) == 5 + @test hasind(psi[3], linkind(psi, 2)) + @test hasind(psi[3], linkind(psi, 3)) + + @test isnothing(linkind(psi, length(psi))) + @test isnothing(linkind(psi, length(psi) + 1)) + @test isnothing(linkind(psi, 0)) + @test isnothing(linkind(psi, -1)) + @test linkind(psi, 3) == commonind(psi[3], psi[4]) + + psi[1] = ITensor(sites[1]) + @test hasind(psi[1], sites[1]) + + @testset "N=1 MPS" begin + sites1 = [Index(2, "Site,n=1")] + psi = MPS(sites1) + @test length(psi) == 1 + @test siteind(psi, 1) == sites1[1] + @test siteinds(psi)[1] == sites1[1] + end + + @testset "Missing links" begin + psi = MPS([random_itensor(sites[i]) for i in 1:10]) + @test isnothing(linkind(psi, 1)) + @test isnothing(linkind(psi, 5)) + @test isnothing(linkind(psi, length(psi))) + @test maxlinkdim(psi) == 1 + @test psi ⋅ psi ≈ *(dag(psi)..., psi...)[] + end + + @testset "MPS" begin + @testset "vector of string input" begin + sites = siteinds("S=1/2", 10) + state = fill("", length(sites)) + for j in 1:length(sites) + state[j] = isodd(j) ? "Up" : "Dn" + end + psi = MPS(sites, state) + for j in 1:length(psi) + sign = isodd(j) ? +1.0 : -1.0 + @test (psi[j] * op(sites, "Sz", j) * dag(prime(psi[j], "Site")))[] ≈ sign / 2 + end + psi = MPS(sites, state) + for j in 1:length(psi) + sign = isodd(j) ? +1.0 : -1.0 + @test (psi[j] * op(sites, "Sz", j) * dag(prime(psi[j], "Site")))[] ≈ sign / 2 + end + @test_throws DimensionMismatch MPS(sites, fill("", length(psi) - 1)) + @test_throws DimensionMismatch MPS(sites, fill("", length(psi) - 1)) + end + + @testset "String input" begin + sites = siteinds("S=1/2", 10) + psi = MPS(sites, "Dn") + for j in 1:length(psi) + sign = -1.0 + @test (psi[j] * op(sites, "Sz", j) * dag(prime(psi[j], "Site")))[] ≈ sign / 2 + end + psi = MPS(sites, "Dn") + for j in 1:length(psi) + sign = -1.0 + @test (psi[j] * op(sites, "Sz", j) * dag(prime(psi[j], "Site")))[] ≈ sign / 2 + end + + psi = MPS(sites, "X+") + for j in 1:length(psi) + @test (psi[j] * op(sites, "X", j) * dag(prime(psi[j], "Site")))[] ≈ 1.0 + end + end + + @testset "Int input" begin + sites = siteinds("S=1/2", 10) + psi = MPS(sites, 2) + for j in 1:length(psi) + sign = -1.0 + @test (psi[j] * op(sites, "Sz", j) * dag(prime(psi[j], "Site")))[] ≈ sign / 2 + end + psi = MPS(sites, 2) + for j in 1:length(psi) + sign = -1.0 + @test (psi[j] * op(sites, "Sz", j) * dag(prime(psi[j], "Site")))[] ≈ sign / 2 + end + end + + @testset "vector of int input" begin + sites = siteinds("S=1/2", 10) + state = fill(0, length(sites)) + for j in 1:length(sites) + state[j] = isodd(j) ? 1 : 2 + end + psi = MPS(sites, state) + for j in 1:length(psi) + sign = isodd(j) ? +1.0 : -1.0 + @test (psi[j] * op(sites, "Sz", j) * dag(prime(psi[j], "Site")))[] ≈ sign / 2 + end + psi = MPS(sites, state) + for j in 1:length(psi) + sign = isodd(j) ? +1.0 : -1.0 + @test (psi[j] * op(sites, "Sz", j) * dag(prime(psi[j], "Site")))[] ≈ sign / 2 + end + end + + @testset "vector of ivals input" begin + sites = siteinds("S=1/2", 10) + states = fill(0, length(sites)) + for j in 1:length(sites) + states[j] = isodd(j) ? 1 : 2 + end + ivals = [sites[n] => states[n] for n in 1:length(sites)] + psi = MPS(ivals) + for j in 1:length(psi) + sign = isodd(j) ? +1.0 : -1.0 + @test (psi[j] * op(sites, "Sz", j) * dag(prime(psi[j], "Site")))[] ≈ sign / 2 + end + psi = MPS(ivals) + for j in 1:length(psi) + sign = isodd(j) ? +1.0 : -1.0 + @test (psi[j] * op(sites, "Sz", j) * dag(prime(psi[j], "Site")))[] ≈ sign / 2 + end + + @testset "ComplexF64 eltype" begin + sites = siteinds("S=1/2", 10) + psi = MPS(ComplexF64, sites, fill(1, length(sites))) + for j in 1:length(psi) + @test eltype(psi[j]) == ComplexF64 + end + psi = MPS(ComplexF64, sites, fill(1, length(psi))) + for j in 1:length(psi) + @test eltype(psi[j]) == ComplexF64 + end + @test eltype(psi) == ITensor + @test ITensorMPS.promote_itensor_eltype(psi) == ComplexF64 + end + end + + @testset "N=1 case" begin + site = Index(2, "Site,n=1") + psi = MPS([site], [1]) + @test psi[1][1] ≈ 1.0 + @test psi[1][2] ≈ 0.0 + psi = MPS([site], [1]) + @test psi[1][1] ≈ 1.0 + @test psi[1][2] ≈ 0.0 + psi = MPS([site], [2]) + @test psi[1][1] ≈ 0.0 + @test psi[1][2] ≈ 1.0 + psi = MPS([site], [2]) + @test psi[1][1] ≈ 0.0 + @test psi[1][2] ≈ 1.0 + end + end + + @testset "random_mps with chi==1" begin + phi = random_mps(sites) + phic = random_mps(ComplexF64, sites) + + @test maxlinkdim(phi) == 1 + @test maxlinkdim(phic) == 1 + + @test hasind(phi[1], sites[1]) + @test norm(phi[1]) ≈ 1.0 + @test norm(phic[1]) ≈ 1.0 + + @test hasind(phi[4], sites[4]) + @test norm(phi[4]) ≈ 1.0 + @test norm(phic[4]) ≈ 1.0 + end + + @testset "random_mps with chi>1" for linkdims in [1, 4] + phi = random_mps(Float32, sites; linkdims) + @test LinearAlgebra.promote_leaf_eltypes(phi) === Float32 + @test all(x -> eltype(x) === Float32, phi) + @test maxlinkdim(phi) == linkdims + phic = random_mps(ComplexF32, sites; linkdims) + @test LinearAlgebra.promote_leaf_eltypes(phic) === ComplexF32 + @test maxlinkdim(phic) == linkdims + @test all(x -> eltype(x) === ComplexF32, phic) + end + + @testset "random_mps with nonuniform dimensions" begin + _linkdims = [2, 3, 4, 2, 4, 3, 2, 2, 2] + phi = random_mps(sites; linkdims=_linkdims) + @test linkdims(phi) == _linkdims + end + + @testset "QN random_mps" begin + s = siteinds("S=1/2", 5; conserve_qns=true) + ψ = random_mps(s, n -> isodd(n) ? "↑" : "↓"; linkdims=2) + @test linkdims(ψ) == [2, 2, 2, 2] + ψ = random_mps(s, n -> isodd(n) ? "↑" : "↓"; linkdims=[2, 3, 2, 2]) + @test linkdims(ψ) == [2, 3, 2, 2] + end + + @testset "inner different MPS" begin + phi = random_mps(sites) + psi = random_mps(sites) + phipsi = dag(phi[1]) * psi[1] + for j in 2:length(psi) + phipsi *= dag(phi[j]) * psi[j] + end + @test phipsi[] ≈ inner(phi, psi) + + badsites = [Index(2) for n in 1:(length(psi) + 1)] + badpsi = random_mps(badsites) + @test_throws DimensionMismatch inner(phi, badpsi) + end + + @testset "loginner" begin + n = 4 + c = 2 + + s = siteinds("S=1/2", n) + ψ = c .* random_mps(s; linkdims=4) + @test exp(loginner(ψ, ψ)) ≈ c^(2n) + @test exp(loginner(ψ, -ψ)) ≈ -c^(2n) + + α = randn(ComplexF64) + @test exp(loginner(ψ, α * ψ)) ≈ α * c^(2n) + end + + @testset "broadcasting" begin + psi = random_mps(sites) + orthogonalize!(psi, 1) + @test ortho_lims(psi) == 1:1 + @test dim.(psi) == fill(2, length(psi)) + psi′ = prime.(psi) + @test ortho_lims(psi′) == 1:length(psi′) + @test ortho_lims(psi) == 1:1 + for n in 1:length(psi) + @test prime(psi[n]) == psi′[n] + end + psi_copy = copy(psi) + psi_copy .= addtags(psi_copy, "x") + @test ortho_lims(psi_copy) == 1:length(psi_copy) + @test ortho_lims(psi) == 1:1 + for n in 1:length(psi) + @test addtags(psi[n], "x") == psi_copy[n] + end + end + + @testset "replace_siteinds" begin + s = siteinds("S=1/2", 4) + x = MPS(s, j -> isodd(j) ? "↑" : "↓") + @test siteinds(x) == s + t = sim.(s) + y = replace_siteinds(x, t) + @test siteinds(y) == t + # Regression test for https://github.com/ITensor/ITensors.jl/issues/1439. + @test siteinds(x) == s + end + + @testset "copy and deepcopy" begin + s = siteinds("S=1/2", 3) + M1 = random_mps(s; linkdims=3) + @test norm(M1) ≈ 1 + + M2 = deepcopy(M1) + M2[1] .*= 2 # Modifies the tensor data + @test norm(M1) ≈ 1 + @test norm(M2) ≈ 2 + + M3 = copy(M1) + M3[1] *= 3 + @test norm(M1) ≈ 1 + @test norm(M3) ≈ 3 + + M4 = copy(M1) + M4[1] .*= 4 + @test norm(M1) ≈ 4 + @test norm(M4) ≈ 4 + end + + @testset "inner same MPS" begin + psi = random_mps(sites) + psidag = dag(psi) + #ITensors.prime_linkinds!(psidag) + psipsi = psidag[1] * psi[1] + for j in 2:length(psi) + psipsi *= psidag[j] * psi[j] + end + @test psipsi[] ≈ inner(psi, psi) + end + + @testset "norm MPS (eltype=$elt)" for elt in ( + Float32, Float64, Complex{Float32}, Complex{Float64} + ) + psi = random_mps(elt, sites; linkdims=10) + psidag = sim(linkinds, dag(psi)) + psi² = ITensor(1) + for j in 1:length(psi) + psi² *= psidag[j] * psi[j] + end + @test psi²[] ≈ psi ⋅ psi + @test sqrt(psi²[]) ≈ norm(psi) + + psi = random_mps(elt, sites; linkdims=10) + psi .*= 1:length(psi) + @test norm(psi) ≈ factorial(length(psi)) + @test norm(psi) isa real(elt) + + psi = random_mps(elt, sites; linkdims=10) + for j in 1:length(psi) + psi[j] .*= j + end + # This fails because it modifies the MPS ITensors + # directly, which ruins the orthogonality + @test norm(psi) ≉ factorial(length(psi)) + reset_ortho_lims!(psi) + @test norm(psi) ≈ factorial(length(psi)) + + psi = random_mps(elt, sites; linkdims=10) + + norm_psi = norm(psi) + @test norm_psi ≈ 1 + @test norm_psi isa real(elt) + @test isreal(norm_psi) + + lognorm_psi = lognorm(psi) + @test lognorm_psi isa real(elt) + @test lognorm_psi ≈ 0 atol = eps(real(elt)) * 10 + @test isreal(lognorm_psi) + + psi = psi .* 2 + + norm_psi = norm(psi) + @test norm_psi ≈ 2^length(psi) + @test isreal(norm_psi) + + lognorm_psi = lognorm(psi) + @test lognorm_psi ≈ log(2) * length(psi) + @test isreal(lognorm_psi) + end + + @testset "lognorm checking real tolerance error regression test" begin + # Test that lognorm doesn't throw an error when the norm isn't real + # up to a certain tolerance + Random.seed!(1234) + s = siteinds("S=1/2", 10) + ψ = random_mps(ComplexF64, s; linkdims=4) + reset_ortho_lims!(ψ) + @test exp(lognorm(ψ)) ≈ 1 + end + + @testset "normalize/normalize! MPS" begin + psi = random_mps(sites; linkdims=10) + + @test norm(psi) ≈ 1 + @test norm(normalize(psi)) ≈ 1 + + α = 3.5 + phi = α * psi + @test norm(phi) ≈ α + @test norm(normalize(phi)) ≈ 1 + @test norm(psi) ≈ 1 + @test inner(phi, psi) ≈ α + + normalize!(phi) + @test norm(phi) ≈ 1 + @test norm(normalize(phi)) ≈ 1 + @test norm(psi) ≈ 1 + @test inner(phi, psi) ≈ 1 + + # Zero norm + @test norm(0phi) == 0 + @test lognorm(0phi) == -Inf + + zero_phi = 0phi + lognorm_zero_phi = [] + normalize!(zero_phi; (lognorm!)=lognorm_zero_phi) + @test lognorm_zero_phi[1] == -Inf + @test norm(zero_phi) == 0 + @test norm(normalize(0phi)) == 0 + + # Large number of sites + psi = random_mps(siteinds("S=1/2", 1_000); linkdims=10) + + @test norm(psi) ≈ 1.0 + @test lognorm(psi) ≈ 0.0 atol = 1e-15 + + α = 2 + phi = α .* psi + + @test isnan(norm(phi)) + @test lognorm(phi) ≈ length(psi) * log(α) + + phi = normalize(phi) + + @test norm(phi) ≈ 1 + + # Test scaling only a subset of sites + psi = random_mps(siteinds("S=1/2", 10); linkdims=10) + + @test norm(psi) ≈ 1.0 + @test lognorm(psi) ≈ 0.0 atol = 1e-15 + + α = 2 + r = (length(psi) ÷ 2 - 1):(length(psi) ÷ 2 + 1) + phi = copy(psi) + for n in r + phi[n] = α * psi[n] + end + + @test norm(phi) ≈ α^length(r) + @test lognorm(phi) ≈ length(r) * log(α) + + phi = normalize(phi) + + @test norm(phi) ≈ 1 + + # Output the lognorm + α = 2 + psi = random_mps(siteinds("S=1/2", 30); linkdims=10) + psi = α .* psi + @test norm(psi) ≈ α^length(psi) + @test lognorm(psi) ≈ length(psi) * log(α) + lognorm_psi = Float64[] + phi = normalize(psi; (lognorm!)=lognorm_psi) + @test lognorm_psi[end] ≈ lognorm(psi) + @test norm(phi) ≈ 1 + @test lognorm(phi) ≈ 0 atol = 1e-14 + end + + @testset "lognorm MPS" begin + psi = random_mps(sites; linkdims=10) + for j in eachindex(psi) + psi[j] .*= j + end + psidag = sim(linkinds, dag(psi)) + psi² = ITensor(1) + for j in eachindex(psi) + psi² *= psidag[j] * psi[j] + end + @test psi²[] ≈ psi ⋅ psi + @test 0.5 * log(psi²[]) ≉ lognorm(psi) + @test lognorm(psi) ≉ log(factorial(length(psi))) + # Need to manually change the orthogonality + # limits back to 1:length(psi) + reset_ortho_lims!(psi) + @test 0.5 * log(psi²[]) ≈ lognorm(psi) + @test lognorm(psi) ≈ log(factorial(length(psi))) + end + + @testset "scaling MPS" begin + psi = random_mps(sites; linkdims=4) + twopsidag = 2.0 * dag(psi) + #ITensors.prime_linkinds!(twopsidag) + @test inner(twopsidag, psi) ≈ 2.0 * inner(psi, psi) + end + + @testset "flip sign of MPS" begin + psi = random_mps(sites) + minuspsidag = -dag(psi) + #ITensors.primelinkinds!(minuspsidag) + @test inner(minuspsidag, psi) ≈ -inner(psi, psi) + end + + @testset "add MPS" begin + psi = random_mps(sites) + phi = deepcopy(psi) + xi = add(psi, phi) + @test inner(xi, xi) ≈ 4.0 * inner(psi, psi) + # sum of many MPSs + Ks = [random_mps(sites) for i in 1:3] + K12 = add(Ks[1], Ks[2]) + K123 = add(K12, Ks[3]) + @test inner(sum(Ks), K123) ≈ inner(K123, K123) + # https://github.com/ITensor/ITensors.jl/issues/1000 + @test sum([psi]) ≈ psi + end + + @testset "+ MPS" begin + psi = random_mps(sites) + phi = deepcopy(psi) + xi = psi + phi + @test inner(xi, xi) ≈ 4.0 * inner(psi, psi) + # sum of many MPSs + Ks = [random_mps(sites) for i in 1:3] + K12 = Ks[1] + Ks[2] + K123 = K12 + Ks[3] + @test inner(sum(Ks), K123) ≈ inner(K123, K123) + + χ1 = 2 + χ2 = 3 + ψ1 = random_mps(sites; linkdims=χ1) + ψ2 = 0.0 * random_mps(sites; linkdims=χ2) + + ϕ1 = +(ψ1, ψ2; alg="densitymatrix", cutoff=nothing) + for j in 2:7 + @test linkdim(ϕ1, j) == χ1 + χ2 + end + @test inner(ϕ1, ψ1) + inner(ϕ1, ψ2) ≈ inner(ϕ1, ϕ1) + + ϕ2 = +(ψ1, ψ2; alg="directsum") + for j in 1:8 + @test linkdim(ϕ2, j) == χ1 + χ2 + end + @test inner(ϕ2, ψ1) + inner(ϕ2, ψ2) ≈ inner(ϕ2, ϕ2) + end + + @testset "+ MPS with coefficients" begin + Random.seed!(1234) + + conserve_qns = true + + s = siteinds("S=1/2", 20; conserve_qns=conserve_qns) + state = n -> isodd(n) ? "↑" : "↓" + + ψ₁ = random_mps(s, state; linkdims=4) + ψ₂ = random_mps(s, state; linkdims=4) + ψ₃ = random_mps(s, state; linkdims=4) + + ψ = ψ₁ + ψ₂ + + @test inner(ψ, ψ) ≈ inner_add(ψ₁, ψ₂) + @test maxlinkdim(ψ) ≤ maxlinkdim(ψ₁) + maxlinkdim(ψ₂) + + ψ = +(ψ₁, ψ₂; cutoff=0.0) + + @test_throws ErrorException ψ₁ + ψ₂' + + @test inner(ψ, ψ) ≈ inner_add(ψ₁, ψ₂) + @test maxlinkdim(ψ) ≤ maxlinkdim(ψ₁) + maxlinkdim(ψ₂) + + ψ = ψ₁ + (-ψ₂) + + @test inner(ψ, ψ) ≈ inner_add((1, ψ₁), (-1, ψ₂)) + @test maxlinkdim(ψ) ≤ maxlinkdim(ψ₁) + maxlinkdim(ψ₂) + + α₁ = 2.2 + α₂ = -4.1 + ψ = +(α₁ * ψ₁, α₂ * ψ₂; cutoff=1e-8) + + @test inner(ψ, ψ) ≈ inner_add((α₁, ψ₁), (α₂, ψ₂)) + @test maxlinkdim(ψ) ≤ maxlinkdim(ψ₁) + maxlinkdim(ψ₂) + + α₁ = 2 + 3im + α₂ = -4 + 1im + ψ = α₁ * ψ₁ + α₂ * ψ₂ + + @test inner(ψ, ψ) ≈ inner_add((α₁, ψ₁), (α₂, ψ₂)) + @test maxlinkdim(ψ) ≤ maxlinkdim(ψ₁) + maxlinkdim(ψ₂) + + α₁ = 2 + 3im + α₂ = -4 + 1im + ψ = α₁ * ψ₁ + α₂ * ψ₂ + ψ₃ + + @test inner(ψ, ψ) ≈ inner_add((α₁, ψ₁), (α₂, ψ₂), ψ₃) + @test maxlinkdim(ψ) ≤ maxlinkdim(ψ₁) + maxlinkdim(ψ₂) + maxlinkdim(ψ₃) + + ψ = ψ₁ - ψ₂ + + @test inner(ψ, ψ) ≈ inner_add(ψ₁, (-1, ψ₂)) + @test maxlinkdim(ψ) ≤ maxlinkdim(ψ₁) + maxlinkdim(ψ₂) + end + + sites = siteinds(2, 10) + psi = MPS(sites) + @test length(psi) == 10 # just make sure this works + @test length(siteinds(psi)) == length(psi) + + psi = random_mps(sites) + l0s = linkinds(psi) + orthogonalize!(psi, length(psi) - 1) + ls = linkinds(psi) + for (l0, l) in zip(l0s, ls) + @test tags(l0) == tags(l) + end + @test ITensorMPS.leftlim(psi) == length(psi) - 2 + @test ITensorMPS.rightlim(psi) == length(psi) + orthogonalize!(psi, 2) + @test ITensorMPS.leftlim(psi) == 1 + @test ITensorMPS.rightlim(psi) == 3 + psi = random_mps(sites) + ITensorMPS.setrightlim!(psi, length(psi) + 1) # do this to test qr + # from rightmost tensor + orthogonalize!(psi, div(length(psi), 2)) + @test ITensorMPS.leftlim(psi) == div(length(psi), 2) - 1 + @test ITensorMPS.rightlim(psi) == div(length(psi), 2) + 1 + + @test isnothing(linkind(MPS(fill(ITensor(), length(psi)), 0, length(psi) + 1), 1)) + + @testset "replacebond!" begin + # make sure factorization preserves the bond index tags + psi = random_mps(sites) + phi = psi[1] * psi[2] + bondindtags = tags(linkind(psi, 1)) + replacebond!(psi, 1, phi) + @test tags(linkind(psi, 1)) == bondindtags + + # check that replacebond! updates llim and rlim properly + orthogonalize!(psi, 5) + phi = psi[5] * psi[6] + replacebond!(psi, 5, phi; ortho="left") + @test ITensorMPS.leftlim(psi) == 5 + @test ITensorMPS.rightlim(psi) == 7 + + phi = psi[5] * psi[6] + replacebond!(psi, 5, phi; ortho="right") + @test ITensorMPS.leftlim(psi) == 4 + @test ITensorMPS.rightlim(psi) == 6 + + ITensorMPS.setleftlim!(psi, 3) + ITensorMPS.setrightlim!(psi, 7) + phi = psi[5] * psi[6] + replacebond!(psi, 5, phi; ortho="left") + @test ITensorMPS.leftlim(psi) == 3 + @test ITensorMPS.rightlim(psi) == 7 + + # check that replacebond! runs with svd kwargs + psi = random_mps(sites) + phi = psi[1] * psi[2] + replacebond!(psi, 1, phi; ortho="left", which_decomp="svd", use_relative_cutoff=true) + phi = psi[5] * psi[6] + replacebond!( + psi, + 5, + phi; + ortho="right", + which_decomp="svd", + use_absolute_cutoff=true, + min_blockdim=2, + ) + end +end + +@testset "orthogonalize! with QNs" begin + sites = siteinds("S=1/2", 8; conserve_qns=true) + init_state = [isodd(n) ? "Up" : "Dn" for n in 1:length(sites)] + psi0 = MPS(sites, init_state) + orthogonalize!(psi0, 4) + @test ITensorMPS.leftlim(psi0) == 3 + @test ITensorMPS.rightlim(psi0) == 5 +end + +# Helper function for making MPS +function basicRandomMPS(N::Int; dim=4) + sites = [Index(2, "Site") for n in 1:N] + M = MPS(sites) + links = [Index(dim, "n=$(n-1),Link") for n in 1:(N + 1)] + for n in 1:N + M[n] = random_itensor(links[n], sites[n], links[n + 1]) + end + M[1] *= delta(links[1]) + M[N] *= delta(links[N + 1]) + M[1] /= sqrt(inner(M, M)) + return M +end + +function test_correlation_matrix(psi::MPS, ops::Vector{Tuple{String,String}}; kwargs...) + N = length(psi) + s = siteinds(psi) + for op in ops + Cpm = correlation_matrix(psi, op[1], op[2]; kwargs...) + # Check using OpSum: + Copsum = 0.0 * Cpm + for i in 1:N, j in 1:N + a = OpSum() + a += op[1], i, op[2], j + Copsum[i, j] = inner(psi', MPO(a, s), psi) + end + @test Cpm ≈ Copsum rtol = 1E-11 + + PM = expect(psi, op[1] * " * " * op[2]) + @test norm(PM - diag(Cpm)) < 1E-8 + end +end + +@testset "MPS gauging and truncation" begin + @testset "orthogonalize! method" begin + c = 12 + M = basicRandomMPS(30) + orthogonalize!(M, c) + + @test ITensorMPS.leftlim(M) == c - 1 + @test ITensorMPS.rightlim(M) == c + 1 + + # Test for left-orthogonality + L = M[1] * prime(M[1], "Link") + l = linkind(M, 1) + @test norm(L - delta(l, l')) < 1E-12 + for j in 2:(c - 1) + L = L * M[j] * prime(M[j], "Link") + l = linkind(M, j) + @test norm(L - delta(l, l')) < 1E-12 + end + + # Test for right-orthogonality + R = M[length(M)] * prime(M[length(M)], "Link") + r = linkind(M, length(M) - 1) + @test norm(R - delta(r, r')) < 1E-12 + for j in reverse((c + 1):(length(M) - 1)) + R = R * M[j] * prime(M[j], "Link") + r = linkind(M, j - 1) + @test norm(R - delta(r, r')) < 1E-12 + end + + @test norm(M[c]) ≈ 1.0 + end + + @testset "truncate! method" begin + M = basicRandomMPS(10; dim=10) + M0 = copy(M) + truncate!(M; maxdim=5) + + @test ITensorMPS.rightlim(M) == 2 + + # Test for right-orthogonality + R = M[length(M)] * prime(M[length(M)], "Link") + r = linkind(M, length(M) - 1) + @test norm(R - delta(r, r')) < 1E-12 + for j in reverse(2:(length(M) - 1)) + R = R * M[j] * prime(M[j], "Link") + r = linkind(M, j - 1) + @test norm(R - delta(r, r')) < 1E-12 + end + + @test inner(M, M0) > 0.1 + end + + @testset "truncate! with site_range" begin + M = basicRandomMPS(10; dim=10) + truncate!(M; site_range=3:7, maxdim=2) + @test linkdims(M) == [2, 4, 2, 2, 2, 2, 8, 4, 2] + end +end + +@testset "Other MPS methods" begin + @testset "sample! method" begin + sites = [Index(3, "Site,n=$n") for n in 1:10] + psi = random_mps(sites; linkdims=3) + nrm2 = inner(psi, psi) + psi[1] *= (1.0 / sqrt(nrm2)) + + s = sample!(psi) + + @test length(s) == length(psi) + for n in 1:length(psi) + @test 1 <= s[n] <= 3 + end + + # Throws becase not orthogonalized to site 1: + orthogonalize!(psi, 3) + @test_throws ErrorException sample(psi) + + # Throws because not normalized + orthogonalize!(psi, 1) + psi[1] *= (5.0 / norm(psi[1])) + @test_throws ErrorException sample(psi) + + # Works when ortho & normalized: + orthogonalize!(psi, 1) + psi[1] *= (1.0 / norm(psi[1])) + s = sample(psi) + @test length(s) == length(psi) + end + + @testset "random_mps with chi > 1" begin + chi = 8 + sites = siteinds(2, 20) + M = random_mps(sites; linkdims=chi) + + @test ITensorMPS.leftlim(M) == 0 + @test ITensorMPS.rightlim(M) == 2 + + @test norm(M[1]) ≈ 1.0 + + @test maxlinkdim(M) == chi + + # Test for right-orthogonality + R = M[length(M)] * prime(M[length(M)], "Link") + r = linkind(M, length(M) - 1) + @test norm(R - delta(r, r')) < 1E-10 + for j in reverse(2:(length(M) - 1)) + R = R * M[j] * prime(M[j], "Link") + r = linkind(M, j - 1) + @test norm(R - delta(r, r')) < 1E-10 + end + + # Complex case + Mc = random_mps(sites; linkdims=chi) + @test inner(Mc, Mc) ≈ 1.0 + 0.0im + end + + @testset "random_mps from initial state (QN case)" begin + chi = 8 + sites = siteinds("S=1/2", 20; conserve_qns=true) + + # Make flux-zero random MPS + state = [isodd(n) ? 1 : 2 for n in 1:length(sites)] + M = random_mps(sites, state; linkdims=chi) + @test flux(M) == QN("Sz", 0) + + @test ITensorMPS.leftlim(M) == 0 + @test ITensorMPS.rightlim(M) == 2 + + @test norm(M[1]) ≈ 1.0 + @test inner(M, M) ≈ 1.0 + + @test maxlinkdim(M) == chi + + # Test making random MPS with different flux + state[1] = 2 + M = random_mps(sites, state; linkdims=chi) + @test flux(M) == QN("Sz", -2) + state[3] = 2 + M = random_mps(sites, state; linkdims=chi) + @test flux(M) == QN("Sz", -4) + end + + @testset "expect Function" begin + N = 8 + s = siteinds("S=1/2", N) + psi = random_mps(ComplexF64, s; linkdims=2) + + eSz = zeros(N) + eSx = zeros(N) + for j in 1:N + orthogonalize!(psi, j) + eSz[j] = real(scalar(dag(prime(psi[j], "Site")) * op("Sz", s[j]) * psi[j])) + eSx[j] = real(scalar(dag(prime(psi[j], "Site")) * op("Sx", s[j]) * psi[j])) + end + + res = expect(psi, "Sz") + @test res ≈ eSz + + res = expect(psi, "Sz"; sites=2:4) + @test res ≈ eSz[2:4] + + res = expect(psi, "Sz"; sites=[2, 4, 8]) + @test res[1] ≈ eSz[2] + @test res[2] ≈ eSz[4] + @test res[3] ≈ eSz[8] + + res = expect(psi, "Sz", "Sx") + @test res[1] ≈ eSz + @test res[2] ≈ eSx + + res = expect(psi, "Sz"; sites=3) + @test res isa Float64 + @test res ≈ eSz[3] + + res = expect(psi, "Sz", "Sx"; sites=3) + @test res isa Tuple{Float64,Float64} + @test res[1] ≈ eSz[3] + @test res[2] ≈ eSx[3] + + res = expect(psi, ("Sz", "Sx")) + @test res isa Tuple{Vector{Float64},Vector{Float64}} + @test res[1] ≈ eSz + @test res[2] ≈ eSx + + res = expect(psi, ["Sz" "Sx"; "Sx" "Sz"]; sites=3:7) + @test res isa Matrix{Vector{Float64}} + @test res[1, 1] ≈ eSz[3:7] + @test res[2, 1] ≈ eSx[3:7] + @test res[1, 2] ≈ eSx[3:7] + @test res[2, 2] ≈ eSz[3:7] + + # Test that passing zero-norm MPS leads to an error + # (expect not well-defined in that case) + psi0 = copy(psi) + psi0[1] *= zero(Bool) + @test iszero(norm(psi0)) + @test_throws ErrorException expect(psi0, "Sz") + end + + @testset "Expected value and Correlations" begin + m = 2 + + # Non-fermionic real case - spin system with QNs (very restrictive on allowed ops) + s = siteinds("S=1/2", 4; conserve_qns=true) + psi = random_mps(s, n -> isodd(n) ? "Up" : "Dn"; linkdims=m) + test_correlation_matrix(psi, [("S-", "S+"), ("S+", "S-")]) + + @test correlation_matrix(psi, [1/2 0; 0 -1/2], [1/2 0; 0 -1/2]) ≈ + correlation_matrix(psi, "Sz", "Sz") + @test expect(psi, [1/2 0; 0 -1/2]) ≈ expect(psi, "Sz") + @test all(expect(psi, [1/2 0; 0 -1/2], [1/2 0; 0 -1/2]) .≈ expect(psi, "Sz", "Sz")) + @test expect(psi, [[1/2 0; 0 -1/2], [1/2 0; 0 -1/2]]) ≈ expect(psi, ["Sz", "Sz"]) + + s = siteinds("S=1/2", length(s); conserve_qns=false) + psi = random_mps(s, n -> isodd(n) ? "Up" : "Dn"; linkdims=m) + test_correlation_matrix( + psi, + [ + ("Sz", "Sz"), + ("iSy", "iSy"), + ("Sx", "Sx"), + ("Sz", "Sx"), + ("S+", "S+"), + ("S-", "S+"), + ("S+", "S-"), + ("Sx", "S+"), + ("iSy", "iSy"), + ("Sx", "iSy"), + ], + ) + + test_correlation_matrix(psi, [("Sz", "Sz")]; ishermitian=false) + test_correlation_matrix(psi, [("Sz", "Sz")]; ishermitian=true) + test_correlation_matrix(psi, [("Sz", "Sx")]; ishermitian=false) + # This will fail becuase Sz*Sx is not hermitian, so the below diagonla corr matrix + # need to be calculated explicitely. + #test_correlation_matrix(psi,[("Sz", "Sx")];ishermitian=true) + + #Test sites feature + s = siteinds("S=1/2", 8; conserve_qns=false) + psi = random_mps(s, n -> isodd(n) ? "Up" : "Dn"; linkdims=m) + PM = expect(psi, "S+ * S-") + Cpm = correlation_matrix(psi, "S+", "S-") + range = 3:7 + Cpm37 = correlation_matrix(psi, "S+", "S-"; sites=range) + @test norm(Cpm37 - Cpm[range, range]) < 1E-8 + + @test norm(PM[range] - expect(psi, "S+ * S-"; sites=range)) < 1E-8 + + # With start_site, end_site arguments: + s = siteinds("S=1/2", 8) + psi = random_mps(ComplexF64, s; linkdims=m) + ss, es = 3, 6 + Nb = es - ss + 1 + Cpm = correlation_matrix(psi, "S+", "S-"; sites=ss:es) + Czz = correlation_matrix(psi, "Sz", "Sz"; sites=ss:es) + @test size(Cpm) == (Nb, Nb) + # Check using OpSum: + for i in ss:es, j in i:es + a = OpSum() + a += "S+", i, "S-", j + @test inner(psi', MPO(a, s), psi) ≈ Cpm[i - ss + 1, j - ss + 1] + end + + # Electron case + s = siteinds("Electron", 8) + psi = random_mps(s; linkdims=m) + test_correlation_matrix( + psi, [("Cdagup", "Cup"), ("Cup", "Cdagup"), ("Cup", "Cdn"), ("Cdagdn", "Cdn")] + ) + + s = siteinds("Electron", 8; conserve_qns=false) + psi = random_mps(s; linkdims=m) + test_correlation_matrix( + psi, + [ + ("Ntot", "Ntot"), + ("Nup", "Nup"), + ("Ndn", "Ndn"), + ("Cdagup", "Cup"), + ("Adagup", "Aup"), + ("Cdn", "Cdagdn"), + ("Adn", "Adagdn"), + ("Sz", "Sz"), + ("S+", "S-"), + ], + ) + # can't test ,("Cdn","Cdn") yet, because OpSum to MPO thinks this is antisymmetric + + #trigger unsupported error + let err = nothing + try + test_correlation_matrix(psi, [("Cup", "Aup")]) + catch err + end + + @test err isa Exception + @test sprint(showerror, err) == + "correlation_matrix: Mixed fermionic and bosonic operators are not supported yet." + end + + # Fermion case + s = siteinds("Fermion", 8) + psi = random_mps(s; linkdims=m) + test_correlation_matrix(psi, [("N", "N"), ("Cdag", "C"), ("C", "Cdag"), ("C", "C")]) + + s = siteinds("Fermion", 8; conserve_qns=false) + psi = random_mps(s; linkdims=m) + test_correlation_matrix(psi, [("N", "N"), ("Cdag", "C"), ("C", "Cdag"), ("C", "C")]) + + # + # Test non-contiguous sites input + # + C = correlation_matrix(psi, "N", "N") + non_contiguous = [1, 3, 8] + Cs = correlation_matrix(psi, "N", "N"; sites=non_contiguous) + for (ni, i) in enumerate(non_contiguous), (nj, j) in enumerate(non_contiguous) + @test Cs[ni, nj] ≈ C[i, j] + end + + C2 = correlation_matrix(psi, "N", "N"; sites=2) + @test C2 isa Matrix + @test C2[1, 1] ≈ C[2, 2] + end #testset + + @testset "correlation_matrix with dangling bonds" begin + l01 = Index(3) + l̃01 = sim(l01) + l12 = Index(3) + l23 = Index(3) + l34 = Index(3) + l̃34 = sim(l34) + s1 = Index(2, "Qubit") + s2 = Index(2, "Qubit") + s3 = Index(2, "Qubit") + b01 = random_itensor(l01, l̃01) + A1 = random_itensor(l̃01, s1, l12) + A2 = random_itensor(l12, s2, l23) + A3 = random_itensor(l23, s3, l̃34) + b34 = random_itensor(l̃34, l34) + ψ = MPS([b01, A1, A2, A3, b34]) + sites = 1:3 + C = correlation_matrix(ψ, "Z", "Z"; sites=(sites .+ 1)) + siteinds = [l01, s1, s2, s3, l34] + ψψ = inner(ψ, ψ) + zz = map(Iterators.product(sites .+ 1, sites .+ 1)) do I + i, j = I + ZiZj = MPO(OpSum() + ("Z", i, "Z", j), siteinds) + return inner(ψ', ZiZj, ψ) / ψψ + end + for I in eachindex(C) + @test C[I] ≈ zz[I] + end + end + + @testset "expect regression test for in-place modification of input MPS" begin + s = siteinds("S=1/2", 5) + psi = random_mps(s; linkdims=3) + orthogonalize!(psi, 1) + expect_init = expect(psi, "Sz") + norm_scale = 10 + psi[1] *= norm_scale + @test ortho_lims(psi) == 1:1 + @test norm(psi) ≈ norm_scale + expect_Sz = expect(psi, "Sz") + @test all(≤(1 / 2), expect_Sz) + @test expect_Sz ≈ expect_init + @test ortho_lims(psi) == 1:1 + @test norm(psi) ≈ norm_scale + end + + @testset "correlation_matrix regression test for in-place modification of input MPS" begin + s = siteinds("S=1/2", 5) + psi = random_mps(s; linkdims=3) + orthogonalize!(psi, 1) + correlation_matrix_init = correlation_matrix(psi, "Sz", "Sz") + norm_scale = 10 + psi[1] *= norm_scale + @test ortho_lims(psi) == 1:1 + @test norm(psi) ≈ norm_scale + correlation_matrix_SzSz = correlation_matrix(psi, "Sz", "Sz") + @test all(≤(1 / 2), correlation_matrix_SzSz) + @test correlation_matrix_SzSz ≈ correlation_matrix_init + @test ortho_lims(psi) == 1:1 + @test norm(psi) ≈ norm_scale + end + + @testset "swapbondsites" begin + sites = siteinds("S=1/2", 5) + ψ0 = random_mps(sites) + ψ = replacebond(ψ0, 3, ψ0[3] * ψ0[4]; swapsites=true, cutoff=1e-15) + @test siteind(ψ, 1) == siteind(ψ0, 1) + @test siteind(ψ, 2) == siteind(ψ0, 2) + @test siteind(ψ, 4) == siteind(ψ0, 3) + @test siteind(ψ, 3) == siteind(ψ0, 4) + @test siteind(ψ, 5) == siteind(ψ0, 5) + @test prod(ψ) ≈ prod(ψ0) + @test maxlinkdim(ψ) == 1 + + ψ = swapbondsites(ψ0, 4; cutoff=1e-15) + @test siteind(ψ, 1) == siteind(ψ0, 1) + @test siteind(ψ, 2) == siteind(ψ0, 2) + @test siteind(ψ, 3) == siteind(ψ0, 3) + @test siteind(ψ, 5) == siteind(ψ0, 4) + @test siteind(ψ, 4) == siteind(ψ0, 5) + @test prod(ψ) ≈ prod(ψ0) + @test maxlinkdim(ψ) == 1 + end + + @testset "map!" begin + s = siteinds("S=½", 5) + M0 = MPS(s, "↑") + + # Test map! with limits getting set + M = orthogonalize(M0, 1) + @test ITensorMPS.leftlim(M) == 0 + @test ITensorMPS.rightlim(M) == 2 + map!(prime, M) + @test ITensorMPS.leftlim(M) == 0 + @test ITensorMPS.rightlim(M) == length(M0) + 1 + + # Test map! without limits getting set + M = orthogonalize(M0, 1) + map!(prime, M; set_limits=false) + @test ITensorMPS.leftlim(M) == 0 + @test ITensorMPS.rightlim(M) == 2 + + # Test prime! with limits getting set + M = orthogonalize(M0, 1) + @test ITensorMPS.leftlim(M) == 0 + @test ITensorMPS.rightlim(M) == 2 + prime!(M; set_limits=true) + @test ITensorMPS.leftlim(M) == 0 + @test ITensorMPS.rightlim(M) == length(M0) + 1 + + # Test prime! without limits getting set + M = orthogonalize(M0, 1) + prime!(M) + @test ITensorMPS.leftlim(M) == 0 + @test ITensorMPS.rightlim(M) == 2 + end + + @testset "setindex!(::MPS, _, ::Colon)" begin + s = siteinds("S=½", 4) + ψ = random_mps(s) + ϕ = MPS(s, "↑") + orthogonalize!(ϕ, 1) + ψ[:] = ϕ + @test ITensorMPS.orthocenter(ψ) == 1 + @test inner(ψ, ϕ) ≈ 1 + + ψ = random_mps(s) + ϕ = MPS(s, "↑") + orthogonalize!(ϕ, 1) + ψ[:] = ITensorMPS.data(ϕ) + @test ITensorMPS.leftlim(ψ) == 0 + @test ITensorMPS.rightlim(ψ) == length(ψ) + 1 + @test inner(ψ, ϕ) ≈ 1 + end + + @testset "findsite[s](::MPS/MPO, is)" begin + s = siteinds("S=1/2", 5) + ψ = random_mps(s) + l = linkinds(ψ) + + A = random_itensor(s[4]', s[2]', dag(s[4]), dag(s[2])) + + @test findsite(ψ, s[3]) == 3 + @test findsite(ψ, (s[3], s[5])) == 3 + @test findsite(ψ, l[2]) == 2 + @test findsite(ψ, A) == 2 + + @test findsites(ψ, s[3]) == [3] + @test findsites(ψ, (s[4], s[1])) == [1, 4] + @test findsites(ψ, l[2]) == [2, 3] + @test findsites(ψ, (l[2], l[3])) == [2, 3, 4] + @test findsites(ψ, A) == [2, 4] + + M = random_mpo(s) + lM = linkinds(M) + + @test findsite(M, s[4]) == 4 + @test findsite(M, s[4]') == 4 + @test findsite(M, (s[4]', s[4])) == 4 + @test findsite(M, (s[4]', s[3])) == 3 + @test findsite(M, lM[2]) == 2 + @test findsite(M, A) == 2 + + @test findsites(M, s[4]) == [4] + @test findsites(M, s[4]') == [4] + @test findsites(M, (s[4]', s[4])) == [4] + @test findsites(M, (s[4]', s[3])) == [3, 4] + @test findsites(M, (lM[2], lM[3])) == [2, 3, 4] + @test findsites(M, A) == [2, 4] + end + + @testset "[first]siteind[s](::MPS/MPO, j::Int)" begin + s = siteinds("S=1/2", 5) + ψ = random_mps(s) + @test siteind(first, ψ, 3) == s[3] + @test siteind(ψ, 4) == s[4] + @test isnothing(siteind(ψ, 4; plev=1)) + @test siteinds(ψ, 3) == IndexSet(s[3]) + @test siteinds(ψ, 3; plev=1) == IndexSet() + + M = random_mpo(s) + @test noprime(siteind(first, M, 4)) == s[4] + @test siteind(first, M, 4; plev=0) == s[4] + @test siteind(first, M, 4; plev=1) == s[4]' + @test siteind(M, 4) == s[4] + @test siteind(M, 4; plev=0) == s[4] + @test siteind(M, 4; plev=1) == s[4]' + @test isnothing(siteind(M, 4; plev=2)) + @test hassameinds(siteinds(M, 3), (s[3], s[3]')) + @test siteinds(M, 3; plev=1) == IndexSet(s[3]') + @test siteinds(M, 3; plev=0) == IndexSet(s[3]) + @test siteinds(M, 3; tags="n=2") == IndexSet() + end + + @testset "movesites $N sites" for N in 1:4 + s0 = siteinds("S=1/2", N) + for perm in permutations(1:N) + s = s0[perm] + ψ = MPS(s, rand(("↑", "↓"), N)) + ns′ = [findfirst(==(i), s0) for i in s] + @test ns′ == perm + ψ′ = movesites(ψ, 1:N .=> ns′; cutoff=1e-15) + if N == 1 + @test maxlinkdim(ψ′) == 1 + else + @test maxlinkdim(ψ′) == 1 + end + for n in 1:N + @test s0[n] == siteind(ψ′, n) + end + @test prod(ψ) ≈ prod(ψ′) + end + end + + @testset "Construct MPS from ITensor" begin + N = 5 + s = siteinds("S=1/2", N) + l = [Index(3, "left_$n") for n in 1:2] + r = [Index(3, "right_$n") for n in 1:2] + + # + # MPS + # + + A = random_itensor(s...) + ψ = MPS(A, s) + @test prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == N + @test maxlinkdim(ψ) == 4 + + ψ0 = MPS(s, "↑") + A = prod(ψ0) + ψ = MPS(A, s; cutoff=1e-15) + @test prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == N + @test maxlinkdim(ψ) == 1 + + ψ0 = random_mps(s; linkdims=2) + A = prod(ψ0) + ψ = MPS(A, s; cutoff=1e-15, orthocenter=2) + @test prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == 2 + @test maxlinkdim(ψ) == 2 + + A = random_itensor(s..., l[1], r[1]) + ψ = MPS(A, s; leftinds=l[1], orthocenter=3) + ls = linkinds(ψ) + @test hassameinds(ψ[1], (l[1], s[1], ls[1])) + @test hassameinds(ψ[N], (r[1], s[N], ls[N - 1])) + @test prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == 3 + @test maxlinkdim(ψ) == 12 + + A = random_itensor(s..., l..., r...) + ψ = MPS(A, s; leftinds=l) + ls = linkinds(ψ) + @test hassameinds(ψ[1], (l..., s[1], ls[1])) + @test hassameinds(ψ[N], (r..., s[N], ls[N - 1])) + @test prod(ψ) ≈ A + @test ITensorMPS.orthocenter(ψ) == N + @test maxlinkdim(ψ) == 36 + + # + # Construct from regular Julia tensor + # + Ajt = randn(2, 2, 2, 2, 2) # Julia tensor + ψ = MPS(Ajt, s) + @test prod(ψ) ≈ ITensor(Ajt, s...) + + Ajv = randn(2^N) # Julia vector + ψ = MPS(Ajv, s) + @test prod(ψ) ≈ ITensor(Ajv, s...) + end + + @testset "Set range of MPS tensors" begin + N = 5 + s = siteinds("S=1/2", N) + ψ0 = random_mps(s; linkdims=3) + + ψ = orthogonalize(ψ0, 2) + A = prod(ITensorMPS.data(ψ)[2:(N - 1)]) + randn!(A) + ϕ = MPS(A, s[2:(N - 1)]; orthocenter=1) + ψ[2:(N - 1)] = ϕ + @test prod(ψ) ≈ ψ[1] * A * ψ[N] + @test maxlinkdim(ψ) == 4 + @test ITensorMPS.orthocenter(ψ) == 2 + + ψ = orthogonalize(ψ0, 1) + A = prod(ITensorMPS.data(ψ)[2:(N - 1)]) + randn!(A) + @test_throws AssertionError ψ[2:(N - 1)] = A + + ψ = orthogonalize(ψ0, 2) + A = prod(ITensorMPS.data(ψ)[2:(N - 1)]) + randn!(A) + ψ[2:(N - 1), orthocenter=3] = A + @test prod(ψ) ≈ ψ[1] * A * ψ[N] + @test maxlinkdim(ψ) == 4 + @test ITensorMPS.orthocenter(ψ) == 3 + end + + @testset "movesites reverse sites" begin + N = 6 + s = siteinds("S=1/2", N) + ψ0 = random_mps(s) + ψ = movesites(ψ0, 1:N .=> reverse(1:N)) + for n in 1:N + @test siteind(ψ, n) == s[N - n + 1] + end + end + + @testset "movesites subsets of sites" begin + N = 6 + s = siteinds("S=1/2", N) + ψ = random_mps(s) + + for i in 1:N, j in 1:N + ns = [i, j] + !allunique(ns) && continue + min_ns = minimum(ns) + ns′ = collect(min_ns:(min_ns + length(ns) - 1)) + ψ′ = movesites(ψ, ns .=> ns′; cutoff=1e-15) + @test siteind(ψ′, min_ns) == siteind(ψ, i) + @test siteind(ψ′, min_ns + 1) == siteind(ψ, j) + @test maxlinkdim(ψ′) == 1 + ψ̃ = movesites(ψ′, ns′ .=> ns; cutoff=1e-15) + for n in 1:N + @test siteind(ψ̃, n) == siteind(ψ, n) + end + @test maxlinkdim(ψ̃) == 1 + end + + for i in 1:N, j in 1:N, k in 1:N + ns = [i, j, k] + !allunique(ns) && continue + min_ns = minimum(ns) + ns′ = collect(min_ns:(min_ns + length(ns) - 1)) + ψ′ = movesites(ψ, ns .=> ns′; cutoff=1e-15) + @test siteind(ψ′, min_ns) == siteind(ψ, i) + @test siteind(ψ′, min_ns + 1) == siteind(ψ, j) + @test siteind(ψ′, min_ns + 2) == siteind(ψ, k) + @test maxlinkdim(ψ′) == 1 + ψ̃ = movesites(ψ′, ns′ .=> ns; cutoff=1e-15) + for n in 1:N + @test siteind(ψ̃, n) == siteind(ψ, n) + end + @test maxlinkdim(ψ̃) == 1 + end + + for i in 1:N, j in 1:N, k in 1:N, l in 1:N + ns = [i, j, k, l] + !allunique(ns) && continue + min_ns = minimum(ns) + ns′ = collect(min_ns:(min_ns + length(ns) - 1)) + ψ′ = movesites(ψ, ns .=> ns′; cutoff=1e-15) + @test siteind(ψ′, min_ns) == siteind(ψ, i) + @test siteind(ψ′, min_ns + 1) == siteind(ψ, j) + @test siteind(ψ′, min_ns + 2) == siteind(ψ, k) + @test siteind(ψ′, min_ns + 3) == siteind(ψ, l) + @test maxlinkdim(ψ′) == 1 + ψ̃ = movesites(ψ′, ns′ .=> ns; cutoff=1e-15) + for n in 1:N + @test siteind(ψ̃, n) == siteind(ψ, n) + end + @test maxlinkdim(ψ̃) == 1 + end + + for i in 1:N, j in 1:N, k in 1:N, l in 1:N, m in 1:N + ns = [i, j, k, l, m] + !allunique(ns) && continue + min_ns = minimum(ns) + ns′ = collect(min_ns:(min_ns + length(ns) - 1)) + ψ′ = movesites(ψ, ns .=> ns′; cutoff=1e-15) + for n in 1:length(ns) + @test siteind(ψ′, min_ns + n - 1) == siteind(ψ, ns[n]) + end + @test maxlinkdim(ψ′) == 1 + ψ̃ = movesites(ψ′, ns′ .=> ns; cutoff=1e-15) + for n in 1:N + @test siteind(ψ̃, n) == siteind(ψ, n) + end + @test maxlinkdim(ψ̃) == 1 + end + end + + @testset "product(::Vector{ITensor}, ::MPS)" begin + N = 6 + s = siteinds("Qubit", N) + + I = [op("Id", s, n) for n in 1:N] + X = [op("X", s, n) for n in 1:N] + Y = [op("Y", s, n) for n in 1:N] + Z = [op("Z", s, n) for n in 1:N] + H = [op("H", s, n) for n in 1:N] + CX = [op("CX", s, n, m) for n in 1:N, m in 1:N] + CY = [op("CY", s, n, m) for n in 1:N, m in 1:N] + CZ = [op("CZ", s, n, m) for n in 1:N, m in 1:N] + CCNOT = [op("CCNOT", s, n, m, k) for n in 1:N, m in 1:N, k in 1:N] + CSWAP = [op("CSWAP", s, n, m, k) for n in 1:N, m in 1:N, k in 1:N] + CCCNOT = [op("CCCNOT", s, n, m, k, l) for n in 1:N, m in 1:N, k in 1:N, l in 1:N] + + v0 = [onehot(s[n] => "0") for n in 1:N] + v1 = [onehot(s[n] => "1") for n in 1:N] + + # Single qubit + @test product(I[1], v0[1]) ≈ v0[1] + @test product(I[1], v1[1]) ≈ v1[1] + + @test product(H[1], H[1]) ≈ I[1] + @test product(H[1], v0[1]) ≈ 1 / sqrt(2) * (v0[1] + v1[1]) + @test product(H[1], v1[1]) ≈ 1 / sqrt(2) * (v0[1] - v1[1]) + + @test product(X[1], v0[1]) ≈ v1[1] + @test product(X[1], v1[1]) ≈ v0[1] + + @test product(Y[1], v0[1]) ≈ im * v1[1] + @test product(Y[1], v1[1]) ≈ -im * v0[1] + + @test product(Z[1], v0[1]) ≈ v0[1] + @test product(Z[1], v1[1]) ≈ -v1[1] + + @test product(X[1], X[1]) ≈ I[1] + @test product(Y[1], Y[1]) ≈ I[1] + @test product(Z[1], Z[1]) ≈ I[1] + @test -im * product([Y[1], X[1]], Z[1]) ≈ I[1] + + @test dag(X[1]) ≈ -product([X[1], Y[1]], Y[1]) + @test dag(Y[1]) ≈ -product([Y[1], Y[1]], Y[1]) + @test dag(Z[1]) ≈ -product([Z[1], Y[1]], Y[1]) + + @test product(X[1], Y[1]) - product(Y[1], X[1]) ≈ 2 * im * Z[1] + @test product(Y[1], Z[1]) - product(Z[1], Y[1]) ≈ 2 * im * X[1] + @test product(Z[1], X[1]) - product(X[1], Z[1]) ≈ 2 * im * Y[1] + + @test product([Y[1], X[1]], v0[1]) - product([X[1], Y[1]], v0[1]) ≈ + 2 * im * product(Z[1], v0[1]) + @test product([Y[1], X[1]], v1[1]) - product([X[1], Y[1]], v1[1]) ≈ + 2 * im * product(Z[1], v1[1]) + @test product([Z[1], Y[1]], v0[1]) - product([Y[1], Z[1]], v0[1]) ≈ + 2 * im * product(X[1], v0[1]) + @test product([Z[1], Y[1]], v1[1]) - product([Y[1], Z[1]], v1[1]) ≈ + 2 * im * product(X[1], v1[1]) + @test product([X[1], Z[1]], v0[1]) - product([Z[1], X[1]], v0[1]) ≈ + 2 * im * product(Y[1], v0[1]) + @test product([X[1], Z[1]], v1[1]) - product([Z[1], X[1]], v1[1]) ≈ + 2 * im * product(Y[1], v1[1]) + + # + # 2-qubit + # + + @test product(I[1] * I[2], v0[1] * v0[2]) ≈ v0[1] * v0[2] + + @test product(CX[1, 2], v0[1] * v0[2]) ≈ v0[1] * v0[2] + @test product(CX[1, 2], v0[1] * v1[2]) ≈ v0[1] * v1[2] + @test product(CX[1, 2], v1[1] * v0[2]) ≈ v1[1] * v1[2] + @test product(CX[1, 2], v1[1] * v1[2]) ≈ v1[1] * v0[2] + + @test product(CY[1, 2], v0[1] * v0[2]) ≈ v0[1] * v0[2] + @test product(CY[1, 2], v0[1] * v1[2]) ≈ v0[1] * v1[2] + @test product(CY[1, 2], v1[1] * v0[2]) ≈ im * v1[1] * v1[2] + @test product(CY[1, 2], v1[1] * v1[2]) ≈ -im * v1[1] * v0[2] + + @test product(CZ[1, 2], v0[1] * v0[2]) ≈ v0[1] * v0[2] + @test product(CZ[1, 2], v0[1] * v1[2]) ≈ v0[1] * v1[2] + @test product(CZ[1, 2], v1[1] * v0[2]) ≈ v1[1] * v0[2] + @test product(CZ[1, 2], v1[1] * v1[2]) ≈ -v1[1] * v1[2] + + # + # 3-qubit + # + + @test product(CCNOT[1, 2, 3], v0[1] * v0[2] * v0[3]) ≈ v0[1] * v0[2] * v0[3] + @test product(CCNOT[1, 2, 3], v0[1] * v0[2] * v1[3]) ≈ v0[1] * v0[2] * v1[3] + @test product(CCNOT[1, 2, 3], v0[1] * v1[2] * v0[3]) ≈ v0[1] * v1[2] * v0[3] + @test product(CCNOT[1, 2, 3], v0[1] * v1[2] * v1[3]) ≈ v0[1] * v1[2] * v1[3] + @test product(CCNOT[1, 2, 3], v1[1] * v0[2] * v0[3]) ≈ v1[1] * v0[2] * v0[3] + @test product(CCNOT[1, 2, 3], v1[1] * v0[2] * v1[3]) ≈ v1[1] * v0[2] * v1[3] + @test product(CCNOT[1, 2, 3], v1[1] * v1[2] * v0[3]) ≈ v1[1] * v1[2] * v1[3] + @test product(CCNOT[1, 2, 3], v1[1] * v1[2] * v1[3]) ≈ v1[1] * v1[2] * v0[3] + + @test product(CSWAP[1, 2, 3], v0[1] * v0[2] * v0[3]) ≈ v0[1] * v0[2] * v0[3] + @test product(CSWAP[1, 2, 3], v0[1] * v0[2] * v1[3]) ≈ v0[1] * v0[2] * v1[3] + @test product(CSWAP[1, 2, 3], v0[1] * v1[2] * v0[3]) ≈ v0[1] * v1[2] * v0[3] + @test product(CSWAP[1, 2, 3], v0[1] * v1[2] * v1[3]) ≈ v0[1] * v1[2] * v1[3] + @test product(CSWAP[1, 2, 3], v1[1] * v0[2] * v0[3]) ≈ v1[1] * v0[2] * v0[3] + @test product(CSWAP[1, 2, 3], v1[1] * v0[2] * v1[3]) ≈ v1[1] * v1[2] * v0[3] + @test product(CSWAP[1, 2, 3], v1[1] * v1[2] * v0[3]) ≈ v1[1] * v0[2] * v1[3] + @test product(CSWAP[1, 2, 3], v1[1] * v1[2] * v1[3]) ≈ v1[1] * v1[2] * v1[3] + + # + # Apply to an MPS + # + + ψ = MPS(s, "0") + @test prod(product(X[1], ψ)) ≈ prod(MPS(s, n -> n == 1 ? "1" : "0")) + @test prod(product(X[1], product(X[2], ψ))) ≈ + prod(MPS(s, n -> n == 1 || n == 2 ? "1" : "0")) + @test prod(product(X[1] * X[2], ψ)) ≈ prod(MPS(s, n -> n == 1 || n == 2 ? "1" : "0")) + @test prod(product([X[2], X[1]], ψ)) ≈ prod(MPS(s, n -> n == 1 || n == 2 ? "1" : "0")) + @test prod(product(CX[1, 2], ψ)) ≈ prod(MPS(s, "0")) + @test prod(product(CX[1, 2], product(X[1], ψ))) ≈ + prod(MPS(s, n -> n == 1 || n == 2 ? "1" : "0")) + @test prod(product(product(CX[1, 2], X[1]), ψ)) ≈ + prod(MPS(s, n -> n == 1 || n == 2 ? "1" : "0")) + @test prod(product([X[1], CX[1, 2]], ψ)) ≈ + prod(MPS(s, n -> n == 1 || n == 2 ? "1" : "0")) + + for i in 1:N, j in 1:N + !allunique((i, j)) && continue + # Don't move sites back + CXij_ψ = product([X[i], CX[i, j]], ψ; move_sites_back=false, cutoff=1e-15) + @test maxlinkdim(CXij_ψ) == 1 + @test prod(CXij_ψ) ≈ prod(MPS(s, n -> n == i || n == j ? "1" : "0")) + + # Move sites back + CXij_ψ = product([X[i], CX[i, j]], ψ) + for n in 1:N + @test siteind(CXij_ψ, n) == siteind(ψ, n) + end + @test prod(CXij_ψ) ≈ prod(MPS(s, n -> n == i || n == j ? "1" : "0")) + end + + for i in 1:N, j in 1:N, k in 1:N + ns = (i, j, k) + !allunique(ns) && continue + # Don't move sites back + CCNOTijk_ψ = product( + [X[j], X[i], CCNOT[ns...]], ψ; move_sites_back=false, cutoff=1e-15 + ) + @test maxlinkdim(CCNOTijk_ψ) == 1 + @test prod(CCNOTijk_ψ) ≈ prod(MPS(s, n -> n ∈ ns ? "1" : "0")) + + # Move sites back + CCNOTijk_ψ = product([X[j], X[i], CCNOT[ns...]], ψ; cutoff=1e-15) + @test maxlinkdim(CCNOTijk_ψ) == 1 + for n in 1:N + @test siteind(CCNOTijk_ψ, n) == siteind(ψ, n) + end + @test prod(CCNOTijk_ψ) ≈ prod(MPS(s, n -> n ∈ ns ? "1" : "0")) + end + + for i in 1:N, j in i:N, k in 1:N, l in k:N + ns = (i, j, k, l) + !allunique(ns) && continue + # Don't move sites back + CCCNOTijkl_ψ = product( + [X[i], X[j], X[k], CCCNOT[ns...]], ψ; move_sites_back=false, cutoff=1e-15 + ) + @test maxlinkdim(CCCNOTijkl_ψ) == 1 + @test prod(CCCNOTijkl_ψ) ≈ prod(MPS(s, n -> n ∈ ns ? "1" : "0")) + + # Move sites back + CCCNOTijkl_ψ = product([X[i], X[j], X[k], CCCNOT[ns...]], ψ; cutoff=1e-15) + @test maxlinkdim(CCCNOTijkl_ψ) == 1 + for n in 1:N + @test siteind(CCCNOTijkl_ψ, n) == siteind(ψ, n) + end + @test prod(CCCNOTijkl_ψ) ≈ prod(MPS(s, n -> n ∈ ns ? "1" : "0")) + end + end + + @testset "product" begin + @testset "Contraction order of operations" begin + s = siteind("Qubit") + Q = SiteType("Qubit") + @test product(ops([s], [("Y", 1), ("X", 1)]), setelt(s => 1)) ≈ + itensor(op("X", Q) * op("Y", Q) * [1; 0], s) + @test product(ops([s], [("Y", 1), ("Z", 1)]), setelt(s => 1)) ≈ + itensor(op("Z", Q) * op("Y", Q) * [1; 0], s) + @test product(ops([s], [("X", 1), ("Y", 1)]), setelt(s => 1)) ≈ + itensor(op("Y", Q) * op("X", Q) * [1; 0], s) + end + + @testset "Simple on-site state evolution" begin + N = 3 + + pos = [("Z", 3), ("Y", 2), ("X", 1)] + + s = siteinds("Qubit", N) + gates = ops(s, pos) + ψ0 = MPS(s, "0") + + # Apply the gates + ψ = product(gates, ψ0) + + # Move site 1 to position 3 + ψ′ = movesite(ψ, 1 => 3) + @test siteind(ψ′, 1) == s[2] + @test siteind(ψ′, 2) == s[3] + @test siteind(ψ′, 3) == s[1] + @test prod(ψ) ≈ prod(ψ′) + + # Move the site back + ψ′′ = movesite(ψ′, 3 => 1) + @test siteind(ψ′′, 1) == s[1] + @test siteind(ψ′′, 2) == s[2] + @test siteind(ψ′′, 3) == s[3] + @test prod(ψ) ≈ prod(ψ′′) + end + + @testset "More complex evolution" begin + N = 7 + + osX = [("X", n) for n in 1:N] + + osZ = [("Z", n) for n in 1:N] + + osSw = [("SWAP", n, n + 1) for n in 1:(N - 2)] + + osCx = [("CX", n, n + 3) for n in 1:(N - 3)] + + osT = [("CCX", n, n + 1, n + 3) for n in 1:(N - 3)] + + osRx = [("Rx", n, (θ=π,)) for n in 1:N] + + osXX = [("Rxx", (n, n + 1), (ϕ=π / 8,)) for n in 1:(N - 1)] + + #os_noise = [("noise", n, n+2, n+4) for n in 1:N-4] + + os = vcat(osX, osXX, osSw, osRx, osZ, osCx, osT) + s = siteinds("Qubit", N) + gates = ops(os, s) + + @testset "Pure state evolution" begin + ψ0 = MPS(s, "0") + ψ = product(gates, ψ0; cutoff=1e-15) + @test maxlinkdim(ψ) == 8 + prodψ = product(gates, prod(ψ0)) + @test prod(ψ) ≈ prodψ rtol = 1e-12 + end + + M0 = MPO(s, "Id") + maxdim = prod(dim(siteinds(M0, j)) for j in 1:N) + + @testset "Mixed state evolution" begin + M = product(gates, M0; cutoff=1e-15, maxdim=maxdim) + @test maxlinkdim(M) == 24 || maxlinkdim(M) == 25 + sM0 = siteinds(M0) + sM = siteinds(M) + for n in 1:N + @test hassameinds(sM[n], sM0[n]) + end + @set_warn_order 15 begin + prodM = product(gates, prod(M0)) + @test prod(M) ≈ prodM rtol = 1e-6 + end + end + + #@testset "Mixed state noisy evolution" begin + # prepend!(os, os_noise) + # gates = ops(os, s) + # M = product(gates, M0; apply_dag = true, + # cutoff = 1e-15, maxdim = maxdim) + # @test maxlinkdim(M) == 64 + # sM0 = siteinds(M0) + # sM = siteinds(M) + # for n in 1:N + # @test hassameinds(sM[n], sM0[n]) + # end + # @set_warn_order 16 begin + # prodM = product(gates, prod(M0); apply_dag = true) + # @test prod(M) ≈ prodM rtol = 1e-7 + # end + #end + + #@testset "Mixed state noisy evolution" begin + # prepend!(os, os_noise) + # gates = ops(os, s) + # M = product(gates, M0; + # apply_dag = true, cutoff = 1e-15, maxdim = maxdim-1) + # @test maxlinkdim(M) == 64 + # sM0 = siteinds(M0) + # sM = siteinds(M) + # for n in 1:N + # @test hassameinds(sM[n], sM0[n]) + # end + # @set_warn_order 16 begin + # prodM = product(gates, prod(M0); apply_dag = true) + # @test prod(M) ≈ prodM rtol = 1e-7 + # end + #end + + end + + @testset "Gate evolution open system" begin + N = 8 + osX = [("X", n) for n in 1:N] + osZ = [("Z", n) for n in 1:N] + osSw = [("SWAP", n, n + 2) for n in 1:(N - 2)] + osCx = [("CX", n, n + 3) for n in 1:(N - 3)] + osT = [("CCX", n, n + 1, n + 3) for n in 1:(N - 3)] + osRx = [("Rx", n, (θ=π,)) for n in 1:N] + os = vcat(osX, osSw, osRx, osZ, osCx, osT) + + s = siteinds("Qubit", N) + gates = ops(os, s) + + M0 = MPO(s, "Id") + + # Apply the gates + + s0 = siteinds(M0) + + M = apply(gates, M0; apply_dag=true, cutoff=1e-15, maxdim=500, svd_alg="qr_iteration") + + s = siteinds(M) + for n in 1:N + @assert hassameinds(s[n], s0[n]) + end + + @set_warn_order 18 begin + prodM = apply(gates, prod(M0); apply_dag=true) + @test prod(M) ≈ prodM rtol = 1e-6 + end + end + + @testset "Gate evolution state" begin + N = 10 + + osX = [("X", n) for n in 1:N] + osZ = [("Z", n) for n in 1:N] + osSw = [("SWAP", n, n + 1) for n in 1:(N - 1)] + osCx = [("CX", n, n + 1) for n in 1:(N - 1)] + osT = [("CCX", n, n + 2, n + 4) for n in 1:(N - 4)] + os = vcat(osX, osSw, osZ, osCx, osT) + + s = siteinds("Qubit", N) + gates = ops(os, s) + + ψ0 = MPS(s, "0") + + # Apply the gates + ψ = apply(gates, ψ0; cutoff=1e-15, maxdim=100) + + prodψ = apply(gates, prod(ψ0)) + @test prod(ψ) ≈ prodψ rtol = 1e-4 + end + + @testset "With fermions" begin + N = 3 + + s = siteinds("Fermion", N; conserve_qns=true) + + # Ground state |000⟩ + ψ000 = MPS(s, "0") + + # Start state |011⟩ + ψ011 = MPS(s, n -> n == 2 || n == 3 ? "1" : "0") + + # Reference state |110⟩ + ψ110 = MPS(s, n -> n == 1 || n == 2 ? "1" : "0") + + function ITensors.op(::OpName"CdagC1", ::SiteType, s1::Index, s2::Index) + return op("Cdag", s1) * op("C", s2) + end + + os = [("CdagC1", 1, 3)] + Os = ops(os, s) + + # Results in -|110⟩ + ψ1 = product(Os, ψ011; cutoff=1e-15) + + @test inner(ψ1, ψ110) == -1 + + a = OpSum() + a += "Cdag", 1, "C", 3 + H = MPO(a, s) + + # Results in -|110⟩ + ψ2 = noprime(contract(H, ψ011; cutoff=1e-15)) + + @test inner(ψ2, ψ110) == -1 + end + + @testset "Spinless fermion (gate evolution)" begin + N = 6 + + s = siteinds("Fermion", N; conserve_qns=true) + + # Starting state + ψ0 = MPS(s, n -> isodd(n) ? "0" : "1") + + t = 1.0 + U = 1.0 + opsum = OpSum() + for b in 1:(N - 1) + opsum .-= t, "Cdag", b, "C", b + 1 + opsum .-= t, "Cdag", b + 1, "C", b + opsum .+= U, "N", b, "N", b + 1 + end + H = MPO(opsum, s) + + sweeps = Sweeps(6) + maxdim!(sweeps, 10, 20, 40) + cutoff!(sweeps, 1E-12) + energy, ψ0 = dmrg(H, ψ0, sweeps; outputlevel=0) + + function ITensors.op(::OpName"CdagC2", ::SiteType, s1::Index, s2::Index) + return op("Cdag", s1) * op("C", s2) + end + + function ITensors.op( + ::OpName"CCCC", ::SiteType, s1::Index, s2::Index, s3::Index, s4::Index + ) + return -1 * op("Cdag", s1) * op("Cdag", s2) * op("C", s3) * op("C", s4) + end + + for i in 1:(N - 1), j in (i + 1):N + G1 = op("CdagC2", s, i, j) + + @disable_warn_order begin + G2 = op("Cdag", s, i) + for n in (i + 1):(j - 1) + G2 *= op("F", s, n) + end + G2 *= op("C", s, j) + end + + opsum = OpSum() + opsum += "Cdag", i, "C", j + G3 = MPO(opsum, s) + + A_OP = prod(product(G1, ψ0; cutoff=1e-6)) + + A_OPS = noprime(G2 * prod(ψ0)) + + A_MPO = noprime(prod(contract(G3, ψ0; cutoff=1e-6))) + + @test A_OP ≈ A_OPS + @test A_OP ≈ A_MPO + end + + for i in 1:(N - 3), j in (i + 1):(N - 2), k in (j + 1):(N - 1), l in (k + 1):N + G1 = op("CCCC", s, i, j, k, l) + @disable_warn_order begin + G2 = -1 * op("Cdag", s, i) + for n in (i + 1):(j - 1) + G2 *= op("F", s, n) + end + G2 *= op("Cdag", s, j) + for n in (j + 1):(k - 1) + G2 *= op("Id", s, n) + end + G2 *= op("C", s, k) + for n in (k + 1):(l - 1) + G2 *= op("F", s, n) + end + G2 *= op("C", s, l) + + opsum = OpSum() + opsum += "Cdag", i, "Cdag", j, "C", k, "C", l + G3 = MPO(opsum, s) + + A_OP = prod(product(G1, ψ0; cutoff=1e-16)) + + A_OPS = noprime(G2 * prod(ψ0)) + + A_MPO = noprime(prod(contract(G3, ψ0; cutoff=1e-16))) + end + @test A_OPS ≈ A_OP rtol = 1e-12 + @test A_MPO ≈ A_OP rtol = 1e-12 + end + end + + @testset "Spinful Fermions (Electron) gate evolution" begin + N = 8 + s = siteinds("Electron", N; conserve_qns=true) + ψ0 = random_mps(s, n -> isodd(n) ? "↑" : "↓") + t = 1.0 + U = 1.0 + opsum = OpSum() + for b in 1:(N - 1) + opsum .-= t, "Cdagup", b, "Cup", b + 1 + opsum .-= t, "Cdagup", b + 1, "Cup", b + opsum .-= t, "Cdagdn", b, "Cdn", b + 1 + opsum .-= t, "Cdagdn", b + 1, "Cdn", b + end + for n in 1:N + opsum .+= U, "Nupdn", n + end + H = MPO(opsum, s) + sweeps = Sweeps(6) + maxdim!(sweeps, 10, 20, 40) + cutoff!(sweeps, 1E-12) + energy, ψ = dmrg(H, ψ0, sweeps; outputlevel=0) + + function ITensors.op(::OpName"CCup", ::SiteType"Electron", s1::Index, s2::Index) + return op("Adagup * F", s1) * op("Aup", s2) + end + + for i in 1:(N - 1), j in (i + 1):N + opsum = OpSum() + opsum += "Cdagup", i, "Cup", j + G1 = MPO(opsum, s) + G2 = op("CCup", s, i, j) + A_MPO = prod(noprime(contract(G1, ψ; cutoff=1e-8))) + A_OP = prod(product(G2, ψ; cutoff=1e-8)) + @test A_MPO ≈ A_OP atol = 1E-4 + end + end + end + + @testset "dense conversion of MPS" begin + N = 4 + s = siteinds("S=1/2", N; conserve_qns=true) + QM = random_mps(s, ["Up", "Dn", "Up", "Dn"]; linkdims=4) + qsz1 = scalar(QM[1] * op("Sz", s[1]) * dag(prime(QM[1], "Site"))) + + M = dense(QM) + @test !hasqns(M[1]) + sz1 = scalar(M[1] * op("Sz", removeqns(s[1])) * dag(prime(M[1], "Site"))) + @test sz1 ≈ qsz1 + end + + @testset "inner of MPS with more than one site Index" begin + s = siteinds("S=½", 4) + sout = addtags.(s, "out") + sin = addtags.(s, "in") + sinds = IndexSet.(sout, sin) + Cs = combiner.(sinds) + cinds = combinedind.(Cs) + ψ = random_mps(cinds) + @test norm(ψ) ≈ 1 + @test inner(ψ, ψ) ≈ 1 + ψ .*= dag.(Cs) + @test norm(ψ) ≈ 1 + @test inner(ψ, ψ) ≈ 1 + end + + @testset "inner(::MPS, ::MPO, ::MPS) with more than one site Index" begin + N = 8 + s = siteinds("S=1/2", N) + a = OpSum() + for j in 1:(N - 1) + a .+= 0.5, "S+", j, "S-", j + 1 + a .+= 0.5, "S-", j, "S+", j + 1 + a .+= "Sz", j, "Sz", j + 1 + end + H = MPO(a, s) + ψ = random_mps(s, n -> isodd(n) ? "↑" : "↓"; linkdims=10) + # Create MPO/MPS with pairs of sites merged + H2 = MPO([H[b] * H[b + 1] for b in 1:2:N]) + ψ2 = MPS([ψ[b] * ψ[b + 1] for b in 1:2:N]) + @test inner(ψ, ψ) ≈ inner(ψ2, ψ2) + @test inner(ψ', H, ψ) ≈ inner(ψ2', H2, ψ2) + @test_throws ErrorException inner(ψ2, ψ2') + @test_throws ErrorException inner(ψ2, H2, ψ2) + end + + @testset "orthogonalize! on MPS with no link indices" begin + N = 4 + s = siteinds("S=1/2", N) + ψ = MPS([itensor(randn(ComplexF64, 2), s[n]) for n in 1:N]) + @test all(==(IndexSet()), linkinds(all, ψ)) + ϕ = orthogonalize(ψ, 2) + @test ITensorMPS.hasdefaultlinktags(ϕ) + @test ortho_lims(ϕ) == 2:2 + @test ITensorMPS.dist(ψ, ϕ) ≈ 0 atol = 1e-6 + # TODO: use this instead? + # @test lognorm(ψ - ϕ) < -16 + @test norm(ψ - ϕ) ≈ 0 atol = 1e-6 + end + + @testset "MPO from MPS with no link indices" begin + N = 4 + s = siteinds("S=1/2", N) + ψ = MPS([itensor(randn(ComplexF64, 2), s[n]) for n in 1:N]) + ρ = outer(ψ', ψ) + @test !ITensorMPS.hasnolinkinds(ρ) + @test inner(ρ, ρ) ≈ inner(ψ, ψ)^2 + @test inner(ψ', ρ, ψ) ≈ inner(ψ, ψ)^2 + + # Deprecated syntax + @test_deprecated outer(ψ, ψ) + @test_deprecated inner(ψ, ψ') + @test_deprecated inner(ψ, ρ, ψ) + + ρ = @test_deprecated MPO(ψ) + @test !ITensorMPS.hasnolinkinds(ρ) + @test inner(ρ, ρ) ≈ inner(ψ, ψ)^2 + @test inner(ψ', ρ, ψ) ≈ inner(ψ, ψ)^2 + end + + @testset "Truncate MPO with no link indices" begin + N = 4 + s = siteinds("S=1/2", N) + M = MPO([itensor(randn(ComplexF64, 2, 2), s[n]', dag(s[n])) for n in 1:N]) + @test ITensorMPS.hasnolinkinds(M) + Mt = truncate(M; cutoff=1e-15) + @test ITensorMPS.hasdefaultlinktags(Mt) + @test norm(M - Mt) ≈ 0 atol = 1e-12 + end +end +end diff --git a/test/base/test_qnmpo.jl b/test/base/test_qnmpo.jl new file mode 100644 index 0000000..038f081 --- /dev/null +++ b/test/base/test_qnmpo.jl @@ -0,0 +1,375 @@ +using ITensors, Test + +function op_mpo(sites, which_op, j) + left_ops = "Id" + right_ops = "Id" + if has_fermion_string(which_op, sites[j]) + left_ops = "F" + end + ops = [n < j ? left_ops : (n > j ? right_ops : which_op) for n in 1:length(sites)] + return MPO([op(ops[n], sites[n]) for n in 1:length(sites)]) +end + +@testset "MPO Basics" begin + N = 6 + sites = [Index(QN(-1) => 1, QN(1) => 1; tags="Site,n=$n") for n in 1:N] + links = [Index(QN() => 1; tags="Links,l=$n") for n in 1:(N - 1)] + @test length(MPO()) == 0 + #O = MPO(sites) + O = MPO(N) + for i in 1:length(O) + O[i] = random_itensor(QN(), sites[i], sites[i]') + end + @test length(O) == N + + O[1] = emptyITensor(sites[1], prime(sites[1])) + @test hasind(O[1], sites[1]) + @test hasind(O[1], prime(sites[1])) + P = copy(O) + @test hasind(P[1], sites[1]) + @test hasind(P[1], prime(sites[1])) + # test constructor from Vector{ITensor} + + K = MPO(N) + K[1] = random_itensor(QN(), dag(sites[1]), sites[1]', links[1]) + for i in 2:(N - 1) + K[i] = random_itensor(QN(), dag(sites[i]), sites[i]', dag(links[i - 1]), links[i]) + end + K[N] = random_itensor(QN(), dag(sites[N]), sites[N]', dag(links[N - 1])) + + J = MPO(N) + J[1] = random_itensor(QN(), dag(sites[1]), sites[1]', links[1]) + for i in 2:(N - 1) + J[i] = random_itensor(QN(), dag(sites[i]), sites[i]', dag(links[i - 1]), links[i]) + end + J[N] = random_itensor(QN(), dag(sites[N]), sites[N]', dag(links[N - 1])) + + L = MPO(N) + L[1] = random_itensor(QN(), dag(sites[1]), sites[1]', links[1]) + for i in 2:(N - 1) + L[i] = random_itensor(QN(), dag(sites[i]), sites[i]', dag(links[i - 1]), links[i]) + end + L[N] = random_itensor(QN(), dag(sites[N]), sites[N]', dag(links[N - 1])) + + @test length(K) == N + @test ITensors.data(MPO(copy(ITensors.data(K)))) == ITensors.data(K) + + phi = MPS(N) + phi[1] = random_itensor(QN(-1), sites[1], links[1]) + for i in 2:(N - 1) + phi[i] = random_itensor(QN(-1), sites[i], dag(links[i - 1]), links[i]) + end + phi[N] = random_itensor(QN(-1), sites[N], dag(links[N - 1])) + + psi = MPS(N) + psi[1] = random_itensor(QN(-1), sites[1], links[1]) + for i in 2:(N - 1) + psi[i] = random_itensor(QN(-1), sites[i], dag(links[i - 1]), links[i]) + end + psi[N] = random_itensor(QN(-1), sites[N], dag(links[N - 1])) + + @testset "orthogonalize!" begin + orthogonalize!(phi, 1) + orthogonalize!(K, 1) + orig_inner = ⋅(phi', K, phi) + orthogonalize!(phi, div(N, 2)) + orthogonalize!(K, div(N, 2)) + @test ⋅(phi', K, phi) ≈ orig_inner + end + + @testset "inner " begin + @test maxlinkdim(K) == 1 + phidag = dag(phi) + prime!(phidag) + phiKpsi = phidag[1] * K[1] * psi[1] + for j in 2:N + phiKpsi *= phidag[j] * K[j] * psi[j] + end + @test phiKpsi[] ≈ inner(phi', K, psi) + end + + @testset "inner " begin + phidag = dag(phi) + prime!(phidag, 2) + Jdag = dag(J) + prime!(Jdag) + for j in eachindex(Jdag) + swapprime!(Jdag[j], 2, 3) + swapprime!(Jdag[j], 1, 2) + swapprime!(Jdag[j], 3, 1) + end + + phiJdagKpsi = phidag[1] * Jdag[1] * K[1] * psi[1] + for j in eachindex(psi)[2:end] + phiJdagKpsi = phiJdagKpsi * phidag[j] * Jdag[j] * K[j] * psi[j] + end + + @test phiJdagKpsi[] ≈ inner(J, phi, K, psi) + + badsites = [Index(2, "Site") for n in 1:(N + 1)] + badpsi = random_mps(badsites) + @test_throws DimensionMismatch inner(J, phi, K, badpsi) + end + + @testset "error_contract" begin + dist = sqrt( + abs(1 + (inner(phi, phi) - 2 * real(inner(phi', K, psi))) / inner(K, psi, K, psi)) + ) + @test dist ≈ error_contract(phi, K, psi) + end + + @testset "contract" begin + @test maxlinkdim(K) == 1 + psi_out = contract(K, psi; maxdim=1) + @test inner(phi', psi_out) ≈ inner(phi', K, psi) + @test_throws MethodError contract(K, psi; method="fakemethod") + end + + # TODO: implement add for QN MPOs and add this test back + #@testset "add(::MPO, ::MPO)" begin + # shsites = siteinds("S=1/2", N) + # M = add(K, L) + # @test length(M) == N + # k_psi = contract(K, psi, maxdim=1) + # l_psi = contract(L, psi, maxdim=1) + # @test inner(psi', k_psi + l_psi) ≈ ⋅(psi', M, psi) atol=5e-3 + # @test inner(psi', sum([k_psi, l_psi])) ≈ dot(psi', M, psi) atol=5e-3 + # for dim in 2:4 + # shsites = siteinds("S=1/2",N) + # K = basicRandomMPO(N, shsites; dim=dim) + # L = basicRandomMPO(N, shsites; dim=dim) + # M = K + L + # @test length(M) == N + # psi = random_mps(shsites) + # k_psi = contract(K, psi) + # l_psi = contract(L, psi) + # @test inner(psi, k_psi + l_psi) ≈ dot(psi, M, psi) atol=5e-3 + # @test inner(psi, sum([k_psi, l_psi])) ≈ inner(psi, M, psi) atol=5e-3 + # psi = random_mps(shsites) + # M = add(K, L; cutoff=1E-9) + # k_psi = contract(K, psi) + # l_psi = contract(L, psi) + # @test inner(psi, k_psi + l_psi) ≈ inner(psi, M, psi) atol=5e-3 + # end + #end + + @testset "contract(::MPO, ::MPO)" begin + @test maxlinkdim(K) == 1 + @test maxlinkdim(L) == 1 + KL = contract(prime(K), L; maxdim=1) + Lpsi = contract(L, psi; maxdim=1) + psi_kl_out = contract(prime(K), Lpsi; maxdim=1) + @test inner(psi'', KL, psi) ≈ inner(psi'', psi_kl_out) atol = 5e-3 + end + + @testset "contract(::MPO, ::MPO) without truncation" begin + s = siteinds("Electron", 10; conserve_qns=true) + j1, j2 = 2, 4 + Cdagup = op_mpo(s, "Cdagup", j1) + Cdagdn = op_mpo(s, "Cdagdn", j2) + Cdagmpo = apply(Cdagup, Cdagdn; alg="naive", truncate=false) + @test norm(Cdagmpo) ≈ 2^length(s) / 2 + for j in 1:length(s) + if (j == j1) || (j == j2) + @test norm(Cdagmpo[j]) ≈ √2 + else + @test norm(Cdagmpo[j]) ≈ 2 + end + end + end + + @testset "*(::MPO, ::MPO)" begin + @test maxlinkdim(K) == 1 + @test maxlinkdim(L) == 1 + KL = *(prime(K), L; maxdim=1) + psi_kl_out = *(prime(K), *(L, psi; maxdim=1); maxdim=1) + @test ⋅(psi'', KL, psi) ≈ dot(psi'', psi_kl_out) atol = 5e-3 + end + + sites = siteinds("S=1/2", N) + O = MPO(sites, "Sz") + @test length(O) == N # just make sure this works + + @test_throws ArgumentError random_mpo(sites, 2) + @test isnothing(linkind(MPO(fill(ITensor(), N), 0, N + 1), 1)) +end + +@testset "splitblocks" begin + N = 4 + sites = siteinds("S=1", N; conserve_qns=true) + opsum = OpSum() + for j in 1:(N - 1) + opsum .+= 0.5, "S+", j, "S-", j + 1 + opsum .+= 0.5, "S-", j, "S+", j + 1 + opsum .+= "Sz", j, "Sz", j + 1 + end + H = MPO(opsum, sites; splitblocks=false) + + # Split the tensors to make them more sparse + # Drops zero blocks by default + H̃ = splitblocks(linkinds, H) + + H̃2 = MPO(opsum, sites; splitblocks=true) + + # Defaults to true + H̃3 = MPO(opsum, sites) + + @test prod(H) ≈ prod(H̃) + @test prod(H) ≈ prod(H̃2) + @test prod(H) ≈ prod(H̃3) + + @test nnz(H[1]) == 9 + @test nnz(H[2]) == 18 + @test nnz(H[3]) == 18 + @test nnz(H[4]) == 9 + + @test nnzblocks(H[1]) == 9 + @test nnzblocks(H[2]) == 18 + @test nnzblocks(H[3]) == 18 + @test nnzblocks(H[4]) == 9 + + @test nnz(H̃[1]) == nnzblocks(H̃[1]) == count(≠(0), H[1]) == count(≠(0), H̃[1]) == 9 + @test nnz(H̃[2]) == nnzblocks(H̃[2]) == count(≠(0), H[2]) == count(≠(0), H̃[2]) == 18 + @test nnz(H̃[3]) == nnzblocks(H̃[3]) == count(≠(0), H[3]) == count(≠(0), H̃[3]) == 18 + @test nnz(H̃[4]) == nnzblocks(H̃[4]) == count(≠(0), H[4]) == count(≠(0), H̃[4]) == 9 + + @test nnz(H̃2[1]) == nnzblocks(H̃2[1]) == count(≠(0), H[1]) == count(≠(0), H̃2[1]) == 9 + @test nnz(H̃2[2]) == nnzblocks(H̃2[2]) == count(≠(0), H[2]) == count(≠(0), H̃2[2]) == 18 + @test nnz(H̃2[3]) == nnzblocks(H̃2[3]) == count(≠(0), H[3]) == count(≠(0), H̃2[3]) == 18 + @test nnz(H̃2[4]) == nnzblocks(H̃2[4]) == count(≠(0), H[4]) == count(≠(0), H̃2[4]) == 9 + + @test nnz(H̃3[1]) == nnzblocks(H̃3[1]) == count(≠(0), H[1]) == count(≠(0), H̃3[1]) == 9 + @test nnz(H̃3[2]) == nnzblocks(H̃3[2]) == count(≠(0), H[2]) == count(≠(0), H̃3[2]) == 18 + @test nnz(H̃3[3]) == nnzblocks(H̃3[3]) == count(≠(0), H[3]) == count(≠(0), H̃3[3]) == 18 + @test nnz(H̃3[4]) == nnzblocks(H̃3[4]) == count(≠(0), H[4]) == count(≠(0), H̃3[4]) == 9 +end + +@testset "MPO operations with one or two sites" begin + for N in 1:4, conserve_szparity in (true, false) + s = siteinds("S=1/2", N; conserve_szparity=conserve_szparity) + a = OpSum() + h = 0.5 + for j in 1:(N - 1) + a .-= 1, "Sx", j, "Sx", j + 1 + end + for j in 1:N + a .+= h, "Sz", j + end + H = MPO(a, s) + if conserve_szparity + ψ = random_mps(s, n -> isodd(n) ? "↑" : "↓") + else + ψ = random_mps(s) + end + + # MPO * MPS + Hψ = H * ψ + @test prod(Hψ) ≈ prod(H) * prod(ψ) + + # MPO * MPO + H² = H' * H + @test prod(H²) ≈ prod(H') * prod(H) + + # DMRG + sweeps = Sweeps(3) + maxdim!(sweeps, 10) + if N == 1 + @test_throws ErrorException dmrg( + H, ψ, sweeps; eigsolve_maxiter=10, eigsolve_krylovdim=10, outputlevel=0 + ) + else + e, ψgs = dmrg(H, ψ, sweeps; eigsolve_maxiter=10, eigsolve_krylovdim=10, outputlevel=0) + @test prod(H) * prod(ψgs) ≈ e * prod(ψgs)' + D, V = eigen(prod(H); ishermitian=true) + if hasqns(ψ) + fluxψ = flux(ψ) + d = commonind(D, V) + b = ITensors.findfirstblock(indblock -> ITensors.qn(indblock) == fluxψ, d) + @test e ≈ minimum(storage(D[Block(b, b)])) + else + @test e ≈ minimum(storage(D)) + end + end + end +end + +# +# Build up Hamiltonians with non trival QN spaces in the link indices and further neighbour interactions. +# +function make_heisenberg_opsum(sites, NNN::Int64; J::Float64=1.0, kwargs...)::MPO + N = length(sites) + @assert N >= NNN + opsum = OpSum() + for dj in 1:NNN + f = J / dj + for j in 1:(N - dj) + add!(opsum, f, "Sz", j, "Sz", j + dj) + add!(opsum, f * 0.5, "S+", j, "S-", j + dj) + add!(opsum, f * 0.5, "S-", j, "S+", j + dj) + end + end + return MPO(opsum, sites; kwargs...) +end + +function make_hubbard_opsum( + sites, NNN::Int64; U::Float64=1.0, t::Float64=1.0, V::Float64=0.5, kwargs... +)::MPO + N = length(sites) + @assert(N >= NNN) + os = OpSum() + for i in 1:N + os += (U, "Nupdn", i) + end + for dn in 1:NNN + tj, Vj = t / dn, V / dn + for n in 1:(N - dn) + os -= tj, "Cdagup", n, "Cup", n + dn + os -= tj, "Cdagup", n + dn, "Cup", n + os -= tj, "Cdagdn", n, "Cdn", n + dn + os -= tj, "Cdagdn", n + dn, "Cdn", n + os += Vj, "Ntot", n, "Ntot", n + dn + end + end + return MPO(os, sites; kwargs...) +end + +test_combos = [(make_heisenberg_opsum, "S=1/2"), (make_hubbard_opsum, "Electron")] + +@testset "QR/QL MPO tensors with complex block structures, H=$(test_combo[1])" for test_combo in + test_combos + N, NNN = 10, 7 #10 lattice site, up 7th neight interactions + sites = siteinds(test_combo[2], N; conserve_qns=true) + H = test_combo[1](sites, NNN) + for n in 1:(N - 1) + W = H[n] + @test flux(W) == QN("Sz", 0) + ilr = filterinds(W; tags="l=$n")[1] + ilq = noncommoninds(W, ilr) + Q, R, q = qr(W, ilq) + @test flux(Q) == QN("Sz", 0) #qr should move all flux on W (0 in this case) onto R + @test flux(R) == QN("Sz", 0) #this effectively removes all flux between Q and R in thie case. + @test W ≈ Q * R atol = 1e-13 + # blocksparse - diag is not supported so we must convert Q*Q_dagger to dense. + # Also fails with error in permutedims so below we use norm(a-b)≈ 0.0 instead. + # @test dense(Q*dag(prime(Q, q))) ≈ δ(Float64, q, q') atol = 1e-13 + @test norm(dense(Q * dag(prime(Q, q))) - δ(Float64, q, q')) ≈ 0.0 atol = 1e-13 + + R, Q, q = ITensors.rq(W, ilr) + @test flux(Q) == QN("Sz", 0) + @test flux(R) == QN("Sz", 0) + @test W ≈ Q * R atol = 1e-13 + @test norm(dense(Q * dag(prime(Q, q))) - δ(Float64, q, q')) ≈ 0.0 atol = 1e-13 + + Q, L, q = ITensors.ql(W, ilq) + @test flux(Q) == QN("Sz", 0) + @test flux(L) == QN("Sz", 0) + @test W ≈ Q * L atol = 1e-13 + @test norm(dense(Q * dag(prime(Q, q))) - δ(Float64, q, q')) ≈ 0.0 atol = 1e-13 + + L, Q, q = ITensors.lq(W, ilr) + @test flux(Q) == QN("Sz", 0) + @test flux(L) == QN("Sz", 0) + @test W ≈ Q * L atol = 1e-13 + @test norm(dense(Q * dag(prime(Q, q))) - δ(Float64, q, q')) ≈ 0.0 atol = 1e-13 + end +end diff --git a/test/base/test_readme.jl b/test/base/test_readme.jl new file mode 100644 index 0000000..e3b8b94 --- /dev/null +++ b/test/base/test_readme.jl @@ -0,0 +1,99 @@ +using ITensorMPS, ITensors, Test + +@testset "README Examples" begin + @testset "ITensor Basics" begin + i = Index(3) + j = Index(5) + k = Index(2) + l = Index(7) + + A = ITensor(i, j, k) + B = ITensor(j, l) + + A[i => 1, j => 1, k => 1] = 11.1 + A[i => 2, j => 1, k => 2] = -21.2 + A[k => 1, i => 3, j => 1] = 31.1 # can provide Index values in any order + # ... + + # Contract over shared index j + C = A * B + + @test hasinds(C, i, k, l) == true + + D = random_itensor(k, j, i) # ITensor with random elements + + # Add two ITensors + # must have same set of indices + # but can be in any order + R = A + D + end + + @testset "SVD of a Matrix" begin + i = Index(10) + j = Index(20) + M = random_itensor(i, j) + U, S, V = svd(M, i) + @test norm(M - U * S * V) < 1E-12 + end + + @testset "SVD of a Tensor" begin + i = Index(4, "i") + j = Index(4, "j") + k = Index(4, "k") + l = Index(4, "l") + T = random_itensor(i, j, k, l) + U, S, V = svd(T, i, k) + @test hasinds(U, i, k) + @test hasinds(V, j, l) + @test norm(T - U * S * V) < 1E-12 + end + + @testset "Making Tensor Indices" begin + i = Index(3) # Index of dimension 3 + @test dim(i) == 3 # dim(i) = 3 + + ci = copy(i) + @test ci == i # true + + j = Index(5, "j") # Index with a tag "j" + + @test j != i # false + + s = Index(2, "n=1,Site") # Index with two tags, + # "Site" and "n=1" + @test hastags(s, "Site") # hastags(s,"Site") = true + @test hastags(s, "n=1") # hastags(s,"n=1") = true + + i1 = prime(i) # i1 has a "prime level" of 1 + # but otherwise same properties as i + @test i1 != i # false, prime levels do not match + end + + @testset "DMRG" begin + N = 100 + sites = siteinds("S=1", N) + + # Input operator terms which define + # a Hamiltonian matrix, and convert + # these terms to an MPO tensor network + opsum = OpSum() + for j in 1:(N - 1) + add!(opsum, "Sz", j, "Sz", j + 1) + add!(opsum, 0.5, "S+", j, "S-", j + 1) + add!(opsum, 0.5, "S-", j, "S+", j + 1) + end + H = MPO(opsum, sites) + + # Create an initial random matrix product state + psi0 = random_mps(sites) + + sweeps = Sweeps(2) + maxdim!(sweeps, 10, 20, 100, 100, 200) + cutoff!(sweeps, 1E-10) + + # Run the DMRG algorithm, returning energy + # (dominant eigenvalue) and optimized MPS + energy, psi = dmrg(H, psi0, sweeps; outputlevel=0) + #println("Final energy = $energy") + end +end diff --git a/test/base/test_readwrite.jl b/test/base/test_readwrite.jl new file mode 100644 index 0000000..74679c0 --- /dev/null +++ b/test/base/test_readwrite.jl @@ -0,0 +1,40 @@ +@eval module $(gensym()) +using ITensorMPS, ITensors, HDF5, Test + +include(joinpath(@__DIR__, "utils", "util.jl")) + +@testset "HDF5 Read and Write" begin + @testset "MPO/MPS" begin + N = 6 + sites = siteinds("S=1/2", N) + + # MPO + mpo = makeRandomMPO(sites) + + h5open("data.h5", "w") do fo + write(fo, "mpo", mpo) + end + + h5open("data.h5", "r") do fi + rmpo = read(fi, "mpo", MPO) + @test prod([norm(rmpo[i] - mpo[i]) / norm(mpo[i]) < 1E-10 for i in 1:N]) + end + + # MPS + mps = makeRandomMPS(sites) + h5open("data.h5", "w") do fo + write(fo, "mps", mps) + end + + h5open("data.h5", "r") do fi + rmps = read(fi, "mps", MPS) + @test prod([norm(rmps[i] - mps[i]) / norm(mps[i]) < 1E-10 for i in 1:N]) + end + end + + # + # Clean up the test hdf5 file + # + rm("data.h5"; force=true) +end +end diff --git a/test/base/test_solvers/Project.toml b/test/base/test_solvers/Project.toml new file mode 100644 index 0000000..99a05ad --- /dev/null +++ b/test/base/test_solvers/Project.toml @@ -0,0 +1,13 @@ +[deps] +Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" +ITensorMPS = "0d1a4710-d33b-49a5-8f18-73bdf49b47e2" +ITensors = "9136182c-28ba-11e9-034c-db9fb085ebd5" +KrylovKit = "0b1a1467-8014-51b9-945f-bf0ae24f4b77" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Observers = "338f10d5-c7f1-4033-a7d1-f9dec39bcaa0" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/base/test_solvers/runtests.jl b/test/base/test_solvers/runtests.jl new file mode 100644 index 0000000..e6e99d3 --- /dev/null +++ b/test/base/test_solvers/runtests.jl @@ -0,0 +1,14 @@ +@eval module $(gensym()) +using Test: @testset +using ITensorMPS: ITensorMPS +test_path = @__DIR__ +test_files = filter(readdir(test_path)) do file + return startswith("test_")(file) && endswith(".jl")(file) +end +@testset "$test_path" begin + @testset "$filename" for filename in test_files + println("Running $filename") + @time include(joinpath(test_path, filename)) + end +end +end diff --git a/test/base/test_solvers/test_contract.jl b/test/base/test_solvers/test_contract.jl new file mode 100644 index 0000000..8fc0390 --- /dev/null +++ b/test/base/test_solvers/test_contract.jl @@ -0,0 +1,65 @@ +@eval module $(gensym()) +using ITensors: ITensors, dag, delta, denseblocks +using ITensorMPS: MPO, OpSum, apply, contract, inner, random_mps, siteinds, truncate! +using StableRNGs: StableRNG +using Test: @test, @test_throws, @testset +@testset "Contract MPO (eltype=$elt, conserve_qns=$conserve_qns)" for elt in ( + Float32, Float64, Complex{Float32}, Complex{Float64} + ), + conserve_qns in [false, true] + + N = 20 + s = siteinds("S=1/2", N; conserve_qns) + rng = StableRNG(1234) + psi = random_mps(rng, elt, s, j -> isodd(j) ? "↑" : "↓"; linkdims=8) + os = OpSum() + for j in 1:(N - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + for j in 1:(N - 2) + os += 0.5, "S+", j, "S-", j + 2 + os += 0.5, "S-", j, "S+", j + 2 + os += "Sz", j, "Sz", j + 2 + end + H = MPO(elt, os, s) + @testset "apply (standard indices, nsite=2)" begin + Hpsi = apply(H, psi; alg="fit", nsweeps=2) + @test_throws ErrorException apply(H, psi; alg="fit") + @test ITensors.scalartype(Hpsi) == elt + @test inner(psi, Hpsi) ≈ inner(psi', H, psi) rtol = 10 * √eps(real(elt)) + end + @testset "contract (non-standard indices)" begin + # Change "top" indices of MPO to be a different set + t = siteinds("S=1/2", N; conserve_qns) + Ht = deepcopy(H) + psit = deepcopy(psi) + for j in 1:N + Ht[j] *= delta(elt, dag(s[j])', t[j]) + psit[j] *= delta(elt, dag(s[j]), t[j]) + end + + # Test with nsweeps=2 + Hpsit = contract(Ht, psi; alg="fit", nsweeps=2) + @test ITensors.scalartype(Hpsit) == elt + @test inner(psit, Hpsit) ≈ inner(psit, Ht, psi) rtol = 10 * √eps(real(elt)) + + # Test with less good initial guess MPS not equal to psi + psit_guess = copy(psit) + truncate!(psit_guess; maxdim=2) + Hpsit = contract(Ht, psi; alg="fit", nsweeps=4, init=psit_guess) + @test ITensors.scalartype(Hpsit) == elt + @test inner(psit, Hpsit) ≈ inner(psit, Ht, psi) rtol = 20 * √eps(real(elt)) + end + @testset "apply (standard indices, nsite=1)" begin + # Test with nsite=1 + Hpsi_guess = apply(H, psi; alg="naive", cutoff=1e-4) + Hpsi = apply(H, psi; alg="fit", init=Hpsi_guess, nsite=1, nsweeps=2) + @test ITensors.scalartype(Hpsi) == elt + scale(::Type{Float32}) = 10^2 + scale(::Type{Float64}) = 10^6 + @test inner(psi, Hpsi) ≈ inner(psi', H, psi) rtol = √eps(real(elt)) * scale(real(elt)) + end +end +end diff --git a/test/base/test_solvers/test_dmrg.jl b/test/base/test_solvers/test_dmrg.jl new file mode 100644 index 0000000..d48afb6 --- /dev/null +++ b/test/base/test_solvers/test_dmrg.jl @@ -0,0 +1,37 @@ +@eval module $(gensym()) +using ITensors: ITensors +using ITensorMPS: Experimental, MPO, OpSum, dmrg, inner, random_mps, siteinds +using StableRNGs: StableRNG +using Test: @test, @test_throws, @testset +@testset "DMRG (eltype=$elt, nsite=$nsite, conserve_qns=$conserve_qns)" for elt in ( + Float32, Float64, Complex{Float32}, Complex{Float64} + ), + nsite in [1, 2], + conserve_qns in [false, true] + + N = 10 + cutoff = eps(real(elt)) * 10^4 + s = siteinds("S=1/2", N; conserve_qns) + os = OpSum() + for j in 1:(N - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + H = MPO(elt, os, s) + rng = StableRNG(1234) + psi = random_mps(rng, elt, s, j -> isodd(j) ? "↑" : "↓"; linkdims=20) + nsweeps = 10 + maxdim = [10, 20, 40, 100] + @test_throws ErrorException Experimental.dmrg(H, psi; maxdim, cutoff, nsite) + e, psi = Experimental.dmrg( + H, psi; nsweeps, maxdim, cutoff, nsite, updater_kwargs=(; krylovdim=3, maxiter=1) + ) + @test inner(psi', H, psi) ≈ e + e2, psi2 = dmrg(H, psi; nsweeps, maxdim, cutoff, outputlevel=0) + @test ITensors.scalartype(psi2) == elt + @test e2 isa real(elt) + @test e ≈ e2 rtol = √(eps(real(elt))) * 10 + @test inner(psi', H, psi) ≈ inner(psi2', H, psi2) rtol = √(eps(real(elt))) * 10 +end +end diff --git a/test/base/test_solvers/test_dmrg_x.jl b/test/base/test_solvers/test_dmrg_x.jl new file mode 100644 index 0000000..ee6b015 --- /dev/null +++ b/test/base/test_solvers/test_dmrg_x.jl @@ -0,0 +1,55 @@ +@eval module $(gensym()) +using ITensors: ITensors +using ITensorMPS: MPO, MPS, OpSum, ProjMPO, dmrg_x, inner, siteinds +using Random: Random +using StableRNGs: StableRNG +using Test: @test, @test_throws, @testset +@testset "DMRG-X (eltype=$elt, conserve_qns=$conserve_qns)" for elt in ( + Float32, Float64, Complex{Float32}, Complex{Float64} + ), + conserve_qns in [false, true] + + function heisenberg(n; h=zeros(n)) + os = OpSum() + for j in 1:(n - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + for j in 1:n + if h[j] ≠ 0 + os -= h[j], "Sz", j + end + end + return os + end + n = 10 + s = siteinds("S=1/2", n; conserve_qns) + Random.seed!(12) + W = 12 + # Random fields h ∈ [-W, W] + rng = StableRNG(1234) + h = W * (2 * rand(rng, real(elt), n) .- 1) + H = MPO(elt, heisenberg(n; h), s) + initstate = rand(rng, ["↑", "↓"], n) + ψ = MPS(elt, s, initstate) + @test_throws ErrorException dmrg_x(H, ψ; nsite=2, maxdim=20, cutoff=1e-10) + dmrg_x_kwargs = (; nsweeps=20, normalize=true, maxdim=20, cutoff=1e-10, outputlevel=0) + e, ϕ = dmrg_x(H, ψ; nsite=2, dmrg_x_kwargs...) + @test ITensors.scalartype(ϕ) == elt + @test inner(ϕ', H, ϕ) / inner(ϕ, ϕ) ≈ e + @test inner(H, ψ, H, ψ) ≉ inner(ψ', H, ψ)^2 rtol = √eps(real(elt)) + @test inner(ψ', H, ψ) / inner(ψ, ψ) ≈ inner(ϕ', H, ϕ) / inner(ϕ, ϕ) rtol = 1e-1 + @test inner(H, ϕ, H, ϕ) ≈ inner(ϕ', H, ϕ)^2 rtol = √eps(real(elt)) + ẽ, ϕ̃ = dmrg_x(ProjMPO(H), ϕ; nsite=1, dmrg_x_kwargs...) + @test ITensors.scalartype(ϕ̃) == elt + @test inner(ϕ̃', H, ϕ̃) / inner(ϕ̃, ϕ̃) ≈ ẽ + @test inner(ψ', H, ψ) / inner(ψ, ψ) ≈ inner(ϕ̃', H, ϕ̃) / inner(ϕ̃, ϕ̃) rtol = 1e-1 + scale(::Type{Float32}) = 10^2 + scale(::Type{Float64}) = 10^5 + @test inner(H, ϕ̃, H, ϕ̃) ≈ inner(ϕ̃', H, ϕ̃)^2 rtol = + √(eps(real(elt))) * scale(real(elt)) + # Sometimes broken, sometimes not + # @test abs(loginner(ϕ̃, ϕ) / n) ≈ 0.0 atol = 1e-6 +end +end diff --git a/test/test_examples.jl b/test/base/test_solvers/test_examples.jl similarity index 85% rename from test/test_examples.jl rename to test/base/test_solvers/test_examples.jl index 3e0f481..a497f54 100644 --- a/test/test_examples.jl +++ b/test/base/test_solvers/test_examples.jl @@ -1,12 +1,12 @@ @eval module $(gensym()) -using Suppressor: @suppress using ITensorMPS: ITensorMPS +using Suppressor: @suppress using Test: @testset @testset "Run examples" begin examples_files = [ "01_tdvp.jl", "02_dmrg-x.jl", "03_tdvp_time_dependent.jl", "04_tdvp_observers.jl" ] - examples_path = joinpath(pkgdir(ITensorMPS), "examples") + examples_path = joinpath(pkgdir(ITensorMPS), "examples", "solvers") @testset "Running example file $f" for f in examples_files println("Running example file $f") @suppress include(joinpath(examples_path, f)) diff --git a/test/base/test_solvers/test_expand.jl b/test/base/test_solvers/test_expand.jl new file mode 100644 index 0000000..9a7b477 --- /dev/null +++ b/test/base/test_solvers/test_expand.jl @@ -0,0 +1,95 @@ +@eval module $(gensym()) +using ITensors: scalartype +using ITensorMPS: + OpSum, MPO, MPS, expand, inner, linkdims, maxlinkdim, random_mps, siteinds, tdvp +using ITensorMPS.Experimental: dmrg +using LinearAlgebra: normalize +using StableRNGs: StableRNG +using Test: @test, @testset +const elts = (Float32, Float64, Complex{Float32}, Complex{Float64}) +@testset "expand (eltype=$elt)" for elt in elts + @testset "expand (alg=\"orthogonalize\", conserve_qns=$conserve_qns, eltype=$elt)" for conserve_qns in + ( + false, true + ) + n = 6 + s = siteinds("S=1/2", n; conserve_qns) + rng = StableRNG(1234) + state = random_mps(rng, elt, s, j -> isodd(j) ? "↑" : "↓"; linkdims=4) + reference = random_mps(rng, elt, s, j -> isodd(j) ? "↑" : "↓"; linkdims=2) + state_expanded = expand(state, [reference]; alg="orthogonalize") + @test scalartype(state_expanded) === elt + @test inner(state_expanded, state) ≈ inner(state, state) + @test inner(state_expanded, reference) ≈ inner(state, reference) + end + @testset "expand (alg=\"global_krylov\", conserve_qns=$conserve_qns, eltype=$elt)" for conserve_qns in + ( + false, true + ) + n = 10 + s = siteinds("S=1/2", n; conserve_qns) + opsum = OpSum() + for j in 1:(n - 1) + opsum += 0.5, "S+", j, "S-", j + 1 + opsum += 0.5, "S-", j, "S+", j + 1 + opsum += "Sz", j, "Sz", j + 1 + end + operator = MPO(elt, opsum, s) + state = MPS(elt, s, j -> isodd(j) ? "↑" : "↓") + state_expanded = expand(state, operator; alg="global_krylov") + @test scalartype(state_expanded) === elt + @test maxlinkdim(state_expanded) > 1 + @test inner(state_expanded, state) ≈ inner(state, state) + end + @testset "Decoupled ladder (alg=\"global_krylov\", eltype=$elt)" begin + nx = 10 + ny = 2 + n = nx * ny + s = siteinds("S=1/2", n) + opsum = OpSum() + for j in 1:2:(n - 2) + opsum += 1 / 2, "S+", j, "S-", j + 2 + opsum += 1 / 2, "S-", j, "S+", j + 2 + opsum += "Sz", j, "Sz", j + 2 + end + for j in 2:2:(n - 2) + opsum += 1 / 2, "S+", j, "S-", j + 2 + opsum += 1 / 2, "S-", j, "S+", j + 2 + opsum += "Sz", j, "Sz", j + 2 + end + operator = MPO(elt, opsum, s) + rng = StableRNG(1234) + init = random_mps(rng, elt, s; linkdims=30) + reference_energy, reference_state = dmrg( + operator, + init; + nsweeps=15, + maxdim=[10, 10, 20, 20, 40, 80, 100], + cutoff=(√(eps(real(elt)))), + noise=(√(eps(real(elt)))), + ) + rng = StableRNG(1234) + state = random_mps(rng, elt, s) + nexpansions = 10 + tau = elt(0.5) + for step in 1:nexpansions + # TODO: Use `fourthroot`/`∜` in Julia 1.10 and above. + state = expand( + state, operator; alg="global_krylov", krylovdim=3, cutoff=eps(real(elt))^(1//4) + ) + state = tdvp( + operator, + -4tau, + state; + nsteps=4, + cutoff=1e-5, + updater_kwargs=(; tol=1e-3, krylovdim=5), + ) + state = normalize(state) + end + @test scalartype(state) === elt + # TODO: Use `fourthroot`/`∜` in Julia 1.10 and above. + @test inner(state', operator, state) ≈ reference_energy rtol = 5 * eps(real(elt))^(1//4) + end +end +end diff --git a/test/base/test_solvers/test_linsolve.jl b/test/base/test_solvers/test_linsolve.jl new file mode 100644 index 0000000..1e91592 --- /dev/null +++ b/test/base/test_solvers/test_linsolve.jl @@ -0,0 +1,45 @@ +@eval module $(gensym()) +using ITensors: scalartype +using ITensorMPS: MPO, OpSum, apply, random_mps, siteinds +using ITensorMPS.Experimental: dmrg +using KrylovKit: linsolve +using LinearAlgebra: norm +using StableRNGs: StableRNG +using Test: @test, @test_throws, @testset +using Random: Random +@testset "linsolve (eltype=$elt, conserve_qns=$conserve_qns)" for elt in ( + Float32, Float64, Complex{Float32}, Complex{Float64} + ), + conserve_qns in [false, true] + + N = 6 + s = siteinds("S=1/2", N; conserve_qns) + os = OpSum() + for j in 1:(N - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + H = MPO(elt, os, s) + state = [isodd(n) ? "Up" : "Dn" for n in 1:N] + rng = StableRNG(1234) + x_c = random_mps(rng, elt, s, state; linkdims=2) + e, x_c = dmrg(H, x_c; nsweeps=10, cutoff=1e-6, maxdim=20, outputlevel=0) + @test scalartype(x_c) == elt + # Compute `b = H * x_c` + b = apply(H, x_c; cutoff=1e-8) + @test scalartype(b) == elt + # Starting guess + rng = StableRNG(1234) + x0 = x_c + elt(0.05) * random_mps(rng, elt, s, state; linkdims=2) + @test scalartype(x0) == elt + nsweeps = 10 + cutoff = 1e-5 + maxdim = 20 + updater_kwargs = (; tol=1e-4, maxiter=20, krylovdim=30, ishermitian=true) + @test_throws ErrorException linsolve(H, b, x0; cutoff, maxdim, updater_kwargs) + x = linsolve(H, b, x0; nsweeps, cutoff, maxdim, updater_kwargs) + @test scalartype(x) == elt + @test norm(x - x_c) < 1e-2 +end +end diff --git a/test/base/test_solvers/test_tdvp.jl b/test/base/test_solvers/test_tdvp.jl new file mode 100644 index 0000000..bf57e4d --- /dev/null +++ b/test/base/test_solvers/test_tdvp.jl @@ -0,0 +1,336 @@ +@eval module $(gensym()) +using ITensorMPS: + AbstractObserver, MPO, MPS, OpSum, apply, expect, inner, random_mps, siteinds, tdvp +using ITensors: ITensors, ITensor, dag, noprime, op, prime, scalar +using KrylovKit: exponentiate +using LinearAlgebra: norm +using Observers: observer +using StableRNGs: StableRNG +using Test: @test, @test_throws, @testset +const elts = (Float32, Float64, Complex{Float32}, Complex{Float64}) +@testset "Basic TDVP (eltype=$elt)" for elt in elts + N = 10 + cutoff = eps(real(elt)) * 10^4 + s = siteinds("S=1/2", N) + os = OpSum() + for j in 1:(N - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + H = MPO(elt, os, s) + rng = StableRNG(1234) + ψ0 = random_mps(rng, elt, s; linkdims=10) + time_step = elt(0.1) * im + # Time evolve forward: + ψ1 = tdvp(H, -time_step, ψ0; cutoff, nsite=1) + @test ITensors.scalartype(ψ1) == complex(elt) + #Different backend updaters, default updater_backend = "exponentiate" + @test ψ1 ≈ tdvp(H, -time_step, ψ0; cutoff, nsite=1, updater_backend="applyexp") + @test norm(ψ1) ≈ 1 rtol = √eps(real(elt)) * 10 + ## Should lose fidelity: + #@test abs(inner(ψ0,ψ1)) < 0.9 + # Average energy should be conserved: + @test real(inner(ψ1', H, ψ1)) ≈ inner(ψ0', H, ψ0) rtol = √eps(real(elt)) * 10 + # Time evolve backwards: + ψ2 = tdvp(H, time_step, ψ1; cutoff) + @test ITensors.scalartype(ψ2) == complex(elt) + @test norm(ψ2) ≈ 1 rtol = √eps(real(elt)) * 10 + # Should rotate back to original state: + @test abs(inner(ψ0, ψ2)) > 0.99 +end + +@testset "TDVP: Sum of Hamiltonians (eltype=$elt)" for elt in elts + N = 10 + cutoff = 1e-10 + + s = siteinds("S=1/2", N) + + os1 = OpSum() + for j in 1:(N - 1) + os1 += 0.5, "S+", j, "S-", j + 1 + os1 += 0.5, "S-", j, "S+", j + 1 + end + os2 = OpSum() + for j in 1:(N - 1) + os2 += "Sz", j, "Sz", j + 1 + end + H1 = MPO(elt, os1, s) + H2 = MPO(elt, os2, s) + Hs = [H1, H2] + rng = StableRNG(1234) + ψ0 = random_mps(rng, elt, s; linkdims=10) + ψ1 = tdvp(Hs, -elt(0.1) * im, ψ0; cutoff, nsite=1) + @test ITensors.scalartype(ψ1) === complex(elt) + @test norm(ψ1) ≈ 1 rtol = √eps(real(elt)) + ## Should lose fidelity: + #@test abs(inner(ψ0,ψ1)) < 0.9 + # Average energy should be conserved: + @test real(sum(H -> inner(ψ1', H, ψ1), Hs)) ≈ sum(H -> inner(ψ0', H, ψ0), Hs) rtol = + 4 * √eps(real(elt)) + # Time evolve backwards: + ψ2 = tdvp(Hs, elt(0.1) * im, ψ1; cutoff) + @test ITensors.scalartype(ψ2) === complex(elt) + @test norm(ψ2) ≈ 1 rtol = √eps(real(elt)) + # Should rotate back to original state: + @test abs(inner(ψ0, ψ2)) > 0.99 +end +@testset "Custom updater in TDVP" begin + N = 10 + cutoff = 1e-12 + s = siteinds("S=1/2", N) + os = OpSum() + for j in 1:(N - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + H = MPO(os, s) + rng = StableRNG(1234) + ψ0 = random_mps(rng, s; linkdims=10) + function updater(PH, state0; internal_kwargs, kwargs...) + return exponentiate(PH, internal_kwargs.time_step, state0; kwargs...) + end + updater_kwargs = (; + ishermitian=true, tol=1e-12, krylovdim=30, maxiter=100, verbosity=0, eager=true + ) + t = -0.1im + ψ1 = tdvp(H, t, ψ0; updater, updater_kwargs, cutoff, nsite=1) + @test norm(ψ1) ≈ 1 + ## Should lose fidelity: + #@test abs(inner(ψ0,ψ1)) < 0.9 + # Average energy should be conserved: + @test real(inner(ψ1', H, ψ1)) ≈ inner(ψ0', H, ψ0) + # Time evolve backwards: + ψ2 = tdvp(H, +0.1im, ψ1; cutoff) + @test norm(ψ2) ≈ 1 + # Should rotate back to original state: + @test abs(inner(ψ0, ψ2)) > 0.99 +end +@testset "Accuracy Test" begin + N = 4 + tau = 0.1 + ttotal = 1.0 + cutoff = 1e-12 + s = siteinds("S=1/2", N; conserve_qns=false) + os = OpSum() + for j in 1:(N - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + H = MPO(os, s) + HM = prod(H) + Ut = exp(-im * tau * HM) + state = MPS(s, n -> isodd(n) ? "Up" : "Dn") + state2 = deepcopy(state) + statex = prod(state) + Sz_tdvp = Float64[] + Sz_tdvp2 = Float64[] + Sz_exact = Float64[] + c = div(N, 2) + Szc = op("Sz", s[c]) + Nsteps = Int(ttotal / tau) + for step in 1:Nsteps + statex = noprime(Ut * statex) + statex /= norm(statex) + + state = tdvp( + H, + -im * tau, + state; + cutoff, + normalize=false, + updater_kwargs=(; tol=1e-12, maxiter=500, krylovdim=25), + ) + push!(Sz_tdvp, real(expect(state, "Sz"; sites=c:c)[1])) + state2 = tdvp( + H, + -im * tau, + state2; + cutoff, + normalize=false, + updater_kwargs=(; tol=1e-12, maxiter=500, krylovdim=25), + ) + push!(Sz_tdvp2, real(expect(state2, "Sz"; sites=c:c)[1])) + push!(Sz_exact, real(scalar(dag(prime(statex, s[c])) * Szc * statex))) + F = abs(scalar(dag(statex) * prod(state))) + end + @test norm(Sz_tdvp - Sz_exact) < 1e-5 + @test norm(Sz_tdvp2 - Sz_exact) < 1e-5 +end +@testset "TEBD Comparison" begin + N = 10 + cutoff = 1e-12 + tau = 0.1 + ttotal = 1.0 + s = siteinds("S=1/2", N; conserve_qns=true) + os = OpSum() + for j in 1:(N - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + H = MPO(os, s) + gates = ITensor[] + for j in 1:(N - 1) + s1 = s[j] + s2 = s[j + 1] + hj = + op("Sz", s1) * op("Sz", s2) + + 1 / 2 * op("S+", s1) * op("S-", s2) + + 1 / 2 * op("S-", s1) * op("S+", s2) + Gj = exp(-1.0im * tau / 2 * hj) + push!(gates, Gj) + end + append!(gates, reverse(gates)) + state = MPS(s, n -> isodd(n) ? "Up" : "Dn") + phi = deepcopy(state) + c = div(N, 2) + # Evolve using TEBD + Nsteps = convert(Int, ceil(abs(ttotal / tau))) + Sz1 = zeros(Nsteps) + En1 = zeros(Nsteps) + Sz2 = zeros(Nsteps) + En2 = zeros(Nsteps) + for step in 1:Nsteps + state = apply(gates, state; cutoff) + nsite = (step <= 3 ? 2 : 1) + phi = tdvp( + H, -tau * im, phi; cutoff, nsite, normalize=true, updater_kwargs=(; krylovdim=15) + ) + Sz1[step] = expect(state, "Sz"; sites=c:c)[1] + Sz2[step] = expect(phi, "Sz"; sites=c:c)[1] + En1[step] = real(inner(state', H, state)) + En2[step] = real(inner(phi', H, phi)) + end + # Evolve using TDVP + function measure_sz(; state, bond, half_sweep) + if bond == 1 && half_sweep == 2 + return expect(state, "Sz"; sites=c) + end + return nothing + end + function measure_en(; state, bond, half_sweep) + if bond == 1 && half_sweep == 2 + return real(inner(state', H, state)) + end + return nothing + end + obs = observer("Sz" => measure_sz, "En" => measure_en) + + phi = MPS(s, n -> isodd(n) ? "Up" : "Dn") + phi = tdvp( + H, -im * ttotal, phi; time_step=-im * tau, cutoff, normalize=false, (observer!)=obs + ) + Sz2 = obs.Sz + En2 = obs.En + @test norm(Sz1 - Sz2) < 1e-3 + @test norm(En1 - En2) < 1e-3 +end +@testset "Imaginary Time Evolution" for reverse_step in [true, false] + N = 10 + cutoff = 1e-12 + tau = 1.0 + ttotal = 50.0 + s = siteinds("S=1/2", N) + os = OpSum() + for j in 1:(N - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + H = MPO(os, s) + rng = StableRNG(1234) + state = random_mps(rng, s; linkdims=2) + state2 = deepcopy(state) + trange = 0.0:tau:ttotal + for (step, t) in enumerate(trange) + nsite = (step <= 10 ? 2 : 1) + state = tdvp( + H, + -tau, + state; + cutoff, + nsite, + reverse_step, + normalize=true, + updater_kwargs=(; krylovdim=15), + ) + state2 = tdvp( + H, + -tau, + state2; + cutoff, + nsite, + reverse_step, + normalize=true, + updater_kwargs=(; krylovdim=15), + ) + end + @test state ≈ state2 rtol = 1e-6 + en1 = inner(state', H, state) + en2 = inner(state2', H, state2) + @test en1 < -4.25 + @test en1 ≈ en2 +end +@testset "Observers" begin + N = 10 + cutoff = 1e-12 + tau = 0.1 + ttotal = 1.0 + s = siteinds("S=1/2", N; conserve_qns=true) + os = OpSum() + for j in 1:(N - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + H = MPO(os, s) + c = div(N, 2) + # Using Observers.jl + function measure_sz(; state, bond, half_sweep) + if bond == 1 && half_sweep == 2 + return expect(state, "Sz"; sites=c) + end + return nothing + end + function measure_en(; state, bond, half_sweep) + if bond == 1 && half_sweep == 2 + return real(inner(state', H, state)) + end + return nothing + end + function identity_info(; info) + return info + end + obs = observer("Sz" => measure_sz, "En" => measure_en, "info" => identity_info) + step_measure_sz(; state) = expect(state, "Sz"; sites=c) + step_measure_en(; state) = real(inner(state', H, state)) + step_obs = observer("Sz" => step_measure_sz, "En" => step_measure_en) + state = MPS(s, n -> isodd(n) ? "Up" : "Dn") + tdvp( + H, + -im * ttotal, + state; + time_step=-im * tau, + cutoff, + normalize=false, + (observer!)=obs, + (step_observer!)=step_obs, + ) + Sz = filter(!isnothing, obs.Sz) + En = filter(!isnothing, obs.En) + infos = obs.info + Sz_step = step_obs.Sz + En_step = step_obs.En + @test length(Sz) == 10 + @test length(En) == 10 + @test length(Sz_step) == 10 + @test length(En_step) == 10 + @test Sz ≈ Sz_step + @test En ≈ En_step + @test all(x -> x.info.converged == 1, infos) + @test length(values(infos)) == 180 +end +end diff --git a/test/base/test_solvers/test_tdvp_time_dependent.jl b/test/base/test_solvers/test_tdvp_time_dependent.jl new file mode 100644 index 0000000..5fcc972 --- /dev/null +++ b/test/base/test_solvers/test_tdvp_time_dependent.jl @@ -0,0 +1,118 @@ +@eval module $(gensym()) +using ITensors: ITensors, Index, QN, contract, scalartype +using ITensorMPS: + ITensorMPS, + MPO, + MPS, + ProjMPO, + ProjMPOSum, + TimeDependentSum, + position!, + random_mps, + siteinds, + tdvp +using LinearAlgebra: norm +using StableRNGs: StableRNG +using Test: @test, @test_skip, @testset +include(joinpath(pkgdir(ITensorMPS), "examples", "solvers", "03_models.jl")) +include(joinpath(pkgdir(ITensorMPS), "examples", "solvers", "03_updaters.jl")) +@testset "TDVP with ODE local updater" begin + @testset "TimeDependentSum (eltype=$elt)" for elt in ( + Float32, Float64, Complex{Float32}, Complex{Float64} + ), + conserve_qns in [false, true] + + n = 4 + s = siteinds("S=1/2", 4; conserve_qns) + H = MPO(elt, s, "I") + H⃗ = (H, H) + region = 2:3 + rng = StableRNG(1234) + ψ = random_mps(rng, elt, s, j -> isodd(j) ? "↑" : "↓"; linkdims=2) + H⃗ᵣ = ProjMPO.(H⃗) + map(Hᵣ -> position!(Hᵣ, ψ, first(region)), H⃗ᵣ) + ∑Hᵣ = ProjMPOSum(collect(H⃗)) + position!(∑Hᵣ, ψ, first(region)) + f⃗ₜ = (t -> sin(elt(0.1) * t), t -> cos(elt(0.2) * t)) + α = elt(0.5) + ∑Hₜ = α * TimeDependentSum(f⃗ₜ, ITensors.terms(∑Hᵣ)) + t₀ = elt(0.5) + ∑Hₜ₀ = ∑Hₜ(t₀) + ψᵣ = reduce(*, map(v -> ψ[v], region)) + Hψ = ∑Hₜ₀(ψᵣ) + @test eltype(Hψ) == elt + @test Hψ ≈ sum(i -> α * f⃗ₜ[i](t₀) * H⃗ᵣ[i](ψᵣ), eachindex(H⃗)) + end + @testset "Time dependent Hamiltonian (eltype=$elt, conserve_qns=$conserve_qns)" for elt in + ( + Float32, Float64, Complex{Float32}, Complex{Float64} + ), + conserve_qns in [false, true] + + n = 4 + J₁ = elt(1) + J₂ = elt(0.1) + ω₁ = real(elt)(0.1) + ω₂ = real(elt)(0.2) + ω⃗ = (ω₁, ω₂) + f⃗ = map(ω -> (t -> cos(ω * t)), ω⃗) + time_step = real(elt)(0.1) + time_stop = real(elt)(1) + nsite = 2 + maxdim = 100 + cutoff = √(eps(real(elt))) + tol = √eps(real(elt)) + s = siteinds("S=1/2", n) + ℋ₁₀ = heisenberg(n; J=J₁, J2=zero(elt)) + ℋ₂₀ = heisenberg(n; J=zero(elt), J2=J₂) + ℋ⃗₀ = (ℋ₁₀, ℋ₂₀) + H⃗₀ = map(ℋ₀ -> MPO(elt, ℋ₀, s), ℋ⃗₀) + ψ₀ = complex.(MPS(elt, s, j -> isodd(j) ? "↑" : "↓")) + ψₜ_ode = tdvp( + -im * TimeDependentSum(f⃗, H⃗₀), + time_stop, + ψ₀; + updater=ode_updater, + updater_kwargs=(; reltol=tol, abstol=tol), + time_step, + maxdim, + cutoff, + nsite, + ) + ψₜ_krylov = tdvp( + -im * TimeDependentSum(f⃗, H⃗₀), + time_stop, + ψ₀; + updater=krylov_updater, + updater_kwargs=(; tol, eager=true), + time_step, + maxdim, + cutoff, + nsite, + ) + ψₜ_full, _ = ode_updater( + -im * TimeDependentSum(f⃗, contract.(H⃗₀)), + contract(ψ₀); + internal_kwargs=(; time_step=time_stop), + reltol=tol, + abstol=tol, + ) + + @test scalartype(ψ₀) == complex(elt) + @test scalartype(ψₜ_ode) == complex(elt) + @test scalartype(ψₜ_krylov) == complex(elt) + @test scalartype(ψₜ_full) == complex(elt) + @test norm(ψ₀) ≈ 1 + @test norm(ψₜ_ode) ≈ 1 + @test norm(ψₜ_krylov) ≈ 1 rtol = √(eps(real(elt))) + @test norm(ψₜ_full) ≈ 1 + + ode_err = norm(contract(ψₜ_ode) - ψₜ_full) + krylov_err = norm(contract(ψₜ_krylov) - ψₜ_full) + + @test krylov_err > ode_err + @test ode_err < √(eps(real(elt))) * 10^4 + @test krylov_err < √(eps(real(elt))) * 10^5 + end +end +end diff --git a/test/base/test_sweepnext.jl b/test/base/test_sweepnext.jl new file mode 100644 index 0000000..c74008c --- /dev/null +++ b/test/base/test_sweepnext.jl @@ -0,0 +1,51 @@ +using ITensors, Test + +@testset "sweepnext function" begin + @testset "one site" begin + N = 6 + count = 1 + output = [ + (1, 1), + (2, 1), + (3, 1), + (4, 1), + (5, 1), + (6, 1), + (6, 2), + (5, 2), + (4, 2), + (3, 2), + (2, 2), + (1, 2), + ] + for (b, ha) in sweepnext(N; ncenter=1) + @test (b, ha) == output[count] + count += 1 + end + @test count == 2 * N + 1 + end + + @testset "two site" begin + N = 6 + count = 1 + output = [ + (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (5, 2), (4, 2), (3, 2), (2, 2), (1, 2) + ] + for (b, ha) in sweepnext(N) + @test (b, ha) == output[count] + count += 1 + end + @test count == 2 * (N - 1) + 1 + end + + @testset "three site" begin + N = 6 + count = 1 + output = [(1, 1), (2, 1), (3, 1), (4, 1), (4, 2), (3, 2), (2, 2), (1, 2)] + for (b, ha) in sweepnext(N; ncenter=3) + @test (b, ha) == output[count] + count += 1 + end + @test count == 2 * (N - 2) + 1 + end +end diff --git a/test/base/test_sweeps.jl b/test/base/test_sweeps.jl new file mode 100644 index 0000000..49afcc6 --- /dev/null +++ b/test/base/test_sweeps.jl @@ -0,0 +1,151 @@ +using ITensors +using Test + +@testset "Sweeps constructor" begin + + #Sweeps + #1 cutoff=1.0E-12, maxdim=50, mindim=10, noise=1.0E-07 + #2 cutoff=1.0E-12, maxdim=100, mindim=20, noise=1.0E-08 + #3 cutoff=1.0E-12, maxdim=200, mindim=20, noise=1.0E-10 + #4 cutoff=1.0E-12, maxdim=400, mindim=20, noise=0.0E+00 + #5 cutoff=1.0E-12, maxdim=800, mindim=20, noise=1.0E-11 + #6 cutoff=1.0E-12, maxdim=800, mindim=20, noise=0.0E+00 + + sweep_args = [ + "maxdim" "mindim" "cutoff" "noise" + 50 10 1e-12 1e-7 + 100 20 1e-12 1e-8 + 200 20 1e-12 1e-10 + 400 20 1e-12 0 + 800 20 1e-12 1e-11 + 800 20 1e-12 0 + ] + + @testset "Don't specify nsweep" begin + nsw = size(sweep_args, 1) - 1 + sw = Sweeps(sweep_args) + + @test nsweep(sw) == nsw + + @test maxdim(sw, 1) == 50 + @test maxdim(sw, 2) == 100 + @test maxdim(sw, 3) == 200 + @test maxdim(sw, 4) == 400 + for n in 5:nsw + @test maxdim(sw, n) == 800 + end + + @test mindim(sw, 1) == 10 + for n in 2:nsw + @test mindim(sw, n) == 20 + end + + for n in 1:nsw + @test cutoff(sw, n) == 1e-12 + end + + @test noise(sw, 1) == 1e-7 + @test noise(sw, 2) == 1e-8 + @test noise(sw, 3) == 1e-10 + @test noise(sw, 4) == 0 + @test noise(sw, 5) == 1e-11 + @test noise(sw, 6) == 0 + end + + @testset "Specify nsweep, more than data" begin + nsw = 7 + sw = Sweeps(nsw, sweep_args) + + @test nsweep(sw) == nsw + + @test maxdim(sw, 1) == 50 + @test maxdim(sw, 2) == 100 + @test maxdim(sw, 3) == 200 + @test maxdim(sw, 4) == 400 + for n in 5:nsw + @test maxdim(sw, n) == 800 + end + + @test mindim(sw, 1) == 10 + for n in 2:nsw + @test mindim(sw, n) == 20 + end + + for n in 1:nsw + @test cutoff(sw, n) == 1e-12 + end + + @test noise(sw, 1) == 1e-7 + @test noise(sw, 2) == 1e-8 + @test noise(sw, 3) == 1e-10 + @test noise(sw, 4) == 0 + @test noise(sw, 5) == 1e-11 + @test noise(sw, 6) == 0 + @test noise(sw, 7) == 0 + end + + @testset "Specify nsweep, less than data" begin + nsw = 5 + sw = Sweeps(nsw, sweep_args) + + @test nsweep(sw) == nsw + + @test maxdim(sw, 1) == 50 + @test maxdim(sw, 2) == 100 + @test maxdim(sw, 3) == 200 + @test maxdim(sw, 4) == 400 + for n in 5:nsw + @test maxdim(sw, n) == 800 + end + + @test mindim(sw, 1) == 10 + for n in 2:nsw + @test mindim(sw, n) == 20 + end + + for n in 1:nsw + @test cutoff(sw, n) == 1e-12 + end + + @test noise(sw, 1) == 1e-7 + @test noise(sw, 2) == 1e-8 + @test noise(sw, 3) == 1e-10 + @test noise(sw, 4) == 0 + @test noise(sw, 5) == 1e-11 + end + + @testset "Variable types of input" begin + sw = Sweeps(5) + setnoise!(sw, 1E-8, 0) + @test noise(sw, 1) ≈ 1E-8 + @test noise(sw, 2) ≈ 0.0 + @test noise(sw, 3) ≈ 0.0 + setcutoff!(sw, 0, 1E-8, 0, 1E-12) + @test cutoff(sw, 1) ≈ 0.0 + @test cutoff(sw, 2) ≈ 1E-8 + @test cutoff(sw, 3) ≈ 0.0 + @test cutoff(sw, 4) ≈ 1E-12 + end + + @testset "Keyword args to constructor" begin + sw = Sweeps(5; maxdim=[4, 8, 16], mindim=1, cutoff=[1E-5, 1E-8]) + @test maxdim(sw, 1) == 4 + @test maxdim(sw, 2) == 8 + @test maxdim(sw, 3) == 16 + @test maxdim(sw, 4) == 16 + @test maxdim(sw, 5) == 16 + + @test mindim(sw, 1) == 1 + @test mindim(sw, 5) == 1 + + @test cutoff(sw, 1) ≈ 1E-5 + @test cutoff(sw, 2) ≈ 1E-8 + @test cutoff(sw, 3) ≈ 1E-8 + @test cutoff(sw, 4) ≈ 1E-8 + @test cutoff(sw, 5) ≈ 1E-8 + + sw = Sweeps(5; cutoff=1E-8) + @test maxdim(sw, 1) == typemax(Int) + @test maxdim(sw, 5) == typemax(Int) + end +end diff --git a/test/base/test_symmetrystyle.jl b/test/base/test_symmetrystyle.jl new file mode 100644 index 0000000..66c5823 --- /dev/null +++ b/test/base/test_symmetrystyle.jl @@ -0,0 +1,13 @@ +using ITensors +using ITensors.NDTensors +using Test + +@testset "SymmetryStyle trait" begin + sqn = siteinds("S=1/2", 10; conserve_qns=true) + s = removeqns(sqn) + psi = MPS(s) + psiqn = MPS(sqn) + @test @inferred(ITensors.SymmetryStyle, ITensors.symmetrystyle(psi)) == ITensors.NonQN() + @test @inferred(ITensors.SymmetryStyle, ITensors.symmetrystyle(psiqn)) == + ITensors.HasQNs() +end diff --git a/test/base/test_threading.jl b/test/base/test_threading.jl new file mode 100644 index 0000000..28e1408 --- /dev/null +++ b/test/base/test_threading.jl @@ -0,0 +1,57 @@ +using Compat +using ITensors +using Test +using LinearAlgebra + +if isone(Threads.nthreads()) + @warn "Testing block sparse multithreading but only one thread is set!" +end + +@testset "Threading" begin + blas_num_threads = Compat.get_num_threads() + strided_num_threads = ITensors.NDTensors.Strided.get_num_threads() + + BLAS.set_num_threads(1) + ITensors.NDTensors.Strided.set_num_threads(1) + + @testset "Bug fixed in threaded block sparse" begin + maxdim = 10 + nsweeps = 2 + outputlevel = 0 + cutoff = 0.0 + Nx = 4 + Ny = 2 + U = 4.0 + t = 1.0 + N = Nx * Ny + sweeps = Sweeps(nsweeps) + maxdims = min.(maxdim, [100, 200, 400, 800, 2000, 3000, maxdim]) + maxdim!(sweeps, maxdims...) + cutoff!(sweeps, cutoff) + noise!(sweeps, 1e-6, 1e-7, 1e-8, 0.0) + sites = siteinds("Electron", N; conserve_qns=true) + lattice = square_lattice(Nx, Ny; yperiodic=true) + opsum = OpSum() + for b in lattice + opsum .-= t, "Cdagup", b.s1, "Cup", b.s2 + opsum .-= t, "Cdagup", b.s2, "Cup", b.s1 + opsum .-= t, "Cdagdn", b.s1, "Cdn", b.s2 + opsum .-= t, "Cdagdn", b.s2, "Cdn", b.s1 + end + for n in 1:N + opsum .+= U, "Nupdn", n + end + H = MPO(opsum, sites) + Hsplit = splitblocks(linkinds, H) + state = [isodd(n) ? "↑" : "↓" for n in 1:N] + ψ0 = MPS(sites, state) + enabled = ITensors.enable_threaded_blocksparse(true) + energy, _ = dmrg(H, ψ0, sweeps; outputlevel=outputlevel) + energy_split, _ = dmrg(Hsplit, ψ0, sweeps; outputlevel=outputlevel) + @test energy_split ≈ energy + ITensors.enable_threaded_blocksparse(enabled) + end + + BLAS.set_num_threads(blas_num_threads) + ITensors.NDTensors.Strided.set_num_threads(strided_num_threads) +end diff --git a/test/utils/TestITensorMPSExportedNames.jl b/test/base/utils/TestITensorMPSExportedNames.jl similarity index 92% rename from test/utils/TestITensorMPSExportedNames.jl rename to test/base/utils/TestITensorMPSExportedNames.jl index 7993079..ba05f9a 100644 --- a/test/utils/TestITensorMPSExportedNames.jl +++ b/test/base/utils/TestITensorMPSExportedNames.jl @@ -6,12 +6,6 @@ const ITENSORMPS_EXPORTED_NAMES = [ Symbol("@TagType_str"), Symbol("@ValName_str"), Symbol("@preserve_ortho"), - Symbol("@visualize"), - Symbol("@visualize!"), - Symbol("@visualize_noeval"), - Symbol("@visualize_noeval!"), - Symbol("@visualize_sequence"), - Symbol("@visualize_sequence_noeval"), :AbstractMPS, :AbstractObserver, :Apply, @@ -39,6 +33,7 @@ const ITENSORMPS_EXPORTED_NAMES = [ :Sum, :TagType, :Sweeps, + :TimeDependentSum, :Trotter, :ValName, :add, @@ -58,6 +53,7 @@ const ITENSORMPS_EXPORTED_NAMES = [ :cutoff!, :disk, :dmrg, + :dmrg_x, :dot, :eigs, :energies, @@ -66,6 +62,7 @@ const ITENSORMPS_EXPORTED_NAMES = [ :error_contract, :error_mpoprod, :error_mul, + :expand, :expect, :findfirstsiteind, :findfirstsiteinds, @@ -86,6 +83,7 @@ const ITENSORMPS_EXPORTED_NAMES = [ :linkind, :linkindex, :linkinds, + :linsolve, :logdot, :loginner, :lognorm, @@ -142,6 +140,7 @@ const ITENSORMPS_EXPORTED_NAMES = [ :setmaxdim!, :setmindim!, :setnoise!, + :sim!, :simlinks!, :siteind, :siteindex, @@ -152,7 +151,9 @@ const ITENSORMPS_EXPORTED_NAMES = [ :sum, :swapbondsites, :sweepnext, + :tdvp, :tensors, + :to_vec, :toMPO, :totalqn, :tr, diff --git a/test/base/utils/opsum_hash_bug.jld2 b/test/base/utils/opsum_hash_bug.jld2 new file mode 100644 index 0000000..ac16f6c Binary files /dev/null and b/test/base/utils/opsum_hash_bug.jld2 differ diff --git a/test/base/utils/util.jl b/test/base/utils/util.jl new file mode 100644 index 0000000..6bc27e9 --- /dev/null +++ b/test/base/utils/util.jl @@ -0,0 +1,56 @@ +using ITensorMPS +using ITensors +using Random +using ITensorMPS: AbstractMPS + +function fill_trivial_coefficients(ψ) + return ψ isa AbstractMPS ? (1, ψ) : ψ +end + +function inner_add(α⃗ψ⃗::Tuple{<:Number,<:MPST}...) where {MPST<:AbstractMPS} + Nₘₚₛ = length(α⃗ψ⃗) + α⃗ = first.(α⃗ψ⃗) + ψ⃗ = last.(α⃗ψ⃗) + N⃡ = (conj(α⃗[i]) * α⃗[j] * inner(ψ⃗[i], ψ⃗[j]) for i in 1:Nₘₚₛ, j in 1:Nₘₚₛ) + return sum(N⃡) +end + +inner_add(ψ⃗...) = inner_add(fill_trivial_coefficients.(ψ⃗)...) + +# TODO: this is no longer needed, use random_mps +function makeRandomMPS(sites; chi::Int=4)::MPS + N = length(sites) + v = Vector{ITensor}(undef, N) + l = [Index(chi, "Link,l=$n") for n in 1:(N - 1)] + for n in 1:N + s = sites[n] + if n == 1 + v[n] = random_itensor(l[n], s) + elseif n == N + v[n] = random_itensor(l[n - 1], s) + else + v[n] = random_itensor(l[n - 1], l[n], s) + end + normalize!(v[n]) + end + return MPS(v, 0, N + 1) +end + +function makeRandomMPO(sites; chi::Int=4)::MPO + N = length(sites) + v = Vector{ITensor}(undef, N) + l = [Index(chi, "Link,l=$n") for n in 1:(N - 1)] + for n in 1:N + s = sites[n] + if n == 1 + v[n] = ITensor(l[n], s, s') + elseif n == N + v[n] = ITensor(l[n - 1], s, s') + else + v[n] = ITensor(l[n - 1], s, s', l[n]) + end + randn!(v[n]) + normalize!(v[n]) + end + return MPO(v, 0, N + 1) +end diff --git a/test/ext/ITensorMPSChainRulesCoreExt/Project.toml b/test/ext/ITensorMPSChainRulesCoreExt/Project.toml new file mode 100644 index 0000000..444818b --- /dev/null +++ b/test/ext/ITensorMPSChainRulesCoreExt/Project.toml @@ -0,0 +1,5 @@ +[deps] +ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +ITensors = "9136182c-28ba-11e9-034c-db9fb085ebd5" +OptimKit = "77e91f04-9b3b-57a6-a776-40b61faaebe0" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" diff --git a/test/ext/ITensorMPSChainRulesCoreExt/runtests.jl b/test/ext/ITensorMPSChainRulesCoreExt/runtests.jl new file mode 100644 index 0000000..c2fe5a8 --- /dev/null +++ b/test/ext/ITensorMPSChainRulesCoreExt/runtests.jl @@ -0,0 +1,16 @@ +using ITensors +using Test + +ITensors.Strided.disable_threads() +ITensors.BLAS.set_num_threads(1) +ITensors.disable_threaded_blocksparse() + +@testset "$(@__DIR__)" begin + filenames = filter(readdir(@__DIR__)) do f + startswith("test_")(f) && endswith(".jl")(f) + end + @testset "Test $(@__DIR__)/$filename" for filename in filenames + println("Running $(@__DIR__)/$filename") + @time include(filename) + end +end diff --git a/test/ext/ITensorMPSChainRulesCoreExt/test_chainrules.jl b/test/ext/ITensorMPSChainRulesCoreExt/test_chainrules.jl new file mode 100644 index 0000000..c704920 --- /dev/null +++ b/test/ext/ITensorMPSChainRulesCoreExt/test_chainrules.jl @@ -0,0 +1,375 @@ +using ITensorMPS +using ITensors +using Random +using Test +using Zygote + +Random.seed!(1234) + +@testset "ChainRules/Zygote AD tests for MPS/MPO" begin + @testset "issue 936" begin + # https://github.com/ITensor/ITensors.jl/issues/936 + n = 2 + s = siteinds("S=1/2", n) + x = (x -> outer(x', x))(random_mps(s)) + f1 = x -> tr(x) + f2 = x -> 2tr(x) + f3 = x -> -tr(x) + @test f1'(x) ≈ MPO(s, "I") + @test f2'(x) ≈ 2MPO(s, "I") + @test f3'(x) ≈ -MPO(s, "I") + end + + @testset "MPS ($ElType)" for ElType in (Float64, ComplexF64) + Random.seed!(1234) + n = 4 + ϵ = 1e-8 + s = siteinds("S=1/2", n; conserve_qns=true) + function heisenberg(n) + os = OpSum() + for j in 1:(n - 1) + os += 0.5, "S+", j, "S-", j + 1 + os += 0.5, "S-", j, "S+", j + 1 + os += "Sz", j, "Sz", j + 1 + end + return os + end + H = MPO(heisenberg(n), s) + ψ = random_mps(s, n -> isodd(n) ? "Up" : "Dn"; linkdims=2) + + f = x -> inner(x, x) + args = (ψ,) + d_args = gradient(f, args...) + @test norm(d_args[1] - 2 * args[1]) ≈ 0 atol = 1e-13 + + f = x -> inner(x', H, x) + args = (ψ,) + d_args = gradient(f, args...) + @test norm(d_args[1]' - 2 * H * args[1]) ≈ 0 atol = 1e-13 + + f = x -> inner(x', x) + args = (ψ,) + @test_throws ErrorException gradient(f, args...) + + f = x -> inner(x, H, x) + args = (ψ,) + @test_throws ErrorException gradient(f, args...) + + # apply on MPS + s = siteinds("S=1/2", n) + ϕ = random_mps(ElType, s) + ψ = random_mps(ElType, s) + f = function (x) + U = [op("Ry", s[2]; θ=x), op("CX", s[1], s[2]), op("Rx", s[3]; θ=x)] + ψθ = apply(U, ψ) + return abs2(inner(ϕ, ψθ)) + end + θ = 0.5 + ∇f = f'(θ) + ∇num = (f(θ + ϵ) - f(θ)) / ϵ + @test ∇f ≈ ∇num atol = 1e-5 + end + + @testset "MPS rrules" begin + Random.seed!(1234) + s = siteinds("S=1/2", 4) + ψ = random_mps(s) + args = (ψ,) + f = x -> inner(x, x) + # TODO: Need to make MPS type compatible with FiniteDifferences. + #test_rrule(ZygoteRuleConfig(), f, args...; rrule_f=rrule_via_ad, check_inferred=false) + d_args = gradient(f, args...) + @test norm(d_args[1] - 2 * args[1]) ≈ 0 atol = 1e-13 + + args = (ψ,) + f = x -> inner(prime(x), prime(x)) + # TODO: Need to make MPS type compatible with FiniteDifferences. + #test_rrule(ZygoteRuleConfig(), f, args...; rrule_f=rrule_via_ad, check_inferred=false) + d_args = gradient(f, args...) + @test norm(d_args[1] - 2 * args[1]) ≈ 0 atol = 1e-13 + + ψ = random_mps(ComplexF64, s) + ψtensors = ITensors.data(ψ) + ϕ = random_mps(ComplexF64, s) + f = function (x) + ψ̃tensors = [x^j * ψtensors[j] for j in 1:length(ψtensors)] + ψ̃ = MPS(ψ̃tensors) + return abs2(inner(ϕ, ψ̃)) + end + x = 0.5 + ϵ = 1e-10 + @test f'(x) ≈ (f(x + ϵ) - f(x)) / ϵ atol = 1e-6 + + ρ = random_mpo(s) + f = function (x) + ψ̃tensors = [x^j * ψtensors[j] for j in 1:length(ψtensors)] + ψ̃ = MPS(ψ̃tensors) + return real(inner(ψ̃', ρ, ψ̃)) + end + @test f'(x) ≈ (f(x + ϵ) - f(x)) / ϵ atol = 1e-6 + end + + #@testset "MPO rules" begin + # Random.seed!(1234) + # s = siteinds("S=1/2", 2) + # + # #ρ = random_mpo(s) + # #ρtensors = ITensors.data(ρ) + # #ϕ = random_mps(ComplexF64, s) + # #f = function (x) + # # ρ̃tensors = [2 * x * ρtensors[1], log(x) * ρtensors[2]] + # # ρ̃ = MPO(ρ̃tensors) + # # #@show typeof(ρ̃) + # # return real(inner(ϕ', ρ̃, ϕ)) + # #end + # #x = 3.0 + # #ϵ = 1e-8 + # #@show (f(x+ϵ) - f(x)) / ϵ + # #@show f'(x) + # ##@test f'(x) ≈ (f(x+ϵ) - f(x)) / ϵ atol = 1e-6 + # # + # + # #ϕ = random_mpo(s) + # #f = function (x) + # # ψ̃tensors = [2 * x * ψtensors[1], log(x) * ψtensors[2]] + # # ψ̃ = MPS(ψ̃tensors) + # # return abs2(inner(ϕ, ψ̃)) + # #end + # #x = 3.0 + # #ϵ = 1e-8 + # #@test f'(x) ≈ (f(x+ϵ) - f(x)) / ϵ atol = 1e-6 + # + # #ρ = random_mpo(s) + #end + @testset "MPO: apply" begin + Random.seed!(1234) + ϵ = 1e-8 + n = 3 + s = siteinds("Qubit", n) + function ising(n, h) + os = OpSum() + for j in 1:(n - 1) + os -= 1, "Z", j, "Z", j + 1 + os -= h, "X", j + end + os -= h, "X", n + return os + end + H = MPO(ising(n, 1.0), s) + A = random_mpo(s) + ϕ = random_mps(ComplexF64, s; linkdims=10) + + # apply on mpo with apply_dag=true + f = function (x) + U = [op("Ry", s[2]; θ=x), op("CX", s[1], s[2]), op("Rx", s[3]; θ=x)] + Hθ = apply(U, H; apply_dag=true) + return real(inner(ϕ', Hθ, ϕ)) + end + θ = 0.5 + ∇f = f'(θ) + ∇num = (f(θ + ϵ) - f(θ)) / ϵ + @test ∇f ≈ ∇num atol = 1e-5 + + # test that apply on non-Hermitian mpo with apply_dag=true + # throws an error. + f = function (x) + U = [op("Ry", s[2]; θ=x), op("CX", s[1], s[2]), op("Rx", s[3]; θ=x)] + Aθ = apply(U, A; apply_dag=true) + return real(inner(ϕ', Aθ, ϕ)) + end + θ = 0.5 + ∇f = f'(θ) + ∇num = (f(θ + ϵ) - f(θ)) / ϵ + @test ∇f ≈ ∇num atol = 1e-5 + + # apply on Hermitian MPO with apply_dag=false + f = function (x) + U = [op("Ry", s[2]; θ=x), op("CX", s[1], s[2]), op("Rx", s[3]; θ=x)] + Hθ = apply(U, H; apply_dag=false) + return real(inner(ϕ', Hθ, ϕ)) + end + θ = 0.5 + ∇f = f'(θ) + ∇num = (f(θ + ϵ) - f(θ)) / ϵ + @test ∇f ≈ ∇num atol = 1e-5 + + # apply on non-Hermitian MPO with apply_dag=false + f = function (x) + U = [op("Ry", s[2]; θ=x), op("CX", s[1], s[2]), op("Rx", s[3]; θ=x)] + Aθ = apply(U, A; apply_dag=false) + return real(inner(ϕ', Aθ, ϕ)) + end + θ = 0.5 + ∇f = f'(θ) + ∇num = (f(θ + ϵ) - f(θ)) / ϵ + @test ∇f ≈ ∇num atol = 1e-5 + + # multiply two MPOs + V = random_mpo(s) + f = function (x) + U = [op("Ry", s[2]; θ=x), op("CX", s[1], s[2]), op("Rx", s[3]; θ=x)] + Hθ = apply(U, H; apply_dag=false) + X = replaceprime(V' * Hθ, 2 => 1) + return real(inner(ϕ', X, ϕ)) + end + + θ = 0.5 + ∇f = f'(θ) + ∇num = (f(θ + ϵ) - f(θ)) / ϵ + @test ∇f ≈ ∇num atol = 1e-5 + + # trace(MPO) + V1 = random_mpo(s) + V2 = random_mpo(s) + f = function (x) + U = [op("Ry", s[2]; θ=x), op("CX", s[1], s[2]), op("Rx", s[3]; θ=x)] + Hθ = apply(U, H; apply_dag=false) + X = V1''' * Hθ'' * V2' * Hθ + return real(tr(X; plev=4 => 0)) + end + + θ = 0.5 + ∇f = f'(θ) + ∇num = (f(θ + ϵ) - f(θ)) / ϵ + @test ∇f ≈ ∇num atol = 1e-5 + + # add(MPO, MPO) + f = function (x, y) + z = x + y + return inner(z, z) + end + V1 = random_mpo(s) + V2 = random_mpo(s) + g1, g2 = gradient(f, V1, V2) + @test g1 ≈ 2 * (V1 + V2) + @test g2 ≈ 2 * (V1 + V2) + + # subtract(MPO, MPO) + f = function (x, y) + z = x - y + return inner(z, z) + end + V1 = random_mpo(s) + V2 = random_mpo(s) + g1, g2 = gradient(f, V1, V2) + @test g1 ≈ 2 * (V1 - V2) + @test g2 ≈ -2 * (V1 - V2) + end + + @testset "contract/apply MPOs" begin + n = 2 + s = siteinds("S=1/2", n) + x = (x -> outer(x', x))(random_mps(s; linkdims=4)) + x_itensor = contract(x) + + f = x -> tr(apply(x, x)) + @test f(x) ≈ f(x_itensor) + @test contract(f'(x)) ≈ f'(x_itensor) + + f = x -> tr(replaceprime(contract(x', x), 2 => 1)) + @test f(x) ≈ f(x_itensor) + @test contract(f'(x)) ≈ f'(x_itensor) + + f = x -> tr(replaceprime(*(x', x), 2 => 1)) + @test f(x) ≈ f(x_itensor) + @test contract(f'(x)) ≈ f'(x_itensor) + end + + @testset "contract/apply MPOs on MPSs" begin + n = 2 + s = siteinds("S=1/2", n) + x = (x -> outer(x', x))(random_mps(s; linkdims=4)) + x_itensor = contract(x) + y = random_mps(s; linkdims=4) + y_itensor = contract(y) + + f = x -> inner(apply(x, y), apply(x, y)) + g = x -> inner(apply(x, y_itensor), apply(x, y_itensor)) + @test f(x) ≈ g(x_itensor) + @test contract(f'(x)) ≈ g'(x_itensor) + + f = y -> inner(apply(x, y), apply(x, y)) + g = y -> inner(apply(x_itensor, y), apply(x_itensor, y)) + @test f(y) ≈ g(y_itensor) + @test contract(f'(y)) ≈ g'(y_itensor) + + f = + x -> inner(replaceprime(contract(x, y), 2 => 1), replaceprime(contract(x, y), 2 => 1)) + g = + x -> inner( + replaceprime(contract(x, y_itensor), 2 => 1), + replaceprime(contract(x, y_itensor), 2 => 1), + ) + @test f(x) ≈ g(x_itensor) + @test contract(f'(x)) ≈ g'(x_itensor) + + f = + y -> inner(replaceprime(contract(x, y), 2 => 1), replaceprime(contract(x, y), 2 => 1)) + g = + y -> inner( + replaceprime(contract(x_itensor, y), 2 => 1), + replaceprime(contract(x_itensor, y), 2 => 1), + ) + @test f(y) ≈ g(y_itensor) + @test contract(f'(y)) ≈ g'(y_itensor) + + f = x -> inner(replaceprime(*(x, y), 2 => 1), replaceprime(*(x, y), 2 => 1)) + g = + x -> + inner(replaceprime(*(x, y_itensor), 2 => 1), replaceprime(*(x, y_itensor), 2 => 1)) + @test f(x) ≈ g(x_itensor) + @test contract(f'(x)) ≈ g'(x_itensor) + + f = y -> inner(replaceprime(*(x, y), 2 => 1), replaceprime(*(x, y), 2 => 1)) + g = + y -> + inner(replaceprime(*(x_itensor, y), 2 => 1), replaceprime(*(x_itensor, y), 2 => 1)) + @test f(y) ≈ g(y_itensor) + @test contract(f'(y)) ≈ g'(y_itensor) + end + @testset "Calling apply multiple times (ITensors #924 regression test)" begin + n = 1 + θ = 3.0 + p = 2 + + s = siteinds("S=1/2", n) + + ψ₀ₘₚₛ = MPS(s, "↑") + ψ₀ = contract(ψ₀ₘₚₛ) + + U(θ) = [θ * op("Z", s, 1)] + + function f(θ, ψ) + ψθ = ψ + Uθ = U(θ) + for _ in 1:p + ψθ = apply(Uθ, ψθ) + end + return inner(ψ, ψθ) + end + + function g(θ, ψ) + Uθ = U(θ) + Utot = Uθ + for _ in 2:p + Utot = [Utot; Uθ] + end + ψθ = apply(Utot, ψ) + return inner(ψ, ψθ) + end + + f_itensor(θ) = f(θ, ψ₀) + f_mps(θ) = f(θ, ψ₀ₘₚₛ) + g_itensor(θ) = g(θ, ψ₀) + g_mps(θ) = g(θ, ψ₀ₘₚₛ) + + @test f_itensor(θ) ≈ θ^p + @test f_mps(θ) ≈ θ^p + @test f_itensor'(θ) ≈ p * θ^(p - 1) + @test f_mps'(θ) ≈ p * θ^(p - 1) + @test g_itensor(θ) ≈ θ^p + @test g_mps(θ) ≈ θ^p + @test g_itensor'(θ) ≈ p * θ^(p - 1) + @test g_mps'(θ) ≈ p * θ^(p - 1) + end +end diff --git a/test/ext/ITensorMPSChainRulesCoreExt/test_optimization.jl b/test/ext/ITensorMPSChainRulesCoreExt/test_optimization.jl new file mode 100644 index 0000000..3835f95 --- /dev/null +++ b/test/ext/ITensorMPSChainRulesCoreExt/test_optimization.jl @@ -0,0 +1,261 @@ +using ITensors +using OptimKit +using Random +using Test +using Zygote + +include(joinpath(@__DIR__, "utils", "circuit.jl")) + +@testset "optimization" begin + @testset "Energy minimization" begin + N = 3 + s = siteinds("S=1/2", N; conserve_qns=true) + os = OpSum() + for n in 1:(N - 1) + os .+= 0.5, "S+", n, "S-", n + 1 + os .+= 0.5, "S-", n, "S+", n + 1 + os .+= "Sz", n, "Sz", n + 1 + end + Hmpo = MPO(os, s) + ψ₀mps = random_mps(s, n -> isodd(n) ? "↑" : "↓") + H = prod(Hmpo) + ψ₀ = prod(ψ₀mps) + # The Rayleigh quotient to minimize + function E(H::ITensor, ψ::ITensor) + ψdag = dag(ψ) + return (ψdag' * H * ψ)[] / (ψdag * ψ)[] + end + E(ψ::ITensor) = E(H, ψ) + ∇E(ψ::ITensor) = E'(ψ) + fg(ψ::ITensor) = (E(ψ), ∇E(ψ)) + linesearch = HagerZhangLineSearch(; + c₁=0.1, c₂=0.9, ϵ=1e-6, θ=1 / 2, γ=2 / 3, ρ=5.0, verbosity=0 + ) + algorithm = LBFGS(3; maxiter=20, gradtol=1e-8, linesearch=linesearch) + ψ, fψ, gψ, numfg, normgradhistory = optimize(fg, ψ₀, algorithm) + D, _ = eigen(H; ishermitian=true) + @test E(H, ψ) < E(H, ψ₀) + @test E(H, ψ) ≈ minimum(D) + end + + @testset "Energy minimization (MPS)" begin + N = 4 + χ = 4 + s = siteinds("S=1/2", N; conserve_qns=true) + os = OpSum() + for n in 1:(N - 1) + os .+= 0.5, "S+", n, "S-", n + 1 + os .+= 0.5, "S-", n, "S+", n + 1 + os .+= "Sz", n, "Sz", n + 1 + end + Hmpo = MPO(os, s) + + Random.seed!(1234) + ψ₀mps = random_mps(s, n -> isodd(n) ? "↑" : "↓"; linkdims=χ) + + H = ITensors.data(Hmpo) + ψ₀ = ITensors.data(ψ₀mps) + # The Rayleigh quotient to minimize + function E(H::Vector{ITensor}, ψ::Vector{ITensor}) + N = length(ψ) + ψdag = dag.(addtags.(ψ, "bra"; tags="Link")) + ψ′dag = prime.(ψdag) + e = ITensor(1.0) + for n in 1:N + e = e * ψ′dag[n] * H[n] * ψ[n] + end + norm = ITensor(1.0) + for n in 1:N + norm = norm * ψdag[n] * ψ[n] + end + return e[] / norm[] + end + E(ψ) = E(H, ψ) + ∇E(ψ) = E'(ψ) + fg(ψ) = (E(ψ), ∇E(ψ)) + linesearch = HagerZhangLineSearch(; c₁=0.1, c₂=0.9, ϵ=1e-6, θ=1 / 2, γ=2 / 3, ρ=5.0) + algorithm = LBFGS(5; maxiter=50, gradtol=1e-4, linesearch=linesearch, verbosity=0) + ψ, fψ, gψ, numfg, normgradhistory = optimize(fg, ψ₀, algorithm) + sweeps = Sweeps(5) + setmaxdim!(sweeps, χ) + fψmps, ψmps = dmrg(Hmpo, ψ₀mps, sweeps; outputlevel=0) + @test E(H, ψ) ≈ inner(ψmps', Hmpo, ψmps) / inner(ψmps, ψmps) rtol = 1e-2 + end + + @testset "State preparation (full state)" begin + function Rylayer(N, θ⃗) + return [("Ry", (n,), (θ=θ⃗[n],)) for n in 1:N] + end + + function CXlayer(N) + return [("CX", (n, n + 1)) for n in 1:2:(N - 1)] + end + + # The variational circuit we want to optimize + function variational_circuit(θ⃗) + N = length(θ⃗) + return vcat(Rylayer(N, θ⃗), CXlayer(N)) + end + + N = 4 + Random.seed!(1234) + θ⃗ = 2π .* rand(N) + gates = variational_circuit(θ⃗) + + s = siteinds("Qubit", N) + ψₘₚₛ = MPS(s, "0") + ψ = prod(ψₘₚₛ) + U = buildcircuit(gates, s) + # Create the target state + Uψ = apply(U, ψ) + + @test inner_circuit(Uψ, U, ψ) ≈ 1 + + function loss(θ⃗) + gates = variational_circuit(θ⃗) + U = buildcircuit(gates, s) + return -abs(inner_circuit(Uψ, U, ψ))^2 + end + + θ⃗₀ = randn!(copy(θ⃗)) + fg(x) = (loss(x), convert(Vector, loss'(x))) + θ⃗ₒₚₜ, fₒₚₜ, gₒₚₜ, numfg, normgradhistory = optimize(fg, θ⃗₀, GradientDescent()) + @test loss(θ⃗ₒₚₜ) ≈ loss(θ⃗) rtol = 1e-2 + end + + @testset "State preparation (MPS)" begin + for gate in ["Ry"] #="Rx", =# + nsites = 4 # Number of sites + nlayers = 2 # Layers of gates in the ansatz + gradtol = 1e-3 # Tolerance for stopping gradient descent + + # A layer of the circuit we want to optimize + function layer(nsites, θ⃗) + gate_layer = [(gate, (n,), (θ=θ⃗[n],)) for n in 1:nsites] + CX_layer = [("CX", (n, n + 1)) for n in 1:2:(nsites - 1)] + return [gate_layer; CX_layer] + end + + # The variational circuit we want to optimize + function variational_circuit(nsites, nlayers, θ⃗) + range = 1:nsites + circuit = layer(nsites, θ⃗[range]) + for n in 1:(nlayers - 1) + circuit = [circuit; layer(nsites, θ⃗[range .+ n * nsites])] + end + return circuit + end + + Random.seed!(1234) + + θ⃗ᵗᵃʳᵍᵉᵗ = 2π * rand(nsites * nlayers) + 𝒰ᵗᵃʳᵍᵉᵗ = variational_circuit(nsites, nlayers, θ⃗ᵗᵃʳᵍᵉᵗ) + + s = siteinds("Qubit", nsites) + Uᵗᵃʳᵍᵉᵗ = ops(𝒰ᵗᵃʳᵍᵉᵗ, s) + + ψ0 = MPS(s, "0") + + # Create the random target state + ψᵗᵃʳᵍᵉᵗ = apply(Uᵗᵃʳᵍᵉᵗ, ψ0; cutoff=1e-8) + + # + # The loss function, a function of the gate parameters + # and implicitly depending on the target state: + # + # loss(θ⃗) = -|⟨θ⃗ᵗᵃʳᵍᵉᵗ|U(θ⃗)|0⟩|² = -|⟨θ⃗ᵗᵃʳᵍᵉᵗ|θ⃗⟩|² + # + function loss(θ⃗) + nsites = length(ψ0) + s = siteinds(ψ0) + 𝒰θ⃗ = variational_circuit(nsites, nlayers, θ⃗) + Uθ⃗ = ops(𝒰θ⃗, s) + ψθ⃗ = apply(Uθ⃗, ψ0; cutoff=1e-8) + return -abs(inner(ψᵗᵃʳᵍᵉᵗ, ψθ⃗))^2 + end + + θ⃗₀ = randn!(copy(θ⃗ᵗᵃʳᵍᵉᵗ)) + + @test loss(θ⃗₀) ≉ loss(θ⃗ᵗᵃʳᵍᵉᵗ) + + loss_∇loss(x) = (loss(x), convert(Vector, loss'(x))) + @show gate + algorithm = LBFGS(; gradtol=gradtol, verbosity=2) + θ⃗ₒₚₜ, lossₒₚₜ, ∇lossₒₚₜ, numfg, normgradhistory = optimize( + loss_∇loss, θ⃗₀, algorithm + ) + + @test loss(θ⃗ₒₚₜ) ≈ loss(θ⃗ᵗᵃʳᵍᵉᵗ) rtol = 1e-5 + end + end + + @testset "VQE (MPS)" begin + nsites = 4 # Number of sites + nlayers = 2 # Layers of gates in the ansatz + gradtol = 1e-3 # Tolerance for stopping gradient descent + + # The Hamiltonian we are minimizing + function ising_hamiltonian(nsites; h) + ℋ = OpSum() + for j in 1:(nsites - 1) + ℋ -= 1, "Z", j, "Z", j + 1 + end + for j in 1:nsites + ℋ += h, "X", j + end + return ℋ + end + + # A layer of the circuit we want to optimize + function layer(nsites, θ⃗) + RY_layer = [("Ry", (n,), (θ=θ⃗[n],)) for n in 1:nsites] + CX_layer = [("CX", (n, n + 1)) for n in 1:2:(nsites - 1)] + return [RY_layer; CX_layer] + end + + # The variational circuit we want to optimize + function variational_circuit(nsites, nlayers, θ⃗) + range = 1:nsites + circuit = layer(nsites, θ⃗[range]) + for n in 1:(nlayers - 1) + circuit = [circuit; layer(nsites, θ⃗[range .+ n * nsites])] + end + return circuit + end + + s = siteinds("Qubit", nsites) + + h = 1.3 + ℋ = ising_hamiltonian(nsites; h=h) + H = MPO(ℋ, s) + ψ0 = MPS(s, "0") + + # + # The loss function, a function of the gate parameters + # and implicitly depending on the Hamiltonian and state: + # + # loss(θ⃗) = ⟨0|U(θ⃗)† H U(θ⃗)|0⟩ = ⟨θ⃗|H|θ⃗⟩ + # + function loss(θ⃗) + nsites = length(ψ0) + s = siteinds(ψ0) + 𝒰θ⃗ = variational_circuit(nsites, nlayers, θ⃗) + Uθ⃗ = ops(𝒰θ⃗, s) + ψθ⃗ = apply(Uθ⃗, ψ0; cutoff=1e-8) + return inner(ψθ⃗', H, ψθ⃗; cutoff=1e-8) + end + + Random.seed!(1234) + θ⃗₀ = 2π * rand(nsites * nlayers) + + loss_∇loss(x) = (loss(x), convert(Vector, loss'(x))) + algorithm = LBFGS(; gradtol=gradtol, verbosity=0) + θ⃗ₒₚₜ, lossₒₚₜ, ∇lossₒₚₜ, numfg, normgradhistory = optimize(loss_∇loss, θ⃗₀, algorithm) + + sweeps = Sweeps(5) + setmaxdim!(sweeps, 10) + e_dmrg, ψ_dmrg = dmrg(H, ψ0, sweeps; outputlevel=0) + + @test loss(θ⃗ₒₚₜ) ≈ e_dmrg rtol = 1e-1 + end +end diff --git a/test/ext/ITensorMPSChainRulesCoreExt/utils/circuit.jl b/test/ext/ITensorMPSChainRulesCoreExt/utils/circuit.jl new file mode 100644 index 0000000..74b4eef --- /dev/null +++ b/test/ext/ITensorMPSChainRulesCoreExt/utils/circuit.jl @@ -0,0 +1,69 @@ +using ITensors +using ChainRulesCore: ZeroTangent + +# Write a custom `rrule` for this. +function inner_circuit(ϕ::ITensor, U::Vector{ITensor}, ψ::ITensor) + Uψ = ψ + for u in U + s = commoninds(u, Uψ) + s′ = s' + Uψ = replaceinds(u * Uψ, s′ => s) + end + return (ϕ * Uψ)[] +end + +name(g::Tuple{String,Vararg}) = g[1] +gate_sites(g::Tuple{<:Any,Tuple{Vararg{Int}},Vararg}) = g[2] +params(g::Tuple{<:Any,<:Any,<:NamedTuple}) = g[3] +params(g::Tuple{<:Any,<:Any}) = NamedTuple() + +function gate(g::Tuple, s::Vector{<:Index}) + return gate(name(g), params(g), s[collect(gate_sites(g))]) +end + +function gate(gn::String, params::NamedTuple, s::Vector{<:Index}) + return gate(gate(gn, params), s) +end + +function gate(gn, params::NamedTuple) + return gate(gn; params...) +end + +function gate(gn::String, params::NamedTuple) + return gate(Val{Symbol(gn)}(), params) +end + +function gate(g::Matrix, s::Vector{<:Index}) + s = reverse(s) + return itensor(g, s'..., dag(s)...) +end + +function buildcircuit(gates::Vector, s::Vector{<:Index}) + return [gate(g, s) for g in gates] +end + +# Gate definitions +function gate(gn::Val; params...) + return error("Gate $gn not defined") +end + +function gate(::Val{:Ry}; θ) + return [ + cos(θ / 2) -sin(θ / 2) + sin(θ / 2) cos(θ / 2) + ] +end + +function gate(::Val{:CX}) + return [ + 1 0 0 0 + 0 1 0 0 + 0 0 0 1 + 0 0 1 0 + ] +end + +# XXX: For some reason Zygote needs these definitions? +Base.reverse(z::ZeroTangent) = z +Base.adjoint(::Tuple{Nothing}) = nothing +Base.adjoint(::Tuple{Nothing,Nothing}) = nothing diff --git a/test/ext/ITensorMPSPackageCompilerExt/Project.toml b/test/ext/ITensorMPSPackageCompilerExt/Project.toml new file mode 100644 index 0000000..d9571fe --- /dev/null +++ b/test/ext/ITensorMPSPackageCompilerExt/Project.toml @@ -0,0 +1,5 @@ +[deps] +ITensorMPS = "0d1a4710-d33b-49a5-8f18-73bdf49b47e2" +ITensors = "9136182c-28ba-11e9-034c-db9fb085ebd5" +PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/ext/ITensorMPSPackageCompilerExt/runtests.jl b/test/ext/ITensorMPSPackageCompilerExt/runtests.jl new file mode 100644 index 0000000..cd167f7 --- /dev/null +++ b/test/ext/ITensorMPSPackageCompilerExt/runtests.jl @@ -0,0 +1,11 @@ +@eval module $(gensym()) +using ITensorMPS: ITensorMPS +using ITensors: ITensors +using PackageCompiler: PackageCompiler +using Test: @testset, @test +@testset "ITensorMPSPackageCompilerExt" begin + # Testing `ITensors.compile` would take too long so we just check + # that `ITensorsPackageCompilerExt` overloads `ITensors.compile`. + @test hasmethod(ITensors.compile, Tuple{ITensors.Algorithm"PackageCompiler"}) +end +end diff --git a/test/runtests.jl b/test/runtests.jl index 44e961e..1c7c6bd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,14 +1,13 @@ -@eval module $(gensym()) +using ITensors: ITensors using Test: @testset -using ITensorMPS: ITensorMPS -test_path = joinpath(pkgdir(ITensorMPS), "test") -test_files = filter( - file -> startswith(file, "test_") && endswith(file, ".jl"), readdir(test_path) -) -@testset "ITensorMPS.jl" begin - @testset "$filename" for filename in test_files - println("Running $filename") - @time include(joinpath(test_path, filename)) + +ITensors.Strided.disable_threads() +ITensors.BLAS.set_num_threads(1) +ITensors.disable_threaded_blocksparse() + +@testset "$(@__DIR__)" begin + dirs = ["ext/ITensorMPSChainRulesCoreExt", "Ops", "base"] + for dir in dirs + @time include(joinpath(@__DIR__, dir, "runtests.jl")) end end -end diff --git a/test/test_exports.jl b/test/test_exports.jl deleted file mode 100644 index 7515de7..0000000 --- a/test/test_exports.jl +++ /dev/null @@ -1,46 +0,0 @@ -@eval module $(gensym()) -using ITensorMPS: ITensorMPS -using ITensorTDVP: ITensorTDVP -using ITensors: ITensors -include("utils/TestITensorMPSExportedNames.jl") -using Test: @test, @test_broken, @testset -@testset "Exports and aliases" begin - @testset "Exports" begin - @test issetequal( - names(ITensorMPS), - [ - [:ITensorMPS] - # ITensorTDVP reexports - [:TimeDependentSum, :dmrg_x, :expand, :linsolve, :tdvp, :to_vec] - # ITensors and ITensors.ITensorMPS reexports - TestITensorMPSExportedNames.ITENSORMPS_EXPORTED_NAMES - ], - ) - end - @testset "Aliases" begin - @test ITensorMPS.Experimental.dmrg === ITensorTDVP.dmrg - @test ITensorMPS.dmrg === ITensors.ITensorMPS.dmrg - end - @testset "Not exported" begin - @test ITensorMPS.sortmergeterms === ITensors.ITensorMPS.sortmergeterms - # Should we fix this in ITensors.jl by adding: - # ```julia - # using .ITensorMPS: sortmergeterms - # ``` - # ? - @test_broken ITensorMPS.sortmergeterms === ITensors.sortmergeterms - for f in [ - :AbstractProjMPO, - :AbstractMPS, - :ProjMPS, - :makeL!, - :makeR!, - :set_terms, - :sortmergeterms, - :terms, - ] - @test getfield(ITensorMPS, f) === getfield(ITensors.ITensorMPS, f) - end - end -end -end