From 6d4d6e5f92d5991aad76db96bd60cdc9769df640 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 24 Mar 2026 15:26:09 -0400 Subject: [PATCH 01/14] update testing infrastructure and Project.toml --- Project.toml | 26 ++------------- test/Project.toml | 32 ++++++++++++++++++ test/runtests.jl | 83 +++++++++-------------------------------------- 3 files changed, 51 insertions(+), 90 deletions(-) create mode 100644 test/Project.toml diff --git a/Project.toml b/Project.toml index 07f05b69a..5b0f444f4 100644 --- a/Project.toml +++ b/Project.toml @@ -32,6 +32,9 @@ TensorKitChainRulesCoreExt = "ChainRulesCore" TensorKitFiniteDifferencesExt = "FiniteDifferences" TensorKitMooncakeExt = "Mooncake" +[workspace] +projects = ["test"] + [compat] Adapt = "4" AllocCheck = "0.2.3" @@ -64,26 +67,3 @@ Zygote = "0.7" cuTENSOR = "2" julia = "1.10" -[extras] -Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -AllocCheck = "9b6a8646-10ed-4001-bbdc-1d2f46dfbb1a" -Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" -CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" -ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" -ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" -Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" -FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000" -GPUArrays = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7" -JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" -SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" -TensorOperations = "6aa20fa7-93e2-5fca-9bc0-fbd0db3c71a2" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -TestExtras = "5ed8adda-3752-4e41-b88a-e8b09835ee3a" -Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" -cuTENSOR = "011b41b2-24ef-40a8-b3eb-fa098493e9e1" - -[targets] -test = ["ArgParse", "Adapt", "Aqua", "AllocCheck", "Combinatorics", "CUDA", "cuTENSOR", "GPUArrays", "JET", "LinearAlgebra", "SafeTestsets", "TensorOperations", "Test", "TestExtras", "ChainRulesCore", "ChainRulesTestUtils", "FiniteDifferences", "Zygote", "Mooncake"] diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 000000000..a0cb81d62 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,32 @@ +name = "TensorKitTests" + +[deps] +Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +AllocCheck = "9b6a8646-10ed-4001-bbdc-1d2f46dfbb1a" +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" +Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" +FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000" +GPUArrays = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" +Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" +ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" +TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" +TensorOperations = "6aa20fa7-93e2-5fca-9bc0-fbd0db3c71a2" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestExtras = "5ed8adda-3752-4e41-b88a-e8b09835ee3a" +VectorInterface = "409d34a3-91d5-4945-b6ec-7529ddf182d8" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" +cuTENSOR = "011b41b2-24ef-40a8-b3eb-fa098493e9e1" + +[compat] +ParallelTestRunner = "2" + +[sources] +TensorKit = {path = ".."} diff --git a/test/runtests.jl b/test/runtests.jl index ad7b4006e..85fb6f2be 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,73 +1,22 @@ -# ARGS parsing -# ------------ -using ArgParse: ArgParse -using SafeTestsets: @safetestset +using ParallelTestRunner +using TensorKit -function parse_commandline(args = ARGS) - s = ArgParse.ArgParseSettings() - ArgParse.@add_arg_table! s begin - "--groups" - action => :store_arg - nargs => '+' - arg_type = String - end - return ArgParse.parse_args(args, s; as_symbols = true) -end - -settings = parse_commandline() - -if isempty(settings[:groups]) - if haskey(ENV, "GROUP") - groups = [ENV["GROUP"]] - else - groups = filter(isdir ∘ Base.Fix1(joinpath, @__DIR__), readdir(@__DIR__)) - end -else - groups = settings[:groups] -end - -checktestgroup(group) = isdir(joinpath(@__DIR__, group)) || - throw(ArgumentError("Invalid group ($group), no such folder")) -foreach(checktestgroup, groups) +testsuite = ParallelTestRunner.find_tests(@__DIR__) -@info "Loaded test groups:" groups +# Exclude non-test files +delete!(testsuite, "setup") # shared setup module +delete!(testsuite, "braidingtensor") # not part of the testsuite (see file header) -# don't run all tests on GPU, only the GPU specific ones -is_buildkite = get(ENV, "BUILDKITE", "false") == "true" +# CUDA tests: only run if CUDA is functional +using CUDA: CUDA +CUDA.functional() || filter!(k -> !startswith(k, "cuda"), testsuite) -# Run test groups -# --------------- +# On Buildkite (GPU CI runner): only run CUDA tests +get(ENV, "BUILDKITE", "false") == "true" && filter!(k -> startswith(k, "cuda"), testsuite) -"match files of the form `*.jl`, but exclude `*setup*.jl`" -istestfile(fn) = endswith(fn, ".jl") && !contains(fn, "setup") - -# process test groups -@time for group in groups - @info "Running test group: $group" - - # handle GPU cases separately - if group == "cuda" - using CUDA - CUDA.functional() || continue - @time include("cuda/tensors.jl") - @time include("cuda/factorizations.jl") - elseif is_buildkite - continue - end - - # somehow AD tests are unreasonably slow on Apple CI - # and ChainRulesTestUtils doesn't like prereleases - if group == "chainrules" || group == "mooncake" - Sys.isapple() && get(ENV, "CI", "false") == "true" && continue - isempty(VERSION.prerelease) || continue - end - - grouppath = joinpath(@__DIR__, group) - @time for file in filter(istestfile, readdir(grouppath)) - @info "Running test file: $file" - filepath = joinpath(grouppath, file) - @eval @safetestset $file begin - include($filepath) - end - end +# ChainRules / Mooncake: skip on Apple CI and on Julia prerelease builds +if (Sys.isapple() && get(ENV, "CI", "false") == "true") || !isempty(VERSION.prerelease) + filter!(k -> !startswith(k, "chainrules") && !startswith(k, "mooncake"), testsuite) end + +ParallelTestRunner.runtests(TensorKit, ARGS; testsuite) From 77f37c71f7252b0727d7c7111469e0b9a5d8fccb Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 24 Mar 2026 15:26:37 -0400 Subject: [PATCH 02/14] simplify testcode include --- test/cuda/factorizations.jl | 2 +- test/cuda/tensors.jl | 2 +- test/mooncake/factorizations.jl | 2 +- test/mooncake/indexmanipulations.jl | 2 +- test/mooncake/linalg.jl | 2 +- test/mooncake/planaroperations.jl | 2 +- test/mooncake/tangent.jl | 2 +- test/mooncake/tensoroperations.jl | 2 +- test/mooncake/vectorinterface.jl | 2 +- test/symmetries/fusiontrees.jl | 3 +-- test/symmetries/spaces.jl | 2 +- test/tensors/factorizations.jl | 2 +- test/tensors/planar.jl | 2 +- test/tensors/tensors.jl | 2 +- 14 files changed, 14 insertions(+), 15 deletions(-) diff --git a/test/cuda/factorizations.jl b/test/cuda/factorizations.jl index 1b46cb646..03a334678 100644 --- a/test/cuda/factorizations.jl +++ b/test/cuda/factorizations.jl @@ -11,7 +11,7 @@ const curandn = getglobal(CUDAExt, :curandn) const curand! = getglobal(CUDAExt, :curand!) using CUDA: rand as curand, rand! as curand!, randn as curandn, randn! as curandn! -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup spacelist = if get(ENV, "CI", "false") == "true" diff --git a/test/cuda/tensors.jl b/test/cuda/tensors.jl index 7bdd90f9d..9a6334096 100644 --- a/test/cuda/tensors.jl +++ b/test/cuda/tensors.jl @@ -10,7 +10,7 @@ const curandn = getglobal(CUDAExt, :curandn) const curand! = getglobal(CUDAExt, :curand!) using CUDA: rand as curand, rand! as curand!, randn as curandn, randn! as curandn! -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup for V in (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂) #, VSU₃) diff --git a/test/mooncake/factorizations.jl b/test/mooncake/factorizations.jl index 63d563336..747f5cfc7 100644 --- a/test/mooncake/factorizations.jl +++ b/test/mooncake/factorizations.jl @@ -6,7 +6,7 @@ using MatrixAlgebraKit using Mooncake using Random -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup mode = Mooncake.ReverseMode diff --git a/test/mooncake/indexmanipulations.jl b/test/mooncake/indexmanipulations.jl index dca3979fa..bc372fb26 100644 --- a/test/mooncake/indexmanipulations.jl +++ b/test/mooncake/indexmanipulations.jl @@ -5,7 +5,7 @@ using VectorInterface: Zero, One using Mooncake using Random -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup mode = Mooncake.ReverseMode diff --git a/test/mooncake/linalg.jl b/test/mooncake/linalg.jl index 8638f08bd..aa54bfe9b 100644 --- a/test/mooncake/linalg.jl +++ b/test/mooncake/linalg.jl @@ -3,7 +3,7 @@ using TensorKit using Mooncake using Random -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup mode = Mooncake.ReverseMode diff --git a/test/mooncake/planaroperations.jl b/test/mooncake/planaroperations.jl index fa67ea7e5..4f5f4c0c7 100644 --- a/test/mooncake/planaroperations.jl +++ b/test/mooncake/planaroperations.jl @@ -5,7 +5,7 @@ using VectorInterface: Zero, One using Mooncake using Random -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup using .TestSetup: _repartition diff --git a/test/mooncake/tangent.jl b/test/mooncake/tangent.jl index a791b061b..4e66e4091 100644 --- a/test/mooncake/tangent.jl +++ b/test/mooncake/tangent.jl @@ -4,7 +4,7 @@ using Mooncake using Random using JET, AllocCheck -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup using .TestSetup: _repartition diff --git a/test/mooncake/tensoroperations.jl b/test/mooncake/tensoroperations.jl index d787de04e..f1e694a10 100644 --- a/test/mooncake/tensoroperations.jl +++ b/test/mooncake/tensoroperations.jl @@ -5,7 +5,7 @@ using VectorInterface: One, Zero using Mooncake using Random -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup mode = Mooncake.ReverseMode diff --git a/test/mooncake/vectorinterface.jl b/test/mooncake/vectorinterface.jl index 71b04603d..859a6f992 100644 --- a/test/mooncake/vectorinterface.jl +++ b/test/mooncake/vectorinterface.jl @@ -4,7 +4,7 @@ using TensorOperations using Mooncake using Random -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup mode = Mooncake.ReverseMode diff --git a/test/symmetries/fusiontrees.jl b/test/symmetries/fusiontrees.jl index a0b734d97..190794038 100644 --- a/test/symmetries/fusiontrees.jl +++ b/test/symmetries/fusiontrees.jl @@ -14,8 +14,7 @@ using TensorKitSectors _isunitary(x::Number; kwargs...) = isapprox(x * x', one(x); kwargs...) _isunitary(x; kwargs...) = isunitary(x; kwargs...) _isone(x; kwargs...) = isapprox(x, one(x); kwargs...) - -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup @timedtestset "Fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in sectorlist diff --git a/test/symmetries/spaces.jl b/test/symmetries/spaces.jl index 75e9fd0b2..8bccded6f 100644 --- a/test/symmetries/spaces.jl +++ b/test/symmetries/spaces.jl @@ -5,7 +5,7 @@ using TensorKit: hassector, type_repr, HomSpace # TODO: remove this once type_repr works for all included types using TensorKitSectors -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup """ diff --git a/test/tensors/factorizations.jl b/test/tensors/factorizations.jl index de326cc13..c2a013694 100644 --- a/test/tensors/factorizations.jl +++ b/test/tensors/factorizations.jl @@ -3,7 +3,7 @@ using TensorKit using LinearAlgebra: LinearAlgebra using MatrixAlgebraKit: diagview -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup spacelist = try diff --git a/test/tensors/planar.jl b/test/tensors/planar.jl index 31b8c143e..ce8fd27e8 100644 --- a/test/tensors/planar.jl +++ b/test/tensors/planar.jl @@ -5,7 +5,7 @@ using TensorKit: PlanarTrivial, ℙ using TensorKit: planaradd!, planartrace!, planarcontract! using TensorOperations -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup @testset "Braiding tensor" begin diff --git a/test/tensors/tensors.jl b/test/tensors/tensors.jl index 813c25ea5..949fabf09 100644 --- a/test/tensors/tensors.jl +++ b/test/tensors/tensors.jl @@ -4,7 +4,7 @@ using TensorKit: type_repr using Combinatorics: permutations using LinearAlgebra: LinearAlgebra -@isdefined(TestSetup) || include("../setup.jl") +include("../setup.jl") using .TestSetup spacelist = try From b862086ee85335dd58e5c1af3143e8d40a44727a Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 24 Mar 2026 15:26:44 -0400 Subject: [PATCH 03/14] update CI instructions --- .github/workflows/CI.yml | 102 +++++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 32 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8880dfcf1..b2e840a83 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -11,6 +11,7 @@ on: paths-ignore: - 'docs/**' pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] workflow_dispatch: concurrency: @@ -19,36 +20,61 @@ concurrency: cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} jobs: + setup-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.mk.outputs.matrix }} + steps: + - uses: actions/checkout@v4 + - id: mk + shell: bash + run: | + # Auto-discover test groups from all subdirectory names. + # CUDA tests are included but skipped at runtime if no GPU is available (see runtests.jl). + groups=$(find test -mindepth 1 -maxdepth 1 -type d \ + | xargs -I{} basename {} | sort \ + | jq -R -s -c '[split("\n")[] | select(length > 0)]') + + # Draft PR: only run ubuntu-latest + version=1 + if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.draft }}" == "true" ]]; then + echo "matrix={\"version\":[\"1\"],\"os\":[\"ubuntu-latest\"],\"group\":${groups}}" >> "$GITHUB_OUTPUT" + else + echo "matrix={\"version\":[\"lts\",\"1\"],\"os\":[\"ubuntu-latest\",\"macOS-latest\",\"windows-latest\"],\"group\":${groups}}" >> "$GITHUB_OUTPUT" + fi + test: + name: "Tests" + needs: setup-matrix strategy: fail-fast: false - matrix: - version: - - 'lts' - - '1' - group: - - symmetries - - tensors - - other - - mooncake - - chainrules - os: - - ubuntu-latest - - macOS-latest - - windows-latest - uses: "QuantumKitHub/QuantumKitHubActions/.github/workflows/Tests.yml@main" - with: - group: "${{ matrix.group }}" - julia-version: "${{ matrix.version }}" - os: "${{ matrix.os }}" - nthreads: 4 - timeout-minutes: 120 - secrets: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + matrix: ${{ fromJSON(needs.setup-matrix.outputs.matrix) }} + runs-on: ${{ matrix.os }} + timeout-minutes: 120 + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.version }} + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + with: + test_args: '${{ matrix.group }}' + - uses: julia-actions/julia-processcoverage@v1 + with: + directories: 'src,ext' + - uses: codecov/codecov-action@v5 + with: + files: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false test-nightly: + name: "Tests (nightly)" needs: test + if: github.event.pull_request.draft != true strategy: + fail-fast: false matrix: version: - 'nightly' @@ -62,12 +88,24 @@ jobs: - ubuntu-latest - macOS-latest - windows-latest - uses: "QuantumKitHub/QuantumKitHubActions/.github/workflows/Tests.yml@main" - with: - group: "${{ matrix.group }}" - julia-version: "${{ matrix.version }}" - os: "${{ matrix.os }}" - nthreads: 4 - timeout-minutes: 120 - secrets: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + runs-on: ${{ matrix.os }} + timeout-minutes: 120 + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.version }} + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + with: + test_args: '${{ matrix.group }}' + continue-on-error: true + - uses: julia-actions/julia-processcoverage@v1 + with: + directories: 'src,ext' + - uses: codecov/codecov-action@v5 + with: + files: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false From 2d382d34bdf2b22ca0900be6923cb16925209ce3 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 24 Mar 2026 16:36:11 -0400 Subject: [PATCH 04/14] add fast testing --- test/cuda/factorizations.jl | 2 -- test/cuda/tensors.jl | 2 -- test/mooncake/factorizations.jl | 2 -- test/mooncake/indexmanipulations.jl | 2 -- test/mooncake/linalg.jl | 2 -- test/mooncake/planaroperations.jl | 2 -- test/mooncake/tangent.jl | 2 -- test/mooncake/tensoroperations.jl | 2 -- test/mooncake/vectorinterface.jl | 2 -- test/runtests.jl | 15 ++++++++++++++- test/setup.jl | 8 +++++++- test/symmetries/fusiontrees.jl | 8 +------- test/symmetries/spaces.jl | 2 -- test/tensors/diagonal.jl | 2 +- test/tensors/factorizations.jl | 26 +++++++++++--------------- test/tensors/planar.jl | 2 -- test/tensors/tensors.jl | 28 ++++++++++++---------------- 17 files changed, 46 insertions(+), 63 deletions(-) diff --git a/test/cuda/factorizations.jl b/test/cuda/factorizations.jl index 03a334678..e51b76605 100644 --- a/test/cuda/factorizations.jl +++ b/test/cuda/factorizations.jl @@ -11,8 +11,6 @@ const curandn = getglobal(CUDAExt, :curandn) const curand! = getglobal(CUDAExt, :curand!) using CUDA: rand as curand, rand! as curand!, randn as curandn, randn! as curandn! -include("../setup.jl") -using .TestSetup spacelist = if get(ENV, "CI", "false") == "true" println("Detected running on CI") diff --git a/test/cuda/tensors.jl b/test/cuda/tensors.jl index 9a6334096..0260bc287 100644 --- a/test/cuda/tensors.jl +++ b/test/cuda/tensors.jl @@ -10,8 +10,6 @@ const curandn = getglobal(CUDAExt, :curandn) const curand! = getglobal(CUDAExt, :curand!) using CUDA: rand as curand, rand! as curand!, randn as curandn, randn! as curandn! -include("../setup.jl") -using .TestSetup for V in (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂) #, VSU₃) V1, V2, V3, V4, V5 = V diff --git a/test/mooncake/factorizations.jl b/test/mooncake/factorizations.jl index 747f5cfc7..f63b9f359 100644 --- a/test/mooncake/factorizations.jl +++ b/test/mooncake/factorizations.jl @@ -6,8 +6,6 @@ using MatrixAlgebraKit using Mooncake using Random -include("../setup.jl") -using .TestSetup mode = Mooncake.ReverseMode rng = Random.default_rng() diff --git a/test/mooncake/indexmanipulations.jl b/test/mooncake/indexmanipulations.jl index bc372fb26..48f6c7223 100644 --- a/test/mooncake/indexmanipulations.jl +++ b/test/mooncake/indexmanipulations.jl @@ -5,8 +5,6 @@ using VectorInterface: Zero, One using Mooncake using Random -include("../setup.jl") -using .TestSetup mode = Mooncake.ReverseMode rng = Random.default_rng() diff --git a/test/mooncake/linalg.jl b/test/mooncake/linalg.jl index aa54bfe9b..30727a524 100644 --- a/test/mooncake/linalg.jl +++ b/test/mooncake/linalg.jl @@ -3,8 +3,6 @@ using TensorKit using Mooncake using Random -include("../setup.jl") -using .TestSetup mode = Mooncake.ReverseMode rng = Random.default_rng() diff --git a/test/mooncake/planaroperations.jl b/test/mooncake/planaroperations.jl index 4f5f4c0c7..bfffde462 100644 --- a/test/mooncake/planaroperations.jl +++ b/test/mooncake/planaroperations.jl @@ -5,8 +5,6 @@ using VectorInterface: Zero, One using Mooncake using Random -include("../setup.jl") -using .TestSetup using .TestSetup: _repartition mode = Mooncake.ReverseMode diff --git a/test/mooncake/tangent.jl b/test/mooncake/tangent.jl index 4e66e4091..ba56dcf72 100644 --- a/test/mooncake/tangent.jl +++ b/test/mooncake/tangent.jl @@ -4,8 +4,6 @@ using Mooncake using Random using JET, AllocCheck -include("../setup.jl") -using .TestSetup using .TestSetup: _repartition mode = Mooncake.ReverseMode diff --git a/test/mooncake/tensoroperations.jl b/test/mooncake/tensoroperations.jl index f1e694a10..711f3a003 100644 --- a/test/mooncake/tensoroperations.jl +++ b/test/mooncake/tensoroperations.jl @@ -5,8 +5,6 @@ using VectorInterface: One, Zero using Mooncake using Random -include("../setup.jl") -using .TestSetup mode = Mooncake.ReverseMode rng = Random.default_rng() diff --git a/test/mooncake/vectorinterface.jl b/test/mooncake/vectorinterface.jl index 859a6f992..17ebf9bbf 100644 --- a/test/mooncake/vectorinterface.jl +++ b/test/mooncake/vectorinterface.jl @@ -4,8 +4,6 @@ using TensorOperations using Mooncake using Random -include("../setup.jl") -using .TestSetup mode = Mooncake.ReverseMode rng = Random.default_rng() diff --git a/test/runtests.jl b/test/runtests.jl index 85fb6f2be..2fdb313f0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,4 +19,17 @@ if (Sys.isapple() && get(ENV, "CI", "false") == "true") || !isempty(VERSION.prer filter!(k -> !startswith(k, "chainrules") && !startswith(k, "mooncake"), testsuite) end -ParallelTestRunner.runtests(TensorKit, ARGS; testsuite) +# --fast: skip AD tests and inject fast_tests=true into each worker sandbox +fast = "--fast" in ARGS +filtered_args = filter(!=("--fast"), ARGS) +if fast + filter!(k -> !startswith(k, "chainrules") && !startswith(k, "mooncake"), testsuite) +end +setup_path = joinpath(@__DIR__, "setup.jl") +init_code = quote + const fast_tests = $fast + include($setup_path) + using .TestSetup +end + +ParallelTestRunner.runtests(TensorKit, filtered_args; testsuite, init_code) diff --git a/test/setup.jl b/test/setup.jl index afc8091a5..db9a12d6d 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -4,7 +4,7 @@ export randindextuple, randcircshift, _repartition, trivtuple export default_tol export smallset, randsector, hasfusiontensor, force_planar export random_fusion -export sectorlist +export sectorlist, fast_sectorlist export test_dim_isapprox export Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, Vfib, VIB_diag, VIB_M @@ -146,6 +146,11 @@ function test_dim_isapprox(V::ProductSpace, d::Int) return @test max(0, d - dim_c_max) ≤ dim(V) ≤ d + dim_c_max end +_isunitary(x::Number; kwargs...) = isapprox(x * x', one(x); kwargs...) +_isunitary(x; kwargs...) = isunitary(x; kwargs...) +_isone(x; kwargs...) = isapprox(x, one(x); kwargs...) + + uniquefusionsectorlist = ( Z2Irrep, Z3Irrep, Z4Irrep, Z3Irrep ⊠ Z4Irrep, U1Irrep, FermionParity, FermionParity ⊠ FermionParity, FermionNumber, @@ -169,6 +174,7 @@ sectorlist = ( genericfusionsectorlist..., multifusionsectorlist..., ) +fast_sectorlist = (Z2Irrep, SU2Irrep, FermionParity ⊠ U1Irrep ⊠ SU2Irrep, FibonacciAnyon) # spaces Vtr = (ℂ^2, (ℂ^3)', ℂ^4, ℂ^3, (ℂ^2)') diff --git a/test/symmetries/fusiontrees.jl b/test/symmetries/fusiontrees.jl index 190794038..ba2627a56 100644 --- a/test/symmetries/fusiontrees.jl +++ b/test/symmetries/fusiontrees.jl @@ -11,13 +11,7 @@ using TupleTools # TODO: remove this once type_repr works for all included types using TensorKitSectors -_isunitary(x::Number; kwargs...) = isapprox(x * x', one(x); kwargs...) -_isunitary(x; kwargs...) = isunitary(x; kwargs...) -_isone(x; kwargs...) = isapprox(x, one(x); kwargs...) -include("../setup.jl") -using .TestSetup - -@timedtestset "Fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in sectorlist +@timedtestset "Fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in (fast_tests ? fast_sectorlist : sectorlist) Istr = TensorKit.type_repr(I) N = 5 out = random_fusion(I, Val(N)) diff --git a/test/symmetries/spaces.jl b/test/symmetries/spaces.jl index 8bccded6f..8e2fca2fe 100644 --- a/test/symmetries/spaces.jl +++ b/test/symmetries/spaces.jl @@ -5,8 +5,6 @@ using TensorKit: hassector, type_repr, HomSpace # TODO: remove this once type_repr works for all included types using TensorKitSectors -include("../setup.jl") -using .TestSetup """ eval_show(x) diff --git a/test/tensors/diagonal.jl b/test/tensors/diagonal.jl index 76862a6f1..3da1fd78f 100644 --- a/test/tensors/diagonal.jl +++ b/test/tensors/diagonal.jl @@ -11,7 +11,7 @@ diagspacelist = ( @testset "DiagonalTensor with domain $V" for V in diagspacelist @timedtestset "Basic properties and algebra" begin - for T in (Float32, Float64, ComplexF32, ComplexF64, BigFloat) + for T in (fast_tests ? (Float64, ComplexF64) : (Float32, Float64, ComplexF32, ComplexF64, BigFloat)) # constructors t = @constinferred DiagonalTensorMap{T}(undef, V) t = @constinferred DiagonalTensorMap(rand(T, reduceddim(V)), V) diff --git a/test/tensors/factorizations.jl b/test/tensors/factorizations.jl index c2a013694..878d9e88c 100644 --- a/test/tensors/factorizations.jl +++ b/test/tensors/factorizations.jl @@ -3,23 +3,19 @@ using TensorKit using LinearAlgebra: LinearAlgebra using MatrixAlgebraKit: diagview -include("../setup.jl") -using .TestSetup - -spacelist = try - if ENV["CI"] == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) - elseif Sys.isapple() - (Vtr, Vℤ₃, VfU₁, VfSU₂, VIB_M) - else - (Vtr, VU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) - end + +spacelist = if fast_tests + (Vtr, Vℤ₂, VSU₂) +elseif get(ENV, "CI", "false") == "true" + println("Detected running on CI") + if Sys.iswindows() + (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) + elseif Sys.isapple() + (Vtr, Vℤ₃, VfU₁, VfSU₂, VIB_M) else - (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) + (Vtr, VU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) end -catch +else (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) end diff --git a/test/tensors/planar.jl b/test/tensors/planar.jl index ce8fd27e8..30e98ac96 100644 --- a/test/tensors/planar.jl +++ b/test/tensors/planar.jl @@ -5,8 +5,6 @@ using TensorKit: PlanarTrivial, ℙ using TensorKit: planaradd!, planartrace!, planarcontract! using TensorOperations -include("../setup.jl") -using .TestSetup @testset "Braiding tensor" begin for V in (Vtr, VU₁, VfU₁, VfSU₂, Vfib) diff --git a/test/tensors/tensors.jl b/test/tensors/tensors.jl index 949fabf09..a00fb2c20 100644 --- a/test/tensors/tensors.jl +++ b/test/tensors/tensors.jl @@ -4,23 +4,19 @@ using TensorKit: type_repr using Combinatorics: permutations using LinearAlgebra: LinearAlgebra -include("../setup.jl") -using .TestSetup - -spacelist = try - if get(ENV, "CI", "false") == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) - elseif Sys.isapple() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂, VSU₂U₁, VIB_M) #, VSU₃) - else - (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) - end + +spacelist = if fast_tests + (Vtr, Vℤ₂, VSU₂) +elseif get(ENV, "CI", "false") == "true" + println("Detected running on CI") + if Sys.iswindows() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) + elseif Sys.isapple() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂, VSU₂U₁, VIB_M) #, VSU₃) else - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) + (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) end -catch +else (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) end @@ -35,7 +31,7 @@ for V in spacelist V1, V2, V3, V4, V5 = V @timedtestset "Basic tensor properties" begin W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 - for T in (Int, Float32, Float64, ComplexF32, ComplexF64, BigFloat) + for T in (fast_tests ? (Float64, ComplexF64) : (Int, Float32, Float64, ComplexF32, ComplexF64, BigFloat)) t = @constinferred zeros(T, W) @test @constinferred(hash(t)) == hash(deepcopy(t)) @test scalartype(t) == T From d4ef6ee742d80f6400aa89983be51df7369439b7 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 24 Mar 2026 16:36:19 -0400 Subject: [PATCH 05/14] add a readme file --- test/README.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 test/README.md diff --git a/test/README.md b/test/README.md new file mode 100644 index 000000000..d978612f6 --- /dev/null +++ b/test/README.md @@ -0,0 +1,81 @@ +# TensorKit.jl test suite + +Tests use [ParallelTestRunner.jl](https://github.com/vchuravy/ParallelTestRunner.jl) for parallel +execution. Each test file runs in its own worker process. Shared helpers are loaded automatically +via `init_code` — test files do not need to include `setup.jl` themselves. + +## Running tests + +```julia +# Standard — works on all supported Julia versions +using Pkg; Pkg.test() +``` + +```bash +# Direct invocation — requires Julia 1.12+ (workspace support) +julia --project=test test/runtests.jl + +# Run only a specific group (directory prefix) +julia --project=test test/runtests.jl symmetries + +# Run only a specific file +julia --project=test test/runtests.jl tensors/factorizations + +# Fast mode: fewer sectors, fewer scalar types, AD tests skipped +julia --project=test test/runtests.jl --fast + +# Combine --fast with a group or file filter +julia --project=test test/runtests.jl --fast tensors + +# Control parallelism +julia --project=test test/runtests.jl --jobs=4 +``` + +## Test groups + +| Group | Contents | +|-------|----------| +| `symmetries` | Spaces and fusion trees | +| `tensors` | Core tensor operations, factorizations, planar tensors, diagonal tensors | +| `other` | Aqua code-quality checks, bug-fix regressions | +| `chainrules` | ChainRulesCore AD tests | +| `mooncake` | Mooncake AD tests | +| `cuda` | CUDA GPU tests (only run when a functional GPU is present) | + +## Fast mode (`--fast`) + +Skips `chainrules` and `mooncake` groups entirely, and reduces coverage in the remaining tests: + +- **Sector types**: tests only `Z2Irrep`, `SU2Irrep`, `FermionParity ⊠ U1Irrep ⊠ SU2Irrep`, + and `FibonacciAnyon` (instead of the full `sectorlist`) +- **Space lists**: tests only `(Vtr, Vℤ₂, VSU₂)` (trivial, abelian, non-abelian) +- **Scalar types**: tests only `Float64` and `ComplexF64` (instead of all integer/float variants) + +## `setup.jl` + +Defines the `TestSetup` module, which is loaded into every worker sandbox automatically. It +exports: + +- **Spaces**: `Vtr`, `Vℤ₂`, `Vfℤ₂`, `Vℤ₃`, `VU₁`, `VfU₁`, `VCU₁`, `VSU₂`, `VfSU₂`, + `VSU₂U₁`, `Vfib`, `VIB_diag`, `VIB_M` +- **Sector lists**: `sectorlist` (full), `fast_sectorlist` (reduced) +- **Utilities**: `randsector`, `smallset`, `hasfusiontensor`, `force_planar`, `random_fusion`, + `randindextuple`, `randcircshift`, `_repartition`, `trivtuple`, `test_dim_isapprox`, `default_tol` + +The `fast_tests::Bool` constant is also available in every test file (injected alongside +`TestSetup` via `init_code` in `runtests.jl`). + +## Adding a new test file + +Create a `.jl` file anywhere under `test/`. It is auto-discovered by `ParallelTestRunner` and +must be self-contained (worker processes have no shared state). `TestSetup` and `fast_tests` are +already in scope — no include needed. + +```julia +using Test, TestExtras +using TensorKit + +@testset "My tests" begin + # fast_tests and all TestSetup exports (Vtr, sectorlist, …) are available here +end +``` From bb02b982797449058f210cc475b7f03d0bf73ad7 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 24 Mar 2026 17:08:48 -0400 Subject: [PATCH 06/14] small fixes --- Project.toml | 11 ----------- test/Project.toml | 10 +++++++++- test/runtests.jl | 10 ++++++---- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Project.toml b/Project.toml index 5b0f444f4..51b799bc1 100644 --- a/Project.toml +++ b/Project.toml @@ -37,16 +37,9 @@ projects = ["test"] [compat] Adapt = "4" -AllocCheck = "0.2.3" -Aqua = "0.6, 0.7, 0.8" -ArgParse = "1.2.0" CUDA = "5.9" ChainRulesCore = "1" -ChainRulesTestUtils = "1" -Combinatorics = "1" FiniteDifferences = "0.12" -GPUArrays = "11.3.1" -JET = "0.9, 0.10, 0.11" LRUCache = "1.0.2" LinearAlgebra = "1" MatrixAlgebraKit = "0.6.5" @@ -54,16 +47,12 @@ Mooncake = "0.5" OhMyThreads = "0.8.0" Printf = "1" Random = "1" -SafeTestsets = "0.1" ScopedValues = "1.3.0" Strided = "2" TensorKitSectors = "0.3.6" TensorOperations = "5.1" -Test = "1" -TestExtras = "0.2,0.3" TupleTools = "1.5" VectorInterface = "0.4.8, 0.5" -Zygote = "0.7" cuTENSOR = "2" julia = "1.10" diff --git a/test/Project.toml b/test/Project.toml index a0cb81d62..0a68af210 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -2,7 +2,6 @@ name = "TensorKitTests" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -AllocCheck = "9b6a8646-10ed-4001-bbdc-1d2f46dfbb1a" Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" @@ -21,12 +20,21 @@ TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" TensorOperations = "6aa20fa7-93e2-5fca-9bc0-fbd0db3c71a2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestExtras = "5ed8adda-3752-4e41-b88a-e8b09835ee3a" +TupleTools = "9d95972d-f1c8-5527-a6e0-b4b365fa01f6" VectorInterface = "409d34a3-91d5-4945-b6ec-7529ddf182d8" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" cuTENSOR = "011b41b2-24ef-40a8-b3eb-fa098493e9e1" [compat] +Aqua = "0.6, 0.7, 0.8" +ChainRulesTestUtils = "1" +Combinatorics = "1" +GPUArrays = "11.3.1" +JET = "0.9, 0.10, 0.11" ParallelTestRunner = "2" +Test = "1" +TestExtras = "0.2,0.3" +Zygote = "0.7" [sources] TensorKit = {path = ".."} diff --git a/test/runtests.jl b/test/runtests.jl index 2fdb313f0..f4095b242 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,21 +9,23 @@ delete!(testsuite, "braidingtensor") # not part of the testsuite (see file heade # CUDA tests: only run if CUDA is functional using CUDA: CUDA -CUDA.functional() || filter!(k -> !startswith(k, "cuda"), testsuite) +CUDA.functional() || filter!(!startswith("cuda") ∘ first, testsuite) # On Buildkite (GPU CI runner): only run CUDA tests -get(ENV, "BUILDKITE", "false") == "true" && filter!(k -> startswith(k, "cuda"), testsuite) +get(ENV, "BUILDKITE", "false") == "true" && filter!(startswith("cuda") ∘ first, testsuite) # ChainRules / Mooncake: skip on Apple CI and on Julia prerelease builds if (Sys.isapple() && get(ENV, "CI", "false") == "true") || !isempty(VERSION.prerelease) - filter!(k -> !startswith(k, "chainrules") && !startswith(k, "mooncake"), testsuite) + filter!(!startswith("chainrules") ∘ first, testsuite) + filter!(!startswith("mooncake") ∘ first, testsuite) end # --fast: skip AD tests and inject fast_tests=true into each worker sandbox fast = "--fast" in ARGS filtered_args = filter(!=("--fast"), ARGS) if fast - filter!(k -> !startswith(k, "chainrules") && !startswith(k, "mooncake"), testsuite) + filter!(!startswith("chainrules") ∘ first, testsuite) + filter!(!startswith("mooncake") ∘ first, testsuite) end setup_path = joinpath(@__DIR__, "setup.jl") init_code = quote From 20351e6b80eea8f57923aec4654934c9d590b99c Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 24 Mar 2026 18:02:14 -0400 Subject: [PATCH 07/14] run fast tests in draft mode --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b2e840a83..f3279a1b6 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -59,7 +59,7 @@ jobs: - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 with: - test_args: '${{ matrix.group }}' + test_args: '${{ matrix.group }}${{ github.event.pull_request.draft == true && '' --fast'' || '''' }}' - uses: julia-actions/julia-processcoverage@v1 with: directories: 'src,ext' From ac5c2a0417da6b6146ea3557b11a9dd9cbce79c7 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 25 Mar 2026 08:47:00 -0400 Subject: [PATCH 08/14] rework some test infrastructure again --- .github/workflows/CI.yml | 59 +++++++++++++++++++++++-------- .github/workflows/CompatCheck.yml | 38 -------------------- 2 files changed, 44 insertions(+), 53 deletions(-) delete mode 100644 .github/workflows/CompatCheck.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f3279a1b6..3c971f604 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -23,23 +23,28 @@ jobs: setup-matrix: runs-on: ubuntu-latest outputs: - matrix: ${{ steps.mk.outputs.matrix }} + groups: ${{ steps.mk.outputs.groups }} + version: ${{ steps.mk.outputs.version }} + os: ${{ steps.mk.outputs.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - id: mk shell: bash run: | # Auto-discover test groups from all subdirectory names. - # CUDA tests are included but skipped at runtime if no GPU is available (see runtests.jl). groups=$(find test -mindepth 1 -maxdepth 1 -type d \ | xargs -I{} basename {} | sort \ | jq -R -s -c '[split("\n")[] | select(length > 0)]') + echo "groups=${groups}" >> "$GITHUB_OUTPUT" + # Draft PR: only run ubuntu-latest + version=1 if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.draft }}" == "true" ]]; then - echo "matrix={\"version\":[\"1\"],\"os\":[\"ubuntu-latest\"],\"group\":${groups}}" >> "$GITHUB_OUTPUT" + echo 'version=["1"]' >> "$GITHUB_OUTPUT" + echo 'os=["ubuntu-latest"]' >> "$GITHUB_OUTPUT" else - echo "matrix={\"version\":[\"lts\",\"1\"],\"os\":[\"ubuntu-latest\",\"macOS-latest\",\"windows-latest\"],\"group\":${groups}}" >> "$GITHUB_OUTPUT" + echo 'version=["lts","1"]' >> "$GITHUB_OUTPUT" + echo 'os=["ubuntu-latest","macOS-latest","windows-latest"]' >> "$GITHUB_OUTPUT" fi test: @@ -47,11 +52,14 @@ jobs: needs: setup-matrix strategy: fail-fast: false - matrix: ${{ fromJSON(needs.setup-matrix.outputs.matrix) }} + matrix: + version: ${{ fromJSON(needs.setup-matrix.outputs.version) }} + os: ${{ fromJSON(needs.setup-matrix.outputs.os) }} + group: ${{ fromJSON(needs.setup-matrix.outputs.groups) }} runs-on: ${{ matrix.os }} timeout-minutes: 120 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} @@ -60,6 +68,8 @@ jobs: - uses: julia-actions/julia-runtest@v1 with: test_args: '${{ matrix.group }}${{ github.event.pull_request.draft == true && '' --fast'' || '''' }}' + env: + JULIA_NUM_THREADS: "4" - uses: julia-actions/julia-processcoverage@v1 with: directories: 'src,ext' @@ -71,19 +81,14 @@ jobs: test-nightly: name: "Tests (nightly)" - needs: test + needs: [setup-matrix, test] if: github.event.pull_request.draft != true strategy: fail-fast: false matrix: version: - 'nightly' - group: - - symmetries - - tensors - - other - - mooncake - - chainrules + group: ${{ fromJSON(needs.setup-matrix.outputs.groups) }} os: - ubuntu-latest - macOS-latest @@ -91,7 +96,7 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 120 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} @@ -109,3 +114,27 @@ jobs: files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false + + compatcheck: + name: "Compat check - ${{ matrix.julia-version }}" + needs: [setup-matrix, test] + if: github.event.pull_request.draft != true + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + downgrade_mode: ['deps'] + group: ${{ fromJSON(needs.setup-matrix.outputs.groups) }} + julia-version: ['1.10', '1'] + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.julia-version }} + - uses: julia-actions/julia-downgrade-compat@v2 + with: + mode: ${{ matrix.downgrade_mode }} + skip: Random, LinearAlgebra, Test, Combinatorics + julia_version: ${{ matrix.julia-version }} + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 diff --git a/.github/workflows/CompatCheck.yml b/.github/workflows/CompatCheck.yml deleted file mode 100644 index eca191e7b..000000000 --- a/.github/workflows/CompatCheck.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Compat Check - -on: - push: - branches: - - 'master' - - 'main' - - 'release-' - tags: '*' - pull_request: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - test: - name: Compat bounds check - ${{ matrix.julia-version }} - runs-on: ubuntu-latest - strategy: - matrix: - downgrade_mode: ['deps'] - # TODO: this should be ['1.10', '1'] but that is currently broken: - # https://github.com/julia-actions/julia-downgrade-compat/issues/25 - julia-version: ['1.10', '1.12'] - steps: - - uses: actions/checkout@v6 - - uses: julia-actions/setup-julia@v2 - with: - version: ${{ matrix.julia-version }} - - uses: julia-actions/julia-downgrade-compat@v2 - with: - mode: ${{ matrix.downgrade_mode }} - skip: Random, LinearAlgebra, Test, Combinatorics - julia_version: ${{ matrix.julia-version }} - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 From 48cccb7c1d94fb061f32c48c74803eea7f0aab9b Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 25 Mar 2026 13:06:34 -0400 Subject: [PATCH 09/14] small changes to avoid accidental diagonal tensors --- test/setup.jl | 4 ++-- test/tensors/factorizations.jl | 2 +- test/tensors/tensors.jl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/setup.jl b/test/setup.jl index db9a12d6d..d1562e113 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -179,14 +179,14 @@ fast_sectorlist = (Z2Irrep, SU2Irrep, FermionParity ⊠ U1Irrep ⊠ SU2Irrep, Fi # spaces Vtr = (ℂ^2, (ℂ^3)', ℂ^4, ℂ^3, (ℂ^2)') Vℤ₂ = ( - Vect[Z2Irrep](0 => 1, 1 => 1), + Vect[Z2Irrep](0 => 2, 1 => 1), Vect[Z2Irrep](0 => 1, 1 => 2)', Vect[Z2Irrep](0 => 3, 1 => 2)', Vect[Z2Irrep](0 => 2, 1 => 3), Vect[Z2Irrep](0 => 2, 1 => 5), ) Vfℤ₂ = ( - Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](0 => 2, 1 => 1), Vect[FermionParity](0 => 1, 1 => 2)', Vect[FermionParity](0 => 2, 1 => 1)', Vect[FermionParity](0 => 2, 1 => 3), diff --git a/test/tensors/factorizations.jl b/test/tensors/factorizations.jl index 878d9e88c..046b3b90b 100644 --- a/test/tensors/factorizations.jl +++ b/test/tensors/factorizations.jl @@ -5,7 +5,7 @@ using MatrixAlgebraKit: diagview spacelist = if fast_tests - (Vtr, Vℤ₂, VSU₂) + (Vtr, Vℤ₃, VSU₂) elseif get(ENV, "CI", "false") == "true" println("Detected running on CI") if Sys.iswindows() diff --git a/test/tensors/tensors.jl b/test/tensors/tensors.jl index a00fb2c20..084030451 100644 --- a/test/tensors/tensors.jl +++ b/test/tensors/tensors.jl @@ -6,7 +6,7 @@ using LinearAlgebra: LinearAlgebra spacelist = if fast_tests - (Vtr, Vℤ₂, VSU₂) + (Vtr, Vℤ₃, VSU₂) elseif get(ENV, "CI", "false") == "true" println("Detected running on CI") if Sys.iswindows() From 38f39e653cf298b529947e6a8c9da45be25a9dd4 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 25 Mar 2026 14:56:25 -0400 Subject: [PATCH 10/14] split up chainrules tests --- .../{chainrules.jl => factorizations.jl} | 295 ------------------ test/chainrules/linalg.jl | 244 +++++++++++++++ test/chainrules/tensoroperations.jl | 215 +++++++++++++ 3 files changed, 459 insertions(+), 295 deletions(-) rename test/chainrules/{chainrules.jl => factorizations.jl} (56%) create mode 100644 test/chainrules/linalg.jl create mode 100644 test/chainrules/tensoroperations.jl diff --git a/test/chainrules/chainrules.jl b/test/chainrules/factorizations.jl similarity index 56% rename from test/chainrules/chainrules.jl rename to test/chainrules/factorizations.jl index 0b2bfe5c9..f47db82b9 100644 --- a/test/chainrules/chainrules.jl +++ b/test/chainrules/factorizations.jl @@ -11,12 +11,6 @@ using Zygote using MatrixAlgebraKit using MatrixAlgebraKit: LAPACK_HouseholderQR, LAPACK_HouseholderLQ, diagview -const _repartition = @static if isdefined(Base, :get_extension) - Base.get_extension(TensorKit, :TensorKitChainRulesCoreExt)._repartition -else - TensorKit.TensorKitChainRulesCoreExt._repartition -end - # Test utility # ------------- function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::AbstractTensorMap) @@ -40,12 +34,6 @@ end precision(::Type{<:Union{Float32, Complex{Float32}}}) = 1.0e-2 precision(::Type{<:Union{Float64, Complex{Float64}}}) = 1.0e-5 -function randindextuple(N::Int, k::Int = rand(0:N)) - @assert 0 ≤ k ≤ N - _p = randperm(N) - return (tuple(_p[1:k]...), tuple(_p[(k + 1):end]...)) -end - function test_ad_rrule(f, args...; check_inferred = false, kwargs...) test_rrule( Zygote.ZygoteRuleConfig(), f, args...; @@ -133,8 +121,6 @@ end # Tests # ----- -ChainRulesTestUtils.test_method_tables() - spacelist = ( (ℂ^2, (ℂ^3)', ℂ^3, ℂ^2, (ℂ^2)'), ( @@ -178,250 +164,14 @@ for V in spacelist I = sectortype(eltype(V)) Istr = type_repr(I) eltypes = isreal(sectortype(eltype(V))) ? (Float64, ComplexF64) : (ComplexF64,) - symmetricbraiding = BraidingStyle(sectortype(eltype(V))) isa SymmetricBraiding println("---------------------------------------") println("Auto-diff with symmetry: $Istr") println("---------------------------------------") @timedtestset "AD with symmetry $Istr" verbose = true begin V1, V2, V3, V4, V5 = V W = V1 ⊗ V2 - @timedtestset "Basic utility" begin - T1 = randn(Float64, V[1] ⊗ V[2] ← V[3] ⊗ V[4]) - T2 = randn(ComplexF64, V[1] ⊗ V[2] ← V[3] ⊗ V[4]) - - P1 = ProjectTo(T1) - @test P1(T1) == T1 - @test P1(T2) == real(T2) - - test_rrule(copy, T1) - test_rrule(copy, T2) - test_rrule(TensorKit.copy_oftype, T1, ComplexF64) - if symmetricbraiding - test_rrule(convert, Array, T1) - test_rrule( - TensorMap, convert(Array, T1), codomain(T1), domain(T1); - fkwargs = (; tol = Inf) - ) - end - - test_rrule(Base.getproperty, T1, :data) - test_rrule(TensorMap{scalartype(T1)}, T1.data, T1.space) - test_rrule(Base.getproperty, T2, :data) - test_rrule(TensorMap{scalartype(T2)}, T2.data, T2.space) - end - - @timedtestset "Basic utility (DiagonalTensor)" begin - for v in V - rdim = reduceddim(v) - D1 = DiagonalTensorMap(randn(rdim), v) - D2 = DiagonalTensorMap(randn(rdim), v) - D = D1 + im * D2 - T1 = TensorMap(D1) - T2 = TensorMap(D2) - T = T1 + im * T2 - - # real -> real - P1 = ProjectTo(D1) - @test P1(D1) == D1 - @test P1(T1) == D1 - - # complex -> complex - P2 = ProjectTo(D) - @test P2(D) == D - @test P2(T) == D - - # real -> complex - @test P2(D1) == D1 + 0 * im * D1 - @test P2(T1) == D1 + 0 * im * D1 - - # complex -> real - @test P1(D) == D1 - @test P1(T) == D1 - - test_rrule(DiagonalTensorMap, D1.data, D1.domain) - test_rrule(DiagonalTensorMap, D.data, D.domain) - test_rrule(Base.getproperty, D, :data) - test_rrule(Base.getproperty, D1, :data) - - test_rrule(DiagonalTensorMap, rand!(T1)) - test_rrule(DiagonalTensorMap, randn!(T)) - end - end - - @timedtestset "Basic Linear Algebra with scalartype $T" for T in eltypes - A = randn(T, V[1] ⊗ V[2] ← V[3] ⊗ V[4] ⊗ V[5]) - B = randn(T, space(A)) - - test_rrule(real, A) - test_rrule(imag, A) - - test_rrule(+, A, B) - test_rrule(-, A) - test_rrule(-, A, B) - - α = randn(T) - test_rrule(*, α, A) - test_rrule(*, A, α) - - C = randn(T, domain(A), codomain(A)) - test_rrule(*, A, C) - - test_rrule(transpose, A, ((2, 5, 4), (1, 3))) - symmetricbraiding && test_rrule(permute, A, ((1, 3, 2), (5, 4))) - test_rrule(twist, A, 1) - test_rrule(twist, A, [1, 3]) - - test_rrule(flip, A, 1) - test_rrule(flip, A, [1, 3, 4]) - - D = randn(T, V[1] ⊗ V[2] ← V[3]) - E = randn(T, V[4] ← V[5]) - symmetricbraiding && test_rrule(⊗, D, E) - end - - @timedtestset "Linear Algebra part II with scalartype $T" for T in eltypes - atol = precision(T) - rtol = precision(T) - for i in 1:3 - E = randn(T, ⊗(V[1:i]...) ← ⊗(V[1:i]...)) - test_rrule(LinearAlgebra.tr, E; atol, rtol) - test_rrule(exp, E; check_inferred = false, atol, rtol) - test_rrule(inv, E; atol, rtol) - end - - A = randn(T, V[1] ⊗ V[2] ← V[3] ⊗ V[4] ⊗ V[5]) - test_rrule(LinearAlgebra.adjoint, A; atol, rtol) - test_rrule(LinearAlgebra.norm, A, 2; atol, rtol) - - B = randn(T, space(A)) - test_rrule(LinearAlgebra.dot, A, B; atol, rtol) - end - - @timedtestset "Matrix functions ($T)" for T in eltypes - atol = precision(T) - rtol = precision(T) - for f in (sqrt, exp) - check_inferred = false # !(T <: Real) # not type-stable for real functions - t1 = randn(T, V[1] ← V[1]) - t2 = randn(T, V[2] ← V[2]) - d = DiagonalTensorMap{T}(undef, V[1]) - d2 = DiagonalTensorMap{T}(undef, V[1]) - d3 = DiagonalTensorMap{T}(undef, V[1]) - if (T <: Real && f === sqrt) - # ensuring no square root of negative numbers - randexp!(d.data) - d.data .+= 5 - randexp!(d2.data) - d2.data .+= 5 - randexp!(d3.data) - d3.data .+= 5 - else - randn!(d.data) - randn!(d2.data) - randn!(d3.data) - end - - test_rrule(f, t1; rrule_f = Zygote.rrule_via_ad, check_inferred, atol, rtol) - test_rrule(f, t2; rrule_f = Zygote.rrule_via_ad, check_inferred, atol, rtol) - test_rrule(f, d ⊢ d2; check_inferred, output_tangent = d3, atol, rtol) - end - end - - symmetricbraiding && - @timedtestset "TensorOperations with scalartype $T" for T in eltypes - atol = precision(T) - rtol = precision(T) - - @timedtestset "tensortrace!" begin - for _ in 1:5 - k1 = rand(0:2) - k2 = rand(1:2) - V1 = map(v -> rand(Bool) ? v' : v, rand(V, k1)) - V2 = map(v -> rand(Bool) ? v' : v, rand(V, k2)) - - (_p, _q) = randindextuple(k1 + 2 * k2, k1) - p = _repartition(_p, rand(0:k1)) - q = _repartition(_q, k2) - ip = _repartition(invperm(linearize((_p, _q))), rand(0:(k1 + 2 * k2))) - A = randn(T, permute(prod(V1) ⊗ prod(V2) ← prod(V2), ip)) - - α = randn(T) - β = randn(T) - for conjA in (false, true) - C = randn!(TensorOperations.tensoralloc_add(T, A, p, conjA, Val(false))) - test_rrule(tensortrace!, C, A, p, q, conjA, α, β; atol, rtol) - end - end - end - - @timedtestset "tensoradd!" begin - A = randn(T, V[1] ⊗ V[2] ← V[4] ⊗ V[5]) - α = randn(T) - β = randn(T) - - # repeat a couple times to get some distribution of arrows - for _ in 1:5 - p = randindextuple(numind(A)) - - C1 = randn!(TensorOperations.tensoralloc_add(T, A, p, false, Val(false))) - test_rrule(tensoradd!, C1, A, p, false, α, β; atol, rtol) - - C2 = randn!(TensorOperations.tensoralloc_add(T, A, p, true, Val(false))) - test_rrule(tensoradd!, C2, A, p, true, α, β; atol, rtol) - - A = rand(Bool) ? C1 : C2 - end - end - - @timedtestset "tensorcontract!" begin - for _ in 1:5 - d = 0 - local V1, V2, V3 - # retry a couple times to make sure there are at least some nonzero elements - for _ in 1:10 - k1 = rand(0:3) - k2 = rand(0:2) - k3 = rand(0:2) - V1 = prod(v -> rand(Bool) ? v' : v, rand(V, k1); init = one(V[1])) - V2 = prod(v -> rand(Bool) ? v' : v, rand(V, k2); init = one(V[1])) - V3 = prod(v -> rand(Bool) ? v' : v, rand(V, k3); init = one(V[1])) - d = min(dim(V1 ← V2), dim(V1' ← V2), dim(V2 ← V3), dim(V2' ← V3)) - d > 0 && break - end - ipA = randindextuple(length(V1) + length(V2)) - pA = _repartition(invperm(linearize(ipA)), length(V1)) - ipB = randindextuple(length(V2) + length(V3)) - pB = _repartition(invperm(linearize(ipB)), length(V2)) - pAB = randindextuple(length(V1) + length(V3)) - - α = randn(T) - β = randn(T) - V2_conj = prod(conj, V2; init = one(V[1])) - - for conjA in (false, true), conjB in (false, true) - A = randn(T, permute(V1 ← (conjA ? V2_conj : V2), ipA)) - B = randn(T, permute((conjB ? V2_conj : V2) ← V3, ipB)) - C = randn!( - TensorOperations.tensoralloc_contract( - T, A, pA, conjA, B, pB, conjB, pAB, Val(false) - ) - ) - test_rrule( - tensorcontract!, C, A, pA, conjA, B, pB, conjB, pAB, α, β; - atol, rtol - ) - end - end - end - - @timedtestset "tensorscalar" begin - A = randn(T, ProductSpace{typeof(V[1]), 0}()) - test_rrule(tensorscalar, A) - end - end @timedtestset "Factorizations" begin - W = V[1] ⊗ V[2] @testset "QR" begin for T in eltypes, t in ( @@ -456,16 +206,6 @@ for V in spacelist fkwargs, atol, rtol, output_tangent = ΔQ ) test_ad_rrule(last ∘ qr_full, t; fkwargs, atol, rtol, output_tangent = ΔR) - - # TODO: figure out the following: - # N = qr_null(t) - # ΔN = Q * rand(T, domain(Q) ← domain(N)) - # test_ad_rrule(qr_null, t; fkwargs, atol, rtol, output_tangent=ΔN) - - # if fuse(domain(t)) ≺ fuse(codomain(t)) - # _, null_pb = Zygote.pullback(qr_null, t) - # @test_logs (:warn, r"^`qr") match_mode = :any null_pb(rand_tangent(N)) - # end end end @@ -504,17 +244,6 @@ for V in spacelist fkwargs, atol, rtol, output_tangent = ΔL ) test_ad_rrule(last ∘ lq_full, t; fkwargs, atol, rtol, output_tangent = ΔQ) - - # TODO: figure out the following - # Nᴴ = lq_null(t) - # ΔN = rand(T, codomain(Nᴴ) ← codomain(Q)) * Q - # test_ad_rrule(lq_null, t; fkwargs, atol, rtol, output_tangent=Nᴴ) - - # if fuse(codomain(t)) ≺ fuse(domain(t)) - # _, null_pb = Zygote.pullback(lq_null, t) - # # broken due to typo in MAK - # # @test_logs (:warn, r"^`lq") match_mode = :any null_pb(rand_tangent(Nᴴ)) - # end end end @@ -614,17 +343,6 @@ for V in spacelist @test g1 ≈ g2 end end - - # let D = LinearAlgebra.eigvals(C) - # ΔD = diag(randn(complex(scalartype(C)), space(C))) - # test_rrule(LinearAlgebra.eigvals, C; atol, output_tangent=ΔD, - # fkwargs=(; sortby=nothing)) - # end - - # let S = LinearAlgebra.svdvals(C) - # ΔS = diag(randn(real(scalartype(C)), space(C))) - # test_rrule(LinearAlgebra.svdvals, C; atol, output_tangent=ΔS) - # end end end end @@ -657,16 +375,3 @@ end grad4, = Zygote.gradient(g, convert(Array, B₀)) @test convert(Array, grad3) ≈ grad4 end - -# https://github.com/quantumkithub/TensorKit.jl/issues/209 -@testset "Issue #209" begin - function f(T, D) - @tensor T[1, 4, 1, 3] * D[3, 4] - end - V = Z2Space(2, 2) - D = DiagonalTensorMap(randn(4), V) - T = randn(V ⊗ V ← V ⊗ V) - g1, = Zygote.gradient(f, T, D) - g2, = Zygote.gradient(f, T, TensorMap(D)) - @test g1 ≈ g2 -end diff --git a/test/chainrules/linalg.jl b/test/chainrules/linalg.jl new file mode 100644 index 000000000..c63b92262 --- /dev/null +++ b/test/chainrules/linalg.jl @@ -0,0 +1,244 @@ +using Test, TestExtras +using TensorKit +using TensorKit: type_repr, SectorDict +using TensorOperations +using ChainRulesCore +using ChainRulesTestUtils +using FiniteDifferences: FiniteDifferences, central_fdm, forward_fdm +using Random +using LinearAlgebra +using Zygote +using MatrixAlgebraKit + +# Test utility +# ------------- +function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::AbstractTensorMap) + return randn!(similar(x)) +end +function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::DiagonalTensorMap) + V = x.domain + return DiagonalTensorMap(randn(eltype(x), reduceddim(V)), V) +end +ChainRulesTestUtils.rand_tangent(::AbstractRNG, ::VectorSpace) = NoTangent() +function ChainRulesTestUtils.test_approx( + actual::AbstractTensorMap, expected::AbstractTensorMap, msg = ""; kwargs... + ) + for (c, b) in blocks(actual) + ChainRulesTestUtils.@test_msg msg isapprox(b, block(expected, c); kwargs...) + end + return nothing +end + +# Float32 and finite differences don't mix well +precision(::Type{<:Union{Float32, Complex{Float32}}}) = 1.0e-2 +precision(::Type{<:Union{Float64, Complex{Float64}}}) = 1.0e-5 + +function test_ad_rrule(f, args...; check_inferred = false, kwargs...) + test_rrule( + Zygote.ZygoteRuleConfig(), f, args...; + rrule_f = rrule_via_ad, check_inferred, kwargs... + ) + return nothing +end + +# project_hermitian is non-differentiable for now +_project_hermitian(x) = (x + x') / 2 + +# Tests +# ----- + +ChainRulesTestUtils.test_method_tables() + +spacelist = ( + (ℂ^2, (ℂ^3)', ℂ^3, ℂ^2, (ℂ^2)'), + ( + Vect[Z2Irrep](0 => 1, 1 => 1), + Vect[Z2Irrep](0 => 1, 1 => 2)', + Vect[Z2Irrep](0 => 2, 1 => 2)', + Vect[Z2Irrep](0 => 2, 1 => 3), + Vect[Z2Irrep](0 => 2, 1 => 2), + ), + ( + Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](0 => 1, 1 => 2)', + Vect[FermionParity](0 => 2, 1 => 1)' , + Vect[FermionParity](0 => 2, 1 => 3), + Vect[FermionParity](0 => 2, 1 => 2), + ), + ( + Vect[U1Irrep](0 => 2, 1 => 1, -1 => 1), + Vect[U1Irrep](0 => 2, 1 => 1, -1 => 1), + Vect[U1Irrep](0 => 2, 1 => 2, -1 => 1)', + Vect[U1Irrep](0 => 1, 1 => 1, -1 => 2), + Vect[U1Irrep](0 => 1, 1 => 2, -1 => 1)', + ), + ( + Vect[SU2Irrep](0 => 2, 1 // 2 => 1), + Vect[SU2Irrep](0 => 1, 1 => 1), + Vect[SU2Irrep](1 // 2 => 1, 1 => 1)', + Vect[SU2Irrep](1 // 2 => 2), + Vect[SU2Irrep](0 => 1, 1 // 2 => 1, 3 // 2 => 1)', + ), + ( + Vect[FibonacciAnyon](:I => 2, :τ => 1), + Vect[FibonacciAnyon](:I => 1, :τ => 2)', + Vect[FibonacciAnyon](:I => 2, :τ => 2)', + Vect[FibonacciAnyon](:I => 2, :τ => 3), + Vect[FibonacciAnyon](:I => 2, :τ => 2), + ), +) + +for V in spacelist + I = sectortype(eltype(V)) + Istr = type_repr(I) + eltypes = isreal(sectortype(eltype(V))) ? (Float64, ComplexF64) : (ComplexF64,) + symmetricbraiding = BraidingStyle(sectortype(eltype(V))) isa SymmetricBraiding + println("---------------------------------------") + println("Auto-diff with symmetry: $Istr") + println("---------------------------------------") + @timedtestset "AD with symmetry $Istr" verbose = true begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 + @timedtestset "Basic utility" begin + T1 = randn(Float64, V[1] ⊗ V[2] ← V[3] ⊗ V[4]) + T2 = randn(ComplexF64, V[1] ⊗ V[2] ← V[3] ⊗ V[4]) + + P1 = ProjectTo(T1) + @test P1(T1) == T1 + @test P1(T2) == real(T2) + + test_rrule(copy, T1) + test_rrule(copy, T2) + test_rrule(TensorKit.copy_oftype, T1, ComplexF64) + if symmetricbraiding + test_rrule(convert, Array, T1) + test_rrule( + TensorMap, convert(Array, T1), codomain(T1), domain(T1); + fkwargs = (; tol = Inf) + ) + end + + test_rrule(Base.getproperty, T1, :data) + test_rrule(TensorMap{scalartype(T1)}, T1.data, T1.space) + test_rrule(Base.getproperty, T2, :data) + test_rrule(TensorMap{scalartype(T2)}, T2.data, T2.space) + end + + @timedtestset "Basic utility (DiagonalTensor)" begin + for v in V + rdim = reduceddim(v) + D1 = DiagonalTensorMap(randn(rdim), v) + D2 = DiagonalTensorMap(randn(rdim), v) + D = D1 + im * D2 + T1 = TensorMap(D1) + T2 = TensorMap(D2) + T = T1 + im * T2 + + # real -> real + P1 = ProjectTo(D1) + @test P1(D1) == D1 + @test P1(T1) == D1 + + # complex -> complex + P2 = ProjectTo(D) + @test P2(D) == D + @test P2(T) == D + + # real -> complex + @test P2(D1) == D1 + 0 * im * D1 + @test P2(T1) == D1 + 0 * im * D1 + + # complex -> real + @test P1(D) == D1 + @test P1(T) == D1 + + test_rrule(DiagonalTensorMap, D1.data, D1.domain) + test_rrule(DiagonalTensorMap, D.data, D.domain) + test_rrule(Base.getproperty, D, :data) + test_rrule(Base.getproperty, D1, :data) + + test_rrule(DiagonalTensorMap, rand!(T1)) + test_rrule(DiagonalTensorMap, randn!(T)) + end + end + + @timedtestset "Basic Linear Algebra with scalartype $T" for T in eltypes + A = randn(T, V[1] ⊗ V[2] ← V[3] ⊗ V[4] ⊗ V[5]) + B = randn(T, space(A)) + + test_rrule(real, A) + test_rrule(imag, A) + + test_rrule(+, A, B) + test_rrule(-, A) + test_rrule(-, A, B) + + α = randn(T) + test_rrule(*, α, A) + test_rrule(*, A, α) + + C = randn(T, domain(A), codomain(A)) + test_rrule(*, A, C) + + test_rrule(transpose, A, ((2, 5, 4), (1, 3))) + symmetricbraiding && test_rrule(permute, A, ((1, 3, 2), (5, 4))) + test_rrule(twist, A, 1) + test_rrule(twist, A, [1, 3]) + + test_rrule(flip, A, 1) + test_rrule(flip, A, [1, 3, 4]) + + D = randn(T, V[1] ⊗ V[2] ← V[3]) + E = randn(T, V[4] ← V[5]) + symmetricbraiding && test_rrule(⊗, D, E) + end + + @timedtestset "Linear Algebra part II with scalartype $T" for T in eltypes + atol = precision(T) + rtol = precision(T) + for i in 1:3 + E = randn(T, ⊗(V[1:i]...) ← ⊗(V[1:i]...)) + test_rrule(LinearAlgebra.tr, E; atol, rtol) + test_rrule(exp, E; check_inferred = false, atol, rtol) + test_rrule(inv, E; atol, rtol) + end + + A = randn(T, V[1] ⊗ V[2] ← V[3] ⊗ V[4] ⊗ V[5]) + test_rrule(LinearAlgebra.adjoint, A; atol, rtol) + test_rrule(LinearAlgebra.norm, A, 2; atol, rtol) + + B = randn(T, space(A)) + test_rrule(LinearAlgebra.dot, A, B; atol, rtol) + end + + @timedtestset "Matrix functions ($T)" for T in eltypes + atol = precision(T) + rtol = precision(T) + for f in (sqrt, exp) + check_inferred = false # !(T <: Real) # not type-stable for real functions + t1 = randn(T, V[1] ← V[1]) + t2 = randn(T, V[2] ← V[2]) + d = DiagonalTensorMap{T}(undef, V[1]) + d2 = DiagonalTensorMap{T}(undef, V[1]) + d3 = DiagonalTensorMap{T}(undef, V[1]) + if (T <: Real && f === sqrt) + # ensuring no square root of negative numbers + randexp!(d.data) + d.data .+= 5 + randexp!(d2.data) + d2.data .+= 5 + randexp!(d3.data) + d3.data .+= 5 + else + randn!(d.data) + randn!(d2.data) + randn!(d3.data) + end + + test_rrule(f, t1; rrule_f = Zygote.rrule_via_ad, check_inferred, atol, rtol) + test_rrule(f, t2; rrule_f = Zygote.rrule_via_ad, check_inferred, atol, rtol) + test_rrule(f, d ⊢ d2; check_inferred, output_tangent = d3, atol, rtol) + end + end + end +end diff --git a/test/chainrules/tensoroperations.jl b/test/chainrules/tensoroperations.jl new file mode 100644 index 000000000..3216d8b89 --- /dev/null +++ b/test/chainrules/tensoroperations.jl @@ -0,0 +1,215 @@ +using Test, TestExtras +using TensorKit +using TensorKit: type_repr, SectorDict +using TensorOperations +using ChainRulesCore +using ChainRulesTestUtils +using FiniteDifferences: FiniteDifferences, central_fdm, forward_fdm +using Random +using LinearAlgebra +using Zygote +using MatrixAlgebraKit + +const _repartition = @static if isdefined(Base, :get_extension) + Base.get_extension(TensorKit, :TensorKitChainRulesCoreExt)._repartition +else + TensorKit.TensorKitChainRulesCoreExt._repartition +end + +# Test utility +# ------------- +function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::AbstractTensorMap) + return randn!(similar(x)) +end +function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::DiagonalTensorMap) + V = x.domain + return DiagonalTensorMap(randn(eltype(x), reduceddim(V)), V) +end +ChainRulesTestUtils.rand_tangent(::AbstractRNG, ::VectorSpace) = NoTangent() +function ChainRulesTestUtils.test_approx( + actual::AbstractTensorMap, expected::AbstractTensorMap, msg = ""; kwargs... + ) + for (c, b) in blocks(actual) + ChainRulesTestUtils.@test_msg msg isapprox(b, block(expected, c); kwargs...) + end + return nothing +end + +# Float32 and finite differences don't mix well +precision(::Type{<:Union{Float32, Complex{Float32}}}) = 1.0e-2 +precision(::Type{<:Union{Float64, Complex{Float64}}}) = 1.0e-5 + +function randindextuple(N::Int, k::Int = rand(0:N)) + @assert 0 ≤ k ≤ N + _p = randperm(N) + return (tuple(_p[1:k]...), tuple(_p[(k + 1):end]...)) +end + +function test_ad_rrule(f, args...; check_inferred = false, kwargs...) + test_rrule( + Zygote.ZygoteRuleConfig(), f, args...; + rrule_f = rrule_via_ad, check_inferred, kwargs... + ) + return nothing +end + +# Tests +# ----- + +spacelist = ( + (ℂ^2, (ℂ^3)', ℂ^3, ℂ^2, (ℂ^2)'), + ( + Vect[Z2Irrep](0 => 1, 1 => 1), + Vect[Z2Irrep](0 => 1, 1 => 2)', + Vect[Z2Irrep](0 => 2, 1 => 2)', + Vect[Z2Irrep](0 => 2, 1 => 3), + Vect[Z2Irrep](0 => 2, 1 => 2), + ), + ( + Vect[FermionParity](0 => 1, 1 => 1), + Vect[FermionParity](0 => 1, 1 => 2)', + Vect[FermionParity](0 => 2, 1 => 1)', + Vect[FermionParity](0 => 2, 1 => 3), + Vect[FermionParity](0 => 2, 1 => 2), + ), + ( + Vect[U1Irrep](0 => 2, 1 => 1, -1 => 1), + Vect[U1Irrep](0 => 2, 1 => 1, -1 => 1), + Vect[U1Irrep](0 => 2, 1 => 2, -1 => 1)', + Vect[U1Irrep](0 => 1, 1 => 1, -1 => 2), + Vect[U1Irrep](0 => 1, 1 => 2, -1 => 1)', + ), + ( + Vect[SU2Irrep](0 => 2, 1 // 2 => 1), + Vect[SU2Irrep](0 => 1, 1 => 1), + Vect[SU2Irrep](1 // 2 => 1, 1 => 1)', + Vect[SU2Irrep](1 // 2 => 2), + Vect[SU2Irrep](0 => 1, 1 // 2 => 1, 3 // 2 => 1)', + ), + ( + Vect[FibonacciAnyon](:I => 2, :τ => 1), + Vect[FibonacciAnyon](:I => 1, :τ => 2)', + Vect[FibonacciAnyon](:I => 2, :τ => 2)', + Vect[FibonacciAnyon](:I => 2, :τ => 3), + Vect[FibonacciAnyon](:I => 2, :τ => 2), + ), +) + +for V in spacelist + I = sectortype(eltype(V)) + Istr = type_repr(I) + eltypes = isreal(sectortype(eltype(V))) ? (Float64, ComplexF64) : (ComplexF64,) + symmetricbraiding = BraidingStyle(sectortype(eltype(V))) isa SymmetricBraiding + println("---------------------------------------") + println("Auto-diff with symmetry: $Istr") + println("---------------------------------------") + @timedtestset "AD with symmetry $Istr" verbose = true begin + V1, V2, V3, V4, V5 = V + + symmetricbraiding && + @timedtestset "TensorOperations with scalartype $T" for T in eltypes + atol = precision(T) + rtol = precision(T) + + @timedtestset "tensortrace!" begin + for _ in 1:5 + k1 = rand(0:2) + k2 = rand(1:2) + V1 = map(v -> rand(Bool) ? v' : v, rand(V, k1)) + V2 = map(v -> rand(Bool) ? v' : v, rand(V, k2)) + + (_p, _q) = randindextuple(k1 + 2 * k2, k1) + p = _repartition(_p, rand(0:k1)) + q = _repartition(_q, k2) + ip = _repartition(invperm(linearize((_p, _q))), rand(0:(k1 + 2 * k2))) + A = randn(T, permute(prod(V1) ⊗ prod(V2) ← prod(V2), ip)) + + α = randn(T) + β = randn(T) + for conjA in (false, true) + C = randn!(TensorOperations.tensoralloc_add(T, A, p, conjA, Val(false))) + test_rrule(tensortrace!, C, A, p, q, conjA, α, β; atol, rtol) + end + end + end + + @timedtestset "tensoradd!" begin + A = randn(T, V[1] ⊗ V[2] ← V[4] ⊗ V[5]) + α = randn(T) + β = randn(T) + + # repeat a couple times to get some distribution of arrows + for _ in 1:5 + p = randindextuple(numind(A)) + + C1 = randn!(TensorOperations.tensoralloc_add(T, A, p, false, Val(false))) + test_rrule(tensoradd!, C1, A, p, false, α, β; atol, rtol) + + C2 = randn!(TensorOperations.tensoralloc_add(T, A, p, true, Val(false))) + test_rrule(tensoradd!, C2, A, p, true, α, β; atol, rtol) + + A = rand(Bool) ? C1 : C2 + end + end + + @timedtestset "tensorcontract!" begin + for _ in 1:5 + d = 0 + local V1, V2, V3 + # retry a couple times to make sure there are at least some nonzero elements + for _ in 1:10 + k1 = rand(0:3) + k2 = rand(0:2) + k3 = rand(0:2) + V1 = prod(v -> rand(Bool) ? v' : v, rand(V, k1); init = one(V[1])) + V2 = prod(v -> rand(Bool) ? v' : v, rand(V, k2); init = one(V[1])) + V3 = prod(v -> rand(Bool) ? v' : v, rand(V, k3); init = one(V[1])) + d = min(dim(V1 ← V2), dim(V1' ← V2), dim(V2 ← V3), dim(V2' ← V3)) + d > 0 && break + end + ipA = randindextuple(length(V1) + length(V2)) + pA = _repartition(invperm(linearize(ipA)), length(V1)) + ipB = randindextuple(length(V2) + length(V3)) + pB = _repartition(invperm(linearize(ipB)), length(V2)) + pAB = randindextuple(length(V1) + length(V3)) + + α = randn(T) + β = randn(T) + V2_conj = prod(conj, V2; init = one(V[1])) + + for conjA in (false, true), conjB in (false, true) + A = randn(T, permute(V1 ← (conjA ? V2_conj : V2), ipA)) + B = randn(T, permute((conjB ? V2_conj : V2) ← V3, ipB)) + C = randn!( + TensorOperations.tensoralloc_contract( + T, A, pA, conjA, B, pB, conjB, pAB, Val(false) + ) + ) + test_rrule( + tensorcontract!, C, A, pA, conjA, B, pB, conjB, pAB, α, β; + atol, rtol + ) + end + end + end + + @timedtestset "tensorscalar" begin + A = randn(T, ProductSpace{typeof(V[1]), 0}()) + test_rrule(tensorscalar, A) + end + end + end +end + +# https://github.com/quantumkithub/TensorKit.jl/issues/209 +@testset "Issue #209" begin + function f(T, D) + @tensor T[1, 4, 1, 3] * D[3, 4] + end + V = Z2Space(2, 2) + D = DiagonalTensorMap(randn(4), V) + T = randn(V ⊗ V ← V ⊗ V) + g1, = Zygote.gradient(f, T, D) + g2, = Zygote.gradient(f, T, TensorMap(D)) + @test g1 ≈ g2 +end From 0db3438e34a1899fc5629a926df4103fd4e03e41 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 25 Mar 2026 14:57:32 -0400 Subject: [PATCH 11/14] split up factorization tests --- test/factorizations/eig.jl | 94 +++++++++++++ test/factorizations/ortho.jl | 138 ++++++++++++++++++ test/factorizations/projections.jl | 131 +++++++++++++++++ test/factorizations/svd.jl | 219 +++++++++++++++++++++++++++++ 4 files changed, 582 insertions(+) create mode 100644 test/factorizations/eig.jl create mode 100644 test/factorizations/ortho.jl create mode 100644 test/factorizations/projections.jl create mode 100644 test/factorizations/svd.jl diff --git a/test/factorizations/eig.jl b/test/factorizations/eig.jl new file mode 100644 index 000000000..9b7ae46cd --- /dev/null +++ b/test/factorizations/eig.jl @@ -0,0 +1,94 @@ +using Test, TestExtras +using TensorKit +using LinearAlgebra: LinearAlgebra +using MatrixAlgebraKit: diagview + + +spacelist = if fast_tests + (Vtr, Vℤ₃, VSU₂) +elseif get(ENV, "CI", "false") == "true" + println("Detected running on CI") + if Sys.iswindows() + (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) + elseif Sys.isapple() + (Vtr, Vℤ₃, VfU₁, VfSU₂, VIB_M) + else + (Vtr, VU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) + end +else + (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) +end + +eltypes = (Float32, ComplexF64) + +for V in spacelist + I = sectortype(first(V)) + Istr = TensorKit.type_repr(I) + println("---------------------------------------") + println("Factorizations with symmetry: $Istr") + println("---------------------------------------") + @timedtestset "Factorizations with symmetry: $Istr" verbose = true begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 + @assert !isempty(blocksectors(W)) + @assert !isempty(intersect(blocksectors(V4), blocksectors(W))) + + @testset "Eigenvalue decomposition" begin + for T in eltypes, + t in ( + rand(T, V1, V1), rand(T, W, W), rand(T, W, W)', + DiagonalTensorMap(rand(T, reduceddim(V1)), V1), + ) + + d, v = @constinferred eig_full(t) + @test t * v ≈ v * d + + d′ = @constinferred eig_vals(t) + @test d′ ≈ diagview(d) + @test d′ isa TensorKit.SectorVector + + d2 = @constinferred DiagonalTensorMap(d′) + @test d2 ≈ d + + vdv = project_hermitian!(v' * v) + @test @constinferred isposdef(vdv) + t isa DiagonalTensorMap || @test !isposdef(t) # unlikely for non-hermitian map + + nvals = round(Int, dim(domain(t)) / 2) + d, v = @constinferred eig_trunc(t; trunc = truncrank(nvals)) + @test t * v ≈ v * d + test_dim_isapprox(domain(d), nvals) + + t2 = @constinferred project_hermitian(t) + D, V = eigen(t2) + @test isisometric(V) + D̃, Ṽ = @constinferred eigh_full(t2) + @test D ≈ D̃ + @test V ≈ Ṽ + λ = minimum(real, diagview(D)) + @test cond(Ṽ) ≈ one(real(T)) + @test isposdef(t2) == isposdef(λ) + @test isposdef(t2 - λ * one(t2) + 0.1 * one(t2)) + @test !isposdef(t2 - λ * one(t2) - 0.1 * one(t2)) + + d, v = @constinferred eigh_full(t2) + @test t2 * v ≈ v * d + @test isunitary(v) + + d′ = @constinferred eigh_vals(t2) + @test d′ ≈ diagview(d) + @test d′ isa TensorKit.SectorVector + + λ = minimum(real, diagview(d)) + @test cond(v) ≈ one(real(T)) + @test isposdef(t2) == isposdef(λ) + @test isposdef(t2 - λ * one(t) + 0.1 * one(t2)) + @test !isposdef(t2 - λ * one(t) - 0.1 * one(t2)) + + d, v = @constinferred eigh_trunc(t2; trunc = truncrank(nvals)) + @test t2 * v ≈ v * d + test_dim_isapprox(domain(d), nvals) + end + end + end +end diff --git a/test/factorizations/ortho.jl b/test/factorizations/ortho.jl new file mode 100644 index 000000000..18a21c7b7 --- /dev/null +++ b/test/factorizations/ortho.jl @@ -0,0 +1,138 @@ +using Test, TestExtras +using TensorKit +using LinearAlgebra: LinearAlgebra +using MatrixAlgebraKit: diagview + + +spacelist = if fast_tests + (Vtr, Vℤ₃, VSU₂) +elseif get(ENV, "CI", "false") == "true" + println("Detected running on CI") + if Sys.iswindows() + (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) + elseif Sys.isapple() + (Vtr, Vℤ₃, VfU₁, VfSU₂, VIB_M) + else + (Vtr, VU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) + end +else + (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) +end + +eltypes = (Float32, ComplexF64) + +for V in spacelist + I = sectortype(first(V)) + Istr = TensorKit.type_repr(I) + println("---------------------------------------") + println("Factorizations with symmetry: $Istr") + println("---------------------------------------") + @timedtestset "Factorizations with symmetry: $Istr" verbose = true begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 + @assert !isempty(blocksectors(W)) + @assert !isempty(intersect(blocksectors(V4), blocksectors(W))) + + @testset "QR decomposition" begin + for T in eltypes, + t in ( + rand(T, W, W), rand(T, W, W)', rand(T, W, V4), rand(T, V4, W)', + DiagonalTensorMap(rand(T, reduceddim(V1)), V1), + ) + + Q, R = @constinferred qr_full(t) + @test Q * R ≈ t + @test isunitary(Q) + + Q, R = @constinferred qr_compact(t) + @test Q * R ≈ t + @test isisometric(Q) + + Q, R = @constinferred left_orth(t) + @test Q * R ≈ t + @test isisometric(Q) + + N = @constinferred qr_null(t) + @test isisometric(N) + @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) + + N = @constinferred left_null(t) + @test isisometric(N) + @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) + end + + # empty tensor + for T in eltypes + t = rand(T, V1 ⊗ V2, zerospace(V1)) + + Q, R = @constinferred qr_full(t) + @test Q * R ≈ t + @test isunitary(Q) + @test dim(R) == dim(t) == 0 + + Q, R = @constinferred qr_compact(t) + @test Q * R ≈ t + @test isisometric(Q) + @test dim(Q) == dim(R) == dim(t) + + Q, R = @constinferred left_orth(t) + @test Q * R ≈ t + @test isisometric(Q) + @test dim(Q) == dim(R) == dim(t) + + N = @constinferred qr_null(t) + @test isunitary(N) + @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) + end + end + + @testset "LQ decomposition" begin + for T in eltypes, + t in ( + rand(T, W, W), rand(T, W, W)', rand(T, W, V4), rand(T, V4, W)', + DiagonalTensorMap(rand(T, reduceddim(V1)), V1), + ) + + L, Q = @constinferred lq_full(t) + @test L * Q ≈ t + @test isunitary(Q) + + L, Q = @constinferred lq_compact(t) + @test L * Q ≈ t + @test isisometric(Q; side = :right) + + L, Q = @constinferred right_orth(t) + @test L * Q ≈ t + @test isisometric(Q; side = :right) + + Nᴴ = @constinferred lq_null(t) + @test isisometric(Nᴴ; side = :right) + @test norm(t * Nᴴ') ≈ 0 atol = 100 * eps(norm(t)) + end + + for T in eltypes + # empty tensor + t = rand(T, zerospace(V1), V1 ⊗ V2) + + L, Q = @constinferred lq_full(t) + @test L * Q ≈ t + @test isunitary(Q) + @test dim(L) == dim(t) == 0 + + L, Q = @constinferred lq_compact(t) + @test L * Q ≈ t + @test isisometric(Q; side = :right) + @test dim(Q) == dim(L) == dim(t) + + L, Q = @constinferred right_orth(t) + @test L * Q ≈ t + @test isisometric(Q; side = :right) + @test dim(Q) == dim(L) == dim(t) + + Nᴴ = @constinferred lq_null(t) + @test isunitary(Nᴴ) + @test norm(t * Nᴴ') ≈ 0 atol = 100 * eps(norm(t)) + end + end + end +end diff --git a/test/factorizations/projections.jl b/test/factorizations/projections.jl new file mode 100644 index 000000000..03b9876ba --- /dev/null +++ b/test/factorizations/projections.jl @@ -0,0 +1,131 @@ +using Test, TestExtras +using TensorKit +using LinearAlgebra: LinearAlgebra +using MatrixAlgebraKit: diagview + + +spacelist = if fast_tests + (Vtr, Vℤ₃, VSU₂) +elseif get(ENV, "CI", "false") == "true" + println("Detected running on CI") + if Sys.iswindows() + (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) + elseif Sys.isapple() + (Vtr, Vℤ₃, VfU₁, VfSU₂, VIB_M) + else + (Vtr, VU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) + end +else + (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) +end + +eltypes = (Float32, ComplexF64) + +for V in spacelist + I = sectortype(first(V)) + Istr = TensorKit.type_repr(I) + println("---------------------------------------") + println("Factorizations with symmetry: $Istr") + println("---------------------------------------") + @timedtestset "Factorizations with symmetry: $Istr" verbose = true begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 + @assert !isempty(blocksectors(W)) + @assert !isempty(intersect(blocksectors(V4), blocksectors(W))) + + @testset "Condition number and rank" begin + for T in eltypes, + t in ( + rand(T, W, W), rand(T, W, W)', + rand(T, W, V4), rand(T, V4, W), + rand(T, W, V4)', rand(T, V4, W)', + DiagonalTensorMap(rand(T, reduceddim(V1)), V1), + ) + + d1, d2 = dim(codomain(t)), dim(domain(t)) + r = rank(t) + @test r == min(d1, d2) + @test typeof(r) == typeof(d1) + M = left_null(t) + @test @constinferred(rank(M)) + r ≈ d1 + Mᴴ = right_null(t) + @test rank(Mᴴ) + r ≈ d2 + end + for T in eltypes + u = unitary(T, V1 ⊗ V2, V1 ⊗ V2) + @test @constinferred(cond(u)) ≈ one(real(T)) + @test @constinferred(rank(u)) == dim(V1 ⊗ V2) + + t = rand(T, zerospace(V1), W) + @test rank(t) == 0 + t2 = rand(T, zerospace(V1) * zerospace(V2), zerospace(V1) * zerospace(V2)) + @test rank(t2) == 0 + @test cond(t2) == 0.0 + end + for T in eltypes, t in (rand(T, W, W), rand(T, W, W)') + project_hermitian!(t) + vals = @constinferred LinearAlgebra.eigvals(t) + λmax = maximum(s -> maximum(abs, s), values(vals)) + λmin = minimum(s -> minimum(abs, s), values(vals)) + @test cond(t) ≈ λmax / λmin + end + end + + @testset "Hermitian projections" begin + for T in eltypes, + t in ( + rand(T, V1, V1), rand(T, W, W), rand(T, W, W)', + DiagonalTensorMap(rand(T, reduceddim(V1)), V1), + ) + normalize!(t) + noisefactor = eps(real(T))^(3 / 4) + + th = (t + t') / 2 + ta = (t - t') / 2 + tc = copy(t) + + th′ = @constinferred project_hermitian(t) + @test ishermitian(th′) + @test th′ ≈ th + @test t == tc + th_approx = th + noisefactor * ta + @test !ishermitian(th_approx) || (T <: Real && t isa DiagonalTensorMap) + @test ishermitian(th_approx; atol = 10 * noisefactor) + + ta′ = project_antihermitian(t) + @test isantihermitian(ta′) + @test ta′ ≈ ta + @test t == tc + ta_approx = ta + noisefactor * th + @test !isantihermitian(ta_approx) + @test isantihermitian(ta_approx; atol = 10 * noisefactor) || (T <: Real && t isa DiagonalTensorMap) + end + end + + @testset "Isometric projections" begin + for T in eltypes, + t in ( + randn(T, W, W), randn(T, W, W)', + randn(T, W, V4), randn(T, V4, W)', + ) + t2 = project_isometric(t) + @test isisometric(t2) + t3 = project_isometric(t2) + @test t3 ≈ t2 # stability of the projection + @test t2 * (t2' * t) ≈ t + + tc = similar(t) + t3 = @constinferred project_isometric!(copy!(tc, t), t2) + @test t3 === t2 + @test isisometric(t2) + + # test that t2 is closer to A then any other isometry + for k in 1:10 + δt = randn!(similar(t)) + t3 = project_isometric(t + δt / 100) + @test norm(t - t3) > norm(t - t2) + end + end + end + end +end diff --git a/test/factorizations/svd.jl b/test/factorizations/svd.jl new file mode 100644 index 000000000..15ff34747 --- /dev/null +++ b/test/factorizations/svd.jl @@ -0,0 +1,219 @@ +using Test, TestExtras +using TensorKit +using LinearAlgebra: LinearAlgebra +using MatrixAlgebraKit: diagview + + +spacelist = if fast_tests + (Vtr, Vℤ₃, VSU₂) +elseif get(ENV, "CI", "false") == "true" + println("Detected running on CI") + if Sys.iswindows() + (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) + elseif Sys.isapple() + (Vtr, Vℤ₃, VfU₁, VfSU₂, VIB_M) + else + (Vtr, VU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) + end +else + (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) +end + +eltypes = (Float32, ComplexF64) + +for V in spacelist + I = sectortype(first(V)) + Istr = TensorKit.type_repr(I) + println("---------------------------------------") + println("Factorizations with symmetry: $Istr") + println("---------------------------------------") + @timedtestset "Factorizations with symmetry: $Istr" verbose = true begin + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 + @assert !isempty(blocksectors(W)) + @assert !isempty(intersect(blocksectors(V4), blocksectors(W))) + + @testset "Polar decomposition" begin + for T in eltypes, + t in ( + rand(T, W, W), rand(T, W, W)', rand(T, W, V4), rand(T, V4, W)', + DiagonalTensorMap(rand(T, reduceddim(V1)), V1), + ) + + @assert domain(t) ≾ codomain(t) + w, p = @constinferred left_polar(t) + @test w * p ≈ t + @test isisometric(w) + @test isposdef(p) + + w, p = @constinferred left_orth(t; alg = :polar) + @test w * p ≈ t + @test isisometric(w) + end + + for T in eltypes, + t in (rand(T, W, W), rand(T, W, W)', rand(T, V4, W), rand(T, W, V4)') + + @assert codomain(t) ≾ domain(t) + p, wᴴ = @constinferred right_polar(t) + @test p * wᴴ ≈ t + @test isisometric(wᴴ; side = :right) + @test isposdef(p) + + p, wᴴ = @constinferred right_orth(t; alg = :polar) + @test p * wᴴ ≈ t + @test isisometric(wᴴ; side = :right) + end + end + + @testset "SVD" begin + for T in eltypes, + t in ( + rand(T, W, W), rand(T, W, W)', + rand(T, W, V4), rand(T, V4, W), + rand(T, W, V4)', rand(T, V4, W)', + DiagonalTensorMap(rand(T, reduceddim(V1)), V1), + ) + + u, s, vᴴ = @constinferred svd_full(t) + @test u * s * vᴴ ≈ t + @test isunitary(u) + @test isunitary(vᴴ) + + u, s, vᴴ = @constinferred svd_compact(t) + @test u * s * vᴴ ≈ t + @test isisometric(u) + @test isposdef(s) + @test isisometric(vᴴ; side = :right) + + s′ = @constinferred svd_vals(t) + @test s′ ≈ diagview(s) + @test s′ isa TensorKit.SectorVector + + s2 = @constinferred DiagonalTensorMap(s′) + @test s2 ≈ s + + v, c = @constinferred left_orth(t; alg = :svd) + @test v * c ≈ t + @test isisometric(v) + + c, vᴴ = @constinferred right_orth(t; alg = :svd) + @test c * vᴴ ≈ t + @test isisometric(vᴴ; side = :right) + + N = @constinferred left_null(t; alg = :svd) + @test isisometric(N) + @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) + + N = @constinferred left_null(t; trunc = (; atol = 100 * eps(norm(t)))) + @test isisometric(N) + @test norm(N' * t) ≈ 0 atol = 100 * eps(norm(t)) + + Nᴴ = @constinferred right_null(t; alg = :svd) + @test isisometric(Nᴴ; side = :right) + @test norm(t * Nᴴ') ≈ 0 atol = 100 * eps(norm(t)) + + Nᴴ = @constinferred right_null(t; trunc = (; atol = 100 * eps(norm(t)))) + @test isisometric(Nᴴ; side = :right) + @test norm(t * Nᴴ') ≈ 0 atol = 100 * eps(norm(t)) + end + + # empty tensor + for T in eltypes, t in (rand(T, W, zerospace(V1)), rand(T, zerospace(V1), W)) + U, S, Vᴴ = @constinferred svd_full(t) + @test U * S * Vᴴ ≈ t + @test isunitary(U) + @test isunitary(Vᴴ) + + U, S, Vᴴ = @constinferred svd_compact(t) + @test U * S * Vᴴ ≈ t + @test dim(U) == dim(S) == dim(Vᴴ) == dim(t) == 0 + end + end + + @testset "truncated SVD" begin + for T in eltypes, + t in ( + randn(T, W, W), randn(T, W, W)', + randn(T, W, V4), randn(T, V4, W), + randn(T, W, V4)', randn(T, V4, W)', + DiagonalTensorMap(randn(T, reduceddim(V1)), V1), + ) + + @constinferred normalize!(t) + + U, S, Vᴴ, ϵ = @constinferred svd_trunc(t; trunc = notrunc()) + @test U * S * Vᴴ ≈ t + @test ϵ ≈ 0 + @test isisometric(U) + @test isisometric(Vᴴ; side = :right) + + # when rank of t is already smaller than truncrank + t_rank = ceil(Int, min(dim(codomain(t)), dim(domain(t)))) + U, S, Vᴴ, ϵ = @constinferred svd_trunc(t; trunc = truncrank(t_rank + 1)) + @test U * S * Vᴴ ≈ t + @test ϵ ≈ 0 + @test isisometric(U) + @test isisometric(Vᴴ; side = :right) + + # dimension of S is a float for IsingBimodule + nvals = round(Int, dim(domain(S)) / 2) + trunc = truncrank(nvals) + U1, S1, Vᴴ1, ϵ1 = @constinferred svd_trunc(t; trunc) + @test t * Vᴴ1' ≈ U1 * S1 + @test isisometric(U1) + @test isisometric(Vᴴ1; side = :right) + @test norm(t - U1 * S1 * Vᴴ1) ≈ ϵ1 atol = eps(real(T))^(4 / 5) + test_dim_isapprox(domain(S1), nvals) + + λ = minimum(diagview(S1)) + trunc = trunctol(; atol = λ - 10eps(λ)) + U2, S2, Vᴴ2, ϵ2 = @constinferred svd_trunc(t; trunc) + @test t * Vᴴ2' ≈ U2 * S2 + @test isisometric(U2) + @test isisometric(Vᴴ2; side = :right) + @test norm(t - U2 * S2 * Vᴴ2) ≈ ϵ2 atol = eps(real(T))^(4 / 5) + @test minimum(diagview(S1)) >= λ + @test U2 ≈ U1 + @test S2 ≈ S1 + @test Vᴴ2 ≈ Vᴴ1 + @test ϵ1 ≈ ϵ2 + + trunc = truncspace(space(S2, 1)) + U3, S3, Vᴴ3, ϵ3 = @constinferred svd_trunc(t; trunc) + @test t * Vᴴ3' ≈ U3 * S3 + @test isisometric(U3) + @test isisometric(Vᴴ3; side = :right) + @test norm(t - U3 * S3 * Vᴴ3) ≈ ϵ3 atol = eps(real(T))^(4 / 5) + @test space(S3, 1) ≾ space(S2, 1) + + for trunc in (truncerror(; atol = ϵ2), truncerror(; rtol = ϵ2 / norm(t))) + U4, S4, Vᴴ4, ϵ4 = @constinferred svd_trunc(t; trunc) + @test t * Vᴴ4' ≈ U4 * S4 + @test isisometric(U4) + @test isisometric(Vᴴ4; side = :right) + @test norm(t - U4 * S4 * Vᴴ4) ≈ ϵ4 atol = eps(real(T))^(4 / 5) + @test ϵ4 ≤ ϵ2 + end + + trunc = truncrank(nvals) & trunctol(; atol = λ - 10eps(λ)) + U5, S5, Vᴴ5, ϵ5 = @constinferred svd_trunc(t; trunc) + @test t * Vᴴ5' ≈ U5 * S5 + @test isisometric(U5) + @test isisometric(Vᴴ5; side = :right) + @test norm(t - U5 * S5 * Vᴴ5) ≈ ϵ5 atol = eps(real(T))^(4 / 5) + @test minimum(diagview(S5)) >= λ + test_dim_isapprox(domain(S5), nvals) + + trunc = truncrank(nvals) | trunctol(; atol = λ - 10eps(λ)) + U5, S5, Vᴴ5, ϵ5 = @constinferred svd_trunc(t; trunc) + @test t * Vᴴ5' ≈ U5 * S5 + @test isisometric(U5) + @test isisometric(Vᴴ5; side = :right) + @test norm(t - U5 * S5 * Vᴴ5) ≈ ϵ5 atol = eps(real(T))^(4 / 5) + @test minimum(diagview(S5)) >= λ + test_dim_isapprox(domain(S5), nvals) + end + end + end +end From b4b1377e2f2b385380635676b4a365d4fc955413 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 25 Mar 2026 14:58:07 -0400 Subject: [PATCH 12/14] split up tensor tests --- test/chainrules/linalg.jl | 2 +- test/tensors/construction.jl | 189 ++++++++ test/tensors/contractions.jl | 199 +++++++++ test/tensors/indexmanipulations.jl | 146 +++++++ test/tensors/linalg.jl | 244 +++++++++++ test/tensors/tensors.jl | 681 ----------------------------- 6 files changed, 779 insertions(+), 682 deletions(-) create mode 100644 test/tensors/construction.jl create mode 100644 test/tensors/contractions.jl create mode 100644 test/tensors/indexmanipulations.jl create mode 100644 test/tensors/linalg.jl delete mode 100644 test/tensors/tensors.jl diff --git a/test/chainrules/linalg.jl b/test/chainrules/linalg.jl index c63b92262..460a23742 100644 --- a/test/chainrules/linalg.jl +++ b/test/chainrules/linalg.jl @@ -61,7 +61,7 @@ spacelist = ( ( Vect[FermionParity](0 => 1, 1 => 1), Vect[FermionParity](0 => 1, 1 => 2)', - Vect[FermionParity](0 => 2, 1 => 1)' , + Vect[FermionParity](0 => 2, 1 => 1)', Vect[FermionParity](0 => 2, 1 => 3), Vect[FermionParity](0 => 2, 1 => 2), ), diff --git a/test/tensors/construction.jl b/test/tensors/construction.jl new file mode 100644 index 000000000..2d9374ef8 --- /dev/null +++ b/test/tensors/construction.jl @@ -0,0 +1,189 @@ +using Test, TestExtras +using TensorKit +using TensorKit: type_repr + + +spacelist = if fast_tests + (Vtr, Vℤ₃, VSU₂) +elseif get(ENV, "CI", "false") == "true" + println("Detected running on CI") + if Sys.iswindows() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) + elseif Sys.isapple() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂, VSU₂U₁, VIB_M) #, VSU₃) + else + (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) + end +else + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) +end + +for V in spacelist + I = sectortype(first(V)) + Istr = type_repr(I) + println("---------------------------------------") + println("Tensors with symmetry: $Istr") + println("---------------------------------------") + @timedtestset "Tensors with symmetry: $Istr" verbose = true begin + V1, V2, V3, V4, V5 = V + @timedtestset "Basic tensor properties" begin + W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + for T in (fast_tests ? (Float64, ComplexF64) : (Int, Float32, Float64, ComplexF32, ComplexF64, BigFloat)) + t = @constinferred zeros(T, W) + @test @constinferred(hash(t)) == hash(deepcopy(t)) + @test scalartype(t) == T + @test norm(t) == 0 + @test codomain(t) == W + @test space(t) == (W ← one(W)) + @test domain(t) == one(W) + @test typeof(t) == TensorMap{T, spacetype(t), 5, 0, Vector{T}} + # Array type input + t = @constinferred zeros(Vector{T}, W) + @test @constinferred(hash(t)) == hash(deepcopy(t)) + @test scalartype(t) == T + @test norm(t) == 0 + @test codomain(t) == W + @test space(t) == (W ← one(W)) + @test domain(t) == one(W) + @test typeof(t) == TensorMap{T, spacetype(t), 5, 0, Vector{T}} + # blocks + bs = @constinferred blocks(t) + if !isempty(blocksectors(t)) # multifusion space ending on module gives empty data + (c, b1), state = @constinferred Nothing iterate(bs) + @test c == first(blocksectors(W)) + next = @constinferred Nothing iterate(bs, state) + b2 = @constinferred block(t, first(blocksectors(t))) + @test b1 == b2 + @test eltype(bs) === Pair{typeof(c), typeof(b1)} + @test typeof(b1) === TensorKit.blocktype(t) + @test typeof(c) === sectortype(t) + end + end + end + @timedtestset "Tensor Dict conversion" begin + W = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 + for T in (Int, Float32, ComplexF64) + t = @constinferred rand(T, W) + d = convert(Dict, t) + @test t == convert(TensorMap, d) + end + end + if hasfusiontensor(I) || I == Trivial + @timedtestset "Tensor Array conversion" begin + W1 = V1 ← one(V1) + W2 = one(V2) ← V2 + W3 = V1 ⊗ V2 ← one(V1) + W4 = V1 ← V2 + W5 = one(V1) ← V1 ⊗ V2 + W6 = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 + for W in (W1, W2, W3, W4, W5, W6) + for T in (Int, Float32, ComplexF64) + if T == Int + t = TensorMap{T}(undef, W) + for (_, b) in blocks(t) + rand!(b, -20:20) + end + else + t = @constinferred randn(T, W) + end + a = @constinferred convert(Array, t) + b = reshape(a, dim(codomain(W)), dim(domain(W))) + @test t ≈ @constinferred TensorMap(a, W) + @test t ≈ @constinferred TensorMap(b, W) + @test t === @constinferred TensorMap(t.data, W) + end + end + for T in (Int, Float32, ComplexF64) + t = randn(T, V1 ⊗ V2 ← zerospace(V1)) + a = convert(Array, t) + @test norm(a) == 0 + end + end + end + if hasfusiontensor(I) + @timedtestset "Real and imaginary parts" begin + W = V1 ⊗ V2 + for T in (Float64, ComplexF64, ComplexF32) + t = @constinferred randn(T, W, W) + + tr = @constinferred real(t) + @test scalartype(tr) <: Real + @test real(convert(Array, t)) == convert(Array, tr) + + ti = @constinferred imag(t) + @test scalartype(ti) <: Real + @test imag(convert(Array, t)) == convert(Array, ti) + + tc = @inferred complex(t) + @test scalartype(tc) <: Complex + @test complex(convert(Array, t)) == convert(Array, tc) + + tc2 = @inferred complex(tr, ti) + @test tc2 ≈ tc + end + end + end + @timedtestset "Tensor conversion" begin + W = V1 ⊗ V2 + t = @constinferred randn(W ← W) + @test typeof(convert(TensorMap, t')) == typeof(t) + tc = complex(t) + @test convert(typeof(tc), t) == tc + @test typeof(convert(typeof(tc), t)) == typeof(tc) + @test typeof(convert(typeof(tc), t')) == typeof(tc) + @test Base.promote_typeof(t, tc) == typeof(tc) + @test Base.promote_typeof(tc, t) == typeof(tc + t) + end + end + TensorKit.empty_globalcaches!() +end + +@timedtestset "show tensors" begin + for V in (ℂ^2, Z2Space(0 => 2, 1 => 2), SU2Space(0 => 2, 1 => 2)) + t1 = ones(Float32, V ⊗ V, V) + t2 = randn(ComplexF64, V ⊗ V ⊗ V) + t3 = randn(Float64, zero(V), zero(V)) + # test unlimited output + for t in (t1, t2, t1', t2', t3) + output = IOBuffer() + summary(output, t) + print(output, ":\n codomain: ") + show(output, MIME("text/plain"), codomain(t)) + print(output, "\n domain: ") + show(output, MIME("text/plain"), domain(t)) + print(output, "\n blocks: \n") + first = true + for (c, b) in blocks(t) + first || print(output, "\n\n") + print(output, " * ") + show(output, MIME("text/plain"), c) + print(output, " => ") + show(output, MIME("text/plain"), b) + first = false + end + outputstr = String(take!(output)) + @test outputstr == sprint(show, MIME("text/plain"), t) + end + + # test limited output with a single block + t = randn(Float64, V ⊗ V, V)' # we know there is a single space in the codomain, so that blocks have 2 rows + output = IOBuffer() + summary(output, t) + print(output, ":\n codomain: ") + show(output, MIME("text/plain"), codomain(t)) + print(output, "\n domain: ") + show(output, MIME("text/plain"), domain(t)) + print(output, "\n blocks: \n") + c = unit(sectortype(t)) + b = block(t, c) + print(output, " * ") + show(output, MIME("text/plain"), c) + print(output, " => ") + show(output, MIME("text/plain"), b) + if length(blocks(t)) > 1 + print(output, "\n\n * … [output of 1 more block(s) truncated]") + end + outputstr = String(take!(output)) + @test outputstr == sprint(show, MIME("text/plain"), t; context = (:limit => true, :displaysize => (12, 100))) + end +end diff --git a/test/tensors/contractions.jl b/test/tensors/contractions.jl new file mode 100644 index 000000000..4ca0ace38 --- /dev/null +++ b/test/tensors/contractions.jl @@ -0,0 +1,199 @@ +using Test, TestExtras +using TensorKit +using TensorKit: type_repr + + +spacelist = if fast_tests + (Vtr, Vℤ₃, VSU₂) +elseif get(ENV, "CI", "false") == "true" + println("Detected running on CI") + if Sys.iswindows() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) + elseif Sys.isapple() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂, VSU₂U₁, VIB_M) #, VSU₃) + else + (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) + end +else + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) +end + +for V in spacelist + I = sectortype(first(V)) + Istr = type_repr(I) + symmetricbraiding = BraidingStyle(I) isa SymmetricBraiding + println("---------------------------------------") + println("Tensors with symmetry: $Istr") + println("---------------------------------------") + @timedtestset "Tensors with symmetry: $Istr" verbose = true begin + V1, V2, V3, V4, V5 = V + @timedtestset "Full trace: test self-consistency" begin + if symmetricbraiding + t = rand(ComplexF64, V1 ⊗ V2' ⊗ V2 ⊗ V1') + t2 = permute(t, ((1, 2), (4, 3))) + s = @constinferred tr(t2) + @test conj(s) ≈ tr(t2') + if !isdual(V1) + t2 = twist!(t2, 1) + end + if isdual(V2) + t2 = twist!(t2, 2) + end + ss = tr(t2) + @tensor s2 = t[a, b, b, a] + @tensor t3[a, b] := t[a, c, c, b] + @tensor s3 = t3[a, a] + @test ss ≈ s2 + @test ss ≈ s3 + end + t = rand(ComplexF64, V1 ⊗ V2 ← V1 ⊗ V2) # avoid permutes + ss = @constinferred tr(t) + @test conj(ss) ≈ tr(t') + @planar s2 = t[a b; a b] + @planar t3[a; b] := t[a c; b c] + @planar s3 = t3[a; a] + + @test ss ≈ s2 + @test ss ≈ s3 + end + @timedtestset "Partial trace: test self-consistency" begin + if symmetricbraiding + t = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V1 ⊗ V2 ⊗ V3) + @tensor t2[a; b] := t[c d b; c d a] + @tensor t4[a b; c d] := t[e d c; e b a] + @tensor t5[a; b] := t4[a c; b c] + @test t2 ≈ t5 + end + t = rand(ComplexF64, V3 ⊗ V4 ⊗ V5 ← V3 ⊗ V4 ⊗ V5) # compatible with module fusion + @planar t2[a; b] := t[c a d; c b d] + @planar t4[a b; c d] := t[e a b; e c d] + @planar t5[a; b] := t4[a c; b c] + @test t2 ≈ t5 + end + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + @timedtestset "Trace: test via conversion" begin + t = rand(ComplexF64, V1 ⊗ V2' ⊗ V3 ⊗ V2 ⊗ V1' ⊗ V3') + @tensor t2[a, b] := t[c, d, b, d, c, a] + @tensor t3[a, b] := convert(Array, t)[c, d, b, d, c, a] + @test t3 ≈ convert(Array, t2) + end + end + #TODO: find version that works for all multifusion cases + symmetricbraiding && @timedtestset "Trace and contraction" begin + t1 = rand(ComplexF64, V1 ⊗ V2 ⊗ V3) + t2 = rand(ComplexF64, V2' ⊗ V4 ⊗ V1') + t3 = t1 ⊗ t2 + @tensor ta[a, b] := t1[x, y, a] * t2[y, b, x] + @tensor tb[a, b] := t3[x, y, a, y, b, x] + @test ta ≈ tb + end + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + @timedtestset "Tensor contraction: test via conversion" begin + A1 = randn(ComplexF64, V1' * V2', V3') + A2 = randn(ComplexF64, V3 * V4, V5) + rhoL = randn(ComplexF64, V1, V1) + rhoR = randn(ComplexF64, V5, V5)' # test adjoint tensor + H = randn(ComplexF64, V2 * V4, V2 * V4) + @tensor HrA12[a, s1, s2, c] := rhoL[a, a'] * conj(A1[a', t1, b]) * + A2[b, t2, c'] * rhoR[c', c] * H[s1, s2, t1, t2] + + @tensor HrA12array[a, s1, s2, c] := convert(Array, rhoL)[a, a'] * + conj(convert(Array, A1)[a', t1, b]) * convert(Array, A2)[b, t2, c'] * + convert(Array, rhoR)[c', c] * convert(Array, H)[s1, s2, t1, t2] + + @test HrA12array ≈ convert(Array, HrA12) + end + end + @timedtestset "Tensor product: test via norm preservation" begin + for T in (Float32, ComplexF64) + if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V1)) + t1 = rand(T, V2 ⊗ V3 ⊗ V1, V1 ⊗ V2) + t2 = rand(T, V2 ⊗ V1 ⊗ V3, V1 ⊗ V1) + else + t1 = rand(T, V3 ⊗ V4 ⊗ V5, V1 ⊗ V2) + t2 = rand(T, V5' ⊗ V4' ⊗ V3', V2' ⊗ V1') + end + t = @constinferred (t1 ⊗ t2) + @test norm(t) ≈ norm(t1) * norm(t2) + end + end + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + @timedtestset "Tensor product: test via conversion" begin + for T in (Float32, ComplexF64) + t1 = rand(T, V2 ⊗ V3 ⊗ V1, V1) + t2 = rand(T, V2 ⊗ V1 ⊗ V3, V2) + t = @constinferred (t1 ⊗ t2) + d1 = dim(codomain(t1)) + d2 = dim(codomain(t2)) + d3 = dim(domain(t1)) + d4 = dim(domain(t2)) + At = convert(Array, t) + @test reshape(At, (d1, d2, d3, d4)) ≈ + reshape(convert(Array, t1), (d1, 1, d3, 1)) .* + reshape(convert(Array, t2), (1, d2, 1, d4)) + end + end + end + symmetricbraiding && @timedtestset "Tensor product: test via tensor contraction" begin + for T in (Float32, ComplexF64) + t1 = rand(T, V2 ⊗ V3 ⊗ V1) + t2 = rand(T, V2 ⊗ V1 ⊗ V3) + t = @constinferred (t1 ⊗ t2) + @tensor t′[1, 2, 3, 4, 5, 6] := t1[1, 2, 3] * t2[4, 5, 6] + @test t ≈ t′ + end + end + @timedtestset "Tensor absorption" begin + # absorbing small into large + if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V3)) + t1 = zeros(V1 ⊕ V1, V2 ⊗ V3) + t2 = rand(V1, V2 ⊗ V3) + else + t1 = zeros(V1 ⊕ V2, V3 ⊗ V4 ⊗ V5) + t2 = rand(V1, V3 ⊗ V4 ⊗ V5) + end + t3 = @constinferred absorb(t1, t2) + @test norm(t3) ≈ norm(t2) + @test norm(t1) == 0 + t4 = @constinferred absorb!(t1, t2) + @test t1 === t4 + @test t3 ≈ t4 + + # absorbing large into small + if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V3)) + t1 = rand(V1 ⊕ V1, V2 ⊗ V3) + t2 = zeros(V1, V2 ⊗ V3) + else + t1 = rand(V1 ⊕ V2, V3 ⊗ V4 ⊗ V5) + t2 = zeros(V1, V3 ⊗ V4 ⊗ V5) + end + t3 = @constinferred absorb(t2, t1) + @test norm(t3) < norm(t1) + @test norm(t2) == 0 + t4 = @constinferred absorb!(t2, t1) + @test t2 === t4 + @test t3 ≈ t4 + end + end + TensorKit.empty_globalcaches!() +end + +@timedtestset "Deligne tensor product: test via conversion" begin + @testset for Vlist1 in (Vtr, VSU₂), Vlist2 in (Vtr, Vℤ₂) + V1, V2, V3, V4, V5 = Vlist1 + W1, W2, W3, W4, W5 = Vlist2 + for T in (Float32, ComplexF64) + t1 = rand(T, V1 ⊗ V2, V3' ⊗ V4) + t2 = rand(T, W2, W1 ⊗ W1') + t = @constinferred (t1 ⊠ t2) + d1 = dim(codomain(t1)) + d2 = dim(codomain(t2)) + d3 = dim(domain(t1)) + d4 = dim(domain(t2)) + At = convert(Array, t) + @test reshape(At, (d1, d2, d3, d4)) ≈ + reshape(convert(Array, t1), (d1, 1, d3, 1)) .* + reshape(convert(Array, t2), (1, d2, 1, d4)) + end + end +end diff --git a/test/tensors/indexmanipulations.jl b/test/tensors/indexmanipulations.jl new file mode 100644 index 000000000..d24762d0c --- /dev/null +++ b/test/tensors/indexmanipulations.jl @@ -0,0 +1,146 @@ +using Test, TestExtras +using TensorKit +using TensorKit: type_repr +using Combinatorics: permutations + + +spacelist = if fast_tests + (Vtr, Vℤ₃, VSU₂) +elseif get(ENV, "CI", "false") == "true" + println("Detected running on CI") + if Sys.iswindows() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) + elseif Sys.isapple() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂, VSU₂U₁, VIB_M) #, VSU₃) + else + (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) + end +else + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) +end + +for V in spacelist + I = sectortype(first(V)) + Istr = type_repr(I) + symmetricbraiding = BraidingStyle(I) isa SymmetricBraiding + println("---------------------------------------") + println("Tensors with symmetry: $Istr") + println("---------------------------------------") + @timedtestset "Tensors with symmetry: $Istr" verbose = true begin + V1, V2, V3, V4, V5 = V + @timedtestset "Trivial space insertion and removal" begin + W = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 + for T in (Float32, ComplexF64) + t = @constinferred rand(T, W) + t2 = @constinferred insertleftunit(t) + @test t2 == @constinferred insertrightunit(t) + @test space(t2) == insertleftunit(space(t)) + @test @constinferred(removeunit(t2, $(numind(t2)))) == t + t3 = @constinferred insertleftunit(t; copy = true) + @test t3 == @constinferred insertrightunit(t; copy = true) + @test @constinferred(removeunit(t3, $(numind(t3)))) == t + + @test numind(t2) == numind(t) + 1 + @test scalartype(t2) === T + @test t.data === t2.data + + @test t.data !== t3.data + for (c, b) in blocks(t) + @test b == block(t3, c) + end + + t4 = @constinferred insertrightunit(t, 3; dual = true) + @test numin(t4) == numin(t) + 1 && numout(t4) == numout(t) + for (c, b) in blocks(t) + @test b == block(t4, c) + end + @test @constinferred(removeunit(t4, 4)) == t + + t5 = @constinferred insertleftunit(t, 4; dual = true) + @test numin(t5) == numin(t) + 1 && numout(t5) == numout(t) + for (c, b) in blocks(t) + @test b == block(t5, c) + end + @test @constinferred(removeunit(t5, 4)) == t + end + end + symmetricbraiding && @timedtestset "Permutations: test via inner product invariance" begin + W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + t = rand(ComplexF64, W) + t′ = randn!(similar(t)) + for k in 0:5 + for p in permutations(1:5) + p1 = ntuple(n -> p[n], k) + p2 = ntuple(n -> p[k + n], 5 - k) + t2 = @constinferred permute(t, (p1, p2)) + @test norm(t2) ≈ norm(t) + t2′ = permute(t′, (p1, p2)) + @test dot(t2′, t2) ≈ dot(t′, t) ≈ dot(transpose(t2′), transpose(t2)) + end + + t3 = @constinferred repartition(t, $k) + @test norm(t3) ≈ norm(t) + t3′ = @constinferred repartition!(similar(t3), t′) + @test norm(t3′) ≈ norm(t′) + @test dot(t′, t) ≈ dot(t3′, t3) + end + end + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + @timedtestset "Permutations: test via conversion" begin + W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + t = rand(ComplexF64, W) + a = convert(Array, t) + for k in 0:5 + for p in permutations(1:5) + p1 = ntuple(n -> p[n], k) + p2 = ntuple(n -> p[k + n], 5 - k) + t2 = permute(t, (p1, p2)) + a2 = convert(Array, t2) + @test a2 ≈ permutedims(a, (p1..., p2...)) + @test convert(Array, transpose(t2)) ≈ + permutedims(a2, (5, 4, 3, 2, 1)) + end + + t3 = repartition(t, k) + a3 = convert(Array, t3) + @test a3 ≈ permutedims( + a, (ntuple(identity, k)..., reverse(ntuple(i -> i + k, 5 - k))...) + ) + end + end + end + (BraidingStyle(I) isa HasBraiding) && @timedtestset "Index flipping: test flipping inverse" begin + t = rand(ComplexF64, V1 ⊗ V1' ← V1' ⊗ V1) + for i in 1:4 + @test t ≈ flip(flip(t, i), i; inv = true) + @test t ≈ flip(flip(t, i; inv = true), i) + end + end + symmetricbraiding && @timedtestset "Index flipping: test via explicit flip" begin + t = rand(ComplexF64, V1 ⊗ V1' ← V1' ⊗ V1) + F1 = unitary(flip(V1), V1) + + @tensor tf[a, b; c, d] := F1[a, a'] * t[a', b; c, d] + @test flip(t, 1) ≈ tf + @tensor tf[a, b; c, d] := conj(F1[b, b']) * t[a, b'; c, d] + @test twist!(flip(t, 2), 2) ≈ tf + @tensor tf[a, b; c, d] := F1[c, c'] * t[a, b; c', d] + @test flip(t, 3) ≈ tf + @tensor tf[a, b; c, d] := conj(F1[d, d']) * t[a, b; c, d'] + @test twist!(flip(t, 4), 4) ≈ tf + end + symmetricbraiding && @timedtestset "Index flipping: test via contraction" begin + t1 = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V4) + t2 = rand(ComplexF64, V2' ⊗ V5 ← V4' ⊗ V1) + @tensor ta[a, b] := t1[x, y, a, z] * t2[y, b, z, x] + @tensor tb[a, b] := flip(t1, 1)[x, y, a, z] * flip(t2, 4)[y, b, z, x] + @test ta ≈ tb + @tensor tb[a, b] := flip(t1, (2, 4))[x, y, a, z] * flip(t2, (1, 3))[y, b, z, x] + @test ta ≈ tb + @tensor tb[a, b] := flip(t1, (1, 2, 4))[x, y, a, z] * flip(t2, (1, 3, 4))[y, b, z, x] + @tensor tb[a, b] := flip(t1, (1, 3))[x, y, a, z] * flip(t2, (2, 4))[y, b, z, x] + @test flip(ta, (1, 2)) ≈ tb + end + end + TensorKit.empty_globalcaches!() +end diff --git a/test/tensors/linalg.jl b/test/tensors/linalg.jl new file mode 100644 index 000000000..f727f45a8 --- /dev/null +++ b/test/tensors/linalg.jl @@ -0,0 +1,244 @@ +using Test, TestExtras +using TensorKit +using TensorKit: type_repr +using LinearAlgebra: LinearAlgebra + + +spacelist = if fast_tests + (Vtr, Vℤ₃, VSU₂) +elseif get(ENV, "CI", "false") == "true" + println("Detected running on CI") + if Sys.iswindows() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) + elseif Sys.isapple() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂, VSU₂U₁, VIB_M) #, VSU₃) + else + (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) + end +else + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) +end + +for V in spacelist + I = sectortype(first(V)) + Istr = type_repr(I) + symmetricbraiding = BraidingStyle(I) isa SymmetricBraiding + println("---------------------------------------") + println("Tensors with symmetry: $Istr") + println("---------------------------------------") + @timedtestset "Tensors with symmetry: $Istr" verbose = true begin + V1, V2, V3, V4, V5 = V + @timedtestset "Basic linear algebra" begin + W = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 + for T in (Float32, ComplexF64) + t = @constinferred rand(T, W) + @test scalartype(t) == T + @test space(t) == W + @test space(t') == W' + @test dim(t) == dim(space(t)) + @test codomain(t) == codomain(W) + @test domain(t) == domain(W) + # blocks for adjoint + bs = @constinferred blocks(t') + (c, b1), state = @constinferred Nothing iterate(bs) + @test c == first(blocksectors(W')) + next = @constinferred Nothing iterate(bs, state) + b2 = @constinferred block(t', first(blocksectors(t'))) + @test b1 == b2 + @test eltype(bs) === Pair{typeof(c), typeof(b1)} + @test typeof(b1) === TensorKit.blocktype(t') + @test typeof(c) === sectortype(t) + # linear algebra + @test isa(@constinferred(norm(t)), real(T)) + @test norm(t)^2 ≈ dot(t, t) + α = rand(T) + @test norm(α * t) ≈ abs(α) * norm(t) + @test norm(t + t, 2) ≈ 2 * norm(t, 2) + @test norm(t + t, 1) ≈ 2 * norm(t, 1) + @test norm(t + t, Inf) ≈ 2 * norm(t, Inf) + p = 3 * rand(Float64) + @test norm(t + t, p) ≈ 2 * norm(t, p) + @test norm(t) ≈ norm(t') + + t2 = @constinferred rand!(similar(t)) + β = rand(T) + @test @constinferred(dot(β * t2, α * t)) ≈ conj(β) * α * conj(dot(t, t2)) + @test dot(t2, t) ≈ conj(dot(t, t2)) + @test dot(t2, t) ≈ conj(dot(t2', t')) + @test dot(t2, t) ≈ dot(t', t2') + + if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V1)) + i1 = @constinferred(isomorphism(T, V1 ⊗ V2, V2 ⊗ V1)) # can't reverse fusion here when modules are involved + i2 = @constinferred(isomorphism(Vector{T}, V2 ⊗ V1, V1 ⊗ V2)) + @test i1 * i2 == @constinferred(id(T, V1 ⊗ V2)) + @test i2 * i1 == @constinferred(id(Vector{T}, V2 ⊗ V1)) + end + + w = @constinferred isometry(T, V1 ⊗ (rightunitspace(V1) ⊕ rightunitspace(V1)), V1) + @test dim(w) == 2 * dim(V1 ← V1) + @test w' * w == id(Vector{T}, V1) + @test w * w' == (w * w')^2 + end + end + if hasfusiontensor(I) + @timedtestset "Basic linear algebra: test via conversion" begin + W = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 + for T in (Float32, ComplexF64) + t = rand(T, W) + t2 = @constinferred rand!(similar(t)) + @test norm(t, 2) ≈ norm(convert(Array, t), 2) + @test dot(t2, t) ≈ dot(convert(Array, t2), convert(Array, t)) + α = rand(T) + @test convert(Array, α * t) ≈ α * convert(Array, t) + @test convert(Array, t + t) ≈ 2 * convert(Array, t) + end + end + end + @timedtestset "Multiplication of isometries: test properties" begin + W2 = V4 ⊗ V5 + W1 = W2 ⊗ (unitspace(V1) ⊕ unitspace(V1)) + for T in (Float64, ComplexF64) + t1 = randisometry(T, W1, W2) + t2 = randisometry(T, W2 ← W2) + @test isisometric(t1) + @test isunitary(t2) + P = t1 * t1' + @test P * P ≈ P + end + end + @timedtestset "Multiplication and inverse: test compatibility" begin + W1 = V1 ⊗ V2 ⊗ V3 + W2 = V4 ⊗ V5 + for T in (Float64, ComplexF64) + t1 = rand(T, W1, W1) + t2 = rand(T, W2 ← W2) + t = rand(T, W1, W2) + @test t1 * (t1 \ t) ≈ t + @test (t / t2) * t2 ≈ t + @test t1 \ one(t1) ≈ inv(t1) + @test one(t1) / t1 ≈ pinv(t1) + @test_throws SpaceMismatch inv(t) + @test_throws SpaceMismatch t2 \ t + @test_throws SpaceMismatch t / t1 + tp = pinv(t) * t + @test tp ≈ tp * tp + end + end + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + @timedtestset "Multiplication and inverse: test via conversion" begin + W1 = V1 ⊗ V2 ⊗ V3 + W2 = V4 ⊗ V5 + for T in (Float32, Float64, ComplexF32, ComplexF64) + t1 = rand(T, W1 ← W1) + t2 = rand(T, W2, W2) + t = rand(T, W1 ← W2) + d1 = dim(W1) + d2 = dim(W2) + At1 = reshape(convert(Array, t1), d1, d1) + At2 = reshape(convert(Array, t2), d2, d2) + At = reshape(convert(Array, t), d1, d2) + @test reshape(convert(Array, t1 * t), d1, d2) ≈ At1 * At + @test reshape(convert(Array, t1' * t), d1, d2) ≈ At1' * At + @test reshape(convert(Array, t2 * t'), d2, d1) ≈ At2 * At' + @test reshape(convert(Array, t2' * t'), d2, d1) ≈ At2' * At' + + @test reshape(convert(Array, inv(t1)), d1, d1) ≈ inv(At1) + @test reshape(convert(Array, pinv(t)), d2, d1) ≈ pinv(At) + + if T == Float32 || T == ComplexF32 + continue + end + + @test reshape(convert(Array, t1 \ t), d1, d2) ≈ At1 \ At + @test reshape(convert(Array, t1' \ t), d1, d2) ≈ At1' \ At + @test reshape(convert(Array, t2 \ t'), d2, d1) ≈ At2 \ At' + @test reshape(convert(Array, t2' \ t'), d2, d1) ≈ At2' \ At' + + @test reshape(convert(Array, t2 / t), d2, d1) ≈ At2 / At + @test reshape(convert(Array, t2' / t), d2, d1) ≈ At2' / At + @test reshape(convert(Array, t1 / t'), d1, d2) ≈ At1 / At' + @test reshape(convert(Array, t1' / t'), d1, d2) ≈ At1' / At' + end + end + end + @timedtestset "diag/diagm" begin + W = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 + t = randn(ComplexF64, W) + d = LinearAlgebra.diag(t) + D = LinearAlgebra.diagm(codomain(t), domain(t), d) + @test LinearAlgebra.isdiag(D) + @test LinearAlgebra.diag(D) == d + end + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + @timedtestset "Tensor functions" begin + W = V1 ⊗ V2 + for T in (Float64, ComplexF64) + t = randn(T, W, W) + s = dim(W) + expt = @constinferred exp(t) + @test reshape(convert(Array, expt), (s, s)) ≈ + exp(reshape(convert(Array, t), (s, s))) + + @test (@constinferred sqrt(t))^2 ≈ t + @test reshape(convert(Array, sqrt(t^2)), (s, s)) ≈ + sqrt(reshape(convert(Array, t^2), (s, s))) + + @test exp(@constinferred log(expt)) ≈ expt + @test reshape(convert(Array, log(expt)), (s, s)) ≈ + log(reshape(convert(Array, expt), (s, s))) + + @test (@constinferred cos(t))^2 + (@constinferred sin(t))^2 ≈ id(W) + @test (@constinferred tan(t)) ≈ sin(t) / cos(t) + @test (@constinferred cot(t)) ≈ cos(t) / sin(t) + @test (@constinferred cosh(t))^2 - (@constinferred sinh(t))^2 ≈ id(W) + @test (@constinferred tanh(t)) ≈ sinh(t) / cosh(t) + @test (@constinferred coth(t)) ≈ cosh(t) / sinh(t) + + t1 = sin(t) + @test sin(@constinferred asin(t1)) ≈ t1 + t2 = cos(t) + @test cos(@constinferred acos(t2)) ≈ t2 + t3 = sinh(t) + @test sinh(@constinferred asinh(t3)) ≈ t3 + t4 = cosh(t) + @test cosh(@constinferred acosh(t4)) ≈ t4 + t5 = tan(t) + @test tan(@constinferred atan(t5)) ≈ t5 + t6 = cot(t) + @test cot(@constinferred acot(t6)) ≈ t6 + t7 = tanh(t) + @test tanh(@constinferred atanh(t7)) ≈ t7 + t8 = coth(t) + @test coth(@constinferred acoth(t8)) ≈ t8 + t = randn(T, W, V1) # not square + for f in + ( + cos, sin, tan, cot, cosh, sinh, tanh, coth, atan, acot, asinh, + sqrt, log, asin, acos, acosh, atanh, acoth, + ) + @test_throws SpaceMismatch f(t) + end + end + end + end + @timedtestset "Sylvester equation" begin + for T in (Float32, ComplexF64) + tA = rand(T, V1 ⊗ V3, V1 ⊗ V3) + tB = rand(T, V2 ⊗ V4, V2 ⊗ V4) + tA = 3 // 2 * left_polar(tA)[1] + tB = 1 // 5 * left_polar(tB)[1] + tC = rand(T, V1 ⊗ V3, V2 ⊗ V4) + t = @constinferred sylvester(tA, tB, tC) + @test codomain(t) == V1 ⊗ V3 + @test domain(t) == V2 ⊗ V4 + @test norm(tA * t + t * tB + tC) < + (norm(tA) + norm(tB) + norm(tC)) * eps(real(T))^(2 / 3) + if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) + matrix(x) = reshape(convert(Array, x), dim(codomain(x)), dim(domain(x))) + @test matrix(t) ≈ sylvester(matrix(tA), matrix(tB), matrix(tC)) + end + end + end + end + TensorKit.empty_globalcaches!() +end diff --git a/test/tensors/tensors.jl b/test/tensors/tensors.jl deleted file mode 100644 index 084030451..000000000 --- a/test/tensors/tensors.jl +++ /dev/null @@ -1,681 +0,0 @@ -using Test, TestExtras -using TensorKit -using TensorKit: type_repr -using Combinatorics: permutations -using LinearAlgebra: LinearAlgebra - - -spacelist = if fast_tests - (Vtr, Vℤ₃, VSU₂) -elseif get(ENV, "CI", "false") == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) - elseif Sys.isapple() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂, VSU₂U₁, VIB_M) #, VSU₃) - else - (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) - end -else - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) -end - -for V in spacelist - I = sectortype(first(V)) - Istr = type_repr(I) - symmetricbraiding = BraidingStyle(I) isa SymmetricBraiding - println("---------------------------------------") - println("Tensors with symmetry: $Istr") - println("---------------------------------------") - @timedtestset "Tensors with symmetry: $Istr" verbose = true begin - V1, V2, V3, V4, V5 = V - @timedtestset "Basic tensor properties" begin - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 - for T in (fast_tests ? (Float64, ComplexF64) : (Int, Float32, Float64, ComplexF32, ComplexF64, BigFloat)) - t = @constinferred zeros(T, W) - @test @constinferred(hash(t)) == hash(deepcopy(t)) - @test scalartype(t) == T - @test norm(t) == 0 - @test codomain(t) == W - @test space(t) == (W ← one(W)) - @test domain(t) == one(W) - @test typeof(t) == TensorMap{T, spacetype(t), 5, 0, Vector{T}} - # Array type input - t = @constinferred zeros(Vector{T}, W) - @test @constinferred(hash(t)) == hash(deepcopy(t)) - @test scalartype(t) == T - @test norm(t) == 0 - @test codomain(t) == W - @test space(t) == (W ← one(W)) - @test domain(t) == one(W) - @test typeof(t) == TensorMap{T, spacetype(t), 5, 0, Vector{T}} - # blocks - bs = @constinferred blocks(t) - if !isempty(blocksectors(t)) # multifusion space ending on module gives empty data - (c, b1), state = @constinferred Nothing iterate(bs) - @test c == first(blocksectors(W)) - next = @constinferred Nothing iterate(bs, state) - b2 = @constinferred block(t, first(blocksectors(t))) - @test b1 == b2 - @test eltype(bs) === Pair{typeof(c), typeof(b1)} - @test typeof(b1) === TensorKit.blocktype(t) - @test typeof(c) === sectortype(t) - end - end - end - @timedtestset "Tensor Dict conversion" begin - W = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 - for T in (Int, Float32, ComplexF64) - t = @constinferred rand(T, W) - d = convert(Dict, t) - @test t == convert(TensorMap, d) - end - end - if hasfusiontensor(I) || I == Trivial - @timedtestset "Tensor Array conversion" begin - W1 = V1 ← one(V1) - W2 = one(V2) ← V2 - W3 = V1 ⊗ V2 ← one(V1) - W4 = V1 ← V2 - W5 = one(V1) ← V1 ⊗ V2 - W6 = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 - for W in (W1, W2, W3, W4, W5, W6) - for T in (Int, Float32, ComplexF64) - if T == Int - t = TensorMap{T}(undef, W) - for (_, b) in blocks(t) - rand!(b, -20:20) - end - else - t = @constinferred randn(T, W) - end - a = @constinferred convert(Array, t) - b = reshape(a, dim(codomain(W)), dim(domain(W))) - @test t ≈ @constinferred TensorMap(a, W) - @test t ≈ @constinferred TensorMap(b, W) - @test t === @constinferred TensorMap(t.data, W) - end - end - for T in (Int, Float32, ComplexF64) - t = randn(T, V1 ⊗ V2 ← zerospace(V1)) - a = convert(Array, t) - @test norm(a) == 0 - end - end - end - @timedtestset "Basic linear algebra" begin - W = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 - for T in (Float32, ComplexF64) - t = @constinferred rand(T, W) - @test scalartype(t) == T - @test space(t) == W - @test space(t') == W' - @test dim(t) == dim(space(t)) - @test codomain(t) == codomain(W) - @test domain(t) == domain(W) - # blocks for adjoint - bs = @constinferred blocks(t') - (c, b1), state = @constinferred Nothing iterate(bs) - @test c == first(blocksectors(W')) - next = @constinferred Nothing iterate(bs, state) - b2 = @constinferred block(t', first(blocksectors(t'))) - @test b1 == b2 - @test eltype(bs) === Pair{typeof(c), typeof(b1)} - @test typeof(b1) === TensorKit.blocktype(t') - @test typeof(c) === sectortype(t) - # linear algebra - @test isa(@constinferred(norm(t)), real(T)) - @test norm(t)^2 ≈ dot(t, t) - α = rand(T) - @test norm(α * t) ≈ abs(α) * norm(t) - @test norm(t + t, 2) ≈ 2 * norm(t, 2) - @test norm(t + t, 1) ≈ 2 * norm(t, 1) - @test norm(t + t, Inf) ≈ 2 * norm(t, Inf) - p = 3 * rand(Float64) - @test norm(t + t, p) ≈ 2 * norm(t, p) - @test norm(t) ≈ norm(t') - - t2 = @constinferred rand!(similar(t)) - β = rand(T) - @test @constinferred(dot(β * t2, α * t)) ≈ conj(β) * α * conj(dot(t, t2)) - @test dot(t2, t) ≈ conj(dot(t, t2)) - @test dot(t2, t) ≈ conj(dot(t2', t')) - @test dot(t2, t) ≈ dot(t', t2') - - if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V1)) - i1 = @constinferred(isomorphism(T, V1 ⊗ V2, V2 ⊗ V1)) # can't reverse fusion here when modules are involved - i2 = @constinferred(isomorphism(Vector{T}, V2 ⊗ V1, V1 ⊗ V2)) - @test i1 * i2 == @constinferred(id(T, V1 ⊗ V2)) - @test i2 * i1 == @constinferred(id(Vector{T}, V2 ⊗ V1)) - end - - w = @constinferred isometry(T, V1 ⊗ (rightunitspace(V1) ⊕ rightunitspace(V1)), V1) - @test dim(w) == 2 * dim(V1 ← V1) - @test w' * w == id(Vector{T}, V1) - @test w * w' == (w * w')^2 - end - end - @timedtestset "Trivial space insertion and removal" begin - W = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 - for T in (Float32, ComplexF64) - t = @constinferred rand(T, W) - t2 = @constinferred insertleftunit(t) - @test t2 == @constinferred insertrightunit(t) - @test space(t2) == insertleftunit(space(t)) - @test @constinferred(removeunit(t2, $(numind(t2)))) == t - t3 = @constinferred insertleftunit(t; copy = true) - @test t3 == @constinferred insertrightunit(t; copy = true) - @test @constinferred(removeunit(t3, $(numind(t3)))) == t - - @test numind(t2) == numind(t) + 1 - @test scalartype(t2) === T - @test t.data === t2.data - - @test t.data !== t3.data - for (c, b) in blocks(t) - @test b == block(t3, c) - end - - t4 = @constinferred insertrightunit(t, 3; dual = true) - @test numin(t4) == numin(t) + 1 && numout(t4) == numout(t) - for (c, b) in blocks(t) - @test b == block(t4, c) - end - @test @constinferred(removeunit(t4, 4)) == t - - t5 = @constinferred insertleftunit(t, 4; dual = true) - @test numin(t5) == numin(t) + 1 && numout(t5) == numout(t) - for (c, b) in blocks(t) - @test b == block(t5, c) - end - @test @constinferred(removeunit(t5, 4)) == t - end - end - if hasfusiontensor(I) - @timedtestset "Basic linear algebra: test via conversion" begin - W = V1 ⊗ V2 ⊗ V3 ← V4 ⊗ V5 - for T in (Float32, ComplexF64) - t = rand(T, W) - t2 = @constinferred rand!(similar(t)) - @test norm(t, 2) ≈ norm(convert(Array, t), 2) - @test dot(t2, t) ≈ dot(convert(Array, t2), convert(Array, t)) - α = rand(T) - @test convert(Array, α * t) ≈ α * convert(Array, t) - @test convert(Array, t + t) ≈ 2 * convert(Array, t) - end - end - @timedtestset "Real and imaginary parts" begin - W = V1 ⊗ V2 - for T in (Float64, ComplexF64, ComplexF32) - t = @constinferred randn(T, W, W) - - tr = @constinferred real(t) - @test scalartype(tr) <: Real - @test real(convert(Array, t)) == convert(Array, tr) - - ti = @constinferred imag(t) - @test scalartype(ti) <: Real - @test imag(convert(Array, t)) == convert(Array, ti) - - tc = @inferred complex(t) - @test scalartype(tc) <: Complex - @test complex(convert(Array, t)) == convert(Array, tc) - - tc2 = @inferred complex(tr, ti) - @test tc2 ≈ tc - end - end - end - @timedtestset "Tensor conversion" begin - W = V1 ⊗ V2 - t = @constinferred randn(W ← W) - @test typeof(convert(TensorMap, t')) == typeof(t) - tc = complex(t) - @test convert(typeof(tc), t) == tc - @test typeof(convert(typeof(tc), t)) == typeof(tc) - @test typeof(convert(typeof(tc), t')) == typeof(tc) - @test Base.promote_typeof(t, tc) == typeof(tc) - @test Base.promote_typeof(tc, t) == typeof(tc + t) - end - symmetricbraiding && @timedtestset "Permutations: test via inner product invariance" begin - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 - t = rand(ComplexF64, W) - t′ = randn!(similar(t)) - for k in 0:5 - for p in permutations(1:5) - p1 = ntuple(n -> p[n], k) - p2 = ntuple(n -> p[k + n], 5 - k) - t2 = @constinferred permute(t, (p1, p2)) - @test norm(t2) ≈ norm(t) - t2′ = permute(t′, (p1, p2)) - @test dot(t2′, t2) ≈ dot(t′, t) ≈ dot(transpose(t2′), transpose(t2)) - end - - t3 = @constinferred repartition(t, $k) - @test norm(t3) ≈ norm(t) - t3′ = @constinferred repartition!(similar(t3), t′) - @test norm(t3′) ≈ norm(t′) - @test dot(t′, t) ≈ dot(t3′, t3) - end - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Permutations: test via conversion" begin - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 - t = rand(ComplexF64, W) - a = convert(Array, t) - for k in 0:5 - for p in permutations(1:5) - p1 = ntuple(n -> p[n], k) - p2 = ntuple(n -> p[k + n], 5 - k) - t2 = permute(t, (p1, p2)) - a2 = convert(Array, t2) - @test a2 ≈ permutedims(a, (p1..., p2...)) - @test convert(Array, transpose(t2)) ≈ - permutedims(a2, (5, 4, 3, 2, 1)) - end - - t3 = repartition(t, k) - a3 = convert(Array, t3) - @test a3 ≈ permutedims( - a, (ntuple(identity, k)..., reverse(ntuple(i -> i + k, 5 - k))...) - ) - end - end - end - @timedtestset "Full trace: test self-consistency" begin - if symmetricbraiding - t = rand(ComplexF64, V1 ⊗ V2' ⊗ V2 ⊗ V1') - t2 = permute(t, ((1, 2), (4, 3))) - s = @constinferred tr(t2) - @test conj(s) ≈ tr(t2') - if !isdual(V1) - t2 = twist!(t2, 1) - end - if isdual(V2) - t2 = twist!(t2, 2) - end - ss = tr(t2) - @tensor s2 = t[a, b, b, a] - @tensor t3[a, b] := t[a, c, c, b] - @tensor s3 = t3[a, a] - @test ss ≈ s2 - @test ss ≈ s3 - end - t = rand(ComplexF64, V1 ⊗ V2 ← V1 ⊗ V2) # avoid permutes - ss = @constinferred tr(t) - @test conj(ss) ≈ tr(t') - @planar s2 = t[a b; a b] - @planar t3[a; b] := t[a c; b c] - @planar s3 = t3[a; a] - - @test ss ≈ s2 - @test ss ≈ s3 - end - @timedtestset "Partial trace: test self-consistency" begin - if symmetricbraiding - t = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V1 ⊗ V2 ⊗ V3) - @tensor t2[a; b] := t[c d b; c d a] - @tensor t4[a b; c d] := t[e d c; e b a] - @tensor t5[a; b] := t4[a c; b c] - @test t2 ≈ t5 - end - t = rand(ComplexF64, V3 ⊗ V4 ⊗ V5 ← V3 ⊗ V4 ⊗ V5) # compatible with module fusion - @planar t2[a; b] := t[c a d; c b d] - @planar t4[a b; c d] := t[e a b; e c d] - @planar t5[a; b] := t4[a c; b c] - @test t2 ≈ t5 - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Trace: test via conversion" begin - t = rand(ComplexF64, V1 ⊗ V2' ⊗ V3 ⊗ V2 ⊗ V1' ⊗ V3') - @tensor t2[a, b] := t[c, d, b, d, c, a] - @tensor t3[a, b] := convert(Array, t)[c, d, b, d, c, a] - @test t3 ≈ convert(Array, t2) - end - end - #TODO: find version that works for all multifusion cases - symmetricbraiding && @timedtestset "Trace and contraction" begin - t1 = rand(ComplexF64, V1 ⊗ V2 ⊗ V3) - t2 = rand(ComplexF64, V2' ⊗ V4 ⊗ V1') - t3 = t1 ⊗ t2 - @tensor ta[a, b] := t1[x, y, a] * t2[y, b, x] - @tensor tb[a, b] := t3[x, y, a, y, b, x] - @test ta ≈ tb - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Tensor contraction: test via conversion" begin - A1 = randn(ComplexF64, V1' * V2', V3') - A2 = randn(ComplexF64, V3 * V4, V5) - rhoL = randn(ComplexF64, V1, V1) - rhoR = randn(ComplexF64, V5, V5)' # test adjoint tensor - H = randn(ComplexF64, V2 * V4, V2 * V4) - @tensor HrA12[a, s1, s2, c] := rhoL[a, a'] * conj(A1[a', t1, b]) * - A2[b, t2, c'] * rhoR[c', c] * H[s1, s2, t1, t2] - - @tensor HrA12array[a, s1, s2, c] := convert(Array, rhoL)[a, a'] * - conj(convert(Array, A1)[a', t1, b]) * convert(Array, A2)[b, t2, c'] * - convert(Array, rhoR)[c', c] * convert(Array, H)[s1, s2, t1, t2] - - @test HrA12array ≈ convert(Array, HrA12) - end - end - (BraidingStyle(I) isa HasBraiding) && @timedtestset "Index flipping: test flipping inverse" begin - t = rand(ComplexF64, V1 ⊗ V1' ← V1' ⊗ V1) - for i in 1:4 - @test t ≈ flip(flip(t, i), i; inv = true) - @test t ≈ flip(flip(t, i; inv = true), i) - end - end - symmetricbraiding && @timedtestset "Index flipping: test via explicit flip" begin - t = rand(ComplexF64, V1 ⊗ V1' ← V1' ⊗ V1) - F1 = unitary(flip(V1), V1) - - @tensor tf[a, b; c, d] := F1[a, a'] * t[a', b; c, d] - @test flip(t, 1) ≈ tf - @tensor tf[a, b; c, d] := conj(F1[b, b']) * t[a, b'; c, d] - @test twist!(flip(t, 2), 2) ≈ tf - @tensor tf[a, b; c, d] := F1[c, c'] * t[a, b; c', d] - @test flip(t, 3) ≈ tf - @tensor tf[a, b; c, d] := conj(F1[d, d']) * t[a, b; c, d'] - @test twist!(flip(t, 4), 4) ≈ tf - end - symmetricbraiding && @timedtestset "Index flipping: test via contraction" begin - t1 = rand(ComplexF64, V1 ⊗ V2 ⊗ V3 ← V4) - t2 = rand(ComplexF64, V2' ⊗ V5 ← V4' ⊗ V1) - @tensor ta[a, b] := t1[x, y, a, z] * t2[y, b, z, x] - @tensor tb[a, b] := flip(t1, 1)[x, y, a, z] * flip(t2, 4)[y, b, z, x] - @test ta ≈ tb - @tensor tb[a, b] := flip(t1, (2, 4))[x, y, a, z] * flip(t2, (1, 3))[y, b, z, x] - @test ta ≈ tb - @tensor tb[a, b] := flip(t1, (1, 2, 4))[x, y, a, z] * flip(t2, (1, 3, 4))[y, b, z, x] - @tensor tb[a, b] := flip(t1, (1, 3))[x, y, a, z] * flip(t2, (2, 4))[y, b, z, x] - @test flip(ta, (1, 2)) ≈ tb - end - @timedtestset "Multiplication of isometries: test properties" begin - W2 = V4 ⊗ V5 - W1 = W2 ⊗ (unitspace(V1) ⊕ unitspace(V1)) - for T in (Float64, ComplexF64) - t1 = randisometry(T, W1, W2) - t2 = randisometry(T, W2 ← W2) - @test isisometric(t1) - @test isunitary(t2) - P = t1 * t1' - @test P * P ≈ P - end - end - @timedtestset "Multiplication and inverse: test compatibility" begin - W1 = V1 ⊗ V2 ⊗ V3 - W2 = V4 ⊗ V5 - for T in (Float64, ComplexF64) - t1 = rand(T, W1, W1) - t2 = rand(T, W2 ← W2) - t = rand(T, W1, W2) - @test t1 * (t1 \ t) ≈ t - @test (t / t2) * t2 ≈ t - @test t1 \ one(t1) ≈ inv(t1) - @test one(t1) / t1 ≈ pinv(t1) - @test_throws SpaceMismatch inv(t) - @test_throws SpaceMismatch t2 \ t - @test_throws SpaceMismatch t / t1 - tp = pinv(t) * t - @test tp ≈ tp * tp - end - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Multiplication and inverse: test via conversion" begin - W1 = V1 ⊗ V2 ⊗ V3 - W2 = V4 ⊗ V5 - for T in (Float32, Float64, ComplexF32, ComplexF64) - t1 = rand(T, W1 ← W1) - t2 = rand(T, W2, W2) - t = rand(T, W1 ← W2) - d1 = dim(W1) - d2 = dim(W2) - At1 = reshape(convert(Array, t1), d1, d1) - At2 = reshape(convert(Array, t2), d2, d2) - At = reshape(convert(Array, t), d1, d2) - @test reshape(convert(Array, t1 * t), d1, d2) ≈ At1 * At - @test reshape(convert(Array, t1' * t), d1, d2) ≈ At1' * At - @test reshape(convert(Array, t2 * t'), d2, d1) ≈ At2 * At' - @test reshape(convert(Array, t2' * t'), d2, d1) ≈ At2' * At' - - @test reshape(convert(Array, inv(t1)), d1, d1) ≈ inv(At1) - @test reshape(convert(Array, pinv(t)), d2, d1) ≈ pinv(At) - - if T == Float32 || T == ComplexF32 - continue - end - - @test reshape(convert(Array, t1 \ t), d1, d2) ≈ At1 \ At - @test reshape(convert(Array, t1' \ t), d1, d2) ≈ At1' \ At - @test reshape(convert(Array, t2 \ t'), d2, d1) ≈ At2 \ At' - @test reshape(convert(Array, t2' \ t'), d2, d1) ≈ At2' \ At' - - @test reshape(convert(Array, t2 / t), d2, d1) ≈ At2 / At - @test reshape(convert(Array, t2' / t), d2, d1) ≈ At2' / At - @test reshape(convert(Array, t1 / t'), d1, d2) ≈ At1 / At' - @test reshape(convert(Array, t1' / t'), d1, d2) ≈ At1' / At' - end - end - end - @timedtestset "diag/diagm" begin - W = V1 ⊗ V2 ← V3 ⊗ V4 ⊗ V5 - t = randn(ComplexF64, W) - d = LinearAlgebra.diag(t) - D = LinearAlgebra.diagm(codomain(t), domain(t), d) - @test LinearAlgebra.isdiag(D) - @test LinearAlgebra.diag(D) == d - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Tensor functions" begin - W = V1 ⊗ V2 - for T in (Float64, ComplexF64) - t = randn(T, W, W) - s = dim(W) - expt = @constinferred exp(t) - @test reshape(convert(Array, expt), (s, s)) ≈ - exp(reshape(convert(Array, t), (s, s))) - - @test (@constinferred sqrt(t))^2 ≈ t - @test reshape(convert(Array, sqrt(t^2)), (s, s)) ≈ - sqrt(reshape(convert(Array, t^2), (s, s))) - - @test exp(@constinferred log(expt)) ≈ expt - @test reshape(convert(Array, log(expt)), (s, s)) ≈ - log(reshape(convert(Array, expt), (s, s))) - - @test (@constinferred cos(t))^2 + (@constinferred sin(t))^2 ≈ id(W) - @test (@constinferred tan(t)) ≈ sin(t) / cos(t) - @test (@constinferred cot(t)) ≈ cos(t) / sin(t) - @test (@constinferred cosh(t))^2 - (@constinferred sinh(t))^2 ≈ id(W) - @test (@constinferred tanh(t)) ≈ sinh(t) / cosh(t) - @test (@constinferred coth(t)) ≈ cosh(t) / sinh(t) - - t1 = sin(t) - @test sin(@constinferred asin(t1)) ≈ t1 - t2 = cos(t) - @test cos(@constinferred acos(t2)) ≈ t2 - t3 = sinh(t) - @test sinh(@constinferred asinh(t3)) ≈ t3 - t4 = cosh(t) - @test cosh(@constinferred acosh(t4)) ≈ t4 - t5 = tan(t) - @test tan(@constinferred atan(t5)) ≈ t5 - t6 = cot(t) - @test cot(@constinferred acot(t6)) ≈ t6 - t7 = tanh(t) - @test tanh(@constinferred atanh(t7)) ≈ t7 - t8 = coth(t) - @test coth(@constinferred acoth(t8)) ≈ t8 - t = randn(T, W, V1) # not square - for f in - ( - cos, sin, tan, cot, cosh, sinh, tanh, coth, atan, acot, asinh, - sqrt, log, asin, acos, acosh, atanh, acoth, - ) - @test_throws SpaceMismatch f(t) - end - end - end - end - @timedtestset "Sylvester equation" begin - for T in (Float32, ComplexF64) - tA = rand(T, V1 ⊗ V3, V1 ⊗ V3) - tB = rand(T, V2 ⊗ V4, V2 ⊗ V4) - tA = 3 // 2 * left_polar(tA)[1] - tB = 1 // 5 * left_polar(tB)[1] - tC = rand(T, V1 ⊗ V3, V2 ⊗ V4) - t = @constinferred sylvester(tA, tB, tC) - @test codomain(t) == V1 ⊗ V3 - @test domain(t) == V2 ⊗ V4 - @test norm(tA * t + t * tB + tC) < - (norm(tA) + norm(tB) + norm(tC)) * eps(real(T))^(2 / 3) - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - matrix(x) = reshape(convert(Array, x), dim(codomain(x)), dim(domain(x))) - @test matrix(t) ≈ sylvester(matrix(tA), matrix(tB), matrix(tC)) - end - end - end - @timedtestset "Tensor product: test via norm preservation" begin - for T in (Float32, ComplexF64) - if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V1)) - t1 = rand(T, V2 ⊗ V3 ⊗ V1, V1 ⊗ V2) - t2 = rand(T, V2 ⊗ V1 ⊗ V3, V1 ⊗ V1) - else - t1 = rand(T, V3 ⊗ V4 ⊗ V5, V1 ⊗ V2) - t2 = rand(T, V5' ⊗ V4' ⊗ V3', V2' ⊗ V1') - end - t = @constinferred (t1 ⊗ t2) - @test norm(t) ≈ norm(t1) * norm(t2) - end - end - if BraidingStyle(I) isa Bosonic && hasfusiontensor(I) - @timedtestset "Tensor product: test via conversion" begin - for T in (Float32, ComplexF64) - t1 = rand(T, V2 ⊗ V3 ⊗ V1, V1) - t2 = rand(T, V2 ⊗ V1 ⊗ V3, V2) - t = @constinferred (t1 ⊗ t2) - d1 = dim(codomain(t1)) - d2 = dim(codomain(t2)) - d3 = dim(domain(t1)) - d4 = dim(domain(t2)) - At = convert(Array, t) - @test reshape(At, (d1, d2, d3, d4)) ≈ - reshape(convert(Array, t1), (d1, 1, d3, 1)) .* - reshape(convert(Array, t2), (1, d2, 1, d4)) - end - end - end - symmetricbraiding && @timedtestset "Tensor product: test via tensor contraction" begin - for T in (Float32, ComplexF64) - t1 = rand(T, V2 ⊗ V3 ⊗ V1) - t2 = rand(T, V2 ⊗ V1 ⊗ V3) - t = @constinferred (t1 ⊗ t2) - @tensor t′[1, 2, 3, 4, 5, 6] := t1[1, 2, 3] * t2[4, 5, 6] - @test t ≈ t′ - end - end - @timedtestset "Tensor absorption" begin - # absorbing small into large - if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V3)) - t1 = zeros(V1 ⊕ V1, V2 ⊗ V3) - t2 = rand(V1, V2 ⊗ V3) - else - t1 = zeros(V1 ⊕ V2, V3 ⊗ V4 ⊗ V5) - t2 = rand(V1, V3 ⊗ V4 ⊗ V5) - end - t3 = @constinferred absorb(t1, t2) - @test norm(t3) ≈ norm(t2) - @test norm(t1) == 0 - t4 = @constinferred absorb!(t1, t2) - @test t1 === t4 - @test t3 ≈ t4 - - # absorbing large into small - if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V3)) - t1 = rand(V1 ⊕ V1, V2 ⊗ V3) - t2 = zeros(V1, V2 ⊗ V3) - else - t1 = rand(V1 ⊕ V2, V3 ⊗ V4 ⊗ V5) - t2 = zeros(V1, V3 ⊗ V4 ⊗ V5) - end - t3 = @constinferred absorb(t2, t1) - @test norm(t3) < norm(t1) - @test norm(t2) == 0 - t4 = @constinferred absorb!(t2, t1) - @test t2 === t4 - @test t3 ≈ t4 - end - end - TensorKit.empty_globalcaches!() -end - -@timedtestset "Deligne tensor product: test via conversion" begin - @testset for Vlist1 in (Vtr, VSU₂), Vlist2 in (Vtr, Vℤ₂) - V1, V2, V3, V4, V5 = Vlist1 - W1, W2, W3, W4, W5 = Vlist2 - for T in (Float32, ComplexF64) - t1 = rand(T, V1 ⊗ V2, V3' ⊗ V4) - t2 = rand(T, W2, W1 ⊗ W1') - t = @constinferred (t1 ⊠ t2) - d1 = dim(codomain(t1)) - d2 = dim(codomain(t2)) - d3 = dim(domain(t1)) - d4 = dim(domain(t2)) - At = convert(Array, t) - @test reshape(At, (d1, d2, d3, d4)) ≈ - reshape(convert(Array, t1), (d1, 1, d3, 1)) .* - reshape(convert(Array, t2), (1, d2, 1, d4)) - end - end -end - -@timedtestset "show tensors" begin - for V in (ℂ^2, Z2Space(0 => 2, 1 => 2), SU2Space(0 => 2, 1 => 2)) - t1 = ones(Float32, V ⊗ V, V) - t2 = randn(ComplexF64, V ⊗ V ⊗ V) - t3 = randn(Float64, zero(V), zero(V)) - # test unlimited output - for t in (t1, t2, t1', t2', t3) - output = IOBuffer() - summary(output, t) - print(output, ":\n codomain: ") - show(output, MIME("text/plain"), codomain(t)) - print(output, "\n domain: ") - show(output, MIME("text/plain"), domain(t)) - print(output, "\n blocks: \n") - first = true - for (c, b) in blocks(t) - first || print(output, "\n\n") - print(output, " * ") - show(output, MIME("text/plain"), c) - print(output, " => ") - show(output, MIME("text/plain"), b) - first = false - end - outputstr = String(take!(output)) - @test outputstr == sprint(show, MIME("text/plain"), t) - end - - # test limited output with a single block - t = randn(Float64, V ⊗ V, V)' # we know there is a single space in the codomain, so that blocks have 2 rows - output = IOBuffer() - summary(output, t) - print(output, ":\n codomain: ") - show(output, MIME("text/plain"), codomain(t)) - print(output, "\n domain: ") - show(output, MIME("text/plain"), domain(t)) - print(output, "\n blocks: \n") - c = unit(sectortype(t)) - b = block(t, c) - print(output, " * ") - show(output, MIME("text/plain"), c) - print(output, " => ") - show(output, MIME("text/plain"), b) - if length(blocks(t)) > 1 - print(output, "\n\n * … [output of 1 more block(s) truncated]") - end - outputstr = String(take!(output)) - @test outputstr == sprint(show, MIME("text/plain"), t; context = (:limit => true, :displaysize => (12, 100))) - end -end From 2295e2c4166ad042d03fa7b67fede8ec2e844fac Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 25 Mar 2026 14:58:28 -0400 Subject: [PATCH 13/14] split up fusiontree tests --- test/symmetries/doubletree.jl | 174 +++++++++ test/symmetries/fusiontrees.jl | 696 --------------------------------- test/symmetries/singletree.jl | 314 +++++++++++++++ 3 files changed, 488 insertions(+), 696 deletions(-) create mode 100644 test/symmetries/doubletree.jl delete mode 100644 test/symmetries/fusiontrees.jl create mode 100644 test/symmetries/singletree.jl diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl new file mode 100644 index 000000000..3d7420801 --- /dev/null +++ b/test/symmetries/doubletree.jl @@ -0,0 +1,174 @@ +using Test, TestExtras +using TensorKit +import TensorKit as TK +using Random: randperm +using TensorOperations + +# TODO: remove this once type_repr works for all included types +using TensorKitSectors + + +@timedtestset "Fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in (fast_tests ? fast_sectorlist : sectorlist) + Istr = TensorKit.type_repr(I) + N = I <: ProductSector ? 3 : 4 + + if UnitStyle(I) isa SimpleUnit + out = random_fusion(I, Val(N)) + numtrees = count(n -> true, fusiontrees((out..., map(dual, out)...))) + while !(0 < numtrees < 100) + out = random_fusion(I, Val(N)) + numtrees = count(n -> true, fusiontrees((out..., map(dual, out)...))) + end + incoming = rand(collect(⊗(out...))) + f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) + f2 = rand(collect(fusiontrees(out[randperm(N)], incoming, ntuple(n -> rand(Bool), N)))) + else + out = random_fusion(I, Val(N)) + out2 = random_fusion(I, Val(N)) + tp = ⊗(out...) + tp2 = ⊗(out2...) + while isempty(intersect(tp, tp2)) # guarantee fusion to same coloring + out2 = random_fusion(I, Val(N)) + tp2 = ⊗(out2...) + end + @test_throws ArgumentError fusiontrees((out..., map(dual, out)...)) + incoming = rand(collect(intersect(tp, tp2))) + f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) + f2 = rand(collect(fusiontrees(out2, incoming, ntuple(n -> rand(Bool), N)))) # no permuting + end + + if FusionStyle(I) isa UniqueFusion + f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) + f2 = rand(collect(fusiontrees(out[randperm(N)], incoming, ntuple(n -> rand(Bool), N)))) + src = (f1, f2) + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + A = fusiontensor(src) + end + else + src = FusionTreeBlock{I}((out, out), (ntuple(n -> rand(Bool), N), ntuple(n -> rand(Bool), N))) + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + A = map(fusiontensor, fusiontrees(src)) + end + end + + @testset "Double fusion tree: bending" begin + # single bend + dst, U = @constinferred TK.bendright(src) + dst2, U2 = @constinferred TK.bendleft(dst) + @test src == dst2 + @test _isone(U2 * U) + # double bend + dst1, U1 = @constinferred TK.bendleft(src) + dst2, U2 = @constinferred TK.bendleft(dst1) + dst3, U3 = @constinferred TK.bendright(dst2) + dst4, U4 = @constinferred TK.bendright(dst3) + @test src == dst4 + @test _isone(U4 * U3 * U2 * U1) + + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + all_inds = (ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...) + p₁ = ntuple(i -> all_inds[i], numout(dst2)) + p₂ = reverse(ntuple(i -> all_inds[i + numout(dst2)], numin(dst2))) + U = U2 * U1 + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst) + else + A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) + A″ = map(fusiontensor, fusiontrees(dst2)) + for (i, Ai) in enumerate(A′) + @test Ai ≈ sum(A″ .* U[:, i]) + end + end + end + end + + @testset "Double fusion tree: folding" begin + # single bend + dst, U = @constinferred TK.foldleft(src) + dst2, U2 = @constinferred TK.foldright(dst) + @test src == dst2 + @test _isone(U2 * U) + # double bend + dst1, U1 = @constinferred TK.foldright(src) + dst2, U2 = @constinferred TK.foldright(dst1) + dst3, U3 = @constinferred TK.foldleft(dst2) + dst4, U4 = @constinferred TK.foldleft(dst3) + @test src == dst4 + @test _isone(U4 * U3 * U2 * U1) + + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + all_inds = TupleTools.circshift((ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...), -2) + p₁ = ntuple(i -> all_inds[i], numout(dst2)) + p₂ = reverse(ntuple(i -> all_inds[i + numout(dst2)], numin(dst2))) + U = U2 * U1 + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst2) + else + A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) + A″ = map(fusiontensor, fusiontrees(dst2)) + for (i, Ai) in enumerate(A′) + @test Ai ≈ sum(A″ .* U[:, i]) + end + end + end + end + + @testset "Double fusion tree: repartitioning" begin + for n in 0:(2 * N) + dst, U = @constinferred TK.repartition(src, $n) + # @test _isunitary(U) + + dst′, U′ = repartition(dst, N) + @test _isone(U * U′) + + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + all_inds = (ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...) + p₁ = ntuple(i -> all_inds[i], numout(dst)) + p₂ = reverse(ntuple(i -> all_inds[i + numout(dst)], numin(dst))) + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst) + else + A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) + A″ = map(fusiontensor, fusiontrees(dst)) + for (i, Ai) in enumerate(A′) + @test Ai ≈ sum(A″ .* U[:, i]) + end + end + end + end + end + + @testset "Double fusion tree: transposition" begin + for n in 0:(2N) + i0 = rand(1:(2N)) + p = mod1.(i0 .+ (1:(2N)), 2N) + ip = mod1.(-i0 .+ (1:(2N)), 2N) + p′ = tuple(getindex.(Ref(vcat(1:N, (2N):-1:(N + 1))), p)...) + p1, p2 = p′[1:n], p′[(2N):-1:(n + 1)] + ip′ = tuple(getindex.(Ref(vcat(1:n, (2N):-1:(n + 1))), ip)...) + ip1, ip2 = ip′[1:N], ip′[(2N):-1:(N + 1)] + + dst, U = @constinferred transpose(src, (p1, p2)) + dst′, U′ = @constinferred transpose(dst, (ip1, ip2)) + @test _isone(U * U′) + + if BraidingStyle(I) isa Bosonic + dst″, U″ = permute(src, (p1, p2)) + @test U″ ≈ U + end + + if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) + if FusionStyle(I) isa UniqueFusion + @test permutedims(A, (p1..., p2...)) ≈ U * fusiontensor(dst) + else + A′ = map(Base.Fix2(permutedims, (p1..., p2...)), A) + A″ = map(fusiontensor, fusiontrees(dst)) + for (i, Ai) in enumerate(A′) + @test Ai ≈ sum(U[:, i] .* A″) + end + end + end + end + end + TK.empty_globalcaches!() +end diff --git a/test/symmetries/fusiontrees.jl b/test/symmetries/fusiontrees.jl deleted file mode 100644 index ba2627a56..000000000 --- a/test/symmetries/fusiontrees.jl +++ /dev/null @@ -1,696 +0,0 @@ -using Test, TestExtras -using TensorKit -using TensorKit: FusionTreeBlock, × -import TensorKit as TK -using Random: randperm -using TensorOperations -using MatrixAlgebraKit: isunitary -using LinearAlgebra -using TupleTools - -# TODO: remove this once type_repr works for all included types -using TensorKitSectors - -@timedtestset "Fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in (fast_tests ? fast_sectorlist : sectorlist) - Istr = TensorKit.type_repr(I) - N = 5 - out = random_fusion(I, Val(N)) - isdual = ntuple(n -> rand(Bool), N) - in = rand(collect(⊗(out...))) - numtrees = length(fusiontrees(out, in, isdual)) - @test numtrees == count(n -> true, fusiontrees(out, in, isdual)) - while !(0 < numtrees < 30) && !(one(in) in ⊗(out...)) - out = ntuple(n -> randsector(I), N) - in = rand(collect(⊗(out...))) - numtrees = length(fusiontrees(out, in, isdual)) - @test numtrees == count(n -> true, fusiontrees(out, in, isdual)) - end - it = @constinferred fusiontrees(out, in, isdual) - @constinferred Nothing iterate(it) - f, s = iterate(it) - @constinferred Nothing iterate(it, s) - @test f == @constinferred first(it) - - @testset "Fusion tree: printing" begin - @test eval(Meta.parse(sprint(show, f; context = (:module => @__MODULE__)))) == f - end - - @testset "Fusion tree: constructor properties" begin - for u in allunits(I) - @constinferred FusionTree((), u, (), (), ()) - @constinferred FusionTree((u,), u, (false,), (), ()) - @constinferred FusionTree((u, u), u, (false, false), (), (1,)) - @constinferred FusionTree((u, u, u), u, (false, false, false), (u,), (1, 1)) - @constinferred FusionTree( - (u, u, u, u), u, (false, false, false, false), (u, u), (1, 1, 1) - ) - @test_throws MethodError FusionTree((u, u, u), u, (false, false), (u,), (1, 1)) - @test_throws MethodError FusionTree( - (u, u, u), u, (false, false, false), (u, u), (1, 1) - ) - @test_throws MethodError FusionTree( - (u, u, u), u, (false, false, false), (u,), (1, 1, 1) - ) - @test_throws MethodError FusionTree((u, u, u), u, (false, false, false), (), (1,)) - - f = FusionTree((u, u, u), u, (false, false, false), (u,), (1, 1)) - @test sectortype(f) == I - @test length(f) == 3 - @test FusionStyle(f) == FusionStyle(I) - @test BraidingStyle(f) == BraidingStyle(I) - - if FusionStyle(I) isa UniqueFusion - @constinferred FusionTree((), u, ()) - @constinferred FusionTree((u,), u, (false,)) - @constinferred FusionTree((u, u), u, (false, false)) - @constinferred FusionTree((u, u, u), u) - if UnitStyle(I) isa SimpleUnit - @constinferred FusionTree((u, u, u, u)) - else - @test_throws ArgumentError FusionTree((u, u, u, u)) - end - @test_throws MethodError FusionTree((u, u), u, (false, false, false)) - else - @test_throws ArgumentError FusionTree((), u, ()) - @test_throws ArgumentError FusionTree((u,), u, (false,)) - @test_throws ArgumentError FusionTree((u, u), u, (false, false)) - @test_throws ArgumentError FusionTree((u, u, u), u) - if I <: ProductSector && UnitStyle(I) isa GenericUnit - @test_throws DomainError FusionTree((u, u, u, u)) - else - @test_throws ArgumentError FusionTree((u, u, u, u)) - end - end - end - end - - # Basic associativity manipulations of individual fusion trees - @testset "Fusion tree: split and join" begin - N = 6 - uncoupled = random_fusion(I, Val(N)) - coupled = rand(collect(⊗(uncoupled...))) - isdual = ntuple(n -> rand(Bool), N) - f = rand(collect(fusiontrees(uncoupled, coupled, isdual))) - for i in 0:N - f₁, f₂ = @constinferred TK.split(f, $i) - @test length(f₁) == i - @test length(f₂) == N - i + 1 - f′ = @constinferred TK.join(f₁, f₂) - @test f′ == f - end - end - - @testset "Fusion tree: multi_Fmove" begin - N = 6 - uncoupled = random_fusion(I, Val(N)) - coupled = rand(collect(⊗(uncoupled...))) - isdualrest = ntuple(n -> rand(Bool), N - 1) - for isdual in ((false, isdualrest...), (true, isdualrest...)) - trees = collect(fusiontrees(uncoupled, coupled, isdual)) - # trees = rand(trees, min(5, length(trees))) # limit number of tests? - for f in trees - a = f.uncoupled[1] - isduala = f.isdual[1] - c = f.coupled - f′s, coeffs = @constinferred TK.multi_Fmove(f) - @test norm(coeffs) ≈ 1 atol = 1.0e-12 # expansion should have unit norm - d = Dict(f => -one(eltype(eltype(coeffs)))) - for (f′, coeff) in zip(f′s, coeffs) - @test coeff ≈ TK.multi_associator(f, f′) - f′′s, coeff′s = @constinferred TK.multi_Fmove_inv(a, c, f′, isduala) - if FusionStyle(I) isa MultiplicityFreeFusion - @test norm(coeff′s) ≈ 1 atol = 1.0e-12 # expansion should have unit norm - else - for i in 1:Nsymbol(a, f′.coupled, c) - @test norm(getindex.(coeff′s, i)) ≈ 1 atol = 1.0e-12 # expansion should have unit norm for every possible fusion channel at the top vertex - end - end - for (f′′, coeff′) in zip(f′′s, coeff′s) - @test coeff′ ≈ conj(TK.multi_associator(f′′, f′)) - d[f′′] = get(d, f′′, zero(eltype(coeff′))) + sum(coeff .* coeff′) - end - end - @test norm(values(d)) < 1.0e-12 - end - end - - if hasfusiontensor(I) # because no permutations are involved, this also works for fermionic braiding - N = 4 - uncoupled = random_fusion(I, Val(N)) - coupled = rand(collect(⊗(uncoupled...))) - isdualrest = ntuple(n -> rand(Bool), N - 1) - for isdual in ((false, isdualrest...), (true, isdualrest...)) - trees = collect(fusiontrees(uncoupled, coupled, isdual)) - for f in trees - ftensor = fusiontensor(f) - ftensor′ = zero(ftensor) - a = f.uncoupled[1] - isduala = f.isdual[1] - c = f.coupled - f′s, coeffs = @constinferred TK.multi_Fmove(f) - for (f′, coeff) in zip(f′s, coeffs) - f′tensor = fusiontensor(f′) - for i in 1:Nsymbol(a, f′.coupled, c) - f′′ = FusionTree{I}((a, f′.coupled), c, (isduala, false), (), (i,)) - f′′tensor = fusiontensor(f′′) - ftensor′ += coeff[i] * tensorcontract(1:(N + 1), f′tensor, [(2:N)..., -1], f′′tensor, [1, -1, N + 1]) - end - end - @test ftensor′ ≈ ftensor atol = 1.0e-12 - end - end - end - end - - @testset "Fusion tree: insertat" begin - # just check some basic consistency properties here - # correctness should follow from multi_Fmove tests - N = 4 - out2 = random_fusion(I, Val(N)) - in2 = rand(collect(⊗(out2...))) - isdual2 = ntuple(n -> rand(Bool), N) - f2 = rand(collect(fusiontrees(out2, in2, isdual2))) - for i in 1:N - out1 = random_fusion(I, Val(N)) # guaranteed good fusion - out1 = Base.setindex(out1, in2, i) # can lead to poor fusion - while isempty(⊗(out1...)) # TODO: better way to do this? - out1 = random_fusion(I, Val(N)) - out1 = Base.setindex(out1, in2, i) - end - in1 = rand(collect(⊗(out1...))) - isdual1 = ntuple(n -> rand(Bool), N) - isdual1 = Base.setindex(isdual1, false, i) - f1 = rand(collect(fusiontrees(out1, in1, isdual1))) - - trees = @constinferred TK.insertat(f1, i, f2) - @test norm(values(trees)) ≈ 1 - - if hasfusiontensor(I) - Af1 = fusiontensor(f1) - Af2 = fusiontensor(f2) - Af = tensorcontract( - 1:(2N), Af1, - [1:(i - 1); -1; N - 1 .+ ((i + 1):(N + 1))], - Af2, [i - 1 .+ (1:N); -1] - ) - Af′ = zero(Af) - for (f, coeff) in trees - Af′ .+= coeff .* fusiontensor(f) - end - @test Af ≈ Af′ - end - end - end - - @testset "Fusion tree: merging" begin - N = 3 - out1 = random_fusion(I, Val(N)) - out2 = random_fusion(I, Val(N)) - in1 = rand(collect(⊗(out1...))) - in2 = rand(collect(⊗(out2...))) - tp = ⊗(in1, in2) # messy solution but it works - while isempty(tp) - out1 = random_fusion(I, Val(N)) - out2 = random_fusion(I, Val(N)) - in1 = rand(collect(⊗(out1...))) - in2 = rand(collect(⊗(out2...))) - tp = ⊗(in1, in2) - end - - f1 = rand(collect(fusiontrees(out1, in1))) - f2 = rand(collect(fusiontrees(out2, in2))) - - d = @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) - @test norm(values(d)) ≈ 1 - if !(FusionStyle(I) isa GenericFusion) - @constinferred TK.merge(f1, f2, first(in1 ⊗ in2)) - end - @test dim(in1) * dim(in2) ≈ sum( - abs2(coeff) * dim(c) for c in in1 ⊗ in2 - for μ in 1:Nsymbol(in1, in2, c) - for (f, coeff) in TK.merge(f1, f2, c, μ) - ) - - if hasfusiontensor(I) - for c in in1 ⊗ in2 - for μ in 1:Nsymbol(in1, in2, c) - Af1 = fusiontensor(f1) - Af2 = fusiontensor(f2) - Af0 = fusiontensor(FusionTree((in1, in2), c, (false, false), (), (μ,))) - _Af = tensorcontract( - 1:(N + 2), Af1, [1:N; -1], Af0, [-1; N + 1; N + 2] - ) - Af = tensorcontract( - 1:(2N + 1), Af2, [N .+ (1:N); -1], _Af, [1:N; -1; 2N + 1] - ) - Af′ = zero(Af) - for (f, coeff) in TK.merge(f1, f2, c, μ) - Af′ .+= coeff .* fusiontensor(f) - end - @test Af ≈ Af′ - end - end - end - end - - # Duality tests - @testset "Fusion tree: elementary planar trace" begin - N = 5 - uncoupled = random_fusion(I, Val(N)) - coupled = rand(collect(⊗(uncoupled...))) - isdual = ntuple(n -> rand(Bool), N) - f = rand(collect(fusiontrees(uncoupled, coupled, isdual))) - for i in 0:N # insert a (b b̄ ← 1) vertex in the tree after ith uncoupled sector and then trace it away - f₁, f₂ = TK.split(f, i) - c = f₁.coupled - funit = FusionTree{I}((c, rightunit(c)), c, (false, false), (), (1,)) - f′ = TK.join(TK.join(f₁, funit), f₂) - for b in smallset(I) - leftunit(b) == rightunit(c) || continue - out = Dict(f => -sqrtdim(b) * one(fusionscalartype(I))) - fbb = FusionTree{I}((b, dual(b)), leftunit(b), (false, true), (), (1,)) - for (f′′, coeff) in TK.insertat(f′, i + 1, fbb) - d = @constinferred TK.elementary_trace(f′′, i + 1) - for (tree, coeff2) in d - out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 - end - end - @test norm(values(out)) < 1.0e-12 - out = Dict(f => -frobenius_schur_phase(b) * sqrtdim(b) * one(fusionscalartype(I))) - fbb = FusionTree{I}((b, dual(b)), leftunit(b), (true, false), (), (1,)) - for (f′′, coeff) in TK.insertat(f′, i + 1, fbb) - for (tree, coeff2) in TK.elementary_trace(f′′, i + 1) - out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 - end - end - @test norm(values(out)) < 1.0e-12 - end - end - # insert f′ in between the two legs of a (b b̄ ← 1) vertex and then trace the outer legs away - f′ = TK.join(f, FusionTree{I}((coupled, dual(coupled)), leftunit(coupled), (false, true), (), (1,))) - for b in smallset(I) - rightunit(b) == leftunit(coupled) || continue - fbb = FusionTree{I}((b, rightunit(b), dual(b)), leftunit(b), (false, false, true), (b,), (1, 1)) - out = Dict(f′ => -sqrtdim(b) * one(fusionscalartype(I))) - for (f′′, coeff) in TK.insertat(fbb, 2, f′) - d = @constinferred TK.elementary_trace(f′′, N + 3) - for (tree, coeff2) in d - out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 - end - end - @test norm(values(out)) < 1.0e-12 - fbb = FusionTree{I}((b, rightunit(b), dual(b)), leftunit(b), (true, false, false), (b,), (1, 1)) - out = Dict(f′ => -frobenius_schur_phase(b) * sqrtdim(b) * one(fusionscalartype(I))) - for (f′′, coeff) in TK.insertat(fbb, 2, f′) - for (tree, coeff2) in TK.elementary_trace(f′′, N + 3) - out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 - end - end - @test norm(values(out)) < 1.0e-12 - end - end - - # from here: splitting-fusion tree pairs - if I <: ProductSector - N = 3 - else - N = 4 - end - if UnitStyle(I) isa SimpleUnit - out = random_fusion(I, Val(N)) - numtrees = count(n -> true, fusiontrees((out..., map(dual, out)...))) - while !(0 < numtrees < 100) - out = random_fusion(I, Val(N)) - numtrees = count(n -> true, fusiontrees((out..., map(dual, out)...))) - end - incoming = rand(collect(⊗(out...))) - f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) - f2 = rand(collect(fusiontrees(out[randperm(N)], incoming, ntuple(n -> rand(Bool), N)))) - else - out = random_fusion(I, Val(N)) - out2 = random_fusion(I, Val(N)) - tp = ⊗(out...) - tp2 = ⊗(out2...) - while isempty(intersect(tp, tp2)) # guarantee fusion to same coloring - out2 = random_fusion(I, Val(N)) - tp2 = ⊗(out2...) - end - @test_throws ArgumentError fusiontrees((out..., map(dual, out)...)) - incoming = rand(collect(intersect(tp, tp2))) - f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) - f2 = rand(collect(fusiontrees(out2, incoming, ntuple(n -> rand(Bool), N)))) # no permuting - end - - if FusionStyle(I) isa UniqueFusion - f1 = rand(collect(fusiontrees(out, incoming, ntuple(n -> rand(Bool), N)))) - f2 = rand(collect(fusiontrees(out[randperm(N)], incoming, ntuple(n -> rand(Bool), N)))) - src = (f1, f2) - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - A = fusiontensor(src) - end - else - src = FusionTreeBlock{I}((out, out), (ntuple(n -> rand(Bool), N), ntuple(n -> rand(Bool), N))) - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - A = map(fusiontensor, fusiontrees(src)) - end - end - - @testset "Double fusion tree: bending" begin - # single bend - dst, U = @constinferred TK.bendright(src) - dst2, U2 = @constinferred TK.bendleft(dst) - @test src == dst2 - @test _isone(U2 * U) - # double bend - dst1, U1 = @constinferred TK.bendleft(src) - dst2, U2 = @constinferred TK.bendleft(dst1) - dst3, U3 = @constinferred TK.bendright(dst2) - dst4, U4 = @constinferred TK.bendright(dst3) - @test src == dst4 - @test _isone(U4 * U3 * U2 * U1) - - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - all_inds = (ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...) - p₁ = ntuple(i -> all_inds[i], numout(dst2)) - p₂ = reverse(ntuple(i -> all_inds[i + numout(dst2)], numin(dst2))) - U = U2 * U1 - if FusionStyle(I) isa UniqueFusion - @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst) - else - A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) - A″ = map(fusiontensor, fusiontrees(dst2)) - for (i, Ai) in enumerate(A′) - @test Ai ≈ sum(A″ .* U[:, i]) - end - end - end - end - - @testset "Double fusion tree: folding" begin - # single bend - dst, U = @constinferred TK.foldleft(src) - dst2, U2 = @constinferred TK.foldright(dst) - @test src == dst2 - @test _isone(U2 * U) - # double bend - dst1, U1 = @constinferred TK.foldright(src) - dst2, U2 = @constinferred TK.foldright(dst1) - dst3, U3 = @constinferred TK.foldleft(dst2) - dst4, U4 = @constinferred TK.foldleft(dst3) - @test src == dst4 - @test _isone(U4 * U3 * U2 * U1) - - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - all_inds = TupleTools.circshift((ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...), -2) - p₁ = ntuple(i -> all_inds[i], numout(dst2)) - p₂ = reverse(ntuple(i -> all_inds[i + numout(dst2)], numin(dst2))) - U = U2 * U1 - if FusionStyle(I) isa UniqueFusion - @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst2) - else - A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) - A″ = map(fusiontensor, fusiontrees(dst2)) - for (i, Ai) in enumerate(A′) - @test Ai ≈ sum(A″ .* U[:, i]) - end - end - end - end - - @testset "Double fusion tree: repartitioning" begin - for n in 0:(2 * N) - dst, U = @constinferred TK.repartition(src, $n) - # @test _isunitary(U) - - dst′, U′ = repartition(dst, N) - @test _isone(U * U′) - - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - all_inds = (ntuple(identity, numout(src))..., reverse(ntuple(i -> i + numout(src), numin(src)))...) - p₁ = ntuple(i -> all_inds[i], numout(dst)) - p₂ = reverse(ntuple(i -> all_inds[i + numout(dst)], numin(dst))) - if FusionStyle(I) isa UniqueFusion - @test permutedims(A, (p₁..., p₂...)) ≈ U * fusiontensor(dst) - else - A′ = map(Base.Fix2(permutedims, (p₁..., p₂...)), A) - A″ = map(fusiontensor, fusiontrees(dst)) - for (i, Ai) in enumerate(A′) - @test Ai ≈ sum(A″ .* U[:, i]) - end - end - end - end - end - - @testset "Double fusion tree: transposition" begin - for n in 0:(2N) - i0 = rand(1:(2N)) - p = mod1.(i0 .+ (1:(2N)), 2N) - ip = mod1.(-i0 .+ (1:(2N)), 2N) - p′ = tuple(getindex.(Ref(vcat(1:N, (2N):-1:(N + 1))), p)...) - p1, p2 = p′[1:n], p′[(2N):-1:(n + 1)] - ip′ = tuple(getindex.(Ref(vcat(1:n, (2N):-1:(n + 1))), ip)...) - ip1, ip2 = ip′[1:N], ip′[(2N):-1:(N + 1)] - - dst, U = @constinferred transpose(src, (p1, p2)) - dst′, U′ = @constinferred transpose(dst, (ip1, ip2)) - @test _isone(U * U′) - - if BraidingStyle(I) isa Bosonic - dst″, U″ = permute(src, (p1, p2)) - @test U″ ≈ U - end - - if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - if FusionStyle(I) isa UniqueFusion - @test permutedims(A, (p1..., p2...)) ≈ U * fusiontensor(dst) - else - A′ = map(Base.Fix2(permutedims, (p1..., p2...)), A) - A″ = map(fusiontensor, fusiontrees(dst)) - for (i, Ai) in enumerate(A′) - @test Ai ≈ sum(U[:, i] .* A″) - end - end - end - end - end - - # @testset "Double fusion tree: planar trace" begin - # if FusionStyle(I) isa UniqueFusion - # f1, f1 = src - # dst, U = transpose((f1, f1), ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) - # d1 = zip((dst,), (U,)) - # else - # f1, f1 = first(fusiontrees(src)) - # src′ = FusionTreeBlock{I}((f1.uncoupled, f1.uncoupled), (f1.isdual, f1.isdual)) - # dst, U = transpose(src′, ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) - # d1 = zip(fusiontrees(dst), U[:, 1]) - # end - - # f1front, = TK.split(f1, N - 1) - # T = sectorscalartype(I) - # d2 = Dict{typeof((f1front, f1front)), T}() - # for ((f1′, f2′), coeff′) in d1 - # for ((f1′′, f2′′), coeff′′) in TK.planar_trace( - # (f1′, f2′), ((2:N...,), (1, ((2N):-1:(N + 3))...)), ((N + 1,), (N + 2,)) - # ) - # coeff = coeff′ * coeff′′ - # d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff - # end - # end - # for ((f1_, f2_), coeff) in d2 - # if (f1_, f2_) == (f1front, f1front) - # @test coeff ≈ dim(f1.coupled) / dim(f1front.coupled) - # else - # @test abs(coeff) < 1.0e-12 - # end - # end - # end - - - # # TODO: find better test for this - # @testset "Fusion tree: planar trace" begin - # if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - # s = randsector(I) - # N = 6 - # outgoing = (s, dual(s), s, dual(s), s, dual(s)) - # for bool in (true, false) - # isdual = (bool, !bool, bool, !bool, bool, !bool) - # for f in fusiontrees(outgoing, unit(s), isdual) - # af = convert(Array, f) - # T = eltype(af) - - # for i in 1:N - # d = @constinferred TK.elementary_trace(f, i) - # j = mod1(i + 1, N) - # inds = collect(1:(N + 1)) - # inds[i] = inds[j] - # bf = tensortrace(af, inds) - # bf′ = zero(bf) - # for (f′, coeff) in d - # bf′ .+= coeff .* convert(Array, f′) - # end - # @test bf ≈ bf′ atol = 1.0e-12 - # end - - # d2 = @constinferred TK.planar_trace(f, ((1, 3), (2, 4))) - # oind2 = (5, 6, 7) - # bf2 = tensortrace(af, (:a, :a, :b, :b, :c, :d, :e)) - # bf2′ = zero(bf2) - # for (f2′, coeff) in d2 - # bf2′ .+= coeff .* convert(Array, f2′) - # end - # @test bf2 ≈ bf2′ atol = 1.0e-12 - - # d2 = @constinferred TK.planar_trace(f, ((5, 6), (2, 1))) - # oind2 = (3, 4, 7) - # bf2 = tensortrace(af, (:a, :b, :c, :d, :b, :a, :e)) - # bf2′ = zero(bf2) - # for (f2′, coeff) in d2 - # bf2′ .+= coeff .* convert(Array, f2′) - # end - # @test bf2 ≈ bf2′ atol = 1.0e-12 - - # d2 = @constinferred TK.planar_trace(f, ((1, 4), (6, 3))) - # bf2 = tensortrace(af, (:a, :b, :c, :c, :d, :a, :e)) - # bf2′ = zero(bf2) - # for (f2′, coeff) in d2 - # bf2′ .+= coeff .* convert(Array, f2′) - # end - # @test bf2 ≈ bf2′ atol = 1.0e-12 - - # q1 = (1, 3, 5) - # q2 = (2, 4, 6) - # d3 = @constinferred TK.planar_trace(f, (q1, q2)) - # bf3 = tensortrace(af, (:a, :a, :b, :b, :c, :c, :d)) - # bf3′ = zero(bf3) - # for (f3′, coeff) in d3 - # bf3′ .+= coeff .* convert(Array, f3′) - # end - # @test bf3 ≈ bf3′ atol = 1.0e-12 - - # q1 = (1, 3, 5) - # q2 = (6, 2, 4) - # d3 = @constinferred TK.planar_trace(f, (q1, q2)) - # bf3 = tensortrace(af, (:a, :b, :b, :c, :c, :a, :d)) - # bf3′ = zero(bf3) - # for (f3′, coeff) in d3 - # bf3′ .+= coeff .* convert(Array, f3′) - # end - # @test bf3 ≈ bf3′ atol = 1.0e-12 - - # q1 = (1, 2, 3) - # q2 = (6, 5, 4) - # d3 = @constinferred TK.planar_trace(f, (q1, q2)) - # bf3 = tensortrace(af, (:a, :b, :c, :c, :b, :a, :d)) - # bf3′ = zero(bf3) - # for (f3′, coeff) in d3 - # bf3′ .+= coeff .* convert(Array, f3′) - # end - # @test bf3 ≈ bf3′ atol = 1.0e-12 - - # q1 = (1, 2, 4) - # q2 = (6, 3, 5) - # d3 = @constinferred TK.planar_trace(f, (q1, q2)) - # bf3 = tensortrace(af, (:a, :b, :b, :c, :c, :a, :d)) - # bf3′ = zero(bf3) - # for (f3′, coeff) in d3 - # bf3′ .+= coeff .* convert(Array, f3′) - # end - # @test bf3 ≈ bf3′ atol = 1.0e-12 - # end - # end - # end - # end - - - # TODO: disabled because errors for ZNElement; needs to be fixed - # BraidingStyle(I) isa HasBraiding && @testset "Double fusion tree: permutation and braiding" begin - # for n in 0:(2N) - # p = (randperm(2 * N)...,) - # p1, p2 = p[1:n], p[(n + 1):(2N)] - # ip = invperm(p) - # ip1, ip2 = ip[1:N], ip[(N + 1):(2N)] - # levels = ntuple(identity, 2N) - # l1, l2 = levels[1:N], levels[(N + 1):(2N)] - # ilevels = TupleTools.getindices(levels, p) - # il1, il2 = ilevels[1:n], ilevels[(n + 1):(2N)] - - # if BraidingStyle(I) isa SymmetricBraiding - # dst, U = @constinferred TensorKit.permute(src, (p1, p2)) - # else - # dst, U = @constinferred TensorKit.braid(src, (p1, p2), (l1, l2)) - # end - - # # check norm-preserving - # if FusionStyle(I) isa UniqueFusion - # @test abs(U) ≈ 1 - # else - # dim1 = map(fusiontrees(src)) do (f1, f2) - # return dim(f1.coupled) - # end - # dim2 = map(fusiontrees(dst)) do (f1, f2) - # return dim(f1.coupled) - # end - # @test vec(sum(abs2.(U) .* dim2; dims = 1)) ≈ dim1 - # end - - # # check reversible - # if BraidingStyle(I) isa SymmetricBraiding - # dst′, U′ = @constinferred TensorKit.permute(dst, (ip1, ip2)) - # else - # dst′, U′ = @constinferred TensorKit.braid(dst, (ip1, ip2), (il1, il2)) - # end - # @test _isone(U * U′) - - # # check fusiontensor compatibility - # if (BraidingStyle(I) isa Bosonic) && hasfusiontensor(I) - # if FusionStyle(I) isa UniqueFusion - # @test permutedims(A, (p1..., p2...)) ≈ U * fusiontensor(dst) - # else - # A′ = map(Base.Fix2(permutedims, (p1..., p2...)), A) - # A″ = map(fusiontensor, fusiontrees(dst)) - # for (i, Ai) in enumerate(A′) - # Aj = sum(A″ .* U[:, i]) - # @test Ai ≈ Aj - # end - # end - # end - # end - # end - - # TODO: even these ones fail for ZNElement, which is unexpected as they do not rely on braiding - - # @testset "Double fusion tree: planar trace" begin - # if FusionStyle(I) isa UniqueFusion - # f1, f1 = src - # dst, U = transpose((f1, f1), ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) - # d1 = zip((dst,), (U,)) - # else - # f1, f1 = first(fusiontrees(src)) - # src′ = FusionTreeBlock{I}((f1.uncoupled, f1.uncoupled), (f1.isdual, f1.isdual)) - # dst, U = transpose(src′, ((N + 1, 1:N..., ((2N):-1:(N + 3))...), (N + 2,))) - # d1 = zip(fusiontrees(dst), U[:, 1]) - # end - - # f1front, = TK.split(f1, N - 1) - # T = sectorscalartype(I) - # d2 = Dict{typeof((f1front, f1front)), T}() - # for ((f1′, f2′), coeff′) in d1 - # for ((f1′′, f2′′), coeff′′) in TK.planar_trace( - # (f1′, f2′), ((2:N...,), (1, ((2N):-1:(N + 3))...)), ((N + 1,), (N + 2,)) - # ) - # coeff = coeff′ * coeff′′ - # d2[(f1′′, f2′′)] = get(d2, (f1′′, f2′′), zero(coeff)) + coeff - # end - # end - # for ((f1_, f2_), coeff) in d2 - # if (f1_, f2_) == (f1front, f1front) - # @test coeff ≈ dim(f1.coupled) / dim(f1front.coupled) - # else - # @test abs(coeff) < 1.0e-12 - # end - # end - # end - TK.empty_globalcaches!() -end diff --git a/test/symmetries/singletree.jl b/test/symmetries/singletree.jl new file mode 100644 index 000000000..d4d6bb23e --- /dev/null +++ b/test/symmetries/singletree.jl @@ -0,0 +1,314 @@ +using Test, TestExtras +using TensorKit +using TensorKit: FusionTreeBlock, × +import TensorKit as TK +using Random: randperm +using TensorOperations +using MatrixAlgebraKit: isunitary +using LinearAlgebra +using TupleTools + +# TODO: remove this once type_repr works for all included types +using TensorKitSectors + +@timedtestset "Fusion trees for $(TensorKit.type_repr(I))" verbose = true for I in (fast_tests ? fast_sectorlist : sectorlist) + Istr = TensorKit.type_repr(I) + N = 5 + out = random_fusion(I, Val(N)) + isdual = ntuple(n -> rand(Bool), N) + in = rand(collect(⊗(out...))) + numtrees = length(fusiontrees(out, in, isdual)) + @test numtrees == count(n -> true, fusiontrees(out, in, isdual)) + while !(0 < numtrees < 30) && !(one(in) in ⊗(out...)) + out = ntuple(n -> randsector(I), N) + in = rand(collect(⊗(out...))) + numtrees = length(fusiontrees(out, in, isdual)) + @test numtrees == count(n -> true, fusiontrees(out, in, isdual)) + end + it = @constinferred fusiontrees(out, in, isdual) + @constinferred Nothing iterate(it) + f, s = iterate(it) + @constinferred Nothing iterate(it, s) + @test f == @constinferred first(it) + + @testset "Fusion tree: printing" begin + @test eval(Meta.parse(sprint(show, f; context = (:module => @__MODULE__)))) == f + end + + @testset "Fusion tree: constructor properties" begin + for u in allunits(I) + @constinferred FusionTree((), u, (), (), ()) + @constinferred FusionTree((u,), u, (false,), (), ()) + @constinferred FusionTree((u, u), u, (false, false), (), (1,)) + @constinferred FusionTree((u, u, u), u, (false, false, false), (u,), (1, 1)) + @constinferred FusionTree( + (u, u, u, u), u, (false, false, false, false), (u, u), (1, 1, 1) + ) + @test_throws MethodError FusionTree((u, u, u), u, (false, false), (u,), (1, 1)) + @test_throws MethodError FusionTree( + (u, u, u), u, (false, false, false), (u, u), (1, 1) + ) + @test_throws MethodError FusionTree( + (u, u, u), u, (false, false, false), (u,), (1, 1, 1) + ) + @test_throws MethodError FusionTree((u, u, u), u, (false, false, false), (), (1,)) + + f = FusionTree((u, u, u), u, (false, false, false), (u,), (1, 1)) + @test sectortype(f) == I + @test length(f) == 3 + @test FusionStyle(f) == FusionStyle(I) + @test BraidingStyle(f) == BraidingStyle(I) + + if FusionStyle(I) isa UniqueFusion + @constinferred FusionTree((), u, ()) + @constinferred FusionTree((u,), u, (false,)) + @constinferred FusionTree((u, u), u, (false, false)) + @constinferred FusionTree((u, u, u), u) + if UnitStyle(I) isa SimpleUnit + @constinferred FusionTree((u, u, u, u)) + else + @test_throws ArgumentError FusionTree((u, u, u, u)) + end + @test_throws MethodError FusionTree((u, u), u, (false, false, false)) + else + @test_throws ArgumentError FusionTree((), u, ()) + @test_throws ArgumentError FusionTree((u,), u, (false,)) + @test_throws ArgumentError FusionTree((u, u), u, (false, false)) + @test_throws ArgumentError FusionTree((u, u, u), u) + if I <: ProductSector && UnitStyle(I) isa GenericUnit + @test_throws DomainError FusionTree((u, u, u, u)) + else + @test_throws ArgumentError FusionTree((u, u, u, u)) + end + end + end + end + + # Basic associativity manipulations of individual fusion trees + @testset "Fusion tree: split and join" begin + N = 6 + uncoupled = random_fusion(I, Val(N)) + coupled = rand(collect(⊗(uncoupled...))) + isdual = ntuple(n -> rand(Bool), N) + f = rand(collect(fusiontrees(uncoupled, coupled, isdual))) + for i in 0:N + f₁, f₂ = @constinferred TK.split(f, $i) + @test length(f₁) == i + @test length(f₂) == N - i + 1 + f′ = @constinferred TK.join(f₁, f₂) + @test f′ == f + end + end + + @testset "Fusion tree: multi_Fmove" begin + N = 6 + uncoupled = random_fusion(I, Val(N)) + coupled = rand(collect(⊗(uncoupled...))) + isdualrest = ntuple(n -> rand(Bool), N - 1) + for isdual in ((false, isdualrest...), (true, isdualrest...)) + trees = collect(fusiontrees(uncoupled, coupled, isdual)) + # trees = rand(trees, min(5, length(trees))) # limit number of tests? + for f in trees + a = f.uncoupled[1] + isduala = f.isdual[1] + c = f.coupled + f′s, coeffs = @constinferred TK.multi_Fmove(f) + @test norm(coeffs) ≈ 1 atol = 1.0e-12 # expansion should have unit norm + d = Dict(f => -one(eltype(eltype(coeffs)))) + for (f′, coeff) in zip(f′s, coeffs) + @test coeff ≈ TK.multi_associator(f, f′) + f′′s, coeff′s = @constinferred TK.multi_Fmove_inv(a, c, f′, isduala) + if FusionStyle(I) isa MultiplicityFreeFusion + @test norm(coeff′s) ≈ 1 atol = 1.0e-12 # expansion should have unit norm + else + for i in 1:Nsymbol(a, f′.coupled, c) + @test norm(getindex.(coeff′s, i)) ≈ 1 atol = 1.0e-12 # expansion should have unit norm for every possible fusion channel at the top vertex + end + end + for (f′′, coeff′) in zip(f′′s, coeff′s) + @test coeff′ ≈ conj(TK.multi_associator(f′′, f′)) + d[f′′] = get(d, f′′, zero(eltype(coeff′))) + sum(coeff .* coeff′) + end + end + @test norm(values(d)) < 1.0e-12 + end + end + + if hasfusiontensor(I) # because no permutations are involved, this also works for fermionic braiding + N = 4 + uncoupled = random_fusion(I, Val(N)) + coupled = rand(collect(⊗(uncoupled...))) + isdualrest = ntuple(n -> rand(Bool), N - 1) + for isdual in ((false, isdualrest...), (true, isdualrest...)) + trees = collect(fusiontrees(uncoupled, coupled, isdual)) + for f in trees + ftensor = fusiontensor(f) + ftensor′ = zero(ftensor) + a = f.uncoupled[1] + isduala = f.isdual[1] + c = f.coupled + f′s, coeffs = @constinferred TK.multi_Fmove(f) + for (f′, coeff) in zip(f′s, coeffs) + f′tensor = fusiontensor(f′) + for i in 1:Nsymbol(a, f′.coupled, c) + f′′ = FusionTree{I}((a, f′.coupled), c, (isduala, false), (), (i,)) + f′′tensor = fusiontensor(f′′) + ftensor′ += coeff[i] * tensorcontract(1:(N + 1), f′tensor, [(2:N)..., -1], f′′tensor, [1, -1, N + 1]) + end + end + @test ftensor′ ≈ ftensor atol = 1.0e-12 + end + end + end + end + + @testset "Fusion tree: insertat" begin + # just check some basic consistency properties here + # correctness should follow from multi_Fmove tests + N = 4 + out2 = random_fusion(I, Val(N)) + in2 = rand(collect(⊗(out2...))) + isdual2 = ntuple(n -> rand(Bool), N) + f2 = rand(collect(fusiontrees(out2, in2, isdual2))) + for i in 1:N + out1 = random_fusion(I, Val(N)) # guaranteed good fusion + out1 = Base.setindex(out1, in2, i) # can lead to poor fusion + while isempty(⊗(out1...)) # TODO: better way to do this? + out1 = random_fusion(I, Val(N)) + out1 = Base.setindex(out1, in2, i) + end + in1 = rand(collect(⊗(out1...))) + isdual1 = ntuple(n -> rand(Bool), N) + isdual1 = Base.setindex(isdual1, false, i) + f1 = rand(collect(fusiontrees(out1, in1, isdual1))) + + trees = @constinferred TK.insertat(f1, i, f2) + @test norm(values(trees)) ≈ 1 + + if hasfusiontensor(I) + Af1 = fusiontensor(f1) + Af2 = fusiontensor(f2) + Af = tensorcontract( + 1:(2N), Af1, + [1:(i - 1); -1; N - 1 .+ ((i + 1):(N + 1))], + Af2, [i - 1 .+ (1:N); -1] + ) + Af′ = zero(Af) + for (f, coeff) in trees + Af′ .+= coeff .* fusiontensor(f) + end + @test Af ≈ Af′ + end + end + end + + @testset "Fusion tree: merging" begin + N = 3 + out1 = random_fusion(I, Val(N)) + out2 = random_fusion(I, Val(N)) + in1 = rand(collect(⊗(out1...))) + in2 = rand(collect(⊗(out2...))) + tp = ⊗(in1, in2) # messy solution but it works + while isempty(tp) + out1 = random_fusion(I, Val(N)) + out2 = random_fusion(I, Val(N)) + in1 = rand(collect(⊗(out1...))) + in2 = rand(collect(⊗(out2...))) + tp = ⊗(in1, in2) + end + + f1 = rand(collect(fusiontrees(out1, in1))) + f2 = rand(collect(fusiontrees(out2, in2))) + + d = @constinferred TK.merge(f1, f2, first(in1 ⊗ in2), 1) + @test norm(values(d)) ≈ 1 + if !(FusionStyle(I) isa GenericFusion) + @constinferred TK.merge(f1, f2, first(in1 ⊗ in2)) + end + @test dim(in1) * dim(in2) ≈ sum( + abs2(coeff) * dim(c) for c in in1 ⊗ in2 + for μ in 1:Nsymbol(in1, in2, c) + for (f, coeff) in TK.merge(f1, f2, c, μ) + ) + + if hasfusiontensor(I) + for c in in1 ⊗ in2 + for μ in 1:Nsymbol(in1, in2, c) + Af1 = fusiontensor(f1) + Af2 = fusiontensor(f2) + Af0 = fusiontensor(FusionTree((in1, in2), c, (false, false), (), (μ,))) + _Af = tensorcontract( + 1:(N + 2), Af1, [1:N; -1], Af0, [-1; N + 1; N + 2] + ) + Af = tensorcontract( + 1:(2N + 1), Af2, [N .+ (1:N); -1], _Af, [1:N; -1; 2N + 1] + ) + Af′ = zero(Af) + for (f, coeff) in TK.merge(f1, f2, c, μ) + Af′ .+= coeff .* fusiontensor(f) + end + @test Af ≈ Af′ + end + end + end + end + + # Duality tests + @testset "Fusion tree: elementary planar trace" begin + N = 5 + uncoupled = random_fusion(I, Val(N)) + coupled = rand(collect(⊗(uncoupled...))) + isdual = ntuple(n -> rand(Bool), N) + f = rand(collect(fusiontrees(uncoupled, coupled, isdual))) + for i in 0:N # insert a (b b̄ ← 1) vertex in the tree after ith uncoupled sector and then trace it away + f₁, f₂ = TK.split(f, i) + c = f₁.coupled + funit = FusionTree{I}((c, rightunit(c)), c, (false, false), (), (1,)) + f′ = TK.join(TK.join(f₁, funit), f₂) + for b in smallset(I) + leftunit(b) == rightunit(c) || continue + out = Dict(f => -sqrtdim(b) * one(fusionscalartype(I))) + fbb = FusionTree{I}((b, dual(b)), leftunit(b), (false, true), (), (1,)) + for (f′′, coeff) in TK.insertat(f′, i + 1, fbb) + d = @constinferred TK.elementary_trace(f′′, i + 1) + for (tree, coeff2) in d + out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 + end + end + @test norm(values(out)) < 1.0e-12 + out = Dict(f => -frobenius_schur_phase(b) * sqrtdim(b) * one(fusionscalartype(I))) + fbb = FusionTree{I}((b, dual(b)), leftunit(b), (true, false), (), (1,)) + for (f′′, coeff) in TK.insertat(f′, i + 1, fbb) + for (tree, coeff2) in TK.elementary_trace(f′′, i + 1) + out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 + end + end + @test norm(values(out)) < 1.0e-12 + end + end + # insert f′ in between the two legs of a (b b̄ ← 1) vertex and then trace the outer legs away + f′ = TK.join(f, FusionTree{I}((coupled, dual(coupled)), leftunit(coupled), (false, true), (), (1,))) + for b in smallset(I) + rightunit(b) == leftunit(coupled) || continue + fbb = FusionTree{I}((b, rightunit(b), dual(b)), leftunit(b), (false, false, true), (b,), (1, 1)) + out = Dict(f′ => -sqrtdim(b) * one(fusionscalartype(I))) + for (f′′, coeff) in TK.insertat(fbb, 2, f′) + d = @constinferred TK.elementary_trace(f′′, N + 3) + for (tree, coeff2) in d + out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 + end + end + @test norm(values(out)) < 1.0e-12 + fbb = FusionTree{I}((b, rightunit(b), dual(b)), leftunit(b), (true, false, false), (b,), (1, 1)) + out = Dict(f′ => -frobenius_schur_phase(b) * sqrtdim(b) * one(fusionscalartype(I))) + for (f′′, coeff) in TK.insertat(fbb, 2, f′) + for (tree, coeff2) in TK.elementary_trace(f′′, N + 3) + out[tree] = get(out, tree, zero(eltype(coeff2))) + coeff * coeff2 + end + end + @test norm(values(out)) < 1.0e-12 + end + end + + TK.empty_globalcaches!() +end From e75ce11b1feb543f3254a54925ba38d5aee06eed Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 26 Mar 2026 13:38:35 -0400 Subject: [PATCH 14/14] more shared code in the setup --- test/chainrules/factorizations.jl | 115 +----------------------- test/chainrules/linalg.jl | 42 +-------- test/chainrules/tensoroperations.jl | 46 +--------- test/factorizations/eig.jl | 15 +--- test/factorizations/ortho.jl | 15 +--- test/factorizations/projections.jl | 15 +--- test/factorizations/svd.jl | 15 +--- test/mooncake/factorizations.jl | 70 --------------- test/setup.jl | 135 ++++++++++++++++++++++++++++ test/tensors/construction.jl | 15 +--- test/tensors/contractions.jl | 15 +--- test/tensors/factorizations.jl | 15 +--- test/tensors/indexmanipulations.jl | 15 +--- test/tensors/linalg.jl | 15 +--- 14 files changed, 154 insertions(+), 389 deletions(-) diff --git a/test/chainrules/factorizations.jl b/test/chainrules/factorizations.jl index f47db82b9..88dd3279e 100644 --- a/test/chainrules/factorizations.jl +++ b/test/chainrules/factorizations.jl @@ -11,113 +11,6 @@ using Zygote using MatrixAlgebraKit using MatrixAlgebraKit: LAPACK_HouseholderQR, LAPACK_HouseholderLQ, diagview -# Test utility -# ------------- -function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::AbstractTensorMap) - return randn!(similar(x)) -end -function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::DiagonalTensorMap) - V = x.domain - return DiagonalTensorMap(randn(eltype(x), reduceddim(V)), V) -end -ChainRulesTestUtils.rand_tangent(::AbstractRNG, ::VectorSpace) = NoTangent() -function ChainRulesTestUtils.test_approx( - actual::AbstractTensorMap, expected::AbstractTensorMap, msg = ""; kwargs... - ) - for (c, b) in blocks(actual) - ChainRulesTestUtils.@test_msg msg isapprox(b, block(expected, c); kwargs...) - end - return nothing -end - -# Float32 and finite differences don't mix well -precision(::Type{<:Union{Float32, Complex{Float32}}}) = 1.0e-2 -precision(::Type{<:Union{Float64, Complex{Float64}}}) = 1.0e-5 - -function test_ad_rrule(f, args...; check_inferred = false, kwargs...) - test_rrule( - Zygote.ZygoteRuleConfig(), f, args...; - rrule_f = rrule_via_ad, check_inferred, kwargs... - ) - return nothing -end - -# project_hermitian is non-differentiable for now -_project_hermitian(x) = (x + x') / 2 - -# Gauge fixing tangents -# --------------------- -function remove_qrgauge_dependence!(ΔQ, t, Q) - for (c, b) in blocks(ΔQ) - m, n = size(block(t, c)) - minmn = min(m, n) - Qc = block(Q, c) - Q1 = view(Qc, 1:m, 1:minmn) - ΔQ2 = view(b, :, (minmn + 1):m) - mul!(ΔQ2, Q1, Q1' * ΔQ2) - end - return ΔQ -end -function remove_lqgauge_dependence!(ΔQ, t, Q) - for (c, b) in blocks(ΔQ) - m, n = size(block(t, c)) - minmn = min(m, n) - Qc = block(Q, c) - Q1 = view(Qc, 1:minmn, 1:n) - ΔQ2 = view(b, (minmn + 1):n, :) - mul!(ΔQ2, ΔQ2 * Q1', Q1) - end - return ΔQ -end -function remove_eiggauge_dependence!( - ΔV, D, V; degeneracy_atol = MatrixAlgebraKit.default_pullback_degeneracy_atol(D) - ) - gaugepart = V' * ΔV - for (c, b) in blocks(gaugepart) - Dc = diagview(block(D, c)) - # for some reason this fails only on tests, and I cannot reproduce it in an - # interactive session. - # b[abs.(transpose(diagview(Dc)) .- diagview(Dc)) .>= degeneracy_atol] .= 0 - for j in axes(b, 2), i in axes(b, 1) - abs(Dc[i] - Dc[j]) >= degeneracy_atol && (b[i, j] = 0) - end - end - mul!(ΔV, V / (V' * V), gaugepart, -1, 1) - return ΔV -end -function remove_eighgauge_dependence!( - ΔV, D, V; degeneracy_atol = MatrixAlgebraKit.default_pullback_degeneracy_atol(D) - ) - gaugepart = project_antihermitian!(V' * ΔV) - for (c, b) in blocks(gaugepart) - Dc = diagview(block(D, c)) - # for some reason this fails only on tests, and I cannot reproduce it in an - # interactive session. - # b[abs.(transpose(diagview(Dc)) .- diagview(Dc)) .>= degeneracy_atol] .= 0 - for j in axes(b, 2), i in axes(b, 1) - abs(Dc[i] - Dc[j]) >= degeneracy_atol && (b[i, j] = 0) - end - end - mul!(ΔV, V, gaugepart, -1, 1) - return ΔV -end -function remove_svdgauge_dependence!( - ΔU, ΔVᴴ, U, S, Vᴴ; degeneracy_atol = MatrixAlgebraKit.default_pullback_degeneracy_atol(S) - ) - gaugepart = project_antihermitian!(U' * ΔU + Vᴴ * ΔVᴴ') - for (c, b) in blocks(gaugepart) - Sd = diagview(block(S, c)) - # for some reason this fails only on tests, and I cannot reproduce it in an - # interactive session. - # b[abs.(transpose(diagview(Sc)) .- diagview(Sc)) .>= degeneracy_atol] .= 0 - for j in axes(b, 2), i in axes(b, 1) - abs(Sd[i] - Sd[j]) >= degeneracy_atol && (b[i, j] = 0) - end - end - mul!(ΔU, U, gaugepart, -1, 1) - return ΔU, ΔVᴴ -end - # Tests # ----- @@ -181,7 +74,7 @@ for V in spacelist DiagonalTensorMap(randn(T, reduceddim(V[1])), V[1]), ) - atol = rtol = precision(T) * dim(space(t)) + atol = rtol = default_tol(T) * dim(space(t)) fkwargs = (; positive = true) # make FiniteDifferences happy test_ad_rrule(qr_compact, t; fkwargs, atol, rtol) @@ -218,7 +111,7 @@ for V in spacelist DiagonalTensorMap(randn(T, reduceddim(V[1])), V[1]), ) - atol = rtol = precision(T) * dim(space(t)) + atol = rtol = default_tol(T) * dim(space(t)) fkwargs = (; positive = true) # make FiniteDifferences happy test_ad_rrule(lq_compact, t; fkwargs, atol, rtol) @@ -254,7 +147,7 @@ for V in spacelist # DiagonalTensorMap(rand(T, reduceddim(V[1])), V[1]), # broken in MatrixAlgebraKit ) - atol = rtol = precision(T) * dim(space(t)) + atol = rtol = default_tol(T) * dim(space(t)) d, v = eig_full(t) Δv = rand_tangent(v) @@ -290,7 +183,7 @@ for V in spacelist # TODO: fix diagonaltensormap case # DiagonalTensorMap(rand(T, reduceddim(V1)), V1)) - atol = rtol = degeneracy_atol = precision(T) * dim(space(t)) + atol = rtol = degeneracy_atol = default_tol(T) * dim(space(t)) USVᴴ = svd_compact(t) ΔU, ΔS, ΔVᴴ = rand_tangent.(USVᴴ) ΔS2 = randn!(similar(ΔS, space(ΔS))) diff --git a/test/chainrules/linalg.jl b/test/chainrules/linalg.jl index 460a23742..4b7c4df75 100644 --- a/test/chainrules/linalg.jl +++ b/test/chainrules/linalg.jl @@ -10,40 +10,6 @@ using LinearAlgebra using Zygote using MatrixAlgebraKit -# Test utility -# ------------- -function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::AbstractTensorMap) - return randn!(similar(x)) -end -function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::DiagonalTensorMap) - V = x.domain - return DiagonalTensorMap(randn(eltype(x), reduceddim(V)), V) -end -ChainRulesTestUtils.rand_tangent(::AbstractRNG, ::VectorSpace) = NoTangent() -function ChainRulesTestUtils.test_approx( - actual::AbstractTensorMap, expected::AbstractTensorMap, msg = ""; kwargs... - ) - for (c, b) in blocks(actual) - ChainRulesTestUtils.@test_msg msg isapprox(b, block(expected, c); kwargs...) - end - return nothing -end - -# Float32 and finite differences don't mix well -precision(::Type{<:Union{Float32, Complex{Float32}}}) = 1.0e-2 -precision(::Type{<:Union{Float64, Complex{Float64}}}) = 1.0e-5 - -function test_ad_rrule(f, args...; check_inferred = false, kwargs...) - test_rrule( - Zygote.ZygoteRuleConfig(), f, args...; - rrule_f = rrule_via_ad, check_inferred, kwargs... - ) - return nothing -end - -# project_hermitian is non-differentiable for now -_project_hermitian(x) = (x + x') / 2 - # Tests # ----- @@ -194,8 +160,8 @@ for V in spacelist end @timedtestset "Linear Algebra part II with scalartype $T" for T in eltypes - atol = precision(T) - rtol = precision(T) + atol = default_tol(T) + rtol = default_tol(T) for i in 1:3 E = randn(T, ⊗(V[1:i]...) ← ⊗(V[1:i]...)) test_rrule(LinearAlgebra.tr, E; atol, rtol) @@ -212,8 +178,8 @@ for V in spacelist end @timedtestset "Matrix functions ($T)" for T in eltypes - atol = precision(T) - rtol = precision(T) + atol = default_tol(T) + rtol = default_tol(T) for f in (sqrt, exp) check_inferred = false # !(T <: Real) # not type-stable for real functions t1 = randn(T, V[1] ← V[1]) diff --git a/test/chainrules/tensoroperations.jl b/test/chainrules/tensoroperations.jl index 3216d8b89..5c37e16b5 100644 --- a/test/chainrules/tensoroperations.jl +++ b/test/chainrules/tensoroperations.jl @@ -10,48 +10,6 @@ using LinearAlgebra using Zygote using MatrixAlgebraKit -const _repartition = @static if isdefined(Base, :get_extension) - Base.get_extension(TensorKit, :TensorKitChainRulesCoreExt)._repartition -else - TensorKit.TensorKitChainRulesCoreExt._repartition -end - -# Test utility -# ------------- -function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::AbstractTensorMap) - return randn!(similar(x)) -end -function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::DiagonalTensorMap) - V = x.domain - return DiagonalTensorMap(randn(eltype(x), reduceddim(V)), V) -end -ChainRulesTestUtils.rand_tangent(::AbstractRNG, ::VectorSpace) = NoTangent() -function ChainRulesTestUtils.test_approx( - actual::AbstractTensorMap, expected::AbstractTensorMap, msg = ""; kwargs... - ) - for (c, b) in blocks(actual) - ChainRulesTestUtils.@test_msg msg isapprox(b, block(expected, c); kwargs...) - end - return nothing -end - -# Float32 and finite differences don't mix well -precision(::Type{<:Union{Float32, Complex{Float32}}}) = 1.0e-2 -precision(::Type{<:Union{Float64, Complex{Float64}}}) = 1.0e-5 - -function randindextuple(N::Int, k::Int = rand(0:N)) - @assert 0 ≤ k ≤ N - _p = randperm(N) - return (tuple(_p[1:k]...), tuple(_p[(k + 1):end]...)) -end - -function test_ad_rrule(f, args...; check_inferred = false, kwargs...) - test_rrule( - Zygote.ZygoteRuleConfig(), f, args...; - rrule_f = rrule_via_ad, check_inferred, kwargs... - ) - return nothing -end # Tests # ----- @@ -108,8 +66,8 @@ for V in spacelist symmetricbraiding && @timedtestset "TensorOperations with scalartype $T" for T in eltypes - atol = precision(T) - rtol = precision(T) + atol = default_tol(T) + rtol = default_tol(T) @timedtestset "tensortrace!" begin for _ in 1:5 diff --git a/test/factorizations/eig.jl b/test/factorizations/eig.jl index 9b7ae46cd..c2ab1f3a2 100644 --- a/test/factorizations/eig.jl +++ b/test/factorizations/eig.jl @@ -4,20 +4,7 @@ using LinearAlgebra: LinearAlgebra using MatrixAlgebraKit: diagview -spacelist = if fast_tests - (Vtr, Vℤ₃, VSU₂) -elseif get(ENV, "CI", "false") == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) - elseif Sys.isapple() - (Vtr, Vℤ₃, VfU₁, VfSU₂, VIB_M) - else - (Vtr, VU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) - end -else - (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) -end +spacelist = factorization_spacelist(fast_tests) eltypes = (Float32, ComplexF64) diff --git a/test/factorizations/ortho.jl b/test/factorizations/ortho.jl index 18a21c7b7..2c80bd88f 100644 --- a/test/factorizations/ortho.jl +++ b/test/factorizations/ortho.jl @@ -4,20 +4,7 @@ using LinearAlgebra: LinearAlgebra using MatrixAlgebraKit: diagview -spacelist = if fast_tests - (Vtr, Vℤ₃, VSU₂) -elseif get(ENV, "CI", "false") == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) - elseif Sys.isapple() - (Vtr, Vℤ₃, VfU₁, VfSU₂, VIB_M) - else - (Vtr, VU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) - end -else - (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) -end +spacelist = factorization_spacelist(fast_tests) eltypes = (Float32, ComplexF64) diff --git a/test/factorizations/projections.jl b/test/factorizations/projections.jl index 03b9876ba..a00687e98 100644 --- a/test/factorizations/projections.jl +++ b/test/factorizations/projections.jl @@ -4,20 +4,7 @@ using LinearAlgebra: LinearAlgebra using MatrixAlgebraKit: diagview -spacelist = if fast_tests - (Vtr, Vℤ₃, VSU₂) -elseif get(ENV, "CI", "false") == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) - elseif Sys.isapple() - (Vtr, Vℤ₃, VfU₁, VfSU₂, VIB_M) - else - (Vtr, VU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) - end -else - (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) -end +spacelist = factorization_spacelist(fast_tests) eltypes = (Float32, ComplexF64) diff --git a/test/factorizations/svd.jl b/test/factorizations/svd.jl index 15ff34747..841b8c905 100644 --- a/test/factorizations/svd.jl +++ b/test/factorizations/svd.jl @@ -4,20 +4,7 @@ using LinearAlgebra: LinearAlgebra using MatrixAlgebraKit: diagview -spacelist = if fast_tests - (Vtr, Vℤ₃, VSU₂) -elseif get(ENV, "CI", "false") == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) - elseif Sys.isapple() - (Vtr, Vℤ₃, VfU₁, VfSU₂, VIB_M) - else - (Vtr, VU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) - end -else - (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) -end +spacelist = factorization_spacelist(fast_tests) eltypes = (Float32, ComplexF64) diff --git a/test/mooncake/factorizations.jl b/test/mooncake/factorizations.jl index f63b9f359..a4e72c0ab 100644 --- a/test/mooncake/factorizations.jl +++ b/test/mooncake/factorizations.jl @@ -36,76 +36,6 @@ spacelist = ( ) eltypes = (Float64, ComplexF64) -function remove_qrgauge_dependence!(ΔQ, t, Q) - for (c, b) in blocks(ΔQ) - m, n = size(block(t, c)) - minmn = min(m, n) - Qc = block(Q, c) - Q1 = view(Qc, 1:m, 1:minmn) - ΔQ2 = view(b, :, (minmn + 1):m) - mul!(ΔQ2, Q1, Q1' * ΔQ2) - end - return ΔQ -end -function remove_lqgauge_dependence!(ΔQ, t, Q) - for (c, b) in blocks(ΔQ) - m, n = size(block(t, c)) - minmn = min(m, n) - Qc = block(Q, c) - Q1 = view(Qc, 1:minmn, 1:n) - ΔQ2 = view(b, (minmn + 1):n, :) - mul!(ΔQ2, ΔQ2 * Q1', Q1) - end - return ΔQ -end -function remove_eiggauge_dependence!( - ΔV, D, V; degeneracy_atol = MatrixAlgebraKit.default_pullback_degeneracy_atol(D) - ) - gaugepart = V' * ΔV - for (c, b) in blocks(gaugepart) - Dc = diagview(block(D, c)) - # for some reason this fails only on tests, and I cannot reproduce it in an - # interactive session. - # b[abs.(transpose(diagview(Dc)) .- diagview(Dc)) .>= degeneracy_atol] .= 0 - for j in axes(b, 2), i in axes(b, 1) - abs(Dc[i] - Dc[j]) >= degeneracy_atol && (b[i, j] = 0) - end - end - mul!(ΔV, V / (V' * V), gaugepart, -1, 1) - return ΔV -end -function remove_eighgauge_dependence!( - ΔV, D, V; degeneracy_atol = MatrixAlgebraKit.default_pullback_degeneracy_atol(D) - ) - gaugepart = project_antihermitian!(V' * ΔV) - for (c, b) in blocks(gaugepart) - Dc = diagview(block(D, c)) - # for some reason this fails only on tests, and I cannot reproduce it in an - # interactive session. - # b[abs.(transpose(diagview(Dc)) .- diagview(Dc)) .>= degeneracy_atol] .= 0 - for j in axes(b, 2), i in axes(b, 1) - abs(Dc[i] - Dc[j]) >= degeneracy_atol && (b[i, j] = 0) - end - end - mul!(ΔV, V, gaugepart, -1, 1) - return ΔV -end -function remove_svdgauge_dependence!( - ΔU, ΔVᴴ, U, S, Vᴴ; degeneracy_atol = MatrixAlgebraKit.default_pullback_degeneracy_atol(S) - ) - gaugepart = project_antihermitian!(U' * ΔU + Vᴴ * ΔVᴴ') - for (c, b) in blocks(gaugepart) - Sd = diagview(block(S, c)) - # for some reason this fails only on tests, and I cannot reproduce it in an - # interactive session. - # b[abs.(transpose(diagview(Sc)) .- diagview(Sc)) .>= degeneracy_atol] .= 0 - for j in axes(b, 2), i in axes(b, 1) - abs(Sd[i] - Sd[j]) >= degeneracy_atol && (b[i, j] = 0) - end - end - mul!(ΔU, U, gaugepart, -1, 1) - return ΔU, ΔVᴴ -end @timedtestset "Mooncake - Factorizations: $(TensorKit.type_repr(sectortype(eltype(V)))) ($T)" for V in spacelist, T in eltypes atol = default_tol(T) diff --git a/test/setup.jl b/test/setup.jl index d1562e113..bb8134665 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -7,6 +7,10 @@ export random_fusion export sectorlist, fast_sectorlist export test_dim_isapprox export Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, Vfib, VIB_diag, VIB_M +export default_spacelist, factorization_spacelist +export remove_qrgauge_dependence!, remove_lqgauge_dependence! +export remove_eiggauge_dependence!, remove_eighgauge_dependence!, remove_svdgauge_dependence! +export test_ad_rrule, _project_hermitian using Random using Test: @test @@ -16,6 +20,10 @@ using TensorKitSectors using TensorOperations: IndexTuple, Index2Tuple using Base.Iterators: take, product using TupleTools +using MatrixAlgebraKit: MatrixAlgebraKit, diagview +using ChainRulesCore: NoTangent +using ChainRulesTestUtils: ChainRulesTestUtils, test_rrule +using Zygote: Zygote, rrule_via_ad Random.seed!(123456) @@ -273,4 +281,131 @@ VIB_M = ( Vect[IsingBimodule](D0 => 3, D1 => 4), ) +# Spacelist selection +# ------------------- +function default_spacelist(fast_tests::Bool) + fast_tests && return (Vtr, Vℤ₃, VSU₂) + if get(ENV, "CI", "false") == "true" + println("Detected running on CI") + Sys.iswindows() && return (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) + Sys.isapple() && return (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂, VSU₂U₁, VIB_M) + return (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) + end + return (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) +end + +function factorization_spacelist(fast_tests::Bool) + fast_tests && return (Vtr, Vℤ₃, VSU₂) + if get(ENV, "CI", "false") == "true" + println("Detected running on CI") + Sys.iswindows() && return (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) + Sys.isapple() && return (Vtr, Vℤ₃, VfU₁, VfSU₂, VIB_M) + return (Vtr, VU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) + end + return (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) +end + +# Gauge-fixing tangents for AD factorization tests +# ------------------------------------------------- +function remove_qrgauge_dependence!(ΔQ, t, Q) + for (c, b) in blocks(ΔQ) + m, n = size(block(t, c)) + minmn = min(m, n) + Qc = block(Q, c) + Q1 = view(Qc, 1:m, 1:minmn) + ΔQ2 = view(b, :, (minmn + 1):m) + mul!(ΔQ2, Q1, Q1' * ΔQ2) + end + return ΔQ +end +function remove_lqgauge_dependence!(ΔQ, t, Q) + for (c, b) in blocks(ΔQ) + m, n = size(block(t, c)) + minmn = min(m, n) + Qc = block(Q, c) + Q1 = view(Qc, 1:minmn, 1:n) + ΔQ2 = view(b, (minmn + 1):n, :) + mul!(ΔQ2, ΔQ2 * Q1', Q1) + end + return ΔQ +end +function remove_eiggauge_dependence!( + ΔV, D, V; degeneracy_atol = MatrixAlgebraKit.default_pullback_degeneracy_atol(D) + ) + gaugepart = V' * ΔV + for (c, b) in blocks(gaugepart) + Dc = diagview(block(D, c)) + # for some reason this fails only on tests, and I cannot reproduce it in an + # interactive session. + # b[abs.(transpose(diagview(Dc)) .- diagview(Dc)) .>= degeneracy_atol] .= 0 + for j in axes(b, 2), i in axes(b, 1) + abs(Dc[i] - Dc[j]) >= degeneracy_atol && (b[i, j] = 0) + end + end + mul!(ΔV, V / (V' * V), gaugepart, -1, 1) + return ΔV +end +function remove_eighgauge_dependence!( + ΔV, D, V; degeneracy_atol = MatrixAlgebraKit.default_pullback_degeneracy_atol(D) + ) + gaugepart = project_antihermitian!(V' * ΔV) + for (c, b) in blocks(gaugepart) + Dc = diagview(block(D, c)) + # for some reason this fails only on tests, and I cannot reproduce it in an + # interactive session. + # b[abs.(transpose(diagview(Dc)) .- diagview(Dc)) .>= degeneracy_atol] .= 0 + for j in axes(b, 2), i in axes(b, 1) + abs(Dc[i] - Dc[j]) >= degeneracy_atol && (b[i, j] = 0) + end + end + mul!(ΔV, V, gaugepart, -1, 1) + return ΔV +end +function remove_svdgauge_dependence!( + ΔU, ΔVᴴ, U, S, Vᴴ; degeneracy_atol = MatrixAlgebraKit.default_pullback_degeneracy_atol(S) + ) + gaugepart = project_antihermitian!(U' * ΔU + Vᴴ * ΔVᴴ') + for (c, b) in blocks(gaugepart) + Sd = diagview(block(S, c)) + # for some reason this fails only on tests, and I cannot reproduce it in an + # interactive session. + # b[abs.(transpose(diagview(Sc)) .- diagview(Sc)) .>= degeneracy_atol] .= 0 + for j in axes(b, 2), i in axes(b, 1) + abs(Sd[i] - Sd[j]) >= degeneracy_atol && (b[i, j] = 0) + end + end + mul!(ΔU, U, gaugepart, -1, 1) + return ΔU, ΔVᴴ +end + +# ChainRules test utilities +# ------------------------- +function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::AbstractTensorMap) + return randn!(similar(x)) +end +function ChainRulesTestUtils.rand_tangent(rng::AbstractRNG, x::DiagonalTensorMap) + V = x.domain + return DiagonalTensorMap(randn(eltype(x), reduceddim(V)), V) +end +ChainRulesTestUtils.rand_tangent(::AbstractRNG, ::VectorSpace) = NoTangent() +function ChainRulesTestUtils.test_approx( + actual::AbstractTensorMap, expected::AbstractTensorMap, msg = ""; kwargs... + ) + for (c, b) in blocks(actual) + ChainRulesTestUtils.@test_msg msg isapprox(b, block(expected, c); kwargs...) + end + return nothing +end + +function test_ad_rrule(f, args...; check_inferred = false, kwargs...) + test_rrule( + Zygote.ZygoteRuleConfig(), f, args...; + rrule_f = rrule_via_ad, check_inferred, kwargs... + ) + return nothing +end + +# project_hermitian is non-differentiable for now +_project_hermitian(x) = (x + x') / 2 + end diff --git a/test/tensors/construction.jl b/test/tensors/construction.jl index 2d9374ef8..7a093fae1 100644 --- a/test/tensors/construction.jl +++ b/test/tensors/construction.jl @@ -3,20 +3,7 @@ using TensorKit using TensorKit: type_repr -spacelist = if fast_tests - (Vtr, Vℤ₃, VSU₂) -elseif get(ENV, "CI", "false") == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) - elseif Sys.isapple() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂, VSU₂U₁, VIB_M) #, VSU₃) - else - (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) - end -else - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) -end +spacelist = default_spacelist(fast_tests) for V in spacelist I = sectortype(first(V)) diff --git a/test/tensors/contractions.jl b/test/tensors/contractions.jl index 4ca0ace38..470eb34f5 100644 --- a/test/tensors/contractions.jl +++ b/test/tensors/contractions.jl @@ -3,20 +3,7 @@ using TensorKit using TensorKit: type_repr -spacelist = if fast_tests - (Vtr, Vℤ₃, VSU₂) -elseif get(ENV, "CI", "false") == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) - elseif Sys.isapple() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂, VSU₂U₁, VIB_M) #, VSU₃) - else - (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) - end -else - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) -end +spacelist = default_spacelist(fast_tests) for V in spacelist I = sectortype(first(V)) diff --git a/test/tensors/factorizations.jl b/test/tensors/factorizations.jl index 046b3b90b..622d3b10e 100644 --- a/test/tensors/factorizations.jl +++ b/test/tensors/factorizations.jl @@ -4,20 +4,7 @@ using LinearAlgebra: LinearAlgebra using MatrixAlgebraKit: diagview -spacelist = if fast_tests - (Vtr, Vℤ₃, VSU₂) -elseif get(ENV, "CI", "false") == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) - elseif Sys.isapple() - (Vtr, Vℤ₃, VfU₁, VfSU₂, VIB_M) - else - (Vtr, VU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) - end -else - (Vtr, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VIB_diag, VIB_M) -end +spacelist = factorization_spacelist(fast_tests) eltypes = (Float32, ComplexF64) diff --git a/test/tensors/indexmanipulations.jl b/test/tensors/indexmanipulations.jl index d24762d0c..1f90a5478 100644 --- a/test/tensors/indexmanipulations.jl +++ b/test/tensors/indexmanipulations.jl @@ -4,20 +4,7 @@ using TensorKit: type_repr using Combinatorics: permutations -spacelist = if fast_tests - (Vtr, Vℤ₃, VSU₂) -elseif get(ENV, "CI", "false") == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) - elseif Sys.isapple() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂, VSU₂U₁, VIB_M) #, VSU₃) - else - (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) - end -else - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) -end +spacelist = default_spacelist(fast_tests) for V in spacelist I = sectortype(first(V)) diff --git a/test/tensors/linalg.jl b/test/tensors/linalg.jl index f727f45a8..eb9aafd59 100644 --- a/test/tensors/linalg.jl +++ b/test/tensors/linalg.jl @@ -4,20 +4,7 @@ using TensorKit: type_repr using LinearAlgebra: LinearAlgebra -spacelist = if fast_tests - (Vtr, Vℤ₃, VSU₂) -elseif get(ENV, "CI", "false") == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VIB_diag) - elseif Sys.isapple() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂, VSU₂U₁, VIB_M) #, VSU₃) - else - (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) - end -else - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂, VSU₂U₁, VIB_diag, VIB_M) #, VSU₃) -end +spacelist = default_spacelist(fast_tests) for V in spacelist I = sectortype(first(V))