Convert or Reinterpret Between NamedTuple and SVector

My question is related to Vector to NamedTuple?

Assume my NamedTuple contains only floating point values T and SVector{N, T} entries, e.g.,

using StaticArrays 
nt = ( a = rand(), b = SA[1.0, 2.0 ,3.0] )

Taking such a NamedTuple and interpreting it as an SVector is straightforward

v = reinterpret(SVector{4, Float64}, nt)

I can write a @generated function that does the reverse, but it is awful (see below) and I just wondered whether is a more straightforward way to achieve this?

@generated function _svec2nt(v::SVector, x::NamedTuple) 
   SYMS = fieldnames(x)
   TT = fieldtypes(x)

   _len(::Type{T}) where {T <: Number} = 1
   _len(::Type{SVector{N, T}}) where {N, T <: Number} = N

   i0 = Int[] 
   idx = 1 
   for T in TT
      push!(i0, idx) 
      idx += _len(T)
   end
   push!(i0, idx) 

   # indexing into v::SVector 
   inds = [] 
   for i = 1:length(TT) 
      rg = i0[i]:i0[i+1]-1
      if length(rg) == 1 
         push!(inds, "$(first(rg))")
      else 
         rg = SVector(rg...)
         push!(inds, "SA$rg")
      end
   end

   code = "(; "
   for (sym, ind) in zip(SYMS, inds)
      code *= "$sym = v[$ind], "
   end 
   code *= ")"

   return quote 
      $(Meta.parse(code))
   end
end 

Rather than crafting a statement for parsing, you can use a NamedTuple constructor directly, just find the segments of the vector

julia> NamedTuple{fieldnames(typeof(nt)), Tuple{fieldtypes(typeof(nt))...}}((v[1], v[2:4]))
(a = 0.31259929654404184, b = [1.0, 2.0, 3.0])

or even just

julia> typeof(nt)((v[1], v[2:4]))

But you need a small program to find the indices 1 and 2:4.

That doesn’t sound conceptually easier or clearer. I was hoping some trick that exploits how these objects just occupy the same memory.

If you want, you can again use reinterpret:

julia> nt = ( a = rand(), b = SA[1.0, 2.0 ,3.0] )
(a = 0.4808058471551182, b = [1.0, 2.0, 3.0])

julia> v = reinterpret(SVector{4, Float64}, nt)
4-element SVector{4, Float64} with indices SOneTo(4):
 0.4808058471551182
 1.0
 2.0
 3.0

julia> reinterpret(typeof(nt), Tuple(v))
(a = 0.4808058471551182, b = [1.0, 2.0, 3.0])

For some reason, one has to use Tuple(v) instead of v.

2 Likes

That’s what I was missing. Thanks!

reinterpret(T::DataType, A::AbstractArray) tries to lazily reinterpret an array as an array of a different element type, rather than an instance of said type.

julia> nt2 = reinterpret(typeof(nt), v); # can't be printed

julia> dump(nt2)
Base.ReinterpretArray{@NamedTuple{a::Float64, b::SVector{3, Float64}}, 1, Float64, SVector{4, Float64}, false}
  parent: SVector{4, Float64}
    data: NTuple{4, Float64}
      1: Float64 0.08744277749612084
      2: Float64 1.0
      3: Float64 2.0
      4: Float64 3.0
  readable: Bool true
  writable: Bool true

julia> eltype(nt2)
@NamedTuple{a::Float64, b::SVector{3, Float64}}

If we try to index it though, at some point it tries to convert the reinterpreted axes 1:1 to the type of the parent SVector’s axes SOneTo{4}, which is only compatible with 1:4. Not really sure why that convert happens, maybe fordoes not allow automatic offset indices.

2 Likes

Thanks for explaining, makes sense about SVector being an AbstractArray hence behaves differently.

But if I understand correctly, then reinterpret acting on a bitstype is intended as I’m using it? E.g.

reinterpret(Int, one(Float64))
# 4607182418800017408

reinterpret(Int, zero(Float64))
# 0

also does exactly what I’d expect.

It’s all isbits reinterpretation, it’s just designed to do so across elements of an input AbstractArray. If the axes glitch wasn’t there, indexing the only element of the reinterpreted array would’ve returned the desired NamedTuple. Of course, reinterpreting the same bits as a Tuple is just easier.

1 Like