Skip to content

Commit

Permalink
Merge pull request #2 from abraemer/spatial-reflection
Browse files Browse the repository at this point in the history
* Add another symmetry: SpatialReflection
* Add basissize function
  • Loading branch information
abraemer authored Sep 10, 2021
2 parents a80077d + 5c7ed4e commit 03dad9a
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 14 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/Manifest.toml
/workspace.code-workspace
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "SpinSymmetry"
uuid = "ebcc8a00-959b-4e58-a088-282ffd8a4f25"
authors = ["Adrian Braemer <[email protected]> and contributors"]
version = "0.1.0"
version = "0.2.0"

[compat]
julia = "1.6"
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`,
Expand Down
4 changes: 2 additions & 2 deletions src/SpinSymmetry.jl
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
67 changes: 67 additions & 0 deletions src/basis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,40 @@ 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!"))
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
Expand All @@ -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)

Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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.
Expand Down
36 changes: 35 additions & 1 deletion src/symmetries.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,53 @@ 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

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)
Expand Down
47 changes: 39 additions & 8 deletions test/basis.jl
Original file line number Diff line number Diff line change
@@ -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
Expand Down
28 changes: 28 additions & 0 deletions test/symmetries.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down

2 comments on commit 03dad9a

@abraemer
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register

Release notes:

  • new symmetry: SpatialReflection
  • new function: basissize

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/44640

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.2.0 -m "<description of version>" 03dad9a28e6169941e233933263e45870c4e6ff3
git push origin v0.2.0

Please sign in to comment.