How can you define arrays of increasing dimension in a composite type when they have a simple form such as the following?
struct compound{N} # N is the number of spatial dimensions of the field
scalar_field :: Array{Float64,N}
vector_field :: Array{Float64,N+1}
tensor_field :: Array{Float64,N+2}
end
But this throws the error
MethodError: no method matching +(::TypeVar, ::Int64)
I guess I could make an Array of Arrays, but I would rather keep them flat like this.
I think you have to settle for three type parameters and an assertion:
struct Compound{N, M, P}
scalar_field::Array{Float64, N}
vector_field::Array{Float64, M}
tensor_field::Array{Float64, P}
function Compound(s, v, t)
# alternatively throw a more appropriate error type like
# a DimensionMismatch or MethodError
@assert ndims(s) == ndims(v)-1 == ndims(t)-2 "error message about dimension"
N = ndims(s)
new{N, N+1, N+2}(s, v, t)
end
end
2 Likes
I don’t know of any better way than parameterizing Compound
with three independent parameters (e.g. N
, N1
, N2
) and enforce the relation between them in an inner constructor.
Something along the lines of:
julia> struct Compound{N, N1, N2}
scalar_field :: Array{Float64,N} # N is the number of spatial dimensions of the field
vector_field :: Array{Float64,N1} # N1 should be N+1
tensor_field :: Array{Float64,N2} # N2 should be N+2
function Compound(N)
new{N, N+1, N+2}()
end
end
julia> Compound(5)
Compound{5,6,7}(#undef, #undef, #undef)
1 Like
Drats! That’s what I’m doing now and it is soooo ugly.
struct Compound{N}
scalar_field :: Array{Float64,N}
vector_field :: Array{SVector{1,Float},N}
tensor_field :: Array{SVector{2,Float},N}
end
Maybe ?
Yeah, the is the array of arrays that I was considering above. Is there a performance hit for nesting arrays like this?
You could try with meta programming
abstract type AbstractCompound{N} end
for N = 1:4
name = Symbol("Compound$N")
@eval begin
struct $name <: AbstractCompound{$(N)} # N is the number of spatial dimensions of the field
scalar_field :: Array{Float64,$(N)}
vector_field :: Array{Float64,$(N+1)}
tensor_field :: Array{Float64,$(N+2)}
end
end
end
julia> c = Compound1(zeros(1),zeros(1,1),zeros(1,1,1))
Compound1([0.0], [0.0], [0.0])
julia> c.tensor_field
1Ă—1Ă—1 Array{Float64, 3}:
[:, :, 1] =
0.0
1 Like
Oops ! Too fast reading (although not fast enough to provide the first solution )
I think that you can reshape your arrays as flat arrays.
If you want to minimize the performance risks maybe you may adopt a SoA strategy (but the client code is likely to be “uglier”)
I am thinking about the best strategy and I confess that at this point nothing’s really convince me…
1 Like
Neat! This would be much nicer when writing thing like foo(a::AbstractCompound{N}) where N
.
Would it be possible to overload the internal constructor? I often have code like b = Compound(a.scalar_field)
and now it looks like I would need to check if I needed Compound2
, Compound3
,…
Thanks. That’s my current approach.
You can solve this problem the same way, with meta programming. The only thing to keep in mind is to interpolate the N
variable into the evaluated expression with $(N)
.
2 Likes
This seems to work well!
abstract type AbstractCompound{N} end
for N = 1:4
name = Symbol("Compound$N")
@eval begin
struct $name <: AbstractCompound{$(N)} # N is the number of spatial dimensions of the field
scalar_field :: Array{Float64,$(N)}
vector_field :: Array{Float64,$(N+1)}
tensor_field :: Array{Float64,$(N+2)}
end
Compound(s::AbstractArray{Float64,$(N)},a...) = $name(s,a...)
end
end
Compound(N) = Compound(rand(N...),zeros(N...,length(N)),ones(N...,length(N),length(N)))
julia> Compound((2,2)).vector_field
2Ă—2Ă—2 Array{Float64,3}:
[:, :, 1] =
0.0 0.0
0.0 0.0
[:, :, 2] =
0.0 0.0
0.0 0.0
2 Likes
I’m glad you found a solution that works for you! I do have some more thoughts reading through all the replies though.
What aspect is ugly? The definition itself, the REPL printout Compound{5, 6, 7}
, or something else? If it is the definition, then of course you’re out of luck with this method. If it’s the printout or having to refer to a type with 3 parameters elsewhere in the code, then there are simple enough remedies.
If the N is necessary for the method, you would still need to write it this way I think. If not, you can omit it either way. Note also that you can write parameterized types with only some of the parameters defined. The parameters are interpreted in order.
julia> AbstractCompound == AbstractCompound{N} where N
true
julia> Compound{1}
Compound{1,M,P} where P where M
julia> Array{Int} # same idea as this
Array{Int64,N} where N
If for whatever reason you need the type to be totally concrete for some usage but don’t want to refer to it with all 3 parameters, you could define something like compoundtype(n) = Compound{n, n+1, n+2}
as sugar for referring to the appropriate type. You could also define aliases like const Compound1 = Compound{1,2,3}
which is similar to what you’re doing now.
Finally, the constructor you defined for Compound(N)
would work (as written) just as well for a type with 3 parameters under the hood.
All of this is not to say that metaprogramming and having multiple types for Compound1, Compound2, etc. is not also a perfectly good way to go!
2 Likes
Nice! I didn’t realize you could drop some of the parameters. That does clean up the usage a good deal.
I still prefer strictly defining n,n+1,n+2
if possible since there really is only one parameter for the type, not three.
Point of fact - I ended up doing this since it was the closest to what I had already, so why reinvent the wheel?
1 Like