From b4750948899bd7b6750cb8e12ea701fe7d09f93b Mon Sep 17 00:00:00 2001 From: Katharine Hyatt Date: Thu, 29 Oct 2026 10:13:29 +0100 Subject: [PATCH] Use a testsuite for MPSKit --- test/algorithms/anyonic_braiding.jl | 180 +----- test/algorithms/changebonds.jl | 311 +--------- test/algorithms/correlators.jl | 49 +- test/algorithms/fidelity_susceptibility.jl | 40 +- test/algorithms/periodic_boundary.jl | 39 +- test/algorithms/propagator.jl | 184 +----- test/excitations/excitations.jl | 117 +--- test/groundstate/approximate.jl | 279 +-------- test/groundstate/groundstate.jl | 565 +---------------- test/hamiltonian/derivatives.jl | 120 +--- test/hamiltonian/finite.jl | 221 +------ test/hamiltonian/infinite.jl | 100 +-- test/hamiltonian/mixed.jl | 203 +------ test/mpo/dense_mpo.jl | 19 +- test/mpo/finite.jl | 117 +--- test/mpo/infinite.jl | 79 +-- test/mpo/mixed.jl | 31 +- test/mpo/multiline.jl | 106 +--- test/operators/lazysum.jl | 267 +------- test/operators/projection.jl | 17 +- test/operators/window.jl | 63 +- test/runtests.jl | 3 + test/states/arithmetic.jl | 245 +------- test/states/finitemps.jl | 164 +---- test/states/infinitemps.jl | 140 +---- test/states/multilinemps.jl | 54 +- test/states/quasiparticle.jl | 97 +-- test/states/windowmps.jl | 75 +-- test/statmech/statmech.jl | 122 +--- test/symmetries/multifusion.jl | 172 +----- test/symmetries/sector_conventions.jl | 61 +- test/testsuite/TestSuite.jl | 218 +++++++ test/testsuite/algorithms/anyonic_braiding.jl | 191 ++++++ test/testsuite/algorithms/changebonds.jl | 333 ++++++++++ test/testsuite/algorithms/correlators.jl | 55 ++ .../algorithms/fidelity_susceptibility.jl | 43 ++ .../testsuite/algorithms/periodic_boundary.jl | 45 ++ test/testsuite/algorithms/propagator.jl | 191 ++++++ test/testsuite/excitations/excitations.jl | 120 ++++ test/testsuite/groundstate/approximate.jl | 291 +++++++++ test/testsuite/groundstate/groundstate.jl | 574 ++++++++++++++++++ test/testsuite/hamiltonian/derivatives.jl | 127 ++++ test/testsuite/hamiltonian/finite.jl | 229 +++++++ test/testsuite/hamiltonian/infinite.jl | 109 ++++ test/testsuite/hamiltonian/mixed.jl | 206 +++++++ test/testsuite/mpo/dense_mpo.jl | 23 + test/testsuite/mpo/finite.jl | 124 ++++ test/testsuite/mpo/infinite.jl | 83 +++ test/testsuite/mpo/mixed.jl | 35 ++ test/testsuite/mpo/multiline.jl | 131 ++++ test/testsuite/operators/lazysum.jl | 278 +++++++++ test/testsuite/operators/projection.jl | 20 + test/testsuite/operators/window.jl | 66 ++ test/testsuite/states/arithmetic.jl | 264 ++++++++ test/testsuite/states/finitemps.jl | 173 ++++++ test/testsuite/states/infinitemps.jl | 146 +++++ test/testsuite/states/multilinemps.jl | 59 ++ test/testsuite/states/quasiparticle.jl | 110 ++++ test/testsuite/states/windowmps.jl | 82 +++ test/testsuite/statmech/statmech.jl | 128 ++++ test/testsuite/symmetries/multifusion.jl | 175 ++++++ .../symmetries/sector_conventions.jl | 64 ++ .../timeevolution/clusterexpansion.jl | 169 ++++++ test/testsuite/timeevolution/taylorcluster.jl | 139 +++++ test/testsuite/timeevolution/timestep.jl | 376 ++++++++++++ test/timeevolution/clusterexpansion.jl | 161 +---- test/timeevolution/taylorcluster.jl | 134 +--- test/timeevolution/timestep.jl | 365 +---------- 68 files changed, 5556 insertions(+), 4721 deletions(-) create mode 100644 test/testsuite/TestSuite.jl create mode 100644 test/testsuite/algorithms/anyonic_braiding.jl create mode 100644 test/testsuite/algorithms/changebonds.jl create mode 100644 test/testsuite/algorithms/correlators.jl create mode 100644 test/testsuite/algorithms/fidelity_susceptibility.jl create mode 100644 test/testsuite/algorithms/periodic_boundary.jl create mode 100644 test/testsuite/algorithms/propagator.jl create mode 100644 test/testsuite/excitations/excitations.jl create mode 100644 test/testsuite/groundstate/approximate.jl create mode 100644 test/testsuite/groundstate/groundstate.jl create mode 100644 test/testsuite/hamiltonian/derivatives.jl create mode 100644 test/testsuite/hamiltonian/finite.jl create mode 100644 test/testsuite/hamiltonian/infinite.jl create mode 100644 test/testsuite/hamiltonian/mixed.jl create mode 100644 test/testsuite/mpo/dense_mpo.jl create mode 100644 test/testsuite/mpo/finite.jl create mode 100644 test/testsuite/mpo/infinite.jl create mode 100644 test/testsuite/mpo/mixed.jl create mode 100644 test/testsuite/mpo/multiline.jl create mode 100644 test/testsuite/operators/lazysum.jl create mode 100644 test/testsuite/operators/projection.jl create mode 100644 test/testsuite/operators/window.jl create mode 100644 test/testsuite/states/arithmetic.jl create mode 100644 test/testsuite/states/finitemps.jl create mode 100644 test/testsuite/states/infinitemps.jl create mode 100644 test/testsuite/states/multilinemps.jl create mode 100644 test/testsuite/states/quasiparticle.jl create mode 100644 test/testsuite/states/windowmps.jl create mode 100644 test/testsuite/statmech/statmech.jl create mode 100644 test/testsuite/symmetries/multifusion.jl create mode 100644 test/testsuite/symmetries/sector_conventions.jl create mode 100644 test/testsuite/timeevolution/clusterexpansion.jl create mode 100644 test/testsuite/timeevolution/taylorcluster.jl create mode 100644 test/testsuite/timeevolution/timestep.jl diff --git a/test/algorithms/anyonic_braiding.jl b/test/algorithms/anyonic_braiding.jl index 8ea2d1121..0ab400ff6 100644 --- a/test/algorithms/anyonic_braiding.jl +++ b/test/algorithms/anyonic_braiding.jl @@ -4,175 +4,11 @@ println(" ------------------------------------ ") -using .TestSetup -using Test, TestExtras -using MPSKit -using BlockTensorKit -using MPSKit: AC_hamiltonian, AC2_hamiltonian, AC2, eigsolve -using TensorKit -using LinearAlgebra -using Random - -# when reinterpreting an MPO as an MPS ρ, there are contractions which -# braid the ancilla leg past the operator's virtual leg -# this turned out to be incorrect in several places -# the tests below check that the braiding is consistent with the dense trace for finite systems, -# and with the MPO-level overlap transfer for infinite systems - -@testset "density-matrix sandwich (Fibonacci)" begin - V = Vect[FibonacciAnyon](:I => 1, :τ => 1) - ρ = randn(ComplexF64, V ⊗ V, V ⊗ V) - O = randn(ComplexF64, V ⊗ V, V ⊗ V) - ρ_mps = convert(FiniteMPS, FiniteMPO(ρ)) - - ref = tr(ρ' * O * ρ) - @test dot(FiniteMPO(ρ), FiniteMPO(O) * FiniteMPO(ρ)) ≈ ref - @test dot(ρ_mps, FiniteMPO(O), ρ_mps) ≈ ref -end - -@testset "expectation values with density matrices (Fibonacci)" begin - V = Vect[FibonacciAnyon](:I => 1, :τ => 1) - L = 3 - h = randn(ComplexF64, V ⊗ V, V ⊗ V) - h = h + h' - H = FiniteMPOHamiltonian(fill(V, L), [(i, i + 1) => h for i in 1:(L - 1)]) - - # free workaround: flatten jordan tensors into plain mpo tensors - # don't convert to tensormap, this is an exponential cost in L - H_dense = FiniteMPO([W isa AbstractBlockTensorMap ? TensorMap(W) : W for W in parent(H)]) - ρ = make_time_mpo(H, 0.1, TaylorCluster(; N = 2); imaginary_evolution = true) - ref = dot(ρ, H_dense * ρ) / norm(ρ)^2 - - @test expectation_value(ρ, H) ≈ ref - @test expectation_value(ρ, H_dense) ≈ ref - - # these require the densification in _mpo_to_mps (mpo.jl#90) - Hd = convert(TensorMap, H) - @test expectation_value(ρ, FiniteMPO(Hd)) ≈ ref - @test expectation_value(ρ, (1, 2) => h) + expectation_value(ρ, (2, 3) => h) ≈ ref -end - -@testset "braiding consistency in derivative operators (Fibonacci)" begin - V = Vect[FibonacciAnyon](:I => 1, :τ => 1) - L = 3 - T = ComplexF64 - ρ, O = FiniteMPO(randn(T, V^L, V^L)), FiniteMPO(randn(T, V^L, V^L)) - ρ_mps = convert(FiniteMPS, ρ) - exact = convert(FiniteMPS, O * ρ) - ψ0 = FiniteMPS(randn, T, fill(V ⊗ V', L), Vect[FibonacciAnyon](:I => 24, :τ => 24)) - fid(ψ) = abs(dot(ψ, exact)) / (norm(ψ) * norm(exact)) - - # unprepared AC reached by one-site approximate - ψ1, = approximate(ψ0, (O, ρ_mps), DMRG(; tol = 1.0e-10, maxiter = 50)) # simply doesn't converge without the fix - @test fid(ψ1) ≈ 1 atol = 1.0e-6 - - # unprepared AC2 reached by two-site approximate - ψ2, = approximate(ψ0, (O, ρ_mps), DMRG2(; tol = 1.0e-10, maxiter = 50, trunc = truncrank(64))) # simply doesn't converge without the fix - @test fid(ψ2) ≈ 1 atol = 1.0e-6 - - # prepared AC, reached by any eigensolver - # τ' fix on mpo_derivatives.jl#252 allows the eigensolver to converge at all, - # even though the eigenproblem is degenerate over the ancilla leg - A = randn(T, V^L, V^L) - Oh = FiniteMPO(A + A') - ψ3, _, info3 = find_groundstate(ρ_mps, Oh, DMRG(; tol = 1.0e-10, maxiter = 50)) - @test info3.galerkin ≤ 1.0e-10 - - # prepared AC2, reached by two-site TDVP - Ohd = convert(TensorMap, Oh) - ρd = randn(T, V^L, V^L) - ρ_mps2 = convert(FiniteMPS, FiniteMPO(ρd)) - dt = 0.02 - ρ_exact_d = exp(-im * dt * Ohd) * ρd - target = convert(FiniteMPS, FiniteMPO(ρ_exact_d)) - ψt, = timestep(ρ_mps2, Oh, 0.0, dt, TDVP2(; trunc = truncrank(64))) - tdvp2_fid = abs(dot(ψt, target)) / (norm(ψt) * norm(target)) - @test tdvp2_fid ≈ 1 atol = 1.0e-6 # fidelity was previously high, but not ≈ 1 -end - -braid_spaces = if fast_tests - (Vect[FibonacciAnyon](:I => 1, :τ => 1),) -else - ( - ℂ^2, # bosonic - Vect[FermionParity](0 => 1, 1 => 1), # fermionic - Vect[FibonacciAnyon](:I => 1, :τ => 1), # anyonic - ) -end - -# build L-site operator that's the identity everywhere except at site i -# where it's op with physical space V -_embed(L, op, i) = foldl(⊗, (ntuple(_ -> id(space(op, 1)), i - 1)..., op, ntuple(_ -> id(space(op, 1)), L - i - numout(op) + 1)...)) - -@testset "density-matrix braiding, finite: $(sectortype(V))" for V in braid_spaces - L = 3 - Random.seed!(4321) - ρd = randn(ComplexF64, V^L, V^L) - Od = randn(ComplexF64, V^L, V^L) - ρ, O = FiniteMPO(ρd), FiniteMPO(Od) - ψ = convert(FiniteMPS, ρ) - ref = tr(ρd' * Od * ρd) - nrm = tr(ρd' * ρd) - - # both routes to <ρ|O|ρ> must reproduce the dense trace - @test dot(ρ, O * ρ) ≈ ref - @test dot(ψ, O, ψ) ≈ ref - - # each local derivative operator must reproduce the same scalar - envs = environments(ψ, O, ψ) - for i in 1:L, prepare in (false, true) - h = AC_hamiltonian(i, ψ, O, ψ, envs; prepare) - @test dot(ψ.AC[i], h * ψ.AC[i]) ≈ ref - end - for i in 1:(L - 1) - x = AC2(ψ, i) - h = AC2_hamiltonian(i, ψ, O, ψ, envs; prepare = false) - @test dot(x, h * x) ≈ ref - end - - # The prepared two-site derivative is a separate contraction whose crossings - # are encoded in `braid` levels rather than τ, along with fused legs - for i in 1:(L - 1) - x = AC2(ψ, i) - h = AC2_hamiltonian(i, ψ, O, ψ, envs; prepare = true) - @test dot(x, h * x) ≈ ref - end - - # local observables measured on a density matrix - o1 = randn(ComplexF64, V, V) - o1 = o1 + o1' - @test expectation_value(ρ, 2 => o1) ≈ tr(ρd' * _embed(L, o1, 2) * ρd) / nrm - o2 = randn(ComplexF64, V ⊗ V, V ⊗ V) - o2 = o2 + o2' - i = rand(1:(L - 1)) - @test expectation_value(ρ, (i, i + 1) => o2) ≈ tr(ρd' * _embed(L, o2, i) * ρd) / nrm -end - -# overlap per unit cell of two infinite MPOs as leading eigenvalue of -# TM that contracts both physical legs directly -# contains no braiding tensor -> independent reference for the MPS-MPO-MPS sandwich -function mpo_overlap(mpo1, mpo2) - N = length(mpo1) - function step(v) - for i in 1:N - @plansor w[-1; -2] := v[1; 2] * conj(mpo1[i][1 3; 4 -1]) * mpo2[i][2 3; 4 -2] - v = w - end - return v - end - v0 = randn(ComplexF64, space(mpo1[N], 4)' ← space(mpo2[N], 4)') - vals, = eigsolve(step, v0, 1, :LM; krylovdim = 30) - return vals[1] -end - -@testset "density-matrix braiding, infinite: $(sectortype(V))" for V in braid_spaces - Vv = V isa ComplexSpace ? ℂ^3 : V ⊕ V - for N in (1, 2) - Random.seed!(2024) - ρ = InfiniteMPO([randn(ComplexF64, Vv ⊗ V ← V ⊗ Vv) for _ in 1:N]) - O = InfiniteMPO([randn(ComplexF64, V ⊗ V ← V ⊗ V) for _ in 1:N]) - ref = mpo_overlap(ρ, O * ρ) / mpo_overlap(ρ, ρ) - ψ = convert(InfiniteMPS, ρ) - @test dot(ψ, O, ψ) ≈ ref - end -end +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite + +TestSuite.algorithms_anyonic_braiding_sandwich(Vector; fast = fast_tests) +TestSuite.algorithms_anyonic_braiding_expectation_values(Vector; fast = fast_tests) +TestSuite.algorithms_anyonic_braiding_derivatives(Vector; fast = fast_tests) +TestSuite.algorithms_anyonic_braiding_finite(Vector; fast = fast_tests) +TestSuite.algorithms_anyonic_braiding_infinite(Vector; fast = fast_tests) diff --git a/test/algorithms/changebonds.jl b/test/algorithms/changebonds.jl index 93b894b84..c38f3b070 100644 --- a/test/algorithms/changebonds.jl +++ b/test/algorithms/changebonds.jl @@ -4,300 +4,17 @@ println(" ----------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ -using Random - -spacelist = if fast_tests - [(ℙ^4, ℙ^3)] -else - [(ℙ^4, ℙ^3), (Rep[SU₂](1 => 1), Rep[SU₂](0 => 2, 1 => 2, 2 => 1))] -end -maxbond(ψ) = maximum(i -> dim(left_virtualspace(ψ, i)), 2:length(ψ)) - -@testset "MPO $(spacetype(pspace))" for (pspace, Dspace) in spacelist - nn = rand(ComplexF64, pspace * pspace, pspace * pspace) - nn += nn' - H0 = InfiniteMPOHamiltonian(PeriodicVector(fill(pspace, 1)), (1, 2) => nn) - Δt = 0.1 - expH = make_time_mpo(H0, Δt, WII()) - - O = MPSKit.DenseMPO(expH) - Op = periodic_boundary_conditions(O, 10) - Op′ = changebonds(Op, SvdCut(; trunc = truncrank(5))) - - @test dim(space(Op′[5], 1)) < dim(space(Op[5], 1)) -end - -@testset "InfiniteMPS $(spacetype(pspace))" for (pspace, Dspace) in spacelist - nn = rand(ComplexF64, pspace * pspace, pspace * pspace) - nn += nn' - H0 = InfiniteMPOHamiltonian(PeriodicVector(fill(pspace, 1)), (1, 2) => nn) - - # test rand_expand - for unit_cell_size in 2:3 - H = repeat(H0, unit_cell_size) - state = InfiniteMPS(fill(pspace, unit_cell_size), fill(Dspace, unit_cell_size)) - - state_re = changebonds( - state, RandExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace))) - ) - @test dot(state, state_re) ≈ 1 atol = 1.0e-8 - end - # test optimal_expand - for unit_cell_size in 2:3 - H = repeat(H0, unit_cell_size) - state = InfiniteMPS(fill(pspace, unit_cell_size), fill(Dspace, unit_cell_size)) - - state_oe, _ = changebonds( - state, H, OptimalExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace))) - ) - @test dot(state, state_oe) ≈ 1 atol = 1.0e-8 - end - # test VUMPSSvdCut - for unit_cell_size in [1, 2, 3, 4] - H = repeat(H0, unit_cell_size) - state = InfiniteMPS(fill(pspace, unit_cell_size), fill(Dspace, unit_cell_size)) - - state_vs, _ = changebonds(state, H, VUMPSSvdCut(; trunc = notrunc())) - @test dim(left_virtualspace(state, 1)) < dim(left_virtualspace(state_vs, 1)) - - state_vs_tr = changebonds(state_vs, SvdCut(; trunc = truncrank(dim(Dspace)))) - @test dim(right_virtualspace(state_vs_tr, 1)) < dim(right_virtualspace(state_vs, 1)) - end -end - -@testset "FiniteMPS $(spacetype(pspace))" for (pspace, Dspace) in spacelist - #random nn interaction - L = 10 - nn = rand(ComplexF64, pspace * pspace, pspace * pspace) - nn += nn' - H = FiniteMPOHamiltonian(fill(pspace, L), (i, i + 1) => nn for i in 1:(L - 1)) - - state = FiniteMPS(L, pspace, Dspace) - - state_re = changebonds( - state, RandExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace))) - ) - @test maxbond(state_re) > maxbond(state) - @test dot(state, state_re) ≈ 1 atol = 1.0e-8 - - state_oe, _ = changebonds( - state, H, OptimalExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace))) - ) - @test maxbond(state_oe) > maxbond(state) - @test dot(state, state_oe) ≈ 1 atol = 1.0e-8 - - state_se, _ = changebonds( - state, H, - SketchedExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace)), oversampling = 4) - ) - @test maxbond(state_se) > maxbond(state) - @test dot(state, state_se) ≈ 1 atol = 1.0e-8 - - state_tr = changebonds(state_oe, SvdCut(; trunc = truncrank(dim(Dspace)))) - @test dim(left_virtualspace(state_tr, 5)) < dim(left_virtualspace(state_oe, 5)) -end - -# Regression: CBE expanders must not crash on bonds with no expansion content. A charged -# single-particle fermionic (fℤ₂-graded) state carries only one parity sector on the bonds away -# from the particle, so the projected two-site update `adjoint(NL) * AC2 * adjoint(NR)` is -# *structurally exactly zero* there. `OptimalExpand`/`RandExpand` used to `normalize!` that zero -# tensor, producing NaNs that crashed the SVD (`ArgumentError: invalid argument #4 to LAPACK -# call`); they must instead skip such bonds, leaving the state unchanged. -@testset "CBE zero-content bonds (graded)" begin - L = 6 - H = kitaev_model(ComplexF64, Trivial; t = 1.0, mu = 0, Delta = 0, L = L) - P = physicalspace(H)[1] - S = typeof(P) - Vtriv = unitspace(P) - Vodd = S(c => dim(P, c) for c in sectors(P) if !isone(c)) - # single particle created on the last site: interior bonds carry only the even sector, so - # the expansion content on them is exactly zero - state = FiniteMPS( - [isometry(ComplexF64, (k <= L ? Vtriv : Vodd) ⊗ P, (k < L ? Vtriv : Vodd)) for k in 1:L] - ) - - state_oe, _ = changebonds(state, H, OptimalExpand(; trunc = truncrank(2))) - @test abs(dot(state, state_oe)) ≈ 1 atol = 1.0e-8 - - state_re = changebonds(state, RandExpand(; trunc = truncrank(2))) - @test abs(dot(state, state_re)) ≈ 1 atol = 1.0e-8 -end - -# density-matrix-style MPS: each site carries two physical legs (ket ⊗ bra). The operator-free -# bond-change algorithms (`RandExpand` expansion, `SvdCut` truncation) must handle the extra -# physical leg. Operator-based expanders (`OptimalExpand`/`SketchedExpand`) make use of a -# one-sided MPO application on the first physical leg. -density_matrix_spacelist = if fast_tests - [(ℙ^2 ⊗ (ℙ^2)', ℙ^6)] -else - [ - (ℙ^2 ⊗ (ℙ^2)', ℙ^6), - (Rep[SU₂](1 // 2 => 1) ⊗ Rep[SU₂](1 // 2 => 1)', Rep[SU₂](0 => 4, 1 => 3)), - ] -end - -@testset "Density-matrix FiniteMPS $(spacetype(pcomp))" for (pcomp, Dspace) in density_matrix_spacelist - Random.seed!(2468) - L = 8 - - ψ = FiniteMPS(rand, ComplexF64, fill(pcomp, L), Dspace) - @test numind(ψ.AC[L ÷ 2]) == 4 # two physical legs + two virtual legs - - # RandExpand grows the bond while preserving the state (norm-preserving expansion) - ψ_re = changebonds(ψ, RandExpand(; trunc = truncrank(dim(Dspace) * 2))) - @test numind(ψ_re.AC[L ÷ 2]) == 4 - @test abs(dot(ψ, ψ_re)) ≈ 1 atol = 1.0e-8 - @test maxbond(ψ_re) > maxbond(ψ) - - # SvdCut truncates the enlarged bond back down, leaving a normalized state - ψ_tr = changebonds(ψ_re, SvdCut(; trunc = truncrank(dim(Dspace)))) - @test maxbond(ψ_tr) < maxbond(ψ_re) - @test abs(dot(ψ, ψ_tr)) ≈ 1 atol = 1.0e-8 - - # use random time-evolution MPO to test operator-based expanders - pspace = pcomp[1] - nn = rand(ComplexF64, pspace * pspace, pspace * pspace) - nn += nn' - H = FiniteMPOHamiltonian(fill(pspace, L), (i, i + 1) => nn for i in 1:(L - 1)) - beta = 0.1 - O = make_time_mpo(H, beta, TaylorCluster(; N = 2); imaginary_evolution = true) - - ψ_oe, _ = changebonds( - ψ, O, OptimalExpand(; trunc = truncrank(dim(Dspace) * 2)) - ) - @test maxbond(ψ_oe) > maxbond(ψ) - @test dot(ψ, ψ_oe) ≈ 1 atol = 1.0e-8 - - ψ_se, _ = changebonds( - ψ, O, - SketchedExpand(; trunc = truncrank(dim(Dspace) * 2), oversampling = 4) - ) - @test maxbond(ψ_se) > maxbond(ψ) - @test dot(ψ, ψ_se) ≈ 1 atol = 1.0e-8 -end - -@testset "MultilineMPS $(spacetype(pspace))" for (pspace, Dspace) in spacelist - o = rand(ComplexF64, pspace * pspace, pspace * pspace) - mpo = MultilineMPO(o) - - t = rand(ComplexF64, Dspace * pspace, Dspace) - state = MultilineMPS(fill(t, 1, 1)) - - state_re = changebonds( - state, RandExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace))) - ) - @test dot(state, state_re) ≈ 1 atol = 1.0e-8 - - state_oe, _ = changebonds( - state, mpo, OptimalExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace))) - ) - @test dot(state, state_oe) ≈ 1 atol = 1.0e-8 - - state_tr = changebonds(state_oe, SvdCut(; trunc = truncrank(dim(Dspace)))) - - @test dim(left_virtualspace(state_tr, 1, 1)) < dim(left_virtualspace(state_oe, 1, 1)) -end - -# Regression: the `FiniteMPO` `SvdCut` sweep used to absorb `S * Vᴴ` into the next site without -# renormalizing, so the carried gauge tensor grew by `sqrt(dim(P))` per site. Once its singular -# values passed `sqrt(floatmax)`, `TensorKit`'s block-wise norm overflowed and a relative `trunc` -# discarded *every* singular value, silently returning a zero operator. -@testset "SvdCut scale overflow (long FiniteMPO)" begin - Random.seed!(1234) - pspace = ℙ^16 - L = 300 # (sqrt(dim(P)))^L must exceed sqrt(floatmax) to trigger the old overflow - h = rand(ComplexF64, pspace, pspace) - h += h' - nn = h ⊗ h # keep the term low-rank so that the dense MPO stays small at this length - H = FiniteMPOHamiltonian(fill(pspace, L), (i, i + 1) => nn for i in 1:(L - 1)) - - O = MPSKit.DenseMPO(H) - O′ = changebonds(O, SvdCut(; trunc = trunctol(; rtol = 1.0e-12))) - - @test all(i -> dim(left_virtualspace(O′, i)) > 0, 1:L) - @test maxbond(O′) == maxbond(O) - - ψ = FiniteMPS(rand, ComplexF64, L, pspace, ℙ^4) - @test expectation_value(ψ, O′) ≈ expectation_value(ψ, O) rtol = 1.0e-12 -end - -# Regression: `changebonds(::FiniteMPOHamiltonian, ::SvdCut)` threw a `BoundsError` for any -# Hamiltonian with long-range terms — a star geometry leaves entirely zero rows/columns in the -# Jordan `B`/`C` blocks, and adding such a `SparseBlockTensorMap` into a dense `BlockTensorMap` -# tripped over the structurally absent entries. -@testset "SvdCut on long-range FiniteMPOHamiltonian" begin - Random.seed!(4321) - pspace = ℙ^2 - L = 8 - nn = rand(ComplexF64, pspace * pspace, pspace * pspace) - nn += nn' - # every site couples to a single centre site, so all channels are proportional and compressible - centre = L ÷ 2 - H = FiniteMPOHamiltonian( - fill(pspace, L), (min(i, centre), max(i, centre)) => nn for i in 1:L if i != centre - ) - - H′ = changebonds(H, SvdCut(; trunc = trunctol(; rtol = 1.0e-12))) - @test maxbond(H′) < maxbond(H) - - ψ = FiniteMPS(rand, ComplexF64, L, pspace, ℙ^4) - @test expectation_value(ψ, H′) ≈ expectation_value(ψ, H) rtol = 1.0e-10 -end - -@testset "SvdCut on InfiniteMPOHamiltonian $(spacetype(pspace))" for (pspace, Dspace) in spacelist - Random.seed!(2468) - nn = rand(ComplexF64, pspace * pspace, pspace * pspace) - nn += nn' - L = 3 - lattice = PeriodicVector(fill(pspace, L)) - H0 = InfiniteMPOHamiltonian(lattice, (i, i + 1) => nn for i in 1:L) + - InfiniteMPOHamiltonian(lattice, (i, i + 2) => 0.5^i * nn for i in 1:L) - # adding the terms twice duplicates every interaction channel - H = H0 + 2 * H0 - ψ = InfiniteMPS(rand, ComplexF64, fill(pspace, L), fill(Dspace, L)) - - H′ = changebonds(H, SvdCut(; trunc = trunctol(; atol = 1.0e-12))) - @test all(i -> dim(right_virtualspace(H′[i])) ≤ dim(right_virtualspace(H0[i])), 1:L) - @test expectation_value(ψ, H′) ≈ expectation_value(ψ, H) rtol = 1.0e-10 - - # exponentially decaying interactions, written out as explicit terms - Z = rand(ComplexF64, pspace, pspace) - Z += Z' - H_exp = InfiniteMPOHamiltonian(PeriodicVector([pspace]), (1, 1 + r) => 0.5^r * (Z ⊗ Z) for r in 1:20) - H_exp′ = changebonds(H_exp, SvdCut(; trunc = trunctol(; rtol = 1.0e-5))) - @test dim(right_virtualspace(H_exp′[1])) < dim(right_virtualspace(H_exp[1])) - ψ1 = InfiniteMPS(rand, ComplexF64, [pspace], [Dspace]) - @test expectation_value(ψ1, H_exp′) ≈ expectation_value(ψ1, H_exp) rtol = 1.0e-4 - - # purely on-site terms have no channels to compress - H_onsite = InfiniteMPOHamiltonian(PeriodicVector([pspace]), (1,) => Z) - @test expectation_value(ψ1, changebonds(H_onsite, SvdCut(; trunc = truncrank(0)))) ≈ - expectation_value(ψ1, H_onsite) -end - -# Regression: RandExpand used to be able to introduce a NaN entanglement entropy. -@testset "RandExpand does not introduce NaN entropy" begin - ψ = InfiniteMPS([ℂ^2], [ℂ^5]) - ψ = changebonds(ψ, RandExpand(; trunc = truncrank(2))) - @test !isnan(sum(entropy(ψ))) - @test !isnan(sum(entropy(ψ, 2))) -end - -# Regression: changebonds on an InfiniteMPS with a >1 unit cell and per-site distinct bond -# dimensions used to error. -@testset "changebonds with non-uniform unit cells" begin - ψ = InfiniteMPS([ℂ^2, ℂ^2, ℂ^2], [ℂ^2, ℂ^3, ℂ^4]) - H = repeat(transverse_field_ising(), 3) - ψ1, envs = changebonds(ψ, H, OptimalExpand(; trunc = truncrank(2))) - @test ψ1 isa InfiniteMPS - @test norm(ψ1) ≈ 1 - - ψ2 = changebonds(ψ, RandExpand(; trunc = truncrank(2))) - @test ψ2 isa InfiniteMPS - @test norm(ψ2) ≈ 1 -end +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite + +TestSuite.algorithms_changebonds_mpo(Vector; fast = fast_tests) +TestSuite.algorithms_changebonds_infinitemps(Vector; fast = fast_tests) +TestSuite.algorithms_changebonds_finitemps(Vector; fast = fast_tests) +TestSuite.algorithms_changebonds_cbe_zero_content(Vector; fast = fast_tests) +TestSuite.algorithms_changebonds_density_matrix(Vector; fast = fast_tests) +TestSuite.algorithms_changebonds_multilinemps(Vector; fast = fast_tests) +TestSuite.algorithms_changebonds_svdcut_overflow(Vector; fast = fast_tests) +TestSuite.algorithms_changebonds_svdcut_longrange(Vector; fast = fast_tests) +TestSuite.algorithms_changebonds_svdcut_infinite_hamiltonian(Vector; fast = fast_tests) +TestSuite.algorithms_changebonds_randexpand_nan(Vector; fast = fast_tests) +TestSuite.algorithms_changebonds_nonuniform_unitcell(Vector; fast = fast_tests) diff --git a/test/algorithms/correlators.jl b/test/algorithms/correlators.jl index d285e9c92..ed5584a7e 100644 --- a/test/algorithms/correlators.jl +++ b/test/algorithms/correlators.jl @@ -4,49 +4,8 @@ println(" ----------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "correlation length / entropy" begin - ψ = InfiniteMPS([ℙ^2], [ℙ^10]) - H = force_planar(transverse_field_ising()) - ψ, = find_groundstate(ψ, H, VUMPS(; verbosity = 0)) - len_crit = correlation_length(ψ; sector = leftunit(ψ)) - entrop_crit = entropy(ψ) - - H = force_planar(transverse_field_ising(; g = 4)) - ψ, = find_groundstate(ψ, H, VUMPS(; verbosity = 0)) - len_gapped = correlation_length(ψ; sector = leftunit(ψ)) - entrop_gapped = entropy(ψ) - - @test len_crit > len_gapped - @test real(entrop_crit) > real(entrop_gapped) -end - -@testset "expectation value / correlator" begin - g = 4.0 - ψ = InfiniteMPS(ℂ^2, ℂ^10) - H = transverse_field_ising(; g) - ψ, = find_groundstate(ψ, H, VUMPS(; verbosity = 0)) - - @test expectation_value(ψ, H) ≈ - expectation_value(ψ, 1 => -2g * S_x()) + expectation_value(ψ, (1, 2) => -4S_z_S_z()) - Z_mpo = MPSKit.add_util_leg(S_z()) - G = correlator(ψ, Z_mpo, Z_mpo, 1, 2:5) - G2 = correlator(ψ, S_z_S_z(), 1, 3:2:5) - @test isapprox(G[2], G2[1], atol = 1.0e-2) - @test isapprox(last(G), last(G2), atol = 1.0e-2) - @test isapprox(G[1], expectation_value(ψ, (1, 2) => S_z_S_z()), atol = 1.0e-2) - @test isapprox(G[2], expectation_value(ψ, (1, 3) => S_z_S_z()), atol = 1.0e-2) - - # the sites have to be ordered `i < j`: every `correlator` method funnels its - # arguments through the range version, so all three signatures have to throw - @test_throws ArgumentError correlator(ψ, Z_mpo, Z_mpo, 3, 2) - @test_throws ArgumentError correlator(ψ, Z_mpo, Z_mpo, 3, 1:2) - @test_throws ArgumentError correlator(ψ, S_z_S_z(), 3, 2) - # equal sites are not allowed either - @test_throws ArgumentError correlator(ψ, Z_mpo, Z_mpo, 2, 2) -end +TestSuite.algorithms_correlators_entropy(Vector; fast = fast_tests) +TestSuite.algorithms_correlators_expectation_value(Vector; fast = fast_tests) diff --git a/test/algorithms/fidelity_susceptibility.jl b/test/algorithms/fidelity_susceptibility.jl index 60fc26977..7a684851b 100644 --- a/test/algorithms/fidelity_susceptibility.jl +++ b/test/algorithms/fidelity_susceptibility.jl @@ -4,41 +4,7 @@ println(" -------------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "fidelity susceptibility" begin - X = TensorMap(ComplexF64[0 1; 1 0], ℂ^2 ← ℂ^2) - Z = TensorMap(ComplexF64[1 0; 0 -1], ℂ^2 ← ℂ^2) - - H_X = InfiniteMPOHamiltonian(X) - H_ZZ = InfiniteMPOHamiltonian(Z ⊗ Z) - - hamiltonian(λ) = H_ZZ + λ * H_X - analytical_susceptibility(λ) = abs(1 / (16 * λ^2 * (λ^2 - 1))) - - for λ in [1.05, 2.0, 4.0] - H = hamiltonian(λ) - ψ = InfiniteMPS([ℂ^2], [ℂ^16]) - ψ, envs, = find_groundstate(ψ, H, VUMPS(; maxiter = 100, verbosity = 0)) - - numerical_susceptibility = fidelity_susceptibility(ψ, H, [H_X], envs; maxiter = 10) - @test numerical_susceptibility[1, 1] ≈ analytical_susceptibility(λ) atol = 1.0e-2 - - # test if the finite fid sus approximates the analytical one with increasing system size - fin_en = map([20, 15, 10]) do L - Hfin = open_boundary_conditions(hamiltonian(λ), L) - H_Xfin = open_boundary_conditions(H_X, L) - ψ = FiniteMPS(rand, ComplexF64, L, ℂ^2, ℂ^16) - ψ, envs, = find_groundstate(ψ, Hfin, DMRG(; verbosity = 0)) - numerical_susceptibility = fidelity_susceptibility( - ψ, Hfin, [H_Xfin], envs; maxiter = 10 - ) - return numerical_susceptibility[1, 1] / L - end - @test issorted(abs.(fin_en .- analytical_susceptibility(λ))) - end -end +TestSuite.algorithms_fidelity_susceptibility(Vector; fast = fast_tests) diff --git a/test/algorithms/periodic_boundary.jl b/test/algorithms/periodic_boundary.jl index 2fe7535c5..bb356cae8 100644 --- a/test/algorithms/periodic_boundary.jl +++ b/test/algorithms/periodic_boundary.jl @@ -4,40 +4,7 @@ println(" ------------------------------------ ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "periodic boundary conditions" begin - Hs = [transverse_field_ising(), heisenberg_XXX(), classical_ising(), sixvertex()] - for N in 2:6 - for H in Hs - TH = convert(TensorMap, periodic_boundary_conditions(H, N)) - @test TH ≈ - permute(TH, ((vcat(N, 1:(N - 1))...,), (vcat(2N, (N + 1):(2N - 1))...,))) - end - end - - # non-self-dual virtual space - fast_tests || let V = U1Space(0 => 1, 1 => 1), P = U1Space(0 => 1, 1 => 1) - mpo = InfiniteMPO([randn(ComplexF64, V ⊗ P ← P ⊗ V)]) - for N in 2:4 - TH = convert(TensorMap, periodic_boundary_conditions(mpo, N)) - @test TH ≈ - permute(TH, ((vcat(N, 1:(N - 1))...,), (vcat(2N, (N + 1):(2N - 1))...,))) - end - end - - # fermionic tests - h = f_hopping(Float64, Trivial) - H = InfiniteMPOHamiltonian([space(h, 1)], (1, 2) => h) - for N in 3:5 - H_periodic = periodic_boundary_conditions(H, N) - terms = [(i, i + 1) => h for i in 1:(N - 1)] - push!(terms, (1, N) => permute(h, ((2, 1), (4, 3)))) - H_periodic2 = FiniteMPOHamiltonian(physicalspace(H_periodic), terms) - @test H_periodic ≈ H_periodic2 - end -end +TestSuite.algorithms_periodic_boundary(Vector; fast = fast_tests) diff --git a/test/algorithms/propagator.jl b/test/algorithms/propagator.jl index eb0173229..158e56c77 100644 --- a/test/algorithms/propagator.jl +++ b/test/algorithms/propagator.jl @@ -4,182 +4,10 @@ println(" ----------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ -# KrylovKit is not a direct test dependency; reach the solver types through MPSKit's re-export -const GMRES = MPSKit.KrylovKit.GMRES -const CG = MPSKit.KrylovKit.CG -const BiCGStab = MPSKit.KrylovKit.BiCGStab -const DynamicTol = MPSKit.DynamicTol +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -verbosity_conv = 1 - -# helper: relative residual of (a₀ + a₁·A)·x = b, computed densely (robust for small systems; -# MPS-level `+`/`-` does not reliably cancel across states of differing bond structure) -function rel_residual(A, x, b, a₀, a₁) - Am = convert(TensorMap, A) - xv = convert(TensorMap, x) - bv = convert(TensorMap, b) - return norm(a₀ * xv + a₁ * (Am * xv) - bv) / norm(bv) -end - -@testset "linear_solve FiniteMPS" verbose = true begin - L = 8 - H = force_planar(-transverse_field_ising(; L, g = -4)) - gs, = find_groundstate(FiniteMPS(L, ℙ^2, ℙ^16), H; verbosity = verbosity_conv) - E₀ = real(expectation_value(gs, H)) - - # resolvent of an eigenstate: (z − H)⁻¹|gs⟩ = 1/(z − E₀)|gs⟩, so ⟨gs|x⟩ = 1/(z − E₀). - z = E₀ + 0.5 + 0.3im - predicted = 1 / (z - E₀) - - @testset "resolvent, single-site, formulation $flav" for flav in (Galerkin(), LeastSquares()) - solver = flav isa LeastSquares ? CG(; tol = 1.0e-12) : GMRES(; tol = 1.0e-12) - alg = DMRGSolve(; formulation = flav, solver, tol = 1.0e-10, verbosity = 0) - # solve (z − H) x = gs ⟺ a₀ = z, a₁ = −1 - x, envs, info = linear_solve(complex(copy(gs)), H, gs, alg; a₀ = z, a₁ = -1) - @test info.converged && convergence_measure(info) == info.galerkin - @test dot(gs, x) ≈ predicted atol = 1.0e-6 - @test rel_residual(H, x, gs, z, -1) < 1.0e-6 - end - - @testset "resolvent, two-site (adaptive χ), formulation $flav" for flav in - (Galerkin(), LeastSquares()) - solver = flav isa LeastSquares ? CG(; tol = 1.0e-12) : GMRES(; tol = 1.0e-12) - alg = DMRGSolve2(; - formulation = flav, solver, trunc = truncrank(16), tol = 1.0e-10, verbosity = 0 - ) - x, = linear_solve(complex(copy(gs)), H, gs, alg; a₀ = z, a₁ = -1) - @test dot(gs, x) ≈ predicted atol = 1.0e-5 - @test rel_residual(H, x, gs, z, -1) < 1.0e-5 - end - - @testset "matches propagator (correction vector)" begin - alg_ls = DMRGSolve(; solver = GMRES(; tol = 1.0e-12), tol = 1.0e-10, verbosity = 0) - x, = linear_solve(complex(copy(gs)), H, gs, alg_ls; a₀ = z, a₁ = -1) - for f in (NaiveInvert(), Jeckelmann()) - g_prop, = propagator(gs, z, H, DynamicalDMRG(; flavour = f, tol = 1.0e-10, verbosity = 0)) - @test dot(gs, x) ≈ g_prop atol = 1.0e-5 - end - end - - @testset "adaptive tolerances (incl. BiCGStab)" begin - # DynamicTol works for any solver, including short-recurrence BiCGStab (retunes only `tol`) - alg_bicg = DMRGSolve(; - solver = DynamicTol(BiCGStab(; tol = 1.0e-12); tol_min = 1.0e-14, tol_max = 1.0e-4), - tol = 1.0e-9, verbosity = 0 - ) - x, = linear_solve(complex(copy(gs)), H, gs, alg_bicg; a₀ = z, a₁ = -1) - @test dot(gs, x) ≈ predicted atol = 1.0e-5 - @test rel_residual(H, x, gs, z, -1) < 1.0e-5 - - # adaptive-by-default keyword path (DynamicTol-wrapped GMRES) - x2, = linear_solve(complex(copy(gs)), H, gs; a₀ = z, a₁ = -1, tol = 1.0e-9, verbosity = 0) - @test dot(gs, x2) ≈ predicted atol = 1.0e-5 - @test rel_residual(H, x2, gs, z, -1) < 1.0e-5 - end - - @testset "positive-definite solve via keyword interface" begin - # s ≫ spectrum ⇒ (s − H) is positive-definite; the CG path is selected by `isposdef` - s = E₀ + 50.0 - # eigenstate RHS: exact solution 1/(s − E₀) · gs, real and positive - x, = linear_solve(copy(gs), H, gs; a₀ = s, a₁ = -1, isposdef = true, tol = 1.0e-10, verbosity = 0) - @test dot(gs, x) ≈ 1 / (s - E₀) atol = 1.0e-8 - @test rel_residual(H, x, gs, s, -1) < 1.0e-8 - end - - @testset "general RHS, plain A·x = b against dense" begin - # shift H to a well-conditioned, invertible operator A = s·I − H (s ≫ spectrum) - s = E₀ + 50.0 - b = normalize!(FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^8)) - alg = DMRGSolve2(; - solver = CG(; tol = 1.0e-12), trunc = truncrank(64), tol = 1.0e-9, verbosity = 0 - ) & DMRGSolve(; solver = CG(; tol = 1.0e-12), tol = 1.0e-9, verbosity = 0) - x, = linear_solve(complex(copy(b)), H, b, alg; a₀ = s, a₁ = -1) - @test rel_residual(H, x, b, s, -1) < 1.0e-4 - end -end - -@testset "linear_solve WindowMPS" verbose = true begin - # `LeastSquares` builds the `A†b` term from the mixed sandwich ⟨x|A|b⟩, which is the only way to - # reach it for a window operator (there is no `*(::WindowMPOHamiltonian, ::WindowMPS)`) - N = 10 - H = transverse_field_ising(; g = -4) - Ω, = find_groundstate(InfiniteMPS(ℂ^2, ℂ^10), H, VUMPS(; verbosity = 0)) - XΩ = WindowMPS(Ω, N) - H_w = WindowMPOHamiltonian(H, 1:N) - - E₀ = expectation_value(XΩ, H_w) - z = E₀ + 0.5 + 0.3im - predicted = 1 / (z - E₀) - - @testset "resolvent, formulation $flav" for flav in (Galerkin(), LeastSquares()) - solver = flav isa LeastSquares ? CG(; tol = 1.0e-12) : GMRES(; tol = 1.0e-12) - alg = DMRGSolve(; formulation = flav, solver, tol = 1.0e-10, verbosity = 0) - x, = linear_solve(XΩ, H_w, XΩ, alg; a₀ = z, a₁ = -1) - @test dot(XΩ, x) ≈ predicted atol = 1.0e-6 - end -end - -@testset "Dynamical DMRG FiniteMPS" verbose = true begin - L = 10 - H = force_planar(-transverse_field_ising(; L, g = -4)) - gs, = find_groundstate(FiniteMPS(L, ℙ^2, ℙ^10), H; verbosity = verbosity_conv) - E₀ = expectation_value(gs, H) - - vals = (-0.5:0.2:0.5) .+ E₀ - eta = 0.3im - - predicted = [1 / (v + eta - E₀) for v in vals] - - @testset "Flavour $f" for f in (Jeckelmann(), NaiveInvert()) - alg = DynamicalDMRG(; flavour = f, verbosity = 0, tol = 1.0e-8) - data = map(vals) do v - result, = propagator(gs, v + eta, H, alg) - return result - end - @test data ≈ predicted atol = 1.0e-8 - end - - # `trunc` prepends a two-site sweep, so the bond dimension of the initial guess need not - # already be large enough: start from χ = 2 and let it grow back to the χ = 10 of the solution - @testset "trunc (adaptive χ), flavour $f" for f in (Jeckelmann(), NaiveInvert()) - alg = DynamicalDMRG(; - flavour = f, verbosity = 0, tol = 1.0e-8, trunc = truncrank(16) - ) - init = FiniteMPS(L, ℙ^2, ℙ^2) - data = map(vals) do v - result, = propagator(gs, v + eta, H, alg; init) - return result - end - @test data ≈ predicted atol = 1.0e-6 - end -end - -@testset "Dynamical DMRG WindowMPS" verbose = true begin - N = 20 - - H = transverse_field_ising(g = -4) - Ω = InfiniteMPS(ComplexSpace(2), ComplexSpace(20)) - - (Ω, _) = find_groundstate(Ω, H, VUMPS(verbosity = verbosity_conv)) - XΩ = WindowMPS(Ω, N) - H_w = WindowMPOHamiltonian(H, 1:N) - - gs_en = expectation_value(XΩ, H_w) - vals = range(gs_en - 1.0, gs_en + 1.0, length = 5) - eta = 0.3im - predicted = [1 / (v + eta - gs_en) for v in vals] - - @testset "Flavour $f" for f in (Jeckelmann(), NaiveInvert()) - alg = DynamicalDMRG(; flavour = f, verbosity = 0, tol = 1.0e-8) - data = map(vals) do v - result, = propagator(XΩ, v + eta, H_w, alg) - return result - end - @test data ≈ predicted atol = 1.0e-8 - end -end +TestSuite.algorithms_propagator_linear_solve_finitemps(Vector; fast = fast_tests) +TestSuite.algorithms_propagator_linear_solve_windowmps(Vector; fast = fast_tests) +TestSuite.algorithms_propagator_dynamical_dmrg_finitemps(Vector; fast = fast_tests) +TestSuite.algorithms_propagator_dynamical_dmrg_windowmps(Vector; fast = fast_tests) diff --git a/test/excitations/excitations.jl b/test/excitations/excitations.jl index c0636e23f..6c396eab0 100644 --- a/test/excitations/excitations.jl +++ b/test/excitations/excitations.jl @@ -4,118 +4,7 @@ println(" ----------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -verbosity_conv = 1 - -@testset "excitations" verbose = true begin - @testset "infinite (ham)" begin - H = repeat(force_planar(heisenberg_XXX()), 2) - ψ = InfiniteMPS([ℙ^3, ℙ^3], [ℙ^48, ℙ^48]) - ψ, envs, _ = find_groundstate( - ψ, H; maxiter = 400, verbosity = verbosity_conv, tol = 1.0e-10 - ) - energies, ϕs = @testinferred excitations( - H, QuasiparticleAnsatz(), Float64(pi), ψ, envs - ) - @test energies[1] ≈ 0.41047925 atol = 1.0e-4 - @test variance(ϕs[1], H) < 1.0e-8 - end - fast_tests || @testset "infinite sector convention" begin - g = 4 - H = repeat(transverse_field_ising(ComplexF64, Z2Irrep; g), 2) - V = Z2Space(0 => 24, 1 => 24) - ψ = InfiniteMPS(physicalspace(H), [V, V]) - ψ, envs = find_groundstate(ψ, H, VUMPS(; tol = 1.0e-10, maxiter = 400)) - - # testing istrivial and istopological - momentum = 0 - exc0, qp0 = excitations(H, QuasiparticleAnsatz(), momentum, ψ; sector = Z2Irrep(0)) - exc1, qp1 = excitations(H, QuasiparticleAnsatz(), momentum, ψ; sector = Z2Irrep(1)) - @test isapprox(first(exc1), abs(2 * (g - 1)); atol = 1.0e-6) # charged excitation lower in energy - @test first(exc0) > 2 * first(exc1) - @test variance(qp1[1], H) < 1.0e-8 - end - @testset "infinite (mpo)" begin - H = repeat(sixvertex(), 2) - ψ = InfiniteMPS([ℂ^2, ℂ^2], [ℂ^10, ℂ^10]) - ψ, envs, _ = leading_boundary( - ψ, H, VUMPS(; maxiter = 400, verbosity = verbosity_conv, tol = 1.0e-10) - ) - energies, ϕs = @testinferred excitations( - H, QuasiparticleAnsatz(), [0.0, Float64(pi / 2)], ψ, envs; verbosity = 0 - ) - @test abs(energies[1]) > abs(energies[2]) # has a minimum at pi/2 - end - - @testset "infinite (mpo) - quasiparticle input" begin - # regression test: handing an `InfiniteQP` to an `InfiniteMPO`, rather than a - # momentum, used to build the effective Hamiltonian out of the (still undefined) - # `H_eff` instead of `H`, so every call through this entry point threw an - # `UndefVarError`. The momentum entry point is unaffected, as it converts to a - # `MultilineMPO` first. The classical Ising transfer matrix is used here (rather than - # the six-vertex model above) because its quasiparticle eigenproblem is well enough - # conditioned to compare the two entry points at tight tolerance. - H = classical_ising(; β = 0.4) - ψ = InfiniteMPS([ℂ^2], [ℂ^12]) - ψ, envs, = leading_boundary(ψ, H, VUMPS(; maxiter = 400, verbosity = 0, tol = 1.0e-12)) - - alg = QuasiparticleAnsatz(; tol = 1.0e-10) - for momentum in (0.0, Float64(pi / 2)) - ϕ₀ = MPSKit.LeftGaugedQP(rand, ψ, ψ; momentum) - Es, ϕs = excitations(H, alg, ϕ₀, envs, envs; num = 1) - - # same spectrum as the momentum entry point, which routes through MultilineMPO - Es_momentum, = excitations(H, alg, momentum, ψ, envs; num = 1) - @test Es[1] ≈ Es_momentum[1] atol = 1.0e-8 - - # the operator really is `H`: the result is an eigenpair of the corresponding - # effective excitation Hamiltonian - E = MPSKit.effective_excitation_renormalization_energy(H, ϕs[1], envs, envs) - H_eff = MPSKit.EffectiveExcitationHamiltonian(H, envs, envs, E) - @test norm(H_eff(ϕs[1], alg.alg_environments) - Es[1] * ϕs[1]) < 1.0e-8 - end - end - - @testset "finite" begin - verbosity = verbosity_conv - H_inf = force_planar(transverse_field_ising()) - ψ_inf = InfiniteMPS([ℙ^2], [ℙ^10]) - ψ_inf, envs, _ = find_groundstate(ψ_inf, H_inf; maxiter = 400, verbosity, tol = 1.0e-9) - energies, ϕs = @inferred excitations(H_inf, QuasiparticleAnsatz(), 0.0, ψ_inf, envs) - inf_en = energies[1] - - fin_en = map([20, 10]) do len - H = force_planar(transverse_field_ising(; L = len)) - ψ = FiniteMPS(rand, ComplexF64, len, ℙ^2, ℙ^10) - ψ, envs, = find_groundstate(ψ, H; verbosity) - - # find energy with quasiparticle ansatz - energies_QP, ϕs = @testinferred excitations(H, QuasiparticleAnsatz(), ψ, envs) - @test variance(ϕs[1], H) < 1.0e-6 - - # find energy with normal dmrg - for gsalg in ( - DMRG(; verbosity, tol = 1.0e-6), - DMRG2(; verbosity, tol = 1.0e-6, trunc = trunctol(; atol = 1.0e-4)), - ) - energies_dm, _ = @testinferred excitations(H, FiniteExcited(; gsalg), ψ; num = 3) - @test energies_dm[1] ≈ energies_QP[1] + expectation_value(ψ, H, envs) atol = 1.0e-4 - @test issorted(real.(energies_dm)) - end - - # find energy with Chepiga ansatz - energies_ch, _ = @testinferred excitations(H, ChepigaAnsatz(), ψ, envs) - @test energies_ch[1] ≈ energies_QP[1] + expectation_value(ψ, H, envs) atol = 1.0e-4 - energies_ch2, _ = @testinferred excitations(H, ChepigaAnsatz2(), ψ, envs) - @test energies_ch2[1] ≈ energies_QP[1] + expectation_value(ψ, H, envs) atol = 1.0e-4 - return energies_QP[1] - end - - @test issorted(abs.(fin_en .- inf_en)) - end -end +TestSuite.excitations_excitations(Vector; fast = fast_tests) diff --git a/test/groundstate/approximate.jl b/test/groundstate/approximate.jl index 14e8d6801..b1ae11520 100644 --- a/test/groundstate/approximate.jl +++ b/test/groundstate/approximate.jl @@ -4,280 +4,7 @@ println(" ----------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ -using Random +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -verbosity_conv = 1 - -# fixtures for the `Zipup` testsets -zipup_spacelist = if fast_tests - [(ℙ^4, ℙ^3, 4)] -else - [(ℙ^4, ℙ^3, 4), (Rep[SU₂](1 => 1), Rep[SU₂](0 => 2, 1 => 2, 2 => 1), 8)] -end - -function _random_mpo_mps(pspace, Dspace, L; elt = ComplexF64) - Random.seed!(1357) - Wspace = Dspace - Vspaces = [unitspace(Wspace); fill(Wspace, L - 1); unitspace(Wspace)] - O = FiniteMPO( - [rand(ComplexF64, Vspaces[i] ⊗ pspace ← pspace ⊗ Vspaces[i + 1]) for i in 1:L] - ) - ψ = FiniteMPS(rand, elt, L, pspace, Dspace) - return O, ψ -end - -@testset "approximate" verbose = true begin - verbosity = verbosity_conv - @testset "mpo * infinite ≈ infinite" begin - ψ = InfiniteMPS([ℙ^2, ℙ^2], [ℙ^10, ℙ^10]) - ψ0 = InfiniteMPS([ℙ^2, ℙ^2], [ℙ^12, ℙ^12]) - - H = force_planar(repeat(transverse_field_ising(; g = 4), 2)) - - dt = 1.0e-3 - sW1 = make_time_mpo(H, dt, TaylorCluster(; N = 3, compression = true, extension = true)) - sW2 = make_time_mpo(H, dt, WII()) - W1 = MPSKit.DenseMPO(sW1) - W2 = MPSKit.DenseMPO(sW2) - - ψ1, _ = approximate(ψ0, (sW1, ψ), VOMPS(; verbosity)) - # VOMPS runs the AC and C projections of a site concurrently, so the allocator serving them - # is shared; the serial arm takes the other branch of that choice - ψ2, _ = with_scheduler(MPSKit.SerialScheduler()) do - return approximate(ψ0, (W2, ψ), VOMPS(; verbosity)) - end - - ψ3, _ = approximate(ψ0, (W1, ψ), IDMRG(; verbosity)) - ψ4, _, info4 = approximate(ψ0, (sW2, ψ), IDMRG2(; trunc = truncrank(12), verbosity)) - ψ5, _, info5 = timestep(ψ, H, 0.0, dt, TDVP()) - ψ6 = changebonds(W1 * ψ, SvdCut(; trunc = truncrank(12))) - - @test info4.truncation_errors isa AbstractVector - @test length(info4.truncation_errors) == length(ψ0) - @test !haskey(info5, :truncation_errors) # infinite one-site TDVP never truncates - - @test abs(dot(ψ1, ψ5)) ≈ 1.0 atol = dt - @test abs(dot(ψ3, ψ5)) ≈ 1.0 atol = dt - @test abs(dot(ψ6, ψ5)) ≈ 1.0 atol = dt - @test abs(dot(ψ2, ψ4)) ≈ 1.0 atol = dt - - nW1 = changebonds(W1, SvdCut(; trunc = trunctol(; atol = dt))) # this should be a trivial mpo now - @test dim(space(nW1[1], 1)) == 1 - end - - # every row of a multiline problem has to land in its own row of the result, including at the - # unit cell edge, which used to write the last row's update into the wrong row - @testset "multiline IDMRG2 rows" begin - Random.seed!(42) - dt = 1.0e-2 - Wa = make_time_mpo(force_planar(repeat(transverse_field_ising(; g = 4), 2)), dt, WII()) - Wb = make_time_mpo(force_planar(repeat(transverse_field_ising(; g = 0.5), 2)), dt, WII()) - ϕa, ϕb = (InfiniteMPS([ℙ^2, ℙ^2], [ℙ^8, ℙ^8]) for _ in 1:2) - ψ0a, ψ0b = (InfiniteMPS([ℙ^2, ℙ^2], [ℙ^10, ℙ^10]) for _ in 1:2) - alg = IDMRG2(; trunc = truncrank(10), verbosity, maxiter = 50) - - multi, = approximate( - MultilineMPS([ψ0a, ψ0b]), (MultilineMPO([Wa, Wb]), MultilineMPS([ϕa, ϕb])), alg - ) - ref_a, = approximate(ψ0a, (Wa, ϕa), alg) - ref_b, = approximate(ψ0b, (Wb, ϕb), alg) - - # row `r + 1` of the result approximates `O[r] * ϕ[r]` - @test abs(dot(multi[2], ref_a)) ≈ 1 atol = 1.0e-8 - @test abs(dot(multi[1], ref_b)) ≈ 1 atol = 1.0e-8 - end - - finite_algs = [DMRG(; verbosity), DMRG2(; verbosity, trunc = truncrank(10))] - @testset "finitemps1 ≈ finitemps2" for alg in finite_algs - a = FiniteMPS(10, ℂ^2, ℂ^10) - b = FiniteMPS(10, ℂ^2, ℂ^20) - - before = abs(dot(a, b)) - - a = first(approximate(a, b, alg)) - - after = abs(dot(a, b)) - - @test before < after - end - - @testset "sparse_mpo * finitemps1 ≈ finitemps2" for alg in finite_algs - L = 10 - ψ₁ = FiniteMPS(L, ℂ^2, ℂ^30) - ψ₂ = FiniteMPS(L, ℂ^2, ℂ^25) - - H = transverse_field_ising(; g = 4.0, L) - τ = 1.0e-3 - - expH = make_time_mpo(H, τ, WI) - ψ₂, = approximate(ψ₂, (expH, ψ₁), alg) - normalize!(ψ₂) - ψ₂′, = timestep(ψ₁, H, 0.0, τ, TDVP()) - @test abs(dot(ψ₁, ψ₁)) ≈ abs(dot(ψ₂, ψ₂′)) atol = 0.001 - end - - @testset "dense_mpo * finitemps1 ≈ finitemps2" for alg in finite_algs - L = 10 - ψ₁ = FiniteMPS(L, ℂ^2, ℂ^20) - ψ₂ = FiniteMPS(L, ℂ^2, ℂ^10) - - O = classical_ising(; L) - ψ₂, = approximate(ψ₂, (O, ψ₁), alg) - - @test norm(O * ψ₁ - ψ₂) ≈ 0 atol = 0.001 - end - - @testset "Finite MPO-MPS zip-up $(spacetype(pspace))" for (pspace, Dspace, _) in zipup_spacelist - L = 6 - - # dense MPO - O, ψ = _random_mpo_mps(pspace, Dspace, L) - O_copy = copy(O) - ψ_copy = copy(ψ) - - trunc = trunctol(; atol = 1.0e-10) - ref = changebonds(O * ψ, SvdCut(; trunc); normalize = false) - - # both sweep directions, with and without the zip-down pass - for left_to_right in (true, false), trunc′ in (trunc, (notrunc(), trunc)) - got, info = approximate((O, ψ), Zipup(; trunc = trunc′, left_to_right)) - @test norm(ref - got) / norm(ref) < 1.0e-10 - @test length(info.truncation_errors) == length(ψ) - 1 - @test maximum(info.truncation_errors) < 1.0e-10 - @test !haskey(info, :converged) && isnothing(convergence_measure(info)) - end - - @test norm(ψ - ψ_copy) < 1.0e-12 - @test all(i -> norm(O[i] - O_copy[i]) < 1.0e-12, 1:length(O)) - - # sparse MPO - nn = rand(ComplexF64, pspace * pspace, pspace * pspace) - nn += nn' - H = FiniteMPOHamiltonian(fill(pspace, L), (i, i + 1) => nn for i in 1:(L - 1)) - τ = 1.0e-3 - expH = make_time_mpo(H, τ, WI) - - # reference via TDVP - ref_s, = timestep(ψ, H, 0.0, τ, TDVP()) - normalize!(ref_s) - - # both sweep directions, with and without the zip-down pass - for left_to_right in (true, false), trunc′ in (trunc, (notrunc(), trunc)) - got_s, info = approximate((expH, ψ), Zipup(; trunc = trunc′, left_to_right)) - normalize!(got_s) - @test norm(ref_s - got_s) < 0.002 - @test norm(ψ - got_s) > 0.002 - @test maximum(info.truncation_errors) < 1.0e-10 - end - end - - @testset "Paeckel two-stage zip-up $(spacetype(pspace)), left_to_right = $left_to_right" for - (pspace, Dspace, Dcut) in zipup_spacelist, left_to_right in (true, false) - O, ψ = _random_mpo_mps(pspace, Dspace, 6) - rtol = 1.0e-8 - final_trunc = truncrank(Dcut) & truncerror(; rtol) - zipup_trunc = truncrank(2Dcut) & truncerror(; rtol = rtol / 10) - - ref_tr = changebonds(O * ψ, SvdCut(; trunc = final_trunc); normalize = false) - got_one_sweep, info_one_sweep = approximate((O, ψ), Zipup(; trunc = final_trunc, left_to_right)) - got_two_sweep, _ = approximate( - (O, ψ), Zipup(; trunc = (zipup_trunc, final_trunc), left_to_right) - ) - @test maximum(info_one_sweep.truncation_errors) > 0 - - err_one_sweep = norm(ref_tr - got_one_sweep) / norm(ref_tr) - err_two_sweep = norm(ref_tr - got_two_sweep) / norm(ref_tr) - @test err_two_sweep < err_one_sweep / 2 - @test maximum(i -> dim(left_virtualspace(got_one_sweep, i)), 2:length(got_one_sweep)) <= Dcut - @test maximum(i -> dim(left_virtualspace(got_two_sweep, i)), 2:length(got_two_sweep)) <= Dcut - end - - @testset "In-place zip-up $(spacetype(pspace)), left_to_right = $left_to_right" for - (pspace, Dspace, Dcut) in zipup_spacelist, left_to_right in (true, false) - O, ψ = _random_mpo_mps(pspace, Dspace, 6) - alg = Zipup(; trunc = (truncrank(2Dcut), truncrank(Dcut)), left_to_right) - ref, info_ref = approximate((O, ψ), alg) - - # empty destination - dst = similar(ψ, ComplexF64) - got, info = approximate!(dst, (O, ψ), alg) - @test got === dst - @test norm(ref - got) / norm(ref) < 1.0e-12 - @test info.truncation_errors ≈ info_ref.truncation_errors - - # a destination with unrelated contents is overwritten entirely - dst = FiniteMPS(rand, ComplexF64, length(ψ), pspace, unitspace(Dspace) ⊕ Dspace ⊕ Dspace) - got, info = approximate!(dst, (O, ψ), alg) - @test norm(ref - got) / norm(ref) < 1.0e-12 - @test info.truncation_errors ≈ info_ref.truncation_errors - - # the input may serve as its own destination - got, info = approximate!(ψ, (O, ψ), alg) - @test got === ψ - @test norm(ref - got) / norm(ref) < 1.0e-12 - @test info.truncation_errors ≈ info_ref.truncation_errors - end - - @testset "Zip-up with non-trivial boundary spaces $(spacetype(pspace))" for (pspace, Dspace, _) in zipup_spacelist - Random.seed!(1357) - L = 4 - Vspaces = fill(Dspace, L + 1) - O = FiniteMPO( - [rand(ComplexF64, Vspaces[i] ⊗ pspace ← pspace ⊗ Vspaces[i + 1]) for i in 1:L] - ) - ψ = FiniteMPS(rand, ComplexF64, L, pspace, Dspace; left = Dspace, right = Dspace) - - trunc = trunctol(; atol = 1.0e-12) - got, _ = approximate((O, ψ), Zipup(; trunc)) - got_two_sweep, _ = approximate((O, ψ), Zipup(; trunc = (notrunc(), trunc))) - - # the boundary virtual spaces of the product are the fused ones, in both variants - for ψ′ in (got, got_two_sweep) - @test left_virtualspace(ψ′, 1) == fuse(left_virtualspace(ψ, 1) ⊗ left_virtualspace(O, 1)) - @test right_virtualspace(ψ′, L) == fuse(right_virtualspace(ψ, L) ⊗ right_virtualspace(O, L)) - end - @test norm(got - got_two_sweep) / norm(got) < 1.0e-10 - @test norm(got) ≈ norm(O * ψ) - end - - @testset "Zip-up with non-square MPO $(spacetype(pspace))" for (pspace, Dspace, _) in zipup_spacelist - Random.seed!(1357) - L = 4 - pspace′ = pspace ⊕ unitspace(pspace) # output physical space, different from the input one - Vspaces = [unitspace(Dspace); fill(Dspace, L - 1); unitspace(Dspace)] - O = FiniteMPO( - [rand(ComplexF64, Vspaces[i] ⊗ pspace′ ← pspace ⊗ Vspaces[i + 1]) for i in 1:L] - ) - ψ = FiniteMPS(rand, ComplexF64, L, pspace, Dspace) - - trunc = trunctol(; atol = 1.0e-12) - ref = O * ψ - got, _ = approximate((O, ψ), Zipup(; trunc)) - got_two_sweep, _ = approximate((O, ψ), Zipup(; trunc = (notrunc(), trunc))) - - @test all(i -> physicalspace(got, i) == pspace′, 1:L) - @test norm(ref - got) / norm(ref) < 1.0e-10 - @test norm(ref - got_two_sweep) / norm(ref) < 1.0e-10 - - # the reverse product is not defined - @test_throws SpaceMismatch approximate((O, got), Zipup(; trunc)) - end - - @testset "Zip-up scalar type promotion $(spacetype(pspace))" for (pspace, Dspace, _) in zipup_spacelist - O, ψ = _random_mpo_mps(pspace, Dspace, 6; elt = Float64) - alg = Zipup(; trunc = trunctol(; atol = 1.0e-10)) - - got, _ = approximate((O, ψ), alg) - @test scalartype(got) === ComplexF64 - ref = changebonds(O * ψ, SvdCut(; trunc = trunctol(; atol = 1.0e-10)); normalize = false) - @test norm(ref - got) / norm(ref) < 1.0e-10 - - # a real destination cannot hold the complex result - @test_throws ArgumentError approximate!(ψ, (O, ψ), alg) - end -end +TestSuite.groundstate_approximate(Vector; fast = fast_tests) diff --git a/test/groundstate/groundstate.jl b/test/groundstate/groundstate.jl index 58e349692..e92655180 100644 --- a/test/groundstate/groundstate.jl +++ b/test/groundstate/groundstate.jl @@ -4,559 +4,12 @@ println(" ----------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ -using Random - -verbosity_full = 5 -verbosity_conv = 1 - -@testset "FiniteMPS ground state" verbose = true begin - tol = 1.0e-8 - g = 4.0 - D = 6 - L = 10 - - models = [ - "nearest-neighbour" => force_planar(transverse_field_ising(; g, L)), - "long-range, real scalartype" => force_planar(long_range_ising(Float64; g, L)), - ] - - @testset "$name" for (name, H) in models - @testset "DMRG" begin - ψ₀ = FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^D) - v₀ = variance(ψ₀, H) - - # test logging - ψ, envs, info = find_groundstate( - ψ₀, H, DMRG(; verbosity = verbosity_full, maxiter = 2) - ) - - ψ, envs, info = find_groundstate( - ψ, H, DMRG(; verbosity = verbosity_conv, maxiter = 10), envs - ) - v = variance(ψ, H) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test v < v₀ - @test v < 1.0e-2 - @test info.truncation_errors == zeros(L - 1) - end - - @testset "DMRG2" begin - ψ₀ = FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^D) - v₀ = variance(ψ₀, H) - trunc = truncrank(floor(Int, D * 1.5)) - # test logging - ψ, envs, info = find_groundstate( - ψ₀, H, DMRG2(; verbosity = verbosity_full, maxiter = 2, trunc) - ) - - ψ, envs, info = find_groundstate( - ψ, H, DMRG2(; verbosity = verbosity_conv, maxiter = 10, trunc), envs - ) - v = variance(ψ, H) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test v < v₀ - @test v < 1.0e-2 - - @test length(info.truncation_errors) == L - 1 - @test maximum(info.truncation_errors) > 0 - end - - @testset "CBEDMRG" begin - # start from a small bond so the bond expansion is exercised - ψ₀ = FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^(D ÷ 2)) - v₀ = variance(ψ₀, H) - expand = OptimalExpand(; trunc = truncrank(D ÷ 2)) - trunc = truncrank(D) - - # test logging - ψ, envs, info = find_groundstate( - ψ₀, H, DMRG(; verbosity = verbosity_full, maxiter = 2, alg_expand = expand, trunc) - ) - - ψ, envs, info = find_groundstate( - ψ, H, DMRG(; verbosity = verbosity_conv, maxiter = 10, alg_expand = expand, trunc), envs - ) - v = variance(ψ, H) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test v < v₀ - @test v < 1.0e-2 - # the bond should have grown to the truncation target - @test dim(left_virtualspace(ψ, L ÷ 2)) == D - end - - @testset "CBEDMRG (SketchedExpand)" begin - # randomized bond expansion at single-site cost. The sketch is redrawn every sweep, so an - # aggressive expansion (a large fraction of the bond) keeps the single-site Galerkin error - # noisy; a gentle per-sweep increment lets it converge like the deterministic expanders. - Random.seed!(1234) - ψ₀ = FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^(D ÷ 2)) - v₀ = variance(ψ₀, H) - expand = SketchedExpand(; trunc = truncrank(2), oversampling = 4) - trunc = truncrank(D) - - # test logging - ψ, envs, info = find_groundstate( - ψ₀, H, DMRG(; verbosity = verbosity_full, maxiter = 2, alg_expand = expand, trunc) - ) - - ψ, envs, info = find_groundstate( - ψ, H, DMRG(; verbosity = verbosity_conv, maxiter = 15, alg_expand = expand, trunc), envs - ) - v = variance(ψ, H) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test v < v₀ - @test v < 1.0e-2 - # the bond should have grown to the truncation target - @test dim(left_virtualspace(ψ, L ÷ 2)) == D - end - - @testset "DMRG3S" begin - # start from a small bond so the post-expansion is exercised, mirroring CBEDMRG above - Random.seed!(1234) - ψ₀ = FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^(D ÷ 2)) - v₀ = variance(ψ₀, H) - alg_gauge = DMRG3S(0.1, ExponentialDecay(0.7)) # TODO: match final constructor API - trunc = truncrank(D) - - # test logging - ψ, envs, info = find_groundstate( - ψ₀, H, DMRG(; verbosity = verbosity_full, maxiter = 2, alg_gauge, trunc) - ) - - ψ, envs, info = find_groundstate( - ψ, H, DMRG(; verbosity = verbosity_conv, maxiter = 10, alg_gauge, trunc), envs - ) - v = variance(ψ, H) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test v < v₀ - @test v < 1.0e-2 - # the bond should have grown to the truncation target - @test dim(left_virtualspace(ψ, L ÷ 2)) == D - end - - @testset "GradientGrassmann" begin - ψ₀ = FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^D) - v₀ = variance(ψ₀, H) - - # test logging - ψ, envs, info = find_groundstate( - ψ₀, H, GradientGrassmann(; verbosity = verbosity_full, maxiter = 2) - ) - - ψ, envs, info = find_groundstate( - ψ, H, GradientGrassmann(; tol, verbosity = verbosity_conv, maxiter = 50), envs - ) - v = variance(ψ, H) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test v < v₀ && v < 1.0e-2 - end - end - - fast_tests || @testset "DMRG3S escapes local minimum (Hubig et al. 2015, Sec. VII A)" begin - L_heis = 20 - H_heis = heisenberg_XXX(ComplexF64, U1Irrep; spin = 1 // 2, L = L_heis) - - Random.seed!(1234) - ψ_bad = bad_initial_state(H_heis, L_heis) - - ψ_stuck, envs_stuck, info_stuck = find_groundstate( - ψ_bad, H_heis, DMRG(; verbosity = verbosity_conv, maxiter = 30) - ) - E_stuck = real(expectation_value(ψ_stuck, H_heis, envs_stuck)) - - alg_gauge = DMRG3S(0.1, ExponentialDecay(0.8)) - ψ_escape, envs_escape, info_escape = find_groundstate( - ψ_bad, H_heis, DMRG(; - verbosity = verbosity_conv, maxiter = 30, - alg_gauge, trunc = truncrank(20), - ) - ) - E_escape = real(expectation_value(ψ_escape, H_heis, envs_escape)) - - # paper reports E(α=0) = -6.35479, E(α≠0) = -8.6824724 for this L_heis = 20, S = 1/2 AFM setup - @test E_escape < E_stuck - 1.0 - @test isapprox(E_escape, -8.6824724; atol = 1.0e-4) - end -end - -@testset "GradientGrassmann preconditioner is positive definite" begin - # nearly rank-deficient bonds, as for the long-range model above, make the regularized - # inverse of `ρ = C * C'` ill-conditioned beyond `1 / eps` once the gradient is small - Grassmann = MPSKit.GrassmannMPS.Grassmann - Random.seed!(123) - V, P = ℙ^6, ℙ^2 - S = DiagonalTensorMap([1.0, 0.06, 2.0e-10, 1.5e-10, 5.0e-11, 1.0e-11], V) - @testset "$T" for T in (Float64, ComplexF64) - isdescent = map(1:50) do _ - AL = randisometry(T, V ⊗ P, V) - C = randisometry(T, V, V) * S * randisometry(T, V, V) - Δ = MPSKit.GrassmannMPS.rmul(Grassmann.project(randn(T, V ⊗ P, V), AL), C') - Δ = MPSKit.GrassmannMPS.scale!(Δ, 1.0e-9 / norm(Δ.Z)) - PΔ = MPSKit.GrassmannMPS.rmul_rho_inv_regularized(Δ, C; rtol = norm(Δ.Z)) - return real(Grassmann.inner(AL, Δ, PΔ)) > 0 - end - @test all(isdescent) - end -end - -@testset "InfiniteMPS ground state" verbose = true begin - tol = 1.0e-8 - g = 4.0 - D = 6 - - H_ref = force_planar(transverse_field_ising(; g)) - ψ = InfiniteMPS(ℙ^2, ℙ^D) - v₀ = variance(ψ, H_ref) - - H_ref_realT = force_planar(transverse_field_ising(Float64; g)) - @test variance(ψ, H_ref_realT) ≈ v₀ atol = 1.0e-10 - - - @testset "VUMPS (unit cell $unit_cell_size, $schedname)" for unit_cell_size in [1, 3], - (schedname, scheduler) in SCHEDULERS - - ψ₀ = repeat(InfiniteMPS(ℙ^2, ℙ^D), unit_cell_size) - H = repeat(H_ref, unit_cell_size) - - ψ′, envs, info = with_scheduler(scheduler) do - # test logging - ψ₁, = find_groundstate( - ψ₀, H, VUMPS(; tol, verbosity = verbosity_full, maxiter = 2) - ) - return find_groundstate(ψ₁, H, VUMPS(; tol, verbosity = verbosity_conv)) - end - v = variance(ψ′, H, envs) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test v < v₀ - @test v < 1.0e-2 - end - - @testset "VUMPS (long-range, real scalartype)" begin - H = force_planar(long_range_ising_infinite(Float64; g, L = 3)) - ψ₀ = InfiniteMPS(fill(ℙ^2, 3), fill(ℙ^D, 3)) - v₀ = variance(ψ₀, H) - - ψ′, envs, info = find_groundstate(ψ₀, H, VUMPS(; tol, verbosity = verbosity_conv, maxiter = 20)) - v = variance(ψ′, H, envs) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test v < v₀ - @test v < 1.0e-2 - end - - @testset "IDMRG" for unit_cell_size in [1, 3] - ψ = unit_cell_size == 1 ? InfiniteMPS(ℙ^2, ℙ^D) : repeat(ψ, unit_cell_size) - H = repeat(H_ref, unit_cell_size) - - # test logging - ψ, envs, info = find_groundstate( - ψ, H, IDMRG(; tol, verbosity = verbosity_full, maxiter = 2) - ) - - ψ, envs, info = find_groundstate(ψ, H, IDMRG(; tol, verbosity = verbosity_conv)) - v = variance(ψ, H, envs) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test !haskey(info, :truncation_errors) # single-site IDMRG never truncates - @test v < v₀ - @test v < 1.0e-2 - end - - @testset "IDMRG (long-range, real scalartype)" begin - H = force_planar(long_range_ising_infinite(Float64; g, L = 3)) - ψ₀ = InfiniteMPS(fill(ℙ^2, 3), fill(ℙ^D, 3)) - v₀ = variance(ψ₀, H) - - ψ, envs, info = find_groundstate(ψ₀, H, IDMRG(; tol, verbosity = verbosity_conv, maxiter = 20)) - v = variance(ψ, H, envs) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test v < v₀ - @test v < 1.0e-2 - end - - @testset "IDMRG2" begin - ψ = repeat(InfiniteMPS(ℙ^2, ℙ^D), 2) - H = repeat(H_ref, 2) - - trunc = trunctol(; atol = 1.0e-8) - - # test logging - ψ, envs, info = find_groundstate( - ψ, H, IDMRG2(; tol, verbosity = verbosity_full, maxiter = 2, trunc) - ) - - ψ, envs, info = find_groundstate( - ψ, H, IDMRG2(; tol, verbosity = verbosity_conv, trunc) - ) - v = variance(ψ, H, envs) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test v < v₀ - @test v < 1.0e-2 - - @test length(info.truncation_errors) == length(ψ) - @test maximum(info.truncation_errors) > 0 - end - - # Regression: IDMRG2 used to error on a non-abelian (SU2) unit cell due to a space mismatch. - @testset "IDMRG2 space mismatch (SU2)" begin - N = 6 - H = repeat(bilinear_biquadratic_model(ComplexF64, SU2Irrep; θ = atan(1 / 3)), N) - ψ₀ = InfiniteMPS( - fill(SU2Space(1 => 1), N), - fill(SU2Space(1 // 2 => 2, 3 // 2 => 1), N) - ) - alg = IDMRG2(; verbosity = 0, tol = 1.0e-5, trunc = truncrank(32)) - - ψ, envs, δ = find_groundstate(ψ₀, H, alg) # used to error - @test ψ isa InfiniteMPS - end - - # the gradient is computed concurrently over the unit cell, so the scheduler decides its - # allocator here too - @testset "GradientGrassmann (unit cell $unit_cell_size, $schedname)" for unit_cell_size in - [1, 3], (schedname, scheduler) in SCHEDULERS - - ψ₀ = repeat(InfiniteMPS(ℙ^2, ℙ^D), unit_cell_size) - H = repeat(H_ref, unit_cell_size) - - ψ′, envs, info = with_scheduler(scheduler) do - # test logging - ψ₁, = find_groundstate( - ψ₀, H, GradientGrassmann(; tol, verbosity = verbosity_full, maxiter = 2) - ) - return find_groundstate( - ψ₁, H, GradientGrassmann(; tol, verbosity = verbosity_conv) - ) - end - v = variance(ψ′, H, envs) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test v < v₀ - @test v < 1.0e-2 - end - - @testset "Combination" for unit_cell_size in [1, 3] - ψ = unit_cell_size == 1 ? InfiniteMPS(ℙ^2, ℙ^D) : repeat(ψ, unit_cell_size) - H = repeat(H_ref, unit_cell_size) - - alg = VUMPS(; tol = 100 * tol, verbosity = verbosity_conv, maxiter = 10) & - GradientGrassmann(; tol, verbosity = verbosity_conv, maxiter = 50) - ψ, envs, info = find_groundstate(ψ, H, alg) - - v = variance(ψ, H, envs) - - # test using low variance - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test v < v₀ - @test v < 1.0e-2 - end -end - -@testset "LazySum FiniteMPS ground state" verbose = true begin - tol = 1.0e-8 - D = 15 - atol = 1.0e-2 - L = 10 - - # test using XXZ model, Δ > 1 is gapped - spin = 1 - local_operators = [S_x_S_x(; spin), S_y_S_y(; spin), 1.7 * S_z_S_z(; spin)] - Pspace = space(local_operators[1], 1) - lattice = fill(Pspace, L) - - mpo_hamiltonians = map(local_operators) do O - return FiniteMPOHamiltonian(lattice, (i, i + 1) => O for i in 1:(L - 1)) - end - - H_lazy = LazySum(mpo_hamiltonians) - H = sum(H_lazy) - - ψ₀ = FiniteMPS(randn, ComplexF64, 10, ℂ^3, ℂ^D) - ψ₀, = find_groundstate(ψ₀, H; tol, verbosity = 1) - - @testset "DMRG" begin - # test logging passes - ψ, envs, info = find_groundstate( - ψ₀, H_lazy, DMRG(; tol, verbosity = verbosity_full, maxiter = 1) - ) - - # compare states - alg = DMRG(; tol, verbosity = verbosity_conv) - ψ, envs, info = find_groundstate(ψ, H_lazy, alg) - - @test abs(dot(ψ₀, ψ)) ≈ 1 atol = atol - end - - @testset "DMRG2" begin - # test logging passes - trunc = truncrank(floor(Int, D * 1.5)) - ψ, envs, info = find_groundstate( - ψ₀, H_lazy, DMRG2(; tol, verbosity = verbosity_full, maxiter = 1, trunc) - ) - - # compare states - alg = DMRG2(; tol, verbosity = verbosity_conv, trunc) - ψ, = find_groundstate(ψ₀, H, alg) - ψ_lazy, envs, info = find_groundstate(ψ₀, H_lazy, alg) - - @test abs(dot(ψ₀, ψ_lazy)) ≈ 1 atol = atol - end - - @testset "GradientGrassmann" begin - # test logging passes - ψ, envs, info = find_groundstate( - ψ₀, H_lazy, GradientGrassmann(; tol, verbosity = verbosity_full, maxiter = 2) - ) - - # compare states - alg = GradientGrassmann(; tol, verbosity = verbosity_conv) - ψ, = find_groundstate(ψ₀, H, alg) - ψ_lazy, envs, info = find_groundstate(ψ₀, H_lazy, alg) - - @test abs(dot(ψ₀, ψ_lazy)) ≈ 1 atol = atol - end -end - -@testset "LazySum InfiniteMPS ground state" verbose = true begin - tol = 1.0e-8 - D = 16 - atol = 1.0e-2 - - spin = 1 - local_operators = [S_x_S_x(; spin), S_y_S_y(; spin), 0.7 * S_z_S_z(; spin)] - Pspace = space(local_operators[1], 1) - lattice = PeriodicVector([Pspace]) - mpo_hamiltonians = map(local_operators) do O - return InfiniteMPOHamiltonian(lattice, (1, 2) => O) - end - - H_lazy = LazySum(mpo_hamiltonians) - H = sum(H_lazy) - - ψ₀ = InfiniteMPS(ℂ^3, ℂ^D) - ψ₀, = find_groundstate(ψ₀, H; tol, verbosity = 1) - - @testset "VUMPS" begin - # test logging passes - ψ, envs, info = find_groundstate( - ψ₀, H_lazy, VUMPS(; tol, verbosity = verbosity_full, maxiter = 2) - ) - - # compare states - alg = VUMPS(; tol, verbosity = verbosity_conv) - ψ, envs, info = find_groundstate(ψ, H_lazy, alg) - - @test abs(dot(ψ₀, ψ)) ≈ 1 atol = atol - end - - @testset "IDMRG" begin - # test logging passes - ψ, envs, info = find_groundstate( - ψ₀, H_lazy, IDMRG(; tol, verbosity = verbosity_full, maxiter = 2) - ) - - # compare states - alg = IDMRG(; tol, verbosity = verbosity_conv, maxiter = 300) - ψ, envs, info = find_groundstate(ψ, H_lazy, alg) - - @test abs(dot(ψ₀, ψ)) ≈ 1 atol = atol - end - - @testset "IDMRG2" begin - ψ₀′ = repeat(ψ₀, 2) - H_lazy′ = repeat(H_lazy, 2) - H′ = repeat(H, 2) - - trunc = truncrank(floor(Int, D * 1.5)) - # test logging passes - ψ, envs, info = find_groundstate( - ψ₀′, H_lazy′, IDMRG2(; tol, verbosity = verbosity_full, maxiter = 2, trunc) - ) - - # compare states - alg = IDMRG2(; tol, verbosity = verbosity_conv, trunc) - ψ, envs, info = find_groundstate(ψ, H_lazy′, alg) - - @test abs(dot(ψ₀′, ψ)) ≈ 1 atol = atol - end - - @testset "GradientGrassmann" begin - # test logging passes - ψ, envs, info = find_groundstate( - ψ₀, H_lazy, GradientGrassmann(; tol, verbosity = verbosity_full, maxiter = 2) - ) - - # compare states - alg = GradientGrassmann(; tol, verbosity = verbosity_conv) - ψ, envs, info = find_groundstate(ψ₀, H_lazy, alg) - - @test abs(dot(ψ₀, ψ)) ≈ 1 atol = atol - end -end - -@testset "leading_boundary" verbose = true begin - tol = 1.0e-4 - verbosity = verbosity_conv - D = 10 - D1 = 13 - algs = [ - VUMPS(; tol, verbosity), VOMPS(; tol, verbosity), - GradientGrassmann(; tol, verbosity), IDMRG(; tol, verbosity), - IDMRG2(; tol, verbosity, trunc = truncrank(D1)), - ] - mpo = force_planar(classical_ising()) - - ψ₀ = InfiniteMPS([ℙ^2], [ℙ^D]) - @testset "Infinite $i" for (i, alg) in enumerate(algs) - if alg isa IDMRG2 - ψ2 = repeat(ψ₀, 2) - mpo2 = repeat(mpo, 2) - ψ, envs = leading_boundary(ψ2, mpo2, alg) - @test dim(space(ψ.AL[1, 1], 1)) == dim(space(ψ₀.AL[1, 1], 1)) + (D1 - D) - @test expectation_value(ψ, mpo2, envs) ≈ 2.5337^2 atol = 1.0e-3 - else - ψ, envs = leading_boundary(ψ₀, mpo, alg) - ψ, envs = changebonds(ψ, mpo, OptimalExpand(; trunc = truncrank(D1 - D)), envs) - ψ, envs = leading_boundary(ψ, mpo, alg) - @test dim(space(ψ.AL[1, 1], 1)) == dim(space(ψ₀.AL[1, 1], 1)) + (D1 - D) - @test expectation_value(ψ, mpo, envs) ≈ 2.5337 atol = 1.0e-3 - end - end - - fast_tests || @testset "IDMRG2 growing bond dimension" begin - Random.seed!(1234) - V = Vect[Z2Irrep](0 => 1, 1 => 1) - O = randn(ComplexF64, V ⊗ V, V ⊗ V) - mpo = InfiniteMPO([O, O]) - P = physicalspace(O) - ψ₀ = InfiniteMPS([P, P], [V, V]) - ψ, envs = leading_boundary(ψ₀, mpo, IDMRG2(; verbosity = 0, maxiter = 1, trunc = truncrank(8))) - @test ψ isa InfiniteMPS - end -end +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite + +TestSuite.groundstate_finite(Vector; fast = fast_tests) +TestSuite.groundstate_grassmann_preconditioner(Vector; fast = fast_tests) +TestSuite.groundstate_infinite(Vector; fast = fast_tests) +TestSuite.groundstate_lazysum_finite(Vector; fast = fast_tests) +TestSuite.groundstate_lazysum_infinite(Vector; fast = fast_tests) +TestSuite.groundstate_leading_boundary(Vector; fast = fast_tests) diff --git a/test/hamiltonian/derivatives.jl b/test/hamiltonian/derivatives.jl index 246637444..045ba70df 100644 --- a/test/hamiltonian/derivatives.jl +++ b/test/hamiltonian/derivatives.jl @@ -4,119 +4,9 @@ println(" ---------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: C_hamiltonian, AC_hamiltonian, AC2_hamiltonian -using MPSKit: _transpose_front, _transpose_tail -using TensorKit -using BlockTensorKit: nonzero_length -using Random +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -Random.seed!(1234) - -# `JordanMPO_AC(2)_Hamiltonian` has two construction paths: a fast one, taken when every -# block already has the storagetype of the environments, and a converting one -# this converting path is reached when `scalartype(H) != scalartype(ψ)` and an on-site D block is passed -# nearest-neighbour models have an ending B block on every site (but the first) - -@testset "Jordan block structure of the long-range models" begin - H = long_range_ising(Float64; L = 4) - Hi = long_range_ising_infinite(Float64; L = 3) - - # nothing ends before the far end of the (long-range) bond - @test all(i -> nonzero_length(H[i].B) == 0, 1:3) - @test nonzero_length(H[4].B) == 1 - @test all(i -> nonzero_length(Hi[i].B) == 0, 1:2) - @test nonzero_length(Hi[3].B) == 1 - - # while the left virtual space is non-trivial everywhere but on the first site of - # the finite chain, so `E` is present exactly where `B` is not - @test size(H[1], 1) == 1 - @test all(i -> size(H[i], 1) > 1, 2:4) - @test all(i -> size(Hi[i], 1) > 1, 1:3) - - # every site carries an on-site term, which is what forces the converting constructor - @test all(i -> nonzero_length(H[i].D) == 1, 1:4) - @test all(i -> nonzero_length(Hi[i].D) == 1, 1:3) -end - -@testset "MPOHamiltonian derivatives: real operator, complex state" verbose = true begin - D = 8 - L_inf, L_fin = 3, 4 - ψ_inf = InfiniteMPS(randn, ComplexF64, fill(ℂ^2, L_inf), fill(ℂ^D, L_inf)) - ψ_fin = FiniteMPS(randn, ComplexF64, L_fin, ℂ^2, ℂ^D) - models = [ - "FiniteMPS, long-range" => (long_range_ising(Float64; L = L_fin), ψ_fin), - "FiniteMPS, nearest-neighbour" => (transverse_field_ising(Float64; g = 4.0, L = L_fin), ψ_fin), - "InfiniteMPS, long-range" => (long_range_ising_infinite(Float64; L = L_inf), ψ_inf), - "InfiniteMPS, nearest-neighbour" => (repeat(transverse_field_ising(Float64; g = 4.0), 3), ψ_inf), - ] - - @testset "$name" for (name, (H, ψ)) in models - @test scalartype(H) <: Real - @test scalartype(ψ) <: Complex - - # `complex(H)` takes the fast path, `H` the converting one: both must agree - Hc = complex(H) - @test scalartype(Hc) <: Complex - envs = environments(ψ, H, ψ) - envs_c = environments(ψ, Hc, ψ) - @test expectation_value(ψ, H, envs) ≈ expectation_value(ψ, Hc, envs_c) - - L = length(ψ) - bonds = isfinite(H) ? (1:(L - 1)) : (1:L) - - @testset "AC" begin - for site in 1:L - AC = ψ.AC[site] - @test AC_hamiltonian(site, ψ, H, ψ, envs)(AC) ≈ - AC_hamiltonian(site, ψ, Hc, ψ, envs_c)(AC) - end - end - - @testset "AC2" begin - for site in bonds - AC2 = _transpose_front(ψ.AC[site]) * _transpose_tail(ψ.AR[site + 1]) - @test AC2_hamiltonian(site, ψ, H, ψ, envs)(AC2) ≈ - AC2_hamiltonian(site, ψ, Hc, ψ, envs_c)(AC2) - end - end - - @testset "C" begin - for site in bonds - C = ψ.C[site] - @test C_hamiltonian(site, ψ, H, ψ, envs)(C) ≈ - C_hamiltonian(site, ψ, Hc, ψ, envs_c)(C) - end - end - end -end - -@testset "FiniteMPS derivatives reproduce the expectation value" verbose = true begin - # in mixed gauge the derivative operators contract everything but the center site(s), - # so their expectation value is the full energy, independent of where we sit - D = 8 - L = 10 - models = [ - "long-range" => long_range_ising(Float64; L = L), - "nearest-neighbour" => transverse_field_ising(Float64; g = 4.0, L = L), - ] - - @testset "$name" for (name, H) in models - for T in (Float64, ComplexF64) - ψ = normalize!(FiniteMPS(randn, T, L, ℂ^2, ℂ^D)) - envs = environments(ψ, H, ψ) - E = expectation_value(ψ, H, envs) - - for site in 1:L - AC = ψ.AC[site] - @test dot(AC, AC_hamiltonian(site, ψ, H, ψ, envs)(AC)) ≈ E - end - for site in 1:(L - 1) - AC2 = _transpose_front(ψ.AC[site]) * _transpose_tail(ψ.AR[site + 1]) - @test dot(AC2, AC2_hamiltonian(site, ψ, H, ψ, envs)(AC2)) ≈ E - end - end - end -end +TestSuite.hamiltonian_derivatives_jordan_structure(Vector; fast = fast_tests) +TestSuite.hamiltonian_derivatives_real_complex(Vector; fast = fast_tests) +TestSuite.hamiltonian_derivatives_finite_expval(Vector; fast = fast_tests) diff --git a/test/hamiltonian/finite.jl b/test/hamiltonian/finite.jl index 3fe4cd3ef..d2eb8729c 100644 --- a/test/hamiltonian/finite.jl +++ b/test/hamiltonian/finite.jl @@ -4,220 +4,9 @@ println(" ----------------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "Finite MPOHamiltonian" begin - L = 3 - T = ComplexF64 - mpoham_finite_Vs = fast_tests ? (ℂ^2,) : (ℂ^2, U1Space(-1 => 1, 0 => 1, 1 => 1)) - Ts = fast_tests ? (Float64,) : (Float64, ComplexF64) - for (T, V) in zip(Ts, mpoham_finite_Vs) - lattice = fill(V, L) - O₁ = randn(T, V, V) - O₁ += O₁' - E = id(storagetype(O₁), domain(O₁)) - O₂ = randn(T, V^2 ← V^2) - O₂ += O₂' - - H1 = FiniteMPOHamiltonian(lattice, i => O₁ for i in 1:L) - H2 = FiniteMPOHamiltonian(lattice, (i, i + 1) => O₂ for i in 1:(L - 1)) - H3 = FiniteMPOHamiltonian(lattice, 1 => O₁, (2, 3) => O₂, (1, 3) => O₂) - - @test scalartype(H1) == scalartype(H2) == scalartype(H3) == T - if !(T <: Complex) - for H in (H1, H2, H3) - Hc = @constinferred complex(H) - @test scalartype(Hc) == complex(T) - # cannot define `real(H)`, so only test elementwise - for (Wc, W) in zip(parent(Hc), parent(H)) - Wr = @constinferred real(Wc) - @test scalartype(Wr) == T - @test Wr ≈ W atol = 1.0e-6 - end - end - end - - # A one-site Hamiltonian must not be contracted with itself. - Hsingle = FiniteMPOHamiltonian([V], 1 => O₁) - @test convert(TensorMap, Hsingle) ≈ O₁ - - # check if constructor works by converting back to tensormap - H1_tm = convert(TensorMap, H1) - operators = vcat(fill(E, L - 1), O₁) - @test H1_tm ≈ mapreduce(+, 1:L) do i - return reduce(⊗, circshift(operators, i)) - end - operators = vcat(fill(E, L - 2), O₂) - @test convert(TensorMap, H2) ≈ mapreduce(+, 1:(L - 1)) do i - return reduce(⊗, circshift(operators, i)) - end - @test convert(TensorMap, H3) ≈ - O₁ ⊗ E ⊗ E + E ⊗ O₂ + permute(O₂ ⊗ E, ((1, 3, 2), (4, 6, 5))) - - # check if adding terms on the same site works - single_terms = Iterators.flatten(Iterators.repeated((i => O₁ / 2 for i in 1:L), 2)) - H4 = FiniteMPOHamiltonian(lattice, single_terms) - @test H4 ≈ H1 atol = 1.0e-6 - double_terms = Iterators.flatten( - Iterators.repeated(((i, i + 1) => O₂ / 2 for i in 1:(L - 1)), 2) - ) - H5 = FiniteMPOHamiltonian(lattice, double_terms) - @test H5 ≈ H2 atol = 1.0e-6 - - # test linear algebra - @test H1 ≈ - FiniteMPOHamiltonian(lattice, 1 => O₁) + - FiniteMPOHamiltonian(lattice, 2 => O₁) + - FiniteMPOHamiltonian(lattice, 3 => O₁) - @test 0.8 * H1 + 0.2 * H1 ≈ H1 atol = 1.0e-6 - @test convert(TensorMap, H1 + H2) ≈ convert(TensorMap, H1) + convert(TensorMap, H2) atol = 1.0e-6 - H1_trunc = changebonds(H1, SvdCut(; trunc = truncrank(0))) - @test H1_trunc ≈ H1 - @test all(left_virtualspace(H1_trunc) .== left_virtualspace(H1)) - - # test dot and application - state = rand(T, prod(lattice)) - mps = FiniteMPS(state) - - @test convert(TensorMap, H1 * mps) ≈ H1_tm * state - @test H1 * state ≈ H1_tm * state - @test dot(mps, H2, mps) ≈ dot(mps, H2 * mps) - - # test constructor from dictionary with mixed linear and Cartesian lattice indices as keys - grid = square = fill(V, 3, 3) - - local_operators = Dict((I,) => O₁ for I in eachindex(grid)) - I_vertical = CartesianIndex(1, 0) - vertical_operators = Dict( - (I, I + I_vertical) => O₂ for I in eachindex(IndexCartesian(), square) if I[1] < size(square, 1) - ) - I_horizontal = CartesianIndex(0, 1) - horizontal_operators = Dict( - (I, I + I_horizontal) => O₂ for I in eachindex(IndexCartesian(), square) if I[2] < size(square, 1) - ) - operators = merge(local_operators, vertical_operators, horizontal_operators) - H4 = FiniteMPOHamiltonian(grid, operators) - - @test H4 ≈ - FiniteMPOHamiltonian(grid, local_operators) + - FiniteMPOHamiltonian(grid, vertical_operators) + - FiniteMPOHamiltonian(grid, horizontal_operators) atol = 1.0e-4 - - H5 = changebonds(H4 / 3 + 2H4 / 3, SvdCut(; trunc = trunctol(; atol = 1.0e-12))) - psi = FiniteMPS(physicalspace(H5), V ⊕ rightunitspace(V)) - @test expectation_value(psi, H4) ≈ expectation_value(psi, H5) - end -end - -@testset "Finite MPOHamiltonian canonicalization" begin - T = ComplexF64 - for V in (ℂ^2, U1Space(-1 => 1, 0 => 1, 1 => 1), Rep[SU₂](1 // 2 => 1)) - O₁ = randn(T, V, V) - O₁ += O₁' - O₂ = randn(T, V^2 ← V^2) - O₂ += O₂' - E = id(storagetype(O₁), domain(O₁)) - - # regression test for https://github.com/QuantumKitHub/MPSKit.jl/issues/522 - # bulk block of the middle site is empty on the left but not on the right - H_right = FiniteMPOHamiltonian(fill(V, 3), (1,) => O₁, (2, 3) => O₂) - H_right_tm = O₁ ⊗ E ⊗ E + E ⊗ O₂ - H, = MPSKit.right_canonicalize!(copy(H_right)) - @test convert(TensorMap, H) ≈ H_right_tm - - # bulk block of the middle site is empty on the right but not on the left - H_left = FiniteMPOHamiltonian(fill(V, 3), (1, 2) => O₂, (3,) => O₁) - H_left_tm = O₂ ⊗ E + E ⊗ E ⊗ O₁ - H, = MPSKit.left_canonicalize!(copy(H_left)) - @test convert(TensorMap, H) ≈ H_left_tm - - L = 5 - H5 = FiniteMPOHamiltonian(fill(V, L), (1, 2) => O₂, (3,) => O₁, (4, 5) => O₂) - H5_tm = O₂ ⊗ E ⊗ E ⊗ E + E ⊗ E ⊗ O₁ ⊗ E ⊗ E + E ⊗ E ⊗ E ⊗ O₂ - H, = MPSKit.right_canonicalize!(copy(H5)) - @test convert(TensorMap, H) ≈ H5_tm - H, = MPSKit.left_canonicalize!(H) - @test convert(TensorMap, H) ≈ H5_tm - - H5_trunc = changebonds(H5, SvdCut(; trunc = truncrank(16))) - @test convert(TensorMap, H5_trunc) ≈ H5_tm - - # canonical forms, also when `C`/`B` have identity components and the blocks are rank-deficient - L = 4 - H = FiniteMPOHamiltonian(fill(V, L), (i, i + 1) => O₂ for i in 1:(L - 1)) - H_tm = convert(TensorMap, H) - - HL, Ls = MPSKit.left_canonicalize!(copy(H)) - @test length(Ls) == L - 1 - @test all(i -> codomain(Ls[i])[1] == right_virtualspace(HL[i].A), 1:(L - 1)) - @test convert(TensorMap, HL) ≈ H_tm - for W in HL[2:(L - 1)] - Vb = MPSKit.SparseBlockTensorMap(W)[1:(end - 1), 1, 1, 1:(end - 1)] - @plansor G[r; r′] := conj(Vb[l p; p′ r]) * Vb[l p; p′ r′] - G = TensorMap(G) / dim(physicalspace(W)) - @test G ≈ id(domain(G)) - end - - HR, Rs = MPSKit.right_canonicalize!(copy(H)) - @test length(Rs) == L - 1 - @test all(i -> domain(Rs[i])[1] == left_virtualspace(HR[i + 1].A), 1:(L - 1)) - @test convert(TensorMap, HR) ≈ H_tm - for W in HR[2:(L - 1)] - Vb = MPSKit.SparseBlockTensorMap(W)[2:end, 1, 1, 2:end] - @plansor G[l; l′] := Vb[l p; p′ r] * conj(Vb[l′ p; p′ r]) - G = TensorMap(G) / dim(physicalspace(W)) - @test G ≈ id(codomain(G)) - end - end -end - -@testset "Finite MPOHamiltonian repeated indices" begin - X = randn(ComplexF64, ℂ^2, ℂ^2) - X += X' - Y = randn(ComplexF64, ℂ^2, ℂ^2) - Y += Y' - L = 4 - chain = fill(space(X, 1), 4) - - H1 = FiniteMPOHamiltonian(chain, (1,) => (X * X * Y * Y)) - H2 = FiniteMPOHamiltonian(chain, (1, 1, 1, 1) => (X ⊗ X ⊗ Y ⊗ Y)) - @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) - - H1 = FiniteMPOHamiltonian(chain, (1, 2) => ((X * Y) ⊗ (X * Y))) - H2 = FiniteMPOHamiltonian(chain, (1, 2, 1, 2) => (X ⊗ X ⊗ Y ⊗ Y)) - @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) - - H1 = FiniteMPOHamiltonian(chain, (1, 2) => ((X * X * Y) ⊗ Y)) - H2 = FiniteMPOHamiltonian(chain, (1, 1, 1, 2) => (X ⊗ X ⊗ Y ⊗ Y)) - @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) - - H1 = FiniteMPOHamiltonian(chain, (1, 2) => ((X * Y * Y) ⊗ X)) - H2 = FiniteMPOHamiltonian(chain, (1, 2, 1, 1) => (X ⊗ X ⊗ Y ⊗ Y)) - @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) - - H1 = FiniteMPOHamiltonian(chain, (1, 2, 3) => FiniteMPO((X * X) ⊗ Y ⊗ Y)) - H2 = FiniteMPOHamiltonian(chain, (1, 1, 2, 3) => FiniteMPO(X ⊗ X ⊗ Y ⊗ Y)) - @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) - - H1 = FiniteMPOHamiltonian(chain, (1, 2, 3) => FiniteMPO((Y * Y) ⊗ X ⊗ X)) - H2 = FiniteMPOHamiltonian(chain, (2, 3, 1, 1) => FiniteMPO(X ⊗ X ⊗ Y ⊗ Y)) - @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) - - H1 = FiniteMPOHamiltonian(chain, (1, 2, 3) => FiniteMPO(X ⊗ (X * Y) ⊗ Y)) - H2 = FiniteMPOHamiltonian(chain, (1, 2, 2, 3) => FiniteMPO(X ⊗ X ⊗ Y ⊗ Y)) - @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) - - # a caller-supplied MPO is only copied when the indices actually need canonicalising, - # so check that neither path writes back into it - for inds in ((1, 2, 3), (1, 2, 2, 3), (2, 3, 1, 1)) - mpo = FiniteMPO(X ⊗ X ⊗ Y ⊗ Y) - length(inds) == 3 && (mpo = FiniteMPO((X * X) ⊗ Y ⊗ Y)) - before = map(copy, parent(mpo)) - FiniteMPOHamiltonian(chain, inds => mpo) - @test all(splat(≈), zip(parent(mpo), before)) - end -end +TestSuite.hamiltonian_finite_mpohamiltonian(Vector; fast = fast_tests) +TestSuite.hamiltonian_finite_canonicalization(Vector; fast = fast_tests) +TestSuite.hamiltonian_finite_repeated_indices(Vector; fast = fast_tests) diff --git a/test/hamiltonian/infinite.jl b/test/hamiltonian/infinite.jl index 4dc2f4c51..dd2e37cd6 100644 --- a/test/hamiltonian/infinite.jl +++ b/test/hamiltonian/infinite.jl @@ -4,100 +4,8 @@ println(" -------------------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -pspaces = PSPACES_TRIPLE -vspaces = VSPACES_TRIPLE -if fast_tests - pspaces = pspaces[1:1] - vspaces = vspaces[1:1] -end - -@testset "InfiniteMPOHamiltonian $(sectortype(pspace))" for (pspace, Dspace) in zip(pspaces, vspaces) - # generate a 1-2-3 body interaction - operators = ntuple(3) do i - O = rand(ComplexF64, pspace^i, pspace^i) - return O += O' - end - - H1 = InfiniteMPOHamiltonian(operators[1]) - H2 = InfiniteMPOHamiltonian(operators[2]) - H3 = repeat(InfiniteMPOHamiltonian(operators[3]), 2) - - # make a teststate to measure expectation values for - ψ1 = InfiniteMPS([pspace], [Dspace]) - ψ2 = InfiniteMPS([pspace, pspace], [Dspace, Dspace]) - - e1 = expectation_value(ψ1, H1) - e2 = expectation_value(ψ1, H2) - - H1 = 2 * H1 - [1] - @test e1 * 2 - 1 ≈ expectation_value(ψ1, H1) atol = 1.0e-10 - - H1 = H1 + H2 - - @test e1 * 2 + e2 - 1 ≈ expectation_value(ψ1, H1) atol = 1.0e-10 - - H1 = repeat(H1, 2) - - e1 = expectation_value(ψ2, H1) - e3 = expectation_value(ψ2, H3) - - @test e1 + e3 ≈ expectation_value(ψ2, H1 + H3) atol = 1.0e-10 - - H4 = H1 + H3 - @test real(expectation_value(ψ2, H4)) >= 0 - - O1_real = project_hermitian!(randn(Float64, pspace, pspace)) - O2_real = project_hermitian!(randn(Float64, pspace ⊗ pspace, pspace ⊗ pspace)) - Hreal_long_range = InfiniteMPOHamiltonian(O1_real) + InfiniteMPOHamiltonian(O2_real) - Hcomplex_local = InfiniteMPOHamiltonian(operators[1]) - - @test scalartype(Hreal_long_range) == Float64 - @test scalartype(Hcomplex_local) == ComplexF64 - for H in ( - Hreal_long_range + Hcomplex_local, Hcomplex_local + Hreal_long_range, - Hreal_long_range - Hcomplex_local, Hcomplex_local - Hreal_long_range, - ) - @test scalartype(H) == ComplexF64 - end - @test expectation_value(ψ1, Hreal_long_range + Hcomplex_local) ≈ - expectation_value(ψ1, Hreal_long_range) + expectation_value(ψ1, Hcomplex_local) atol = 1.0e-10 -end - -@testset "InfiniteMPOHamiltonian canonical forms $(sectortype(pspace))" for (pspace, Dspace) in zip(pspaces, vspaces) - O₂ = rand(ComplexF64, pspace^2, pspace^2) - O₂ += O₂' - O₃ = rand(ComplexF64, pspace^3, pspace^3) - O₃ += O₃' - lattice = PeriodicVector(fill(pspace, 2)) - H = InfiniteMPOHamiltonian(lattice, (i, i + 1) => O₂ for i in 1:2) + - InfiniteMPOHamiltonian(lattice, (i, i + 1, i + 2) => O₃ for i in 1:2) - - ψ = InfiniteMPS(rand, ComplexF64, fill(pspace, 2), fill(Dspace, 2)) - E = expectation_value(ψ, H) - - # the upper-left block `[1 C; 0 A]` has orthonormal columns - HL, = MPSKit.left_canonicalize!(copy(H)) - @test expectation_value(ψ, HL) ≈ E - for W in parent(HL) - V = MPSKit.SparseBlockTensorMap(W)[1:(end - 1), 1, 1, 1:(end - 1)] - @plansor G[r; r′] := conj(V[l p; p′ r]) * V[l p; p′ r′] - G = TensorMap(G) / dim(physicalspace(W)) - @test G ≈ id(domain(G)) - end - - # the lower-right block `[A B; 0 1]` has orthonormal rows - HR, = MPSKit.right_canonicalize!(copy(H)) - @test expectation_value(ψ, HR) ≈ E - for W in parent(HR) - V = MPSKit.SparseBlockTensorMap(W)[2:end, 1, 1, 2:end] - @plansor G[l; l′] := V[l p; p′ r] * conj(V[l′ p; p′ r]) - G = TensorMap(G) / dim(physicalspace(W)) - @test G ≈ id(codomain(G)) - end -end +TestSuite.hamiltonian_infinite_mpohamiltonian(Vector; fast = fast_tests) +TestSuite.hamiltonian_infinite_canonical_forms(Vector; fast = fast_tests) diff --git a/test/hamiltonian/mixed.jl b/test/hamiltonian/mixed.jl index 7c6e0679f..5c8fd8af6 100644 --- a/test/hamiltonian/mixed.jl +++ b/test/hamiltonian/mixed.jl @@ -4,202 +4,9 @@ println(" ----------------------------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: GeometryStyle, FiniteChainStyle, InfiniteChainStyle, OperatorStyle, HamiltonianStyle -using TensorKit -using TensorKit: ℙ -using Adapt +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -pspaces = (ℙ^4, Rep[U₁](0 => 2), Rep[SU₂](1 => 1)) -vspaces = (ℙ^10, Rep[U₁]((0 => 20)), Rep[SU₂](1 // 2 => 10, 3 // 2 => 5, 5 // 2 => 1)) -if fast_tests - pspaces = pspaces[1:1] - vspaces = vspaces[1:1] -end - -@testset "MPOHamiltonian constructors" begin - P = ℂ^2 - T = Float64 - - H1 = randn(T, P ← P) - H1 += H1' - D = FiniteMPO(H1)[1] - - H2 = randn(T, P^2 ← P^2) - H2 += H2' - C, B = FiniteMPO(H2)[1:2] - - Elt = Union{Missing, typeof(D), scalartype(D)} - Wmid = Elt[1.0 C D; 0.0 0.0 B; 0.0 0.0 1.0] - Wleft = Wmid[1:1, :] - Wright = Wmid[:, end:end] - - # Finite - Ws = [Wleft, Wmid, Wmid, Wright] - H = FiniteMPOHamiltonian( - fill(P, 4), [(i,) => H1 for i in 1:4]..., [(i, i + 1) => H2 for i in 1:3]... - ) - H′ = FiniteMPOHamiltonian(Ws) - @test H ≈ H′ - - H′ = FiniteMPOHamiltonian(map(Base.Fix1(collect, Any), Ws)) # without type info - @test H ≈ H′ - - @test isfinite(H) - @test isfinite(typeof(H)) - @test GeometryStyle(typeof(H)) == FiniteChainStyle() - @test GeometryStyle(H) == FiniteChainStyle() - @test OperatorStyle(typeof(H)) == HamiltonianStyle() - @test OperatorStyle(H) == HamiltonianStyle() - @test OperatorStyle(H, H′) == OperatorStyle(H) - @test TensorKit.storagetype(H) == Vector{T} - @test TensorKit.storagetype(typeof(H)) == Vector{T} - - # Infinite - Ws = [Wmid] - H = InfiniteMPOHamiltonian( - fill(P, 1), [(i,) => H1 for i in 1:1]..., [(i, i + 1) => H2 for i in 1:1]... - ) - H′ = InfiniteMPOHamiltonian(Ws) - @test all(parent(H) .≈ parent(H′)) - - H′ = InfiniteMPOHamiltonian(map(Base.Fix1(collect, Any), Ws)) # without type info - @test all(parent(H) .≈ parent(H′)) - - @test !isfinite(H) - @test !isfinite(typeof(H)) - @test GeometryStyle(typeof(H)) == InfiniteChainStyle() - @test GeometryStyle(H) == InfiniteChainStyle() - @test OperatorStyle(typeof(H)) == HamiltonianStyle() - @test OperatorStyle(H) == HamiltonianStyle() - @test TensorKit.storagetype(H′) == Vector{T} - @test TensorKit.storagetype(typeof(H′)) == Vector{T} -end - -adapt_Vs = fast_tests ? (ℂ^2,) : (ℂ^2, U1Space(-1 => 1, 0 => 1, 1 => 1)) -Ts = fast_tests ? (Float64,) : (Float64, ComplexF64) -@testset "Adapt" for V in adapt_Vs - h = rand(Float32, V^2 ← V^2) - h += h' - - L = 4 - H1 = FiniteMPOHamiltonian( - fill(V, L), - ((i, i + 1) => h for i in 1:(L - 1))..., - ((i, i + 2) => h for i in 1:(L - 2))..., - ((i, i + 3) => h for i in 1:(L - 3))..., - ) - mps1 = FiniteMPS(physicalspace(H1), unitspace(V)) - - for T in Ts - H2 = if VERSION <= v"1.12" - adapt(Vector{T}, H1) - else - @testinferred adapt(Vector{T}, H1) - end - @test H2 isa FiniteMPOHamiltonian - @test scalartype(H2) == T - @test storagetype(H2) == Vector{T} - @test expectation_value(mps1, H1) ≈ expectation_value(mps1, H2) - end - - H3 = InfiniteMPOHamiltonian(fill(V, L), (1, 2) => h, (1, 3) => h, (1, 4) => h) - mps2 = InfiniteMPS(physicalspace(H3), [unitspace(V)]) - for T in Ts - H4 = if VERSION <= v"1.12" - # this is type unstable for LTS for some reason - adapt(Vector{T}, H3) - else - @testinferred adapt(Vector{T}, H3) - end - @test H4 isa InfiniteMPOHamiltonian - @test scalartype(H4) == T - @test storagetype(H4) == Vector{T} - @test storagetype(typeof(H4)) == Vector{T} - @test expectation_value(mps2, H3) ≈ expectation_value(mps2, H4) - end -end - -@testset "MPOHamiltonian channel sharing" begin - # Terms that start out identically share a virtual channel, which is what keeps the bond - # dimension of a long-range interaction linear -- instead of quadratic -- in its range. - # Correctness is checked against the same Hamiltonian assembled by summing single-term - # Hamiltonians, which cannot share anything and is thus independent of how channels are - # assigned. - Z = TensorMap(Float64[1 0; 0 -1], ℂ^2, ℂ^2) - X = TensorMap(Float64[0 1; 1 0], ℂ^2, ℂ^2) - - infinite_ev(lattice, terms) = let L = length(lattice) - ψ = InfiniteMPS(collect(lattice), fill(ℂ^8, L)) - H = InfiniteMPOHamiltonian(lattice, terms...) - Href = sum(InfiniteMPOHamiltonian(lattice, t) for t in terms) - (H, sum(expectation_value(ψ, H)), sum(expectation_value(ψ, Href))) - end - - @testset "power-law couplings, unit cell L=$L" for L in (1, 2, 3) - R = 5 - lattice = fill(ℂ^2, L) - terms = Pair[(i, i + d) => (1 / d^3) * (Z ⊗ Z) for i in 1:L for d in 1:R] - append!(terms, [(i,) => 0.7 * X for i in 1:L]) - H, e, eref = infinite_ev(lattice, terms) - @test e ≈ eref - @test all(i -> dim(left_virtualspace(H, i)) ≤ R + 2, 1:L) - end - - @testset "proportional terms are merged" begin - lattice = fill(ℂ^2, 1) - H = InfiniteMPOHamiltonian(lattice, (1, 2) => Z ⊗ Z, (1, 2) => 0.5 * (Z ⊗ Z)) - Href = InfiniteMPOHamiltonian(lattice, (1, 2) => 1.5 * (Z ⊗ Z)) - ψ = InfiniteMPS([ℂ^2], [ℂ^8]) - @test sum(expectation_value(ψ, H)) ≈ sum(expectation_value(ψ, Href)) - @test dim(left_virtualspace(H, 1)) == dim(left_virtualspace(Href, 1)) - end - - @testset "negative prefactors" begin - lattice = fill(ℂ^2, 1) - terms = Pair[ - (1, 2) => Z ⊗ Z, (1, 3) => -2.0 * (Z ⊗ Z), (1, 4) => 0.25 * (Z ⊗ Z), - ] - _, e, eref = infinite_ev(lattice, terms) - @test e ≈ eref - end - - @testset "mixed operators and ranges" begin - lattice = fill(ℂ^2, 2) - terms = Pair[ - (1, 2) => Z ⊗ Z, (1, 3) => Z ⊗ X, (1, 4) => 3.0 * (Z ⊗ Z), - (2, 3, 4) => X ⊗ Z ⊗ X, (1,) => 0.7 * X, - ] - _, e, eref = infinite_ev(lattice, terms) - @test e ≈ eref - end - - @testset "finite chain" begin - L = 6 - lattice = fill(ℂ^2, L) - terms = Pair[ - (i, j) => (1 / (j - i)^3) * (Z ⊗ Z) for i in 1:L for j in (i + 1):L - ] - push!(terms, (1,) => 0.7 * X) - H = FiniteMPOHamiltonian(lattice, terms...) - Href = sum(FiniteMPOHamiltonian(lattice, t) for t in terms) - @test convert(TensorMap, H) ≈ convert(TensorMap, Href) - @test maximum(i -> dim(left_virtualspace(H, i)), 1:L) < - maximum(i -> dim(left_virtualspace(Href, i)), 1:L) - end - - @testset "symmetric tensors $(sectortype(pspace))" for (pspace, Dspace) in - zip(pspaces, vspaces) - O = rand(ComplexF64, pspace^2, pspace^2) - O += O' - terms = Pair[(1, 1 + d) => (1 / d^3) * O for d in 1:4] - lattice = fill(pspace, 1) - H = InfiniteMPOHamiltonian(lattice, terms...) - Href = sum(InfiniteMPOHamiltonian(lattice, t) for t in terms) - ψ = InfiniteMPS([pspace], [Dspace]) - @test sum(expectation_value(ψ, H)) ≈ sum(expectation_value(ψ, Href)) - @test dim(left_virtualspace(H, 1)) < dim(left_virtualspace(Href, 1)) - end -end +TestSuite.hamiltonian_mixed_constructors(Vector; fast = fast_tests) +TestSuite.hamiltonian_mixed_adapt(Vector; fast = fast_tests) +TestSuite.hamiltonian_mixed_channel_sharing(Vector; fast = fast_tests) diff --git a/test/mpo/dense_mpo.jl b/test/mpo/dense_mpo.jl index 6d04eaa2e..716d9c900 100644 --- a/test/mpo/dense_mpo.jl +++ b/test/mpo/dense_mpo.jl @@ -4,20 +4,7 @@ println(" -------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: GeometryStyle -using TensorKit +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "DenseMPO" for ham in (transverse_field_ising(), heisenberg_XXX(; spin = 1)) - pspace = physicalspace(ham, 1) - ou = rightunitspace(pspace) - - ψ = InfiniteMPS([pspace], [ou ⊕ pspace]) - - W = MPSKit.DenseMPO(make_time_mpo(ham, 1im * 0.5, WII())) - - @test GeometryStyle(ψ, W) == GeometryStyle(ψ) - @test W * (W * ψ) ≈ (W * W) * ψ atol = 1.0e-2 # TODO: there is a normalization issue here -end +TestSuite.mpo_dense_mpo(Vector; fast = fast_tests) diff --git a/test/mpo/finite.jl b/test/mpo/finite.jl index b16d8081e..a95039a5d 100644 --- a/test/mpo/finite.jl +++ b/test/mpo/finite.jl @@ -4,117 +4,8 @@ println(" -------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: GeometryStyle, FiniteChainStyle, OperatorStyle, MPOStyle -using MPSKit: remove_orphans! -using TensorKit -using TensorKit: ℙ -using BlockTensorKit -using BlockTensorKit: SparseBlockTensorMap, nonzero_keys +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "FiniteMPO" begin - # start from random operators - L = 4 - T = ComplexF64 - - finite_mpo_Vs = fast_tests ? (ℂ^2,) : (ℂ^2, U1Space(0 => 1, 1 => 1)) - for V in finite_mpo_Vs - O₁ = rand(T, V^L, V^L) - O₂ = rand(T, space(O₁)) - O₃ = rand(real(T), space(O₁)) - - # create MPO and convert it back to see if it is the same - mpo₁ = FiniteMPO(O₁) # type-unstable for now! - mpo₂ = FiniteMPO(O₂) - mpo₃ = FiniteMPO(O₃) - - @test isfinite(mpo₁) - @test isfinite(typeof(mpo₁)) - @test GeometryStyle(typeof(mpo₁)) == FiniteChainStyle() - @test GeometryStyle(mpo₁) == FiniteChainStyle() - @test OperatorStyle(typeof(mpo₁)) == MPOStyle() - - @test @constinferred physicalspace(mpo₁) == fill(V, L) - Vleft = @constinferred left_virtualspace(mpo₁) - Vright = @constinferred right_virtualspace(mpo₂) - for i in 1:L - @test Vleft[i] == left_virtualspace(mpo₁, i) - @test Vright[i] == right_virtualspace(mpo₁, i) - end - - @test convert(TensorMap, mpo₁) ≈ O₁ - @test convert(TensorMap, -mpo₂) ≈ -O₂ - @test convert(TensorMap, @constinferred complex(mpo₃)) ≈ complex(O₃) - - # test scalar multiplication - α = rand(T) - @test convert(TensorMap, α * mpo₁) ≈ α * O₁ - @test convert(TensorMap, mpo₁ * α) ≈ O₁ * α - @test α * mpo₃ ≈ α * complex(mpo₃) atol = 1.0e-6 - - # test addition and multiplication - @test convert(TensorMap, mpo₁ + mpo₂) ≈ O₁ + O₂ - @test convert(TensorMap, mpo₁ + mpo₃) ≈ O₁ + O₃ - @test convert(TensorMap, mpo₁ * mpo₂) ≈ O₁ * O₂ - @test convert(TensorMap, mpo₁ * mpo₃) ≈ O₁ * O₃ - - # test application to a state - ψ₁ = rand(T, domain(O₁)) - ψ₂ = rand(real(T), domain(O₂)) - mps₁ = FiniteMPS(ψ₁) - mps₂ = FiniteMPS(ψ₂) - - @test @constinferred GeometryStyle(mps₁, mpo₁, mps₁) == GeometryStyle(mps₁) - - @test convert(TensorMap, mpo₁ * mps₁) ≈ O₁ * ψ₁ - @test mpo₁ * ψ₁ ≈ O₁ * ψ₁ - @test convert(TensorMap, mpo₃ * mps₁) ≈ O₃ * ψ₁ - @test mpo₃ * ψ₁ ≈ O₃ * ψ₁ - @test convert(TensorMap, mpo₁ * mps₂) ≈ O₁ * ψ₂ - @test mpo₁ * ψ₂ ≈ O₁ * ψ₂ - - @test dot(mps₁, mpo₁, mps₁) ≈ dot(ψ₁, O₁, ψ₁) - @test dot(mps₁, mpo₁, mps₁) ≈ dot(mps₁, mpo₁ * mps₁) - # test conversion to and from mps - mpomps₁ = convert(FiniteMPS, mpo₁) - mpompsmpo₁ = convert(FiniteMPO, mpomps₁) - - @test convert(FiniteMPO, mpomps₁) ≈ mpo₁ rtol = 1.0e-6 - - @test dot(mpomps₁, mpomps₁) ≈ dot(mpo₁, mpo₁) - end -end - -@testset "remove_orphans!" begin - T = ComplexF64 - P = ℂ^2 - TT = tensormaptype(ComplexSpace, 2, 2, Vector{T}) - randmpotensor() = rand(T, ℂ^1 ⊗ P ← P ⊗ ℂ^1) - - @testset "finite: dead-end chain" begin - # channel 2 is never entered on site 1, so the (2, 2) block on site 2 and the - # (2, 1) block on site 3 are orphaned - V₁ = SumSpace(ℂ^1) - V₂ = SumSpace(ℂ^1, ℂ^1) - Ws = [ - SparseBlockTensorMap{TT}(undef, V₁ ⊗ P ← P ⊗ V₂), - SparseBlockTensorMap{TT}(undef, V₂ ⊗ P ← P ⊗ V₂), - SparseBlockTensorMap{TT}(undef, V₂ ⊗ P ← P ⊗ V₁), - ] - Ws[1][1, 1, 1, 1] = randmpotensor() - Ws[2][1, 1, 1, 1] = randmpotensor() - Ws[2][2, 1, 1, 2] = randmpotensor() - Ws[3][1, 1, 1, 1] = randmpotensor() - Ws[3][2, 1, 1, 1] = randmpotensor() - - mpo = MPO(Ws) - @test isfinite(mpo) - O = convert(TensorMap, mpo) - remove_orphans!(mpo) - - @test left_virtualspace(mpo) == right_virtualspace(mpo) == fill(SumSpace(ℂ^1), 3) - @test convert(TensorMap, mpo) ≈ O - end -end +TestSuite.mpo_finite_basics(Vector; fast = fast_tests) +TestSuite.mpo_finite_remove_orphans(Vector; fast = fast_tests) diff --git a/test/mpo/infinite.jl b/test/mpo/infinite.jl index ff7dbbd50..c8978c937 100644 --- a/test/mpo/infinite.jl +++ b/test/mpo/infinite.jl @@ -4,79 +4,8 @@ println(" ---------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: GeometryStyle, InfiniteChainStyle, OperatorStyle, MPOStyle -using MPSKit: remove_orphans! -using TensorKit -using BlockTensorKit -using BlockTensorKit: SparseBlockTensorMap, nonzero_keys +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "InfiniteMPO" begin - P = ℂ^2 - V = ℂ^2 - T = Float64 - - H1 = randn(T, V ⊗ P ← P ⊗ V) - H = InfiniteMPO([H1]) - - @test !isfinite(H) - @test !isfinite(typeof(H)) - @test GeometryStyle(typeof(H)) == InfiniteChainStyle() - @test GeometryStyle(H) == InfiniteChainStyle() - @test OperatorStyle(typeof(H)) == MPOStyle() - @test OperatorStyle(H) == MPOStyle() - - @test physicalspace(H, 1) == P - @test left_virtualspace(H, 1) == V - @test left_virtualspace(H, 4) == V - @test right_virtualspace(H, 1) == V - - multiH = MultilineMPO([H, H]) - @test physicalspace(multiH, 1, 1) == P - @test left_virtualspace(multiH, 1, 1) == left_virtualspace(multiH, 2, 1) == V - @test right_virtualspace(multiH, CartesianIndex(1, 2)) == V - @test leftunit(multiH) == leftunit(H) == unit(sectortype(P)) - @test rightunit(multiH) == rightunit(H) == unit(sectortype(P)) -end - -@testset "remove_orphans!" begin - T = ComplexF64 - P = ℂ^2 - TT = tensormaptype(ComplexSpace, 2, 2, Vector{T}) - randmpotensor() = rand(T, ℂ^1 ⊗ P ← P ⊗ ℂ^1) - - @testset "infinite: dead-end chain" begin - # 1 -> 1, 1 -> 2 -> 3 -> 4, where channel 4 is a dead end: removing it orphans 3, - # removing 3 orphans 2, ... so the fixed point takes several passes to reach - V = SumSpace(fill(ℂ^1, 4)...) - W = SparseBlockTensorMap{TT}(undef, V ⊗ P ← P ⊗ V) - for (i, j) in ((1, 1), (1, 2), (2, 3), (3, 4)) - W[i, 1, 1, j] = randmpotensor() - end - W₁₁ = copy(W[1, 1, 1, 1]) - - mpo = InfiniteMPO([W]) - @test !isfinite(mpo) - @test remove_orphans!(mpo) === mpo - - # only the (1, 1) self-loop survives - @test left_virtualspace(mpo, 1) == right_virtualspace(mpo, 1) == SumSpace(ℂ^1) - @test collect(nonzero_keys(mpo[1])) == [CartesianIndex(1, 1, 1, 1)] - @test mpo[1][1, 1, 1, 1] ≈ W₁₁ - end - - @testset "infinite: nothing to remove" begin - # every channel is alive, so this must terminate immediately and change nothing - H = transverse_field_ising(T; g = 0.7) - mpo = MPO(map(SparseBlockTensorMap, parent(H))) - @test !isfinite(mpo) - - spaces₀ = map(space, parent(mpo)) - keys₀ = map(W -> sort!(collect(nonzero_keys(W))), parent(mpo)) - remove_orphans!(mpo) - @test map(space, parent(mpo)) == spaces₀ - @test map(W -> sort!(collect(nonzero_keys(W))), parent(mpo)) == keys₀ - end -end +TestSuite.mpo_infinite_basics(Vector; fast = fast_tests) +TestSuite.mpo_infinite_remove_orphans(Vector; fast = fast_tests) diff --git a/test/mpo/mixed.jl b/test/mpo/mixed.jl index f8106c304..c88e2c289 100644 --- a/test/mpo/mixed.jl +++ b/test/mpo/mixed.jl @@ -4,32 +4,7 @@ println(" ------------------------ ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using Adapt +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -mpo_adapt_Vs = fast_tests ? (ℂ^2,) : (ℂ^2, U1Space(-1 => 1, 0 => 1, 1 => 1)) -Ts = fast_tests ? (Float64,) : (Float64, ComplexF64) -@testset "Adapt" for V in mpo_adapt_Vs - L = 3 - o = rand(Float32, V^L ← V^L) - mpo1 = FiniteMPO(o) - for T in Ts - mpo2 = @testinferred adapt(Vector{T}, mpo1) - @test mpo2 isa FiniteMPO - @test scalartype(mpo2) == T - @test storagetype(mpo2) == Vector{T} - @test convert(TensorMap, mpo2) ≈ o - end - - mpo3 = InfiniteMPO(mpo1[2:2]) - for T in Ts - mpo4 = @testinferred adapt(Vector{T}, mpo3) - @test mpo4 isa InfiniteMPO - @test scalartype(mpo4) == T - @test storagetype(mpo4) == Vector{T} - @test dot(mpo3, mpo4) ≈ norm(mpo3)^2 atol = 1.0e-4 - end -end +TestSuite.mpo_mixed_adapt(Vector; fast = fast_tests) diff --git a/test/mpo/multiline.jl b/test/mpo/multiline.jl index 564bd267a..57c2faeee 100644 --- a/test/mpo/multiline.jl +++ b/test/mpo/multiline.jl @@ -4,100 +4,12 @@ println(" ---------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: leading_eigenvalue -using TensorKit - -d = 3 -P = ℂ^d - -# ψ[i] = |i⟩ (3 rows, 1 column), O[i]|i⟩ = |i + 1⟩ exactly -# ψ[i] is an InfiniteMPS and is periodic in the line index i -# this properly tests row-shift convention: row i of a MultilineMPO maps row i of the network onto row i + 1 -ψ = MultilineMPS([product_mps(1), product_mps(2), product_mps(3)]) -O = MultilineMPO( - [ - perm_mpo([2, 3, 1]), # row 1: |1⟩ -> |2⟩ - perm_mpo([1, 3, 2]), # row 2: |2⟩ -> |3⟩ - perm_mpo([3, 2, 1]), # row 3: |3⟩ -> |1⟩ - ] -) - -@testset "MultilineMPO * InfiniteMPS" begin - for i in 1:3 - @test abs(dot(ψ[i + 1], O[i] * ψ[i])) ≈ 1 atol = 1.0e-10 - @test abs(dot(ψ[i], O[i] * ψ[i])) ≈ 0 atol = 1.0e-10 - end - - @test abs(dot(ψ[1], O * ψ[1])) ≈ 1 atol = 1.0e-10 - @test abs(dot(ψ[2], O * ψ[2])) ≈ 1 atol = 1.0e-10 # cyclic period - - # the order matters, here testing reverse order not returning to ψ[1] - reversed = foldl((st, i) -> O[i] * st, 3:-1:1; init = ψ[1]) - @test abs(dot(ψ[1], reversed)) ≈ 0 atol = 1.0e-10 -end - -@testset "leading_eigenvalue" begin - @test leading_eigenvalue(ψ, O) ≈ 1 atol = 1.0e-10 - - # row-accumulated eigenvalues - scaled(mpo, c) = InfiniteMPO([c * mpo[i] for i in 1:length(mpo)]) - cs = (2.0, 3.0, 5.0) - O_rows = MultilineMPO([scaled(O[i], cs[i]) for i in 1:3]) - @test leading_eigenvalue(ψ, O_rows) ≈ prod(cs) atol = 1.0e-8 - - # column-accumulated eigenvalues - id_mpo = perm_mpo([1, 2, 3]) # identity operator, so anything's a fixed point - cs = (2.0, 3.0) - O_cols = MultilineMPO([InfiniteMPO([cs[1] * id_mpo[1], cs[2] * id_mpo[1]])]) - ψ_cols = MultilineMPS([InfiniteMPS([P, P], [ℂ^2, ℂ^2])]) - @test leading_eigenvalue(ψ_cols, O_cols) ≈ prod(cs) atol = 1.0e-8 -end - -@testset "multi-row fixed point" begin - # stacking the same row twice squares the eigenvalue of the single-row fixed point - O₁ = classical_ising(; β = 0.5) - alg = VUMPS(; tol = 1.0e-10, verbosity = 0) - ψ₁, = leading_boundary(InfiniteMPS(ℂ^2, ℂ^10), O₁, alg) - λ₁ = leading_eigenvalue(ψ₁, O₁) - - O₂ = MultilineMPO([O₁, O₁]) - ψ₂₀ = MultilineMPS([InfiniteMPS(ℂ^2, ℂ^10), InfiniteMPS(ℂ^2, ℂ^10)]) - ψ₂, = leading_boundary(ψ₂₀, O₂, alg) - @test leading_eigenvalue(ψ₂, O₂) ≈ λ₁^2 rtol = 1.0e-8 -end - -@testset "unsupported inputs" begin - @test_throws MethodError expectation_value(ψ, O) - - # Hamiltonian lines are a valid `MultilineMPO`, but not a transfer operator - H = transverse_field_ising() - ψ_H = MultilineMPS([InfiniteMPS(ℂ^2, ℂ^4), InfiniteMPS(ℂ^2, ℂ^4)]) - O_H = MultilineMPO([H, H]) - @test O_H isa MultilineMPO && !(O_H isa InfiniteMultilineMPO) - @test_throws MethodError expectation_value(ψ_H, O_H) - @test_throws MethodError leading_eigenvalue(ψ_H, O_H) - - ψ_fin = MultilineMPS([FiniteMPS(4, P, ℂ^2)]) - @test_throws MethodError leading_boundary(ψ_fin, MultilineMPO([perm_mpo([1, 2, 3])]), VUMPS()) -end - -@testset "line aliases" begin - @test ψ isa InfiniteMultilineMPS - @test !(ψ isa FiniteMultilineMPS) - @test O isa InfiniteMultilineMPO - ψ_fin = MultilineMPS([FiniteMPS(4, P, ℂ^2)]) - @test ψ_fin isa FiniteMultilineMPS && ψ_fin isa MultilineMPS - O_fin = MultilineMPO([FiniteMPO(fill(perm_mpo([1, 2, 3])[1], 4))]) - @test O_fin isa FiniteMultilineMPO && O_fin isa MultilineMPO -end - -@testset "view bounds" begin - # rows are periodic, and so are the columns of infinite lines - @test checkbounds(Bool, ψ.AC, 0, 0) && checkbounds(Bool, ψ.C, 4, -1) - ψ_fin = MultilineMPS([FiniteMPS(4, P, ℂ^2), FiniteMPS(4, P, ℂ^2)]) - @test checkbounds(Bool, ψ_fin.AC, 3, 4) && !checkbounds(Bool, ψ_fin.AC, 1, 5) - @test checkbounds(Bool, ψ_fin.C, 1, 0) && !checkbounds(Bool, ψ_fin.C, 1, 5) -end +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite + +TestSuite.mpo_multiline_mpo_times_mps(Vector; fast = fast_tests) +TestSuite.mpo_multiline_leading_eigenvalue(Vector; fast = fast_tests) +TestSuite.mpo_multiline_multirow_fixed_point(Vector; fast = fast_tests) +TestSuite.mpo_multiline_unsupported_inputs(Vector; fast = fast_tests) +TestSuite.mpo_multiline_line_aliases(Vector; fast = fast_tests) +TestSuite.mpo_multiline_view_bounds(Vector; fast = fast_tests) diff --git a/test/operators/lazysum.jl b/test/operators/lazysum.jl index b4afa5a7a..7cd7991b8 100644 --- a/test/operators/lazysum.jl +++ b/test/operators/lazysum.jl @@ -4,265 +4,10 @@ println(" ----------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: _transpose_front, _transpose_tail, C_hamiltonian, AC_hamiltonian, AC2_hamiltonian -using TensorKit -using VectorInterface: One +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "General LazySum of $(eltype(Os))" for Os in ( - rand(ComplexF64, rand(1:10)), - map(i -> rand(ComplexF64, ℂ^13, ℂ^7), 1:rand(1:10)), - map(i -> rand(ComplexF64, ℂ^1 ⊗ ℂ^2, ℂ^3 ⊗ ℂ^4), 1:rand(1:10)), - ) - LazyOs = LazySum(Os) - - #test user interface - summed = sum(Os) - - @test sum(LazyOs) ≈ summed atol = 1 - 08 - - LazyOs_added = +(LazyOs, Os...) - - @test sum(LazyOs_added) ≈ 2 * summed atol = 1 - 08 -end - -@testset "MultipliedOperator of $(typeof(O)) with $(typeof(f))" for (O, f) in - zip( - (rand(ComplexF64), rand(ComplexF64, ℂ^13, ℂ^7), rand(ComplexF64, ℂ^1 ⊗ ℂ^2, ℂ^3 ⊗ ℂ^4)), - (t -> 3t, 1.1, One()) - ) - tmp = MPSKit.MultipliedOperator(O, f) - if tmp isa TimedOperator - @test tmp(1.1)() ≈ f(1.1) * O atol = 1 - 08 - elseif tmp isa UntimedOperator - @test tmp() ≈ f * O atol = 1 - 08 - end -end - -@testset "General Time-dependent LazySum of $(eltype(Os))" for Os in ( - rand(ComplexF64, 4), - fill(rand(ComplexF64, ℂ^13, ℂ^7), 4), - fill(rand(ComplexF64, ℂ^1 ⊗ ℂ^2, ℂ^3 ⊗ ℂ^4), 4), - ) - - #test user interface - fs = [t -> 3t, t -> t + 2, 4, 1] - Ofs = map(zip(fs, Os)) do (f, O) - if f == 1 - return O - else - return MPSKit.MultipliedOperator(O, f) - end - end - LazyOs = LazySum(Ofs) - summed = sum(zip(fs, Os)) do (f, O) - if f isa Function - f(1.1) * O - else - f * O - end - end - - @test sum(LazyOs(1.1)) ≈ summed atol = 1 - 08 - - LazyOs_added = +(LazyOs, Ofs...) - - @test sum(LazyOs_added(1.1)) ≈ 2 * summed atol = 1 - 08 -end - -# LazySum/MultipliedOperator only wrap arithmetic/dispatch bookkeeping around a Hamiltonian; -# they don't exercise sector-specific fusion math themselves (that's covered by -# hamiltonian/infinite.jl and symmetries/), so testing planar (no symmetry) + U1 -# (abelian) is sufficient here without also paying to compile the SU2 (non-abelian) case. -pspaces = PSPACES_TRIPLE[1:2] -vspaces = VSPACES_TRIPLE[1:2] -if fast_tests - pspaces = pspaces[1:1] - vspaces = vspaces[1:1] -end - -@testset "LazySum of (effective) Hamiltonian $(sectortype(pspace))" for (pspace, Dspace) in - zip(pspaces, vspaces) - Os = map(1:3) do i - O = rand(ComplexF64, pspace^i, pspace^i) - return O += O' - end - fs = [t -> 3t, 2, 1] - - @testset "LazySum FiniteMPOHamiltonian" begin - L = rand(3:2:20) - ψ = FiniteMPS(rand, ComplexF64, L, pspace, Dspace) - lattice = fill(pspace, L) - Hs = map(enumerate(Os)) do (i, O) - return FiniteMPOHamiltonian( - lattice, - ntuple(x -> x + j, i) => O for j in 0:(L - i) - ) - end - summedH = LazySum(Hs) - - envs = map(H -> environments(ψ, H, ψ), Hs) - summed_envs = environments(ψ, summedH, ψ) - - expval = sum(zip(Hs, envs)) do (H, env) - return expectation_value(ψ, H, env) - end - expval1 = expectation_value(ψ, sum(summedH)) - expval2 = expectation_value(ψ, summedH, summed_envs) - expval3 = expectation_value(ψ, summedH) - @test expval ≈ expval1 - @test expval ≈ expval2 - @test expval ≈ expval3 - - # test derivatives - summedhct = C_hamiltonian(1, ψ, summedH, ψ, summed_envs) - sum1 = sum(zip(Hs, envs)) do (H, env) - return C_hamiltonian(1, ψ, H, ψ, env)(ψ.C[1]) - end - @test summedhct(ψ.C[1], 0.0) ≈ sum1 - - summedhct = AC_hamiltonian(1, ψ, summedH, ψ, summed_envs) - sum2 = sum(zip(Hs, envs)) do (H, env) - return AC_hamiltonian(1, ψ, H, ψ, env)(ψ.AC[1]) - end - @test summedhct(ψ.AC[1], 0.0) ≈ sum2 - - v = _transpose_front(ψ.AC[1]) * _transpose_tail(ψ.AR[2]) - summedhct = AC2_hamiltonian(1, ψ, summedH, ψ, summed_envs) - sum3 = sum(zip(Hs, envs)) do (H, env) - return AC2_hamiltonian(1, ψ, H, ψ, env)(v) - end - @test summedhct(v, 0.0) ≈ sum3 - - Hts = [MultipliedOperator(Hs[1], fs[1]), MultipliedOperator(Hs[2], fs[2]), Hs[3]] - summedH = LazySum(Hts) - t = 1.1 - summedH_at = summedH(t) - - envs = map(H -> environments(ψ, H, ψ), Hs) - summed_envs = environments(ψ, summedH, ψ) - - expval = sum(zip(fs, Hs, envs)) do (f, H, env) - return (f isa Function ? f(t) : f) * expectation_value(ψ, H, env) - end - expval1 = expectation_value(ψ, sum(summedH_at)) - expval2 = expectation_value(ψ, summedH_at, summed_envs) - expval3 = expectation_value(ψ, summedH_at) - @test expval ≈ expval1 - @test expval ≈ expval2 - @test expval ≈ expval3 - - # test derivatives - summedhct = C_hamiltonian(1, ψ, summedH, ψ, summed_envs) - sum1 = sum(zip(fs, Hs, envs)) do (f, H, env) - if f isa Function - f = f(t) - end - return f * C_hamiltonian(1, ψ, H, ψ, env)(ψ.C[1]) - end - @test summedhct(ψ.C[1], t) ≈ sum1 - - summedhct = AC_hamiltonian(1, ψ, summedH, ψ, summed_envs) - sum2 = sum(zip(fs, Hs, envs)) do (f, H, env) - if f isa Function - f = f(t) - end - return f * AC_hamiltonian(1, ψ, H, ψ, env)(ψ.AC[1]) - end - @test summedhct(ψ.AC[1], t) ≈ sum2 - - v = _transpose_front(ψ.AC[1]) * _transpose_tail(ψ.AR[2]) - summedhct = AC2_hamiltonian(1, ψ, summedH, ψ, summed_envs) - sum3 = sum(zip(fs, Hs, envs)) do (f, H, env) - return (f isa Function ? f(t) : f) * AC2_hamiltonian(1, ψ, H, ψ, env)(v) - end - @test summedhct(v, t) ≈ sum3 - end - - @testset "LazySum InfiniteMPOHamiltonian" begin - ψ = repeat(InfiniteMPS(pspace, Dspace), 2) - Hs = map(Os) do O - H = InfiniteMPOHamiltonian(O) - return repeat(H, 2) - end - summedH = LazySum(Hs) - envs = map(H -> environments(ψ, H, ψ), Hs) - summed_envs = environments(ψ, summedH, ψ) - - expval = sum(zip(Hs, envs)) do (H, Env) - return expectation_value(ψ, H, Env) - end - expval1 = expectation_value(ψ, sum(summedH)) - expval2 = expectation_value(ψ, summedH, summed_envs) - expval3 = expectation_value(ψ, summedH) - @test expval ≈ expval1 - @test expval ≈ expval2 - @test expval ≈ expval3 - - # test derivatives - summedhct = C_hamiltonian(1, ψ, summedH, ψ, summed_envs) - sum1 = sum(zip(Hs, envs)) do (H, env) - return C_hamiltonian(1, ψ, H, ψ, env)(ψ.C[1]) - end - @test summedhct(ψ.C[1], 0.0) ≈ sum1 - - summedhct = AC_hamiltonian(1, ψ, summedH, ψ, summed_envs) - sum2 = sum(zip(Hs, envs)) do (H, env) - return AC_hamiltonian(1, ψ, H, ψ, env)(ψ.AC[1]) - end - @test summedhct(ψ.AC[1], 0.0) ≈ sum2 - - v = _transpose_front(ψ.AC[1]) * _transpose_tail(ψ.AR[2]) - summedhct = AC2_hamiltonian(1, ψ, summedH, ψ, summed_envs) - sum3 = sum(zip(Hs, envs)) do (H, env) - return AC2_hamiltonian(1, ψ, H, ψ, env)(v) - end - @test summedhct(v, 0.0) ≈ sum3 - - Hts = [MultipliedOperator(Hs[1], fs[1]), MultipliedOperator(Hs[2], fs[2]), Hs[3]] - summedH = LazySum(Hts) - t = 1.1 - summedH_at = summedH(t) - - envs = map(H -> environments(ψ, H, ψ), Hs) - summed_envs = environments(ψ, summedH, ψ) - - expval = sum(zip(fs, Hs, envs)) do (f, H, env) - return (f isa Function ? f(t) : f) * expectation_value(ψ, H, env) - end - expval1 = expectation_value(ψ, sum(summedH_at)) - expval2 = expectation_value(ψ, summedH_at, summed_envs) - expval3 = expectation_value(ψ, summedH_at) - @test expval ≈ expval1 - @test expval ≈ expval2 - @test expval ≈ expval3 - - # test derivatives - summedhct = C_hamiltonian(1, ψ, summedH, ψ, summed_envs) - sum1 = sum(zip(fs, Hs, envs)) do (f, H, env) - if f isa Function - f = f(t) - end - return f * C_hamiltonian(1, ψ, H, ψ, env)(ψ.C[1]) - end - @test summedhct(ψ.C[1], t) ≈ sum1 - - summedhct = AC_hamiltonian(1, ψ, summedH, ψ, summed_envs) - sum2 = sum(zip(fs, Hs, envs)) do (f, H, env) - if f isa Function - f = f(t) - end - return f * AC_hamiltonian(1, ψ, H, ψ, env)(ψ.AC[1]) - end - @test summedhct(ψ.AC[1], t) ≈ sum2 - - v = _transpose_front(ψ.AC[1]) * _transpose_tail(ψ.AR[2]) - summedhct = AC2_hamiltonian(1, ψ, summedH, ψ, summed_envs) - sum3 = sum(zip(fs, Hs, envs)) do (f, H, env) - return (f isa Function ? f(t) : f) * AC2_hamiltonian(1, ψ, H, ψ, env)(v) - end - @test summedhct(v, t) ≈ sum3 - end -end +TestSuite.operators_lazysum_general(Vector; fast = fast_tests) +TestSuite.operators_lazysum_multipliedoperator(Vector; fast = fast_tests) +TestSuite.operators_lazysum_timedependent(Vector; fast = fast_tests) +TestSuite.operators_lazysum_hamiltonian(Vector; fast = fast_tests) diff --git a/test/operators/projection.jl b/test/operators/projection.jl index ad6165d8d..875466a93 100644 --- a/test/operators/projection.jl +++ b/test/operators/projection.jl @@ -4,18 +4,7 @@ println(" --------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "ProjectionOperator" begin - L = 10 - psi = FiniteMPS(rand, ComplexF64, L, ℙ^2, ℙ^2) - psi2 = FiniteMPS(rand, ComplexF64, L, ℙ^2, ℙ^2) - O = MPSKit.ProjectionOperator(psi) - - @test expectation_value(psi, O) ≈ 1.0 - @test expectation_value(psi2, O) ≈ dot(psi, psi2) * dot(psi2, psi) -end +TestSuite.operators_projection(Vector; fast = fast_tests) diff --git a/test/operators/window.jl b/test/operators/window.jl index 3537d3d2b..d9b8aeb37 100644 --- a/test/operators/window.jl +++ b/test/operators/window.jl @@ -4,64 +4,7 @@ println(" --------------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "WindowMPOHamiltonian" begin - # a uniform state, represented with a two-site unit cell, so that carving out a window - # at different offsets exercises the (non-trivial) circshift alignment of the infinite - # environments while the physics stays translationally invariant. - H1 = force_planar(transverse_field_ising(; g = 1.5)) - gs1, = find_groundstate(InfiniteMPS([ℙ^2], [ℙ^12]), H1, VUMPS(; verbosity = 0)) - gs = repeat(gs1, 2) - H = repeat(H1, 2) - - L = 6 - - @testset "interval offset (circshift)" begin - # the window energy must be independent of the offset for a uniform state - energies = map((1:L, 2:(L + 1), 3:(L + 2))) do interval - ψ = WindowMPS(gs, interval) - Hw = WindowMPOHamiltonian(H, interval) - @test length(Hw) == length(interval) - # the window itself is finite, even though its environments are not - @test isfinite(Hw) - @test isfinite(typeof(Hw)) - @test isfinite(Hw) == isfinite(typeof(Hw)) - return expectation_value(ψ, Hw) - end - @test energies[1] ≈ energies[2] atol = 1.0e-8 - @test energies[1] ≈ energies[3] atol = 1.0e-8 - end - - @testset "linear algebra" begin - ψ = WindowMPS(gs, 1:L) - Hw = WindowMPOHamiltonian(H, 1:L) - e = expectation_value(ψ, Hw) - - @test expectation_value(ψ, Hw + Hw) ≈ 2 * e atol = 1.0e-8 - @test expectation_value(ψ, Hw - Hw) ≈ 0 atol = 1.0e-8 - @test expectation_value(ψ, 3 * Hw) ≈ 3 * e atol = 1.0e-8 - @test expectation_value(ψ, Hw * 3) ≈ 3 * e atol = 1.0e-8 - @test expectation_value(ψ, -Hw) ≈ -e atol = 1.0e-8 - end - - @testset "distinct summands" begin - # add two genuinely different Hamiltonians: the ZZ Ising string has a single bulk - # level while the XY hopping needs two, so the boundary/bulk virtual spaces of the - # two summands differ and have to be block-diagonalized consistently. - ψ = WindowMPS(gs1, 1:L) - Ha = WindowMPOHamiltonian(H1, 1:L) - Hb = WindowMPOHamiltonian(force_planar(XY_model(; g = 0.7)), 1:L) - @test right_virtualspace(Ha.finite_ham, 3) != right_virtualspace(Hb.finite_ham, 3) - - ea = expectation_value(ψ, Ha) - eb = expectation_value(ψ, Hb) - @test expectation_value(ψ, Ha + Hb) ≈ ea + eb atol = 1.0e-8 - @test expectation_value(ψ, Hb + Ha) ≈ ea + eb atol = 1.0e-8 - @test expectation_value(ψ, Ha - Hb) ≈ ea - eb atol = 1.0e-8 - end -end +TestSuite.operators_window(Vector; fast = fast_tests) diff --git a/test/runtests.jl b/test/runtests.jl index ef29c29ad..7e022481d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,6 +10,9 @@ testsuite = find_tests(@__DIR__) # remove setup code and add as init filter!(!(startswith("setup") ∘ first), testsuite) +# the testsuite is included by the individual test files +filter!(!(startswith("testsuite") ∘ first), testsuite) + # only run CUDA if on buildkite is_buildkite = get(ENV, "BUILDKITE", "false") == "true" is_buildkite && filter!(startswith("gpu") ∘ first, testsuite) diff --git a/test/states/arithmetic.jl b/test/states/arithmetic.jl index e3edc7795..bdcbe8654 100644 --- a/test/states/arithmetic.jl +++ b/test/states/arithmetic.jl @@ -4,238 +4,13 @@ println(" -------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: _transpose_front, _transpose_tail, left_gauge -using TensorKit -using TensorKit: PlanarTrivial, ℙ - -""" - gauge_error(ψ::FiniteMPS) - -Largest relative violation of the defining gauge relations `AL[i] * C[i] == AC[i] == C[i-1] * AR[i]`. -""" -function gauge_error(ψ::FiniteMPS) - err = zero(real(scalartype(ψ))) - for i in 1:length(ψ) - AC = ψ.AC[i] - err = max(err, norm(ψ.AL[i] * ψ.C[i] - AC) / norm(AC)) - err = max( - err, - norm(_transpose_front(ψ.C[i - 1] * _transpose_tail(ψ.AR[i])) - AC) / norm(AC) - ) - end - return err -end - -""" - flip_gauge!(ψ::FiniteMPS) - -Re-gauge site 1 into an equally valid but different gauge (a sign flip) and materialise `AC[1]`. -This reproduces the cache state left behind by e.g. DMRG, which installs `AL, C` from a truncated -SVD while the lazy gauge sweep re-derives them from a positive QR. -""" -function flip_gauge!(ψ::FiniteMPS) - AL, C, = left_gauge(ψ.AC[1]) - ψ.AC[1] = (-AL, -C) - ψ.AC[1] - return ψ -end - -# (virtual space, second virtual space, physical space, scalartype) -const arithmetic_spaces = [ - (ℙ^8, ℙ^5, ℙ^2, ComplexF64), - ( - Rep[U₁](-1 => 2, 0 => 2, 1 => 2), Rep[U₁](-1 => 1, 0 => 3, 1 => 1), - Rep[U₁](-1 => 1, 0 => 1, 1 => 1), ComplexF64, - ), - # integer spin, so that a trivial-sector state exists for every chain length - ( - Rep[SU₂](0 => 2, 1 => 2, 2 => 1), Rep[SU₂](0 => 1, 1 => 3), - Rep[SU₂](1 => 1), ComplexF64, - ), -] - -@testset "lazy gauging is a cache ($(sectortype(D)))" for (D, _, d, elt) in arithmetic_spaces - for L in (4, 6) - # sweeping right must not change tensors already handed out - ψ = flip_gauge!(FiniteMPS(rand, elt, L, d, D)) - n = norm(ψ) - AL₁, C₀ = copy(ψ.AL[1]), copy(ψ.C[1]) - ψ.AL[2] # triggers a gauge-center move - @test ψ.AL[1] ≈ AL₁ - @test ψ.C[1] ≈ C₀ - @test norm(ψ) ≈ n - @test gauge_error(ψ) ≤ sqrt(eps(real(elt))) - - # ... and neither must sweeping back left - ϕ = flip_gauge!(FiniteMPS(rand, elt, L, d, D)) - ϕ.C[L - 1] - ARₗ, Cₗ = copy(ϕ.AR[L]), copy(ϕ.C[L - 1]) - ϕ.AR[2] - @test ϕ.AR[L] ≈ ARₗ - @test ϕ.C[L - 1] ≈ Cₗ - @test gauge_error(ϕ) ≤ sqrt(eps(real(elt))) - end -end - -@testset "FiniteMPS ± dense cross-check ($(sectortype(D)), L=$L)" for (D, D₂, d, elt) in - arithmetic_spaces, - # `convert(TensorMap, ψ)` compiles per (sectortype, length), so only sweep `L` once. - # `L = 1` is only swept for the trivial sector: a single spin-1 site carries no singlet. - L in (sectortype(D) === PlanarTrivial ? (1, 2, 3, 4, 5) : (4,)) - - ψ₁ = FiniteMPS(rand, elt, L, d, D) - ψ₂ = FiniteMPS(rand, elt, L, d, D) - ψ₃ = FiniteMPS(rand, elt, L, d, D₂) # mismatched bond dimensions - t₁, t₂, t₃ = convert.(TensorMap, (ψ₁, ψ₂, ψ₃)) - atol = sqrt(eps(real(elt))) - - @test convert(TensorMap, ψ₁ + ψ₂) ≈ t₁ + t₂ atol = atol - @test convert(TensorMap, ψ₁ - ψ₂) ≈ t₁ - t₂ atol = atol - @test convert(TensorMap, ψ₁ + ψ₃) ≈ t₁ + t₃ atol = atol - @test convert(TensorMap, ψ₃ - ψ₁) ≈ t₃ - t₁ atol = atol - @test norm(ψ₁ + ψ₂) ≈ norm(t₁ + t₂) atol = atol - @test dot(ψ₁ + ψ₂, ψ₁ - ψ₂) ≈ dot(t₁ + t₂, t₁ - t₂) atol = atol - - # scaling is distributive over addition - @test convert(TensorMap, 2 * ψ₁ + 3 * ψ₂) ≈ 2 * t₁ + 3 * t₂ atol = atol -end - -@testset "FiniteMPS argument checks" begin - @test_throws DimensionMismatch FiniteMPS(rand, ComplexF64, 4, ℂ^2, ℂ^4) + - FiniteMPS(rand, ComplexF64, 5, ℂ^2, ℂ^4) - # mismatched boundary virtual spaces, at length 1 and beyond - onesite(V₁, V₂) = FiniteMPS( - rand, ComplexF64, [ℂ^2], ComplexSpace[]; left = V₁, right = V₂, normalize = false - ) - @test_throws SpaceMismatch onesite(ℂ^3, ℂ^1) + onesite(ℂ^2, ℂ^1) - @test_throws SpaceMismatch onesite(ℂ^1, ℂ^3) + onesite(ℂ^1, ℂ^2) -end - -@testset "FiniteMPS cancellation across gauges ($(sectortype(D)))" for (D, _, d, elt) in - arithmetic_spaces - # same vector, different tensor network: re-canonicalised through a dense tensor - ψ₄ = FiniteMPS(rand, elt, 4, d, D) - @test norm(ψ₄ - FiniteMPS(convert(TensorMap, ψ₄))) ≈ 0 atol = - sqrt(eps(real(elt))) * norm(ψ₄) - - for L in 2:6 - ψ = FiniteMPS(rand, elt, L, d, D) - atol = sqrt(eps(real(elt))) * norm(ψ) - - # same vector, different *gauge history* -- this is issue #473 - χ = flip_gauge!(copy(ψ)) - @test norm(ψ - χ) ≈ 0 atol = atol - @test norm(χ - ψ) ≈ 0 atol = atol - @test norm(2 * ψ - χ - ψ) ≈ 0 atol = atol - @test norm(3 * ψ - χ) ≈ 2 * norm(ψ) atol = atol - end -end - -@testset "eigenstate cancellation, issue #473 (L=$L)" for L in (4, 6) - # `gs` is an eigenstate of `H`, so `E₀ * gs - H * gs` must be the zero vector, even though the - # two operands are carried by completely different tensor networks. - H = transverse_field_ising(; g = 1.0, L = L) - ψ₀ = FiniteMPS(rand, ComplexF64, L, physicalspace(H, 1), ℂ^16) - gs, envs, = find_groundstate(ψ₀, H, DMRG(; verbosity = 0)) - E₀ = real(expectation_value(gs, H, envs)) - atol = 1.0e-8 * abs(E₀) - - a = E₀ * gs - b = H * gs - @test convert(TensorMap, a) ≈ convert(TensorMap, b) atol = atol # the operands agree - - @test norm(a - b) ≈ 0 atol = atol - @test norm(convert(TensorMap, a - b)) ≈ 0 atol = atol - @test norm(b - a) ≈ 0 atol = atol - - # ... independent of whether the operands were gauged before the subtraction - a′, b′ = E₀ * gs, H * gs - convert(TensorMap, a′) - convert(TensorMap, b′) - @test norm(a′ - b′) ≈ 0 atol = atol - - # a non-trivial linear combination is still correct - @test norm(3 * (E₀ * gs) - H * gs) ≈ 2 * abs(E₀) * norm(gs) atol = atol -end - -@testset "single-site addition" begin - atol = sqrt(eps(Float64)) - - # trivial boundaries: cross-check against the dense state - ψ₁ = FiniteMPS(rand, ComplexF64, 1, ℂ^3, ℂ^4) - ψ₂ = FiniteMPS(rand, ComplexF64, 1, ℂ^3, ℂ^4) - t₁, t₂ = convert(TensorMap, ψ₁), convert(TensorMap, ψ₂) - @test length(ψ₁ + ψ₂) == 1 - @test convert(TensorMap, ψ₁ + ψ₂) ≈ t₁ + t₂ atol = atol - @test convert(TensorMap, ψ₁ - ψ₂) ≈ t₁ - t₂ atol = atol - @test norm(ψ₁ + ψ₂) ≈ norm(t₁ + t₂) atol = atol - @test norm(ψ₁ - ψ₁) ≈ 0 atol = atol - @test gauge_error(ψ₁ + ψ₂) ≤ atol - - # non-trivial boundaries, e.g. a single-site window carved out of a larger chain. There is no - # dense state to compare against, but the whole state *is* the one center tensor. - onesite(V₁, V₂) = FiniteMPS( - rand, ComplexF64, [ℂ^2], ComplexSpace[]; left = V₁, right = V₂, normalize = false - ) - ϕ₁, ϕ₂ = onesite(ℂ^3, ℂ^4), onesite(ℂ^3, ℂ^4) - AC₁, AC₂ = copy(ϕ₁.AC[1]), copy(ϕ₂.AC[1]) - @test (ϕ₁ + ϕ₂).AC[1] ≈ AC₁ + AC₂ atol = atol - @test (ϕ₁ - ϕ₂).AC[1] ≈ AC₁ - AC₂ atol = atol - @test left_virtualspace(ϕ₁ + ϕ₂, 1) == ℂ^3 - @test right_virtualspace(ϕ₁ + ϕ₂, 1) == ℂ^4 - @test norm(ϕ₁ + ϕ₂) ≈ norm(AC₁ + AC₂) atol = atol - @test gauge_error(ϕ₁ + ϕ₂) ≤ atol - - # the operands are left untouched - @test ϕ₁.AC[1] ≈ AC₁ && ϕ₂.AC[1] ≈ AC₂ - - # ... and the same for a single-site `FiniteMPO` - o₁, o₂ = rand(ComplexF64, ℂ^3 ← ℂ^3), rand(ComplexF64, ℂ^3 ← ℂ^3) - O₁, O₂ = FiniteMPO(o₁), FiniteMPO(o₂) - @test length(O₁) == 1 - # `convert` on a single-site MPO used to contract `mpo[1]` with `mpo[end]`, the same tensor - @test convert(TensorMap, O₁) ≈ o₁ atol = atol - @test length(O₁ + O₂) == 1 - @test (O₁ + O₂)[1] ≈ O₁[1] + O₂[1] atol = atol - @test (O₁ - O₂)[1] ≈ O₁[1] - O₂[1] atol = atol - @test convert(TensorMap, O₁ + O₂) ≈ o₁ + o₂ atol = atol - @test convert(TensorMap, O₁ - O₂) ≈ o₁ - o₂ atol = atol - - # scalar types promote at length 1 too - @test scalartype( - FiniteMPS(rand, Float64, 1, ℂ^3, ℂ^4) + FiniteMPS(rand, ComplexF64, 1, ℂ^3, ℂ^4) - ) == - ComplexF64 - @test scalartype(FiniteMPO(rand(Float64, ℂ^3 ← ℂ^3)) + FiniteMPO(rand(ComplexF64, ℂ^3 ← ℂ^3))) == - ComplexF64 -end - -@testset "scalar-type promotion" begin - L = 4 - - ψᵣ = FiniteMPS(rand, Float64, L, ℂ^2, ℂ^4) - ψᶜ = FiniteMPS(rand, ComplexF64, L, ℂ^2, ℂ^4) - tᵣ, tᶜ = convert(TensorMap, ψᵣ), convert(TensorMap, ψᶜ) - - @test scalartype(ψᵣ + ψᶜ) == ComplexF64 - @test scalartype(ψᶜ + ψᵣ) == ComplexF64 - @test convert(TensorMap, ψᵣ + ψᶜ) ≈ tᵣ + tᶜ - @test convert(TensorMap, ψᶜ + ψᵣ) ≈ tᶜ + tᵣ - @test scalartype(ψᵣ - ψᶜ) == ComplexF64 - @test convert(TensorMap, ψᵣ - ψᶜ) ≈ tᵣ - tᶜ - # both operands real stays real - @test scalartype(ψᵣ + ψᵣ) == Float64 - - Oᵣ = FiniteMPO(rand(Float64, (ℂ^2)^L ← (ℂ^2)^L)) - Oᶜ = FiniteMPO(rand(ComplexF64, (ℂ^2)^L ← (ℂ^2)^L)) - oᵣ, oᶜ = convert(TensorMap, Oᵣ), convert(TensorMap, Oᶜ) - - @test scalartype(Oᵣ + Oᶜ) == ComplexF64 - @test scalartype(Oᶜ + Oᵣ) == ComplexF64 - @test convert(TensorMap, Oᵣ + Oᶜ) ≈ oᵣ + oᶜ - @test convert(TensorMap, Oᶜ + Oᵣ) ≈ oᶜ + oᵣ - @test scalartype(Oᵣ + Oᵣ) == Float64 -end +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite + +TestSuite.states_arithmetic_lazy_gauging(Vector; fast = fast_tests) +TestSuite.states_arithmetic_dense_crosscheck(Vector; fast = fast_tests) +TestSuite.states_arithmetic_argument_checks(Vector; fast = fast_tests) +TestSuite.states_arithmetic_cancellation_across_gauges(Vector; fast = fast_tests) +TestSuite.states_arithmetic_eigenstate_cancellation(Vector; fast = fast_tests) +TestSuite.states_arithmetic_single_site_addition(Vector; fast = fast_tests) +TestSuite.states_arithmetic_scalartype_promotion(Vector; fast = fast_tests) diff --git a/test/states/finitemps.jl b/test/states/finitemps.jl index daf76bb2f..c69d0f290 100644 --- a/test/states/finitemps.jl +++ b/test/states/finitemps.jl @@ -4,158 +4,12 @@ println(" ---------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: _transpose_front, _transpose_tail -using MPSKit: GeometryStyle, FiniteChainStyle -using TensorKit -using TensorKit: ℙ -using Adapt - -@testset "FiniteMPS ($(sectortype(D)), $elt)" for (D, d, elt) in [ - (ℙ^10, ℙ^2, ComplexF64), - ( - Rep[SU₂](1 => 1, 0 => 3), - Rep[SU₂](0 => 1) * Rep[SU₂](0 => 1), - ComplexF32, - ), - ] - L = rand(3:20) - ψ = FiniteMPS(rand, elt, L, d, D) - - @test eachindex(IndexLinear(), ψ) == eachindex(ψ) - @test isfinite(ψ) - @test isfinite(typeof(ψ)) - @test isfinite(ψ) == isfinite(typeof(ψ)) - @test GeometryStyle(typeof(ψ)) == FiniteChainStyle() - @test GeometryStyle(ψ) == FiniteChainStyle() - @test @constinferred physicalspace(ψ) == fill(d, L) - @test all(x -> x ≾ D, @constinferred left_virtualspace(ψ)) - @test all(x -> x ≾ D, @constinferred right_virtualspace(ψ)) - - @test eltype(ψ) == eltype(typeof(ψ)) - - ovl = @constinferred dot(ψ, ψ) - - @test ovl ≈ norm(ψ.AC[1])^2 - @test ovl isa scalartype(ψ) - - for i in 1:length(ψ) - @test ψ.AC[i] ≈ ψ.AL[i] * ψ.C[i] - @test ψ.AC[i] ≈ _transpose_front(ψ.C[i - 1] * _transpose_tail(ψ.AR[i])) - end - - @test elt == scalartype(ψ) - - ψ = ψ * 3 - @test ovl * 9 ≈ norm(ψ)^2 - ψ = 3 * ψ - @test ovl * 9 * 9 ≈ norm(ψ)^2 - - @test norm(2 * ψ + ψ - 3 * ψ) ≈ 0.0 atol = sqrt(eps(real(elt))) -end - -@testset "FiniteMPS ($(sectortype(D)), $elt)" for (D, d, elt) in MPS_TEST_SPACES - ψ_small = FiniteMPS(rand, elt, 4, d, D) - ψ_small2 = FiniteMPS(convert(TensorMap, ψ_small)) - @test dot(ψ_small, ψ_small2) ≈ dot(ψ_small, ψ_small) - - ψ′ = @constinferred complex(ψ_small) - @test scalartype(ψ′) <: Complex - if elt <: Complex - @test ψ_small === ψ′ - else - @test norm(ψ_small) ≈ norm(ψ′) - @test complex(convert(TensorMap, ψ_small)) ≈ convert(TensorMap, ψ′) - end -end - -@testset "FiniteMPS center + (slice) indexing" begin - L = 11 - ψ = FiniteMPS(L, ℂ^2, ℂ^16) - - ψ.AC[6] # moving the center to site 6 - - @test ψ.center == 6 - - @test ψ[5] == ψ.ALs[5] - @test ψ[6] == ψ.ACs[6] - @test ψ[7] == ψ.ARs[7] - - @test ψ[5:7] == [ψ.ALs[5], ψ.ACs[6], ψ.ARs[7]] - - @inferred ψ[5] - - @test_throws BoundsError ψ[0] - @test_throws BoundsError ψ[L + 1] - - ψ.C[6] = randn(ComplexF64, space(ψ.C[6])) # setting the center between sites 6 and 7 - @test ψ.center == 13 / 2 - @test ψ[5:7] == [ψ.ALs[5], ψ.ACs[6], ψ.ARs[7]] -end - -# Regression: invalidating a long chain of cached AL tensors (by setting AC near one end) -# used to stack overflow, since re-gauging walked the invalidated range recursively. -@testset "FiniteMPS gauging does not stack overflow" begin - ψ = FiniteMPS(10_000, ℂ^2, ℂ^1) - @test ψ.AR[1] isa MPSKit.MPSTensor - ψ.AC[1] = -ψ.AR[1] # force invalidation of ALs - @test ψ.AL[end] isa MPSKit.MPSTensor -end - -@testset "FiniteMPS copying" begin - L = 10 - mps1 = FiniteMPS(rand, ComplexF64, L, ℂ^2, ℂ^5) - mps2 = copy(mps1) - - @test mps1 !== mps2 - - # arrays are distinct - @test mps1.ALs !== mps2.ALs - @test mps1.ARs !== mps2.ARs - @test mps1.ACs !== mps2.ACs - @test mps1.Cs !== mps2.Cs - - # tensors are distinct but equal - for i in 1:L - @test (ismissing(mps1.ALs[i]) && ismissing(mps2.ALs[i])) || - (mps1.ALs[i] !== mps2.ALs[i] && mps1.ALs[i] == mps2.ALs[i]) - @test (ismissing(mps1.ARs[i]) && ismissing(mps2.ARs[i])) || - (mps1.ARs[i] !== mps2.ARs[i] && mps1.ARs[i] == mps2.ARs[i]) - @test (ismissing(mps1.ACs[i]) && ismissing(mps2.ACs[i])) || - (mps1.ACs[i] !== mps2.ACs[i] && mps1.ACs[i] == mps2.ACs[i]) - @test (ismissing(mps1.Cs[i]) && ismissing(mps2.Cs[i])) || - (mps1.Cs[i] !== mps2.Cs[i] && mps1.Cs[i] == mps2.Cs[i]) - end - @test (ismissing(mps1.Cs[end]) && ismissing(mps2.Cs[end])) || - mps1.Cs[end] !== mps2.Cs[end] -end - -@testset "FiniteMPS entropy ($(sectortype(D)), $elt)" for (D, d, elt) in MPS_TEST_SPACES - L = 6 - ψ = FiniteMPS(rand, elt, L, d, D) - - # entropy is non-negative at all sites - for site in 1:L - @test real(entropy(ψ, site)) >= 0 - end - - @test entropy(ψ, 0) ≈ 0 atol = 1.0e-10 # trivial left bond - @test_throws BoundsError entanglement_spectrum(ψ, -1) - @test_throws BoundsError entanglement_spectrum(ψ, L + 1) - - # entropy is consistent with entanglement_spectrum - for site in 1:L - @test entropy(ψ, site) ≈ entropy(entanglement_spectrum(ψ, site)) - end - - # entropy is zero at the right boundary (trivial bond) - @test entropy(ψ, L) ≈ 0 atol = 1.0e-10 - - # product state has zero entropy everywhere - ψ_product = FiniteMPS(rand, elt, L, d, unitspace(D)) - for site in 1:L - @test entropy(ψ_product, site) ≈ 0 atol = 1.0e-10 - end -end +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite + +TestSuite.states_finitemps_basics(Vector; fast = fast_tests) +TestSuite.states_finitemps_complex(Vector; fast = fast_tests) +TestSuite.states_finitemps_center_indexing(Vector; fast = fast_tests) +TestSuite.states_finitemps_gauging_stackoverflow(Vector; fast = fast_tests) +TestSuite.states_finitemps_copying(Vector; fast = fast_tests) +TestSuite.states_finitemps_entropy(Vector; fast = fast_tests) diff --git a/test/states/infinitemps.jl b/test/states/infinitemps.jl index 90f03c468..e6870af81 100644 --- a/test/states/infinitemps.jl +++ b/test/states/infinitemps.jl @@ -4,135 +4,11 @@ println(" ------------------------ ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: GeometryStyle, InfiniteChainStyle, TransferMatrix -using TensorKit -using TensorKit: ℙ -using Adapt - -@testset "InfiniteMPS ($(sectortype(D)), $elt)" for (D, d, elt) in MPS_TEST_SPACES - tol = Float64(eps(real(elt)) * 100) - - ψ = InfiniteMPS([rand(elt, D * d, D), rand(elt, D * d, D)]; tol) - - @test !isfinite(typeof(ψ)) - @test !isfinite(ψ) - @test isfinite(ψ) == isfinite(typeof(ψ)) - @test GeometryStyle(typeof(ψ)) == InfiniteChainStyle() - @test GeometryStyle(ψ) == InfiniteChainStyle() - - @test eltype(ψ) == eltype(typeof(ψ)) - - @test physicalspace(ψ) == fill(d, 2) - @test all(x -> x ≾ D, left_virtualspace(ψ)) - @test all(x -> x ≾ D, right_virtualspace(ψ)) - - for i in 1:length(ψ) - @plansor difference[-1 -2; -3] := ψ.AL[i][-1 -2; 1] * ψ.C[i][1; -3] - - ψ.C[i - 1][-1; 1] * ψ.AR[i][1 -2; -3] - @test norm(difference, Inf) < tol * 10 - - @test l_LL(ψ, i) * TransferMatrix(ψ.AL[i], ψ.AL[i]) ≈ l_LL(ψ, i + 1) - @test l_LR(ψ, i) * TransferMatrix(ψ.AL[i], ψ.AR[i]) ≈ l_LR(ψ, i + 1) - @test l_RL(ψ, i) * TransferMatrix(ψ.AR[i], ψ.AL[i]) ≈ l_RL(ψ, i + 1) - @test l_RR(ψ, i) * TransferMatrix(ψ.AR[i], ψ.AR[i]) ≈ l_RR(ψ, i + 1) - - @test TransferMatrix(ψ.AL[i], ψ.AL[i]) * r_LL(ψ, i) ≈ r_LL(ψ, i + 1) - @test TransferMatrix(ψ.AL[i], ψ.AR[i]) * r_LR(ψ, i) ≈ r_LR(ψ, i + 1) - @test TransferMatrix(ψ.AR[i], ψ.AL[i]) * r_RL(ψ, i) ≈ r_RL(ψ, i + 1) - @test TransferMatrix(ψ.AR[i], ψ.AR[i]) * r_RR(ψ, i) ≈ r_RR(ψ, i + 1) - end - - # rebuilding from the left or right isometries and a bond tensor gives back the same state - for ψ′ in (InfiniteMPS(ψ.AL, ψ.C[end]; tol), InfiniteMPS(ψ.C[0], ψ.AR; tol)) - @test scalartype(ψ′) == elt - @test abs(dot(ψ, ψ′)) ≈ 1 - end -end - -@testset "InfiniteMPS copying" begin - mps1 = InfiniteMPS(rand, ComplexF64, [2], [5]) - mps2 = copy(mps1) - - @test mps1 !== mps2 - - # elements are equal - @test mps1.AL[1] == mps2.AL[1] - @test mps1.AR[1] == mps2.AR[1] - @test mps1.AC[1] == mps2.AC[1] - @test mps1.C[1] == mps2.C[1] - - # arrays are distinct - @test mps1.AL !== mps2.AL - @test mps1.AR !== mps2.AR - @test mps1.AC !== mps2.AC - @test mps1.C !== mps2.C - - # tensors are distinct - @test mps1.AL[1] !== mps2.AL[1] - @test mps1.AR[1] !== mps2.AR[1] - @test mps1.AC[1] !== mps2.AC[1] - @test mps1.C[1] !== mps2.C[1] -end - -Ts = fast_tests ? (Float64,) : (Float64, ComplexF64) -@testset "Adapt" begin - for (d, D) in [(ℂ^2, ℂ^4), (ℙ^2, ℙ^4)] - mps1 = InfiniteMPS(Float32, d, D) - for T in Ts - mps2 = @testinferred adapt(Vector{T}, mps1) - @test mps2 isa InfiniteMPS - @test scalartype(mps2) == T - @test storagetype(mps2) == Vector{T} - @test dot(mps1, mps2) ≈ 1 atol = 1.0e-4 - end - end -end - -@testset "InfiniteMPS entropy ($(sectortype(D)), $elt)" for (D, d, elt) in MPS_TEST_SPACES - ψ = InfiniteMPS([d, d], [D, D]) - - # entropy(ψ) returns one value per site, all non-negative - Ss = entropy(ψ) - @test length(Ss) == length(ψ) - @test all(isreal, Ss) - @test all(>=(-1.0e-8), Ss) - - # entropy(ψ, site) is non-negative and consistent with entropy(ψ) - for site in 1:length(ψ) - @test entropy(ψ, site) ≈ Ss[site] - @test entropy(ψ, site) ≈ entropy(entanglement_spectrum(ψ, site)) - end - - # product state has zero entropy everywhere - ψ_product = InfiniteMPS([d, d], [unitspace(D), unitspace(D)]) - Ss_product = entropy(ψ_product) - @test all(S -> isapprox(S, 0; atol = 1.0e-10), Ss_product) -end - -@testset "InfiniteMPS copying" begin - mps1 = InfiniteMPS(2, 5) - mps2 = copy(mps1) - - @test mps1 !== mps2 - - # elements are equal - @test mps1.AL[1] == mps2.AL[1] - @test mps1.AR[1] == mps2.AR[1] - @test mps1.AC[1] == mps2.AC[1] - @test mps1.C[1] == mps2.C[1] - - # arrays are distinct - @test mps1.AL !== mps2.AL - @test mps1.AR !== mps2.AR - @test mps1.AC !== mps2.AC - @test mps1.C !== mps2.C - - # tensors are distinct - @test mps1.AL[1] !== mps2.AL[1] - @test mps1.AR[1] !== mps2.AR[1] - @test mps1.AC[1] !== mps2.AC[1] - @test mps1.C[1] !== mps2.C[1] -end +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite + +TestSuite.states_infinitemps_basics(Vector; fast = fast_tests) +TestSuite.states_infinitemps_copying(Vector; fast = fast_tests) +TestSuite.states_infinitemps_adapt(Vector; fast = fast_tests) +TestSuite.states_infinitemps_entropy(Vector; fast = fast_tests) +TestSuite.states_infinitemps_copying_default(Vector; fast = fast_tests) diff --git a/test/states/multilinemps.jl b/test/states/multilinemps.jl index d8de181e9..8221412a6 100644 --- a/test/states/multilinemps.jl +++ b/test/states/multilinemps.jl @@ -4,55 +4,7 @@ println(" ------------------------ ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: GeometryStyle, InfiniteChainStyle, TransferMatrix -using TensorKit +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "MultilineMPS ($(sectortype(D)), $elt)" for (D, d, elt) in MPS_TEST_SPACES - tol = Float64(eps(real(elt)) * 100) - ψ = MultilineMPS( - [ - rand(elt, D * d, D) rand(elt, D * d, D) - rand(elt, D * d, D) rand(elt, D * d, D) - ]; tol - ) - - @test GeometryStyle(typeof(ψ)) == InfiniteChainStyle() - @test GeometryStyle(ψ) == InfiniteChainStyle() - @test TensorKit.storagetype(ψ) == Vector{elt} - @test TensorKit.sectortype(ψ) == sectortype(D) - - @test !isfinite(typeof(ψ)) - @test !isfinite(ψ) - - @test length(ψ) == 2 - @test size(ψ) == (2,) - @test size(ψ, 2) == 1 - @test axes(ψ) == (Base.OneTo(2),) - @test eachindex(ψ) == 1:2 - @test eltype(ψ) <: InfiniteMPS - @test size(ψ.AL) == size(ψ.AC) == size(ψ.C) == (2, 2) - - @test physicalspace(ψ) == fill(d, 2, 2) - @test all(x -> x ≾ D, left_virtualspace(ψ)) - @test all(x -> x ≾ D, right_virtualspace(ψ)) - @test leftunit(ψ) == unit(sectortype(D)) - - for i in 1:length(ψ), j in 1:length(ψ[1]) - @plansor difference[-1 -2; -3] := ψ.AL[i, j][-1 -2; 1] * ψ.C[i, j][1; -3] - - ψ.C[i, j - 1][-1; 1] * ψ.AR[i, j][1 -2; -3] - @test norm(difference, Inf) < tol * 10 - - @test l_LL(ψ, i, j) * TransferMatrix(ψ.AL[i, j], ψ.AL[i, j]) ≈ l_LL(ψ, i, j + 1) - @test l_LR(ψ, i, j) * TransferMatrix(ψ.AL[i, j], ψ.AR[i, j]) ≈ l_LR(ψ, i, j + 1) - @test l_RL(ψ, i, j) * TransferMatrix(ψ.AR[i, j], ψ.AL[i, j]) ≈ l_RL(ψ, i, j + 1) - @test l_RR(ψ, i, j) * TransferMatrix(ψ.AR[i, j], ψ.AR[i, j]) ≈ l_RR(ψ, i, j + 1) - - @test TransferMatrix(ψ.AL[i, j], ψ.AL[i, j]) * r_LL(ψ, i, j) ≈ r_LL(ψ, i, j + 1) - @test TransferMatrix(ψ.AL[i, j], ψ.AR[i, j]) * r_LR(ψ, i, j) ≈ r_LR(ψ, i, j + 1) - @test TransferMatrix(ψ.AR[i, j], ψ.AL[i, j]) * r_RL(ψ, i, j) ≈ r_RL(ψ, i, j + 1) - @test TransferMatrix(ψ.AR[i, j], ψ.AR[i, j]) * r_RR(ψ, i, j) ≈ r_RR(ψ, i, j + 1) - end -end +TestSuite.states_multilinemps(Vector; fast = fast_tests) diff --git a/test/states/quasiparticle.jl b/test/states/quasiparticle.jl index e9a81bd6c..643fd0515 100644 --- a/test/states/quasiparticle.jl +++ b/test/states/quasiparticle.jl @@ -4,98 +4,7 @@ println(" ----------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: GeometryStyle, FiniteChainStyle, InfiniteChainStyle -using TensorKit -using TensorKit: ℙ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -quasiparticle_finite_cases = [ - (force_planar(transverse_field_ising(; L = 10)), ℙ^10, ℙ^2), - ( - heisenberg_XXX(ComplexF64, SU2Irrep; spin = 1, L = 10), Rep[SU₂](1 => 1, 0 => 3), - Rep[SU₂](1 => 1), - ), -] -fast_tests && (quasiparticle_finite_cases = quasiparticle_finite_cases[1:1]) - -quasiparticle_infinite_cases = [ - (force_planar(transverse_field_ising()), ℙ^10, ℙ^2), - ( - heisenberg_XXX(ComplexF64, SU2Irrep; spin = 1), Rep[SU₂](1 => 3, 0 => 2), - Rep[SU₂](1 => 1), - ), -] -fast_tests && (quasiparticle_infinite_cases = quasiparticle_infinite_cases[1:1]) - -@testset "Quasiparticle state" verbose = true begin - L = 10 - @testset "Finite" verbose = true for (H, D, d) in quasiparticle_finite_cases - ψ = FiniteMPS(rand, ComplexF64, L, d, D) - normalize!(ψ) - - #rand_quasiparticle is a private non-exported function - ϕ₁ = LeftGaugedQP(rand, ψ) - ϕ₂ = LeftGaugedQP(rand, ψ) - @test TensorKit.storagetype(ϕ₁) == TensorKit.storagetype(ψ) - @test TensorKit.storagetype(typeof(ϕ₁)) == TensorKit.storagetype(ψ) - @test TensorKit.storagetype(ϕ₂) == TensorKit.storagetype(ψ) - @test TensorKit.storagetype(typeof(ϕ₂)) == TensorKit.storagetype(ψ) - - @test GeometryStyle(ϕ₁) == FiniteChainStyle() - @test GeometryStyle(typeof(ϕ₂)) == FiniteChainStyle() - - @test @constinferred physicalspace(ϕ₁) == physicalspace(ψ) - @test @constinferred left_virtualspace(ϕ₁) == left_virtualspace(ψ) - @test @constinferred right_virtualspace(ϕ₁) == right_virtualspace(ψ) - @test TensorKit.sectortype(ϕ₁) == TensorKit.sectortype(ψ) - - @test norm(axpy!(1, ϕ₁, copy(ϕ₂))) ≤ norm(ϕ₁) + norm(ϕ₂) - @test norm(ϕ₁) * 3 ≈ norm(ϕ₁ * 3) - - normalize!(ϕ₁) - - ϕ₁_f = convert(FiniteMPS, ϕ₁) - ϕ₂_f = convert(FiniteMPS, ϕ₂) - - @test dot(ϕ₁_f, ϕ₂_f) ≈ dot(ϕ₁, ϕ₂) atol = 1.0e-5 - @test norm(ϕ₁_f) ≈ norm(ϕ₁) atol = 1.0e-5 - - ev_f = expectation_value(ϕ₁_f, H) - expectation_value(ψ, H) - ev_q = dot(ϕ₁, MPSKit.effective_excitation_hamiltonian(H, ϕ₁)) - @test ev_f ≈ ev_q atol = 1.0e-5 - end - - @testset "Infinite" for (th, D, d) in quasiparticle_infinite_cases - period = rand(1:4) - ψ = InfiniteMPS(fill(d, period), fill(D, period)) - - @test eltype(ψ) == eltype(typeof(ψ)) - - #rand_quasiparticle is a private non-exported function - ϕ₁ = LeftGaugedQP(rand, ψ) - ϕ₂ = LeftGaugedQP(rand, ψ) - - @test GeometryStyle(ϕ₁) == InfiniteChainStyle() - @test GeometryStyle(typeof(ϕ₂)) == InfiniteChainStyle() - - @test @constinferred physicalspace(ϕ₁) == physicalspace(ψ) - @test @constinferred left_virtualspace(ϕ₁) == left_virtualspace(ψ) - @test @constinferred right_virtualspace(ϕ₁) == right_virtualspace(ψ) - for i in 1:period - @test physicalspace(ψ, i) == physicalspace(ϕ₁, i) - @test left_virtualspace(ψ, i) == left_virtualspace(ϕ₁, i) - @test right_virtualspace(ψ, i) == right_virtualspace(ϕ₁, i) - end - - @test norm(axpy!(1, ϕ₁, copy(ϕ₂))) ≤ norm(ϕ₁) + norm(ϕ₂) - @test norm(ϕ₁) * 3 ≈ norm(ϕ₁ * 3) - - @test dot( - ϕ₁, - convert(LeftGaugedQP, convert(RightGaugedQP, ϕ₁)) - ) ≈ - dot(ϕ₁, ϕ₁) atol = 1.0e-10 - end -end +TestSuite.states_quasiparticle(Vector; fast = fast_tests) diff --git a/test/states/windowmps.jl b/test/states/windowmps.jl index 0586488c8..473a4417e 100644 --- a/test/states/windowmps.jl +++ b/test/states/windowmps.jl @@ -4,76 +4,7 @@ println(" ---------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: _transpose_front, _transpose_tail -using TensorKit -using TensorKit: ℙ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "WindowMPS" begin - g = 8.0 - ham = force_planar(transverse_field_ising(; g)) - - # operator for testing expectation_value - X = S_x(; spin = 1 // 2) - E = TensorMap(ComplexF64[1 0; 0 1], ℂ^2 ← ℂ^2) - O = force_planar(-(S_z_S_z(; spin = 1 // 2) + (g / 2) * (X ⊗ E + E ⊗ X))) - - gs, = find_groundstate(InfiniteMPS([ℙ^2], [ℙ^10]), ham, VUMPS(; verbosity = 0)) - - # constructor 1 - give it a plain array of tensors - window_1 = WindowMPS(gs, copy.([gs.AC[1]; [gs.AR[i] for i in 2:10]]), gs) - - # constructor 2 - used to take a "slice" from an infinite mps - window_2 = WindowMPS(gs, 10) - - @test eltype(window_1) == eltype(typeof(window_1)) - - P = @constinferred physicalspace(window_2) - Vleft = @constinferred left_virtualspace(window_2) - Vright = @constinferred right_virtualspace(window_2) - - for i in -3:13 - @test physicalspace(window_2, i) == P[i] - @test left_virtualspace(window_2, i) == Vleft[i] - @test right_virtualspace(window_2, i) == Vright[i] - end - - # we should logically have that window_1 approximates window_2 - ovl = dot(window_1, window_2) - @test ovl ≈ 1 atol = 1.0e-8 - - # constructor 3 - random initial tensors - window = WindowMPS(rand, ComplexF64, 10, ℙ^2, ℙ^10, gs, gs) - normalize!(window) - - for i in 1:length(window) - @test window.AC[i] ≈ window.AL[i] * window.C[i] - @test window.AC[i] ≈ - _transpose_front(window.C[i - 1] * _transpose_tail(window.AR[i])) - end - - @test norm(window) ≈ 1 - window = window * 3 - @test 9 ≈ norm(window)^2 - window = 3 * window - @test 9 * 9 ≈ norm(window)^2 - normalize!(window) - - e1 = expectation_value(window, (2, 3) => O) - - w_ham = WindowMPOHamiltonian(ham, 1:length(window)) - window, envs, _ = find_groundstate(window, w_ham, DMRG(; verbosity = 0)) - - e2 = expectation_value(window, (2, 3) => O) - - @test real(e2) ≤ real(e1) - - window, envs = timestep(window, w_ham, 0.1, 0.0, TDVP2(; trunc = truncrank(20)), envs) - window, envs = timestep(window, w_ham, 0.1, 0.0, TDVP(), envs) - - e3 = expectation_value(window, (2, 3) => O) - - @test e2 ≈ e3 atol = 1.0e-4 -end +TestSuite.states_windowmps(Vector; fast = fast_tests) diff --git a/test/statmech/statmech.jl b/test/statmech/statmech.jl index 8a736c02b..3f84197f0 100644 --- a/test/statmech/statmech.jl +++ b/test/statmech/statmech.jl @@ -4,123 +4,7 @@ println(" ----------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "Finite temperature methods" begin - imaginary_evolution = true - @testset "Finite-size" begin - L = 6 - H = transverse_field_ising(; L) - trunc = truncrank(20) - verbosity = 1 - beta = 0.1 - - # exact diagonalization - H_dense = convert(TensorMap, H) - Z_dense_1 = tr(exp(-beta * H_dense))^(1 / L) - Z_dense_2 = tr(exp(-2beta * H_dense))^(1 / L) - - # taylor cluster - rho_taylor_1 = make_time_mpo(H, beta, TaylorCluster(; N = 2); imaginary_evolution) - Z_taylor_1 = tr(rho_taylor_1)^(1 / L) - @test Z_taylor_1 ≈ Z_dense_1 atol = 1.0e-2 - Z_taylor_2 = real(dot(rho_taylor_1, rho_taylor_1))^(1 / L) - @test Z_taylor_2 ≈ Z_dense_2 atol = 1.0e-2 - - E_x_taylor = @constinferred expectation_value(rho_taylor_1, 1 => S_x()) - E_xx_taylor = @constinferred expectation_value(rho_taylor_1, (1, 2) => S_x_S_x()) - - # WII - rho_wii = make_time_mpo(H, beta, WII(); imaginary_evolution) - Z_wii = tr(rho_wii)^(1 / L) - @test Z_wii ≈ Z_dense_1 atol = 1.0e-2 - @test expectation_value(rho_wii, 1 => S_x()) ≈ E_x_taylor atol = 1.0e-2 - @test expectation_value(rho_wii, (1, 2) => S_x_S_x()) ≈ E_xx_taylor atol = 1.0e-2 - - # MPO multiplication - rho_mps = convert(FiniteMPS, rho_taylor_1) - rho_mps, = approximate(rho_mps, (rho_taylor_1, rho_mps), DMRG2(; trunc, verbosity)) - Z_mpomul = tr(convert(FiniteMPO, rho_mps))^(1 / L) - @test Z_mpomul ≈ Z_dense_2 atol = 1.0e-2 - - # TDVP - rho_0 = MPSKit.infinite_temperature_density_matrix(H) - rho_0_mps = convert(FiniteMPS, rho_0) - rho_mps, = timestep(rho_0_mps, H, 0.0, beta, TDVP2(; trunc); imaginary_evolution) - Z_tdvp = real(dot(rho_mps, rho_mps))^(1 / L) - @test Z_tdvp ≈ Z_dense_2 atol = 1.0e-2 - - @test expectation_value(rho_0_mps, 1 => S_x()) ≈ 0 - @test expectation_value(rho_0_mps, (1, 2) => S_x_S_x()) ≈ 0 - @test expectation_value(rho_mps, 1 => S_x()) ≈ E_x_taylor atol = 1.0e-2 - @test expectation_value(rho_mps, (1, 2) => S_x_S_x()) ≈ E_xx_taylor atol = 1.0e-2 - end - - @testset "Infinite-size" begin - H = transverse_field_ising() - trunc = truncrank(20) - verbosity = 1 - beta = 0.1 - - # taylor cluster - alg_taylor = TaylorCluster(; N = 2) - rho_taylor = make_time_mpo(H, beta, alg_taylor; imaginary_evolution) - rho_taylor_2 = make_time_mpo(H, 2beta, alg_taylor; imaginary_evolution) - E_taylor = @constinferred expectation_value(rho_taylor, H) - E_taylor2 = @constinferred expectation_value(rho_taylor_2, H) - - E_z_taylor = @constinferred expectation_value(rho_taylor, 1 => S_z()) - @test E_z_taylor ≈ 0 atol = 1.0e-4 # no spontaneous symmetry breaking at finite T - - # WII - rho_wii = make_time_mpo(H, beta, WII(); imaginary_evolution) - @test expectation_value(rho_wii, H) ≈ E_taylor atol = 1.0e-2 - @test expectation_value(rho_wii, 1 => S_z()) ≈ E_z_taylor atol = 1.0e-2 - - # MPO multiplication - rho_mps = convert(InfiniteMPS, rho_taylor) - rho_mps2, = approximate(rho_mps, (rho_taylor, rho_mps), IDMRG(; verbosity)) - E_mpomul = expectation_value(rho_mps2, H) - @test E_mpomul ≈ E_taylor2 atol = 1.0e-2 - - # TDVP - rho_0 = MPSKit.infinite_temperature_density_matrix(H) - rho_0_mps, = changebonds(convert(InfiniteMPS, rho_0), H, OptimalExpand(; trunc = truncrank(20))) - rho_mps_tdvp, = timestep(rho_0_mps, H, 0.0, beta, TDVP(); imaginary_evolution) - E_tdvp = expectation_value(rho_mps_tdvp, H) - @test E_tdvp ≈ E_taylor atol = 1.0e-2 - - howmany = 2 - vals_taylor = @constinferred(transfer_spectrum(convert(InfiniteMPS, rho_taylor); howmany)) - vals_mps = @constinferred(transfer_spectrum(rho_mps; howmany)) - @test all(c -> vals_taylor[c] ≈ vals_mps[c], keys(vals_taylor)) - end - - @testset "2D infinite partition functions with boundary MPS" verbose = true begin - beta = 0.5 # ferromagnetic phase - f_th = -2.0515856253898357 - m_th = 0.911319377877496 - e_th = -1.7455645753125533 - - alg = VOMPS(; tol = 1.0e-8, verbosity = 1) - O_mpo = classical_ising(; β = beta) - ψ₀ = InfiniteMPS(ℂ^2, ℂ^10) - ψ, envs = leading_boundary(ψ₀, O_mpo, alg) - - λ = expectation_value(ψ, O_mpo, envs) - f = -log(λ) / beta - @test f ≈ f_th atol = 1.0e-10 - - O, M, E = classical_ising_tensors(beta) - - m = expectation_value(ψ, (O_mpo, 1 => M)) # normalised to give density - @test abs(m) ≈ m_th atol = 1.0e-8 # account for spin flip - - e = expectation_value(ψ, (O_mpo, 1 => E)) - @test e ≈ e_th atol = 1.0e-2 - end -end +TestSuite.statmech_finite_temperature(Vector; fast = fast_tests) diff --git a/test/symmetries/multifusion.jl b/test/symmetries/multifusion.jl index 0e2d9b09f..933010b8e 100644 --- a/test/symmetries/multifusion.jl +++ b/test/symmetries/multifusion.jl @@ -4,167 +4,11 @@ println(" ----------------- ") -module TestMultifusion - - using ..TestSetup - using Test, TestExtras - using MPSKit - using TensorKit - - I = IsingBimodule - - M = I(1, 2, 0) # σ - Mop = I(2, 1, 0) - C0 = I(1, 1, 0) # unit of C - C1 = I(1, 1, 1) - D0 = I(2, 2, 0) # unit of D - D1 = I(2, 2, 1) - V = Vect[I](M => 1) - Vop = Vect[I](Mop => 1) - PD = Vect[I](D0 => 1, D1 => 1) - PC = Vect[I](C0 => 1, C1 => 1) - - bad_fusions = [(PC, PD), (PD, PC), (V, V), (Vop, Vop), (V, PC), (Vop, PD), (V, PD), (Vop, PC), (V, Vop), (Vop, V)] - - flippy(charge::IsingBimodule) = only(charge ⊗ D1) - - function TFIM_multifusion(::Type{T} = ComplexF64; g = 1.0, L = Inf, twosite = false) where {T <: Number} - P = Vect[I](D0 => 1, D1 => 1) - X = zeros(T, P ← P) - for (s, f) in fusiontrees(X) - isunit(only(f.uncoupled)) ? X[s, f] .= g : X[s, f] .= -g - end - ZZ = zeros(T, P^2 ← P^2) - for (s, f) in fusiontrees(ZZ) - s.uncoupled == map(flippy, f.uncoupled) ? ZZ[s, f] .= 1 : nothing - end - - if L == Inf - lattice = twosite ? PeriodicArray([P, P]) : PeriodicArray([P]) - H₁ = InfiniteMPOHamiltonian(lattice, i => X for i in 1:length(lattice)) - H₂ = InfiniteMPOHamiltonian(lattice, (i, i + 1) => ZZ for i in 1:length(lattice)) - else - lattice = fill(P, L) - H₁ = FiniteMPOHamiltonian(lattice, i => X for i in 1:L) - H₂ = FiniteMPOHamiltonian(lattice, (i, i + 1) => ZZ for i in 1:(L - 1)) - end - return H₁ + H₂ - end - - @testset "InfiniteMPS construction" begin - for (P, V) in bad_fusions - @test_throws SpaceMismatch InfiniteMPS([P], [V]) - end - end - - @testset "FiniteMPS construction" begin - for (P, V) in bad_fusions - @test_throws ArgumentError FiniteMPS(rand(2:100), P, V) - @test_throws SpaceMismatch FiniteMPS(rand(2:100), P, V; left = V, right = V) - end - end - - @testset "Exact diagonalization" begin - H = TFIM_multifusion(; g = 0, L = 4) - E, ψ = exact_diagonalization(H; sector = D0) # test that it runs with kwarg - @test isapprox(E, [-3, -1, 1, 3]; atol = 1.0e-6) - end - - @testset "Finite systems" begin - L = 6 - g = 4 - H = TFIM_multifusion(; g = g, L = L) - V = Vect[I](M => 8) - init = FiniteMPS(L, PD, V; left = Vect[I](M => 1), right = Vect[I](M => 1)) - v₀ = variance(init, H) - ψ, envs, info = find_groundstate(init, H, DMRG()) - v = variance(ψ, H) - E = expectation_value(ψ, H, envs) - - ψ2, envs2, info2 = find_groundstate(init, H, DMRG2(; trunc = trunctol(; atol = 1.0e-6))) - v2 = variance(ψ2, H) - E2 = expectation_value(ψ2, H, envs2) - - @test convergence_measure(info) ≈ 0 atol = 1.0e-3 - @test convergence_measure(info2) ≈ 0 atol = 1.0e-3 - @test v < v₀ && v2 < v₀ - - @test isapprox(E, E2; atol = 1.0e-6) - - ED, _ = exact_diagonalization(H; sector = D0) - @test isapprox(E, first(ED); atol = 1.0e-6) - - excE, qp = excitations(H, QuasiparticleAnsatz(), ψ2; sector = C1, num = 1) - @test 0 < variance(qp[1], H) < 1.0e-8 - - excE_DM, qp_DM = excitations(H, FiniteExcited(; gsalg = DMRG2(; trunc = trunctol(; atol = 1.0e-6))), ψ2; num = 1) - @test isapprox(first(excE_DM), first(excE) + E2; atol = 1.0e-6) - end - - @testset "Infinite systems" begin - # Multifusion: effectively studying the KW dual in SSB phase - g = 1 / 4 - H = TFIM_multifusion(; g = g, L = Inf, twosite = true) - V = Vect[I](M => 48) - init = InfiniteMPS([PD, PD], [V, V]) - v₀ = variance(init, H) - tol = 1.0e-10 - ψ, envs, info = find_groundstate(init, H, IDMRG(; tol = tol, maxiter = 400)) - E = expectation_value(ψ, H, envs) - v = variance(ψ, H) - - ψ2, envs2, info2 = find_groundstate(init, H, IDMRG2(; tol = tol, trunc = trunctol(; atol = 1.0e-6), maxiter = 400)) - E2 = expectation_value(ψ2, H, envs2) - v2 = variance(ψ2, H) - - ψ3, envs3, info3 = find_groundstate(init, H, VUMPS(; tol = tol, maxiter = 400)) - E3 = expectation_value(ψ3, H, envs3) - v3 = variance(ψ3, H) - - @test isapprox(E, E2; atol = 1.0e-6) - @test isapprox(E, E3; atol = 1.0e-6) - for delta in [convergence_measure(info), convergence_measure(info2), convergence_measure(info3)] - @test delta ≈ 0 atol = 1.0e-3 - end - for var in [v, v2, v3] - @test var < v₀ - @test var < 1.0e-8 - end - - spec = transfer_spectrum(ψ2) - @test first(spec[C0]) ≈ 1 - @test !(abs(first(spec[C1])) ≈ 1) # testing injectivity - - @test only(keys(entanglement_spectrum(ψ2))) == M - - momentum = 0 - excC1, qpC1 = excitations(H, QuasiparticleAnsatz(), momentum, ψ3; sector = C1) - @test isapprox(first(excC1), abs(2 * (g - 1)); atol = 1.0e-6) # charged excitation lower in energy - @test variance(qpC1[1], H) < 1.0e-8 - - # diagonal test (M = D): injective GS in symmetric phase - Hdual = TFIM_multifusion(; g = 1 / g, L = Inf, twosite = true) - Vdiag = Vect[I](D0 => 24, D1 => 24) - initdiag = InfiniteMPS([PD, PD], [Vdiag, Vdiag]) - gsdiag, envsdiag = find_groundstate(initdiag, Hdual, VUMPS(; tol = tol, maxiter = 400)) - Ediag = expectation_value(gsdiag, Hdual, envsdiag) - - excD1, qpD1 = excitations(Hdual, QuasiparticleAnsatz(), momentum, gsdiag; sector = D1) - @test isapprox(first(excD1), abs(2 * (1 / g - 1)); atol = 1.0e-6) # charged excitation lower in energy - @test variance(qpD1[1], Hdual) < 1.0e-8 - - # comparison to Z2 Ising: injective in symmetric phase - HZ2 = repeat(transverse_field_ising(ComplexF64, Z2Irrep; g = 1 / g, L = Inf), 2) - VZ2 = Z2Space(0 => 24, 1 => 24) - PZ2 = Z2Space(0 => 1, 1 => 1) - initZ2 = InfiniteMPS([PZ2, PZ2], [VZ2, VZ2]) - gsZ2, envsZ2 = find_groundstate(initZ2, HZ2, VUMPS(; tol = tol, maxiter = 400)) - EZ2 = expectation_value(gsZ2, HZ2, envsZ2) - @test isapprox(EZ2, Ediag; atol = 1.0e-6) - - excZ2_1, qpZ2_1 = excitations(HZ2, QuasiparticleAnsatz(), momentum, gsZ2; sector = Z2Irrep(1)) - @test isapprox(first(excZ2_1), first(excD1); atol = 1.0e-6) - @test variance(qpZ2_1[1], HZ2) < 1.0e-8 - end - -end # module TestMultifusion +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite + +TestSuite.symmetries_multifusion_infinitemps_construction(Vector; fast = fast_tests) +TestSuite.symmetries_multifusion_finitemps_construction(Vector; fast = fast_tests) +TestSuite.symmetries_multifusion_exact_diagonalization(Vector; fast = fast_tests) +TestSuite.symmetries_multifusion_finite(Vector; fast = fast_tests) +TestSuite.symmetries_multifusion_infinite(Vector; fast = fast_tests) diff --git a/test/symmetries/sector_conventions.jl b/test/symmetries/sector_conventions.jl index c619cbb8d..4c30d1c2d 100644 --- a/test/symmetries/sector_conventions.jl +++ b/test/symmetries/sector_conventions.jl @@ -4,62 +4,7 @@ println(" ----------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ -using LinearAlgebra: eigvals +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "Sector conventions" begin - L = 4 - H = XY_model(ComplexF64, U1Irrep; L) - - H_dense = convert(TensorMap, H) - vals_dense = eigvals(H_dense) - for v in values(vals_dense) - sort!(v; by = real) - end - - tol = 1.0e-18 # tolerance required to separate degenerate eigenvalues - alg = MPSKit.Defaults.alg_eigsolve(; dynamic_tols = false, tol) - - maxVspaces = MPSKit.max_virtualspaces(physicalspace(H)) - gs, = find_groundstate( - FiniteMPS(physicalspace(H), maxVspaces[2:(end - 1)]), H; verbosity = 0 - ) - E₀ = expectation_value(gs, H) - @test E₀ ≈ first(vals_dense[unit(U1Irrep)]) - - for (sector, vals) in pairs(vals_dense) - # ED tests - num = length(vals) - E₀s, ψ₀s, info = exact_diagonalization(H; num, sector, alg) - @test E₀s[1:num] ≈ vals[1:num] - # this is a trick to make the mps full-rank again, which is not guaranteed by ED - ψ₀ = changebonds(first(ψ₀s), SvdCut(; trunc = notrunc())) - Vspaces = left_virtualspace.(Ref(ψ₀), 1:L) - push!(Vspaces, right_virtualspace(ψ₀, L)) - @test all(splat(==), zip(Vspaces, MPSKit.max_virtualspaces(ψ₀))) - - # Quasiparticle tests - Es, Bs = excitations(H, QuasiparticleAnsatz(; tol), gs; sector, num = 1) - Es = Es .+ E₀ - # first excited state is second eigenvalue if sector is trivial - @test Es[1] ≈ vals[isunit(sector) ? 2 : 1] atol = 1.0e-8 - end - - # shifted charges tests - # targeting states with Sz = 1 => vals_shift_dense[0] == vals_dense[1] - # so effectively shifting the charges by -1 - H_shift = MPSKit.add_physical_charge(H, U1Irrep.([1, 0, 0, 0])) - H_shift_dense = convert(TensorMap, H_shift) - vals_shift_dense = eigvals(H_shift_dense) - for v in values(vals_shift_dense) - sort!(v; by = real) - end - for (sector, vals) in pairs(vals_dense) - sector′ = only(sector ⊗ U1Irrep(-1)) - @test vals ≈ vals_shift_dense[sector′] - end -end +TestSuite.symmetries_sector_conventions(Vector; fast = fast_tests) diff --git a/test/testsuite/TestSuite.jl b/test/testsuite/TestSuite.jl new file mode 100644 index 000000000..fe2e26744 --- /dev/null +++ b/test/testsuite/TestSuite.jl @@ -0,0 +1,218 @@ +# Based on the design of GPUArrays.jl + +""" + TestSuite + +Suite of tests that can be run for any array type `AT` (e.g. `Vector`, `CuVector`, ...). + +Every test file is included into its own submodule, so that the parameters a file +defines at top level stay private to that file instead of clobbering the identically +named parameters of its neighbours. Each file exports the test entry points it defines, +which `TestSuite` re-exports in turn, so that callers can use `TestSuite.some_test(AT)`. + +Every entry point has the signature `some_test(AT; fast = false)`, where `fast` selects +a reduced set of cases. +""" +module TestSuite + +# shared helpers live in `TestSetup`, which the test runner loads into `Main` +isdefined(Main, :TestSetup) || + Base.include(Main, joinpath(@__DIR__, "..", "setup", "testsetup.jl")) +using Main: TestSetup + + +# Algorithms +# ---------- +module AlgorithmsAnyonicBraiding + include("algorithms/anyonic_braiding.jl") +end +using .AlgorithmsAnyonicBraiding + +module AlgorithmsChangebonds + include("algorithms/changebonds.jl") +end +using .AlgorithmsChangebonds + +module AlgorithmsCorrelators + include("algorithms/correlators.jl") +end +using .AlgorithmsCorrelators + +module AlgorithmsFidelitySusceptibility + include("algorithms/fidelity_susceptibility.jl") +end +using .AlgorithmsFidelitySusceptibility + +module AlgorithmsPeriodicBoundary + include("algorithms/periodic_boundary.jl") +end +using .AlgorithmsPeriodicBoundary + +module AlgorithmsPropagator + include("algorithms/propagator.jl") +end +using .AlgorithmsPropagator + + +# Excitations +# ----------- +module ExcitationsExcitations + include("excitations/excitations.jl") +end +using .ExcitationsExcitations + + +# GroundState +# ----------- +module GroundStateApproximate + include("groundstate/approximate.jl") +end +using .GroundStateApproximate + +module GroundStateGroundstate + include("groundstate/groundstate.jl") +end +using .GroundStateGroundstate + + +# Hamiltonian +# ----------- +module HamiltonianDerivatives + include("hamiltonian/derivatives.jl") +end +using .HamiltonianDerivatives + +module HamiltonianFinite + include("hamiltonian/finite.jl") +end +using .HamiltonianFinite + +module HamiltonianInfinite + include("hamiltonian/infinite.jl") +end +using .HamiltonianInfinite + +module HamiltonianMixed + include("hamiltonian/mixed.jl") +end +using .HamiltonianMixed + + +# MPO +# --- +module MPODenseMPO + include("mpo/dense_mpo.jl") +end +using .MPODenseMPO + +module MPOFinite + include("mpo/finite.jl") +end +using .MPOFinite + +module MPOInfinite + include("mpo/infinite.jl") +end +using .MPOInfinite + +module MPOMixed + include("mpo/mixed.jl") +end +using .MPOMixed + +module MPOMultiline + include("mpo/multiline.jl") +end +using .MPOMultiline + + +# Operators +# --------- +module OperatorsLazysum + include("operators/lazysum.jl") +end +using .OperatorsLazysum + +module OperatorsProjection + include("operators/projection.jl") +end +using .OperatorsProjection + +module OperatorsWindow + include("operators/window.jl") +end +using .OperatorsWindow + + +# States +# ------ +module StatesArithmetic + include("states/arithmetic.jl") +end +using .StatesArithmetic + +module StatesFinitemps + include("states/finitemps.jl") +end +using .StatesFinitemps + +module StatesInfinitemps + include("states/infinitemps.jl") +end +using .StatesInfinitemps + +module StatesMultilinemps + include("states/multilinemps.jl") +end +using .StatesMultilinemps + +module StatesQuasiparticle + include("states/quasiparticle.jl") +end +using .StatesQuasiparticle + +module StatesWindowmps + include("states/windowmps.jl") +end +using .StatesWindowmps + + +# StatMech +# -------- +module StatMechStatmech + include("statmech/statmech.jl") +end +using .StatMechStatmech + + +# Symmetries +# ---------- +module SymmetriesMultifusion + include("symmetries/multifusion.jl") +end +using .SymmetriesMultifusion + +module SymmetriesSectorConventions + include("symmetries/sector_conventions.jl") +end +using .SymmetriesSectorConventions + + +# TimeEvolution +# ------------- +module TimeEvolutionClusterexpansion + include("timeevolution/clusterexpansion.jl") +end +using .TimeEvolutionClusterexpansion + +module TimeEvolutionTaylorcluster + include("timeevolution/taylorcluster.jl") +end +using .TimeEvolutionTaylorcluster + +module TimeEvolutionTimestep + include("timeevolution/timestep.jl") +end +using .TimeEvolutionTimestep + +end diff --git a/test/testsuite/algorithms/anyonic_braiding.jl b/test/testsuite/algorithms/anyonic_braiding.jl new file mode 100644 index 000000000..7703010b8 --- /dev/null +++ b/test/testsuite/algorithms/anyonic_braiding.jl @@ -0,0 +1,191 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using BlockTensorKit +using MPSKit: AC_hamiltonian, AC2_hamiltonian, AC2, eigsolve +using TensorKit +using LinearAlgebra +using Random +using Adapt + +export algorithms_anyonic_braiding_sandwich, algorithms_anyonic_braiding_expectation_values, + algorithms_anyonic_braiding_derivatives, algorithms_anyonic_braiding_finite, + algorithms_anyonic_braiding_infinite + +# when reinterpreting an MPO as an MPS ρ, there are contractions which +# braid the ancilla leg past the operator's virtual leg +# this turned out to be incorrect in several places +# the tests below check that the braiding is consistent with the dense trace for finite systems, +# and with the MPO-level overlap transfer for infinite systems + +function algorithms_anyonic_braiding_sandwich(AT; fast = false) + return @testset "density-matrix sandwich (Fibonacci, $AT)" begin + V = Vect[FibonacciAnyon](:I => 1, :τ => 1) + ρ = randn(ComplexF64, V ⊗ V, V ⊗ V) + O = randn(ComplexF64, V ⊗ V, V ⊗ V) + ρ_mpo = adapt(AT, FiniteMPO(ρ)) + O_mpo = adapt(AT, FiniteMPO(O)) + ρ_mps = convert(FiniteMPS, ρ_mpo) + + ref = tr(ρ' * O * ρ) + @test dot(ρ_mpo, O_mpo * ρ_mpo) ≈ ref + @test dot(ρ_mps, O_mpo, ρ_mps) ≈ ref + end +end + +function algorithms_anyonic_braiding_expectation_values(AT; fast = false) + return @testset "expectation values with density matrices (Fibonacci, $AT)" begin + V = Vect[FibonacciAnyon](:I => 1, :τ => 1) + L = 3 + h = randn(ComplexF64, V ⊗ V, V ⊗ V) + h = adapt(AT, h + h') + H = adapt(AT, FiniteMPOHamiltonian(fill(V, L), [(i, i + 1) => h for i in 1:(L - 1)])) + + # free workaround: flatten jordan tensors into plain mpo tensors + # don't convert to tensormap, this is an exponential cost in L + H_dense = FiniteMPO([W isa AbstractBlockTensorMap ? TensorMap(W) : W for W in parent(H)]) + ρ = make_time_mpo(H, 0.1, TaylorCluster(; N = 2); imaginary_evolution = true) + ref = dot(ρ, H_dense * ρ) / norm(ρ)^2 + + @test expectation_value(ρ, H) ≈ ref + @test expectation_value(ρ, H_dense) ≈ ref + + # these require the densification in _mpo_to_mps (mpo.jl#90) + Hd = convert(TensorMap, H) + @test expectation_value(ρ, FiniteMPO(Hd)) ≈ ref + @test expectation_value(ρ, (1, 2) => h) + expectation_value(ρ, (2, 3) => h) ≈ ref + end +end + +function algorithms_anyonic_braiding_derivatives(AT; fast = false) + return @testset "braiding consistency in derivative operators (Fibonacci, $AT)" begin + V = Vect[FibonacciAnyon](:I => 1, :τ => 1) + L = 3 + T = ComplexF64 + ρ = adapt(AT, FiniteMPO(randn(T, V^L, V^L))) + O = adapt(AT, FiniteMPO(randn(T, V^L, V^L))) + ρ_mps = convert(FiniteMPS, ρ) + exact = convert(FiniteMPS, O * ρ) + ψ0 = adapt(AT, FiniteMPS(randn, T, fill(V ⊗ V', L), Vect[FibonacciAnyon](:I => 24, :τ => 24))) + fid(ψ) = abs(dot(ψ, exact)) / (norm(ψ) * norm(exact)) + + # unprepared AC reached by one-site approximate + ψ1, = approximate(ψ0, (O, ρ_mps), DMRG(; tol = 1.0e-10, maxiter = 50)) # simply doesn't converge without the fix + @test fid(ψ1) ≈ 1 atol = 1.0e-6 + + # unprepared AC2 reached by two-site approximate + ψ2, = approximate(ψ0, (O, ρ_mps), DMRG2(; tol = 1.0e-10, maxiter = 50, trunc = truncrank(64))) # simply doesn't converge without the fix + @test fid(ψ2) ≈ 1 atol = 1.0e-6 + + # prepared AC, reached by any eigensolver + # τ' fix on mpo_derivatives.jl#252 allows the eigensolver to converge at all, + # even though the eigenproblem is degenerate over the ancilla leg + A = randn(T, V^L, V^L) + Oh = adapt(AT, FiniteMPO(A + A')) + ψ3, _, info3 = find_groundstate(ρ_mps, Oh, DMRG(; tol = 1.0e-10, maxiter = 50)) + @test info3.galerkin ≤ 1.0e-10 + + # prepared AC2, reached by two-site TDVP + Ohd = convert(TensorMap, Oh) + ρd = adapt(AT, randn(T, V^L, V^L)) + ρ_mps2 = convert(FiniteMPS, FiniteMPO(ρd)) + dt = 0.02 + ρ_exact_d = exp(-im * dt * Ohd) * ρd + target = convert(FiniteMPS, FiniteMPO(ρ_exact_d)) + ψt, = timestep(ρ_mps2, Oh, 0.0, dt, TDVP2(; trunc = truncrank(64))) + tdvp2_fid = abs(dot(ψt, target)) / (norm(ψt) * norm(target)) + @test tdvp2_fid ≈ 1 atol = 1.0e-6 # fidelity was previously high, but not ≈ 1 + end +end + +braid_spaces(fast) = if fast + (Vect[FibonacciAnyon](:I => 1, :τ => 1),) +else + ( + ℂ^2, # bosonic + Vect[FermionParity](0 => 1, 1 => 1), # fermionic + Vect[FibonacciAnyon](:I => 1, :τ => 1), # anyonic + ) +end + +# build L-site operator that's the identity everywhere except at site i +# where it's op with physical space V +_embed(L, op, i) = foldl(⊗, (ntuple(_ -> id(space(op, 1)), i - 1)..., op, ntuple(_ -> id(space(op, 1)), L - i - numout(op) + 1)...)) + +function algorithms_anyonic_braiding_finite(AT; fast = false) + return @testset "density-matrix braiding, finite: $(sectortype(V)), $AT" for V in braid_spaces(fast) + L = 3 + Random.seed!(4321) + ρd = adapt(AT, randn(ComplexF64, V^L, V^L)) + Od = adapt(AT, randn(ComplexF64, V^L, V^L)) + ρ, O = FiniteMPO(ρd), FiniteMPO(Od) + ψ = convert(FiniteMPS, ρ) + ref = tr(ρd' * Od * ρd) + nrm = tr(ρd' * ρd) + + # both routes to <ρ|O|ρ> must reproduce the dense trace + @test dot(ρ, O * ρ) ≈ ref + @test dot(ψ, O, ψ) ≈ ref + + # each local derivative operator must reproduce the same scalar + envs = environments(ψ, O, ψ) + for i in 1:L, prepare in (false, true) + h = AC_hamiltonian(i, ψ, O, ψ, envs; prepare) + @test dot(ψ.AC[i], h * ψ.AC[i]) ≈ ref + end + for i in 1:(L - 1) + x = AC2(ψ, i) + h = AC2_hamiltonian(i, ψ, O, ψ, envs; prepare = false) + @test dot(x, h * x) ≈ ref + end + + # The prepared two-site derivative is a separate contraction whose crossings + # are encoded in `braid` levels rather than τ, along with fused legs + for i in 1:(L - 1) + x = AC2(ψ, i) + h = AC2_hamiltonian(i, ψ, O, ψ, envs; prepare = true) + @test dot(x, h * x) ≈ ref + end + + # local observables measured on a density matrix + o1 = adapt(AT, randn(ComplexF64, V, V)) + o1 = o1 + o1' + @test expectation_value(ρ, 2 => o1) ≈ tr(ρd' * _embed(L, o1, 2) * ρd) / nrm + o2 = adapt(AT, randn(ComplexF64, V ⊗ V, V ⊗ V)) + o2 = o2 + o2' + i = rand(1:(L - 1)) + @test expectation_value(ρ, (i, i + 1) => o2) ≈ tr(ρd' * _embed(L, o2, i) * ρd) / nrm + end +end + +# overlap per unit cell of two infinite MPOs as leading eigenvalue of +# TM that contracts both physical legs directly +# contains no braiding tensor -> independent reference for the MPS-MPO-MPS sandwich +function mpo_overlap(mpo1, mpo2) + N = length(mpo1) + function step(v) + for i in 1:N + @plansor w[-1; -2] := v[1; 2] * conj(mpo1[i][1 3; 4 -1]) * mpo2[i][2 3; 4 -2] + v = w + end + return v + end + v0 = similar(mpo1[N], space(mpo1[N], 4)' ← space(mpo2[N], 4)') + randn!(v0) + vals, = eigsolve(step, v0, 1, :LM; krylovdim = 30) + return vals[1] +end + +function algorithms_anyonic_braiding_infinite(AT; fast = false) + return @testset "density-matrix braiding, infinite: $(sectortype(V)), $AT" for V in braid_spaces(fast) + Vv = V isa ComplexSpace ? ℂ^3 : V ⊕ V + for N in (1, 2) + Random.seed!(2024) + ρ = adapt(AT, InfiniteMPO([randn(ComplexF64, Vv ⊗ V ← V ⊗ Vv) for _ in 1:N])) + O = adapt(AT, InfiniteMPO([randn(ComplexF64, V ⊗ V ← V ⊗ V) for _ in 1:N])) + ref = mpo_overlap(ρ, O * ρ) / mpo_overlap(ρ, ρ) + ψ = convert(InfiniteMPS, ρ) + @test dot(ψ, O, ψ) ≈ ref + end + end +end diff --git a/test/testsuite/algorithms/changebonds.jl b/test/testsuite/algorithms/changebonds.jl new file mode 100644 index 000000000..a3e1daa56 --- /dev/null +++ b/test/testsuite/algorithms/changebonds.jl @@ -0,0 +1,333 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Random +using Adapt + +export algorithms_changebonds_mpo, algorithms_changebonds_infinitemps, + algorithms_changebonds_finitemps, algorithms_changebonds_cbe_zero_content, + algorithms_changebonds_density_matrix, algorithms_changebonds_multilinemps, + algorithms_changebonds_svdcut_overflow, algorithms_changebonds_svdcut_longrange, + algorithms_changebonds_svdcut_infinite_hamiltonian, algorithms_changebonds_randexpand_nan, + algorithms_changebonds_nonuniform_unitcell + +spacelist(fast) = if fast + [(ℙ^4, ℙ^3)] +else + [(ℙ^4, ℙ^3), (Rep[SU₂](1 => 1), Rep[SU₂](0 => 2, 1 => 2, 2 => 1))] +end +maxbond(ψ) = maximum(i -> dim(left_virtualspace(ψ, i)), 2:length(ψ)) + +function algorithms_changebonds_mpo(AT; fast = false) + return @testset "MPO $(spacetype(pspace)), $AT" for (pspace, Dspace) in spacelist(fast) + nn = rand(ComplexF64, pspace * pspace, pspace * pspace) + nn += nn' + H0 = adapt(AT, InfiniteMPOHamiltonian(PeriodicVector(fill(pspace, 1)), (1, 2) => nn)) + Δt = 0.1 + expH = make_time_mpo(H0, Δt, WII()) + + O = MPSKit.DenseMPO(expH) + Op = periodic_boundary_conditions(O, 10) + Op′ = changebonds(Op, SvdCut(; trunc = truncrank(5))) + + @test dim(space(Op′[5], 1)) < dim(space(Op[5], 1)) + end +end + +function algorithms_changebonds_infinitemps(AT; fast = false) + return @testset "InfiniteMPS $(spacetype(pspace)), $AT" for (pspace, Dspace) in spacelist(fast) + nn = rand(ComplexF64, pspace * pspace, pspace * pspace) + nn += nn' + H0 = adapt(AT, InfiniteMPOHamiltonian(PeriodicVector(fill(pspace, 1)), (1, 2) => nn)) + + # test rand_expand + for unit_cell_size in 2:3 + H = repeat(H0, unit_cell_size) + state = adapt(AT, InfiniteMPS(fill(pspace, unit_cell_size), fill(Dspace, unit_cell_size))) + + state_re = changebonds( + state, RandExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace))) + ) + @test dot(state, state_re) ≈ 1 atol = 1.0e-8 + end + # test optimal_expand + for unit_cell_size in 2:3 + H = repeat(H0, unit_cell_size) + state = adapt(AT, InfiniteMPS(fill(pspace, unit_cell_size), fill(Dspace, unit_cell_size))) + + state_oe, _ = changebonds( + state, H, OptimalExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace))) + ) + @test dot(state, state_oe) ≈ 1 atol = 1.0e-8 + end + # test VUMPSSvdCut + for unit_cell_size in [1, 2, 3, 4] + H = repeat(H0, unit_cell_size) + state = adapt(AT, InfiniteMPS(fill(pspace, unit_cell_size), fill(Dspace, unit_cell_size))) + + state_vs, _ = changebonds(state, H, VUMPSSvdCut(; trunc = notrunc())) + @test dim(left_virtualspace(state, 1)) < dim(left_virtualspace(state_vs, 1)) + + state_vs_tr = changebonds(state_vs, SvdCut(; trunc = truncrank(dim(Dspace)))) + @test dim(right_virtualspace(state_vs_tr, 1)) < dim(right_virtualspace(state_vs, 1)) + end + end +end + +function algorithms_changebonds_finitemps(AT; fast = false) + return @testset "FiniteMPS $(spacetype(pspace)), $AT" for (pspace, Dspace) in spacelist(fast) + #random nn interaction + L = 10 + nn = rand(ComplexF64, pspace * pspace, pspace * pspace) + nn += nn' + H = adapt(AT, FiniteMPOHamiltonian(fill(pspace, L), (i, i + 1) => nn for i in 1:(L - 1))) + + state = adapt(AT, FiniteMPS(L, pspace, Dspace)) + + state_re = changebonds( + state, RandExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace))) + ) + @test maxbond(state_re) > maxbond(state) + @test dot(state, state_re) ≈ 1 atol = 1.0e-8 + + state_oe, _ = changebonds( + state, H, OptimalExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace))) + ) + @test maxbond(state_oe) > maxbond(state) + @test dot(state, state_oe) ≈ 1 atol = 1.0e-8 + + state_se, _ = changebonds( + state, H, + SketchedExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace)), oversampling = 4) + ) + @test maxbond(state_se) > maxbond(state) + @test dot(state, state_se) ≈ 1 atol = 1.0e-8 + + state_tr = changebonds(state_oe, SvdCut(; trunc = truncrank(dim(Dspace)))) + @test dim(left_virtualspace(state_tr, 5)) < dim(left_virtualspace(state_oe, 5)) + end +end + +# Regression: CBE expanders must not crash on bonds with no expansion content. A charged +# single-particle fermionic (fℤ₂-graded) state carries only one parity sector on the bonds away +# from the particle, so the projected two-site update `adjoint(NL) * AC2 * adjoint(NR)` is +# *structurally exactly zero* there. `OptimalExpand`/`RandExpand` used to `normalize!` that zero +# tensor, producing NaNs that crashed the SVD (`ArgumentError: invalid argument #4 to LAPACK +# call`); they must instead skip such bonds, leaving the state unchanged. +function algorithms_changebonds_cbe_zero_content(AT; fast = false) + return @testset "CBE zero-content bonds (graded, $AT)" begin + L = 6 + H = adapt(AT, kitaev_model(ComplexF64, Trivial; t = 1.0, mu = 0, Delta = 0, L = L)) + P = physicalspace(H)[1] + S = typeof(P) + Vtriv = unitspace(P) + Vodd = S(c => dim(P, c) for c in sectors(P) if !isone(c)) + # single particle created on the last site: interior bonds carry only the even sector, so + # the expansion content on them is exactly zero + state = adapt( + AT, FiniteMPS( + [isometry(ComplexF64, (k <= L ? Vtriv : Vodd) ⊗ P, (k < L ? Vtriv : Vodd)) for k in 1:L] + ) + ) + + state_oe, _ = changebonds(state, H, OptimalExpand(; trunc = truncrank(2))) + @test abs(dot(state, state_oe)) ≈ 1 atol = 1.0e-8 + + state_re = changebonds(state, RandExpand(; trunc = truncrank(2))) + @test abs(dot(state, state_re)) ≈ 1 atol = 1.0e-8 + end +end + +# density-matrix-style MPS: each site carries two physical legs (ket ⊗ bra). The operator-free +# bond-change algorithms (`RandExpand` expansion, `SvdCut` truncation) must handle the extra +# physical leg. Operator-based expanders (`OptimalExpand`/`SketchedExpand`) make use of a +# one-sided MPO application on the first physical leg. +density_matrix_spacelist(fast) = if fast + [(ℙ^2 ⊗ (ℙ^2)', ℙ^6)] +else + [ + (ℙ^2 ⊗ (ℙ^2)', ℙ^6), + (Rep[SU₂](1 // 2 => 1) ⊗ Rep[SU₂](1 // 2 => 1)', Rep[SU₂](0 => 4, 1 => 3)), + ] +end + +function algorithms_changebonds_density_matrix(AT; fast = false) + return @testset "Density-matrix FiniteMPS $(spacetype(pcomp)), $AT" for (pcomp, Dspace) in density_matrix_spacelist(fast) + Random.seed!(2468) + L = 8 + + ψ = adapt(AT, FiniteMPS(rand, ComplexF64, fill(pcomp, L), Dspace)) + @test numind(ψ.AC[L ÷ 2]) == 4 # two physical legs + two virtual legs + + # RandExpand grows the bond while preserving the state (norm-preserving expansion) + ψ_re = changebonds(ψ, RandExpand(; trunc = truncrank(dim(Dspace) * 2))) + @test numind(ψ_re.AC[L ÷ 2]) == 4 + @test abs(dot(ψ, ψ_re)) ≈ 1 atol = 1.0e-8 + @test maxbond(ψ_re) > maxbond(ψ) + + # SvdCut truncates the enlarged bond back down, leaving a normalized state + ψ_tr = changebonds(ψ_re, SvdCut(; trunc = truncrank(dim(Dspace)))) + @test maxbond(ψ_tr) < maxbond(ψ_re) + @test abs(dot(ψ, ψ_tr)) ≈ 1 atol = 1.0e-8 + + # use random time-evolution MPO to test operator-based expanders + pspace = pcomp[1] + nn = rand(ComplexF64, pspace * pspace, pspace * pspace) + nn += nn' + H = adapt(AT, FiniteMPOHamiltonian(fill(pspace, L), (i, i + 1) => nn for i in 1:(L - 1))) + beta = 0.1 + O = make_time_mpo(H, beta, TaylorCluster(; N = 2); imaginary_evolution = true) + + ψ_oe, _ = changebonds( + ψ, O, OptimalExpand(; trunc = truncrank(dim(Dspace) * 2)) + ) + @test maxbond(ψ_oe) > maxbond(ψ) + @test dot(ψ, ψ_oe) ≈ 1 atol = 1.0e-8 + + ψ_se, _ = changebonds( + ψ, O, + SketchedExpand(; trunc = truncrank(dim(Dspace) * 2), oversampling = 4) + ) + @test maxbond(ψ_se) > maxbond(ψ) + @test dot(ψ, ψ_se) ≈ 1 atol = 1.0e-8 + end +end + +function algorithms_changebonds_multilinemps(AT; fast = false) + return @testset "MultilineMPS $(spacetype(pspace)), $AT" for (pspace, Dspace) in spacelist(fast) + o = rand(ComplexF64, pspace * pspace, pspace * pspace) + mpo = adapt(AT, MultilineMPO(o)) + + t = rand(ComplexF64, Dspace * pspace, Dspace) + state = adapt(AT, MultilineMPS(fill(t, 1, 1))) + + state_re = changebonds( + state, RandExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace))) + ) + @test dot(state, state_re) ≈ 1 atol = 1.0e-8 + + state_oe, _ = changebonds( + state, mpo, OptimalExpand(; trunc = truncrank(dim(Dspace) * dim(Dspace))) + ) + @test dot(state, state_oe) ≈ 1 atol = 1.0e-8 + + state_tr = changebonds(state_oe, SvdCut(; trunc = truncrank(dim(Dspace)))) + + @test dim(left_virtualspace(state_tr, 1, 1)) < dim(left_virtualspace(state_oe, 1, 1)) + end +end + +# Regression: the `FiniteMPO` `SvdCut` sweep used to absorb `S * Vᴴ` into the next site without +# renormalizing, so the carried gauge tensor grew by `sqrt(dim(P))` per site. Once its singular +# values passed `sqrt(floatmax)`, `TensorKit`'s block-wise norm overflowed and a relative `trunc` +# discarded *every* singular value, silently returning a zero operator. +function algorithms_changebonds_svdcut_overflow(AT; fast = false) + return @testset "SvdCut scale overflow (long FiniteMPO, $AT)" begin + Random.seed!(1234) + pspace = ℙ^16 + L = 300 # (sqrt(dim(P)))^L must exceed sqrt(floatmax) to trigger the old overflow + h = rand(ComplexF64, pspace, pspace) + h += h' + nn = h ⊗ h # keep the term low-rank so that the dense MPO stays small at this length + H = adapt(AT, FiniteMPOHamiltonian(fill(pspace, L), (i, i + 1) => nn for i in 1:(L - 1))) + + O = MPSKit.DenseMPO(H) + O′ = changebonds(O, SvdCut(; trunc = trunctol(; rtol = 1.0e-12))) + + @test all(i -> dim(left_virtualspace(O′, i)) > 0, 1:L) + @test maxbond(O′) == maxbond(O) + + ψ = adapt(AT, FiniteMPS(rand, ComplexF64, L, pspace, ℙ^4)) + @test expectation_value(ψ, O′) ≈ expectation_value(ψ, O) rtol = 1.0e-12 + end +end + +# Regression: `changebonds(::FiniteMPOHamiltonian, ::SvdCut)` threw a `BoundsError` for any +# Hamiltonian with long-range terms — a star geometry leaves entirely zero rows/columns in the +# Jordan `B`/`C` blocks, and adding such a `SparseBlockTensorMap` into a dense `BlockTensorMap` +# tripped over the structurally absent entries. +function algorithms_changebonds_svdcut_longrange(AT; fast = false) + return @testset "SvdCut on long-range FiniteMPOHamiltonian ($AT)" begin + Random.seed!(4321) + pspace = ℙ^2 + L = 8 + nn = rand(ComplexF64, pspace * pspace, pspace * pspace) + nn += nn' + # every site couples to a single centre site, so all channels are proportional and compressible + centre = L ÷ 2 + H = adapt( + AT, FiniteMPOHamiltonian( + fill(pspace, L), (min(i, centre), max(i, centre)) => nn for i in 1:L if i != centre + ) + ) + + H′ = changebonds(H, SvdCut(; trunc = trunctol(; rtol = 1.0e-12))) + @test maxbond(H′) < maxbond(H) + + ψ = adapt(AT, FiniteMPS(rand, ComplexF64, L, pspace, ℙ^4)) + @test expectation_value(ψ, H′) ≈ expectation_value(ψ, H) rtol = 1.0e-10 + end +end + +function algorithms_changebonds_svdcut_infinite_hamiltonian(AT; fast = false) + return @testset "SvdCut on InfiniteMPOHamiltonian $(spacetype(pspace)), $AT" for (pspace, Dspace) in spacelist(fast) + Random.seed!(2468) + nn = rand(ComplexF64, pspace * pspace, pspace * pspace) + nn += nn' + L = 3 + lattice = PeriodicVector(fill(pspace, L)) + H0 = adapt( + AT, InfiniteMPOHamiltonian(lattice, (i, i + 1) => nn for i in 1:L) + + InfiniteMPOHamiltonian(lattice, (i, i + 2) => 0.5^i * nn for i in 1:L) + ) + # adding the terms twice duplicates every interaction channel + H = H0 + 2 * H0 + ψ = adapt(AT, InfiniteMPS(rand, ComplexF64, fill(pspace, L), fill(Dspace, L))) + + H′ = changebonds(H, SvdCut(; trunc = trunctol(; atol = 1.0e-12))) + @test all(i -> dim(right_virtualspace(H′[i])) ≤ dim(right_virtualspace(H0[i])), 1:L) + @test expectation_value(ψ, H′) ≈ expectation_value(ψ, H) rtol = 1.0e-10 + + # exponentially decaying interactions, written out as explicit terms + Z = rand(ComplexF64, pspace, pspace) + Z += Z' + H_exp = adapt(AT, InfiniteMPOHamiltonian(PeriodicVector([pspace]), (1, 1 + r) => 0.5^r * (Z ⊗ Z) for r in 1:20)) + H_exp′ = changebonds(H_exp, SvdCut(; trunc = trunctol(; rtol = 1.0e-5))) + @test dim(right_virtualspace(H_exp′[1])) < dim(right_virtualspace(H_exp[1])) + ψ1 = adapt(AT, InfiniteMPS(rand, ComplexF64, [pspace], [Dspace])) + @test expectation_value(ψ1, H_exp′) ≈ expectation_value(ψ1, H_exp) rtol = 1.0e-4 + + # purely on-site terms have no channels to compress + H_onsite = adapt(AT, InfiniteMPOHamiltonian(PeriodicVector([pspace]), (1,) => Z)) + @test expectation_value(ψ1, changebonds(H_onsite, SvdCut(; trunc = truncrank(0)))) ≈ + expectation_value(ψ1, H_onsite) + end +end + +# Regression: RandExpand used to be able to introduce a NaN entanglement entropy. +function algorithms_changebonds_randexpand_nan(AT; fast = false) + return @testset "RandExpand does not introduce NaN entropy ($AT)" begin + ψ = adapt(AT, InfiniteMPS([ℂ^2], [ℂ^5])) + ψ = changebonds(ψ, RandExpand(; trunc = truncrank(2))) + @test !isnan(sum(entropy(ψ))) + @test !isnan(sum(entropy(ψ, 2))) + end +end + +# Regression: changebonds on an InfiniteMPS with a >1 unit cell and per-site distinct bond +# dimensions used to error. +function algorithms_changebonds_nonuniform_unitcell(AT; fast = false) + return @testset "changebonds with non-uniform unit cells ($AT)" begin + ψ = adapt(AT, InfiniteMPS([ℂ^2, ℂ^2, ℂ^2], [ℂ^2, ℂ^3, ℂ^4])) + H = adapt(AT, repeat(transverse_field_ising(), 3)) + ψ1, envs = changebonds(ψ, H, OptimalExpand(; trunc = truncrank(2))) + @test ψ1 isa InfiniteMPS + @test norm(ψ1) ≈ 1 + + ψ2 = changebonds(ψ, RandExpand(; trunc = truncrank(2))) + @test ψ2 isa InfiniteMPS + @test norm(ψ2) ≈ 1 + end +end diff --git a/test/testsuite/algorithms/correlators.jl b/test/testsuite/algorithms/correlators.jl new file mode 100644 index 000000000..1b9280f9f --- /dev/null +++ b/test/testsuite/algorithms/correlators.jl @@ -0,0 +1,55 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Adapt + +export algorithms_correlators_entropy, algorithms_correlators_expectation_value + +function algorithms_correlators_entropy(AT; fast = false) + return @testset "correlation length / entropy ($AT)" begin + ψ = adapt(AT, InfiniteMPS([ℙ^2], [ℙ^10])) + H = adapt(AT, force_planar(transverse_field_ising())) + ψ, = find_groundstate(ψ, H, VUMPS(; verbosity = 0)) + len_crit = correlation_length(ψ; sector = leftunit(ψ)) + entrop_crit = entropy(ψ) + + H = adapt(AT, force_planar(transverse_field_ising(; g = 4))) + ψ, = find_groundstate(ψ, H, VUMPS(; verbosity = 0)) + len_gapped = correlation_length(ψ; sector = leftunit(ψ)) + entrop_gapped = entropy(ψ) + + @test len_crit > len_gapped + @test real(entrop_crit) > real(entrop_gapped) + end +end + +function algorithms_correlators_expectation_value(AT; fast = false) + return @testset "expectation value / correlator ($AT)" begin + g = 4.0 + ψ = adapt(AT, InfiniteMPS(ℂ^2, ℂ^10)) + H = adapt(AT, transverse_field_ising(; g)) + ψ, = find_groundstate(ψ, H, VUMPS(; verbosity = 0)) + + X = adapt(AT, S_x()) + ZZ = adapt(AT, S_z_S_z()) + @test expectation_value(ψ, H) ≈ + expectation_value(ψ, 1 => -2g * X) + expectation_value(ψ, (1, 2) => -4ZZ) + Z_mpo = MPSKit.add_util_leg(adapt(AT, S_z())) + G = correlator(ψ, Z_mpo, Z_mpo, 1, 2:5) + G2 = correlator(ψ, ZZ, 1, 3:2:5) + @test isapprox(G[2], G2[1], atol = 1.0e-2) + @test isapprox(last(G), last(G2), atol = 1.0e-2) + @test isapprox(G[1], expectation_value(ψ, (1, 2) => ZZ), atol = 1.0e-2) + @test isapprox(G[2], expectation_value(ψ, (1, 3) => ZZ), atol = 1.0e-2) + + # the sites have to be ordered `i < j`: every `correlator` method funnels its + # arguments through the range version, so all three signatures have to throw + @test_throws ArgumentError correlator(ψ, Z_mpo, Z_mpo, 3, 2) + @test_throws ArgumentError correlator(ψ, Z_mpo, Z_mpo, 3, 1:2) + @test_throws ArgumentError correlator(ψ, ZZ, 3, 2) + # equal sites are not allowed either + @test_throws ArgumentError correlator(ψ, Z_mpo, Z_mpo, 2, 2) + end +end diff --git a/test/testsuite/algorithms/fidelity_susceptibility.jl b/test/testsuite/algorithms/fidelity_susceptibility.jl new file mode 100644 index 000000000..7c9eaa005 --- /dev/null +++ b/test/testsuite/algorithms/fidelity_susceptibility.jl @@ -0,0 +1,43 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Adapt + +export algorithms_fidelity_susceptibility + +function algorithms_fidelity_susceptibility(AT; fast = false) + return @testset "fidelity susceptibility ($AT)" begin + X = TensorMap(ComplexF64[0 1; 1 0], ℂ^2 ← ℂ^2) + Z = TensorMap(ComplexF64[1 0; 0 -1], ℂ^2 ← ℂ^2) + + H_X = adapt(AT, InfiniteMPOHamiltonian(X)) + H_ZZ = adapt(AT, InfiniteMPOHamiltonian(Z ⊗ Z)) + + hamiltonian(λ) = H_ZZ + λ * H_X + analytical_susceptibility(λ) = abs(1 / (16 * λ^2 * (λ^2 - 1))) + + for λ in [1.05, 2.0, 4.0] + H = hamiltonian(λ) + ψ = adapt(AT, InfiniteMPS([ℂ^2], [ℂ^16])) + ψ, envs, = find_groundstate(ψ, H, VUMPS(; maxiter = 100, verbosity = 0)) + + numerical_susceptibility = fidelity_susceptibility(ψ, H, [H_X], envs; maxiter = 10) + @test numerical_susceptibility[1, 1] ≈ analytical_susceptibility(λ) atol = 1.0e-2 + + # test if the finite fid sus approximates the analytical one with increasing system size + fin_en = map([20, 15, 10]) do L + Hfin = open_boundary_conditions(hamiltonian(λ), L) + H_Xfin = open_boundary_conditions(H_X, L) + ψ = adapt(AT, FiniteMPS(rand, ComplexF64, L, ℂ^2, ℂ^16)) + ψ, envs, = find_groundstate(ψ, Hfin, DMRG(; verbosity = 0)) + numerical_susceptibility = fidelity_susceptibility( + ψ, Hfin, [H_Xfin], envs; maxiter = 10 + ) + return numerical_susceptibility[1, 1] / L + end + @test issorted(abs.(fin_en .- analytical_susceptibility(λ))) + end + end +end diff --git a/test/testsuite/algorithms/periodic_boundary.jl b/test/testsuite/algorithms/periodic_boundary.jl new file mode 100644 index 000000000..239fcbb6f --- /dev/null +++ b/test/testsuite/algorithms/periodic_boundary.jl @@ -0,0 +1,45 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Adapt + +export algorithms_periodic_boundary + +function algorithms_periodic_boundary(AT; fast = false) + return @testset "periodic boundary conditions ($AT)" begin + Hs = map( + H -> adapt(AT, H), + [transverse_field_ising(), heisenberg_XXX(), classical_ising(), sixvertex()] + ) + for N in 2:6 + for H in Hs + TH = convert(TensorMap, periodic_boundary_conditions(H, N)) + @test TH ≈ + permute(TH, ((vcat(N, 1:(N - 1))...,), (vcat(2N, (N + 1):(2N - 1))...,))) + end + end + + # non-self-dual virtual space + fast || let V = U1Space(0 => 1, 1 => 1), P = U1Space(0 => 1, 1 => 1) + mpo = adapt(AT, InfiniteMPO([randn(ComplexF64, V ⊗ P ← P ⊗ V)])) + for N in 2:4 + TH = convert(TensorMap, periodic_boundary_conditions(mpo, N)) + @test TH ≈ + permute(TH, ((vcat(N, 1:(N - 1))...,), (vcat(2N, (N + 1):(2N - 1))...,))) + end + end + + # fermionic tests + h = f_hopping(Float64, Trivial) + H = adapt(AT, InfiniteMPOHamiltonian([space(h, 1)], (1, 2) => h)) + for N in 3:5 + H_periodic = periodic_boundary_conditions(H, N) + terms = [(i, i + 1) => h for i in 1:(N - 1)] + push!(terms, (1, N) => permute(h, ((2, 1), (4, 3)))) + H_periodic2 = adapt(AT, FiniteMPOHamiltonian(physicalspace(H_periodic), terms)) + @test H_periodic ≈ H_periodic2 + end + end +end diff --git a/test/testsuite/algorithms/propagator.jl b/test/testsuite/algorithms/propagator.jl new file mode 100644 index 000000000..a0e9f441e --- /dev/null +++ b/test/testsuite/algorithms/propagator.jl @@ -0,0 +1,191 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Adapt +export algorithms_propagator_linear_solve_finitemps, algorithms_propagator_linear_solve_windowmps, + algorithms_propagator_dynamical_dmrg_finitemps, algorithms_propagator_dynamical_dmrg_windowmps + +# KrylovKit is not a direct test dependency; reach the solver types through MPSKit's re-export +const GMRES = MPSKit.KrylovKit.GMRES +const CG = MPSKit.KrylovKit.CG +const BiCGStab = MPSKit.KrylovKit.BiCGStab +const DynamicTol = MPSKit.DynamicTol + +verbosity_conv = 1 + +# helper: relative residual of (a₀ + a₁·A)·x = b, computed densely (robust for small systems; +# MPS-level `+`/`-` does not reliably cancel across states of differing bond structure) +function rel_residual(A, x, b, a₀, a₁) + Am = convert(TensorMap, A) + xv = convert(TensorMap, x) + bv = convert(TensorMap, b) + return norm(a₀ * xv + a₁ * (Am * xv) - bv) / norm(bv) +end + +function algorithms_propagator_linear_solve_finitemps(AT; fast = false) + return @testset "linear_solve FiniteMPS ($AT)" verbose = true begin + L = 8 + H = adapt(AT, force_planar(-transverse_field_ising(; L, g = -4))) + gs, = find_groundstate(adapt(AT, FiniteMPS(L, ℙ^2, ℙ^16)), H; verbosity = verbosity_conv) + E₀ = real(expectation_value(gs, H)) + + # resolvent of an eigenstate: (z − H)⁻¹|gs⟩ = 1/(z − E₀)|gs⟩, so ⟨gs|x⟩ = 1/(z − E₀). + z = E₀ + 0.5 + 0.3im + predicted = 1 / (z - E₀) + + @testset "resolvent, single-site, formulation $flav" for flav in (Galerkin(), LeastSquares()) + solver = flav isa LeastSquares ? CG(; tol = 1.0e-12) : GMRES(; tol = 1.0e-12) + alg = DMRGSolve(; formulation = flav, solver, tol = 1.0e-10, verbosity = 0) + # solve (z − H) x = gs ⟺ a₀ = z, a₁ = −1 + x, envs, info = linear_solve(complex(copy(gs)), H, gs, alg; a₀ = z, a₁ = -1) + @test info.converged && convergence_measure(info) == info.galerkin + @test dot(gs, x) ≈ predicted atol = 1.0e-6 + @test rel_residual(H, x, gs, z, -1) < 1.0e-6 + end + + @testset "resolvent, two-site (adaptive χ), formulation $flav" for flav in + (Galerkin(), LeastSquares()) + solver = flav isa LeastSquares ? CG(; tol = 1.0e-12) : GMRES(; tol = 1.0e-12) + alg = DMRGSolve2(; + formulation = flav, solver, trunc = truncrank(16), tol = 1.0e-10, verbosity = 0 + ) + x, = linear_solve(complex(copy(gs)), H, gs, alg; a₀ = z, a₁ = -1) + @test dot(gs, x) ≈ predicted atol = 1.0e-5 + @test rel_residual(H, x, gs, z, -1) < 1.0e-5 + end + + @testset "matches propagator (correction vector)" begin + alg_ls = DMRGSolve(; solver = GMRES(; tol = 1.0e-12), tol = 1.0e-10, verbosity = 0) + x, = linear_solve(complex(copy(gs)), H, gs, alg_ls; a₀ = z, a₁ = -1) + for f in (NaiveInvert(), Jeckelmann()) + g_prop, = propagator(gs, z, H, DynamicalDMRG(; flavour = f, tol = 1.0e-10, verbosity = 0)) + @test dot(gs, x) ≈ g_prop atol = 1.0e-5 + end + end + + @testset "adaptive tolerances (incl. BiCGStab)" begin + # DynamicTol works for any solver, including short-recurrence BiCGStab (retunes only `tol`) + alg_bicg = DMRGSolve(; + solver = DynamicTol(BiCGStab(; tol = 1.0e-12); tol_min = 1.0e-14, tol_max = 1.0e-4), + tol = 1.0e-9, verbosity = 0 + ) + x, = linear_solve(complex(copy(gs)), H, gs, alg_bicg; a₀ = z, a₁ = -1) + @test dot(gs, x) ≈ predicted atol = 1.0e-5 + @test rel_residual(H, x, gs, z, -1) < 1.0e-5 + + # adaptive-by-default keyword path (DynamicTol-wrapped GMRES) + x2, = linear_solve(complex(copy(gs)), H, gs; a₀ = z, a₁ = -1, tol = 1.0e-9, verbosity = 0) + @test dot(gs, x2) ≈ predicted atol = 1.0e-5 + @test rel_residual(H, x2, gs, z, -1) < 1.0e-5 + end + + @testset "positive-definite solve via keyword interface" begin + # s ≫ spectrum ⇒ (s − H) is positive-definite; the CG path is selected by `isposdef` + s = E₀ + 50.0 + # eigenstate RHS: exact solution 1/(s − E₀) · gs, real and positive + x, = linear_solve(copy(gs), H, gs; a₀ = s, a₁ = -1, isposdef = true, tol = 1.0e-10, verbosity = 0) + @test dot(gs, x) ≈ 1 / (s - E₀) atol = 1.0e-8 + @test rel_residual(H, x, gs, s, -1) < 1.0e-8 + end + + @testset "general RHS, plain A·x = b against dense" begin + # shift H to a well-conditioned, invertible operator A = s·I − H (s ≫ spectrum) + s = E₀ + 50.0 + b = normalize!(adapt(AT, FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^8))) + alg = DMRGSolve2(; + solver = CG(; tol = 1.0e-12), trunc = truncrank(64), tol = 1.0e-9, verbosity = 0 + ) & DMRGSolve(; solver = CG(; tol = 1.0e-12), tol = 1.0e-9, verbosity = 0) + x, = linear_solve(complex(copy(b)), H, b, alg; a₀ = s, a₁ = -1) + @test rel_residual(H, x, b, s, -1) < 1.0e-4 + end + end +end + +function algorithms_propagator_linear_solve_windowmps(AT; fast = false) + return @testset "linear_solve WindowMPS ($AT)" verbose = true begin + # `LeastSquares` builds the `A†b` term from the mixed sandwich ⟨x|A|b⟩, which is the only way to + # reach it for a window operator (there is no `*(::WindowMPOHamiltonian, ::WindowMPS)`) + N = 10 + H = adapt(AT, transverse_field_ising(; g = -4)) + Ω, = find_groundstate(adapt(AT, InfiniteMPS(ℂ^2, ℂ^10)), H, VUMPS(; verbosity = 0)) + XΩ = WindowMPS(Ω, N) + H_w = WindowMPOHamiltonian(H, 1:N) + + E₀ = expectation_value(XΩ, H_w) + z = E₀ + 0.5 + 0.3im + predicted = 1 / (z - E₀) + + @testset "resolvent, formulation $flav" for flav in (Galerkin(), LeastSquares()) + solver = flav isa LeastSquares ? CG(; tol = 1.0e-12) : GMRES(; tol = 1.0e-12) + alg = DMRGSolve(; formulation = flav, solver, tol = 1.0e-10, verbosity = 0) + x, = linear_solve(XΩ, H_w, XΩ, alg; a₀ = z, a₁ = -1) + @test dot(XΩ, x) ≈ predicted atol = 1.0e-6 + end + end +end + +function algorithms_propagator_dynamical_dmrg_finitemps(AT; fast = false) + return @testset "Dynamical DMRG FiniteMPS ($AT)" verbose = true begin + L = 10 + H = adapt(AT, force_planar(-transverse_field_ising(; L, g = -4))) + gs, = find_groundstate(adapt(AT, FiniteMPS(L, ℙ^2, ℙ^10)), H; verbosity = verbosity_conv) + E₀ = expectation_value(gs, H) + + vals = (-0.5:0.2:0.5) .+ E₀ + eta = 0.3im + + predicted = [1 / (v + eta - E₀) for v in vals] + + @testset "Flavour $f" for f in (Jeckelmann(), NaiveInvert()) + alg = DynamicalDMRG(; flavour = f, verbosity = 0, tol = 1.0e-8) + data = map(vals) do v + result, = propagator(gs, v + eta, H, alg) + return result + end + @test data ≈ predicted atol = 1.0e-8 + end + + # `trunc` prepends a two-site sweep, so the bond dimension of the initial guess need not + # already be large enough: start from χ = 2 and let it grow back to the χ = 10 of the solution + @testset "trunc (adaptive χ), flavour $f" for f in (Jeckelmann(), NaiveInvert()) + alg = DynamicalDMRG(; + flavour = f, verbosity = 0, tol = 1.0e-8, trunc = truncrank(16) + ) + init = adapt(AT, FiniteMPS(L, ℙ^2, ℙ^2)) + data = map(vals) do v + result, = propagator(gs, v + eta, H, alg; init) + return result + end + @test data ≈ predicted atol = 1.0e-6 + end + end +end + +function algorithms_propagator_dynamical_dmrg_windowmps(AT; fast = false) + return @testset "Dynamical DMRG WindowMPS ($AT)" verbose = true begin + N = 20 + + H = adapt(AT, transverse_field_ising(g = -4)) + Ω = adapt(AT, InfiniteMPS(ComplexSpace(2), ComplexSpace(20))) + + (Ω, _) = find_groundstate(Ω, H, VUMPS(verbosity = verbosity_conv)) + XΩ = WindowMPS(Ω, N) + H_w = WindowMPOHamiltonian(H, 1:N) + + gs_en = expectation_value(XΩ, H_w) + vals = range(gs_en - 1.0, gs_en + 1.0, length = 5) + eta = 0.3im + predicted = [1 / (v + eta - gs_en) for v in vals] + + @testset "Flavour $f" for f in (Jeckelmann(), NaiveInvert()) + alg = DynamicalDMRG(; flavour = f, verbosity = 0, tol = 1.0e-8) + data = map(vals) do v + result, = propagator(XΩ, v + eta, H_w, alg) + return result + end + @test data ≈ predicted atol = 1.0e-8 + end + end +end diff --git a/test/testsuite/excitations/excitations.jl b/test/testsuite/excitations/excitations.jl new file mode 100644 index 000000000..3a888a934 --- /dev/null +++ b/test/testsuite/excitations/excitations.jl @@ -0,0 +1,120 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Adapt + +export excitations_excitations + +verbosity_conv = 1 + +function excitations_excitations(AT; fast = false) + return @testset "excitations ($AT)" verbose = true begin + @testset "infinite (ham)" begin + H = adapt(AT, repeat(force_planar(heisenberg_XXX()), 2)) + ψ = adapt(AT, InfiniteMPS([ℙ^3, ℙ^3], [ℙ^48, ℙ^48])) + ψ, envs, _ = find_groundstate( + ψ, H; maxiter = 400, verbosity = verbosity_conv, tol = 1.0e-10 + ) + energies, ϕs = @testinferred excitations( + H, QuasiparticleAnsatz(), Float64(pi), ψ, envs + ) + @test energies[1] ≈ 0.41047925 atol = 1.0e-4 + @test variance(ϕs[1], H) < 1.0e-8 + end + fast || @testset "infinite sector convention" begin + g = 4 + H = adapt(AT, repeat(transverse_field_ising(ComplexF64, Z2Irrep; g), 2)) + V = Z2Space(0 => 24, 1 => 24) + ψ = adapt(AT, InfiniteMPS(physicalspace(H), [V, V])) + ψ, envs = find_groundstate(ψ, H, VUMPS(; tol = 1.0e-10, maxiter = 400)) + + # testing istrivial and istopological + momentum = 0 + exc0, qp0 = excitations(H, QuasiparticleAnsatz(), momentum, ψ; sector = Z2Irrep(0)) + exc1, qp1 = excitations(H, QuasiparticleAnsatz(), momentum, ψ; sector = Z2Irrep(1)) + @test isapprox(first(exc1), abs(2 * (g - 1)); atol = 1.0e-6) # charged excitation lower in energy + @test first(exc0) > 2 * first(exc1) + @test variance(qp1[1], H) < 1.0e-8 + end + @testset "infinite (mpo)" begin + H = adapt(AT, repeat(sixvertex(), 2)) + ψ = adapt(AT, InfiniteMPS([ℂ^2, ℂ^2], [ℂ^10, ℂ^10])) + ψ, envs, _ = leading_boundary( + ψ, H, VUMPS(; maxiter = 400, verbosity = verbosity_conv, tol = 1.0e-10) + ) + energies, ϕs = @testinferred excitations( + H, QuasiparticleAnsatz(), [0.0, Float64(pi / 2)], ψ, envs; verbosity = 0 + ) + @test abs(energies[1]) > abs(energies[2]) # has a minimum at pi/2 + end + + @testset "infinite (mpo) - quasiparticle input" begin + # regression test: handing an `InfiniteQP` to an `InfiniteMPO`, rather than a + # momentum, used to build the effective Hamiltonian out of the (still undefined) + # `H_eff` instead of `H`, so every call through this entry point threw an + # `UndefVarError`. The momentum entry point is unaffected, as it converts to a + # `MultilineMPO` first. The classical Ising transfer matrix is used here (rather than + # the six-vertex model above) because its quasiparticle eigenproblem is well enough + # conditioned to compare the two entry points at tight tolerance. + H = adapt(AT, classical_ising(; β = 0.4)) + ψ = adapt(AT, InfiniteMPS([ℂ^2], [ℂ^12])) + ψ, envs, = leading_boundary(ψ, H, VUMPS(; maxiter = 400, verbosity = 0, tol = 1.0e-12)) + + alg = QuasiparticleAnsatz(; tol = 1.0e-10) + for momentum in (0.0, Float64(pi / 2)) + ϕ₀ = MPSKit.LeftGaugedQP(rand, ψ, ψ; momentum) + Es, ϕs = excitations(H, alg, ϕ₀, envs, envs; num = 1) + + # same spectrum as the momentum entry point, which routes through MultilineMPO + Es_momentum, = excitations(H, alg, momentum, ψ, envs; num = 1) + @test Es[1] ≈ Es_momentum[1] atol = 1.0e-8 + + # the operator really is `H`: the result is an eigenpair of the corresponding + # effective excitation Hamiltonian + E = MPSKit.effective_excitation_renormalization_energy(H, ϕs[1], envs, envs) + H_eff = MPSKit.EffectiveExcitationHamiltonian(H, envs, envs, E) + @test norm(H_eff(ϕs[1], alg.alg_environments) - Es[1] * ϕs[1]) < 1.0e-8 + end + end + + @testset "finite" begin + verbosity = verbosity_conv + H_inf = adapt(AT, force_planar(transverse_field_ising())) + ψ_inf = adapt(AT, InfiniteMPS([ℙ^2], [ℙ^10])) + ψ_inf, envs, _ = find_groundstate(ψ_inf, H_inf; maxiter = 400, verbosity, tol = 1.0e-9) + energies, ϕs = @inferred excitations(H_inf, QuasiparticleAnsatz(), 0.0, ψ_inf, envs) + inf_en = energies[1] + + fin_en = map([20, 10]) do len + H = adapt(AT, force_planar(transverse_field_ising(; L = len))) + ψ = adapt(AT, FiniteMPS(rand, ComplexF64, len, ℙ^2, ℙ^10)) + ψ, envs, = find_groundstate(ψ, H; verbosity) + + # find energy with quasiparticle ansatz + energies_QP, ϕs = @testinferred excitations(H, QuasiparticleAnsatz(), ψ, envs) + @test variance(ϕs[1], H) < 1.0e-6 + + # find energy with normal dmrg + for gsalg in ( + DMRG(; verbosity, tol = 1.0e-6), + DMRG2(; verbosity, tol = 1.0e-6, trunc = trunctol(; atol = 1.0e-4)), + ) + energies_dm, _ = @testinferred excitations(H, FiniteExcited(; gsalg), ψ; num = 3) + @test energies_dm[1] ≈ energies_QP[1] + expectation_value(ψ, H, envs) atol = 1.0e-4 + @test issorted(real.(energies_dm)) + end + + # find energy with Chepiga ansatz + energies_ch, _ = @testinferred excitations(H, ChepigaAnsatz(), ψ, envs) + @test energies_ch[1] ≈ energies_QP[1] + expectation_value(ψ, H, envs) atol = 1.0e-4 + energies_ch2, _ = @testinferred excitations(H, ChepigaAnsatz2(), ψ, envs) + @test energies_ch2[1] ≈ energies_QP[1] + expectation_value(ψ, H, envs) atol = 1.0e-4 + return energies_QP[1] + end + + @test issorted(abs.(fin_en .- inf_en)) + end + end +end diff --git a/test/testsuite/groundstate/approximate.jl b/test/testsuite/groundstate/approximate.jl new file mode 100644 index 000000000..c59c57f30 --- /dev/null +++ b/test/testsuite/groundstate/approximate.jl @@ -0,0 +1,291 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Random +using Adapt + +export groundstate_approximate + +verbosity_conv = 1 + +# fixtures for the `Zipup` testsets +function zipup_spacelist(fast) + return if fast + [(ℙ^4, ℙ^3, 4)] + else + [(ℙ^4, ℙ^3, 4), (Rep[SU₂](1 => 1), Rep[SU₂](0 => 2, 1 => 2, 2 => 1), 8)] + end +end + +function _random_mpo_mps(AT, pspace, Dspace, L; elt = ComplexF64) + Random.seed!(1357) + Wspace = Dspace + Vspaces = [unitspace(Wspace); fill(Wspace, L - 1); unitspace(Wspace)] + O = adapt( + AT, FiniteMPO( + [rand(ComplexF64, Vspaces[i] ⊗ pspace ← pspace ⊗ Vspaces[i + 1]) for i in 1:L] + ) + ) + ψ = adapt(AT, FiniteMPS(rand, elt, L, pspace, Dspace)) + return O, ψ +end + +function groundstate_approximate(AT; fast = false) + return @testset "approximate ($AT)" verbose = true begin + zipup_spaces = zipup_spacelist(fast) + verbosity = verbosity_conv + @testset "mpo * infinite ≈ infinite" begin + ψ = adapt(AT, InfiniteMPS([ℙ^2, ℙ^2], [ℙ^10, ℙ^10])) + ψ0 = adapt(AT, InfiniteMPS([ℙ^2, ℙ^2], [ℙ^12, ℙ^12])) + + H = adapt(AT, force_planar(repeat(transverse_field_ising(; g = 4), 2))) + + dt = 1.0e-3 + sW1 = make_time_mpo(H, dt, TaylorCluster(; N = 3, compression = true, extension = true)) + sW2 = make_time_mpo(H, dt, WII()) + W1 = MPSKit.DenseMPO(sW1) + W2 = MPSKit.DenseMPO(sW2) + + ψ1, _ = approximate(ψ0, (sW1, ψ), VOMPS(; verbosity)) + # VOMPS runs the AC and C projections of a site concurrently, so the allocator serving them + # is shared; the serial arm takes the other branch of that choice + ψ2, _ = with_scheduler(MPSKit.SerialScheduler()) do + return approximate(ψ0, (W2, ψ), VOMPS(; verbosity)) + end + + ψ3, _ = approximate(ψ0, (W1, ψ), IDMRG(; verbosity)) + ψ4, _, info4 = approximate(ψ0, (sW2, ψ), IDMRG2(; trunc = truncrank(12), verbosity)) + ψ5, _, info5 = timestep(ψ, H, 0.0, dt, TDVP()) + ψ6 = changebonds(W1 * ψ, SvdCut(; trunc = truncrank(12))) + + @test info4.truncation_errors isa AbstractVector + @test length(info4.truncation_errors) == length(ψ0) + @test !haskey(info5, :truncation_errors) # infinite one-site TDVP never truncates + + @test abs(dot(ψ1, ψ5)) ≈ 1.0 atol = dt + @test abs(dot(ψ3, ψ5)) ≈ 1.0 atol = dt + @test abs(dot(ψ6, ψ5)) ≈ 1.0 atol = dt + @test abs(dot(ψ2, ψ4)) ≈ 1.0 atol = dt + + nW1 = changebonds(W1, SvdCut(; trunc = trunctol(; atol = dt))) # this should be a trivial mpo now + @test dim(space(nW1[1], 1)) == 1 + end + + # every row of a multiline problem has to land in its own row of the result, including at the + # unit cell edge, which used to write the last row's update into the wrong row + @testset "multiline IDMRG2 rows" begin + Random.seed!(42) + dt = 1.0e-2 + Wa = adapt(AT, make_time_mpo(force_planar(repeat(transverse_field_ising(; g = 4), 2)), dt, WII())) + Wb = adapt(AT, make_time_mpo(force_planar(repeat(transverse_field_ising(; g = 0.5), 2)), dt, WII())) + ϕa, ϕb = (adapt(AT, InfiniteMPS([ℙ^2, ℙ^2], [ℙ^8, ℙ^8])) for _ in 1:2) + ψ0a, ψ0b = (adapt(AT, InfiniteMPS([ℙ^2, ℙ^2], [ℙ^10, ℙ^10])) for _ in 1:2) + alg = IDMRG2(; trunc = truncrank(10), verbosity, maxiter = 50) + + multi, = approximate( + MultilineMPS([ψ0a, ψ0b]), (MultilineMPO([Wa, Wb]), MultilineMPS([ϕa, ϕb])), alg + ) + ref_a, = approximate(ψ0a, (Wa, ϕa), alg) + ref_b, = approximate(ψ0b, (Wb, ϕb), alg) + + # row `r + 1` of the result approximates `O[r] * ϕ[r]` + @test abs(dot(multi[2], ref_a)) ≈ 1 atol = 1.0e-8 + @test abs(dot(multi[1], ref_b)) ≈ 1 atol = 1.0e-8 + end + + finite_algs = [DMRG(; verbosity), DMRG2(; verbosity, trunc = truncrank(10))] + @testset "finitemps1 ≈ finitemps2" for alg in finite_algs + a = adapt(AT, FiniteMPS(10, ℂ^2, ℂ^10)) + b = adapt(AT, FiniteMPS(10, ℂ^2, ℂ^20)) + + before = abs(dot(a, b)) + + a = first(approximate(a, b, alg)) + + after = abs(dot(a, b)) + + @test before < after + end + + @testset "sparse_mpo * finitemps1 ≈ finitemps2" for alg in finite_algs + L = 10 + ψ₁ = adapt(AT, FiniteMPS(L, ℂ^2, ℂ^30)) + ψ₂ = adapt(AT, FiniteMPS(L, ℂ^2, ℂ^25)) + + H = adapt(AT, transverse_field_ising(; g = 4.0, L)) + τ = 1.0e-3 + + expH = make_time_mpo(H, τ, WI) + ψ₂, = approximate(ψ₂, (expH, ψ₁), alg) + normalize!(ψ₂) + ψ₂′, = timestep(ψ₁, H, 0.0, τ, TDVP()) + @test abs(dot(ψ₁, ψ₁)) ≈ abs(dot(ψ₂, ψ₂′)) atol = 0.001 + end + + @testset "dense_mpo * finitemps1 ≈ finitemps2" for alg in finite_algs + L = 10 + ψ₁ = adapt(AT, FiniteMPS(L, ℂ^2, ℂ^20)) + ψ₂ = adapt(AT, FiniteMPS(L, ℂ^2, ℂ^10)) + + O = adapt(AT, classical_ising(; L)) + ψ₂, = approximate(ψ₂, (O, ψ₁), alg) + + @test norm(O * ψ₁ - ψ₂) ≈ 0 atol = 0.001 + end + + @testset "Finite MPO-MPS zip-up $(spacetype(pspace))" for (pspace, Dspace, _) in zipup_spaces + L = 6 + + # dense MPO + O, ψ = _random_mpo_mps(AT, pspace, Dspace, L) + O_copy = copy(O) + ψ_copy = copy(ψ) + + trunc = trunctol(; atol = 1.0e-10) + ref = changebonds(O * ψ, SvdCut(; trunc); normalize = false) + + # both sweep directions, with and without the zip-down pass + for left_to_right in (true, false), trunc′ in (trunc, (notrunc(), trunc)) + got, info = approximate((O, ψ), Zipup(; trunc = trunc′, left_to_right)) + @test norm(ref - got) / norm(ref) < 1.0e-10 + @test length(info.truncation_errors) == length(ψ) - 1 + @test maximum(info.truncation_errors) < 1.0e-10 + @test !haskey(info, :converged) && isnothing(convergence_measure(info)) + end + + @test norm(ψ - ψ_copy) < 1.0e-12 + @test all(i -> norm(O[i] - O_copy[i]) < 1.0e-12, 1:length(O)) + + # sparse MPO + nn = rand(ComplexF64, pspace * pspace, pspace * pspace) + nn += nn' + H = adapt(AT, FiniteMPOHamiltonian(fill(pspace, L), (i, i + 1) => nn for i in 1:(L - 1))) + τ = 1.0e-3 + expH = make_time_mpo(H, τ, WI) + + # reference via TDVP + ref_s, = timestep(ψ, H, 0.0, τ, TDVP()) + normalize!(ref_s) + + # both sweep directions, with and without the zip-down pass + for left_to_right in (true, false), trunc′ in (trunc, (notrunc(), trunc)) + got_s, info = approximate((expH, ψ), Zipup(; trunc = trunc′, left_to_right)) + normalize!(got_s) + @test norm(ref_s - got_s) < 0.002 + @test norm(ψ - got_s) > 0.002 + @test maximum(info.truncation_errors) < 1.0e-10 + end + end + + @testset "Paeckel two-stage zip-up $(spacetype(pspace)), left_to_right = $left_to_right" for + (pspace, Dspace, Dcut) in zipup_spaces, left_to_right in (true, false) + O, ψ = _random_mpo_mps(AT, pspace, Dspace, 6) + rtol = 1.0e-8 + final_trunc = truncrank(Dcut) & truncerror(; rtol) + zipup_trunc = truncrank(2Dcut) & truncerror(; rtol = rtol / 10) + + ref_tr = changebonds(O * ψ, SvdCut(; trunc = final_trunc); normalize = false) + got_one_sweep, info_one_sweep = approximate((O, ψ), Zipup(; trunc = final_trunc, left_to_right)) + got_two_sweep, _ = approximate( + (O, ψ), Zipup(; trunc = (zipup_trunc, final_trunc), left_to_right) + ) + @test maximum(info_one_sweep.truncation_errors) > 0 + + err_one_sweep = norm(ref_tr - got_one_sweep) / norm(ref_tr) + err_two_sweep = norm(ref_tr - got_two_sweep) / norm(ref_tr) + @test err_two_sweep < err_one_sweep / 2 + @test maximum(i -> dim(left_virtualspace(got_one_sweep, i)), 2:length(got_one_sweep)) <= Dcut + @test maximum(i -> dim(left_virtualspace(got_two_sweep, i)), 2:length(got_two_sweep)) <= Dcut + end + + @testset "In-place zip-up $(spacetype(pspace)), left_to_right = $left_to_right" for + (pspace, Dspace, Dcut) in zipup_spaces, left_to_right in (true, false) + O, ψ = _random_mpo_mps(AT, pspace, Dspace, 6) + alg = Zipup(; trunc = (truncrank(2Dcut), truncrank(Dcut)), left_to_right) + ref, info_ref = approximate((O, ψ), alg) + + # empty destination + dst = similar(ψ, ComplexF64) + got, info = approximate!(dst, (O, ψ), alg) + @test got === dst + @test norm(ref - got) / norm(ref) < 1.0e-12 + @test info.truncation_errors ≈ info_ref.truncation_errors + + # a destination with unrelated contents is overwritten entirely + dst = adapt(AT, FiniteMPS(rand, ComplexF64, length(ψ), pspace, unitspace(Dspace) ⊕ Dspace ⊕ Dspace)) + got, info = approximate!(dst, (O, ψ), alg) + @test norm(ref - got) / norm(ref) < 1.0e-12 + @test info.truncation_errors ≈ info_ref.truncation_errors + + # the input may serve as its own destination + got, info = approximate!(ψ, (O, ψ), alg) + @test got === ψ + @test norm(ref - got) / norm(ref) < 1.0e-12 + @test info.truncation_errors ≈ info_ref.truncation_errors + end + + @testset "Zip-up with non-trivial boundary spaces $(spacetype(pspace))" for (pspace, Dspace, _) in zipup_spaces + Random.seed!(1357) + L = 4 + Vspaces = fill(Dspace, L + 1) + O = adapt( + AT, FiniteMPO( + [rand(ComplexF64, Vspaces[i] ⊗ pspace ← pspace ⊗ Vspaces[i + 1]) for i in 1:L] + ) + ) + ψ = adapt(AT, FiniteMPS(rand, ComplexF64, L, pspace, Dspace; left = Dspace, right = Dspace)) + + trunc = trunctol(; atol = 1.0e-12) + got, _ = approximate((O, ψ), Zipup(; trunc)) + got_two_sweep, _ = approximate((O, ψ), Zipup(; trunc = (notrunc(), trunc))) + + # the boundary virtual spaces of the product are the fused ones, in both variants + for ψ′ in (got, got_two_sweep) + @test left_virtualspace(ψ′, 1) == fuse(left_virtualspace(ψ, 1) ⊗ left_virtualspace(O, 1)) + @test right_virtualspace(ψ′, L) == fuse(right_virtualspace(ψ, L) ⊗ right_virtualspace(O, L)) + end + @test norm(got - got_two_sweep) / norm(got) < 1.0e-10 + @test norm(got) ≈ norm(O * ψ) + end + + @testset "Zip-up with non-square MPO $(spacetype(pspace))" for (pspace, Dspace, _) in zipup_spaces + Random.seed!(1357) + L = 4 + pspace′ = pspace ⊕ unitspace(pspace) # output physical space, different from the input one + Vspaces = [unitspace(Dspace); fill(Dspace, L - 1); unitspace(Dspace)] + O = adapt( + AT, FiniteMPO( + [rand(ComplexF64, Vspaces[i] ⊗ pspace′ ← pspace ⊗ Vspaces[i + 1]) for i in 1:L] + ) + ) + ψ = adapt(AT, FiniteMPS(rand, ComplexF64, L, pspace, Dspace)) + + trunc = trunctol(; atol = 1.0e-12) + ref = O * ψ + got, _ = approximate((O, ψ), Zipup(; trunc)) + got_two_sweep, _ = approximate((O, ψ), Zipup(; trunc = (notrunc(), trunc))) + + @test all(i -> physicalspace(got, i) == pspace′, 1:L) + @test norm(ref - got) / norm(ref) < 1.0e-10 + @test norm(ref - got_two_sweep) / norm(ref) < 1.0e-10 + + # the reverse product is not defined + @test_throws SpaceMismatch approximate((O, got), Zipup(; trunc)) + end + + @testset "Zip-up scalar type promotion $(spacetype(pspace))" for (pspace, Dspace, _) in zipup_spaces + O, ψ = _random_mpo_mps(AT, pspace, Dspace, 6; elt = Float64) + alg = Zipup(; trunc = trunctol(; atol = 1.0e-10)) + + got, _ = approximate((O, ψ), alg) + @test scalartype(got) === ComplexF64 + ref = changebonds(O * ψ, SvdCut(; trunc = trunctol(; atol = 1.0e-10)); normalize = false) + @test norm(ref - got) / norm(ref) < 1.0e-10 + + # a real destination cannot hold the complex result + @test_throws ArgumentError approximate!(ψ, (O, ψ), alg) + end + end +end diff --git a/test/testsuite/groundstate/groundstate.jl b/test/testsuite/groundstate/groundstate.jl new file mode 100644 index 000000000..fd2579f9b --- /dev/null +++ b/test/testsuite/groundstate/groundstate.jl @@ -0,0 +1,574 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Random +using Adapt + +export groundstate_finite, groundstate_grassmann_preconditioner, groundstate_infinite +export groundstate_lazysum_finite, groundstate_lazysum_infinite, groundstate_leading_boundary + +verbosity_full = 5 +verbosity_conv = 1 + +function groundstate_finite(AT; fast = false) + return @testset "FiniteMPS ground state ($AT)" verbose = true begin + tol = 1.0e-8 + g = 4.0 + D = 6 + L = 10 + + models = [ + "nearest-neighbour" => adapt(AT, force_planar(transverse_field_ising(; g, L))), + "long-range, real scalartype" => adapt(AT, force_planar(long_range_ising(Float64; g, L))), + ] + + @testset "$name" for (name, H) in models + @testset "DMRG" begin + ψ₀ = adapt(AT, FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^D)) + v₀ = variance(ψ₀, H) + + # test logging + ψ, envs, info = find_groundstate( + ψ₀, H, DMRG(; verbosity = verbosity_full, maxiter = 2) + ) + + ψ, envs, info = find_groundstate( + ψ, H, DMRG(; verbosity = verbosity_conv, maxiter = 10), envs + ) + v = variance(ψ, H) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test v < v₀ + @test v < 1.0e-2 + @test info.truncation_errors == zeros(L - 1) + end + + @testset "DMRG2" begin + ψ₀ = adapt(AT, FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^D)) + v₀ = variance(ψ₀, H) + trunc = truncrank(floor(Int, D * 1.5)) + # test logging + ψ, envs, info = find_groundstate( + ψ₀, H, DMRG2(; verbosity = verbosity_full, maxiter = 2, trunc) + ) + + ψ, envs, info = find_groundstate( + ψ, H, DMRG2(; verbosity = verbosity_conv, maxiter = 10, trunc), envs + ) + v = variance(ψ, H) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test v < v₀ + @test v < 1.0e-2 + + @test length(info.truncation_errors) == L - 1 + @test maximum(info.truncation_errors) > 0 + end + + @testset "CBEDMRG" begin + # start from a small bond so the bond expansion is exercised + ψ₀ = adapt(AT, FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^(D ÷ 2))) + v₀ = variance(ψ₀, H) + expand = OptimalExpand(; trunc = truncrank(D ÷ 2)) + trunc = truncrank(D) + + # test logging + ψ, envs, info = find_groundstate( + ψ₀, H, DMRG(; verbosity = verbosity_full, maxiter = 2, alg_expand = expand, trunc) + ) + + ψ, envs, info = find_groundstate( + ψ, H, DMRG(; verbosity = verbosity_conv, maxiter = 10, alg_expand = expand, trunc), envs + ) + v = variance(ψ, H) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test v < v₀ + @test v < 1.0e-2 + # the bond should have grown to the truncation target + @test dim(left_virtualspace(ψ, L ÷ 2)) == D + end + + @testset "CBEDMRG (SketchedExpand)" begin + # randomized bond expansion at single-site cost. The sketch is redrawn every sweep, so an + # aggressive expansion (a large fraction of the bond) keeps the single-site Galerkin error + # noisy; a gentle per-sweep increment lets it converge like the deterministic expanders. + Random.seed!(1234) + ψ₀ = adapt(AT, FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^(D ÷ 2))) + v₀ = variance(ψ₀, H) + expand = SketchedExpand(; trunc = truncrank(2), oversampling = 4) + trunc = truncrank(D) + + # test logging + ψ, envs, info = find_groundstate( + ψ₀, H, DMRG(; verbosity = verbosity_full, maxiter = 2, alg_expand = expand, trunc) + ) + + ψ, envs, info = find_groundstate( + ψ, H, DMRG(; verbosity = verbosity_conv, maxiter = 15, alg_expand = expand, trunc), envs + ) + v = variance(ψ, H) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test v < v₀ + @test v < 1.0e-2 + # the bond should have grown to the truncation target + @test dim(left_virtualspace(ψ, L ÷ 2)) == D + end + + @testset "DMRG3S" begin + # start from a small bond so the post-expansion is exercised, mirroring CBEDMRG above + Random.seed!(1234) + ψ₀ = adapt(AT, FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^(D ÷ 2))) + v₀ = variance(ψ₀, H) + alg_gauge = DMRG3S(0.1, ExponentialDecay(0.7)) # TODO: match final constructor API + trunc = truncrank(D) + + # test logging + ψ, envs, info = find_groundstate( + ψ₀, H, DMRG(; verbosity = verbosity_full, maxiter = 2, alg_gauge, trunc) + ) + + ψ, envs, info = find_groundstate( + ψ, H, DMRG(; verbosity = verbosity_conv, maxiter = 10, alg_gauge, trunc), envs + ) + v = variance(ψ, H) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test v < v₀ + @test v < 1.0e-2 + # the bond should have grown to the truncation target + @test dim(left_virtualspace(ψ, L ÷ 2)) == D + end + + @testset "GradientGrassmann" begin + ψ₀ = adapt(AT, FiniteMPS(randn, ComplexF64, L, ℙ^2, ℙ^D)) + v₀ = variance(ψ₀, H) + + # test logging + ψ, envs, info = find_groundstate( + ψ₀, H, GradientGrassmann(; verbosity = verbosity_full, maxiter = 2) + ) + + ψ, envs, info = find_groundstate( + ψ, H, GradientGrassmann(; tol, verbosity = verbosity_conv, maxiter = 50), envs + ) + v = variance(ψ, H) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test v < v₀ && v < 1.0e-2 + end + end + + fast || @testset "DMRG3S escapes local minimum (Hubig et al. 2015, Sec. VII A)" begin + L_heis = 20 + H_heis = adapt(AT, heisenberg_XXX(ComplexF64, U1Irrep; spin = 1 // 2, L = L_heis)) + + Random.seed!(1234) + ψ_bad = adapt(AT, bad_initial_state(H_heis, L_heis)) + + ψ_stuck, envs_stuck, info_stuck = find_groundstate( + ψ_bad, H_heis, DMRG(; verbosity = verbosity_conv, maxiter = 30) + ) + E_stuck = real(expectation_value(ψ_stuck, H_heis, envs_stuck)) + + alg_gauge = DMRG3S(0.1, ExponentialDecay(0.8)) + ψ_escape, envs_escape, info_escape = find_groundstate( + ψ_bad, H_heis, DMRG(; + verbosity = verbosity_conv, maxiter = 30, + alg_gauge, trunc = truncrank(20), + ) + ) + E_escape = real(expectation_value(ψ_escape, H_heis, envs_escape)) + + # paper reports E(α=0) = -6.35479, E(α≠0) = -8.6824724 for this L_heis = 20, S = 1/2 AFM setup + @test E_escape < E_stuck - 1.0 + @test isapprox(E_escape, -8.6824724; atol = 1.0e-4) + end + end +end + +function groundstate_grassmann_preconditioner(AT; fast = false) + return @testset "GradientGrassmann preconditioner is positive definite ($AT)" begin + # nearly rank-deficient bonds, as for the long-range model above, make the regularized + # inverse of `ρ = C * C'` ill-conditioned beyond `1 / eps` once the gradient is small + Grassmann = MPSKit.GrassmannMPS.Grassmann + Random.seed!(123) + V, P = ℙ^6, ℙ^2 + S = adapt(AT, DiagonalTensorMap([1.0, 0.06, 2.0e-10, 1.5e-10, 5.0e-11, 1.0e-11], V)) + @testset "$T" for T in (Float64, ComplexF64) + isdescent = map(1:50) do _ + AL = adapt(AT, randisometry(T, V ⊗ P, V)) + C = adapt(AT, randisometry(T, V, V)) * S * adapt(AT, randisometry(T, V, V)) + Δ = MPSKit.GrassmannMPS.rmul(Grassmann.project(adapt(AT, randn(T, V ⊗ P, V)), AL), C') + Δ = MPSKit.GrassmannMPS.scale!(Δ, 1.0e-9 / norm(Δ.Z)) + PΔ = MPSKit.GrassmannMPS.rmul_rho_inv_regularized(Δ, C; rtol = norm(Δ.Z)) + return real(Grassmann.inner(AL, Δ, PΔ)) > 0 + end + @test all(isdescent) + end + end +end + +function groundstate_infinite(AT; fast = false) + return @testset "InfiniteMPS ground state ($AT)" verbose = true begin + tol = 1.0e-8 + g = 4.0 + D = 6 + + H_ref = adapt(AT, force_planar(transverse_field_ising(; g))) + ψ = adapt(AT, InfiniteMPS(ℙ^2, ℙ^D)) + v₀ = variance(ψ, H_ref) + + H_ref_realT = adapt(AT, force_planar(transverse_field_ising(Float64; g))) + @test variance(ψ, H_ref_realT) ≈ v₀ atol = 1.0e-10 + + + @testset "VUMPS (unit cell $unit_cell_size, $schedname)" for unit_cell_size in [1, 3], + (schedname, scheduler) in SCHEDULERS + + ψ₀ = repeat(adapt(AT, InfiniteMPS(ℙ^2, ℙ^D)), unit_cell_size) + H = repeat(H_ref, unit_cell_size) + + ψ′, envs, info = with_scheduler(scheduler) do + # test logging + ψ₁, = find_groundstate( + ψ₀, H, VUMPS(; tol, verbosity = verbosity_full, maxiter = 2) + ) + return find_groundstate(ψ₁, H, VUMPS(; tol, verbosity = verbosity_conv)) + end + v = variance(ψ′, H, envs) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test v < v₀ + @test v < 1.0e-2 + end + + @testset "VUMPS (long-range, real scalartype)" begin + H = adapt(AT, force_planar(long_range_ising_infinite(Float64; g, L = 3))) + ψ₀ = adapt(AT, InfiniteMPS(fill(ℙ^2, 3), fill(ℙ^D, 3))) + v₀ = variance(ψ₀, H) + + ψ′, envs, info = find_groundstate(ψ₀, H, VUMPS(; tol, verbosity = verbosity_conv, maxiter = 20)) + v = variance(ψ′, H, envs) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test v < v₀ + @test v < 1.0e-2 + end + + @testset "IDMRG" for unit_cell_size in [1, 3] + ψ = unit_cell_size == 1 ? adapt(AT, InfiniteMPS(ℙ^2, ℙ^D)) : repeat(ψ, unit_cell_size) + H = repeat(H_ref, unit_cell_size) + + # test logging + ψ, envs, info = find_groundstate( + ψ, H, IDMRG(; tol, verbosity = verbosity_full, maxiter = 2) + ) + + ψ, envs, info = find_groundstate(ψ, H, IDMRG(; tol, verbosity = verbosity_conv)) + v = variance(ψ, H, envs) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test !haskey(info, :truncation_errors) # single-site IDMRG never truncates + @test v < v₀ + @test v < 1.0e-2 + end + + @testset "IDMRG (long-range, real scalartype)" begin + H = adapt(AT, force_planar(long_range_ising_infinite(Float64; g, L = 3))) + ψ₀ = adapt(AT, InfiniteMPS(fill(ℙ^2, 3), fill(ℙ^D, 3))) + v₀ = variance(ψ₀, H) + + ψ, envs, info = find_groundstate(ψ₀, H, IDMRG(; tol, verbosity = verbosity_conv, maxiter = 20)) + v = variance(ψ, H, envs) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test v < v₀ + @test v < 1.0e-2 + end + + @testset "IDMRG2" begin + ψ = repeat(adapt(AT, InfiniteMPS(ℙ^2, ℙ^D)), 2) + H = repeat(H_ref, 2) + + trunc = trunctol(; atol = 1.0e-8) + + # test logging + ψ, envs, info = find_groundstate( + ψ, H, IDMRG2(; tol, verbosity = verbosity_full, maxiter = 2, trunc) + ) + + ψ, envs, info = find_groundstate( + ψ, H, IDMRG2(; tol, verbosity = verbosity_conv, trunc) + ) + v = variance(ψ, H, envs) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test v < v₀ + @test v < 1.0e-2 + + @test length(info.truncation_errors) == length(ψ) + @test maximum(info.truncation_errors) > 0 + end + + # Regression: IDMRG2 used to error on a non-abelian (SU2) unit cell due to a space mismatch. + @testset "IDMRG2 space mismatch (SU2)" begin + N = 6 + H = adapt(AT, repeat(bilinear_biquadratic_model(ComplexF64, SU2Irrep; θ = atan(1 / 3)), N)) + ψ₀ = adapt( + AT, InfiniteMPS( + fill(SU2Space(1 => 1), N), + fill(SU2Space(1 // 2 => 2, 3 // 2 => 1), N) + ) + ) + alg = IDMRG2(; verbosity = 0, tol = 1.0e-5, trunc = truncrank(32)) + + ψ, envs, δ = find_groundstate(ψ₀, H, alg) # used to error + @test ψ isa InfiniteMPS + end + + # the gradient is computed concurrently over the unit cell, so the scheduler decides its + # allocator here too + @testset "GradientGrassmann (unit cell $unit_cell_size, $schedname)" for unit_cell_size in + [1, 3], (schedname, scheduler) in SCHEDULERS + + ψ₀ = repeat(adapt(AT, InfiniteMPS(ℙ^2, ℙ^D)), unit_cell_size) + H = repeat(H_ref, unit_cell_size) + + ψ′, envs, info = with_scheduler(scheduler) do + # test logging + ψ₁, = find_groundstate( + ψ₀, H, GradientGrassmann(; tol, verbosity = verbosity_full, maxiter = 2) + ) + return find_groundstate( + ψ₁, H, GradientGrassmann(; tol, verbosity = verbosity_conv) + ) + end + v = variance(ψ′, H, envs) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test v < v₀ + @test v < 1.0e-2 + end + + @testset "Combination" for unit_cell_size in [1, 3] + ψ = unit_cell_size == 1 ? adapt(AT, InfiniteMPS(ℙ^2, ℙ^D)) : repeat(ψ, unit_cell_size) + H = repeat(H_ref, unit_cell_size) + + alg = VUMPS(; tol = 100 * tol, verbosity = verbosity_conv, maxiter = 10) & + GradientGrassmann(; tol, verbosity = verbosity_conv, maxiter = 50) + ψ, envs, info = find_groundstate(ψ, H, alg) + + v = variance(ψ, H, envs) + + # test using low variance + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test v < v₀ + @test v < 1.0e-2 + end + end +end + +function groundstate_lazysum_finite(AT; fast = false) + return @testset "LazySum FiniteMPS ground state ($AT)" verbose = true begin + tol = 1.0e-8 + D = 15 + atol = 1.0e-2 + L = 10 + + # test using XXZ model, Δ > 1 is gapped + spin = 1 + local_operators = [S_x_S_x(; spin), S_y_S_y(; spin), 1.7 * S_z_S_z(; spin)] + Pspace = space(local_operators[1], 1) + lattice = fill(Pspace, L) + + mpo_hamiltonians = map(local_operators) do O + return adapt(AT, FiniteMPOHamiltonian(lattice, (i, i + 1) => O for i in 1:(L - 1))) + end + + H_lazy = LazySum(mpo_hamiltonians) + H = sum(H_lazy) + + ψ₀ = adapt(AT, FiniteMPS(randn, ComplexF64, 10, ℂ^3, ℂ^D)) + ψ₀, = find_groundstate(ψ₀, H; tol, verbosity = 1) + + @testset "DMRG" begin + # test logging passes + ψ, envs, info = find_groundstate( + ψ₀, H_lazy, DMRG(; tol, verbosity = verbosity_full, maxiter = 1) + ) + + # compare states + alg = DMRG(; tol, verbosity = verbosity_conv) + ψ, envs, info = find_groundstate(ψ, H_lazy, alg) + + @test abs(dot(ψ₀, ψ)) ≈ 1 atol = atol + end + + @testset "DMRG2" begin + # test logging passes + trunc = truncrank(floor(Int, D * 1.5)) + ψ, envs, info = find_groundstate( + ψ₀, H_lazy, DMRG2(; tol, verbosity = verbosity_full, maxiter = 1, trunc) + ) + + # compare states + alg = DMRG2(; tol, verbosity = verbosity_conv, trunc) + ψ, = find_groundstate(ψ₀, H, alg) + ψ_lazy, envs, info = find_groundstate(ψ₀, H_lazy, alg) + + @test abs(dot(ψ₀, ψ_lazy)) ≈ 1 atol = atol + end + + @testset "GradientGrassmann" begin + # test logging passes + ψ, envs, info = find_groundstate( + ψ₀, H_lazy, GradientGrassmann(; tol, verbosity = verbosity_full, maxiter = 2) + ) + + # compare states + alg = GradientGrassmann(; tol, verbosity = verbosity_conv) + ψ, = find_groundstate(ψ₀, H, alg) + ψ_lazy, envs, info = find_groundstate(ψ₀, H_lazy, alg) + + @test abs(dot(ψ₀, ψ_lazy)) ≈ 1 atol = atol + end + end +end + +function groundstate_lazysum_infinite(AT; fast = false) + return @testset "LazySum InfiniteMPS ground state ($AT)" verbose = true begin + tol = 1.0e-8 + D = 16 + atol = 1.0e-2 + + spin = 1 + local_operators = [S_x_S_x(; spin), S_y_S_y(; spin), 0.7 * S_z_S_z(; spin)] + Pspace = space(local_operators[1], 1) + lattice = PeriodicVector([Pspace]) + mpo_hamiltonians = map(local_operators) do O + return adapt(AT, InfiniteMPOHamiltonian(lattice, (1, 2) => O)) + end + + H_lazy = LazySum(mpo_hamiltonians) + H = sum(H_lazy) + + ψ₀ = adapt(AT, InfiniteMPS(ℂ^3, ℂ^D)) + ψ₀, = find_groundstate(ψ₀, H; tol, verbosity = 1) + + @testset "VUMPS" begin + # test logging passes + ψ, envs, info = find_groundstate( + ψ₀, H_lazy, VUMPS(; tol, verbosity = verbosity_full, maxiter = 2) + ) + + # compare states + alg = VUMPS(; tol, verbosity = verbosity_conv) + ψ, envs, info = find_groundstate(ψ, H_lazy, alg) + + @test abs(dot(ψ₀, ψ)) ≈ 1 atol = atol + end + + @testset "IDMRG" begin + # test logging passes + ψ, envs, info = find_groundstate( + ψ₀, H_lazy, IDMRG(; tol, verbosity = verbosity_full, maxiter = 2) + ) + + # compare states + alg = IDMRG(; tol, verbosity = verbosity_conv, maxiter = 300) + ψ, envs, info = find_groundstate(ψ, H_lazy, alg) + + @test abs(dot(ψ₀, ψ)) ≈ 1 atol = atol + end + + @testset "IDMRG2" begin + ψ₀′ = repeat(ψ₀, 2) + H_lazy′ = repeat(H_lazy, 2) + H′ = repeat(H, 2) + + trunc = truncrank(floor(Int, D * 1.5)) + # test logging passes + ψ, envs, info = find_groundstate( + ψ₀′, H_lazy′, IDMRG2(; tol, verbosity = verbosity_full, maxiter = 2, trunc) + ) + + # compare states + alg = IDMRG2(; tol, verbosity = verbosity_conv, trunc) + ψ, envs, info = find_groundstate(ψ, H_lazy′, alg) + + @test abs(dot(ψ₀′, ψ)) ≈ 1 atol = atol + end + + @testset "GradientGrassmann" begin + # test logging passes + ψ, envs, info = find_groundstate( + ψ₀, H_lazy, GradientGrassmann(; tol, verbosity = verbosity_full, maxiter = 2) + ) + + # compare states + alg = GradientGrassmann(; tol, verbosity = verbosity_conv) + ψ, envs, info = find_groundstate(ψ₀, H_lazy, alg) + + @test abs(dot(ψ₀, ψ)) ≈ 1 atol = atol + end + end +end + +function groundstate_leading_boundary(AT; fast = false) + return @testset "leading_boundary ($AT)" verbose = true begin + tol = 1.0e-4 + verbosity = verbosity_conv + D = 10 + D1 = 13 + algs = [ + VUMPS(; tol, verbosity), VOMPS(; tol, verbosity), + GradientGrassmann(; tol, verbosity), IDMRG(; tol, verbosity), + IDMRG2(; tol, verbosity, trunc = truncrank(D1)), + ] + mpo = adapt(AT, force_planar(classical_ising())) + + ψ₀ = adapt(AT, InfiniteMPS([ℙ^2], [ℙ^D])) + @testset "Infinite $i" for (i, alg) in enumerate(algs) + if alg isa IDMRG2 + ψ2 = repeat(ψ₀, 2) + mpo2 = repeat(mpo, 2) + ψ, envs = leading_boundary(ψ2, mpo2, alg) + @test dim(space(ψ.AL[1, 1], 1)) == dim(space(ψ₀.AL[1, 1], 1)) + (D1 - D) + @test expectation_value(ψ, mpo2, envs) ≈ 2.5337^2 atol = 1.0e-3 + else + ψ, envs = leading_boundary(ψ₀, mpo, alg) + ψ, envs = changebonds(ψ, mpo, OptimalExpand(; trunc = truncrank(D1 - D)), envs) + ψ, envs = leading_boundary(ψ, mpo, alg) + @test dim(space(ψ.AL[1, 1], 1)) == dim(space(ψ₀.AL[1, 1], 1)) + (D1 - D) + @test expectation_value(ψ, mpo, envs) ≈ 2.5337 atol = 1.0e-3 + end + end + + fast || @testset "IDMRG2 growing bond dimension" begin + Random.seed!(1234) + V = Vect[Z2Irrep](0 => 1, 1 => 1) + O = adapt(AT, randn(ComplexF64, V ⊗ V, V ⊗ V)) + mpo = InfiniteMPO([O, O]) + P = physicalspace(O) + ψ₀ = adapt(AT, InfiniteMPS([P, P], [V, V])) + ψ, envs = leading_boundary(ψ₀, mpo, IDMRG2(; verbosity = 0, maxiter = 1, trunc = truncrank(8))) + @test ψ isa InfiniteMPS + end + end +end diff --git a/test/testsuite/hamiltonian/derivatives.jl b/test/testsuite/hamiltonian/derivatives.jl new file mode 100644 index 000000000..7af34f7b9 --- /dev/null +++ b/test/testsuite/hamiltonian/derivatives.jl @@ -0,0 +1,127 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: C_hamiltonian, AC_hamiltonian, AC2_hamiltonian +using MPSKit: _transpose_front, _transpose_tail +using TensorKit +using BlockTensorKit: nonzero_length +using Random +using Adapt + +export hamiltonian_derivatives_jordan_structure, hamiltonian_derivatives_real_complex, + hamiltonian_derivatives_finite_expval + +# `JordanMPO_AC(2)_Hamiltonian` has two construction paths: a fast one, taken when every +# block already has the storagetype of the environments, and a converting one +# this converting path is reached when `scalartype(H) != scalartype(ψ)` and an on-site D block is passed +# nearest-neighbour models have an ending B block on every site (but the first) + +function hamiltonian_derivatives_jordan_structure(AT; fast = false) + Random.seed!(1234) + return @testset "Jordan block structure of the long-range models ($AT)" begin + H = adapt(AT, long_range_ising(Float64; L = 4)) + Hi = adapt(AT, long_range_ising_infinite(Float64; L = 3)) + + # nothing ends before the far end of the (long-range) bond + @test all(i -> nonzero_length(H[i].B) == 0, 1:3) + @test nonzero_length(H[4].B) == 1 + @test all(i -> nonzero_length(Hi[i].B) == 0, 1:2) + @test nonzero_length(Hi[3].B) == 1 + + # while the left virtual space is non-trivial everywhere but on the first site of + # the finite chain, so `E` is present exactly where `B` is not + @test size(H[1], 1) == 1 + @test all(i -> size(H[i], 1) > 1, 2:4) + @test all(i -> size(Hi[i], 1) > 1, 1:3) + + # every site carries an on-site term, which is what forces the converting constructor + @test all(i -> nonzero_length(H[i].D) == 1, 1:4) + @test all(i -> nonzero_length(Hi[i].D) == 1, 1:3) + end +end + +function hamiltonian_derivatives_real_complex(AT; fast = false) + Random.seed!(1234) + return @testset "MPOHamiltonian derivatives: real operator, complex state ($AT)" verbose = true begin + D = 8 + L_inf, L_fin = 3, 4 + ψ_inf = adapt(AT, InfiniteMPS(randn, ComplexF64, fill(ℂ^2, L_inf), fill(ℂ^D, L_inf))) + ψ_fin = adapt(AT, FiniteMPS(randn, ComplexF64, L_fin, ℂ^2, ℂ^D)) + models = [ + "FiniteMPS, long-range" => (adapt(AT, long_range_ising(Float64; L = L_fin)), ψ_fin), + "FiniteMPS, nearest-neighbour" => (adapt(AT, transverse_field_ising(Float64; g = 4.0, L = L_fin)), ψ_fin), + "InfiniteMPS, long-range" => (adapt(AT, long_range_ising_infinite(Float64; L = L_inf)), ψ_inf), + "InfiniteMPS, nearest-neighbour" => (adapt(AT, repeat(transverse_field_ising(Float64; g = 4.0), 3)), ψ_inf), + ] + + @testset "$name" for (name, (H, ψ)) in models + @test scalartype(H) <: Real + @test scalartype(ψ) <: Complex + + # `complex(H)` takes the fast path, `H` the converting one: both must agree + Hc = complex(H) + @test scalartype(Hc) <: Complex + envs = environments(ψ, H, ψ) + envs_c = environments(ψ, Hc, ψ) + @test expectation_value(ψ, H, envs) ≈ expectation_value(ψ, Hc, envs_c) + + L = length(ψ) + bonds = isfinite(H) ? (1:(L - 1)) : (1:L) + + @testset "AC" begin + for site in 1:L + AC = ψ.AC[site] + @test AC_hamiltonian(site, ψ, H, ψ, envs)(AC) ≈ + AC_hamiltonian(site, ψ, Hc, ψ, envs_c)(AC) + end + end + + @testset "AC2" begin + for site in bonds + AC2 = _transpose_front(ψ.AC[site]) * _transpose_tail(ψ.AR[site + 1]) + @test AC2_hamiltonian(site, ψ, H, ψ, envs)(AC2) ≈ + AC2_hamiltonian(site, ψ, Hc, ψ, envs_c)(AC2) + end + end + + @testset "C" begin + for site in bonds + C = ψ.C[site] + @test C_hamiltonian(site, ψ, H, ψ, envs)(C) ≈ + C_hamiltonian(site, ψ, Hc, ψ, envs_c)(C) + end + end + end + end +end + +function hamiltonian_derivatives_finite_expval(AT; fast = false) + Random.seed!(1234) + return @testset "FiniteMPS derivatives reproduce the expectation value ($AT)" verbose = true begin + # in mixed gauge the derivative operators contract everything but the center site(s), + # so their expectation value is the full energy, independent of where we sit + D = 8 + L = 10 + models = [ + "long-range" => adapt(AT, long_range_ising(Float64; L = L)), + "nearest-neighbour" => adapt(AT, transverse_field_ising(Float64; g = 4.0, L = L)), + ] + + @testset "$name" for (name, H) in models + for T in (Float64, ComplexF64) + ψ = normalize!(adapt(AT, FiniteMPS(randn, T, L, ℂ^2, ℂ^D))) + envs = environments(ψ, H, ψ) + E = expectation_value(ψ, H, envs) + + for site in 1:L + AC = ψ.AC[site] + @test dot(AC, AC_hamiltonian(site, ψ, H, ψ, envs)(AC)) ≈ E + end + for site in 1:(L - 1) + AC2 = _transpose_front(ψ.AC[site]) * _transpose_tail(ψ.AR[site + 1]) + @test dot(AC2, AC2_hamiltonian(site, ψ, H, ψ, envs)(AC2)) ≈ E + end + end + end + end +end diff --git a/test/testsuite/hamiltonian/finite.jl b/test/testsuite/hamiltonian/finite.jl new file mode 100644 index 000000000..a3b919b1a --- /dev/null +++ b/test/testsuite/hamiltonian/finite.jl @@ -0,0 +1,229 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Adapt + +export hamiltonian_finite_mpohamiltonian, hamiltonian_finite_canonicalization, + hamiltonian_finite_repeated_indices + +function hamiltonian_finite_mpohamiltonian(AT; fast = false) + return @testset "Finite MPOHamiltonian ($AT)" begin + L = 3 + T = ComplexF64 + mpoham_finite_Vs = fast ? (ℂ^2,) : (ℂ^2, U1Space(-1 => 1, 0 => 1, 1 => 1)) + Ts = fast ? (Float64,) : (Float64, ComplexF64) + for (T, V) in zip(Ts, mpoham_finite_Vs) + lattice = fill(V, L) + O₁ = adapt(AT, randn(T, V, V)) + O₁ += O₁' + E = id(storagetype(O₁), domain(O₁)) + O₂ = adapt(AT, randn(T, V^2 ← V^2)) + O₂ += O₂' + + H1 = FiniteMPOHamiltonian(lattice, i => O₁ for i in 1:L) + H2 = FiniteMPOHamiltonian(lattice, (i, i + 1) => O₂ for i in 1:(L - 1)) + H3 = FiniteMPOHamiltonian(lattice, 1 => O₁, (2, 3) => O₂, (1, 3) => O₂) + + @test scalartype(H1) == scalartype(H2) == scalartype(H3) == T + if !(T <: Complex) + for H in (H1, H2, H3) + # `@constinferred` cannot be used inside a function; `@testinferred` is + # equivalent here since there are no constant arguments + Hc = @testinferred complex(H) + @test scalartype(Hc) == complex(T) + # cannot define `real(H)`, so only test elementwise + for (Wc, W) in zip(parent(Hc), parent(H)) + Wr = @testinferred real(Wc) + @test scalartype(Wr) == T + @test Wr ≈ W atol = 1.0e-6 + end + end + end + + # A one-site Hamiltonian must not be contracted with itself. + Hsingle = FiniteMPOHamiltonian([V], 1 => O₁) + @test convert(TensorMap, Hsingle) ≈ O₁ + + # check if constructor works by converting back to tensormap + H1_tm = convert(TensorMap, H1) + operators = vcat(fill(E, L - 1), O₁) + @test H1_tm ≈ mapreduce(+, 1:L) do i + return reduce(⊗, circshift(operators, i)) + end + operators = vcat(fill(E, L - 2), O₂) + @test convert(TensorMap, H2) ≈ mapreduce(+, 1:(L - 1)) do i + return reduce(⊗, circshift(operators, i)) + end + @test convert(TensorMap, H3) ≈ + O₁ ⊗ E ⊗ E + E ⊗ O₂ + permute(O₂ ⊗ E, ((1, 3, 2), (4, 6, 5))) + + # check if adding terms on the same site works + single_terms = Iterators.flatten(Iterators.repeated((i => O₁ / 2 for i in 1:L), 2)) + H4 = FiniteMPOHamiltonian(lattice, single_terms) + @test H4 ≈ H1 atol = 1.0e-6 + double_terms = Iterators.flatten( + Iterators.repeated(((i, i + 1) => O₂ / 2 for i in 1:(L - 1)), 2) + ) + H5 = FiniteMPOHamiltonian(lattice, double_terms) + @test H5 ≈ H2 atol = 1.0e-6 + + # test linear algebra + @test H1 ≈ + FiniteMPOHamiltonian(lattice, 1 => O₁) + + FiniteMPOHamiltonian(lattice, 2 => O₁) + + FiniteMPOHamiltonian(lattice, 3 => O₁) + @test 0.8 * H1 + 0.2 * H1 ≈ H1 atol = 1.0e-6 + @test convert(TensorMap, H1 + H2) ≈ convert(TensorMap, H1) + convert(TensorMap, H2) atol = 1.0e-6 + H1_trunc = changebonds(H1, SvdCut(; trunc = truncrank(0))) + @test H1_trunc ≈ H1 + @test all(left_virtualspace(H1_trunc) .== left_virtualspace(H1)) + + # test dot and application + state = adapt(AT, rand(T, prod(lattice))) + mps = FiniteMPS(state) + + @test convert(TensorMap, H1 * mps) ≈ H1_tm * state + @test H1 * state ≈ H1_tm * state + @test dot(mps, H2, mps) ≈ dot(mps, H2 * mps) + + # test constructor from dictionary with mixed linear and Cartesian lattice indices as keys + grid = square = fill(V, 3, 3) + + local_operators = Dict((I,) => O₁ for I in eachindex(grid)) + I_vertical = CartesianIndex(1, 0) + vertical_operators = Dict( + (I, I + I_vertical) => O₂ for I in eachindex(IndexCartesian(), square) if I[1] < size(square, 1) + ) + I_horizontal = CartesianIndex(0, 1) + horizontal_operators = Dict( + (I, I + I_horizontal) => O₂ for I in eachindex(IndexCartesian(), square) if I[2] < size(square, 1) + ) + operators = merge(local_operators, vertical_operators, horizontal_operators) + H4 = FiniteMPOHamiltonian(grid, operators) + + @test H4 ≈ + FiniteMPOHamiltonian(grid, local_operators) + + FiniteMPOHamiltonian(grid, vertical_operators) + + FiniteMPOHamiltonian(grid, horizontal_operators) atol = 1.0e-4 + + H5 = changebonds(H4 / 3 + 2H4 / 3, SvdCut(; trunc = trunctol(; atol = 1.0e-12))) + psi = adapt(AT, FiniteMPS(physicalspace(H5), V ⊕ rightunitspace(V))) + @test expectation_value(psi, H4) ≈ expectation_value(psi, H5) + end + end +end + +function hamiltonian_finite_canonicalization(AT; fast = false) + return @testset "Finite MPOHamiltonian canonicalization ($AT)" begin + T = ComplexF64 + for V in (ℂ^2, U1Space(-1 => 1, 0 => 1, 1 => 1), Rep[SU₂](1 // 2 => 1)) + O₁ = adapt(AT, randn(T, V, V)) + O₁ += O₁' + O₂ = adapt(AT, randn(T, V^2 ← V^2)) + O₂ += O₂' + E = id(storagetype(O₁), domain(O₁)) + + # regression test for https://github.com/QuantumKitHub/MPSKit.jl/issues/522 + # bulk block of the middle site is empty on the left but not on the right + H_right = FiniteMPOHamiltonian(fill(V, 3), (1,) => O₁, (2, 3) => O₂) + H_right_tm = O₁ ⊗ E ⊗ E + E ⊗ O₂ + H, = MPSKit.right_canonicalize!(copy(H_right)) + @test convert(TensorMap, H) ≈ H_right_tm + + # bulk block of the middle site is empty on the right but not on the left + H_left = FiniteMPOHamiltonian(fill(V, 3), (1, 2) => O₂, (3,) => O₁) + H_left_tm = O₂ ⊗ E + E ⊗ E ⊗ O₁ + H, = MPSKit.left_canonicalize!(copy(H_left)) + @test convert(TensorMap, H) ≈ H_left_tm + + L = 5 + H5 = FiniteMPOHamiltonian(fill(V, L), (1, 2) => O₂, (3,) => O₁, (4, 5) => O₂) + H5_tm = O₂ ⊗ E ⊗ E ⊗ E + E ⊗ E ⊗ O₁ ⊗ E ⊗ E + E ⊗ E ⊗ E ⊗ O₂ + H, = MPSKit.right_canonicalize!(copy(H5)) + @test convert(TensorMap, H) ≈ H5_tm + H, = MPSKit.left_canonicalize!(H) + @test convert(TensorMap, H) ≈ H5_tm + + H5_trunc = changebonds(H5, SvdCut(; trunc = truncrank(16))) + @test convert(TensorMap, H5_trunc) ≈ H5_tm + + # canonical forms, also when `C`/`B` have identity components and the blocks are rank-deficient + L = 4 + H = FiniteMPOHamiltonian(fill(V, L), (i, i + 1) => O₂ for i in 1:(L - 1)) + H_tm = convert(TensorMap, H) + + HL, Ls = MPSKit.left_canonicalize!(copy(H)) + @test length(Ls) == L - 1 + @test all(i -> codomain(Ls[i])[1] == right_virtualspace(HL[i].A), 1:(L - 1)) + @test convert(TensorMap, HL) ≈ H_tm + for W in HL[2:(L - 1)] + Vb = MPSKit.SparseBlockTensorMap(W)[1:(end - 1), 1, 1, 1:(end - 1)] + @plansor G[r; r′] := conj(Vb[l p; p′ r]) * Vb[l p; p′ r′] + G = TensorMap(G) / dim(physicalspace(W)) + @test G ≈ id(domain(G)) + end + + HR, Rs = MPSKit.right_canonicalize!(copy(H)) + @test length(Rs) == L - 1 + @test all(i -> domain(Rs[i])[1] == left_virtualspace(HR[i + 1].A), 1:(L - 1)) + @test convert(TensorMap, HR) ≈ H_tm + for W in HR[2:(L - 1)] + Vb = MPSKit.SparseBlockTensorMap(W)[2:end, 1, 1, 2:end] + @plansor G[l; l′] := Vb[l p; p′ r] * conj(Vb[l′ p; p′ r]) + G = TensorMap(G) / dim(physicalspace(W)) + @test G ≈ id(codomain(G)) + end + end + end +end + +function hamiltonian_finite_repeated_indices(AT; fast = false) + return @testset "Finite MPOHamiltonian repeated indices ($AT)" begin + X = adapt(AT, randn(ComplexF64, ℂ^2, ℂ^2)) + X += X' + Y = adapt(AT, randn(ComplexF64, ℂ^2, ℂ^2)) + Y += Y' + L = 4 + chain = fill(space(X, 1), 4) + + H1 = FiniteMPOHamiltonian(chain, (1,) => (X * X * Y * Y)) + H2 = FiniteMPOHamiltonian(chain, (1, 1, 1, 1) => (X ⊗ X ⊗ Y ⊗ Y)) + @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) + + H1 = FiniteMPOHamiltonian(chain, (1, 2) => ((X * Y) ⊗ (X * Y))) + H2 = FiniteMPOHamiltonian(chain, (1, 2, 1, 2) => (X ⊗ X ⊗ Y ⊗ Y)) + @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) + + H1 = FiniteMPOHamiltonian(chain, (1, 2) => ((X * X * Y) ⊗ Y)) + H2 = FiniteMPOHamiltonian(chain, (1, 1, 1, 2) => (X ⊗ X ⊗ Y ⊗ Y)) + @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) + + H1 = FiniteMPOHamiltonian(chain, (1, 2) => ((X * Y * Y) ⊗ X)) + H2 = FiniteMPOHamiltonian(chain, (1, 2, 1, 1) => (X ⊗ X ⊗ Y ⊗ Y)) + @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) + + H1 = FiniteMPOHamiltonian(chain, (1, 2, 3) => FiniteMPO((X * X) ⊗ Y ⊗ Y)) + H2 = FiniteMPOHamiltonian(chain, (1, 1, 2, 3) => FiniteMPO(X ⊗ X ⊗ Y ⊗ Y)) + @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) + + H1 = FiniteMPOHamiltonian(chain, (1, 2, 3) => FiniteMPO((Y * Y) ⊗ X ⊗ X)) + H2 = FiniteMPOHamiltonian(chain, (2, 3, 1, 1) => FiniteMPO(X ⊗ X ⊗ Y ⊗ Y)) + @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) + + H1 = FiniteMPOHamiltonian(chain, (1, 2, 3) => FiniteMPO(X ⊗ (X * Y) ⊗ Y)) + H2 = FiniteMPOHamiltonian(chain, (1, 2, 2, 3) => FiniteMPO(X ⊗ X ⊗ Y ⊗ Y)) + @test convert(TensorMap, H1) ≈ convert(TensorMap, H2) + + # a caller-supplied MPO is only copied when the indices actually need canonicalising, + # so check that neither path writes back into it + for inds in ((1, 2, 3), (1, 2, 2, 3), (2, 3, 1, 1)) + mpo = FiniteMPO(X ⊗ X ⊗ Y ⊗ Y) + length(inds) == 3 && (mpo = FiniteMPO((X * X) ⊗ Y ⊗ Y)) + before = map(copy, parent(mpo)) + FiniteMPOHamiltonian(chain, inds => mpo) + @test all(splat(≈), zip(parent(mpo), before)) + end + end +end diff --git a/test/testsuite/hamiltonian/infinite.jl b/test/testsuite/hamiltonian/infinite.jl new file mode 100644 index 000000000..7404bb0c4 --- /dev/null +++ b/test/testsuite/hamiltonian/infinite.jl @@ -0,0 +1,109 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Adapt + +export hamiltonian_infinite_mpohamiltonian, hamiltonian_infinite_canonical_forms + +function test_spaces(fast) + pspaces = PSPACES_TRIPLE + vspaces = VSPACES_TRIPLE + if fast + pspaces = pspaces[1:1] + vspaces = vspaces[1:1] + end + return pspaces, vspaces +end + +function hamiltonian_infinite_mpohamiltonian(AT; fast = false) + pspaces, vspaces = test_spaces(fast) + return @testset "InfiniteMPOHamiltonian ($AT) $(sectortype(pspace))" for (pspace, Dspace) in zip(pspaces, vspaces) + # generate a 1-2-3 body interaction + operators = ntuple(3) do i + O = adapt(AT, rand(ComplexF64, pspace^i, pspace^i)) + return O += O' + end + + H1 = InfiniteMPOHamiltonian(operators[1]) + H2 = InfiniteMPOHamiltonian(operators[2]) + H3 = repeat(InfiniteMPOHamiltonian(operators[3]), 2) + + # make a teststate to measure expectation values for + ψ1 = adapt(AT, InfiniteMPS([pspace], [Dspace])) + ψ2 = adapt(AT, InfiniteMPS([pspace, pspace], [Dspace, Dspace])) + + e1 = expectation_value(ψ1, H1) + e2 = expectation_value(ψ1, H2) + + H1 = 2 * H1 - [1] + @test e1 * 2 - 1 ≈ expectation_value(ψ1, H1) atol = 1.0e-10 + + H1 = H1 + H2 + + @test e1 * 2 + e2 - 1 ≈ expectation_value(ψ1, H1) atol = 1.0e-10 + + H1 = repeat(H1, 2) + + e1 = expectation_value(ψ2, H1) + e3 = expectation_value(ψ2, H3) + + @test e1 + e3 ≈ expectation_value(ψ2, H1 + H3) atol = 1.0e-10 + + H4 = H1 + H3 + @test real(expectation_value(ψ2, H4)) >= 0 + + O1_real = project_hermitian!(adapt(AT, randn(Float64, pspace, pspace))) + O2_real = project_hermitian!(adapt(AT, randn(Float64, pspace ⊗ pspace, pspace ⊗ pspace))) + Hreal_long_range = InfiniteMPOHamiltonian(O1_real) + InfiniteMPOHamiltonian(O2_real) + Hcomplex_local = InfiniteMPOHamiltonian(operators[1]) + + @test scalartype(Hreal_long_range) == Float64 + @test scalartype(Hcomplex_local) == ComplexF64 + for H in ( + Hreal_long_range + Hcomplex_local, Hcomplex_local + Hreal_long_range, + Hreal_long_range - Hcomplex_local, Hcomplex_local - Hreal_long_range, + ) + @test scalartype(H) == ComplexF64 + end + @test expectation_value(ψ1, Hreal_long_range + Hcomplex_local) ≈ + expectation_value(ψ1, Hreal_long_range) + expectation_value(ψ1, Hcomplex_local) atol = 1.0e-10 + end +end + +function hamiltonian_infinite_canonical_forms(AT; fast = false) + pspaces, vspaces = test_spaces(fast) + return @testset "InfiniteMPOHamiltonian canonical forms ($AT) $(sectortype(pspace))" for (pspace, Dspace) in zip(pspaces, vspaces) + O₂ = adapt(AT, rand(ComplexF64, pspace^2, pspace^2)) + O₂ += O₂' + O₃ = adapt(AT, rand(ComplexF64, pspace^3, pspace^3)) + O₃ += O₃' + lattice = PeriodicVector(fill(pspace, 2)) + H = InfiniteMPOHamiltonian(lattice, (i, i + 1) => O₂ for i in 1:2) + + InfiniteMPOHamiltonian(lattice, (i, i + 1, i + 2) => O₃ for i in 1:2) + + ψ = adapt(AT, InfiniteMPS(rand, ComplexF64, fill(pspace, 2), fill(Dspace, 2))) + E = expectation_value(ψ, H) + + # the upper-left block `[1 C; 0 A]` has orthonormal columns + HL, = MPSKit.left_canonicalize!(copy(H)) + @test expectation_value(ψ, HL) ≈ E + for W in parent(HL) + V = MPSKit.SparseBlockTensorMap(W)[1:(end - 1), 1, 1, 1:(end - 1)] + @plansor G[r; r′] := conj(V[l p; p′ r]) * V[l p; p′ r′] + G = TensorMap(G) / dim(physicalspace(W)) + @test G ≈ id(domain(G)) + end + + # the lower-right block `[A B; 0 1]` has orthonormal rows + HR, = MPSKit.right_canonicalize!(copy(H)) + @test expectation_value(ψ, HR) ≈ E + for W in parent(HR) + V = MPSKit.SparseBlockTensorMap(W)[2:end, 1, 1, 2:end] + @plansor G[l; l′] := V[l p; p′ r] * conj(V[l′ p; p′ r]) + G = TensorMap(G) / dim(physicalspace(W)) + @test G ≈ id(codomain(G)) + end + end +end diff --git a/test/testsuite/hamiltonian/mixed.jl b/test/testsuite/hamiltonian/mixed.jl new file mode 100644 index 000000000..2df19ff18 --- /dev/null +++ b/test/testsuite/hamiltonian/mixed.jl @@ -0,0 +1,206 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: GeometryStyle, FiniteChainStyle, InfiniteChainStyle, OperatorStyle, HamiltonianStyle +using TensorKit +using TensorKit: ℙ +using Adapt + +export hamiltonian_mixed_constructors, hamiltonian_mixed_adapt, hamiltonian_mixed_channel_sharing + +function hamiltonian_mixed_constructors(AT; fast = false) + return @testset "MPOHamiltonian constructors ($AT)" begin + P = ℂ^2 + T = Float64 + + H1 = adapt(AT, randn(T, P ← P)) + H1 += H1' + D = FiniteMPO(H1)[1] + + H2 = adapt(AT, randn(T, P^2 ← P^2)) + H2 += H2' + C, B = FiniteMPO(H2)[1:2] + + Elt = Union{Missing, typeof(D), scalartype(D)} + Wmid = Elt[1.0 C D; 0.0 0.0 B; 0.0 0.0 1.0] + Wleft = Wmid[1:1, :] + Wright = Wmid[:, end:end] + + # Finite + Ws = [Wleft, Wmid, Wmid, Wright] + H = FiniteMPOHamiltonian( + fill(P, 4), [(i,) => H1 for i in 1:4]..., [(i, i + 1) => H2 for i in 1:3]... + ) + H′ = FiniteMPOHamiltonian(Ws) + @test H ≈ H′ + + H′ = FiniteMPOHamiltonian(map(Base.Fix1(collect, Any), Ws)) # without type info + @test H ≈ H′ + + @test isfinite(H) + @test isfinite(typeof(H)) + @test GeometryStyle(typeof(H)) == FiniteChainStyle() + @test GeometryStyle(H) == FiniteChainStyle() + @test OperatorStyle(typeof(H)) == HamiltonianStyle() + @test OperatorStyle(H) == HamiltonianStyle() + @test OperatorStyle(H, H′) == OperatorStyle(H) + @test TensorKit.storagetype(H) == TensorKit.similarstoragetype(AT, T) + @test TensorKit.storagetype(typeof(H)) == TensorKit.similarstoragetype(AT, T) + + # Infinite + Ws = [Wmid] + H = InfiniteMPOHamiltonian( + fill(P, 1), [(i,) => H1 for i in 1:1]..., [(i, i + 1) => H2 for i in 1:1]... + ) + H′ = InfiniteMPOHamiltonian(Ws) + @test all(parent(H) .≈ parent(H′)) + + H′ = InfiniteMPOHamiltonian(map(Base.Fix1(collect, Any), Ws)) # without type info + @test all(parent(H) .≈ parent(H′)) + + @test !isfinite(H) + @test !isfinite(typeof(H)) + @test GeometryStyle(typeof(H)) == InfiniteChainStyle() + @test GeometryStyle(H) == InfiniteChainStyle() + @test OperatorStyle(typeof(H)) == HamiltonianStyle() + @test OperatorStyle(H) == HamiltonianStyle() + @test TensorKit.storagetype(H′) == TensorKit.similarstoragetype(AT, T) + @test TensorKit.storagetype(typeof(H′)) == TensorKit.similarstoragetype(AT, T) + end +end + +function hamiltonian_mixed_adapt(AT; fast = false) + adapt_Vs = fast ? (ℂ^2,) : (ℂ^2, U1Space(-1 => 1, 0 => 1, 1 => 1)) + Ts = fast ? (Float64,) : (Float64, ComplexF64) + return @testset "Adapt ($AT)" for V in adapt_Vs + h = adapt(AT, rand(Float32, V^2 ← V^2)) + h += h' + + L = 4 + H1 = FiniteMPOHamiltonian( + fill(V, L), + ((i, i + 1) => h for i in 1:(L - 1))..., + ((i, i + 2) => h for i in 1:(L - 2))..., + ((i, i + 3) => h for i in 1:(L - 3))..., + ) + mps1 = adapt(AT, FiniteMPS(physicalspace(H1), unitspace(V))) + + for T in Ts + H2 = if VERSION <= v"1.12" + adapt(TensorKit.similarstoragetype(AT, T), H1) + else + @testinferred adapt(TensorKit.similarstoragetype(AT, T), H1) + end + @test H2 isa FiniteMPOHamiltonian + @test scalartype(H2) == T + @test storagetype(H2) == TensorKit.similarstoragetype(AT, T) + @test expectation_value(mps1, H1) ≈ expectation_value(mps1, H2) + end + + H3 = InfiniteMPOHamiltonian(fill(V, L), (1, 2) => h, (1, 3) => h, (1, 4) => h) + mps2 = adapt(AT, InfiniteMPS(physicalspace(H3), [unitspace(V)])) + for T in Ts + H4 = if VERSION <= v"1.12" + # this is type unstable for LTS for some reason + adapt(TensorKit.similarstoragetype(AT, T), H3) + else + @testinferred adapt(TensorKit.similarstoragetype(AT, T), H3) + end + @test H4 isa InfiniteMPOHamiltonian + @test scalartype(H4) == T + @test storagetype(H4) == TensorKit.similarstoragetype(AT, T) + @test storagetype(typeof(H4)) == TensorKit.similarstoragetype(AT, T) + @test expectation_value(mps2, H3) ≈ expectation_value(mps2, H4) + end + end +end + +function hamiltonian_mixed_channel_sharing(AT; fast = false) + pspaces = (ℙ^4, Rep[U₁](0 => 2), Rep[SU₂](1 => 1)) + vspaces = (ℙ^10, Rep[U₁]((0 => 20)), Rep[SU₂](1 // 2 => 10, 3 // 2 => 5, 5 // 2 => 1)) + if fast + pspaces = pspaces[1:1] + vspaces = vspaces[1:1] + end + return @testset "MPOHamiltonian channel sharing ($AT)" begin + # Terms that start out identically share a virtual channel, which is what keeps the bond + # dimension of a long-range interaction linear -- instead of quadratic -- in its range. + # Correctness is checked against the same Hamiltonian assembled by summing single-term + # Hamiltonians, which cannot share anything and is thus independent of how channels are + # assigned. + Z = adapt(AT, TensorMap(Float64[1 0; 0 -1], ℂ^2, ℂ^2)) + X = adapt(AT, TensorMap(Float64[0 1; 1 0], ℂ^2, ℂ^2)) + + infinite_ev(lattice, terms) = let L = length(lattice) + ψ = adapt(AT, InfiniteMPS(collect(lattice), fill(ℂ^8, L))) + H = InfiniteMPOHamiltonian(lattice, terms...) + Href = sum(InfiniteMPOHamiltonian(lattice, t) for t in terms) + (H, sum(expectation_value(ψ, H)), sum(expectation_value(ψ, Href))) + end + + @testset "power-law couplings, unit cell L=$L" for L in (1, 2, 3) + R = 5 + lattice = fill(ℂ^2, L) + terms = Pair[(i, i + d) => (1 / d^3) * (Z ⊗ Z) for i in 1:L for d in 1:R] + append!(terms, [(i,) => 0.7 * X for i in 1:L]) + H, e, eref = infinite_ev(lattice, terms) + @test e ≈ eref + @test all(i -> dim(left_virtualspace(H, i)) ≤ R + 2, 1:L) + end + + @testset "proportional terms are merged" begin + lattice = fill(ℂ^2, 1) + H = InfiniteMPOHamiltonian(lattice, (1, 2) => Z ⊗ Z, (1, 2) => 0.5 * (Z ⊗ Z)) + Href = InfiniteMPOHamiltonian(lattice, (1, 2) => 1.5 * (Z ⊗ Z)) + ψ = adapt(AT, InfiniteMPS([ℂ^2], [ℂ^8])) + @test sum(expectation_value(ψ, H)) ≈ sum(expectation_value(ψ, Href)) + @test dim(left_virtualspace(H, 1)) == dim(left_virtualspace(Href, 1)) + end + + @testset "negative prefactors" begin + lattice = fill(ℂ^2, 1) + terms = Pair[ + (1, 2) => Z ⊗ Z, (1, 3) => -2.0 * (Z ⊗ Z), (1, 4) => 0.25 * (Z ⊗ Z), + ] + _, e, eref = infinite_ev(lattice, terms) + @test e ≈ eref + end + + @testset "mixed operators and ranges" begin + lattice = fill(ℂ^2, 2) + terms = Pair[ + (1, 2) => Z ⊗ Z, (1, 3) => Z ⊗ X, (1, 4) => 3.0 * (Z ⊗ Z), + (2, 3, 4) => X ⊗ Z ⊗ X, (1,) => 0.7 * X, + ] + _, e, eref = infinite_ev(lattice, terms) + @test e ≈ eref + end + + @testset "finite chain" begin + L = 6 + lattice = fill(ℂ^2, L) + terms = Pair[ + (i, j) => (1 / (j - i)^3) * (Z ⊗ Z) for i in 1:L for j in (i + 1):L + ] + push!(terms, (1,) => 0.7 * X) + H = FiniteMPOHamiltonian(lattice, terms...) + Href = sum(FiniteMPOHamiltonian(lattice, t) for t in terms) + @test convert(TensorMap, H) ≈ convert(TensorMap, Href) + @test maximum(i -> dim(left_virtualspace(H, i)), 1:L) < + maximum(i -> dim(left_virtualspace(Href, i)), 1:L) + end + + @testset "symmetric tensors $(sectortype(pspace))" for (pspace, Dspace) in + zip(pspaces, vspaces) + O = adapt(AT, rand(ComplexF64, pspace^2, pspace^2)) + O += O' + terms = Pair[(1, 1 + d) => (1 / d^3) * O for d in 1:4] + lattice = fill(pspace, 1) + H = InfiniteMPOHamiltonian(lattice, terms...) + Href = sum(InfiniteMPOHamiltonian(lattice, t) for t in terms) + ψ = adapt(AT, InfiniteMPS([pspace], [Dspace])) + @test sum(expectation_value(ψ, H)) ≈ sum(expectation_value(ψ, Href)) + @test dim(left_virtualspace(H, 1)) < dim(left_virtualspace(Href, 1)) + end + end +end diff --git a/test/testsuite/mpo/dense_mpo.jl b/test/testsuite/mpo/dense_mpo.jl new file mode 100644 index 000000000..98096fea6 --- /dev/null +++ b/test/testsuite/mpo/dense_mpo.jl @@ -0,0 +1,23 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: GeometryStyle +using TensorKit +using Adapt + +export mpo_dense_mpo + +function mpo_dense_mpo(AT; fast = false) + return @testset "DenseMPO ($AT)" for ham in (transverse_field_ising(), heisenberg_XXX(; spin = 1)) + ham = adapt(AT, ham) + pspace = physicalspace(ham, 1) + ou = rightunitspace(pspace) + + ψ = adapt(AT, InfiniteMPS([pspace], [ou ⊕ pspace])) + + W = MPSKit.DenseMPO(make_time_mpo(ham, 1im * 0.5, WII())) + + @test GeometryStyle(ψ, W) == GeometryStyle(ψ) + @test W * (W * ψ) ≈ (W * W) * ψ atol = 1.0e-2 # TODO: there is a normalization issue here + end +end diff --git a/test/testsuite/mpo/finite.jl b/test/testsuite/mpo/finite.jl new file mode 100644 index 000000000..822e28438 --- /dev/null +++ b/test/testsuite/mpo/finite.jl @@ -0,0 +1,124 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: GeometryStyle, FiniteChainStyle, OperatorStyle, MPOStyle +using MPSKit: remove_orphans! +using TensorKit +using TensorKit: ℙ +using BlockTensorKit +using BlockTensorKit: SparseBlockTensorMap, nonzero_keys +using Adapt + +# `@constinferred` evaluates a helper at global scope, which is not allowed inside a function +# body, so the testsuite functions use `@testinferred` (none of the calls have constant arguments) + +export mpo_finite_basics, mpo_finite_remove_orphans + +function mpo_finite_basics(AT; fast = false) + return @testset "FiniteMPO ($AT)" begin + # start from random operators + L = 4 + T = ComplexF64 + + finite_mpo_Vs = fast ? (ℂ^2,) : (ℂ^2, U1Space(0 => 1, 1 => 1)) + for V in finite_mpo_Vs + O₁ = adapt(AT, rand(T, V^L, V^L)) + O₂ = adapt(AT, rand(T, space(O₁))) + O₃ = adapt(AT, rand(real(T), space(O₁))) + + # create MPO and convert it back to see if it is the same + mpo₁ = FiniteMPO(O₁) # type-unstable for now! + mpo₂ = FiniteMPO(O₂) + mpo₃ = FiniteMPO(O₃) + + @test isfinite(mpo₁) + @test isfinite(typeof(mpo₁)) + @test GeometryStyle(typeof(mpo₁)) == FiniteChainStyle() + @test GeometryStyle(mpo₁) == FiniteChainStyle() + @test OperatorStyle(typeof(mpo₁)) == MPOStyle() + + @test @testinferred physicalspace(mpo₁) == fill(V, L) + Vleft = @testinferred left_virtualspace(mpo₁) + Vright = @testinferred right_virtualspace(mpo₂) + for i in 1:L + @test Vleft[i] == left_virtualspace(mpo₁, i) + @test Vright[i] == right_virtualspace(mpo₁, i) + end + + @test convert(TensorMap, mpo₁) ≈ O₁ + @test convert(TensorMap, -mpo₂) ≈ -O₂ + @test convert(TensorMap, @testinferred complex(mpo₃)) ≈ complex(O₃) + + # test scalar multiplication + α = rand(T) + @test convert(TensorMap, α * mpo₁) ≈ α * O₁ + @test convert(TensorMap, mpo₁ * α) ≈ O₁ * α + @test α * mpo₃ ≈ α * complex(mpo₃) atol = 1.0e-6 + + # test addition and multiplication + @test convert(TensorMap, mpo₁ + mpo₂) ≈ O₁ + O₂ + @test convert(TensorMap, mpo₁ + mpo₃) ≈ O₁ + O₃ + @test convert(TensorMap, mpo₁ * mpo₂) ≈ O₁ * O₂ + @test convert(TensorMap, mpo₁ * mpo₃) ≈ O₁ * O₃ + + # test application to a state + ψ₁ = adapt(AT, rand(T, domain(O₁))) + ψ₂ = adapt(AT, rand(real(T), domain(O₂))) + mps₁ = FiniteMPS(ψ₁) + mps₂ = FiniteMPS(ψ₂) + + @test @testinferred GeometryStyle(mps₁, mpo₁, mps₁) == GeometryStyle(mps₁) + + @test convert(TensorMap, mpo₁ * mps₁) ≈ O₁ * ψ₁ + @test mpo₁ * ψ₁ ≈ O₁ * ψ₁ + @test convert(TensorMap, mpo₃ * mps₁) ≈ O₃ * ψ₁ + @test mpo₃ * ψ₁ ≈ O₃ * ψ₁ + @test convert(TensorMap, mpo₁ * mps₂) ≈ O₁ * ψ₂ + @test mpo₁ * ψ₂ ≈ O₁ * ψ₂ + + @test dot(mps₁, mpo₁, mps₁) ≈ dot(ψ₁, O₁, ψ₁) + @test dot(mps₁, mpo₁, mps₁) ≈ dot(mps₁, mpo₁ * mps₁) + # test conversion to and from mps + mpomps₁ = convert(FiniteMPS, mpo₁) + mpompsmpo₁ = convert(FiniteMPO, mpomps₁) + + @test convert(FiniteMPO, mpomps₁) ≈ mpo₁ rtol = 1.0e-6 + + @test dot(mpomps₁, mpomps₁) ≈ dot(mpo₁, mpo₁) + end + end +end + +function mpo_finite_remove_orphans(AT; fast = false) + return @testset "remove_orphans! ($AT)" begin + T = ComplexF64 + P = ℂ^2 + TT = tensormaptype(ComplexSpace, 2, 2, TensorKit.similarstoragetype(AT, T)) + randmpotensor() = adapt(AT, rand(T, ℂ^1 ⊗ P ← P ⊗ ℂ^1)) + + @testset "finite: dead-end chain" begin + # channel 2 is never entered on site 1, so the (2, 2) block on site 2 and the + # (2, 1) block on site 3 are orphaned + V₁ = SumSpace(ℂ^1) + V₂ = SumSpace(ℂ^1, ℂ^1) + Ws = [ + SparseBlockTensorMap{TT}(undef, V₁ ⊗ P ← P ⊗ V₂), + SparseBlockTensorMap{TT}(undef, V₂ ⊗ P ← P ⊗ V₂), + SparseBlockTensorMap{TT}(undef, V₂ ⊗ P ← P ⊗ V₁), + ] + Ws[1][1, 1, 1, 1] = randmpotensor() + Ws[2][1, 1, 1, 1] = randmpotensor() + Ws[2][2, 1, 1, 2] = randmpotensor() + Ws[3][1, 1, 1, 1] = randmpotensor() + Ws[3][2, 1, 1, 1] = randmpotensor() + + mpo = MPO(Ws) + @test isfinite(mpo) + O = convert(TensorMap, mpo) + remove_orphans!(mpo) + + @test left_virtualspace(mpo) == right_virtualspace(mpo) == fill(SumSpace(ℂ^1), 3) + @test convert(TensorMap, mpo) ≈ O + end + end +end diff --git a/test/testsuite/mpo/infinite.jl b/test/testsuite/mpo/infinite.jl new file mode 100644 index 000000000..9c7beb2ea --- /dev/null +++ b/test/testsuite/mpo/infinite.jl @@ -0,0 +1,83 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: GeometryStyle, InfiniteChainStyle, OperatorStyle, MPOStyle +using MPSKit: remove_orphans! +using TensorKit +using BlockTensorKit +using BlockTensorKit: SparseBlockTensorMap, nonzero_keys +using Adapt + +export mpo_infinite_basics, mpo_infinite_remove_orphans + +function mpo_infinite_basics(AT; fast = false) + return @testset "InfiniteMPO ($AT)" begin + P = ℂ^2 + V = ℂ^2 + T = Float64 + + H1 = adapt(AT, randn(T, V ⊗ P ← P ⊗ V)) + H = InfiniteMPO([H1]) + + @test !isfinite(H) + @test !isfinite(typeof(H)) + @test GeometryStyle(typeof(H)) == InfiniteChainStyle() + @test GeometryStyle(H) == InfiniteChainStyle() + @test OperatorStyle(typeof(H)) == MPOStyle() + @test OperatorStyle(H) == MPOStyle() + + @test physicalspace(H, 1) == P + @test left_virtualspace(H, 1) == V + @test left_virtualspace(H, 4) == V + @test right_virtualspace(H, 1) == V + + multiH = MultilineMPO([H, H]) + @test physicalspace(multiH, 1, 1) == P + @test left_virtualspace(multiH, 1, 1) == left_virtualspace(multiH, 2, 1) == V + @test right_virtualspace(multiH, CartesianIndex(1, 2)) == V + @test leftunit(multiH) == leftunit(H) == unit(sectortype(P)) + @test rightunit(multiH) == rightunit(H) == unit(sectortype(P)) + end +end + +function mpo_infinite_remove_orphans(AT; fast = false) + return @testset "remove_orphans! ($AT)" begin + T = ComplexF64 + P = ℂ^2 + TT = tensormaptype(ComplexSpace, 2, 2, TensorKit.similarstoragetype(AT, T)) + randmpotensor() = adapt(AT, rand(T, ℂ^1 ⊗ P ← P ⊗ ℂ^1)) + + @testset "infinite: dead-end chain" begin + # 1 -> 1, 1 -> 2 -> 3 -> 4, where channel 4 is a dead end: removing it orphans 3, + # removing 3 orphans 2, ... so the fixed point takes several passes to reach + V = SumSpace(fill(ℂ^1, 4)...) + W = SparseBlockTensorMap{TT}(undef, V ⊗ P ← P ⊗ V) + for (i, j) in ((1, 1), (1, 2), (2, 3), (3, 4)) + W[i, 1, 1, j] = randmpotensor() + end + W₁₁ = copy(W[1, 1, 1, 1]) + + mpo = InfiniteMPO([W]) + @test !isfinite(mpo) + @test remove_orphans!(mpo) === mpo + + # only the (1, 1) self-loop survives + @test left_virtualspace(mpo, 1) == right_virtualspace(mpo, 1) == SumSpace(ℂ^1) + @test collect(nonzero_keys(mpo[1])) == [CartesianIndex(1, 1, 1, 1)] + @test mpo[1][1, 1, 1, 1] ≈ W₁₁ + end + + @testset "infinite: nothing to remove" begin + # every channel is alive, so this must terminate immediately and change nothing + H = adapt(AT, transverse_field_ising(T; g = 0.7)) + mpo = MPO(map(SparseBlockTensorMap, parent(H))) + @test !isfinite(mpo) + + spaces₀ = map(space, parent(mpo)) + keys₀ = map(W -> sort!(collect(nonzero_keys(W))), parent(mpo)) + remove_orphans!(mpo) + @test map(space, parent(mpo)) == spaces₀ + @test map(W -> sort!(collect(nonzero_keys(W))), parent(mpo)) == keys₀ + end + end +end diff --git a/test/testsuite/mpo/mixed.jl b/test/testsuite/mpo/mixed.jl new file mode 100644 index 000000000..1a74108bd --- /dev/null +++ b/test/testsuite/mpo/mixed.jl @@ -0,0 +1,35 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using Adapt + +export mpo_mixed_adapt + +function mpo_mixed_adapt(AT; fast = false) + mpo_adapt_Vs = fast ? (ℂ^2,) : (ℂ^2, U1Space(-1 => 1, 0 => 1, 1 => 1)) + Ts = fast ? (Float64,) : (Float64, ComplexF64) + return @testset "Adapt ($AT)" for V in mpo_adapt_Vs + L = 3 + o = adapt(AT, rand(Float32, V^L ← V^L)) + mpo1 = FiniteMPO(o) + for T in Ts + TA = TensorKit.similarstoragetype(AT, T) + mpo2 = @testinferred adapt(TA, mpo1) + @test mpo2 isa FiniteMPO + @test scalartype(mpo2) == T + @test storagetype(mpo2) == TA + @test convert(TensorMap, mpo2) ≈ o + end + + mpo3 = InfiniteMPO(mpo1[2:2]) + for T in Ts + TA = TensorKit.similarstoragetype(AT, T) + mpo4 = @testinferred adapt(TA, mpo3) + @test mpo4 isa InfiniteMPO + @test scalartype(mpo4) == T + @test storagetype(mpo4) == TA + @test dot(mpo3, mpo4) ≈ norm(mpo3)^2 atol = 1.0e-4 + end + end +end diff --git a/test/testsuite/mpo/multiline.jl b/test/testsuite/mpo/multiline.jl new file mode 100644 index 000000000..9c21178c9 --- /dev/null +++ b/test/testsuite/mpo/multiline.jl @@ -0,0 +1,131 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: leading_eigenvalue +using TensorKit +using Adapt + +export mpo_multiline_mpo_times_mps, mpo_multiline_leading_eigenvalue, + mpo_multiline_multirow_fixed_point, mpo_multiline_unsupported_inputs, + mpo_multiline_line_aliases, mpo_multiline_view_bounds + +d = 3 +P = ℂ^d + +# ψ[i] = |i⟩ (3 rows, 1 column), O[i]|i⟩ = |i + 1⟩ exactly +# ψ[i] is an InfiniteMPS and is periodic in the line index i +# this properly tests row-shift convention: row i of a MultilineMPO maps row i of the network onto row i + 1 +function multiline_ψ(AT) + return adapt(AT, MultilineMPS([product_mps(1), product_mps(2), product_mps(3)])) +end +function multiline_O(AT) + return adapt( + AT, MultilineMPO( + [ + perm_mpo([2, 3, 1]), # row 1: |1⟩ -> |2⟩ + perm_mpo([1, 3, 2]), # row 2: |2⟩ -> |3⟩ + perm_mpo([3, 2, 1]), # row 3: |3⟩ -> |1⟩ + ] + ) + ) +end + +function mpo_multiline_mpo_times_mps(AT; fast = false) + ψ = multiline_ψ(AT) + O = multiline_O(AT) + return @testset "MultilineMPO * InfiniteMPS ($AT)" begin + for i in 1:3 + @test abs(dot(ψ[i + 1], O[i] * ψ[i])) ≈ 1 atol = 1.0e-10 + @test abs(dot(ψ[i], O[i] * ψ[i])) ≈ 0 atol = 1.0e-10 + end + + @test abs(dot(ψ[1], O * ψ[1])) ≈ 1 atol = 1.0e-10 + @test abs(dot(ψ[2], O * ψ[2])) ≈ 1 atol = 1.0e-10 # cyclic period + + # the order matters, here testing reverse order not returning to ψ[1] + reversed = foldl((st, i) -> O[i] * st, 3:-1:1; init = ψ[1]) + @test abs(dot(ψ[1], reversed)) ≈ 0 atol = 1.0e-10 + end +end + +function mpo_multiline_leading_eigenvalue(AT; fast = false) + ψ = multiline_ψ(AT) + O = multiline_O(AT) + return @testset "leading_eigenvalue ($AT)" begin + @test leading_eigenvalue(ψ, O) ≈ 1 atol = 1.0e-10 + + # row-accumulated eigenvalues + scaled(mpo, c) = InfiniteMPO([c * mpo[i] for i in 1:length(mpo)]) + cs = (2.0, 3.0, 5.0) + O_rows = MultilineMPO([scaled(O[i], cs[i]) for i in 1:3]) + @test leading_eigenvalue(ψ, O_rows) ≈ prod(cs) atol = 1.0e-8 + + # column-accumulated eigenvalues + id_mpo = adapt(AT, perm_mpo([1, 2, 3])) # identity operator, so anything's a fixed point + cs = (2.0, 3.0) + O_cols = MultilineMPO([InfiniteMPO([cs[1] * id_mpo[1], cs[2] * id_mpo[1]])]) + ψ_cols = adapt(AT, MultilineMPS([InfiniteMPS([P, P], [ℂ^2, ℂ^2])])) + @test leading_eigenvalue(ψ_cols, O_cols) ≈ prod(cs) atol = 1.0e-8 + end +end + +function mpo_multiline_multirow_fixed_point(AT; fast = false) + return @testset "multi-row fixed point ($AT)" begin + # stacking the same row twice squares the eigenvalue of the single-row fixed point + O₁ = adapt(AT, classical_ising(; β = 0.5)) + alg = VUMPS(; tol = 1.0e-10, verbosity = 0) + ψ₁, = leading_boundary(adapt(AT, InfiniteMPS(ℂ^2, ℂ^10)), O₁, alg) + λ₁ = leading_eigenvalue(ψ₁, O₁) + + O₂ = MultilineMPO([O₁, O₁]) + ψ₂₀ = adapt(AT, MultilineMPS([InfiniteMPS(ℂ^2, ℂ^10), InfiniteMPS(ℂ^2, ℂ^10)])) + ψ₂, = leading_boundary(ψ₂₀, O₂, alg) + @test leading_eigenvalue(ψ₂, O₂) ≈ λ₁^2 rtol = 1.0e-8 + end +end + +function mpo_multiline_unsupported_inputs(AT; fast = false) + ψ = multiline_ψ(AT) + O = multiline_O(AT) + return @testset "unsupported inputs ($AT)" begin + @test_throws MethodError expectation_value(ψ, O) + + # Hamiltonian lines are a valid `MultilineMPO`, but not a transfer operator + H = adapt(AT, transverse_field_ising()) + ψ_H = adapt(AT, MultilineMPS([InfiniteMPS(ℂ^2, ℂ^4), InfiniteMPS(ℂ^2, ℂ^4)])) + O_H = MultilineMPO([H, H]) + @test O_H isa MultilineMPO && !(O_H isa InfiniteMultilineMPO) + @test_throws MethodError expectation_value(ψ_H, O_H) + @test_throws MethodError leading_eigenvalue(ψ_H, O_H) + + ψ_fin = adapt(AT, MultilineMPS([FiniteMPS(4, P, ℂ^2)])) + @test_throws MethodError leading_boundary( + ψ_fin, adapt(AT, MultilineMPO([perm_mpo([1, 2, 3])])), VUMPS() + ) + end +end + +function mpo_multiline_line_aliases(AT; fast = false) + ψ = multiline_ψ(AT) + O = multiline_O(AT) + return @testset "line aliases ($AT)" begin + @test ψ isa InfiniteMultilineMPS + @test !(ψ isa FiniteMultilineMPS) + @test O isa InfiniteMultilineMPO + ψ_fin = adapt(AT, MultilineMPS([FiniteMPS(4, P, ℂ^2)])) + @test ψ_fin isa FiniteMultilineMPS && ψ_fin isa MultilineMPS + O_fin = adapt(AT, MultilineMPO([FiniteMPO(fill(perm_mpo([1, 2, 3])[1], 4))])) + @test O_fin isa FiniteMultilineMPO && O_fin isa MultilineMPO + end +end + +function mpo_multiline_view_bounds(AT; fast = false) + ψ = multiline_ψ(AT) + return @testset "view bounds ($AT)" begin + # rows are periodic, and so are the columns of infinite lines + @test checkbounds(Bool, ψ.AC, 0, 0) && checkbounds(Bool, ψ.C, 4, -1) + ψ_fin = adapt(AT, MultilineMPS([FiniteMPS(4, P, ℂ^2), FiniteMPS(4, P, ℂ^2)])) + @test checkbounds(Bool, ψ_fin.AC, 3, 4) && !checkbounds(Bool, ψ_fin.AC, 1, 5) + @test checkbounds(Bool, ψ_fin.C, 1, 0) && !checkbounds(Bool, ψ_fin.C, 1, 5) + end +end diff --git a/test/testsuite/operators/lazysum.jl b/test/testsuite/operators/lazysum.jl new file mode 100644 index 000000000..0c051558b --- /dev/null +++ b/test/testsuite/operators/lazysum.jl @@ -0,0 +1,278 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: _transpose_front, _transpose_tail, C_hamiltonian, AC_hamiltonian, AC2_hamiltonian +using TensorKit +using VectorInterface: One +using Adapt + +export operators_lazysum_general, operators_lazysum_multipliedoperator, + operators_lazysum_timedependent, operators_lazysum_hamiltonian + +function operators_lazysum_general(AT; fast = false) + return @testset "General LazySum ($AT) of $(eltype(Os))" for Os in ( + rand(ComplexF64, rand(1:10)), + map(i -> adapt(AT, rand(ComplexF64, ℂ^13, ℂ^7)), 1:rand(1:10)), + map(i -> adapt(AT, rand(ComplexF64, ℂ^1 ⊗ ℂ^2, ℂ^3 ⊗ ℂ^4)), 1:rand(1:10)), + ) + LazyOs = LazySum(Os) + + #test user interface + summed = sum(Os) + + @test sum(LazyOs) ≈ summed atol = 1 - 08 + + LazyOs_added = +(LazyOs, Os...) + + @test sum(LazyOs_added) ≈ 2 * summed atol = 1 - 08 + end +end + +function operators_lazysum_multipliedoperator(AT; fast = false) + return @testset "MultipliedOperator ($AT) of $(typeof(O)) with $(typeof(f))" for (O, f) in + zip( + ( + rand(ComplexF64), + adapt(AT, rand(ComplexF64, ℂ^13, ℂ^7)), + adapt(AT, rand(ComplexF64, ℂ^1 ⊗ ℂ^2, ℂ^3 ⊗ ℂ^4)), + ), + (t -> 3t, 1.1, One()) + ) + tmp = MPSKit.MultipliedOperator(O, f) + if tmp isa TimedOperator + @test tmp(1.1)() ≈ f(1.1) * O atol = 1 - 08 + elseif tmp isa UntimedOperator + @test tmp() ≈ f * O atol = 1 - 08 + end + end +end + +function operators_lazysum_timedependent(AT; fast = false) + return @testset "General Time-dependent LazySum ($AT) of $(eltype(Os))" for Os in ( + rand(ComplexF64, 4), + fill(adapt(AT, rand(ComplexF64, ℂ^13, ℂ^7)), 4), + fill(adapt(AT, rand(ComplexF64, ℂ^1 ⊗ ℂ^2, ℂ^3 ⊗ ℂ^4)), 4), + ) + + #test user interface + fs = [t -> 3t, t -> t + 2, 4, 1] + Ofs = map(zip(fs, Os)) do (f, O) + if f == 1 + return O + else + return MPSKit.MultipliedOperator(O, f) + end + end + LazyOs = LazySum(Ofs) + summed = sum(zip(fs, Os)) do (f, O) + if f isa Function + f(1.1) * O + else + f * O + end + end + + @test sum(LazyOs(1.1)) ≈ summed atol = 1 - 08 + + LazyOs_added = +(LazyOs, Ofs...) + + @test sum(LazyOs_added(1.1)) ≈ 2 * summed atol = 1 - 08 + end +end + +# LazySum/MultipliedOperator only wrap arithmetic/dispatch bookkeeping around a Hamiltonian; +# they don't exercise sector-specific fusion math themselves (that's covered by +# hamiltonian/infinite.jl and symmetries/), so testing planar (no symmetry) + U1 +# (abelian) is sufficient here without also paying to compile the SU2 (non-abelian) case. +function operators_lazysum_hamiltonian(AT; fast = false) + pspaces = PSPACES_TRIPLE[1:2] + vspaces = VSPACES_TRIPLE[1:2] + if fast + pspaces = pspaces[1:1] + vspaces = vspaces[1:1] + end + + return @testset "LazySum of (effective) Hamiltonian ($AT) $(sectortype(pspace))" for (pspace, Dspace) in + zip(pspaces, vspaces) + Os = map(1:3) do i + O = adapt(AT, rand(ComplexF64, pspace^i, pspace^i)) + return O += O' + end + fs = [t -> 3t, 2, 1] + + @testset "LazySum FiniteMPOHamiltonian" begin + L = rand(3:2:20) + ψ = adapt(AT, FiniteMPS(rand, ComplexF64, L, pspace, Dspace)) + lattice = fill(pspace, L) + Hs = map(enumerate(Os)) do (i, O) + return FiniteMPOHamiltonian( + lattice, + ntuple(x -> x + j, i) => O for j in 0:(L - i) + ) + end + summedH = LazySum(Hs) + + envs = map(H -> environments(ψ, H, ψ), Hs) + summed_envs = environments(ψ, summedH, ψ) + + expval = sum(zip(Hs, envs)) do (H, env) + return expectation_value(ψ, H, env) + end + expval1 = expectation_value(ψ, sum(summedH)) + expval2 = expectation_value(ψ, summedH, summed_envs) + expval3 = expectation_value(ψ, summedH) + @test expval ≈ expval1 + @test expval ≈ expval2 + @test expval ≈ expval3 + + # test derivatives + summedhct = C_hamiltonian(1, ψ, summedH, ψ, summed_envs) + sum1 = sum(zip(Hs, envs)) do (H, env) + return C_hamiltonian(1, ψ, H, ψ, env)(ψ.C[1]) + end + @test summedhct(ψ.C[1], 0.0) ≈ sum1 + + summedhct = AC_hamiltonian(1, ψ, summedH, ψ, summed_envs) + sum2 = sum(zip(Hs, envs)) do (H, env) + return AC_hamiltonian(1, ψ, H, ψ, env)(ψ.AC[1]) + end + @test summedhct(ψ.AC[1], 0.0) ≈ sum2 + + v = _transpose_front(ψ.AC[1]) * _transpose_tail(ψ.AR[2]) + summedhct = AC2_hamiltonian(1, ψ, summedH, ψ, summed_envs) + sum3 = sum(zip(Hs, envs)) do (H, env) + return AC2_hamiltonian(1, ψ, H, ψ, env)(v) + end + @test summedhct(v, 0.0) ≈ sum3 + + Hts = [MultipliedOperator(Hs[1], fs[1]), MultipliedOperator(Hs[2], fs[2]), Hs[3]] + summedH = LazySum(Hts) + t = 1.1 + summedH_at = summedH(t) + + envs = map(H -> environments(ψ, H, ψ), Hs) + summed_envs = environments(ψ, summedH, ψ) + + expval = sum(zip(fs, Hs, envs)) do (f, H, env) + return (f isa Function ? f(t) : f) * expectation_value(ψ, H, env) + end + expval1 = expectation_value(ψ, sum(summedH_at)) + expval2 = expectation_value(ψ, summedH_at, summed_envs) + expval3 = expectation_value(ψ, summedH_at) + @test expval ≈ expval1 + @test expval ≈ expval2 + @test expval ≈ expval3 + + # test derivatives + summedhct = C_hamiltonian(1, ψ, summedH, ψ, summed_envs) + sum1 = sum(zip(fs, Hs, envs)) do (f, H, env) + if f isa Function + f = f(t) + end + return f * C_hamiltonian(1, ψ, H, ψ, env)(ψ.C[1]) + end + @test summedhct(ψ.C[1], t) ≈ sum1 + + summedhct = AC_hamiltonian(1, ψ, summedH, ψ, summed_envs) + sum2 = sum(zip(fs, Hs, envs)) do (f, H, env) + if f isa Function + f = f(t) + end + return f * AC_hamiltonian(1, ψ, H, ψ, env)(ψ.AC[1]) + end + @test summedhct(ψ.AC[1], t) ≈ sum2 + + v = _transpose_front(ψ.AC[1]) * _transpose_tail(ψ.AR[2]) + summedhct = AC2_hamiltonian(1, ψ, summedH, ψ, summed_envs) + sum3 = sum(zip(fs, Hs, envs)) do (f, H, env) + return (f isa Function ? f(t) : f) * AC2_hamiltonian(1, ψ, H, ψ, env)(v) + end + @test summedhct(v, t) ≈ sum3 + end + + @testset "LazySum InfiniteMPOHamiltonian" begin + ψ = adapt(AT, repeat(InfiniteMPS(pspace, Dspace), 2)) + Hs = map(Os) do O + H = InfiniteMPOHamiltonian(O) + return repeat(H, 2) + end + summedH = LazySum(Hs) + envs = map(H -> environments(ψ, H, ψ), Hs) + summed_envs = environments(ψ, summedH, ψ) + + expval = sum(zip(Hs, envs)) do (H, Env) + return expectation_value(ψ, H, Env) + end + expval1 = expectation_value(ψ, sum(summedH)) + expval2 = expectation_value(ψ, summedH, summed_envs) + expval3 = expectation_value(ψ, summedH) + @test expval ≈ expval1 + @test expval ≈ expval2 + @test expval ≈ expval3 + + # test derivatives + summedhct = C_hamiltonian(1, ψ, summedH, ψ, summed_envs) + sum1 = sum(zip(Hs, envs)) do (H, env) + return C_hamiltonian(1, ψ, H, ψ, env)(ψ.C[1]) + end + @test summedhct(ψ.C[1], 0.0) ≈ sum1 + + summedhct = AC_hamiltonian(1, ψ, summedH, ψ, summed_envs) + sum2 = sum(zip(Hs, envs)) do (H, env) + return AC_hamiltonian(1, ψ, H, ψ, env)(ψ.AC[1]) + end + @test summedhct(ψ.AC[1], 0.0) ≈ sum2 + + v = _transpose_front(ψ.AC[1]) * _transpose_tail(ψ.AR[2]) + summedhct = AC2_hamiltonian(1, ψ, summedH, ψ, summed_envs) + sum3 = sum(zip(Hs, envs)) do (H, env) + return AC2_hamiltonian(1, ψ, H, ψ, env)(v) + end + @test summedhct(v, 0.0) ≈ sum3 + + Hts = [MultipliedOperator(Hs[1], fs[1]), MultipliedOperator(Hs[2], fs[2]), Hs[3]] + summedH = LazySum(Hts) + t = 1.1 + summedH_at = summedH(t) + + envs = map(H -> environments(ψ, H, ψ), Hs) + summed_envs = environments(ψ, summedH, ψ) + + expval = sum(zip(fs, Hs, envs)) do (f, H, env) + return (f isa Function ? f(t) : f) * expectation_value(ψ, H, env) + end + expval1 = expectation_value(ψ, sum(summedH_at)) + expval2 = expectation_value(ψ, summedH_at, summed_envs) + expval3 = expectation_value(ψ, summedH_at) + @test expval ≈ expval1 + @test expval ≈ expval2 + @test expval ≈ expval3 + + # test derivatives + summedhct = C_hamiltonian(1, ψ, summedH, ψ, summed_envs) + sum1 = sum(zip(fs, Hs, envs)) do (f, H, env) + if f isa Function + f = f(t) + end + return f * C_hamiltonian(1, ψ, H, ψ, env)(ψ.C[1]) + end + @test summedhct(ψ.C[1], t) ≈ sum1 + + summedhct = AC_hamiltonian(1, ψ, summedH, ψ, summed_envs) + sum2 = sum(zip(fs, Hs, envs)) do (f, H, env) + if f isa Function + f = f(t) + end + return f * AC_hamiltonian(1, ψ, H, ψ, env)(ψ.AC[1]) + end + @test summedhct(ψ.AC[1], t) ≈ sum2 + + v = _transpose_front(ψ.AC[1]) * _transpose_tail(ψ.AR[2]) + summedhct = AC2_hamiltonian(1, ψ, summedH, ψ, summed_envs) + sum3 = sum(zip(fs, Hs, envs)) do (f, H, env) + return (f isa Function ? f(t) : f) * AC2_hamiltonian(1, ψ, H, ψ, env)(v) + end + @test summedhct(v, t) ≈ sum3 + end + end +end diff --git a/test/testsuite/operators/projection.jl b/test/testsuite/operators/projection.jl new file mode 100644 index 000000000..2d2011b5a --- /dev/null +++ b/test/testsuite/operators/projection.jl @@ -0,0 +1,20 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Adapt + +export operators_projection + +function operators_projection(AT; fast = false) + return @testset "ProjectionOperator ($AT)" begin + L = 10 + psi = adapt(AT, FiniteMPS(rand, ComplexF64, L, ℙ^2, ℙ^2)) + psi2 = adapt(AT, FiniteMPS(rand, ComplexF64, L, ℙ^2, ℙ^2)) + O = MPSKit.ProjectionOperator(psi) + + @test expectation_value(psi, O) ≈ 1.0 + @test expectation_value(psi2, O) ≈ dot(psi, psi2) * dot(psi2, psi) + end +end diff --git a/test/testsuite/operators/window.jl b/test/testsuite/operators/window.jl new file mode 100644 index 000000000..fe9399174 --- /dev/null +++ b/test/testsuite/operators/window.jl @@ -0,0 +1,66 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Adapt + +export operators_window + +function operators_window(AT; fast = false) + return @testset "WindowMPOHamiltonian ($AT)" begin + # a uniform state, represented with a two-site unit cell, so that carving out a window + # at different offsets exercises the (non-trivial) circshift alignment of the infinite + # environments while the physics stays translationally invariant. + H1 = adapt(AT, force_planar(transverse_field_ising(; g = 1.5))) + gs1, = find_groundstate(adapt(AT, InfiniteMPS([ℙ^2], [ℙ^12])), H1, VUMPS(; verbosity = 0)) + gs = repeat(gs1, 2) + H = repeat(H1, 2) + + L = 6 + + @testset "interval offset (circshift)" begin + # the window energy must be independent of the offset for a uniform state + energies = map((1:L, 2:(L + 1), 3:(L + 2))) do interval + ψ = WindowMPS(gs, interval) + Hw = WindowMPOHamiltonian(H, interval) + @test length(Hw) == length(interval) + # the window itself is finite, even though its environments are not + @test isfinite(Hw) + @test isfinite(typeof(Hw)) + @test isfinite(Hw) == isfinite(typeof(Hw)) + return expectation_value(ψ, Hw) + end + @test energies[1] ≈ energies[2] atol = 1.0e-8 + @test energies[1] ≈ energies[3] atol = 1.0e-8 + end + + @testset "linear algebra" begin + ψ = WindowMPS(gs, 1:L) + Hw = WindowMPOHamiltonian(H, 1:L) + e = expectation_value(ψ, Hw) + + @test expectation_value(ψ, Hw + Hw) ≈ 2 * e atol = 1.0e-8 + @test expectation_value(ψ, Hw - Hw) ≈ 0 atol = 1.0e-8 + @test expectation_value(ψ, 3 * Hw) ≈ 3 * e atol = 1.0e-8 + @test expectation_value(ψ, Hw * 3) ≈ 3 * e atol = 1.0e-8 + @test expectation_value(ψ, -Hw) ≈ -e atol = 1.0e-8 + end + + @testset "distinct summands" begin + # add two genuinely different Hamiltonians: the ZZ Ising string has a single bulk + # level while the XY hopping needs two, so the boundary/bulk virtual spaces of the + # two summands differ and have to be block-diagonalized consistently. + ψ = WindowMPS(gs1, 1:L) + Ha = WindowMPOHamiltonian(H1, 1:L) + Hb = WindowMPOHamiltonian(adapt(AT, force_planar(XY_model(; g = 0.7))), 1:L) + @test right_virtualspace(Ha.finite_ham, 3) != right_virtualspace(Hb.finite_ham, 3) + + ea = expectation_value(ψ, Ha) + eb = expectation_value(ψ, Hb) + @test expectation_value(ψ, Ha + Hb) ≈ ea + eb atol = 1.0e-8 + @test expectation_value(ψ, Hb + Ha) ≈ ea + eb atol = 1.0e-8 + @test expectation_value(ψ, Ha - Hb) ≈ ea - eb atol = 1.0e-8 + end + end +end diff --git a/test/testsuite/states/arithmetic.jl b/test/testsuite/states/arithmetic.jl new file mode 100644 index 000000000..df35f16b5 --- /dev/null +++ b/test/testsuite/states/arithmetic.jl @@ -0,0 +1,264 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: _transpose_front, _transpose_tail, left_gauge +using TensorKit +using TensorKit: PlanarTrivial, ℙ +using Adapt + +export states_arithmetic_lazy_gauging, states_arithmetic_dense_crosscheck, + states_arithmetic_argument_checks, states_arithmetic_cancellation_across_gauges, + states_arithmetic_eigenstate_cancellation, states_arithmetic_single_site_addition, + states_arithmetic_scalartype_promotion + +""" + gauge_error(ψ::FiniteMPS) + +Largest relative violation of the defining gauge relations `AL[i] * C[i] == AC[i] == C[i-1] * AR[i]`. +""" +function gauge_error(ψ::FiniteMPS) + err = zero(real(scalartype(ψ))) + for i in 1:length(ψ) + AC = ψ.AC[i] + err = max(err, norm(ψ.AL[i] * ψ.C[i] - AC) / norm(AC)) + err = max( + err, + norm(_transpose_front(ψ.C[i - 1] * _transpose_tail(ψ.AR[i])) - AC) / norm(AC) + ) + end + return err +end + +""" + flip_gauge!(ψ::FiniteMPS) + +Re-gauge site 1 into an equally valid but different gauge (a sign flip) and materialise `AC[1]`. +This reproduces the cache state left behind by e.g. DMRG, which installs `AL, C` from a truncated +SVD while the lazy gauge sweep re-derives them from a positive QR. +""" +function flip_gauge!(ψ::FiniteMPS) + AL, C, = left_gauge(ψ.AC[1]) + ψ.AC[1] = (-AL, -C) + ψ.AC[1] + return ψ +end + +# (virtual space, second virtual space, physical space, scalartype) +const arithmetic_spaces = [ + (ℙ^8, ℙ^5, ℙ^2, ComplexF64), + ( + Rep[U₁](-1 => 2, 0 => 2, 1 => 2), Rep[U₁](-1 => 1, 0 => 3, 1 => 1), + Rep[U₁](-1 => 1, 0 => 1, 1 => 1), ComplexF64, + ), + # integer spin, so that a trivial-sector state exists for every chain length + ( + Rep[SU₂](0 => 2, 1 => 2, 2 => 1), Rep[SU₂](0 => 1, 1 => 3), + Rep[SU₂](1 => 1), ComplexF64, + ), +] + +function states_arithmetic_lazy_gauging(AT; fast = false) + return @testset "lazy gauging is a cache ($AT, $(sectortype(D)))" for (D, _, d, elt) in + arithmetic_spaces + for L in (4, 6) + # sweeping right must not change tensors already handed out + ψ = flip_gauge!(adapt(AT, FiniteMPS(rand, elt, L, d, D))) + n = norm(ψ) + AL₁, C₀ = copy(ψ.AL[1]), copy(ψ.C[1]) + ψ.AL[2] # triggers a gauge-center move + @test ψ.AL[1] ≈ AL₁ + @test ψ.C[1] ≈ C₀ + @test norm(ψ) ≈ n + @test gauge_error(ψ) ≤ sqrt(eps(real(elt))) + + # ... and neither must sweeping back left + ϕ = flip_gauge!(adapt(AT, FiniteMPS(rand, elt, L, d, D))) + ϕ.C[L - 1] + ARₗ, Cₗ = copy(ϕ.AR[L]), copy(ϕ.C[L - 1]) + ϕ.AR[2] + @test ϕ.AR[L] ≈ ARₗ + @test ϕ.C[L - 1] ≈ Cₗ + @test gauge_error(ϕ) ≤ sqrt(eps(real(elt))) + end + end +end + +function states_arithmetic_dense_crosscheck(AT; fast = false) + return @testset "FiniteMPS ± dense cross-check ($AT, $(sectortype(D)), L=$L)" for (D, D₂, d, elt) in + arithmetic_spaces, + # `convert(TensorMap, ψ)` compiles per (sectortype, length), so only sweep `L` once. + # `L = 1` is only swept for the trivial sector: a single spin-1 site carries no singlet. + L in (sectortype(D) === PlanarTrivial ? (1, 2, 3, 4, 5) : (4,)) + + ψ₁ = adapt(AT, FiniteMPS(rand, elt, L, d, D)) + ψ₂ = adapt(AT, FiniteMPS(rand, elt, L, d, D)) + ψ₃ = adapt(AT, FiniteMPS(rand, elt, L, d, D₂)) # mismatched bond dimensions + t₁, t₂, t₃ = convert.(TensorMap, (ψ₁, ψ₂, ψ₃)) + atol = sqrt(eps(real(elt))) + + @test convert(TensorMap, ψ₁ + ψ₂) ≈ t₁ + t₂ atol = atol + @test convert(TensorMap, ψ₁ - ψ₂) ≈ t₁ - t₂ atol = atol + @test convert(TensorMap, ψ₁ + ψ₃) ≈ t₁ + t₃ atol = atol + @test convert(TensorMap, ψ₃ - ψ₁) ≈ t₃ - t₁ atol = atol + @test norm(ψ₁ + ψ₂) ≈ norm(t₁ + t₂) atol = atol + @test dot(ψ₁ + ψ₂, ψ₁ - ψ₂) ≈ dot(t₁ + t₂, t₁ - t₂) atol = atol + + # scaling is distributive over addition + @test convert(TensorMap, 2 * ψ₁ + 3 * ψ₂) ≈ 2 * t₁ + 3 * t₂ atol = atol + end +end + +function states_arithmetic_argument_checks(AT; fast = false) + return @testset "FiniteMPS argument checks ($AT)" begin + @test_throws DimensionMismatch adapt(AT, FiniteMPS(rand, ComplexF64, 4, ℂ^2, ℂ^4)) + + adapt(AT, FiniteMPS(rand, ComplexF64, 5, ℂ^2, ℂ^4)) + # mismatched boundary virtual spaces, at length 1 and beyond + onesite(V₁, V₂) = adapt( + AT, FiniteMPS( + rand, ComplexF64, [ℂ^2], ComplexSpace[]; left = V₁, right = V₂, normalize = false + ) + ) + @test_throws SpaceMismatch onesite(ℂ^3, ℂ^1) + onesite(ℂ^2, ℂ^1) + @test_throws SpaceMismatch onesite(ℂ^1, ℂ^3) + onesite(ℂ^1, ℂ^2) + end +end + +function states_arithmetic_cancellation_across_gauges(AT; fast = false) + return @testset "FiniteMPS cancellation across gauges ($AT, $(sectortype(D)))" for (D, _, d, elt) in + arithmetic_spaces + # same vector, different tensor network: re-canonicalised through a dense tensor + ψ₄ = adapt(AT, FiniteMPS(rand, elt, 4, d, D)) + @test norm(ψ₄ - FiniteMPS(convert(TensorMap, ψ₄))) ≈ 0 atol = + sqrt(eps(real(elt))) * norm(ψ₄) + + for L in 2:6 + ψ = adapt(AT, FiniteMPS(rand, elt, L, d, D)) + atol = sqrt(eps(real(elt))) * norm(ψ) + + # same vector, different *gauge history* -- this is issue #473 + χ = flip_gauge!(copy(ψ)) + @test norm(ψ - χ) ≈ 0 atol = atol + @test norm(χ - ψ) ≈ 0 atol = atol + @test norm(2 * ψ - χ - ψ) ≈ 0 atol = atol + @test norm(3 * ψ - χ) ≈ 2 * norm(ψ) atol = atol + end + end +end + +function states_arithmetic_eigenstate_cancellation(AT; fast = false) + return @testset "eigenstate cancellation, issue #473 ($AT, L=$L)" for L in (4, 6) + # `gs` is an eigenstate of `H`, so `E₀ * gs - H * gs` must be the zero vector, even though the + # two operands are carried by completely different tensor networks. + H = adapt(AT, transverse_field_ising(; g = 1.0, L = L)) + ψ₀ = adapt(AT, FiniteMPS(rand, ComplexF64, L, physicalspace(H, 1), ℂ^16)) + gs, envs, = find_groundstate(ψ₀, H, DMRG(; verbosity = 0)) + E₀ = real(expectation_value(gs, H, envs)) + atol = 1.0e-8 * abs(E₀) + + a = E₀ * gs + b = H * gs + @test convert(TensorMap, a) ≈ convert(TensorMap, b) atol = atol # the operands agree + + @test norm(a - b) ≈ 0 atol = atol + @test norm(convert(TensorMap, a - b)) ≈ 0 atol = atol + @test norm(b - a) ≈ 0 atol = atol + + # ... independent of whether the operands were gauged before the subtraction + a′, b′ = E₀ * gs, H * gs + convert(TensorMap, a′) + convert(TensorMap, b′) + @test norm(a′ - b′) ≈ 0 atol = atol + + # a non-trivial linear combination is still correct + @test norm(3 * (E₀ * gs) - H * gs) ≈ 2 * abs(E₀) * norm(gs) atol = atol + end +end + +function states_arithmetic_single_site_addition(AT; fast = false) + return @testset "single-site addition ($AT)" begin + atol = sqrt(eps(Float64)) + + # trivial boundaries: cross-check against the dense state + ψ₁ = adapt(AT, FiniteMPS(rand, ComplexF64, 1, ℂ^3, ℂ^4)) + ψ₂ = adapt(AT, FiniteMPS(rand, ComplexF64, 1, ℂ^3, ℂ^4)) + t₁, t₂ = convert(TensorMap, ψ₁), convert(TensorMap, ψ₂) + @test length(ψ₁ + ψ₂) == 1 + @test convert(TensorMap, ψ₁ + ψ₂) ≈ t₁ + t₂ atol = atol + @test convert(TensorMap, ψ₁ - ψ₂) ≈ t₁ - t₂ atol = atol + @test norm(ψ₁ + ψ₂) ≈ norm(t₁ + t₂) atol = atol + @test norm(ψ₁ - ψ₁) ≈ 0 atol = atol + @test gauge_error(ψ₁ + ψ₂) ≤ atol + + # non-trivial boundaries, e.g. a single-site window carved out of a larger chain. There is no + # dense state to compare against, but the whole state *is* the one center tensor. + onesite(V₁, V₂) = adapt( + AT, FiniteMPS( + rand, ComplexF64, [ℂ^2], ComplexSpace[]; left = V₁, right = V₂, normalize = false + ) + ) + ϕ₁, ϕ₂ = onesite(ℂ^3, ℂ^4), onesite(ℂ^3, ℂ^4) + AC₁, AC₂ = copy(ϕ₁.AC[1]), copy(ϕ₂.AC[1]) + @test (ϕ₁ + ϕ₂).AC[1] ≈ AC₁ + AC₂ atol = atol + @test (ϕ₁ - ϕ₂).AC[1] ≈ AC₁ - AC₂ atol = atol + @test left_virtualspace(ϕ₁ + ϕ₂, 1) == ℂ^3 + @test right_virtualspace(ϕ₁ + ϕ₂, 1) == ℂ^4 + @test norm(ϕ₁ + ϕ₂) ≈ norm(AC₁ + AC₂) atol = atol + @test gauge_error(ϕ₁ + ϕ₂) ≤ atol + + # the operands are left untouched + @test ϕ₁.AC[1] ≈ AC₁ && ϕ₂.AC[1] ≈ AC₂ + + # ... and the same for a single-site `FiniteMPO` + o₁, o₂ = adapt(AT, rand(ComplexF64, ℂ^3 ← ℂ^3)), adapt(AT, rand(ComplexF64, ℂ^3 ← ℂ^3)) + O₁, O₂ = FiniteMPO(o₁), FiniteMPO(o₂) + @test length(O₁) == 1 + # `convert` on a single-site MPO used to contract `mpo[1]` with `mpo[end]`, the same tensor + @test convert(TensorMap, O₁) ≈ o₁ atol = atol + @test length(O₁ + O₂) == 1 + @test (O₁ + O₂)[1] ≈ O₁[1] + O₂[1] atol = atol + @test (O₁ - O₂)[1] ≈ O₁[1] - O₂[1] atol = atol + @test convert(TensorMap, O₁ + O₂) ≈ o₁ + o₂ atol = atol + @test convert(TensorMap, O₁ - O₂) ≈ o₁ - o₂ atol = atol + + # scalar types promote at length 1 too + @test scalartype( + adapt(AT, FiniteMPS(rand, Float64, 1, ℂ^3, ℂ^4)) + + adapt(AT, FiniteMPS(rand, ComplexF64, 1, ℂ^3, ℂ^4)) + ) == + ComplexF64 + @test scalartype( + FiniteMPO(adapt(AT, rand(Float64, ℂ^3 ← ℂ^3))) + + FiniteMPO(adapt(AT, rand(ComplexF64, ℂ^3 ← ℂ^3))) + ) == + ComplexF64 + end +end + +function states_arithmetic_scalartype_promotion(AT; fast = false) + return @testset "scalar-type promotion ($AT)" begin + L = 4 + + ψᵣ = adapt(AT, FiniteMPS(rand, Float64, L, ℂ^2, ℂ^4)) + ψᶜ = adapt(AT, FiniteMPS(rand, ComplexF64, L, ℂ^2, ℂ^4)) + tᵣ, tᶜ = convert(TensorMap, ψᵣ), convert(TensorMap, ψᶜ) + + @test scalartype(ψᵣ + ψᶜ) == ComplexF64 + @test scalartype(ψᶜ + ψᵣ) == ComplexF64 + @test convert(TensorMap, ψᵣ + ψᶜ) ≈ tᵣ + tᶜ + @test convert(TensorMap, ψᶜ + ψᵣ) ≈ tᶜ + tᵣ + @test scalartype(ψᵣ - ψᶜ) == ComplexF64 + @test convert(TensorMap, ψᵣ - ψᶜ) ≈ tᵣ - tᶜ + # both operands real stays real + @test scalartype(ψᵣ + ψᵣ) == Float64 + + Oᵣ = FiniteMPO(adapt(AT, rand(Float64, (ℂ^2)^L ← (ℂ^2)^L))) + Oᶜ = FiniteMPO(adapt(AT, rand(ComplexF64, (ℂ^2)^L ← (ℂ^2)^L))) + oᵣ, oᶜ = convert(TensorMap, Oᵣ), convert(TensorMap, Oᶜ) + + @test scalartype(Oᵣ + Oᶜ) == ComplexF64 + @test scalartype(Oᶜ + Oᵣ) == ComplexF64 + @test convert(TensorMap, Oᵣ + Oᶜ) ≈ oᵣ + oᶜ + @test convert(TensorMap, Oᶜ + Oᵣ) ≈ oᶜ + oᵣ + @test scalartype(Oᵣ + Oᵣ) == Float64 + end +end diff --git a/test/testsuite/states/finitemps.jl b/test/testsuite/states/finitemps.jl new file mode 100644 index 000000000..44a892aed --- /dev/null +++ b/test/testsuite/states/finitemps.jl @@ -0,0 +1,173 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: _transpose_front, _transpose_tail +using MPSKit: GeometryStyle, FiniteChainStyle +using TensorKit +using TensorKit: ℙ +using Adapt + +# `@constinferred` evaluates a helper at global scope, which is not allowed inside a function +# body, so the testsuite functions use `@testinferred` (none of the calls have constant arguments) + +export states_finitemps_basics, states_finitemps_complex, states_finitemps_center_indexing, + states_finitemps_gauging_stackoverflow, states_finitemps_copying, states_finitemps_entropy + +function states_finitemps_basics(AT; fast = false) + return @testset "FiniteMPS ($AT, $(sectortype(D)), $elt)" for (D, d, elt) in [ + (ℙ^10, ℙ^2, ComplexF64), + ( + Rep[SU₂](1 => 1, 0 => 3), + Rep[SU₂](0 => 1) * Rep[SU₂](0 => 1), + ComplexF32, + ), + ] + L = rand(3:20) + ψ = adapt(AT, FiniteMPS(rand, elt, L, d, D)) + + @test eachindex(IndexLinear(), ψ) == eachindex(ψ) + @test isfinite(ψ) + @test isfinite(typeof(ψ)) + @test isfinite(ψ) == isfinite(typeof(ψ)) + @test GeometryStyle(typeof(ψ)) == FiniteChainStyle() + @test GeometryStyle(ψ) == FiniteChainStyle() + @test @testinferred physicalspace(ψ) == fill(d, L) + @test all(x -> x ≾ D, @testinferred left_virtualspace(ψ)) + @test all(x -> x ≾ D, @testinferred right_virtualspace(ψ)) + + @test eltype(ψ) == eltype(typeof(ψ)) + + ovl = @testinferred dot(ψ, ψ) + + @test ovl ≈ norm(ψ.AC[1])^2 + @test ovl isa scalartype(ψ) + + for i in 1:length(ψ) + @test ψ.AC[i] ≈ ψ.AL[i] * ψ.C[i] + @test ψ.AC[i] ≈ _transpose_front(ψ.C[i - 1] * _transpose_tail(ψ.AR[i])) + end + + @test elt == scalartype(ψ) + + ψ = ψ * 3 + @test ovl * 9 ≈ norm(ψ)^2 + ψ = 3 * ψ + @test ovl * 9 * 9 ≈ norm(ψ)^2 + + @test norm(2 * ψ + ψ - 3 * ψ) ≈ 0.0 atol = sqrt(eps(real(elt))) + end +end + +function states_finitemps_complex(AT; fast = false) + return @testset "FiniteMPS ($AT, $(sectortype(D)), $elt)" for (D, d, elt) in MPS_TEST_SPACES + ψ_small = adapt(AT, FiniteMPS(rand, elt, 4, d, D)) + ψ_small2 = FiniteMPS(convert(TensorMap, ψ_small)) + @test dot(ψ_small, ψ_small2) ≈ dot(ψ_small, ψ_small) + + ψ′ = @testinferred complex(ψ_small) + @test scalartype(ψ′) <: Complex + if elt <: Complex + @test ψ_small === ψ′ + else + @test norm(ψ_small) ≈ norm(ψ′) + @test complex(convert(TensorMap, ψ_small)) ≈ convert(TensorMap, ψ′) + end + end +end + +function states_finitemps_center_indexing(AT; fast = false) + return @testset "FiniteMPS center + (slice) indexing ($AT)" begin + L = 11 + ψ = adapt(AT, FiniteMPS(L, ℂ^2, ℂ^16)) + + ψ.AC[6] # moving the center to site 6 + + @test ψ.center == 6 + + @test ψ[5] == ψ.ALs[5] + @test ψ[6] == ψ.ACs[6] + @test ψ[7] == ψ.ARs[7] + + @test ψ[5:7] == [ψ.ALs[5], ψ.ACs[6], ψ.ARs[7]] + + @inferred ψ[5] + + @test_throws BoundsError ψ[0] + @test_throws BoundsError ψ[L + 1] + + ψ.C[6] = adapt(AT, randn(ComplexF64, space(ψ.C[6]))) # setting the center between sites 6 and 7 + @test ψ.center == 13 / 2 + @test ψ[5:7] == [ψ.ALs[5], ψ.ACs[6], ψ.ARs[7]] + end +end + +# Regression: invalidating a long chain of cached AL tensors (by setting AC near one end) +# used to stack overflow, since re-gauging walked the invalidated range recursively. +function states_finitemps_gauging_stackoverflow(AT; fast = false) + return @testset "FiniteMPS gauging does not stack overflow ($AT)" begin + ψ = adapt(AT, FiniteMPS(10_000, ℂ^2, ℂ^1)) + @test ψ.AR[1] isa MPSKit.MPSTensor + ψ.AC[1] = -ψ.AR[1] # force invalidation of ALs + @test ψ.AL[end] isa MPSKit.MPSTensor + end +end + +function states_finitemps_copying(AT; fast = false) + return @testset "FiniteMPS copying ($AT)" begin + L = 10 + mps1 = adapt(AT, FiniteMPS(rand, ComplexF64, L, ℂ^2, ℂ^5)) + mps2 = copy(mps1) + + @test mps1 !== mps2 + + # arrays are distinct + @test mps1.ALs !== mps2.ALs + @test mps1.ARs !== mps2.ARs + @test mps1.ACs !== mps2.ACs + @test mps1.Cs !== mps2.Cs + + # tensors are distinct but equal + for i in 1:L + @test (ismissing(mps1.ALs[i]) && ismissing(mps2.ALs[i])) || + (mps1.ALs[i] !== mps2.ALs[i] && mps1.ALs[i] == mps2.ALs[i]) + @test (ismissing(mps1.ARs[i]) && ismissing(mps2.ARs[i])) || + (mps1.ARs[i] !== mps2.ARs[i] && mps1.ARs[i] == mps2.ARs[i]) + @test (ismissing(mps1.ACs[i]) && ismissing(mps2.ACs[i])) || + (mps1.ACs[i] !== mps2.ACs[i] && mps1.ACs[i] == mps2.ACs[i]) + @test (ismissing(mps1.Cs[i]) && ismissing(mps2.Cs[i])) || + (mps1.Cs[i] !== mps2.Cs[i] && mps1.Cs[i] == mps2.Cs[i]) + end + @test (ismissing(mps1.Cs[end]) && ismissing(mps2.Cs[end])) || + mps1.Cs[end] !== mps2.Cs[end] + end +end + +function states_finitemps_entropy(AT; fast = false) + return @testset "FiniteMPS entropy ($AT, $(sectortype(D)), $elt)" for (D, d, elt) in MPS_TEST_SPACES + L = 6 + ψ = adapt(AT, FiniteMPS(rand, elt, L, d, D)) + + # entropy is non-negative at all sites + for site in 1:L + @test real(entropy(ψ, site)) >= 0 + end + + @test entropy(ψ, 0) ≈ 0 atol = 1.0e-10 # trivial left bond + @test_throws BoundsError entanglement_spectrum(ψ, -1) + @test_throws BoundsError entanglement_spectrum(ψ, L + 1) + + # entropy is consistent with entanglement_spectrum + for site in 1:L + @test entropy(ψ, site) ≈ entropy(entanglement_spectrum(ψ, site)) + end + + # entropy is zero at the right boundary (trivial bond) + @test entropy(ψ, L) ≈ 0 atol = 1.0e-10 + + # product state has zero entropy everywhere + ψ_product = adapt(AT, FiniteMPS(rand, elt, L, d, unitspace(D))) + for site in 1:L + @test entropy(ψ_product, site) ≈ 0 atol = 1.0e-10 + end + end +end diff --git a/test/testsuite/states/infinitemps.jl b/test/testsuite/states/infinitemps.jl new file mode 100644 index 000000000..43cf0547e --- /dev/null +++ b/test/testsuite/states/infinitemps.jl @@ -0,0 +1,146 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: GeometryStyle, InfiniteChainStyle, TransferMatrix +using TensorKit +using TensorKit: ℙ +using Adapt + +export states_infinitemps_basics, states_infinitemps_copying, states_infinitemps_adapt, + states_infinitemps_entropy, states_infinitemps_copying_default + +function states_infinitemps_basics(AT; fast = false) + return @testset "InfiniteMPS ($AT, $(sectortype(D)), $elt)" for (D, d, elt) in MPS_TEST_SPACES + tol = Float64(eps(real(elt)) * 100) + + ψ = adapt(AT, InfiniteMPS([rand(elt, D * d, D), rand(elt, D * d, D)]; tol)) + + @test !isfinite(typeof(ψ)) + @test !isfinite(ψ) + @test isfinite(ψ) == isfinite(typeof(ψ)) + @test GeometryStyle(typeof(ψ)) == InfiniteChainStyle() + @test GeometryStyle(ψ) == InfiniteChainStyle() + + @test eltype(ψ) == eltype(typeof(ψ)) + + @test physicalspace(ψ) == fill(d, 2) + @test all(x -> x ≾ D, left_virtualspace(ψ)) + @test all(x -> x ≾ D, right_virtualspace(ψ)) + + for i in 1:length(ψ) + @plansor difference[-1 -2; -3] := ψ.AL[i][-1 -2; 1] * ψ.C[i][1; -3] - + ψ.C[i - 1][-1; 1] * ψ.AR[i][1 -2; -3] + @test norm(difference, Inf) < tol * 10 + + @test l_LL(ψ, i) * TransferMatrix(ψ.AL[i], ψ.AL[i]) ≈ l_LL(ψ, i + 1) + @test l_LR(ψ, i) * TransferMatrix(ψ.AL[i], ψ.AR[i]) ≈ l_LR(ψ, i + 1) + @test l_RL(ψ, i) * TransferMatrix(ψ.AR[i], ψ.AL[i]) ≈ l_RL(ψ, i + 1) + @test l_RR(ψ, i) * TransferMatrix(ψ.AR[i], ψ.AR[i]) ≈ l_RR(ψ, i + 1) + + @test TransferMatrix(ψ.AL[i], ψ.AL[i]) * r_LL(ψ, i) ≈ r_LL(ψ, i + 1) + @test TransferMatrix(ψ.AL[i], ψ.AR[i]) * r_LR(ψ, i) ≈ r_LR(ψ, i + 1) + @test TransferMatrix(ψ.AR[i], ψ.AL[i]) * r_RL(ψ, i) ≈ r_RL(ψ, i + 1) + @test TransferMatrix(ψ.AR[i], ψ.AR[i]) * r_RR(ψ, i) ≈ r_RR(ψ, i + 1) + end + + # rebuilding from the left or right isometries and a bond tensor gives back the same state + for ψ′ in (InfiniteMPS(ψ.AL, ψ.C[end]; tol), InfiniteMPS(ψ.C[0], ψ.AR; tol)) + @test scalartype(ψ′) == elt + @test abs(dot(ψ, ψ′)) ≈ 1 + end + end +end + +function states_infinitemps_copying(AT; fast = false) + return @testset "InfiniteMPS copying ($AT)" begin + mps1 = adapt(AT, InfiniteMPS(rand, ComplexF64, [2], [5])) + mps2 = copy(mps1) + + @test mps1 !== mps2 + + # elements are equal + @test mps1.AL[1] == mps2.AL[1] + @test mps1.AR[1] == mps2.AR[1] + @test mps1.AC[1] == mps2.AC[1] + @test mps1.C[1] == mps2.C[1] + + # arrays are distinct + @test mps1.AL !== mps2.AL + @test mps1.AR !== mps2.AR + @test mps1.AC !== mps2.AC + @test mps1.C !== mps2.C + + # tensors are distinct + @test mps1.AL[1] !== mps2.AL[1] + @test mps1.AR[1] !== mps2.AR[1] + @test mps1.AC[1] !== mps2.AC[1] + @test mps1.C[1] !== mps2.C[1] + end +end + +function states_infinitemps_adapt(AT; fast = false) + Ts = fast ? (Float64,) : (Float64, ComplexF64) + return @testset "Adapt ($AT)" begin + for (d, D) in [(ℂ^2, ℂ^4), (ℙ^2, ℙ^4)] + mps1 = adapt(AT, InfiniteMPS(Float32, d, D)) + for T in Ts + TA = TensorKit.similarstoragetype(AT, T) + mps2 = @testinferred adapt(TA, mps1) + @test mps2 isa InfiniteMPS + @test scalartype(mps2) == T + @test storagetype(mps2) == TA + @test dot(mps1, mps2) ≈ 1 atol = 1.0e-4 + end + end + end +end + +function states_infinitemps_entropy(AT; fast = false) + return @testset "InfiniteMPS entropy ($AT, $(sectortype(D)), $elt)" for (D, d, elt) in MPS_TEST_SPACES + ψ = adapt(AT, InfiniteMPS([d, d], [D, D])) + + # entropy(ψ) returns one value per site, all non-negative + Ss = entropy(ψ) + @test length(Ss) == length(ψ) + @test all(isreal, Ss) + @test all(>=(-1.0e-8), Ss) + + # entropy(ψ, site) is non-negative and consistent with entropy(ψ) + for site in 1:length(ψ) + @test entropy(ψ, site) ≈ Ss[site] + @test entropy(ψ, site) ≈ entropy(entanglement_spectrum(ψ, site)) + end + + # product state has zero entropy everywhere + ψ_product = adapt(AT, InfiniteMPS([d, d], [unitspace(D), unitspace(D)])) + Ss_product = entropy(ψ_product) + @test all(S -> isapprox(S, 0; atol = 1.0e-10), Ss_product) + end +end + +function states_infinitemps_copying_default(AT; fast = false) + return @testset "InfiniteMPS copying ($AT)" begin + mps1 = adapt(AT, InfiniteMPS(2, 5)) + mps2 = copy(mps1) + + @test mps1 !== mps2 + + # elements are equal + @test mps1.AL[1] == mps2.AL[1] + @test mps1.AR[1] == mps2.AR[1] + @test mps1.AC[1] == mps2.AC[1] + @test mps1.C[1] == mps2.C[1] + + # arrays are distinct + @test mps1.AL !== mps2.AL + @test mps1.AR !== mps2.AR + @test mps1.AC !== mps2.AC + @test mps1.C !== mps2.C + + # tensors are distinct + @test mps1.AL[1] !== mps2.AL[1] + @test mps1.AR[1] !== mps2.AR[1] + @test mps1.AC[1] !== mps2.AC[1] + @test mps1.C[1] !== mps2.C[1] + end +end diff --git a/test/testsuite/states/multilinemps.jl b/test/testsuite/states/multilinemps.jl new file mode 100644 index 000000000..cb064257b --- /dev/null +++ b/test/testsuite/states/multilinemps.jl @@ -0,0 +1,59 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: GeometryStyle, InfiniteChainStyle, TransferMatrix +using TensorKit +using Adapt + +export states_multilinemps + +function states_multilinemps(AT; fast = false) + return @testset "MultilineMPS ($AT, $(sectortype(D)), $elt)" for (D, d, elt) in MPS_TEST_SPACES + tol = Float64(eps(real(elt)) * 100) + ψ = adapt( + AT, MultilineMPS( + [ + rand(elt, D * d, D) rand(elt, D * d, D) + rand(elt, D * d, D) rand(elt, D * d, D) + ]; tol + ) + ) + + @test GeometryStyle(typeof(ψ)) == InfiniteChainStyle() + @test GeometryStyle(ψ) == InfiniteChainStyle() + @test TensorKit.storagetype(ψ) == TensorKit.similarstoragetype(AT, elt) + @test TensorKit.sectortype(ψ) == sectortype(D) + + @test !isfinite(typeof(ψ)) + @test !isfinite(ψ) + + @test length(ψ) == 2 + @test size(ψ) == (2,) + @test size(ψ, 2) == 1 + @test axes(ψ) == (Base.OneTo(2),) + @test eachindex(ψ) == 1:2 + @test eltype(ψ) <: InfiniteMPS + @test size(ψ.AL) == size(ψ.AC) == size(ψ.C) == (2, 2) + + @test physicalspace(ψ) == fill(d, 2, 2) + @test all(x -> x ≾ D, left_virtualspace(ψ)) + @test all(x -> x ≾ D, right_virtualspace(ψ)) + @test leftunit(ψ) == unit(sectortype(D)) + + for i in 1:length(ψ), j in 1:length(ψ[1]) + @plansor difference[-1 -2; -3] := ψ.AL[i, j][-1 -2; 1] * ψ.C[i, j][1; -3] - + ψ.C[i, j - 1][-1; 1] * ψ.AR[i, j][1 -2; -3] + @test norm(difference, Inf) < tol * 10 + + @test l_LL(ψ, i, j) * TransferMatrix(ψ.AL[i, j], ψ.AL[i, j]) ≈ l_LL(ψ, i, j + 1) + @test l_LR(ψ, i, j) * TransferMatrix(ψ.AL[i, j], ψ.AR[i, j]) ≈ l_LR(ψ, i, j + 1) + @test l_RL(ψ, i, j) * TransferMatrix(ψ.AR[i, j], ψ.AL[i, j]) ≈ l_RL(ψ, i, j + 1) + @test l_RR(ψ, i, j) * TransferMatrix(ψ.AR[i, j], ψ.AR[i, j]) ≈ l_RR(ψ, i, j + 1) + + @test TransferMatrix(ψ.AL[i, j], ψ.AL[i, j]) * r_LL(ψ, i, j) ≈ r_LL(ψ, i, j + 1) + @test TransferMatrix(ψ.AL[i, j], ψ.AR[i, j]) * r_LR(ψ, i, j) ≈ r_LR(ψ, i, j + 1) + @test TransferMatrix(ψ.AR[i, j], ψ.AL[i, j]) * r_RL(ψ, i, j) ≈ r_RL(ψ, i, j + 1) + @test TransferMatrix(ψ.AR[i, j], ψ.AR[i, j]) * r_RR(ψ, i, j) ≈ r_RR(ψ, i, j + 1) + end + end +end diff --git a/test/testsuite/states/quasiparticle.jl b/test/testsuite/states/quasiparticle.jl new file mode 100644 index 000000000..281c44b26 --- /dev/null +++ b/test/testsuite/states/quasiparticle.jl @@ -0,0 +1,110 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: GeometryStyle, FiniteChainStyle, InfiniteChainStyle +using TensorKit +using TensorKit: ℙ +using Adapt + +# `@constinferred` evaluates a helper at global scope, which is not allowed inside a function +# body, so the testsuite functions use `@testinferred` (none of the calls have constant arguments) + +export states_quasiparticle + +function quasiparticle_finite_cases(AT; fast = false) + cases = [ + (adapt(AT, force_planar(transverse_field_ising(; L = 10))), ℙ^10, ℙ^2), + ( + adapt(AT, heisenberg_XXX(ComplexF64, SU2Irrep; spin = 1, L = 10)), + Rep[SU₂](1 => 1, 0 => 3), Rep[SU₂](1 => 1), + ), + ] + fast && (cases = cases[1:1]) + return cases +end + +function quasiparticle_infinite_cases(AT; fast = false) + cases = [ + (adapt(AT, force_planar(transverse_field_ising())), ℙ^10, ℙ^2), + ( + adapt(AT, heisenberg_XXX(ComplexF64, SU2Irrep; spin = 1)), + Rep[SU₂](1 => 3, 0 => 2), Rep[SU₂](1 => 1), + ), + ] + fast && (cases = cases[1:1]) + return cases +end + +# LeftGaugedQP/RightGaugedQP have no Adapt rules; they are built from an adapted state instead +function states_quasiparticle(AT; fast = false) + return @testset "Quasiparticle state ($AT)" verbose = true begin + L = 10 + @testset "Finite" verbose = true for (H, D, d) in quasiparticle_finite_cases(AT; fast) + ψ = adapt(AT, FiniteMPS(rand, ComplexF64, L, d, D)) + normalize!(ψ) + + #rand_quasiparticle is a private non-exported function + ϕ₁ = LeftGaugedQP(rand, ψ) + ϕ₂ = LeftGaugedQP(rand, ψ) + @test TensorKit.storagetype(ϕ₁) == TensorKit.storagetype(ψ) + @test TensorKit.storagetype(typeof(ϕ₁)) == TensorKit.storagetype(ψ) + @test TensorKit.storagetype(ϕ₂) == TensorKit.storagetype(ψ) + @test TensorKit.storagetype(typeof(ϕ₂)) == TensorKit.storagetype(ψ) + + @test GeometryStyle(ϕ₁) == FiniteChainStyle() + @test GeometryStyle(typeof(ϕ₂)) == FiniteChainStyle() + + @test @testinferred physicalspace(ϕ₁) == physicalspace(ψ) + @test @testinferred left_virtualspace(ϕ₁) == left_virtualspace(ψ) + @test @testinferred right_virtualspace(ϕ₁) == right_virtualspace(ψ) + @test TensorKit.sectortype(ϕ₁) == TensorKit.sectortype(ψ) + + @test norm(axpy!(1, ϕ₁, copy(ϕ₂))) ≤ norm(ϕ₁) + norm(ϕ₂) + @test norm(ϕ₁) * 3 ≈ norm(ϕ₁ * 3) + + normalize!(ϕ₁) + + ϕ₁_f = convert(FiniteMPS, ϕ₁) + ϕ₂_f = convert(FiniteMPS, ϕ₂) + + @test dot(ϕ₁_f, ϕ₂_f) ≈ dot(ϕ₁, ϕ₂) atol = 1.0e-5 + @test norm(ϕ₁_f) ≈ norm(ϕ₁) atol = 1.0e-5 + + ev_f = expectation_value(ϕ₁_f, H) - expectation_value(ψ, H) + ev_q = dot(ϕ₁, MPSKit.effective_excitation_hamiltonian(H, ϕ₁)) + @test ev_f ≈ ev_q atol = 1.0e-5 + end + + @testset "Infinite" for (th, D, d) in quasiparticle_infinite_cases(AT; fast) + period = rand(1:4) + ψ = adapt(AT, InfiniteMPS(fill(d, period), fill(D, period))) + + @test eltype(ψ) == eltype(typeof(ψ)) + + #rand_quasiparticle is a private non-exported function + ϕ₁ = LeftGaugedQP(rand, ψ) + ϕ₂ = LeftGaugedQP(rand, ψ) + + @test GeometryStyle(ϕ₁) == InfiniteChainStyle() + @test GeometryStyle(typeof(ϕ₂)) == InfiniteChainStyle() + + @test @testinferred physicalspace(ϕ₁) == physicalspace(ψ) + @test @testinferred left_virtualspace(ϕ₁) == left_virtualspace(ψ) + @test @testinferred right_virtualspace(ϕ₁) == right_virtualspace(ψ) + for i in 1:period + @test physicalspace(ψ, i) == physicalspace(ϕ₁, i) + @test left_virtualspace(ψ, i) == left_virtualspace(ϕ₁, i) + @test right_virtualspace(ψ, i) == right_virtualspace(ϕ₁, i) + end + + @test norm(axpy!(1, ϕ₁, copy(ϕ₂))) ≤ norm(ϕ₁) + norm(ϕ₂) + @test norm(ϕ₁) * 3 ≈ norm(ϕ₁ * 3) + + @test dot( + ϕ₁, + convert(LeftGaugedQP, convert(RightGaugedQP, ϕ₁)) + ) ≈ + dot(ϕ₁, ϕ₁) atol = 1.0e-10 + end + end +end diff --git a/test/testsuite/states/windowmps.jl b/test/testsuite/states/windowmps.jl new file mode 100644 index 000000000..53e4fd2ba --- /dev/null +++ b/test/testsuite/states/windowmps.jl @@ -0,0 +1,82 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: _transpose_front, _transpose_tail +using TensorKit +using TensorKit: ℙ +using Adapt + +# `@constinferred` evaluates a helper at global scope, which is not allowed inside a function +# body, so the testsuite functions use `@testinferred` (none of the calls have constant arguments) + +export states_windowmps + +function states_windowmps(AT; fast = false) + return @testset "WindowMPS ($AT)" begin + g = 8.0 + ham = adapt(AT, force_planar(transverse_field_ising(; g))) + + # operator for testing expectation_value + X = S_x(; spin = 1 // 2) + E = TensorMap(ComplexF64[1 0; 0 1], ℂ^2 ← ℂ^2) + O = adapt(AT, force_planar(-(S_z_S_z(; spin = 1 // 2) + (g / 2) * (X ⊗ E + E ⊗ X)))) + + gs, = find_groundstate(adapt(AT, InfiniteMPS([ℙ^2], [ℙ^10])), ham, VUMPS(; verbosity = 0)) + + # constructor 1 - give it a plain array of tensors + window_1 = WindowMPS(gs, copy.([gs.AC[1]; [gs.AR[i] for i in 2:10]]), gs) + + # constructor 2 - used to take a "slice" from an infinite mps + window_2 = WindowMPS(gs, 10) + + @test eltype(window_1) == eltype(typeof(window_1)) + + P = @testinferred physicalspace(window_2) + Vleft = @testinferred left_virtualspace(window_2) + Vright = @testinferred right_virtualspace(window_2) + + for i in -3:13 + @test physicalspace(window_2, i) == P[i] + @test left_virtualspace(window_2, i) == Vleft[i] + @test right_virtualspace(window_2, i) == Vright[i] + end + + # we should logically have that window_1 approximates window_2 + ovl = dot(window_1, window_2) + @test ovl ≈ 1 atol = 1.0e-8 + + # constructor 3 - random initial tensors + # TODO: WindowMPS has no Adapt rule, so the random window tensors are not placed in `AT` + window = WindowMPS(rand, ComplexF64, 10, ℙ^2, ℙ^10, gs, gs) + normalize!(window) + + for i in 1:length(window) + @test window.AC[i] ≈ window.AL[i] * window.C[i] + @test window.AC[i] ≈ + _transpose_front(window.C[i - 1] * _transpose_tail(window.AR[i])) + end + + @test norm(window) ≈ 1 + window = window * 3 + @test 9 ≈ norm(window)^2 + window = 3 * window + @test 9 * 9 ≈ norm(window)^2 + normalize!(window) + + e1 = expectation_value(window, (2, 3) => O) + + w_ham = WindowMPOHamiltonian(ham, 1:length(window)) + window, envs, _ = find_groundstate(window, w_ham, DMRG(; verbosity = 0)) + + e2 = expectation_value(window, (2, 3) => O) + + @test real(e2) ≤ real(e1) + + window, envs = timestep(window, w_ham, 0.1, 0.0, TDVP2(; trunc = truncrank(20)), envs) + window, envs = timestep(window, w_ham, 0.1, 0.0, TDVP(), envs) + + e3 = expectation_value(window, (2, 3) => O) + + @test e2 ≈ e3 atol = 1.0e-4 + end +end diff --git a/test/testsuite/statmech/statmech.jl b/test/testsuite/statmech/statmech.jl new file mode 100644 index 000000000..80567fad9 --- /dev/null +++ b/test/testsuite/statmech/statmech.jl @@ -0,0 +1,128 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using Adapt + +# `@constinferred` evaluates a helper at global scope, which is not allowed inside a function +# body, so the testsuite functions use `@testinferred` (none of the calls have constant arguments) + +export statmech_finite_temperature + +function statmech_finite_temperature(AT; fast = false) + return @testset "Finite temperature methods ($AT)" begin + imaginary_evolution = true + @testset "Finite-size" begin + L = 6 + H = adapt(AT, transverse_field_ising(; L)) + trunc = truncrank(20) + verbosity = 1 + beta = 0.1 + + # exact diagonalization + H_dense = convert(TensorMap, H) + Z_dense_1 = tr(exp(-beta * H_dense))^(1 / L) + Z_dense_2 = tr(exp(-2beta * H_dense))^(1 / L) + + # taylor cluster + rho_taylor_1 = make_time_mpo(H, beta, TaylorCluster(; N = 2); imaginary_evolution) + Z_taylor_1 = tr(rho_taylor_1)^(1 / L) + @test Z_taylor_1 ≈ Z_dense_1 atol = 1.0e-2 + Z_taylor_2 = real(dot(rho_taylor_1, rho_taylor_1))^(1 / L) + @test Z_taylor_2 ≈ Z_dense_2 atol = 1.0e-2 + + E_x_taylor = @testinferred expectation_value(rho_taylor_1, 1 => S_x()) + E_xx_taylor = @testinferred expectation_value(rho_taylor_1, (1, 2) => S_x_S_x()) + + # WII + rho_wii = make_time_mpo(H, beta, WII(); imaginary_evolution) + Z_wii = tr(rho_wii)^(1 / L) + @test Z_wii ≈ Z_dense_1 atol = 1.0e-2 + @test expectation_value(rho_wii, 1 => S_x()) ≈ E_x_taylor atol = 1.0e-2 + @test expectation_value(rho_wii, (1, 2) => S_x_S_x()) ≈ E_xx_taylor atol = 1.0e-2 + + # MPO multiplication + rho_mps = convert(FiniteMPS, rho_taylor_1) + rho_mps, = approximate(rho_mps, (rho_taylor_1, rho_mps), DMRG2(; trunc, verbosity)) + Z_mpomul = tr(convert(FiniteMPO, rho_mps))^(1 / L) + @test Z_mpomul ≈ Z_dense_2 atol = 1.0e-2 + + # TDVP + rho_0 = MPSKit.infinite_temperature_density_matrix(H) + rho_0_mps = convert(FiniteMPS, rho_0) + rho_mps, = timestep(rho_0_mps, H, 0.0, beta, TDVP2(; trunc); imaginary_evolution) + Z_tdvp = real(dot(rho_mps, rho_mps))^(1 / L) + @test Z_tdvp ≈ Z_dense_2 atol = 1.0e-2 + + @test expectation_value(rho_0_mps, 1 => S_x()) ≈ 0 + @test expectation_value(rho_0_mps, (1, 2) => S_x_S_x()) ≈ 0 + @test expectation_value(rho_mps, 1 => S_x()) ≈ E_x_taylor atol = 1.0e-2 + @test expectation_value(rho_mps, (1, 2) => S_x_S_x()) ≈ E_xx_taylor atol = 1.0e-2 + end + + @testset "Infinite-size" begin + H = adapt(AT, transverse_field_ising()) + trunc = truncrank(20) + verbosity = 1 + beta = 0.1 + + # taylor cluster + alg_taylor = TaylorCluster(; N = 2) + rho_taylor = make_time_mpo(H, beta, alg_taylor; imaginary_evolution) + rho_taylor_2 = make_time_mpo(H, 2beta, alg_taylor; imaginary_evolution) + E_taylor = @testinferred expectation_value(rho_taylor, H) + E_taylor2 = @testinferred expectation_value(rho_taylor_2, H) + + E_z_taylor = @testinferred expectation_value(rho_taylor, 1 => S_z()) + @test E_z_taylor ≈ 0 atol = 1.0e-4 # no spontaneous symmetry breaking at finite T + + # WII + rho_wii = make_time_mpo(H, beta, WII(); imaginary_evolution) + @test expectation_value(rho_wii, H) ≈ E_taylor atol = 1.0e-2 + @test expectation_value(rho_wii, 1 => S_z()) ≈ E_z_taylor atol = 1.0e-2 + + # MPO multiplication + rho_mps = convert(InfiniteMPS, rho_taylor) + rho_mps2, = approximate(rho_mps, (rho_taylor, rho_mps), IDMRG(; verbosity)) + E_mpomul = expectation_value(rho_mps2, H) + @test E_mpomul ≈ E_taylor2 atol = 1.0e-2 + + # TDVP + rho_0 = MPSKit.infinite_temperature_density_matrix(H) + rho_0_mps, = changebonds(convert(InfiniteMPS, rho_0), H, OptimalExpand(; trunc = truncrank(20))) + rho_mps_tdvp, = timestep(rho_0_mps, H, 0.0, beta, TDVP(); imaginary_evolution) + E_tdvp = expectation_value(rho_mps_tdvp, H) + @test E_tdvp ≈ E_taylor atol = 1.0e-2 + + howmany = 2 + vals_taylor = @testinferred(transfer_spectrum(convert(InfiniteMPS, rho_taylor); howmany)) + vals_mps = @testinferred(transfer_spectrum(rho_mps; howmany)) + @test all(c -> vals_taylor[c] ≈ vals_mps[c], keys(vals_taylor)) + end + + @testset "2D infinite partition functions with boundary MPS" verbose = true begin + beta = 0.5 # ferromagnetic phase + f_th = -2.0515856253898357 + m_th = 0.911319377877496 + e_th = -1.7455645753125533 + + alg = VOMPS(; tol = 1.0e-8, verbosity = 1) + O_mpo = adapt(AT, classical_ising(; β = beta)) + ψ₀ = adapt(AT, InfiniteMPS(ℂ^2, ℂ^10)) + ψ, envs = leading_boundary(ψ₀, O_mpo, alg) + + λ = expectation_value(ψ, O_mpo, envs) + f = -log(λ) / beta + @test f ≈ f_th atol = 1.0e-10 + + O, M, E = adapt(AT, classical_ising_tensors(beta)) + + m = expectation_value(ψ, (O_mpo, 1 => M)) # normalised to give density + @test abs(m) ≈ m_th atol = 1.0e-8 # account for spin flip + + e = expectation_value(ψ, (O_mpo, 1 => E)) + @test e ≈ e_th atol = 1.0e-2 + end + end +end diff --git a/test/testsuite/symmetries/multifusion.jl b/test/testsuite/symmetries/multifusion.jl new file mode 100644 index 000000000..756b95f78 --- /dev/null +++ b/test/testsuite/symmetries/multifusion.jl @@ -0,0 +1,175 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using Adapt + +export symmetries_multifusion_infinitemps_construction, symmetries_multifusion_finitemps_construction, + symmetries_multifusion_exact_diagonalization, symmetries_multifusion_finite, + symmetries_multifusion_infinite + +I = IsingBimodule + +M = I(1, 2, 0) # σ +Mop = I(2, 1, 0) +C0 = I(1, 1, 0) # unit of C +C1 = I(1, 1, 1) +D0 = I(2, 2, 0) # unit of D +D1 = I(2, 2, 1) +V = Vect[I](M => 1) +Vop = Vect[I](Mop => 1) +PD = Vect[I](D0 => 1, D1 => 1) +PC = Vect[I](C0 => 1, C1 => 1) + +bad_fusions = [(PC, PD), (PD, PC), (V, V), (Vop, Vop), (V, PC), (Vop, PD), (V, PD), (Vop, PC), (V, Vop), (Vop, V)] + +flippy(charge::IsingBimodule) = only(charge ⊗ D1) + +function TFIM_multifusion(::Type{T} = ComplexF64; g = 1.0, L = Inf, twosite = false) where {T <: Number} + P = Vect[I](D0 => 1, D1 => 1) + X = zeros(T, P ← P) + for (s, f) in fusiontrees(X) + isunit(only(f.uncoupled)) ? X[s, f] .= g : X[s, f] .= -g + end + ZZ = zeros(T, P^2 ← P^2) + for (s, f) in fusiontrees(ZZ) + s.uncoupled == map(flippy, f.uncoupled) ? ZZ[s, f] .= 1 : nothing + end + + if L == Inf + lattice = twosite ? PeriodicArray([P, P]) : PeriodicArray([P]) + H₁ = InfiniteMPOHamiltonian(lattice, i => X for i in 1:length(lattice)) + H₂ = InfiniteMPOHamiltonian(lattice, (i, i + 1) => ZZ for i in 1:length(lattice)) + else + lattice = fill(P, L) + H₁ = FiniteMPOHamiltonian(lattice, i => X for i in 1:L) + H₂ = FiniteMPOHamiltonian(lattice, (i, i + 1) => ZZ for i in 1:(L - 1)) + end + return H₁ + H₂ +end + +function symmetries_multifusion_infinitemps_construction(AT; fast = false) + return @testset "InfiniteMPS construction ($AT)" begin + for (P, V) in bad_fusions + @test_throws SpaceMismatch InfiniteMPS([P], [V]) + end + end +end + +function symmetries_multifusion_finitemps_construction(AT; fast = false) + return @testset "FiniteMPS construction ($AT)" begin + for (P, V) in bad_fusions + @test_throws ArgumentError FiniteMPS(rand(2:100), P, V) + @test_throws SpaceMismatch FiniteMPS(rand(2:100), P, V; left = V, right = V) + end + end +end + +function symmetries_multifusion_exact_diagonalization(AT; fast = false) + return @testset "Exact diagonalization ($AT)" begin + H = adapt(AT, TFIM_multifusion(; g = 0, L = 4)) + E, ψ = exact_diagonalization(H; sector = D0) # test that it runs with kwarg + @test isapprox(E, [-3, -1, 1, 3]; atol = 1.0e-6) + end +end + +function symmetries_multifusion_finite(AT; fast = false) + return @testset "Finite systems ($AT)" begin + L = 6 + g = 4 + H = adapt(AT, TFIM_multifusion(; g = g, L = L)) + V = Vect[I](M => 8) + init = adapt(AT, FiniteMPS(L, PD, V; left = Vect[I](M => 1), right = Vect[I](M => 1))) + v₀ = variance(init, H) + ψ, envs, info = find_groundstate(init, H, DMRG()) + v = variance(ψ, H) + E = expectation_value(ψ, H, envs) + + ψ2, envs2, info2 = find_groundstate(init, H, DMRG2(; trunc = trunctol(; atol = 1.0e-6))) + v2 = variance(ψ2, H) + E2 = expectation_value(ψ2, H, envs2) + + @test convergence_measure(info) ≈ 0 atol = 1.0e-3 + @test convergence_measure(info2) ≈ 0 atol = 1.0e-3 + @test v < v₀ && v2 < v₀ + + @test isapprox(E, E2; atol = 1.0e-6) + + ED, _ = exact_diagonalization(H; sector = D0) + @test isapprox(E, first(ED); atol = 1.0e-6) + + excE, qp = excitations(H, QuasiparticleAnsatz(), ψ2; sector = C1, num = 1) + @test 0 < variance(qp[1], H) < 1.0e-8 + + excE_DM, qp_DM = excitations(H, FiniteExcited(; gsalg = DMRG2(; trunc = trunctol(; atol = 1.0e-6))), ψ2; num = 1) + @test isapprox(first(excE_DM), first(excE) + E2; atol = 1.0e-6) + end +end + +function symmetries_multifusion_infinite(AT; fast = false) + return @testset "Infinite systems ($AT)" begin + # Multifusion: effectively studying the KW dual in SSB phase + g = 1 / 4 + H = adapt(AT, TFIM_multifusion(; g = g, L = Inf, twosite = true)) + V = Vect[I](M => 48) + init = adapt(AT, InfiniteMPS([PD, PD], [V, V])) + v₀ = variance(init, H) + tol = 1.0e-10 + ψ, envs, info = find_groundstate(init, H, IDMRG(; tol = tol, maxiter = 400)) + E = expectation_value(ψ, H, envs) + v = variance(ψ, H) + + ψ2, envs2, info2 = find_groundstate(init, H, IDMRG2(; tol = tol, trunc = trunctol(; atol = 1.0e-6), maxiter = 400)) + E2 = expectation_value(ψ2, H, envs2) + v2 = variance(ψ2, H) + + ψ3, envs3, info3 = find_groundstate(init, H, VUMPS(; tol = tol, maxiter = 400)) + E3 = expectation_value(ψ3, H, envs3) + v3 = variance(ψ3, H) + + @test isapprox(E, E2; atol = 1.0e-6) + @test isapprox(E, E3; atol = 1.0e-6) + for delta in [convergence_measure(info), convergence_measure(info2), convergence_measure(info3)] + @test delta ≈ 0 atol = 1.0e-3 + end + for var in [v, v2, v3] + @test var < v₀ + @test var < 1.0e-8 + end + + spec = transfer_spectrum(ψ2) + @test first(spec[C0]) ≈ 1 + @test !(abs(first(spec[C1])) ≈ 1) # testing injectivity + + @test only(keys(entanglement_spectrum(ψ2))) == M + + momentum = 0 + excC1, qpC1 = excitations(H, QuasiparticleAnsatz(), momentum, ψ3; sector = C1) + @test isapprox(first(excC1), abs(2 * (g - 1)); atol = 1.0e-6) # charged excitation lower in energy + @test variance(qpC1[1], H) < 1.0e-8 + + # diagonal test (M = D): injective GS in symmetric phase + Hdual = adapt(AT, TFIM_multifusion(; g = 1 / g, L = Inf, twosite = true)) + Vdiag = Vect[I](D0 => 24, D1 => 24) + initdiag = adapt(AT, InfiniteMPS([PD, PD], [Vdiag, Vdiag])) + gsdiag, envsdiag = find_groundstate(initdiag, Hdual, VUMPS(; tol = tol, maxiter = 400)) + Ediag = expectation_value(gsdiag, Hdual, envsdiag) + + excD1, qpD1 = excitations(Hdual, QuasiparticleAnsatz(), momentum, gsdiag; sector = D1) + @test isapprox(first(excD1), abs(2 * (1 / g - 1)); atol = 1.0e-6) # charged excitation lower in energy + @test variance(qpD1[1], Hdual) < 1.0e-8 + + # comparison to Z2 Ising: injective in symmetric phase + HZ2 = adapt(AT, repeat(transverse_field_ising(ComplexF64, Z2Irrep; g = 1 / g, L = Inf), 2)) + VZ2 = Z2Space(0 => 24, 1 => 24) + PZ2 = Z2Space(0 => 1, 1 => 1) + initZ2 = adapt(AT, InfiniteMPS([PZ2, PZ2], [VZ2, VZ2])) + gsZ2, envsZ2 = find_groundstate(initZ2, HZ2, VUMPS(; tol = tol, maxiter = 400)) + EZ2 = expectation_value(gsZ2, HZ2, envsZ2) + @test isapprox(EZ2, Ediag; atol = 1.0e-6) + + excZ2_1, qpZ2_1 = excitations(HZ2, QuasiparticleAnsatz(), momentum, gsZ2; sector = Z2Irrep(1)) + @test isapprox(first(excZ2_1), first(excD1); atol = 1.0e-6) + @test variance(qpZ2_1[1], HZ2) < 1.0e-8 + end +end diff --git a/test/testsuite/symmetries/sector_conventions.jl b/test/testsuite/symmetries/sector_conventions.jl new file mode 100644 index 000000000..f1459bbe5 --- /dev/null +++ b/test/testsuite/symmetries/sector_conventions.jl @@ -0,0 +1,64 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using LinearAlgebra: eigvals +using Adapt + +export symmetries_sector_conventions + +function symmetries_sector_conventions(AT; fast = false) + return @testset "Sector conventions ($AT)" begin + L = 4 + H = adapt(AT, XY_model(ComplexF64, U1Irrep; L)) + + H_dense = convert(TensorMap, H) + vals_dense = eigvals(H_dense) + for v in values(vals_dense) + sort!(v; by = real) + end + + tol = 1.0e-18 # tolerance required to separate degenerate eigenvalues + alg = MPSKit.Defaults.alg_eigsolve(; dynamic_tols = false, tol) + + maxVspaces = MPSKit.max_virtualspaces(physicalspace(H)) + gs, = find_groundstate( + adapt(AT, FiniteMPS(physicalspace(H), maxVspaces[2:(end - 1)])), H; verbosity = 0 + ) + E₀ = expectation_value(gs, H) + @test E₀ ≈ first(vals_dense[unit(U1Irrep)]) + + for (sector, vals) in pairs(vals_dense) + # ED tests + num = length(vals) + E₀s, ψ₀s, info = exact_diagonalization(H; num, sector, alg) + @test E₀s[1:num] ≈ vals[1:num] + # this is a trick to make the mps full-rank again, which is not guaranteed by ED + ψ₀ = changebonds(first(ψ₀s), SvdCut(; trunc = notrunc())) + Vspaces = left_virtualspace.(Ref(ψ₀), 1:L) + push!(Vspaces, right_virtualspace(ψ₀, L)) + @test all(splat(==), zip(Vspaces, MPSKit.max_virtualspaces(ψ₀))) + + # Quasiparticle tests + Es, Bs = excitations(H, QuasiparticleAnsatz(; tol), gs; sector, num = 1) + Es = Es .+ E₀ + # first excited state is second eigenvalue if sector is trivial + @test Es[1] ≈ vals[isunit(sector) ? 2 : 1] atol = 1.0e-8 + end + + # shifted charges tests + # targeting states with Sz = 1 => vals_shift_dense[0] == vals_dense[1] + # so effectively shifting the charges by -1 + H_shift = MPSKit.add_physical_charge(H, U1Irrep.([1, 0, 0, 0])) + H_shift_dense = convert(TensorMap, H_shift) + vals_shift_dense = eigvals(H_shift_dense) + for v in values(vals_shift_dense) + sort!(v; by = real) + end + for (sector, vals) in pairs(vals_dense) + sector′ = only(sector ⊗ U1Irrep(-1)) + @test vals ≈ vals_shift_dense[sector′] + end + end +end diff --git a/test/testsuite/timeevolution/clusterexpansion.jl b/test/testsuite/timeevolution/clusterexpansion.jl new file mode 100644 index 000000000..b8aa1a328 --- /dev/null +++ b/test/testsuite/timeevolution/clusterexpansion.jl @@ -0,0 +1,169 @@ +using ..TestSetup +using Test +using MPSKit, TensorKit, LinearAlgebra, Random +using TensorKit: ℙ +using TensorKitTensors.SpinOperators: σˣ, σʸ, σᶻ +using Adapt + +export timeevolution_clusterexpansion_nonperturbative, timeevolution_clusterexpansion_mixed_spaces, + timeevolution_clusterexpansion_timesteps, timeevolution_clusterexpansion_svd_support + +# Independent dense windows also cover lengths that are not multiples of the unit cell. +function dense_mpo_window(O, start = 1, L = length(O)) + sites = [copy(O[start + i - 1]) for i in 1:L] + sites[1] = sites[1][1, :, :, :] + boundary = O isa MPOHamiltonian ? size(sites[end], 4) : 1 + sites[end] = sites[end][:, :, :, boundary] + window = O isa MPOHamiltonian ? FiniteMPOHamiltonian(sites) : MPSKit.DenseMPO(FiniteMPO(sites)) + return convert(TensorMap, window) +end + +function random_test_hamiltonian(rng, Ps; onsite_only = false, dimers = false) + infinite = Ps isa MPSKit.PeriodicVector + terms = Pair[] + for s in eachindex(Ps) + a = randn!(rng, zeros(ComplexF64, Ps[s] ← Ps[s])) + push!(terms, s => (a + a') / 2) + if (infinite || s < length(Ps)) && !onsite_only && (!dimers || isodd(s)) + b = randn!(rng, zeros(ComplexF64, Ps[s] ⊗ Ps[s + 1] ← Ps[s] ⊗ Ps[s + 1])) + push!(terms, (s, s + 1) => (b + b') / 2) + end + end + return infinite ? InfiniteMPOHamiltonian(Ps, terms...) : FiniteMPOHamiltonian(Ps, terms...) +end + +function timeevolution_clusterexpansion_nonperturbative(AT; fast = false) + return @testset "Nonperturbative cluster expansion ($AT)" begin + X, Y, Z = σˣ(), σʸ(), σᶻ() + P = space(X, 1) + h = Z ⊗ Z + 0.3 * X ⊗ X + 0.2 * (X ⊗ Y + Y ⊗ X) + H = adapt(AT, InfiniteMPOHamiltonian(PeriodicArray([P]), 1 => 0.7X, (1, 2) => h)) + + @testset "Exact clusters and inference, N=$N" for N in 1:5 + U = @inferred MPSKit.make_cluster_expansion_mpo(H, -0.1im, Val(N), 1.0e-12) + @test U isa InfiniteMPO + @test dim(left_virtualspace(U, 1)) == sum(4^l for l in 0:(N ÷ 2)) + for L in 1:N + @test dense_mpo_window(U, 1, L) ≈ exp(-0.1im * dense_mpo_window(H, 1, L)) atol = 1.0e-10 + end + if N ≥ 3 + residual = @inferred MPSKit.evolution_cluster_residual(H, U, 1, -0.1im, Val(N)) + @inferred MPSKit.solve_center_correction(U, 1, residual, Val(N)) + end + if N in (3, 4) + larger = dense_mpo_window(U, 1, N + 1) + reflected = permute(larger, (Tuple((N + 1):-1:1), Tuple((2N + 2):-1:(N + 2)))) + @test larger ≈ reflected atol = 1.0e-10 + U2 = make_time_mpo(repeat(H, 2), 0.1, ClusterExpansion(N)) + @test dense_mpo_window(U2, 2, N + 1) ≈ larger atol = 1.0e-10 + end + end + + @testset "Leading error, N=$N" for N in 2:4 + for model in (H, open_boundary_conditions(H, 5)) + errors = [ + norm( + dense_mpo_window(make_time_mpo(model, dt, ClusterExpansion(N)), 1, N + 1) - + exp(-im * dt * dense_mpo_window(model, 1, N + 1)) + ) for dt in (0.04, 0.02) + ] + @test log2(errors[1] / errors[2]) ≥ N - 0.15 + end + end + + @testset "Finite boundaries and cluster-size cap, L=$L" for L in (1, 2, 3, 6) + finite = open_boundary_conditions(H, L) + U = make_time_mpo(finite, 0.1, ClusterExpansion(L + 3; tol = 0.5)) + @test U isa FiniteMPO + @test dim(left_virtualspace(U, 1)) == dim(right_virtualspace(U, L)) == 1 + @test dense_mpo_window(U) ≈ exp(-0.1im * convert(TensorMap, finite)) atol = 1.0e-10 + end + @testset "Finite/infinite consistency, N=$N" for N in (2, 3, 4, 5) + finite = make_time_mpo(open_boundary_conditions(H, 6), 0.1, ClusterExpansion(N)) + infinite = make_time_mpo(H, 0.1, ClusterExpansion(N)) + @test dense_mpo_window(finite) ≈ dense_mpo_window(infinite, 1, 6) atol = 1.0e-10 + end + + @test_throws ArgumentError ClusterExpansion(0) + @test_throws ArgumentError ClusterExpansion(2; tol = 0) + @test_throws ArgumentError ClusterExpansion(2; tol = Inf) + @test ClusterExpansion(; N = 3).N == 3 + empty = FiniteMPOHamiltonian(similar(parent(open_boundary_conditions(H, 2)), 0)) + @test_throws ArgumentError make_time_mpo(empty, 0.1, ClusterExpansion(2)) + end +end + +function timeevolution_clusterexpansion_mixed_spaces(AT; fast = false) + return @testset "Mixed physical spaces and boundaries ($AT)" begin + rng = MersenneTwister(25) + lattices = ( + [ℂ^2, ℂ^3], [ℂ^2, ℂ^3, ℂ^2], [ℝ^2, ℝ^3], [ℙ^2, ℙ^3], + [Rep[U₁](0 => 1, 1 => 1), dual(Rep[U₁](0 => 1, 1 => 1, 2 => 1))], + [dual(Rep[SU₂](1 // 2 => 1)), Rep[SU₂](1 => 1)], + [Vect[FermionParity](0 => 1, 1 => 1), Vect[FermionParity](0 => 2, 1 => 1)], + ) + for lattice in lattices, infinite in (true, false) + Ps = infinite ? PeriodicArray(lattice) : repeat(lattice, 3)[1:5] + H = adapt(AT, random_test_hamiltonian(rng, Ps)) + for N in (4, 5) + U = @inferred MPSKit.make_cluster_expansion_mpo(H, -0.1im, Val(N), 1.0e-12) + @test length(U) == length(H) + for s in eachindex(Ps) + @test physicalspace(U, s) == Ps[s] + (infinite || s < length(H)) && @test right_virtualspace(U, s) == left_virtualspace(U, s + 1) + width = infinite ? N : min(N, length(H) - s + 1) + @test dense_mpo_window(U, s, width) ≈ exp(-0.1im * dense_mpo_window(H, s, width)) atol = 1.0e-10 + end + end + end + end +end + +function timeevolution_clusterexpansion_timesteps(AT; fast = false) + return @testset "Time steps and rank-deficient environments ($AT)" begin + rng = MersenneTwister(24) + Z = σᶻ() + P = space(Z, 1) + commuting = adapt(AT, InfiniteMPOHamiltonian(PeriodicArray([P]), (1, 2) => Z ⊗ Z)) + U = make_time_mpo(commuting, 0.1, ClusterExpansion(4)) + @test dense_mpo_window(U, 1, 5) ≈ exp(-0.1im * dense_mpo_window(commuting, 1, 5)) atol = 1.0e-10 + + for Ps in (PeriodicArray([ℂ^2, ℂ^3]), [ℂ^2, ℂ^3, ℂ^2, ℂ^3, ℂ^2]) + H = adapt(AT, random_test_hamiltonian(rng, Ps)) + onsite = adapt(AT, random_test_hamiltonian(rng, Ps; onsite_only = true)) + dimers = adapt(AT, random_test_hamiltonian(rng, Ps; dimers = true)) + for (model, dt) in ((onsite, 0.1), (dimers, 0.1), (H, 0.0), (H, -0.1im), (H, -0.1)) + U = make_time_mpo(model, dt, ClusterExpansion(5)) + width = isfinite(model) || model === H && !iszero(dt) ? 5 : 6 + starts = isfinite(model) ? (1:1) : eachindex(Ps) + for s in starts + @test dense_mpo_window(U, s, width) ≈ exp(-im * dt * dense_mpo_window(model, s, width)) atol = 1.0e-10 + end + model === onsite && @test dim(left_virtualspace(U, 1)) == 1 + end + # The SVD cutoff changes the channels, not the matched exponentials. + for N in (4, 5) + U = @inferred MPSKit.make_cluster_expansion_mpo(H, -0.1im, Val(N), 0.5) + starts = isfinite(H) ? (1:(length(H) - N + 1)) : eachindex(Ps) + for s in starts + @test dense_mpo_window(U, s, N) ≈ exp(-0.1im * dense_mpo_window(H, s, N)) atol = 1.0e-10 + end + end + end + end +end + +# Reconstruction alone does not ensure the factors retain all environment directions. +function timeevolution_clusterexpansion_svd_support(AT; fast = false) + return @testset "Completed SVD support ($AT)" begin + L, R = ℂ^2, ℂ^3 + weak = adapt(AT, TensorMap(ComplexF64[1 0 0; 0 1.0e-14 0], L ← R)) + for C in (weak, adapt(AT, zeros(ComplexF64, L ← R))) + A, B = @inferred MPSKit.factor_with_complementary_spaces(C, 1.0e-12) + @test norm(A * B - C) ≤ 1.0e-16 * norm(C) + @test A * pinv(A; rtol = 1.0e-12) ≈ id(storagetype(C), L) atol = 1.0e-10 + @test pinv(B; rtol = 1.0e-12) * B ≈ id(storagetype(C), R) atol = 1.0e-10 + @test dim(only(domain(A))) == (iszero(norm(C)) ? 5 : 4) + end + end +end diff --git a/test/testsuite/timeevolution/taylorcluster.jl b/test/testsuite/timeevolution/taylorcluster.jl new file mode 100644 index 000000000..9982faae4 --- /dev/null +++ b/test/testsuite/timeevolution/taylorcluster.jl @@ -0,0 +1,139 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using MPSKit: fuse_mul_mpo +using TensorKit +using TensorKit: ℙ +using Adapt + +export timeevolution_taylorcluster + +function timeevolution_taylorcluster(AT; fast = false) + return @testset "TaylorCluster time evolution ($AT)" begin + L = 4 + dt = 0.05 + dτ = -im * dt + + @testset "O(1) exact expression" begin + alg = TaylorCluster(; N = 1, compression = true, extension = true) + for H in (adapt(AT, transverse_field_ising()), adapt(AT, heisenberg_XXX())) + # Infinite + mpo = make_time_mpo(H, dt, alg) + O = mpo[1] + I, A, B, C, D = H[1][1], H.A[1], H.B[1], H.C[1], H.D[1] + + @test size(O, 1) == size(D, 1)^2 + (size(D, 1) * size(B, 1)) + @test size(O, 4) == size(D, 4)^2 + (size(C, 4) * size(D, 4)) + + O_exact = similar(O) + O_exact[1, 1, 1, 1] = I + dτ * D + dτ^2 / 2 * fuse_mul_mpo(D, D) + O_exact[1, 1, 1, 2] = C + dτ * symm_mul_mpo(C, D) + O_exact[2, 1, 1, 1] = dτ * (B + dτ * symm_mul_mpo(B, D)) + O_exact[2, 1, 1, 2] = A + dτ * (symm_mul_mpo(A, D) + symm_mul_mpo(C, B)) + + @test all(isapprox.(parent(O), parent(O_exact))) + + # Finite + H_fin = open_boundary_conditions(H, L) + mpo_fin = make_time_mpo(H_fin, dt, alg) + mpo_fin2 = open_boundary_conditions(mpo, L) + for i in 1:L + @test all(isapprox.(parent(mpo_fin[i]), parent(mpo_fin2[i]))) + end + end + end + + @testset "O(2) exact expression" begin + alg = TaylorCluster(; N = 2, compression = true, extension = true) + for H in (adapt(AT, transverse_field_ising()), adapt(AT, heisenberg_XXX())) + # Infinite + mpo = make_time_mpo(H, dt, alg) + O = mpo[1] + I, A, B, C, D = H[1][1], H.A[1], H.B[1], H.C[1], H.D[1] + + @test size(O, 1) == + size(D, 1)^3 + (size(D, 1)^2 * size(B, 1)) + (size(D, 1) * size(B, 1)^2) + @test size(O, 4) == + size(D, 4)^3 + (size(C, 4) * size(D, 4)^2) + (size(D, 4) * size(C, 4)^2) + + O_exact = similar(O) + O_exact[1, 1, 1, 1] = I + dτ * D + dτ^2 / 2 * fuse_mul_mpo(D, D) + + dτ^3 / 6 * fuse_mul_mpo(fuse_mul_mpo(D, D), D) + O_exact[1, 1, 1, 2] = C + dτ * symm_mul_mpo(C, D) + + dτ^2 / 2 * symm_mul_mpo(C, D, D) + O_exact[1, 1, 1, 3] = fuse_mul_mpo(C, C) + dτ * symm_mul_mpo(C, C, D) + O_exact[2, 1, 1, 1] = dτ * + ( + B + dτ * symm_mul_mpo(B, D) + + dτ^2 / 2 * symm_mul_mpo(B, D, D) + ) + O_exact[2, 1, 1, 2] = A + dτ * symm_mul_mpo(A, D) + + dτ^2 / 2 * symm_mul_mpo(A, D, D) + + dτ * (symm_mul_mpo(C, B) + dτ * symm_mul_mpo(C, B, D)) + O_exact[2, 1, 1, 3] = 2 * (symm_mul_mpo(A, C) + dτ * symm_mul_mpo(A, C, D)) + + dτ * symm_mul_mpo(C, C, B) + O_exact[3, 1, 1, 1] = dτ^2 / 2 * + (fuse_mul_mpo(B, B) + dτ * symm_mul_mpo(B, B, D)) + O_exact[3, 1, 1, 2] = dτ * (symm_mul_mpo(A, B) + dτ * symm_mul_mpo(A, B, D)) + + dτ^2 / 2 * symm_mul_mpo(B, B, C) + O_exact[3, 1, 1, 3] = fuse_mul_mpo(A, A) + dτ * symm_mul_mpo(A, A, D) + + 2 * dτ * symm_mul_mpo(A, C, B) + + @test all(isapprox.(parent(O), parent(O_exact))) + + # Finite + H_fin = open_boundary_conditions(H, L) + mpo_fin = make_time_mpo(H_fin, dt, alg) + mpo_fin2 = open_boundary_conditions(mpo, L) + for i in 1:L + @test all(isapprox.(parent(mpo_fin[i]), parent(mpo_fin2[i]))) + end + end + end + + L = 4 + h1 = S_z_S_z(ComplexF64, Trivial; spin = 1 // 2) * 4 + h2 = S_x_S_x(ComplexF64, Trivial; spin = 1 // 2) * 4 + rectangular_lattice = fill(space(h1, 1), L) + rectangular_ising = adapt( + AT, FiniteMPOHamiltonian( + rectangular_lattice, ((i, i + 1) => h1 for i in 1:(L - 1))..., (1, 3) => h2 + ) + ) + + Hs = [adapt(AT, transverse_field_ising(; L)), adapt(AT, heisenberg_XXX(; L)), rectangular_ising] + + Ns = [1, 2, 3] + dts = [1.0e-2, 1.0e-3] + for H in Hs + ψ = adapt(AT, FiniteMPS(L, physicalspace(H, 1), ℂ^16)) + for N in Ns + εs = zeros(ComplexF64, 2) + for (i, dt) in enumerate(dts) + ψ₀, _ = find_groundstate(ψ, H, DMRG(; verbosity = 0)) + E₀ = expectation_value(ψ₀, H) + + O = make_time_mpo(H, dt, TaylorCluster(; N = N)) + + ψ₁, _ = approximate(ψ₀, (O, ψ₀), DMRG(; verbosity = 0)) + εs[i] = norm(dot(ψ₀, ψ₁) - exp(-im * E₀ * dt)) + end + @test (log(εs[2]) - log(εs[1])) / (log(dts[2]) - log(dts[1])) ≈ N + 1 atol = 0.1 + end + + for N in Ns + εs = zeros(ComplexF64, 2) + for (i, dt) in enumerate(dts) + ψ₀, _ = find_groundstate(ψ, H, DMRG(; verbosity = 0)) + E₀ = expectation_value(ψ₀, H) + + O = make_time_mpo(H, dt, TaylorCluster(; N = N, compression = true)) + + ψ₁, _ = approximate(ψ₀, (O, ψ₀), DMRG(; verbosity = 0)) + εs[i] = norm(dot(ψ₀, ψ₁) - exp(-im * E₀ * dt)) + end + @test (log(εs[2]) - log(εs[1])) / (log(dts[2]) - log(dts[1])) ≈ N + 1 atol = 0.1 + end + end + end +end diff --git a/test/testsuite/timeevolution/timestep.jl b/test/testsuite/timeevolution/timestep.jl new file mode 100644 index 000000000..6007c970d --- /dev/null +++ b/test/testsuite/timeevolution/timestep.jl @@ -0,0 +1,376 @@ +using ..TestSetup +using Test, TestExtras +using MPSKit +using TensorKit +using TensorKit: ℙ +using LinearAlgebra: dot, norm +using Random +using Adapt + +export timeevolution_timestep, timeevolution_timestep_imaginary_bug, + timeevolution_timestep_bug_truncation, timeevolution_timestep_symmetric, + timeevolution_timestep_cbe_tdvp, timeevolution_timestep_truncation_error, + timeevolution_time_evolve + +verbosity_full = 5 +verbosity_conv = 1 + +# name a time-evolution algorithm for @testset labels ("TDVP", "TDVP2", "BUG") +algname(alg) = string(nameof(typeof(alg))) + +maxbond(ψ) = maximum(i -> dim(left_virtualspace(ψ, i)), 1:length(ψ)) + +function timeevolution_timestep(AT; fast = false) + return @testset "timestep ($AT)" verbose = true begin + dt = 0.1 + # every rank-adaptive algorithm gets the same cap: without one, BUG's augmentation doubles the + # bond dimension on every half-sweep with nothing ever cutting it back + algs = [TDVP(), TDVP2(; trunc = truncrank(10)), BUG(; trunc = truncrank(10))] + L = 10 + + H = adapt(AT, force_planar(heisenberg_XXX(Float64, Trivial; spin = 1 // 2, L))) + ψ = adapt(AT, FiniteMPS(rand, Float64, L, ℙ^2, ℙ^4)) + E = expectation_value(ψ, H) + ψ₀, = find_groundstate(ψ, H) + E₀ = expectation_value(ψ₀, H) + + @testset "Finite $(algname(alg))" for alg in algs + ψ1, envs = timestep(ψ₀, H, 0.0, dt, alg) + E1 = expectation_value(ψ1, H, envs) + @test E₀ ≈ E1 atol = 1.0e-2 + @test dot(ψ1, ψ₀) ≈ exp(im * dt * E₀) atol = 1.0e-4 + + # the integrator carries no scratch space of its own - the sweep's allocator is obtained + # per `timestep` - so re-using one across steps has to reproduce the answer + ψ2, = timestep(ψ₀, H, 0.0, dt, alg) + @test dot(ψ1, ψ2) ≈ norm(ψ1) * norm(ψ2) atol = 1.0e-10 + end + + Hlazy = LazySum([3 * H, 1.55 * H, -0.1 * H]) + + @testset "Finite LazySum $(algname(alg))" for alg in algs + ψ, envs = timestep(ψ₀, Hlazy, 0.0, dt, alg) + E = expectation_value(ψ, Hlazy, envs) + @test (3 + 1.55 - 0.1) * E₀ ≈ E atol = 1.0e-2 + end + + Ht = MultipliedOperator(H, t -> 4) + MultipliedOperator(H, 1.45) + + @testset "Finite TimeDependent LazySum $(algname(alg))" for alg in algs + ψ, envs = timestep(ψ₀, Ht(1.0), 0.0, dt, alg) + E = expectation_value(ψ, Ht(1.0), envs) + + ψt, envst = timestep(ψ₀, Ht, 1.0, dt, alg) + Et = expectation_value(ψt, Ht(1.0), envst) + @test E ≈ Et atol = 1.0e-8 + end + + Ht2 = MultipliedOperator(H, t -> t < 0 ? error("t < 0!") : 4) + + MultipliedOperator(H, 1.45) + @testset "Finite TimeDependent LazySum (fix negative t issue) $(algname(alg))" for alg in algs + ψ, envs = timestep(ψ₀, Ht2, 0.0, dt, alg) + E = expectation_value(ψ, Ht2(0.0), envs) + + ψt, envst = timestep(ψ₀, Ht2, 0.0, dt, alg) + Et = expectation_value(ψt, Ht2(0.0), envst) + @test E ≈ Et atol = 1.0e-8 + end + + H = adapt(AT, repeat(force_planar(heisenberg_XXX(; spin = 1)), 2)) + ψ₀ = adapt(AT, InfiniteMPS([ℙ^3, ℙ^3], [ℙ^50, ℙ^50])) + E₀ = expectation_value(ψ₀, H) + + # the AC and C sweeps of the infinite integrator spawn over the unit cell, and additionally run + # concurrently with each other, so they share one allocator: check both schedulers agree + @testset "Infinite TDVP ($schedname)" for (schedname, scheduler) in SCHEDULERS + with_scheduler(scheduler) do + ψ, envs = timestep(ψ₀, H, 0.0, dt, TDVP()) + E = expectation_value(ψ, H, envs) + @test E₀ ≈ E atol = 1.0e-2 + + ψ′ = copy(ψ₀) + ψ″, = timestep!(ψ′, H, 0.0, dt, TDVP()) + @test ψ″ === ψ′ + @test ψ″.AC == ψ.AC + + # evolving in the right-orthogonal gauge gives the same state + ψr, = timestep(ψ₀, H, 0.0, dt, TDVP(); leftorthflag = false) + @test abs(dot(ψ, ψr)) ≈ 1 atol = 1.0e-8 + end + end + + @testset "Infinite real TDVP" begin + Hr = adapt(AT, force_planar(transverse_field_ising(Float64; g = 0.7))) + ψr₀ = adapt(AT, InfiniteMPS(rand, Float64, [ℙ^2], [ℙ^8])) + # real-time evolution promotes the state, and with it the environments, to complex + ψ, envs = timestep(ψr₀, Hr, 0.0, dt, TDVP()) + @test scalartype(ψ) <: Complex + ψc, = timestep(complex(ψr₀), Hr, 0.0, dt, TDVP()) + @test abs(dot(ψ, ψc)) ≈ 1 atol = 1.0e-8 + @test expectation_value(ψ, Hr, envs) ≈ expectation_value(ψc, Hr) + # imaginary-time evolution with a real step keeps it real + ψi, = timestep(ψr₀, Hr, 0.0, dt, TDVP(); imaginary_evolution = true) + @test scalartype(ψi) == Float64 + end + + Hlazy = LazySum([3 * deepcopy(H), 1.55 * deepcopy(H), -0.1 * deepcopy(H)]) + + @testset "Infinite LazySum TDVP" begin + ψ, envs = timestep(ψ₀, Hlazy, 0.0, dt, TDVP()) + E = expectation_value(ψ, Hlazy, envs) + @test (3 + 1.55 - 0.1) * E₀ ≈ E atol = 1.0e-2 + end + + Ht = MultipliedOperator(H, t -> 4) + MultipliedOperator(H, 1.45) + + @testset "Infinite TimeDependent LazySum" begin + ψ, envs = timestep(ψ₀, Ht(1.0), 0.0, dt, TDVP()) + E = expectation_value(ψ, Ht(1.0), envs) + + ψt, envst = timestep(ψ₀, Ht, 1.0, dt, TDVP()) + Et = expectation_value(ψt, Ht(1.0), envst) + @test E ≈ Et atol = 1.0e-8 + end + end +end + +# BUG-specific: unlike `TDVP` there is no backward-in-time substep, which makes it a natural +# imaginary-time integrator (cf. its docstring). `TDVP`'s imaginary-time behaviour is covered by the +# CBE block below. +function timeevolution_timestep_imaginary_bug(AT; fast = false) + return @testset "Finite imaginary-time BUG ($AT)" begin + L = 10 + H = adapt(AT, force_planar(heisenberg_XXX(Float64, Trivial; spin = 1 // 2, L))) + + alg = BUG(; trunc = truncrank(4)) + + Random.seed!(5) + ψi = complex(adapt(AT, FiniteMPS(rand, Float64, L, ℙ^2, ℙ^4))) + E_start = real(expectation_value(ψi, H)) + E_prev = E_start + for _ in 1:8 + ψi, = timestep(ψi, H, 0.0, 0.1, alg; imaginary_evolution = true, normalize = true) + E_now = real(expectation_value(ψi, H)) + @test E_now ≤ E_prev + 1.0e-6 # monotone (non-increasing) energy + @test maxbond(ψi) ≤ 8 # the cap bounds the bond at `2D` throughout + E_prev = E_now + end + @test E_prev < E_start - 0.5 # substantial lowering toward the ground state + @test norm(ψi) ≈ 1 atol = 1.0e-6 # `normalize = true` renormalizes each step + end +end + +# The truncating path, which is BUG's distinguishing feature. `BUG(; trunc = …)` cuts the bond +# *ahead of* every local update — the bond carrying the previous half-sweep's augmentation — and +# augments without truncating, so the bond dimension oscillates between the requested rank and (at +# most) twice it. +function timeevolution_timestep_bug_truncation(AT; fast = false) + return @testset "BUG with truncation ($AT)" begin + L = 8 + H = adapt(AT, force_planar(transverse_field_ising(ComplexF64, Trivial; L, g = 1.5))) + δt, nsteps, D = 0.05, 20, 4 + + Random.seed!(11) + ψ₀ = adapt(AT, FiniteMPS(rand, ComplexF64, L, ℙ^2, ℙ^2)) # low rank to start with, entanglement grows + normalize!(ψ₀) + + tovec(ψ) = (v = convert(TensorMap, ψ); v / norm(v)) + ref = exp(-im * convert(TensorMap, H) * (nsteps * δt)) * tovec(ψ₀) + + # the cap actually bites here: the exact state needs bond 16 + ψ = ψ₀ + for k in 0:(nsteps - 1) + ψ, = timestep(ψ, H, k * δt, δt, BUG(; trunc = truncrank(D))) + @test maxbond(ψ) ≤ 2D # `D` right after the cut, at most `2D` after the augment + end + @test 1 - abs(dot(tovec(ψ), ref)) < 1.0e-3 + end +end + +# Genuine symmetric-tensor coverage (no `force_planar`) for the single-site finite integrators. +# BUG's augment step stacks bases per sector, so it can add or drop sectors: both `TDVP` and `BUG` +# must conserve the energy, accrue only an eigenstate phase, and preserve the total boundary charge +# (the fixed `right` virtual space at site L). TDVP2 is excluded here: it requires a `trunc` and is +# the two-site variant; these are single-site conservation properties. +function timeevolution_timestep_symmetric(AT; fast = false) + fast && return nothing + return @testset "Finite symmetric-tensor time evolution ($AT)" begin + dt = 0.1 + L = 6 + + Random.seed!(2718) + H = adapt(AT, heisenberg_XXX(ComplexF64, U1Irrep; spin = 1 // 2, L)) + maxV = MPSKit.max_virtualspaces(physicalspace(H)) + ψ = adapt(AT, FiniteMPS(physicalspace(H), maxV[2:(end - 1)])) + ψ₀, = find_groundstate(ψ, H; verbosity = 0) + E₀ = expectation_value(ψ₀, H) + + # the exact state has Schmidt rank ≤ 8 here, so this cap bounds the bond without discarding + # anything — it only stops BUG's augmentation from doubling unchecked + algs = [TDVP(), BUG(; trunc = truncrank(8))] + + @testset "U(1) Heisenberg ($(algname(alg)))" for alg in algs + ψ1, envs = timestep(ψ₀, H, 0.0, dt, alg) + E1 = expectation_value(ψ1, H, envs) + + @test E₀ ≈ E1 atol = 1.0e-2 + @test imag(E1) ≈ 0 atol = 1.0e-8 + @test dot(ψ1, ψ₀) ≈ exp(im * dt * E₀) atol = 1.0e-4 + @test right_virtualspace(ψ1, L) == right_virtualspace(ψ₀, L) + end + end +end + +function timeevolution_timestep_cbe_tdvp(AT; fast = false) + return @testset "Finite CBE-TDVP ($AT)" verbose = true begin + L = 10 + H = adapt(AT, force_planar(heisenberg_XXX(Float64, Trivial; spin = 1 // 2, L))) + Dstart, Dcap, dt = 2, 16, 0.05 + + # controlled bond expansion lets single-site TDVP grow the bond; the evolution should stay + # unitary (norm-preserving) and energy-conserving while tracking the bond-adaptive TDVP2 + # reference better than fixed-bond single-site TDVP + @testset "$(nameof(Exp))" for (Exp, kw) in + ((OptimalExpand, (;)), (SketchedExpand, (; oversampling = 4))) + Random.seed!(4) + ψ₀ = complex(adapt(AT, FiniteMPS(rand, Float64, L, ℙ^2, ℙ^Dstart))) + alg = TDVP(; alg_expand = Exp(; trunc = truncrank(Dstart), kw...), trunc = truncrank(Dcap)) + E₀ = real(expectation_value(ψ₀, H)) + + ref, cbe, plain = ψ₀, ψ₀, ψ₀ + for _ in 1:6 + ref, = timestep(ref, H, 0.0, dt, TDVP2(; trunc = truncrank(Dcap))) + cbe, = timestep(cbe, H, 0.0, dt, alg) + plain, = timestep(plain, H, 0.0, dt, TDVP()) # stuck at Dstart + end + + @test norm(cbe) ≈ 1 atol = 1.0e-6 + @test real(expectation_value(cbe, H)) ≈ E₀ atol = 1.0e-2 + @test dim(left_virtualspace(cbe, L ÷ 2)) > Dstart + @test dim(left_virtualspace(plain, L ÷ 2)) == Dstart + @test abs(dot(ref, cbe)) > abs(dot(ref, plain)) + end + + # by default (`normalize = false`) the bond truncation preserves the norm, so it reflects the + # discarded weight; `normalize = true` renormalizes each step. This is independent of + # `imaginary_evolution`. + @testset "norm handling" begin + Random.seed!(6) + ψ₀ = complex(adapt(AT, FiniteMPS(rand, Float64, L, ℙ^2, ℙ^Dstart))) + # a deliberately lossy cap so the truncation discards weight every step + lossy = TDVP(; alg_expand = OptimalExpand(; trunc = truncrank(2)), trunc = truncrank(2)) + + ψrt = ψ₀ + for _ in 1:12 + ψrt, = timestep(ψrt, H, 0.0, 0.5, lossy) # real time, norm preserved by default + end + @test norm(ψrt) < 1 - 1.0e-3 # truncation loss is not renormalized away + + # imaginary-time, norm preserved by default: the weight is *not* pinned to unit norm + # (imaginary-time evolution rescales the state, so the norm drifts away from 1) + ψit = ψ₀ + for _ in 1:12 + ψit, = timestep(ψit, H, 0.0, 0.5, lossy; imaginary_evolution = true) + end + @test abs(norm(ψit) - 1) > 1.0e-3 + + # imaginary-time with `normalize = true`: renormalized to unit norm each step + ψn = ψ₀ + for _ in 1:12 + ψn, = timestep(ψn, H, 0.0, 0.5, lossy; imaginary_evolution = true, normalize = true) + end + @test norm(ψn) ≈ 1 atol = 1.0e-6 + end + + @testset "imaginary-time lowers energy" begin + Random.seed!(5) + ψ₀ = complex(adapt(AT, FiniteMPS(rand, Float64, L, ℙ^2, ℙ^Dstart))) + alg = TDVP(; alg_expand = OptimalExpand(; trunc = truncrank(Dstart)), trunc = truncrank(Dcap)) + E₀ = real(expectation_value(ψ₀, H)) + ψ = ψ₀ + for _ in 1:8 + ψ, = timestep(ψ, H, 0.0, 0.1, alg; imaginary_evolution = true, normalize = true) # gauge renormalizes + end + @test real(expectation_value(ψ, H)) < E₀ + @test dim(left_virtualspace(ψ, L ÷ 2)) > Dstart + end + end +end + +function timeevolution_timestep_truncation_error(AT; fast = false) + return @testset "Truncation error ($AT)" verbose = true begin + L = 10 + dt = 0.1 + H = adapt(AT, force_planar(heisenberg_XXX(Float64, Trivial; spin = 1 // 2, L))) + Random.seed!(7) + ψ₀ = normalize!(complex(adapt(AT, FiniteMPS(rand, Float64, L, ℙ^2, ℙ^16)))) + + # fixed bond dimension sweep never discards anything, so the reported errors are exactly zero + @testset "no truncation" begin + for alg in (TDVP(), BUG()) + info = last(timestep(ψ₀, H, 0.0, dt, alg)) + @test info isa MPSKit.AlgorithmInfo + @test info.truncation_errors == zeros(L - 1) + @test info.numiter == 1 + end + end + + @testset "$(nameof(alg))" for alg in (TDVP2, BUG) + _, _, loose = timestep(ψ₀, H, 0.0, dt, alg(; trunc = truncrank(32))) + ψ, _, tight = timestep(ψ₀, H, 0.0, dt, alg(; trunc = truncrank(2))) + + @test length(tight.truncation_errors) == L - 1 + @test maximum(tight.truncation_errors) > 1.0e-4 # throw away real weight + # throw away less weight with a more forgiving truncation + @test maximum(loose.truncation_errors) < maximum(tight.truncation_errors) + # in real time with `normalize = false` the norm loss covers every cut of the step, + # of which the reported last cut per bond is a subset + @test norm(ψ₀)^2 - norm(ψ)^2 >= sum(abs2, tight.truncation_errors) - 1.0e-12 + end + + @testset "history over an evolution" begin + alg = TDVP2(; trunc = truncrank(2)) + nsteps = 4 + _, _, step = timestep(ψ₀, H, 0.0, dt, alg) + _, _, total = time_evolve(ψ₀, H, 0:dt:(nsteps * dt), alg) + + @test total.numiter == nsteps + @test length(total.truncation_errors) == nsteps + @test all(==(L - 1) ∘ length, total.truncation_errors) + @test first(total.truncation_errors) ≈ step.truncation_errors + end + end +end + +function timeevolution_time_evolve(AT; fast = false) + return @testset "time_evolve ($AT)" verbose = true begin + t_span = 0:0.1:0.1 + algs = [TDVP(), TDVP2(; trunc = truncrank(10)), BUG(; trunc = truncrank(10))] + + L = 10 + H = adapt(AT, force_planar(heisenberg_XXX(; spin = 1 // 2, L))) + ψ₀ = adapt(AT, FiniteMPS(L, ℙ^2, ℙ^1)) + E₀ = expectation_value(ψ₀, H) + + @testset "Finite $(algname(alg))" for alg in algs + ψ, envs = time_evolve(ψ₀, H, t_span, alg; verbosity = 2) + E = expectation_value(ψ, H, envs) + @test E₀ ≈ E atol = 1.0e-2 + end + + H = adapt(AT, repeat(force_planar(heisenberg_XXX(; spin = 1)), 2)) + ψ₀ = adapt(AT, InfiniteMPS([ℙ^3, ℙ^3], [ℙ^50, ℙ^50])) + E₀ = expectation_value(ψ₀, H) + + @testset "Infinite TDVP" begin + ψ, envs = time_evolve(ψ₀, H, t_span, TDVP(); verbosity = 2) + E = expectation_value(ψ, H, envs) + @test E₀ ≈ E atol = 1.0e-2 + + ψ′ = copy(ψ₀) + ψ″, = MPSKit.time_evolve!(ψ′, H, t_span, TDVP()) + @test ψ″ === ψ′ + @test ψ″.AC == ψ.AC + end + end +end diff --git a/test/timeevolution/clusterexpansion.jl b/test/timeevolution/clusterexpansion.jl index 43fc85756..6984c7d1f 100644 --- a/test/timeevolution/clusterexpansion.jl +++ b/test/timeevolution/clusterexpansion.jl @@ -1,156 +1,7 @@ -using Test -using MPSKit, TensorKit, LinearAlgebra, Random -using TensorKit: ℙ -using TensorKitTensors.SpinOperators: σˣ, σʸ, σᶻ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -# Independent dense windows also cover lengths that are not multiples of the unit cell. -function dense_mpo_window(O, start = 1, L = length(O)) - sites = [copy(O[start + i - 1]) for i in 1:L] - sites[1] = sites[1][1, :, :, :] - boundary = O isa MPOHamiltonian ? size(sites[end], 4) : 1 - sites[end] = sites[end][:, :, :, boundary] - window = O isa MPOHamiltonian ? FiniteMPOHamiltonian(sites) : MPSKit.DenseMPO(FiniteMPO(sites)) - return convert(TensorMap, window) -end - -function random_test_hamiltonian(rng, Ps; onsite_only = false, dimers = false) - infinite = Ps isa MPSKit.PeriodicVector - terms = Pair[] - for s in eachindex(Ps) - a = randn!(rng, zeros(ComplexF64, Ps[s] ← Ps[s])) - push!(terms, s => (a + a') / 2) - if (infinite || s < length(Ps)) && !onsite_only && (!dimers || isodd(s)) - b = randn!(rng, zeros(ComplexF64, Ps[s] ⊗ Ps[s + 1] ← Ps[s] ⊗ Ps[s + 1])) - push!(terms, (s, s + 1) => (b + b') / 2) - end - end - return infinite ? InfiniteMPOHamiltonian(Ps, terms...) : FiniteMPOHamiltonian(Ps, terms...) -end - -@testset "Nonperturbative cluster expansion" begin - X, Y, Z = σˣ(), σʸ(), σᶻ() - P = space(X, 1) - h = Z ⊗ Z + 0.3 * X ⊗ X + 0.2 * (X ⊗ Y + Y ⊗ X) - H = InfiniteMPOHamiltonian(PeriodicArray([P]), 1 => 0.7X, (1, 2) => h) - - @testset "Exact clusters and inference, N=$N" for N in 1:5 - U = @inferred MPSKit.make_cluster_expansion_mpo(H, -0.1im, Val(N), 1.0e-12) - @test U isa InfiniteMPO - @test dim(left_virtualspace(U, 1)) == sum(4^l for l in 0:(N ÷ 2)) - for L in 1:N - @test dense_mpo_window(U, 1, L) ≈ exp(-0.1im * dense_mpo_window(H, 1, L)) atol = 1.0e-10 - end - if N ≥ 3 - residual = @inferred MPSKit.evolution_cluster_residual(H, U, 1, -0.1im, Val(N)) - @inferred MPSKit.solve_center_correction(U, 1, residual, Val(N)) - end - if N in (3, 4) - larger = dense_mpo_window(U, 1, N + 1) - reflected = permute(larger, (Tuple((N + 1):-1:1), Tuple((2N + 2):-1:(N + 2)))) - @test larger ≈ reflected atol = 1.0e-10 - U2 = make_time_mpo(repeat(H, 2), 0.1, ClusterExpansion(N)) - @test dense_mpo_window(U2, 2, N + 1) ≈ larger atol = 1.0e-10 - end - end - - @testset "Leading error, N=$N" for N in 2:4 - for model in (H, open_boundary_conditions(H, 5)) - errors = [ - norm( - dense_mpo_window(make_time_mpo(model, dt, ClusterExpansion(N)), 1, N + 1) - - exp(-im * dt * dense_mpo_window(model, 1, N + 1)) - ) for dt in (0.04, 0.02) - ] - @test log2(errors[1] / errors[2]) ≥ N - 0.15 - end - end - - @testset "Finite boundaries and cluster-size cap, L=$L" for L in (1, 2, 3, 6) - finite = open_boundary_conditions(H, L) - U = make_time_mpo(finite, 0.1, ClusterExpansion(L + 3; tol = 0.5)) - @test U isa FiniteMPO - @test dim(left_virtualspace(U, 1)) == dim(right_virtualspace(U, L)) == 1 - @test dense_mpo_window(U) ≈ exp(-0.1im * convert(TensorMap, finite)) atol = 1.0e-10 - end - @testset "Finite/infinite consistency, N=$N" for N in (2, 3, 4, 5) - finite = make_time_mpo(open_boundary_conditions(H, 6), 0.1, ClusterExpansion(N)) - infinite = make_time_mpo(H, 0.1, ClusterExpansion(N)) - @test dense_mpo_window(finite) ≈ dense_mpo_window(infinite, 1, 6) atol = 1.0e-10 - end - - @test_throws ArgumentError ClusterExpansion(0) - @test_throws ArgumentError ClusterExpansion(2; tol = 0) - @test_throws ArgumentError ClusterExpansion(2; tol = Inf) - @test ClusterExpansion(; N = 3).N == 3 - empty = FiniteMPOHamiltonian(similar(parent(open_boundary_conditions(H, 2)), 0)) - @test_throws ArgumentError make_time_mpo(empty, 0.1, ClusterExpansion(2)) -end - -@testset "Mixed physical spaces and boundaries" begin - rng = MersenneTwister(25) - lattices = ( - [ℂ^2, ℂ^3], [ℂ^2, ℂ^3, ℂ^2], [ℝ^2, ℝ^3], [ℙ^2, ℙ^3], - [Rep[U₁](0 => 1, 1 => 1), dual(Rep[U₁](0 => 1, 1 => 1, 2 => 1))], - [dual(Rep[SU₂](1 // 2 => 1)), Rep[SU₂](1 => 1)], - [Vect[FermionParity](0 => 1, 1 => 1), Vect[FermionParity](0 => 2, 1 => 1)], - ) - for lattice in lattices, infinite in (true, false) - Ps = infinite ? PeriodicArray(lattice) : repeat(lattice, 3)[1:5] - H = random_test_hamiltonian(rng, Ps) - for N in (4, 5) - U = @inferred MPSKit.make_cluster_expansion_mpo(H, -0.1im, Val(N), 1.0e-12) - @test length(U) == length(H) - for s in eachindex(Ps) - @test physicalspace(U, s) == Ps[s] - (infinite || s < length(H)) && @test right_virtualspace(U, s) == left_virtualspace(U, s + 1) - width = infinite ? N : min(N, length(H) - s + 1) - @test dense_mpo_window(U, s, width) ≈ exp(-0.1im * dense_mpo_window(H, s, width)) atol = 1.0e-10 - end - end - end -end - -@testset "Time steps and rank-deficient environments" begin - rng = MersenneTwister(24) - Z = σᶻ() - P = space(Z, 1) - commuting = InfiniteMPOHamiltonian(PeriodicArray([P]), (1, 2) => Z ⊗ Z) - U = make_time_mpo(commuting, 0.1, ClusterExpansion(4)) - @test dense_mpo_window(U, 1, 5) ≈ exp(-0.1im * dense_mpo_window(commuting, 1, 5)) atol = 1.0e-10 - - for Ps in (PeriodicArray([ℂ^2, ℂ^3]), [ℂ^2, ℂ^3, ℂ^2, ℂ^3, ℂ^2]) - H = random_test_hamiltonian(rng, Ps) - onsite = random_test_hamiltonian(rng, Ps; onsite_only = true) - dimers = random_test_hamiltonian(rng, Ps; dimers = true) - for (model, dt) in ((onsite, 0.1), (dimers, 0.1), (H, 0.0), (H, -0.1im), (H, -0.1)) - U = make_time_mpo(model, dt, ClusterExpansion(5)) - width = isfinite(model) || model === H && !iszero(dt) ? 5 : 6 - starts = isfinite(model) ? (1:1) : eachindex(Ps) - for s in starts - @test dense_mpo_window(U, s, width) ≈ exp(-im * dt * dense_mpo_window(model, s, width)) atol = 1.0e-10 - end - model === onsite && @test dim(left_virtualspace(U, 1)) == 1 - end - # The SVD cutoff changes the channels, not the matched exponentials. - for N in (4, 5) - U = @inferred MPSKit.make_cluster_expansion_mpo(H, -0.1im, Val(N), 0.5) - starts = isfinite(H) ? (1:(length(H) - N + 1)) : eachindex(Ps) - for s in starts - @test dense_mpo_window(U, s, N) ≈ exp(-0.1im * dense_mpo_window(H, s, N)) atol = 1.0e-10 - end - end - end -end - -# Reconstruction alone does not ensure the factors retain all environment directions. -@testset "Completed SVD support" begin - L, R = ℂ^2, ℂ^3 - weak = TensorMap(ComplexF64[1 0 0; 0 1.0e-14 0], L ← R) - for C in (weak, zeros(ComplexF64, L ← R)) - A, B = @inferred MPSKit.factor_with_complementary_spaces(C, 1.0e-12) - @test norm(A * B - C) ≤ 1.0e-16 * norm(C) - @test A * pinv(A; rtol = 1.0e-12) ≈ id(storagetype(C), L) atol = 1.0e-10 - @test pinv(B; rtol = 1.0e-12) * B ≈ id(storagetype(C), R) atol = 1.0e-10 - @test dim(only(domain(A))) == (iszero(norm(C)) ? 5 : 4) - end -end +TestSuite.timeevolution_clusterexpansion_nonperturbative(Vector; fast = fast_tests) +TestSuite.timeevolution_clusterexpansion_mixed_spaces(Vector; fast = fast_tests) +TestSuite.timeevolution_clusterexpansion_timesteps(Vector; fast = fast_tests) +TestSuite.timeevolution_clusterexpansion_svd_support(Vector; fast = fast_tests) diff --git a/test/timeevolution/taylorcluster.jl b/test/timeevolution/taylorcluster.jl index 19042066c..45b602f56 100644 --- a/test/timeevolution/taylorcluster.jl +++ b/test/timeevolution/taylorcluster.jl @@ -4,135 +4,7 @@ println(" --------------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using MPSKit: fuse_mul_mpo -using TensorKit -using TensorKit: ℙ +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite -@testset "TaylorCluster time evolution" begin - L = 4 - dt = 0.05 - dτ = -im * dt - - @testset "O(1) exact expression" begin - alg = TaylorCluster(; N = 1, compression = true, extension = true) - for H in (transverse_field_ising(), heisenberg_XXX()) - # Infinite - mpo = make_time_mpo(H, dt, alg) - O = mpo[1] - I, A, B, C, D = H[1][1], H.A[1], H.B[1], H.C[1], H.D[1] - - @test size(O, 1) == size(D, 1)^2 + (size(D, 1) * size(B, 1)) - @test size(O, 4) == size(D, 4)^2 + (size(C, 4) * size(D, 4)) - - O_exact = similar(O) - O_exact[1, 1, 1, 1] = I + dτ * D + dτ^2 / 2 * fuse_mul_mpo(D, D) - O_exact[1, 1, 1, 2] = C + dτ * symm_mul_mpo(C, D) - O_exact[2, 1, 1, 1] = dτ * (B + dτ * symm_mul_mpo(B, D)) - O_exact[2, 1, 1, 2] = A + dτ * (symm_mul_mpo(A, D) + symm_mul_mpo(C, B)) - - @test all(isapprox.(parent(O), parent(O_exact))) - - # Finite - H_fin = open_boundary_conditions(H, L) - mpo_fin = make_time_mpo(H_fin, dt, alg) - mpo_fin2 = open_boundary_conditions(mpo, L) - for i in 1:L - @test all(isapprox.(parent(mpo_fin[i]), parent(mpo_fin2[i]))) - end - end - end - - @testset "O(2) exact expression" begin - alg = TaylorCluster(; N = 2, compression = true, extension = true) - for H in (transverse_field_ising(), heisenberg_XXX()) - # Infinite - mpo = make_time_mpo(H, dt, alg) - O = mpo[1] - I, A, B, C, D = H[1][1], H.A[1], H.B[1], H.C[1], H.D[1] - - @test size(O, 1) == - size(D, 1)^3 + (size(D, 1)^2 * size(B, 1)) + (size(D, 1) * size(B, 1)^2) - @test size(O, 4) == - size(D, 4)^3 + (size(C, 4) * size(D, 4)^2) + (size(D, 4) * size(C, 4)^2) - - O_exact = similar(O) - O_exact[1, 1, 1, 1] = I + dτ * D + dτ^2 / 2 * fuse_mul_mpo(D, D) + - dτ^3 / 6 * fuse_mul_mpo(fuse_mul_mpo(D, D), D) - O_exact[1, 1, 1, 2] = C + dτ * symm_mul_mpo(C, D) + - dτ^2 / 2 * symm_mul_mpo(C, D, D) - O_exact[1, 1, 1, 3] = fuse_mul_mpo(C, C) + dτ * symm_mul_mpo(C, C, D) - O_exact[2, 1, 1, 1] = dτ * - ( - B + dτ * symm_mul_mpo(B, D) + - dτ^2 / 2 * symm_mul_mpo(B, D, D) - ) - O_exact[2, 1, 1, 2] = A + dτ * symm_mul_mpo(A, D) + - dτ^2 / 2 * symm_mul_mpo(A, D, D) + - dτ * (symm_mul_mpo(C, B) + dτ * symm_mul_mpo(C, B, D)) - O_exact[2, 1, 1, 3] = 2 * (symm_mul_mpo(A, C) + dτ * symm_mul_mpo(A, C, D)) + - dτ * symm_mul_mpo(C, C, B) - O_exact[3, 1, 1, 1] = dτ^2 / 2 * - (fuse_mul_mpo(B, B) + dτ * symm_mul_mpo(B, B, D)) - O_exact[3, 1, 1, 2] = dτ * (symm_mul_mpo(A, B) + dτ * symm_mul_mpo(A, B, D)) + - dτ^2 / 2 * symm_mul_mpo(B, B, C) - O_exact[3, 1, 1, 3] = fuse_mul_mpo(A, A) + dτ * symm_mul_mpo(A, A, D) + - 2 * dτ * symm_mul_mpo(A, C, B) - - @test all(isapprox.(parent(O), parent(O_exact))) - - # Finite - H_fin = open_boundary_conditions(H, L) - mpo_fin = make_time_mpo(H_fin, dt, alg) - mpo_fin2 = open_boundary_conditions(mpo, L) - for i in 1:L - @test all(isapprox.(parent(mpo_fin[i]), parent(mpo_fin2[i]))) - end - end - end - - L = 4 - h1 = S_z_S_z(ComplexF64, Trivial; spin = 1 // 2) * 4 - h2 = S_x_S_x(ComplexF64, Trivial; spin = 1 // 2) * 4 - rectangular_lattice = fill(space(h1, 1), L) - rectangular_ising = FiniteMPOHamiltonian( - rectangular_lattice, ((i, i + 1) => h1 for i in 1:(L - 1))..., (1, 3) => h2 - ) - - Hs = [transverse_field_ising(; L), heisenberg_XXX(; L), rectangular_ising] - - Ns = [1, 2, 3] - dts = [1.0e-2, 1.0e-3] - for H in Hs - ψ = FiniteMPS(L, physicalspace(H, 1), ℂ^16) - for N in Ns - εs = zeros(ComplexF64, 2) - for (i, dt) in enumerate(dts) - ψ₀, _ = find_groundstate(ψ, H, DMRG(; verbosity = 0)) - E₀ = expectation_value(ψ₀, H) - - O = make_time_mpo(H, dt, TaylorCluster(; N = N)) - - ψ₁, _ = approximate(ψ₀, (O, ψ₀), DMRG(; verbosity = 0)) - εs[i] = norm(dot(ψ₀, ψ₁) - exp(-im * E₀ * dt)) - end - @test (log(εs[2]) - log(εs[1])) / (log(dts[2]) - log(dts[1])) ≈ N + 1 atol = 0.1 - end - - for N in Ns - εs = zeros(ComplexF64, 2) - for (i, dt) in enumerate(dts) - ψ₀, _ = find_groundstate(ψ, H, DMRG(; verbosity = 0)) - E₀ = expectation_value(ψ₀, H) - - O = make_time_mpo(H, dt, TaylorCluster(; N = N, compression = true)) - - ψ₁, _ = approximate(ψ₀, (O, ψ₀), DMRG(; verbosity = 0)) - εs[i] = norm(dot(ψ₀, ψ₁) - exp(-im * E₀ * dt)) - end - @test (log(εs[2]) - log(εs[1])) / (log(dts[2]) - log(dts[1])) ≈ N + 1 atol = 0.1 - end - end -end +TestSuite.timeevolution_taylorcluster(Vector; fast = fast_tests) diff --git a/test/timeevolution/timestep.jl b/test/timeevolution/timestep.jl index 75d282957..ff1daa7b7 100644 --- a/test/timeevolution/timestep.jl +++ b/test/timeevolution/timestep.jl @@ -4,358 +4,13 @@ println(" ----------------------------- ") -using .TestSetup -using Test, TestExtras -using MPSKit -using TensorKit -using TensorKit: ℙ -using LinearAlgebra: dot, norm -using Random - -verbosity_full = 5 -verbosity_conv = 1 - -# name a time-evolution algorithm for @testset labels ("TDVP", "TDVP2", "BUG") -algname(alg) = string(nameof(typeof(alg))) - -maxbond(ψ) = maximum(i -> dim(left_virtualspace(ψ, i)), 1:length(ψ)) - -@testset "timestep" verbose = true begin - dt = 0.1 - # every rank-adaptive algorithm gets the same cap: without one, BUG's augmentation doubles the - # bond dimension on every half-sweep with nothing ever cutting it back - algs = [TDVP(), TDVP2(; trunc = truncrank(10)), BUG(; trunc = truncrank(10))] - L = 10 - - H = force_planar(heisenberg_XXX(Float64, Trivial; spin = 1 // 2, L)) - ψ = FiniteMPS(rand, Float64, L, ℙ^2, ℙ^4) - E = expectation_value(ψ, H) - ψ₀, = find_groundstate(ψ, H) - E₀ = expectation_value(ψ₀, H) - - @testset "Finite $(algname(alg))" for alg in algs - ψ1, envs = timestep(ψ₀, H, 0.0, dt, alg) - E1 = expectation_value(ψ1, H, envs) - @test E₀ ≈ E1 atol = 1.0e-2 - @test dot(ψ1, ψ₀) ≈ exp(im * dt * E₀) atol = 1.0e-4 - - # the integrator carries no scratch space of its own - the sweep's allocator is obtained - # per `timestep` - so re-using one across steps has to reproduce the answer - ψ2, = timestep(ψ₀, H, 0.0, dt, alg) - @test dot(ψ1, ψ2) ≈ norm(ψ1) * norm(ψ2) atol = 1.0e-10 - end - - Hlazy = LazySum([3 * H, 1.55 * H, -0.1 * H]) - - @testset "Finite LazySum $(algname(alg))" for alg in algs - ψ, envs = timestep(ψ₀, Hlazy, 0.0, dt, alg) - E = expectation_value(ψ, Hlazy, envs) - @test (3 + 1.55 - 0.1) * E₀ ≈ E atol = 1.0e-2 - end - - Ht = MultipliedOperator(H, t -> 4) + MultipliedOperator(H, 1.45) - - @testset "Finite TimeDependent LazySum $(algname(alg))" for alg in algs - ψ, envs = timestep(ψ₀, Ht(1.0), 0.0, dt, alg) - E = expectation_value(ψ, Ht(1.0), envs) - - ψt, envst = timestep(ψ₀, Ht, 1.0, dt, alg) - Et = expectation_value(ψt, Ht(1.0), envst) - @test E ≈ Et atol = 1.0e-8 - end - - Ht2 = MultipliedOperator(H, t -> t < 0 ? error("t < 0!") : 4) + - MultipliedOperator(H, 1.45) - @testset "Finite TimeDependent LazySum (fix negative t issue) $(algname(alg))" for alg in algs - ψ, envs = timestep(ψ₀, Ht2, 0.0, dt, alg) - E = expectation_value(ψ, Ht2(0.0), envs) - - ψt, envst = timestep(ψ₀, Ht2, 0.0, dt, alg) - Et = expectation_value(ψt, Ht2(0.0), envst) - @test E ≈ Et atol = 1.0e-8 - end - - H = repeat(force_planar(heisenberg_XXX(; spin = 1)), 2) - ψ₀ = InfiniteMPS([ℙ^3, ℙ^3], [ℙ^50, ℙ^50]) - E₀ = expectation_value(ψ₀, H) - - # the AC and C sweeps of the infinite integrator spawn over the unit cell, and additionally run - # concurrently with each other, so they share one allocator: check both schedulers agree - @testset "Infinite TDVP ($schedname)" for (schedname, scheduler) in SCHEDULERS - with_scheduler(scheduler) do - ψ, envs = timestep(ψ₀, H, 0.0, dt, TDVP()) - E = expectation_value(ψ, H, envs) - @test E₀ ≈ E atol = 1.0e-2 - - ψ′ = copy(ψ₀) - ψ″, = timestep!(ψ′, H, 0.0, dt, TDVP()) - @test ψ″ === ψ′ - @test ψ″.AC == ψ.AC - - # evolving in the right-orthogonal gauge gives the same state - ψr, = timestep(ψ₀, H, 0.0, dt, TDVP(); leftorthflag = false) - @test abs(dot(ψ, ψr)) ≈ 1 atol = 1.0e-8 - end - end - - @testset "Infinite real TDVP" begin - Hr = force_planar(transverse_field_ising(Float64; g = 0.7)) - ψr₀ = InfiniteMPS(rand, Float64, [ℙ^2], [ℙ^8]) - # real-time evolution promotes the state, and with it the environments, to complex - ψ, envs = timestep(ψr₀, Hr, 0.0, dt, TDVP()) - @test scalartype(ψ) <: Complex - ψc, = timestep(complex(ψr₀), Hr, 0.0, dt, TDVP()) - @test abs(dot(ψ, ψc)) ≈ 1 atol = 1.0e-8 - @test expectation_value(ψ, Hr, envs) ≈ expectation_value(ψc, Hr) - # imaginary-time evolution with a real step keeps it real - ψi, = timestep(ψr₀, Hr, 0.0, dt, TDVP(); imaginary_evolution = true) - @test scalartype(ψi) == Float64 - end - - Hlazy = LazySum([3 * deepcopy(H), 1.55 * deepcopy(H), -0.1 * deepcopy(H)]) - - @testset "Infinite LazySum TDVP" begin - ψ, envs = timestep(ψ₀, Hlazy, 0.0, dt, TDVP()) - E = expectation_value(ψ, Hlazy, envs) - @test (3 + 1.55 - 0.1) * E₀ ≈ E atol = 1.0e-2 - end - - Ht = MultipliedOperator(H, t -> 4) + MultipliedOperator(H, 1.45) - - @testset "Infinite TimeDependent LazySum" begin - ψ, envs = timestep(ψ₀, Ht(1.0), 0.0, dt, TDVP()) - E = expectation_value(ψ, Ht(1.0), envs) - - ψt, envst = timestep(ψ₀, Ht, 1.0, dt, TDVP()) - Et = expectation_value(ψt, Ht(1.0), envst) - @test E ≈ Et atol = 1.0e-8 - end -end - -# BUG-specific: unlike `TDVP` there is no backward-in-time substep, which makes it a natural -# imaginary-time integrator (cf. its docstring). `TDVP`'s imaginary-time behaviour is covered by the -# CBE block below. -@testset "Finite imaginary-time BUG" begin - L = 10 - H = force_planar(heisenberg_XXX(Float64, Trivial; spin = 1 // 2, L)) - - alg = BUG(; trunc = truncrank(4)) - - Random.seed!(5) - ψi = complex(FiniteMPS(rand, Float64, L, ℙ^2, ℙ^4)) - E_start = real(expectation_value(ψi, H)) - E_prev = E_start - for _ in 1:8 - ψi, = timestep(ψi, H, 0.0, 0.1, alg; imaginary_evolution = true, normalize = true) - E_now = real(expectation_value(ψi, H)) - @test E_now ≤ E_prev + 1.0e-6 # monotone (non-increasing) energy - @test maxbond(ψi) ≤ 8 # the cap bounds the bond at `2D` throughout - E_prev = E_now - end - @test E_prev < E_start - 0.5 # substantial lowering toward the ground state - @test norm(ψi) ≈ 1 atol = 1.0e-6 # `normalize = true` renormalizes each step -end - -# The truncating path, which is BUG's distinguishing feature. `BUG(; trunc = …)` cuts the bond -# *ahead of* every local update — the bond carrying the previous half-sweep's augmentation — and -# augments without truncating, so the bond dimension oscillates between the requested rank and (at -# most) twice it. -@testset "BUG with truncation" begin - L = 8 - H = force_planar(transverse_field_ising(ComplexF64, Trivial; L, g = 1.5)) - δt, nsteps, D = 0.05, 20, 4 - - Random.seed!(11) - ψ₀ = FiniteMPS(rand, ComplexF64, L, ℙ^2, ℙ^2) # low rank to start with, entanglement grows - normalize!(ψ₀) - - tovec(ψ) = (v = convert(TensorMap, ψ); v / norm(v)) - ref = exp(-im * convert(TensorMap, H) * (nsteps * δt)) * tovec(ψ₀) - - # the cap actually bites here: the exact state needs bond 16 - ψ = ψ₀ - for k in 0:(nsteps - 1) - ψ, = timestep(ψ, H, k * δt, δt, BUG(; trunc = truncrank(D))) - @test maxbond(ψ) ≤ 2D # `D` right after the cut, at most `2D` after the augment - end - @test 1 - abs(dot(tovec(ψ), ref)) < 1.0e-3 -end - -# Genuine symmetric-tensor coverage (no `force_planar`) for the single-site finite integrators. -# BUG's augment step stacks bases per sector, so it can add or drop sectors: both `TDVP` and `BUG` -# must conserve the energy, accrue only an eigenstate phase, and preserve the total boundary charge -# (the fixed `right` virtual space at site L). TDVP2 is excluded here: it requires a `trunc` and is -# the two-site variant; these are single-site conservation properties. -fast_tests || @testset "Finite symmetric-tensor time evolution" begin - dt = 0.1 - L = 6 - - Random.seed!(2718) - H = heisenberg_XXX(ComplexF64, U1Irrep; spin = 1 // 2, L) - maxV = MPSKit.max_virtualspaces(physicalspace(H)) - ψ = FiniteMPS(physicalspace(H), maxV[2:(end - 1)]) - ψ₀, = find_groundstate(ψ, H; verbosity = 0) - E₀ = expectation_value(ψ₀, H) - - # the exact state has Schmidt rank ≤ 8 here, so this cap bounds the bond without discarding - # anything — it only stops BUG's augmentation from doubling unchecked - algs = [TDVP(), BUG(; trunc = truncrank(8))] - - @testset "U(1) Heisenberg ($(algname(alg)))" for alg in algs - ψ1, envs = timestep(ψ₀, H, 0.0, dt, alg) - E1 = expectation_value(ψ1, H, envs) - - @test E₀ ≈ E1 atol = 1.0e-2 - @test imag(E1) ≈ 0 atol = 1.0e-8 - @test dot(ψ1, ψ₀) ≈ exp(im * dt * E₀) atol = 1.0e-4 - @test right_virtualspace(ψ1, L) == right_virtualspace(ψ₀, L) - end -end - -@testset "Finite CBE-TDVP" verbose = true begin - L = 10 - H = force_planar(heisenberg_XXX(Float64, Trivial; spin = 1 // 2, L)) - Dstart, Dcap, dt = 2, 16, 0.05 - - # controlled bond expansion lets single-site TDVP grow the bond; the evolution should stay - # unitary (norm-preserving) and energy-conserving while tracking the bond-adaptive TDVP2 - # reference better than fixed-bond single-site TDVP - @testset "$(nameof(Exp))" for (Exp, kw) in - ((OptimalExpand, (;)), (SketchedExpand, (; oversampling = 4))) - Random.seed!(4) - ψ₀ = complex(FiniteMPS(rand, Float64, L, ℙ^2, ℙ^Dstart)) - alg = TDVP(; alg_expand = Exp(; trunc = truncrank(Dstart), kw...), trunc = truncrank(Dcap)) - E₀ = real(expectation_value(ψ₀, H)) - - ref, cbe, plain = ψ₀, ψ₀, ψ₀ - for _ in 1:6 - ref, = timestep(ref, H, 0.0, dt, TDVP2(; trunc = truncrank(Dcap))) - cbe, = timestep(cbe, H, 0.0, dt, alg) - plain, = timestep(plain, H, 0.0, dt, TDVP()) # stuck at Dstart - end - - @test norm(cbe) ≈ 1 atol = 1.0e-6 - @test real(expectation_value(cbe, H)) ≈ E₀ atol = 1.0e-2 - @test dim(left_virtualspace(cbe, L ÷ 2)) > Dstart - @test dim(left_virtualspace(plain, L ÷ 2)) == Dstart - @test abs(dot(ref, cbe)) > abs(dot(ref, plain)) - end - - # by default (`normalize = false`) the bond truncation preserves the norm, so it reflects the - # discarded weight; `normalize = true` renormalizes each step. This is independent of - # `imaginary_evolution`. - @testset "norm handling" begin - Random.seed!(6) - ψ₀ = complex(FiniteMPS(rand, Float64, L, ℙ^2, ℙ^Dstart)) - # a deliberately lossy cap so the truncation discards weight every step - lossy = TDVP(; alg_expand = OptimalExpand(; trunc = truncrank(2)), trunc = truncrank(2)) - - ψrt = ψ₀ - for _ in 1:12 - ψrt, = timestep(ψrt, H, 0.0, 0.5, lossy) # real time, norm preserved by default - end - @test norm(ψrt) < 1 - 1.0e-3 # truncation loss is not renormalized away - - # imaginary-time, norm preserved by default: the weight is *not* pinned to unit norm - # (imaginary-time evolution rescales the state, so the norm drifts away from 1) - ψit = ψ₀ - for _ in 1:12 - ψit, = timestep(ψit, H, 0.0, 0.5, lossy; imaginary_evolution = true) - end - @test abs(norm(ψit) - 1) > 1.0e-3 - - # imaginary-time with `normalize = true`: renormalized to unit norm each step - ψn = ψ₀ - for _ in 1:12 - ψn, = timestep(ψn, H, 0.0, 0.5, lossy; imaginary_evolution = true, normalize = true) - end - @test norm(ψn) ≈ 1 atol = 1.0e-6 - end - - @testset "imaginary-time lowers energy" begin - Random.seed!(5) - ψ₀ = complex(FiniteMPS(rand, Float64, L, ℙ^2, ℙ^Dstart)) - alg = TDVP(; alg_expand = OptimalExpand(; trunc = truncrank(Dstart)), trunc = truncrank(Dcap)) - E₀ = real(expectation_value(ψ₀, H)) - ψ = ψ₀ - for _ in 1:8 - ψ, = timestep(ψ, H, 0.0, 0.1, alg; imaginary_evolution = true, normalize = true) # gauge renormalizes - end - @test real(expectation_value(ψ, H)) < E₀ - @test dim(left_virtualspace(ψ, L ÷ 2)) > Dstart - end -end - -@testset "Truncation error" verbose = true begin - L = 10 - dt = 0.1 - H = force_planar(heisenberg_XXX(Float64, Trivial; spin = 1 // 2, L)) - Random.seed!(7) - ψ₀ = normalize!(complex(FiniteMPS(rand, Float64, L, ℙ^2, ℙ^16))) - - # fixed bond dimension sweep never discards anything, so the reported errors are exactly zero - @testset "no truncation" begin - for alg in (TDVP(), BUG()) - info = last(timestep(ψ₀, H, 0.0, dt, alg)) - @test info isa MPSKit.AlgorithmInfo - @test info.truncation_errors == zeros(L - 1) - @test info.numiter == 1 - end - end - - @testset "$(nameof(alg))" for alg in (TDVP2, BUG) - _, _, loose = timestep(ψ₀, H, 0.0, dt, alg(; trunc = truncrank(32))) - ψ, _, tight = timestep(ψ₀, H, 0.0, dt, alg(; trunc = truncrank(2))) - - @test length(tight.truncation_errors) == L - 1 - @test maximum(tight.truncation_errors) > 1.0e-4 # throw away real weight - # throw away less weight with a more forgiving truncation - @test maximum(loose.truncation_errors) < maximum(tight.truncation_errors) - # in real time with `normalize = false` the norm loss covers every cut of the step, - # of which the reported last cut per bond is a subset - @test norm(ψ₀)^2 - norm(ψ)^2 >= sum(abs2, tight.truncation_errors) - 1.0e-12 - end - - @testset "history over an evolution" begin - alg = TDVP2(; trunc = truncrank(2)) - nsteps = 4 - _, _, step = timestep(ψ₀, H, 0.0, dt, alg) - _, _, total = time_evolve(ψ₀, H, 0:dt:(nsteps * dt), alg) - - @test total.numiter == nsteps - @test length(total.truncation_errors) == nsteps - @test all(==(L - 1) ∘ length, total.truncation_errors) - @test first(total.truncation_errors) ≈ step.truncation_errors - end -end - -@testset "time_evolve" verbose = true begin - t_span = 0:0.1:0.1 - algs = [TDVP(), TDVP2(; trunc = truncrank(10)), BUG(; trunc = truncrank(10))] - - L = 10 - H = force_planar(heisenberg_XXX(; spin = 1 // 2, L)) - ψ₀ = FiniteMPS(L, ℙ^2, ℙ^1) - E₀ = expectation_value(ψ₀, H) - - @testset "Finite $(algname(alg))" for alg in algs - ψ, envs = time_evolve(ψ₀, H, t_span, alg; verbosity = 2) - E = expectation_value(ψ, H, envs) - @test E₀ ≈ E atol = 1.0e-2 - end - - H = repeat(force_planar(heisenberg_XXX(; spin = 1)), 2) - ψ₀ = InfiniteMPS([ℙ^3, ℙ^3], [ℙ^50, ℙ^50]) - E₀ = expectation_value(ψ₀, H) - - @testset "Infinite TDVP" begin - ψ, envs = time_evolve(ψ₀, H, t_span, TDVP(); verbosity = 2) - E = expectation_value(ψ, H, envs) - @test E₀ ≈ E atol = 1.0e-2 - - ψ′ = copy(ψ₀) - ψ″, = MPSKit.time_evolve!(ψ′, H, t_span, TDVP()) - @test ψ″ === ψ′ - @test ψ″.AC == ψ.AC - end -end +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite + +TestSuite.timeevolution_timestep(Vector; fast = fast_tests) +TestSuite.timeevolution_timestep_imaginary_bug(Vector; fast = fast_tests) +TestSuite.timeevolution_timestep_bug_truncation(Vector; fast = fast_tests) +TestSuite.timeevolution_timestep_symmetric(Vector; fast = fast_tests) +TestSuite.timeevolution_timestep_cbe_tdvp(Vector; fast = fast_tests) +TestSuite.timeevolution_timestep_truncation_error(Vector; fast = fast_tests) +TestSuite.timeevolution_time_evolve(Vector; fast = fast_tests)