SquareSymmetries is a small Julia package that allows applying elements of the symmetry group of a square (a.k.a. the dihedral group D4) to matrices. The symmetry group consists of operations like 90° rotations and flipping elements along an axis (see also here).
To install this package, from the Julia REPL, enter Pkg mode by typing ]
and execute the following:
pkg> add SquareSymmetries
You can apply the provided operations (i.e., the group elements) to matrices just like a simple function:
julia> using SquareSymmetries
julia> m = rand(2, 2)
2×2 Matrix{Float64}:
0.945848 0.755452
0.339932 0.975451
julia> rotate90(m)
2×2 Matrix{Float64}:
0.339932 0.945848
0.975451 0.755452
julia> flipx(m)
2×2 Matrix{Float64}:
0.339932 0.975451
0.945848 0.755452
You can also compose operations using Julia's function composition syntax:
julia> (rotate180 ∘ flipdiag)(m) # this is equivalent to flipadiag(m)
2×2 Matrix{Float64}:
0.975451 0.755452
0.339932 0.945848
This package provides all eight elements of D4. Each element is represented by a Julia function: unit
, rotate90
, rotate180
, rotate270
, flipx
, flipy
, flipdiag
, and flipadiag
.
julia> m = ["a11", "a12", "a21", "a22"]
2×2 Matrix{String}:
"a11" "a12"
"a21" "a22"
julia> unit(m) # identity
2×2 Matrix{String}:
"a11" "a12"
"a21" "a22"
julia> rotate90(m) # rotate matrix by 90° to the right
2×2 Matrix{String}:
"a21" "a11"
"a22" "a12"
julia> rotate180(m) # rotate by 180° (i.e., reverse elements)
2×2 Matrix{String}:
"a22" "a21"
"a12" "a11"
julia> rotate270(m) # rotate 270° to the right (or 90° to the left)
2×2 Matrix{String}:
"a12" "a22"
"a11" "a21"
julia> flipx(m) # flip elements along x-axis
2×2 Matrix{String}:
"a21" "a22"
"a11" "a12"
julia> flipy(m) # flip elements along y-axis
2×2 Matrix{String}:
"a12" "a11"
"a22" "a21"
julia> flipdiag(m) # flip elements along main diagonal (i.e., transpose)
2×2 Matrix{String}:
"a11" "a21"
"a12" "a22"
julia> flipadiag(m) # flip elements along anti diagonal
2×2 Matrix{String}:
"a22" "a12"
"a21" "a11"
To obtain all eight symmetries at once, you can use the symmetries
function:
julia> symmetries(m);
Sometimes it might be useful to take advantage of the group structure of D4. The group D4 consists of our group elements, the binary operation ∘
, and the unary operation inv
. We can use this operations directly on our group elements:
julia> rotate90 ∘ rotate180
rotate270 (generic function with 1 method)
julia> rotate180 ∘ rotate180
unit (generic function with 1 method)
julia> inv(rotate270)
rotate90 (generic function with 1 method)
julia> inv(flipdiag)
flipdiag (generic function with 1 method)
We can make use of the fact that for group elements g1,...,gn, the transformation g1(...gn(m)...) can always be replaced by a single group element, i.e., by g1 ∘ ... ∘ gn.
This can improve performance because we effectively replace n computations by only a single computation. Consider this example where we want to validate that rotating by 180° twice indeed yields the original matrix:
julia> m = rand(10000, 10000); # some huge test matrix
julia> @time rotate180(rotate180(m)) == m # this works but rotates the huge matrix twice
0.228518 seconds (4 allocations: 1.490 GiB, 1.19% gc time)
true
julia> @time (rotate180 ∘ rotate180)(m) == m # this is much more efficient as rotate180 ∘ rotate180 = unit = id
0.045496 seconds
true
Admittedly, this example is somewhat artificial. Nevertheless, note the huge difference in allocated memory. This shows that whenever we have a situation where we need to apply multiple group elements consecutively, it is beneficial to take their composition first and apply it afterwards.
It can also be useful to take the inverse of a group element. For example, consider you have an algorithm that performs some matrix transformation and this transformation should be invariant to the elements of D4. Then, we could use the following code to verify this:
julia> my_alg(m) = ... # some super clever matrix transformation
julia> m = rand(10, 10); # our test matrix
julia> for g in SquareSymmetries.D4
@assert inv(g)(my_alg(g(m))) == my_alg(m)
end
The above code checks that applying my_alg
to g(m)
and applying inv(g)
(i.e., g-1) to the output yields the same result as applying my_alg
to m
directly for all g.