PermutationSymmetricTensors.jl
PermutationSymmetricTensors.jl
provides efficient tools for working with multidimensional arrays that are symmetric under any permutation of their indices.
This page provides practical examples, usage tips, and performance insights.
Getting Started
using PermutationSymmetricTensors
using Random # For reproducibility if needed
Creating Symmetric Tensors
1. Low-Level Constructor
N = 3 # Size of each axis
dim = 2 # Number of dimensions
len = find_symmetric_tensor_size(N, dim) # e.g., 6 for N=3, dim=2
data = rand(Float64, len)
tensor_a = SymmetricTensor(data, Val(N), Val(dim))
3×3 SymmetricTensor{Float64, 3, 2}:
0.0343697 0.0810937 0.824177
0.0810937 0.632891 0.724294
0.824177 0.724294 0.707992
2. Built-In Constructors
tensor_b = rand(SymmetricTensor{Float64, 3, 3}) # Random values
3×3×3 SymmetricTensor{Float64, 3, 3}:
[:, :, 1] =
0.012431 0.114933 0.925826
0.114933 0.0432676 0.60299
0.925826 0.60299 0.509273
[:, :, 2] =
0.114933 0.0432676 0.60299
0.0432676 0.957753 0.116302
0.60299 0.116302 0.893451
[:, :, 3] =
0.925826 0.60299 0.509273
0.60299 0.116302 0.893451
0.509273 0.893451 0.753012
tensor_c = zeros(SymmetricTensor{Int, 4, 2}) # Zeros
4×4 SymmetricTensor{Int64, 4, 2}:
0 0 0 0
0 0 0 0
0 0 0 0
0 0 0 0
tensor_d = ones(SymmetricTensor{Bool, 2, 4}) # Ones
2×2×2×2 SymmetricTensor{Bool, 2, 4}:
[:, :, 1, 1] =
1 1
1 1
[:, :, 2, 1] =
1 1
1 1
[:, :, 1, 2] =
1 1
1 1
[:, :, 2, 2] =
1 1
1 1
tensor_e = similar(tensor_c) # Uninitialized with same type
4×4 SymmetricTensor{Int64, 4, 2}:
139692035802416 139691875676624 139691954981184 139691985920752
139691875676624 1 26735 4311810048
139691954981184 26735 72339069014704385 1
139691985920752 4311810048 1 139691836279696
tensor_f = similar(tensor_d, Char) # Uninitialized with new type
2×2×2×2 SymmetricTensor{Char, 2, 4}:
[:, :, 1, 1] =
'\x8d\x91\x21' '\x00\x00\x7f\x0c'
'\x00\x00\x7f\x0c' '\x95\x83\x83\x10'
[:, :, 2, 1] =
'\x00\x00\x7f\x0c' '\x95\x83\x83\x10'
'\x95\x83\x83\x10' '\x00\x00\x7f\x0c'
[:, :, 1, 2] =
'\x00\x00\x7f\x0c' '\x95\x83\x83\x10'
'\x95\x83\x83\x10' '\x00\x00\x7f\x0c'
[:, :, 2, 2] =
'\x95\x83\x83\x10' '\x00\x00\x7f\x0c'
'\x00\x00\x7f\x0c' '\x93\x10\x2c'
Indexing and Symmetry
Indexing into a symmetric tensor is invariant under permutations of the indices:
A = rand(SymmetricTensor{Float64, 2, 3})
A[1, 2, 1] == A[2, 1, 1] == A[1, 1, 2] # All access the same element
A[1, 2, 1] = 42.0
@assert A[2, 1, 1] == 42.0
You can also slice and broadcast:
A[:, 1, 1] .= 0
2-element view(::SymmetricTensor{Float64, 2, 3}, :, 1, 1) with eltype Float64:
0.0
0.0
Utility Functions
find_symmetric_tensor_size
Returns the number of unique values stored in a symmetric tensor of size N
and dimension dim
.
find_symmetric_tensor_size(3, 3) # Returns 10
10
Useful for constructing from raw data:
data = rand(Float64, find_symmetric_tensor_size(4, 3))
T = SymmetricTensor(data, Val(4), Val(3));
4×4×4 SymmetricTensor{Float64, 4, 3}:
[:, :, 1] =
0.563074 0.462475 0.599382 0.450325
0.462475 0.555714 0.462346 0.329872
0.599382 0.462346 0.920014 0.0290472
0.450325 0.329872 0.0290472 0.00306385
[:, :, 2] =
0.462475 0.555714 0.462346 0.329872
0.555714 0.978065 0.410702 0.911111
0.462346 0.410702 0.887652 0.94996
0.329872 0.911111 0.94996 0.689394
[:, :, 3] =
0.599382 0.462346 0.920014 0.0290472
0.462346 0.410702 0.887652 0.94996
0.920014 0.887652 0.925317 0.425696
0.0290472 0.94996 0.425696 0.524713
[:, :, 4] =
0.450325 0.329872 0.0290472 0.00306385
0.329872 0.911111 0.94996 0.689394
0.0290472 0.94996 0.425696 0.524713
0.00306385 0.689394 0.524713 0.858964
find_degeneracy
Returns a tensor indicating how many permutations of the indices point to each element.
A = rand(SymmetricTensor{Float64, 2, 3})
D = find_degeneracy(A)
@show D[1, 1, 2]
3
find_full_indices
Gives you the sorted list of canonical index tuples that correspond to the linear storage layout.
inds = find_full_indices(3, 2)
for (i, idx) in enumerate(inds)
println("Linear index $i maps to Cartesian index $idx")
end
Linear index 1 maps to Cartesian index (1, 1)
Linear index 2 maps to Cartesian index (2, 1)
Linear index 3 maps to Cartesian index (3, 1)
Linear index 4 maps to Cartesian index (2, 2)
Linear index 5 maps to Cartesian index (3, 2)
Linear index 6 maps to Cartesian index (3, 3)
Performance Tips
Memory Savings
A = rand(SymmetricTensor{Float64, 14, 16})
println("Compressed size: ", Base.format_bytes(Base.summarysize(A)))
println("Full array would require: ", round(Float64(big(14)^16 * 8)/2^30, digits=2), " GiB")
Compressed size: 517.763 MiB
Full array would require: 1.622701687969e10 GiB
Efficient Aggregations
Use the internal .data
field with the degeneracy weights:
deg = find_degeneracy(A)
sum(A.data .* deg.data) # Correct full sum over symmetric elements
1.089093842613894e18
Broadcasting Performance
Avoid converting to full arrays unintentionally:
A.data .= log.(A.data .+ 1e-8) # Efficient
#B = A .* 0 # WARNING: returns a full Array{Float64, N}. Will overflow RAM
67863915-element Vector{Float64}:
-2.502680504965209
-1.7028418625714141
-0.4419748679521749
-0.21485011366673443
-0.20071991735384123
-1.638703442741575
-1.5801237225611984
-0.11232186510303867
-3.3863117367525923
-2.6029984067939456
⋮
-2.9626982241222737
-1.1703103227993294
-1.1267726530645852
-0.7774239355460404
-3.092363344888435
-0.5998434707460966
-1.6477318828832068
-1.0512414206725307
-0.6724257098390304
Example: Exploring Internal Representation
A = rand(SymmetricTensor{Float64, 3, 3})
deg = find_degeneracy(A)
inds = find_full_indices(A)
for i in eachindex(A.data)
println("data[$i] = ", A.data[i], ", index: ", inds[i], ", deg: ", deg[inds[i]...])
end
data[1] = 0.06626241426066515, index: (1, 1, 1), deg: 1
data[2] = 0.6598840878949546, index: (2, 1, 1), deg: 3
data[3] = 0.4792112512853204, index: (3, 1, 1), deg: 3
data[4] = 0.5999441924615395, index: (2, 2, 1), deg: 3
data[5] = 0.039245989097449585, index: (3, 2, 1), deg: 6
data[6] = 0.3670803424080171, index: (3, 3, 1), deg: 3
data[7] = 0.5160924252878447, index: (2, 2, 2), deg: 1
data[8] = 0.0031716366236232973, index: (3, 2, 2), deg: 3
data[9] = 0.7070835241742407, index: (3, 3, 2), deg: 3
data[10] = 0.9630041831938483, index: (3, 3, 3), deg: 1
Summary of Public API
Feature | Description |
---|---|
SymmetricTensor{T, N, dim} | Core symmetric tensor type |
find_symmetric_tensor_size | Number of stored unique elements |
find_degeneracy | Permutation multiplicity tensor |
find_full_indices | List of canonical index tuples |
zeros , ones , rand , similar | Tensor constructors |
getindex , setindex! | Symmetric indexing and mutation |
Full API Reference
For a complete overview of all exported functions and types:
PermutationSymmetricTensors.PermutationSymmetricTensors
— ModuleThis module provides the SymmetricTensor
type, representing a tensor whose elements are symmetric under any permutation of their indices. It allows for efficient storage and manipulation of such tensors.
Key functionalities include:
- Creating
SymmetricTensor
instances (e.g., with random values, zeros, ones). - Indexing into the tensor using standard array-like notation.
- Calculating the number of unique elements required to store the tensor using
find_symmetric_tensor_size
. - Determining the degeneracy (number of equivalent permutations) for each element using
find_degeneracy
. - Retrieving the unique sorted Cartesian indices corresponding to the stored elements via
find_full_indices
.
PermutationSymmetricTensors.SymmetricTensor
— TypeSymmetricTensor{T, N, dim} <: AbstractArray{T, dim}
A tensor of dim
dimensions, where each dimension has N
elements of type T
. The tensor is symmetric under permutation of its indices.
Fields
data::Vector{T}
: A flat vector storing the unique elements of the symmetric tensor. The length of this vector is determined byfind_symmetric_tensor_size(N, dim)
.linear_indices::Vector{Vector{Int64}}
: Precomputed indices to map sorted Cartesian indices to the linear index in thedata
vector. This is an internal field used for efficient indexing.
PermutationSymmetricTensors.SymmetricTensor
— MethodSymmetricTensor(data::Array{T, 1}, ::Val{N}, ::Val{dim}) where {T, N, dim}
Low level constructor for the SymmetricTensor type.
Example:
N = 10
dim = 3
Ndata = find_symmetric_tensor_size(N, dim)
T = Float64
data = rand(T, Ndata)
SymmetricTensor(data, Val(N), Val(dim))
Base.getindex
— Methodgetindex(A::SymmetricTensor{T, N, dim}, I::Int...) -> T
Retrieve the element at the specified indices I
from the symmetric tensor A
. The indices I
can be provided in any order; due to the tensor's symmetry, A[i, j, k]
is equivalent to A[k, j, i]
, etc.
The method also supports linear indexing if a single index is provided.
Arguments
A::SymmetricTensor{T, N, dim}
: The symmetric tensor to access.I::Int...
: A sequence ofdim
integer indices, or a single linear index.
Returns
T
: The element at the specified position.
Examples
julia> tensor = ones(SymmetricTensor{Float64, 2, 3});
julia> tensor[1, 2, 1]
1.0
julia> tensor[1, 1, 2] # Same as above due to symmetry
1.0
julia> tensor[2] # Linear indexing (equivalent to tensor[2,1,1] in this case based on internal order)
1.0
This method is implemented using a @generated
function for efficiency, which constructs specialized code based on the tensor's dimension (dim
). For example, for dim = 3
, the internal logic effectively sorts the indices and uses precomputed values to find the element in the underlying data
vector.
Base.setindex!
— Methodsetindex!(A::SymmetricTensor{T, N, dim}, value, I::Int...) -> typeof(value)
Set the element at the specified indices I
in the symmetric tensor A
to value
. The indices I
can be provided in any order; due to the tensor's symmetry, setting A[i, j, k]
will also affect permutations like A[k, j, i]
.
The method also supports linear indexing if a single index is provided.
Arguments
A::SymmetricTensor{T, N, dim}
: The symmetric tensor to modify.value
: The value to assign to the element.I::Int...
: A sequence ofdim
integer indices, or a single linear index.
Returns
- The assigned
value
.
Examples
julia> tensor = zeros(SymmetricTensor{Float64, 2, 3});
julia> tensor[1, 2, 1] = 5.0;
julia> tensor[1, 1, 2]
5.0
julia> tensor[1] = 3.0; # Linear indexing
julia> tensor[1,1,1] # Assuming [1,1,1] is the first linear index
3.0
This method is implemented using a @generated
function for efficiency, which constructs specialized code based on the tensor's dimension (dim
). For example, for dim = 3
, the internal logic effectively sorts the indices and uses precomputed values to find the element in the underlying data
vector to update.
PermutationSymmetricTensors.check_correct_size
— Methodcheck_correct_size(num_elements::Int, N::Int, dim::Int) -> Bool
Internal helper function to verify if num_elements
is the correct count for a SymmetricTensor
with dimension dim
and size N
for each dimension.
Arguments
num_elements::Int
: The number of elements to check (typicallylength(data)
).N::Int
: The size of each dimension.dim::Int
: The number of dimensions.
Returns
Bool
:true
ifnum_elements
matchesfind_symmetric_tensor_size(N, dim)
,false
otherwise.
PermutationSymmetricTensors.find_N_repetitions_sorted!
— Methodfind_N_repetitions_sorted!(reps::Vector{<:Integer}, tup::NTuple)
Internal helper function to count repetitions of elements in a sorted tuple. It updates the reps
vector such that reps[i]
stores the count of distinct elements that appear exactly i
times in the tuple tup
.
This function is used by find_degeneracy
to calculate the multiplicity factor for tensor elements.
Arguments
reps::Vector{<:Integer}
: A vector to store the counts. It will be modified in-place. Its length should be at leastlength(tup)
.tup::NTuple
: A tuple of elements, which must be sorted in non-decreasing order.
Examples
julia> reps = zeros(Int, 8);
julia> tup = (1, 3, 3, 5, 5, 5, 5, 7); # Must be sorted
julia> PermutationSymmetricTensors.find_N_repetitions_sorted!(reps, tup);
julia> reps # Element 1 appears once, 7 once (reps[1]=2). Element 3 appears twice (reps[2]=1). Element 5 appears four times (reps[4]=1).
8-element Vector{Int64}:
2 # Two elements (1 and 7) appear once
1 # One element (3) appears twice
0 # Zero elements appear three times
1 # One element (5) appears four times
0 # Zero elements appear five times
0 # Zero elements appear six times
0 # Zero elements appear seven times
0 # Zero elements appear eight times
PermutationSymmetricTensors.find_degeneracy
— Methodfunction find_degeneracy(N::Int, dim::Int)
function find_degeneracy(A::SymmetricTensor{T, N, dim}) where {T, N, dim}
function find_degeneracy(N, dim, full_indices::Vector{<:NTuple{dim, Any}})
Returns a SymmetricTensor{Int64, N, dim}
where each element d[i,j,...]
contains the number of distinct permutations of the indices (i,j,...)
that map to the same unique element in the SymmetricTensor
. This value represents the "degeneracy" of that particular combination of indices.
Arguments
N::Int
: The size of each dimension of the tensor.dim::Int
: The number of dimensions of the tensor.A::SymmetricTensor
: An existingSymmetricTensor
instance from whichN
anddim
can be derived.full_indices::Vector{<:NTuple{dim, Any}}
: (Optional) The output offind_full_indices(N, dim)
, provided for efficiency if already computed.
Returns
SymmetricTensor{Int64, N, dim}
: A tensor where each element stores its degeneracy.
Examples
julia> find_degeneracy(3, 3)
3×3×3 SymmetricTensor{Int64, 3, 3}:
[:, :, 1] =
1 3 3
3 3 6
3 6 3
[:, :, 2] =
3 3 6
3 1 3
6 3 3
[:, :, 3] =
3 6 3
6 3 3
3 3 1
julia> a = rand(SymmetricTensor{Float64, 2, 4});
julia> d = find_degeneracy(a);
julia> d[1,1,1,1] # Element (1,1,1,1) is unique
1
julia> d[1,1,1,2] # Element (1,1,1,2) has 4 permutations (1112, 1121, 1211, 2111)
4
PermutationSymmetricTensors.find_full_indices
— Methodfunction find_full_indices(N, dim)
Returns an ordered array of tuples of indices (i1, i2, i3, ..., i{dim})
such that i1 >= i2 >= i3 ... >= i{dim}
. This can be used to find the cartesian index that corresponds to a linear index of a SymmetricTensor{T, N, dim}
. Example:
julia> find_full_indices(3, 3)
10-element Vector{Tuple{Int8, Int8, Int8}}:
(1, 1, 1)
(2, 1, 1)
(3, 1, 1)
(2, 2, 1)
(3, 2, 1)
(3, 3, 1)
(2, 2, 2)
(3, 2, 2)
(3, 3, 2)
(3, 3, 3)
It is implemented with a generated function, for dim = 3, the following code will be executed:
function _find_full_indices(N, Val(3))
full_indices = NTuple{3, Int16}[]
for i3 = 1:N
for i2 = i3:N
for i1 = i2:N
push!(full_indices, ((i1..., i2)..., i3))
end
end
end
full_indices
end
PermutationSymmetricTensors.find_linear_index_array
— Methodfind_linear_index_array(N::Int, ::Val{dim}) -> Vector{Int64}
Internal @generated
function to compute a vector of index contributions for a specific dimension, used in calculating the linear index into the data
array of a SymmetricTensor
.
This function is part of the mechanism that maps multi-dimensional indices (i1, i2, ..., idim)
(sorted descendingly) to a unique linear index. The SymmetricTensor
stores dim
such vectors in its linear_indices
field. Each vector A.linear_indices[k]
corresponds to find_linear_index_array(N, Val(k))
.
The linear index for (I1, I2, ..., Ik, ..., Idim)
(where Ik
are sorted indices) is roughly sum(A.linear_indices[k][Ik] for k=1:dim)
.
The actual generated code efficiently calculates these contributions. For example, for dim = 3
, it generates:
function find_linear_index_array(N::Int, ::Val{3})
idim_contribution_array = zeros(Int64, N)
contribution = 0
count = 0
firstcount = 0
for i3 = 1:N
for i2 = i3:N
for i1 = i2:N
count += 1
if ((i1 == i2) && i2 == i3)
if i3 == 1
firstcount = count
end
contribution = count - firstcount
idim_contribution_array[i3] = contribution
end
end
end
end
idim_contribution_array
end
PermutationSymmetricTensors.find_linear_indices
— Methodfind_linear_indices(::Val{N}, ::Val{dim}) -> Vector{Vector{Int64}}
Internal function to compute all necessary linear index contribution vectors for a SymmetricTensor
of size N
and dimension dim
.
This function iteratively calls find_linear_index_array(N, Val(k))
for k
from 1 to dim
. The resulting collection of vectors is stored in the linear_indices
field of a SymmetricTensor
and is crucial for its indexing operations.
Arguments
::Val{N}
: AVal
instance representing the size of each dimension.::Val{dim}
: AVal
instance representing the number of dimensions.
Returns
Vector{Vector{Int64}}
: A vector where each inner vector is the result offind_linear_index_array(N, Val(k))
fork
in1:dim
.
PermutationSymmetricTensors.find_symmetric_tensor_size
— Methodfunction find_symmetric_tensor_size(N, dim)
Returns the number of elements of a symmetric tensor of dimension dim of N elements in each dimension. The results is given by binomial(N-1+dim, dim).
Example: julia` julia> find_symmetric_tensor_size(20, 6) 177100
Random.rand!
— Methodrand!(A::SymmetricTensor, [rng::AbstractRNG], [values])
Fill the symmetric tensor A
with random values.
This function populates the underlying data vector of the SymmetricTensor
with random numbers. Due to the tensor's symmetry, only the unique elements are stored and randomized.
Arguments
A::SymmetricTensor
: The symmetric tensor to be filled with random values. It is modified in-place.rng::AbstractRNG
(optional): A specific random number generator to use. If not provided, the default global RNG is used.values
(optional): A collection of values to sample from (e.g., a range like0:9
, or a specific set like[1.0, 2.5, 3.0]
). If not provided,rand
will produce values of the tensor's element type (e.g.,Float64
in[0,1)
).
Returns
A
: The modified tensorA
, filled with random values. (Note:rand!
traditionally returns the modified array, but the current implementation returnsnothing
. This docstring reflects the traditional behavior for consistency withBase.rand!
, though the implementation detail differs.)
Examples
julia> N = 2; dim = 2;
julia> ts = zeros(SymmetricTensor{Float64, N, dim});
julia> rand!(ts); # Fill with random Float64 values
julia> ts[1,1] # Will be a random Float64
julia> rand!(ts, MersenneTwister(123)); # Using a specific RNG
julia> ts[1,2] # Will be a random Float64
julia> rand!(ts, 1:10); # Fill with random integers from 1 to 10
julia> ts[2,2] # Will be a random integer between 1 and 10