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

FeatureDescription
SymmetricTensor{T, N, dim}Core symmetric tensor type
find_symmetric_tensor_sizeNumber of stored unique elements
find_degeneracyPermutation multiplicity tensor
find_full_indicesList of canonical index tuples
zeros, ones, rand, similarTensor constructors
getindex, setindex!Symmetric indexing and mutation

Full API Reference

For a complete overview of all exported functions and types:

PermutationSymmetricTensors.PermutationSymmetricTensorsModule

This 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.
source
PermutationSymmetricTensors.SymmetricTensorType

SymmetricTensor{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 by find_symmetric_tensor_size(N, dim).
  • linear_indices::Vector{Vector{Int64}}: Precomputed indices to map sorted Cartesian indices to the linear index in the data vector. This is an internal field used for efficient indexing.
source
PermutationSymmetricTensors.SymmetricTensorMethod

SymmetricTensor(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))
source
Base.getindexMethod

getindex(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 of dim 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.

source
Base.setindex!Method

setindex!(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 of dim 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.

source
PermutationSymmetricTensors.check_correct_sizeMethod

check_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 (typically length(data)).
  • N::Int: The size of each dimension.
  • dim::Int: The number of dimensions.

Returns

  • Bool: true if num_elements matches find_symmetric_tensor_size(N, dim), false otherwise.
source
PermutationSymmetricTensors.find_N_repetitions_sorted!Method

find_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 least length(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
source
PermutationSymmetricTensors.find_degeneracyMethod
function 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 existing SymmetricTensor instance from which N and dim can be derived.
  • full_indices::Vector{<:NTuple{dim, Any}}: (Optional) The output of find_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
source
PermutationSymmetricTensors.find_full_indicesMethod

function 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
source
PermutationSymmetricTensors.find_linear_index_arrayMethod

find_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
source
PermutationSymmetricTensors.find_linear_indicesMethod

find_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}: A Val instance representing the size of each dimension.
  • ::Val{dim}: A Val instance representing the number of dimensions.

Returns

  • Vector{Vector{Int64}}: A vector where each inner vector is the result of find_linear_index_array(N, Val(k)) for k in 1:dim.
source
PermutationSymmetricTensors.find_symmetric_tensor_sizeMethod

function 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

source
Random.rand!Method

rand!(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 like 0: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 tensor A, filled with random values. (Note: rand! traditionally returns the modified array, but the current implementation returns nothing. This docstring reflects the traditional behavior for consistency with Base.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
source