diff --git a/.gitignore b/.gitignore index b067edd..ae14a69 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /Manifest.toml +/workspace.code-workspace \ No newline at end of file diff --git a/Project.toml b/Project.toml index f24c076..5dc8c07 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "SpinSymmetry" uuid = "ebcc8a00-959b-4e58-a088-282ffd8a4f25" authors = ["Adrian Braemer <11058200+abraemer@users.noreply.github.com> and contributors"] -version = "0.1.0" +version = "0.2.0" [compat] julia = "1.6" diff --git a/README.md b/README.md index 879de1a..3b215cd 100644 --- a/README.md +++ b/README.md @@ -51,23 +51,32 @@ julia> @btime symm_op = symmetrize_operator(operator, basis) # Features - Use `symmetrized_basis` to construct a collection of your symmetries. Provide as the first argument the system's size (and optionally magnetizaion block) and then follow with symmetry operations and their sector alternating. - Apply the symmetries to a state or an operator using `symmetrize_state` and `symmetrize_operator` +- Find the size of the symmetry sector with `basissize` The symmetry operations supported are: - z-magnetization block (via `zbasis(N, k)`) - Spin flip via `Flip(positions)` or `Flip(N)` - Shift symmetry via `Shift(N, amount=1)` - Swap/Exchange symmetry via `Swap(pos1, pos2)` +- Spatial reflection via `SpatialReflection(N)` where `N` denotes the number of spins in the system and their positions should be given as a Julian index, i.e. in the range `1:N`. -**Note:** The projection on a specific magnetization block is applied first. Thus if you have spin flip symmetry and restrict to a magnetization block, your symmetrized basis states look like "|↑..↑⟩ ± |↓..↑⟩". +**Note:** The projection on a specific magnetization block is applied first. Thus if you have spin flip symmetry and restrict to a magnetization block, your symmetrized basis states look like "|↑..↑⟩ ± |↓..↑⟩". So in this case you effectively specified S_z^2 and parity. +## User-defined symmetries It's also quite easy to define your own symmetry operations. Simply define a function `f` that maps one basis index to the next. -Note that these basis indices are a binary representation of the spin basis where the first spin is represented by the *least* significant bit. +Note that these basis indices are a binary representation (range 0:2^N-1) of the spin basis where the first spin is represented by the *least* significant bit. Then you can use `GenericSymmetry(f, L)` where `L` denotes the order of your symmtry. The order is the smallest number `L` s. t. `f` applied `L` is the identity for all indices. +Suppose the spatial reflection would not be implemented. You could do it yourself by defining: +```julia +julia> reflection(N) = bits -> parse(Int, string(bits; base=2, pad=N)[end:-1:1]; base=2) +julia> SpatialReflection(N) = GenericSymmetry(reflection(N), 2) +``` + # Implementation details Imagine all basis vectors as the vertices of a graph and the symmetries generate (directed) edges between them. These edges carry a phase factor that's `exp(i2π*k/L)`, diff --git a/src/SpinSymmetry.jl b/src/SpinSymmetry.jl index 157bd5a..74c7241 100644 --- a/src/SpinSymmetry.jl +++ b/src/SpinSymmetry.jl @@ -1,7 +1,7 @@ module SpinSymmetry -export Flip, Shift, Swap, GenericSymmetry -export zbasis, FullZBasis, ZBlockBasis +export Flip, Shift, Swap, SpatialReflection, GenericSymmetry +export zbasis, FullZBasis, ZBlockBasis, basissize export SymmetrizedBasis, symmetrized_basis, symmetrize_state, symmetrize_operator include("abstract.jl") diff --git a/src/basis.jl b/src/basis.jl index 2141833..200dd97 100644 --- a/src/basis.jl +++ b/src/basis.jl @@ -2,6 +2,24 @@ abstract type ZBasis end function _indices end +""" + basissize(basis) + +Return number of basis vectors in the `basis`. + +See: +- [`zbasis`](@ref) +- [`symmetrized_basis`](@ref) +""" +function basissize end + +""" + FullZBasis(N) + +Represents the states of a system of N. + +See also: [`zbasis`](@ref) +""" struct FullZBasis <: ZBasis N::Int FullZBasis(N) = N > 0 ? new(N) : throw(ArgumentError("N=$N needs to be a positive integer!")) @@ -9,6 +27,15 @@ end _indices(fzb::FullZBasis) = 1:2^fzb.N +basissize(fzb::FullZBasis) = 2^fzb.N + +""" + ZBlockBasis(N, k) + +Represents the states of a system of N spins whith k |↑⟩ (magnetization = (k-N)/2). + +See also: [`zbasis`](@ref) +""" struct ZBlockBasis <: ZBasis N::Int k::Int @@ -29,6 +56,14 @@ function _indices(zbb::ZBlockBasis) return inds end +basissize(zbb::ZBlockBasis) = binomial(zbb.N, zbb.k) + +""" + zbasis(N[, k]) + +Represent a full z-basis for N spins. If k is provided, this represents only the block +with k |↑⟩ (so magnetization of (k-N)/2). +""" zbasis(N) = FullZBasis(N) zbasis(N, k) = ZBlockBasis(N, k) @@ -53,12 +88,25 @@ function _zblock_inds!(states, N, k) end end +""" + SymmetrizedBasis + +Not intended for direct use. See [`symmetrized_basis`](@ref). +""" struct SymmetrizedBasis basis::ZBasis symmetries::Vector{AbstractSymmetry} sectors::Vector{Int} end +""" + symmetrized_basis(N[, k], symmetry, sector, more...) + symmetrized_basis(zbasis, symmetry, sector, more...) + +Construct a basis in the specified symmetry sectors. Any number of symmetries may be specified. + +Either provide number of spins (and optionally `k` block) or a [`zbasis`](@ref). +""" function symmetrized_basis(N::Int, symmetry::AbstractSymmetry, sector::Int, more...) symmetrized_basis(zbasis(N), symmetry, sector, more...) end @@ -72,9 +120,19 @@ function symmetrized_basis(zbasis::ZBasis, symmetry::AbstractSymmetry, sector::I SymmetrizedBasis(zbasis, [symmetry, more[1:2:end]...], [sector, more[2:2:end]...]) end +basissize(basis::SymmetrizedBasis) = length(_phase_factors(_indices(basis.basis), basis.symmetries, basis.sectors)) symmetrize_state(state, args...) = symmetrize_state(state, symmetrized_basis(args...)) +""" + symmetrize_state(state, basis) + symmetrize_state(state, args...) + +Symmetrize the given `state` into the symmetric sector specified by the [`symmetrized_basis`](@ref). + +Alternatively, provide everything needed to construct the [`symmetrized_basis`](@ref) and +will be constructed internally. +""" function symmetrize_state(state, basis::SymmetrizedBasis) if length(state) != 2^basis.basis.N throw(ArgumentError("""State has wrong size. @@ -97,6 +155,15 @@ end symmetrize_operator(operator, args...) = symmetrize_operator(operator, symmetrized_basis(args...)) +""" + symmetrize_operator(operator, basis) + symmetrize_operator(operator, args...) + +Symmetrize the given `operator` into the symmetric sector specified by the [`symmetrized_basis`](@ref). + +Alternatively, provide everything needed to construct the [`symmetrized_basis`](@ref) and +will be constructed internally. +""" function symmetrize_operator(operator, basis::SymmetrizedBasis) if length(operator) != 4^basis.basis.N || size(operator, 1) != size(operator, 2) throw(ArgumentError("""Operator has wrong size. diff --git a/src/symmetries.jl b/src/symmetries.jl index 14d197b..575d66d 100644 --- a/src/symmetries.jl +++ b/src/symmetries.jl @@ -61,6 +61,10 @@ Positions should be given in the range 1:N. struct Swap <: AbstractSymmetry ind1::Int ind2::Int + function Swap(pos1, pos2) + pos1 == pos2 && @warn "Swap with identical positions is nonsensical and won't work properly!" + new(pos1, pos2) + end end _order(::Swap) = 2 @@ -68,12 +72,42 @@ _order(::Swap) = 2 function _swap_bits(x, ind1, ind2) # compare bits, swap if unequal sw = ((x >> ind1) & 1) ⊻ ((x >> ind2) & 1) # xor == 1 if unequal - xor(x, (sw << ind1) | (sw << ind2)) # x(bit, 1) -> flips bit + return xor(x, (sw << ind1) | (sw << ind2)) # xor(bit, 1) -> flips bit end # convert spin positions to bitstring position (s::Swap)(index) = _swap_bits(index, s.ind1-1, s.ind2-1) +""" + SpatialReflection(N) + +Reflects the whole chain in space. + +# Fields +- `N::Int`: Number of spins +""" +struct SpatialReflection <: AbstractSymmetry + N::Int + function SpatialReflection(N) + N == 1 && @warn "SpatialReflection with N=1 is nonsensical and won't work properly!" + new(N) + end +end + +_order(::SpatialReflection) = 2 + +# approx 10times faster than the simpler +# parse(Int, string(x; base=2, pad=N)[N:-1:1]; base=2) +# Also note that the former does not account for cases where there are actually more than N +# spins +function _reflect_bits(N, bits) + for i in 0:div(N,2)-1 + bits = _swap_bits(bits, i, N-1-i) + end + return bits +end + +(sr::SpatialReflection)(index) = _reflect_bits(sr.N, index) """ GenericSymmetry(f, L) diff --git a/test/basis.jl b/test/basis.jl index ec9615a..1a523e9 100644 --- a/test/basis.jl +++ b/test/basis.jl @@ -1,15 +1,46 @@ @testset "basis.jl" begin @testset "ZBasis" begin - @test_throws ArgumentError zbasis(-1) - @test SpinSymmetry._indices(zbasis(5)) == 1:2^5 + @testset "FullZBasis" begin + @test_throws ArgumentError zbasis(-1) + @test zbasis(5) isa FullZBasis + @test SpinSymmetry._indices(zbasis(5)) == 1:2^5 + end + + @testset "ZBlockBasis" begin + @test_throws ArgumentError zbasis(-1, 0) + @test_throws ArgumentError zbasis(2,-1) + @test_throws ArgumentError zbasis(2,3) + + @test zbasis(2,1) isa ZBlockBasis - @test_throws ArgumentError zbasis(-1, 0) - @test_throws ArgumentError zbasis(2,-1) - @test_throws ArgumentError zbasis(2,3) + # small correctness check + @test SpinSymmetry._indices(zbasis(2,0)) == [1] + @test SpinSymmetry._indices(zbasis(2,1)) == [2,3] + @test SpinSymmetry._indices(zbasis(2,2)) == [4] + + + digitsum(k) = sum(parse.(Int, [string(k-1; base=2)...])) + for k in 0:10 + zblock = zbasis(10, k) + @test all(digitsum.(SpinSymmetry._indices(zblock)) .== k) + end + end + end + + @testset "basissize" begin + let basis = zbasis(10) + @test basissize(basis) == length(SpinSymmetry._indices(basis)) + end + + for k in 0:10 + let basis = zbasis(10, k) + @test basissize(basis) == length(SpinSymmetry._indices(basis)) + end + end - @test SpinSymmetry._indices(zbasis(2,0)) == [1] - @test SpinSymmetry._indices(zbasis(2,1)) == [2,3] - @test SpinSymmetry._indices(zbasis(2,2)) == [4] + @test basissize(symmetrized_basis(10, Flip(10), 0)) == 2^9 + @test basissize(symmetrized_basis(10, 5, Flip(10), 0)) == binomial(10, 5)/2 + @test basissize(symmetrized_basis(10, 4, Flip(10), 0)) == binomial(10, 4) end @testset "symmetrize stuff" begin diff --git a/test/symmetries.jl b/test/symmetries.jl index 1e716be..71c9fa8 100644 --- a/test/symmetries.jl +++ b/test/symmetries.jl @@ -23,6 +23,11 @@ # small correctness test @test Shift(2).(0:3) == [0,2,1,3] + # injectivity test + for k in 0:9 + @test length(Set(Shift(10, k).(0:2^10-1))) == 2^10 + end + # order test for N in 2:10 for amount in 1:div(N,2) @@ -39,6 +44,10 @@ @test Flip(2).(0:3) == [3,2,1,0] @test Flip([1,2]).(0:7) == vcat([3,2,1,0], 4 .+ [3,2,1,0]) + # injectivity test + @test length(Set(Flip(10).(0:2^10-1))) == 2^10 + @test length(Set(Flip(2:2:10).(0:2^10-1))) == 2^10 + # order test for N in 2:10 @test order_test(Flip(N), N) @@ -51,6 +60,11 @@ @test Swap(1,2).(0:3) == [0,2,1,3] @test Swap(2,2).(0:31) == 0:31 # identity + # injectivity test + for (p1,p2) in [(1,2), (3,4), (1,4), (9,6), (1,9), (5,5), (3,7)] + @test length(Set(Swap(p1, p2).(0:2^10-1))) == 2^10 + end + # order test for N in 3:2:16 pos1 = round(Int, N/4) @@ -61,6 +75,20 @@ end end + @testset "SpatialReflection" begin + # small correctness test + @test SpatialReflection(2).(0:3) == [0,2,1,3] + + # injectivity test + @test length(Set(SpatialReflection(10).(0:2^10-1))) == 2^10 + + # order test + for N in 4:12 + @test order_test(SpatialReflection(N), N) + @test order_test(SpatialReflection(div(N,2)), N) + end + end + # rather pointless # GenericSymmetry is as correct as the values it contains... @testset "GenericSymmetry" begin