Defining a rank-N tensor

TL;DR: I have three main questions:

  1. How do I define a struct type such that it is limited to one of a few select options?
  2. How do I define a struct type such that it can only take integer values?
  3. How do I programmatically define a static rank-N tensor of specified proportions?

The longer version:

I am struggling in defining a struct which contains a member variable, m, which is a rank-N tensor (typeof(N) <: Integer) of a fixed size, L where typeof(L) <: Integer, for all dimensions which contains data of type T (limited to Float32/Float64/ComplexF32/ComplexF64), i.e. for a placeholding value X::T

  • N = 0 results in m = X (scalar)
  • N = 1, L = 3 results in m = [X, X, X] (vector)
  • N = 2, L = 2 results in m = [[X, X], [X, X]] (matrix)
    etc.

In order to achieve this, I must:

  1. Limit the data type T to one of a select few
  2. Define N and L within the struct types such that they can only take integer values (if this is even possible)
  3. Programmatically define a rank-N tensor

My thought processes so far:

  1. I initially attempted to harness the functionality and efficiency provided by StaticArrays.jl through e.g.
    SVector{L, T}(fill(X, L))
    SMatrix{L, L, T}(fill(X, (L, L))),
    however, these are respectively specific to N = 1 and N = 2, so this is clearly not generalised to N dimensions.
  2. I found that generic SArrays can be generalised to N dimensions, e.g.
    SArray{Tuple{L}, T}(fill(X, L))
    SArray{Tuple{L, L}, T}(fill(X, (L, L))),
    However, this still requires specification with the correct number of arguments.
  3. My guess in resolving this shortcoming would be to utilise the repeat() function somehow, however I am not seeing how to do so.

My usage of structs in julia is pretty rudimentary, but ultimately I assume the struct would look akin to:

struct My_Struct{T, N, L} where T \in [Float32, Float64, ComplexF32, ComplexF64], typeof(N) <: Integer, typeof(L) <: Integer
    m::SArray{Tuple{repeat([X], L)}, T}
end

Any help would be greatly appreciated!

I think the essence of what you want can be done like this:

using StaticArrays

struct MyStruct{T, N, L}
    m::SArray{NTuple{N, L}, T}
end


# Test
data = reshape(1:8, 2, 2, 2) .|> Float64
sarray =  SArray{NTuple{3,2}}(data)

julia> my_struct = MyStruct(sarray)
MyStruct{Float64, 3, 2}([1.0 3.0; 2.0 4.0]

[5.0 7.0; 6.0 8.0])

julia> typeof(my_struct.m)
SArray{Tuple{2, 2, 2}, Float64, 3, 8}

NTuple{N,L} does what you where getting at with repeat.

You can easily add the constraint on T with Union:

struct MyStruct2{T<:Union{AbstractFloat, ComplexF32, ComplexF64}, N, L} 
    m::SArray{NTuple{N, L}, T}
end

though it makes it less flexible (and less readable in my opinion) so I’m not sure the constraint is a good idea.

I don’t think you can add “integer” constraints on the L and N parameters (see discussion at Non-type as type parameters · Issue #9580 · JuliaLang/julia · GitHub).

3 Likes

The idiom for that is a guarded inner constructor:

struct MyStruct{T, N, L}
    m::Array{T}
    
    function MyStruct{T, N, L}(m) where {T,N,L}
        @assert N isa Integer
        @assert L isa Integer
        new{T, N, L}(m)
    end
end

julia> MyStruct{Float64, 2, 3}(rand(3,3))
...

julia> MyStruct{Float64, 2.0, 3}(rand(3,3))
ERROR: AssertionError: N isa Integer
3 Likes

@sudete Many thanks, that appears to be what I am looking for, and at least I now better understand how Union can be used! Regarding flexibility, I don’t envisage requiring further options than those specified in my OP, but maybe those are famous last words :slight_smile:

@thisrod Is there a way to non-explicitly implement those assertions into every constructor (I will be creating various constructors as I suspect users will desire the ability to fall back on default values), or is it a case of concision vs. rigour?

1 Like

You would normally define the convenience methods as outer constructors, so this happens automatically. The outer constructors are ordinary Julia methods, which call a privileged inner constructor method, which does the checks and creates the MyStruct.

Every value of type MyStruct was created when an inner constructor returned it. If you explicitly define an inner constructor in the type definition, Julia doesn’t define any others, and there are no MyStructs except for the ones returned by your constructor. If you don’t, Julia defines the obvious inner constructors by default.

1 Like