Trying to write a convert() for a complicated type

Stepping back into Julia after a year’s absence.

I’m using RigibBodyDynamics to model a double pendulum. Simulating the system should return an array of system states, ideally an Array{Float64} or similar. The type that is actually returned is a

SegmentedVector{JointID, Float64, Base.OneTo{JointID}, Vector{Float64}}

which I’m sure there was a good reason for using, but it can’t just be dropped into functions where one would normally use a Vector{}, such as for instance plot().

So I’m trying to write a convert() that turns it into an actual vector, and I cannot for the life of me get the type matching system to match my function.

What I’ve done is define:

convert(::Vector{Float64}, x::Vector{SegmentedVector{JointID, Float64, Base.OneTo{JointID}, Vector{Float64}}}) = mapreduce(permutedims, vcat, x)

convert(::Float64, x::SegmentedVector{JointID, Float64, Base.OneTo{JointID}, Vector{Float64}}) = x[1][:]

but when I try to force an implicit conversion, it fails:

julia> typeof(qs)
Vector{SegmentedVector{JointID, Float64, OneTo{JointID}, Vector{Float64}}} (alias for Array{SegmentedVector{JointID, Float64, Base.OneTo{JointID}, Array{Float64, 1}}, 1})

julia> typeof(X)
Vector{Float64} (alias for Array{Float64, 1})

julia> append!(X, qs)
ERROR: MethodError: Cannot `convert` an object of type SegmentedVector{JointID, Float64, Base.OneTo{JointID}, Vector{Float64}} to an object of type Float64

Closest candidates are:
  convert(::Type{Float64}, ::Measures.AbsoluteLength)
   @ Measures ~/.julia/packages/Measures/PKOxJ/src/length.jl:12
  convert(::Type{T}, ::T) where T<:Number
   @ Base number.jl:6
  convert(::Type{T}, ::Gray) where T<:Real
   @ ColorTypes ~/.julia/packages/ColorTypes/1dGw6/src/conversions.jl:113
  ...

Interestingly, defining something like

conv(x::Vector{SegmentedVector{JointID, Float64, Base.OneTo{JointID}, Vector{Float64}}}) = mapreduce(permutedims, vcat, x)
conv (generic function with 3 methods)

julia> conv(qs)
15002×2 Matrix{Float64}:
 0.718177  0.310706
 0.716825  0.314462
 0.715472  0.318206
 0.714121  0.321928
 0.712759  0.325665
 0.711398  0.329381
 0.710036  0.333088

ie doing an explicit conversion, works.

What am I doing wrong?

Did you remember to import Base: convert first? Or define it as Base.convert(...) = ...?

I had forgotten to do that, but it doesn’t fix the problem unfortunately.

Here’s the complete code. I apologize that it’s a little messy.

  using RigidBodyDynamics
  using LinearAlgebra
  using StaticArrays
  using MatrixEquations
  
  import Base: convert, OneTo

  convert(::Array{Float64}, x::Vector{SegmentedVector{JointID, Float64, OneTo{JointID}, Vector{Float64}}}) = mapreduce(permutedims, vcat, x)
  convert(::Float64, x::SegmentedVector{JointID, Float64, OneTo{JointID}, Vector{Float64}}) = x[1][:]
  convert(::Float64, x::Vector{SegmentedVector{JointID, Float64, OneTo{JointID}, Vector{Float64}}}) = mapreduce(permutedims, vcat, x)
  
  # The code below creates a pendulum mechanism
  g = -9.81
  world = RigidBody{Float64}("world")
  doublependulum = Mechanism(world; gravity = SVector(0, 0, g))
  
  axis = SVector(0., 1., 0.)
  I_1 = 0.333
  c_1 = -0.5
  m_1 = 1.
  frame1 = CartesianFrame3D("upper_link")
  inertia1 = SpatialInertia(frame1,
      moment = I_1*axis*axis',
      com = SVector(0, 0, c_1),
      mass=m_1)
  
  upperlink = RigidBody(inertia1)
  shoulder = Joint("shoulder", Revolute(axis))
  before_shoulder_to_world = one(Transform3D, frame_before(shoulder), default_frame(world))
  
  attach!(doublependulum, world, upperlink, shoulder, joint_pose = before_shoulder_to_world)
  # The mechanism is now defined
 
  state = MechanismState(doublependulum)
  set_configuration!(state, shoulder, 0.3)
  set_velocity!(state, shoulder, 1.)
  
  X = Vector{Float64}()
  
  for i in 1:1000
      ts, qs, vs = simulate(state, 15., Δt = 1e-3);
      append!(X, qs)
  end
convert(::Float64, x::Vector{SegmentedVector{JointID, Float64, OneTo{JointID}, Vector{Float64}}}) = mapreduce(permutedims, vcat, x)

what is this actually supposed to do? it appears to return a Matrix{Float64} but I imagine you wanted Float64 ?

Sigh. I’m having a bit of trouble making sense of the errors Julia is giving me. What I want to do is append!(X, qs) and have the language do the right implicit conversion to allow that call to succeed. I would have thought that this would first entail converting qs to a Vector{Float64}, and then executing the append!.

But the actual error I’m getting is that Julia doesn’t know how to convert a SegmentedVector to a Float64. NOT a Vector{Float64}:

ERROR: MethodError: Cannot `convert` an object of type SegmentedVector{JointID, Float64, Base.OneTo{JointID}, Vector{Float64}} to an object of type Float64

…which means that, for reasons I can’t understand, Julia thinks it needs to convert qs to a Float64 in order to append! it to X (which is of type Vector{Float64}). Or at least that’s how I’m reading it. I really don’t understand that.

But, to figure out a solution, I’ve tried to throw the kitchen sink at the problem. I’ve attempted to write convert() functions that do what I think is the right thing – converting a SegmentedVector to a Vector{Float64} – but also defining conversions from a Vector{SegmentedVector} to a Vector{Float64}, and (attempting to address the actual error I’m seeing) converting a SegmentedVector to a Float64. Or at least to get the type dispatch to call that routine.

Note that a SegmentedVector is not a scalar, so it can’t really be converted to one. I know that code won’t work, but I can’t even get Julia to recognize the correct function prototype.

is it important to you that you specifically use convert for this purpose? e.g. the call succeeds if you change it to append!(X, reduce(vcat, qs)) without defining any additional convert dispatches

Not particularly, I just thought it would be the most elegant solution.

If you actually want to plot it (using Plots.jl), the best way is to write a @recipe. For your type it looks like that should be

@recipe function f(::Type{SegmentedVector}, x::SegmentedVector)
    return x[1][:]
end
1 Like

This is type piracy, because you own neither Base.convert nor RigidBodyDynamics.SegmentedVector. The only time you should define a convert method is if you’ve created your own type and you want your type to be implicitly converted to another type inside constructors, push!, and append!.

SegmentedVector is an AbstractVector, so you should be able to treat it like any other vector you’ve worked with in the past.

1 Like

A call to convert passes a type (not an instance of that type) as the first argument, e.g. convert(Float64, x). When defining a convert method you should specify the first argument as accepting a type and not an instance the type:

convert(::Type{Float64}, x::SegmentedVector{JointID, Float64, OneTo{JointID}, Vector{Float64}}) = x[1][:]

See:
https://docs.julialang.org/en/v1/manual/conversion-and-promotion/#Defining-New-Conversions

1 Like

This is fantastic. I did not know about type piracy, but when pointed out it makes sense. I’ll do this as suggested. And thanks for the explanations about plots recipes (which I had never heard of) and how convert works. This community is one of the things that makes Julia great.

2 Likes