Closed GearsAD closed 3 years ago
Just for reference: https://docs.julialang.org/en/v1/manual/types/#man-singleton-types-1
Singleton types can work with current Pose2 for example:
struct Pose2 <: IncrementalInference.InferenceVariable
dims::Int
manifolds::Tuple{Symbol,Symbol,Symbol}
Pose2() = new(3, (:Euclid, :Euclid, :Circular))
end
as
struct Pose2 end
getdims(::Pose2) = 3
getmanifolds(::Pose2) = (:Euclid, :Euclid, :Circular)
Also see JuliaRobotics/RoME.jl#244 on standardizing with Manifolds.jl.
Copy from Slack, @GearsAD asked if this was workable:
# TODO: Confirm that we can switch this out, instead of retrieving the complete variable.
# Original getSofttype(getVariable(dfg,lbl), solvekey)
getSofttype(v::DFGVariable) = typeof(v).parameters[1]()
FIY, I don't know if the introspection is the best way in: getSofttype(v::DFGVariable) = typeof(v).parameters[1]()
rather use: getSofttype(::DFGVariable{T}) where T = T()
Edit: added the brackets, the 2 functions are equivalent. This is just to show a cleaner way to write the same function.
DF edit -- agree it's cleaner, sorry I misunderstood how T
was being used.
For reference:
https://github.com/JuliaRobotics/DistributedFactorGraphs.jl/blob/cdcc0dd0a77fc22f163aefb61db3b105a09746cf/src/entities/DFGVariable.jl#L28
I wanted the same concept in the summary graphs and basically did it with a warning: https://github.com/JuliaRobotics/DistributedFactorGraphs.jl/blob/6b903b79b15c6754191cfb2b673dd89d1eb97e2c/src/services/DFGVariable.jl#L293-L296
also see: https://github.com/JuliaRobotics/DistributedFactorGraphs.jl/issues/237
rather use dispatch: getSofttype(::DFGVariable{T}) where T = T
that would be a hard type -- that's an even bigger discussion. Going softtype allows a bit more freedom and does not require enumeration of all the factor permutations.
EDIT see edit above: https://github.com/JuliaRobotics/IncrementalInference.jl/issues/783#issuecomment-660207623
a related issue is how to make it easy for the user to add their own data types to the default factors. Softtype with state solved that problem, but making it a manifest or hardtype removes that option.
This issue is closely related to the possible future use of a 'manifold type'. I don't quite understand the whole hard-type soft-type thing yet, but that's beside the point. I can see how ContinuousMultivariate
can maybe be referred to as a soft-type. The other types are closely related to traits of the value array (it tells the algorithm how to handle the values).
ContinuousMultivariate: as Sam suggested, it can be solved by having users just define one for their application. ContinuousMultivariate4 or ContinuousMultivariate5, for example.
Additional fields: I would guess the possible use of a manifold type would also require a split in the data with something like the field ut::Int64
a bit of a grey area. This would probably require another structure specifically for this kind of data. like metasolverdata
So is the core of the issue:
some options off the top of my head:
In the visualization use case, I needed the type to visualize it. There are only so many options that can currently be visualised. I only wanted Gaussian with a point, so PPE, but its useless without the softtype to tell you what is going on. With "summary graph", softtypename
was enough for the basic types such as Pose2 and Point2 (The singleton types). The rest is user-specific and hard (impossible) to make a generic visualiser for.
that would be a hard type -- that's an even bigger discussion. Going softtype allows a bit more freedom and does not require enumeration of all the factor permutations.
See edit above in the comment; to make reason a bit clearer and fix brackets.
@dehann, I don't know if I understand your comment correctly, maybe elaborate if it's still applicable after the edit?
@GearsAD if there are not too many, perhaps this idea could work for the short term. The union is not the best and just to illustrate the idea.
getSofttype(::DFGVariable{T}) where T <: Union{"all the singleton types"} = T()
getSofttype(::DFGVariable{T}) where T <: Union{"all the rest"} = ...as it was before...
Hi, trying to simplify and keep short issue -- see summary in first comment edit: https://github.com/JuliaRobotics/IncrementalInference.jl/issues/783#issue-659285005
I don't know if I understand your comment correctly, maybe elaborate if it's still applicable after the edit?
If I understood you right, here is my comment update: https://github.com/JuliaRobotics/IncrementalInference.jl/issues/783#issuecomment-660207623
How do we add user data to VariableNodeData to be used in the solver? Do we split out the user solver data?
I added my vote in first comment, and edited the Johan's suggestion above to add my support for which way forward.
Singleton types can work with current Pose2 for example:
struct Pose2 <: IncrementalInference.InferenceVariable dims::Int manifolds::Tuple{Symbol,Symbol,Symbol} Pose2() = new(3, (:Euclid, :Euclid, :Circular)) end
as
struct Pose2 end getdims(::Pose2) = 3 getmanifolds(::Pose2) = (:Euclid, :Euclid, :Circular)
Right, so this is a more explicit version of the same idea, although in this case we'd be using the singleton form of Pose2? That sounds like a reasonable way to enforce that it doesn't retain state.
As in the edits in the initial description:
it may be slow (is it really though?)
if we use the inheritance approach Johan suggests then we force the user to keep type safety (if I'm understanding the use right). I was trying to do the same thing by using softtype originally, but now we need to do more things with it -- so this is a feature expansion process actually.
type stability is a major performance thing, so we do need a reasonable approach to keeping that for normal use. An intermediate user should be able to augment their own data (like the radar images) and still have type safety. I think if a new user is just trying something, then combo of slow performance and documentation should help them advance towards an intermediate user (i.e. using user type for cached data).
I'm just thinking we may want to serialize and save it somewhere
That goes back to smalldata
. How much should we force type stability in small data?
Currently we're proposing smalldata is like Union{Int, Float64, String, Vector{Int}, Vector{Float64, Vector{String}}. I think that should be a reasonable amount of flexibility and won't hammer performance?
Oh, that will hammer performance unfortunately, the object containing a field ::Union{...}
is the potential problem there (85% sure about that). So either we should think of a way to harden smalldata, or as Johan suggests introduce yet another field "metadata". I'm not too hot on the additional field thing, personally I'd rather just limit the user options on smalldata. However, I think @Affie has something clever up his sleeve to use inheritance with "traits" to build 3rd option here that does a good enough job on both concerns (type safety and flexibility, but usage is one up from novice).
EDIT: functions dispatching on a parameter ::Union{..}
or ::Abstract
is fine, but challenge comes in with ::Union
or ::Abstract
type fields -- the latter uses "dynamic dispatch" to interpret on the fly how to work with the data. The earlier case of dispatch on functions can preemptively do type-inference and then statically compile a hardtype function.
Currently we're proposing smalldata is like Union{Int, Float64, String, Vector{Int}, Vector{Float64, Vector{String}}. I think that should be a reasonable amount of flexibility and won't hammer performance?
Slightly off topic:
This will perform way better than a "type stable" string that has to be parsed every time.
If you limit the instability with other techniques such as adding type annotations and/or breaking up functions, it may be a good trade-off.
For example:
julia> d = Dict{Symbol, Any}();
julia> push!(d, :a=>5);
julia> push!(d, :b=>5.0)
Dict{Symbol,Any} with 2 entries:
:a => 5
:b => 5.0
julia> function sumD(d)
return d[:a]::Int + d[:b]::Float64
end
sumD (generic function with 1 method)
julia> @code_warntype sumD(d)
Variables
#self#::Core.Compiler.Const(sumD, false)
d::Dict{Symbol,Any}
Body::Float64
1 ─ %1 = Base.getindex(d, :a)::Any
│ %2 = Core.typeassert(%1, Main.Int)::Int64
│ %3 = Base.getindex(d, :b)::Any
│ %4 = Core.typeassert(%3, Main.Float64)::Float64
│ %5 = (%2 + %4)::Float64
└── return %5
Right, so this is a more explicit version of the same idea, although in this case we'd be using the singleton form of Pose2? That sounds like a reasonable way to enforce that it doesn't retain state.
The reason to rather use a singleton is it can only have one instance. eg
julia> Pose2() === Pose2()
true
I think the Singleton model from @Affie's suggestion would enforce the stateless structure, I'm just thinking we may want to serialize and save it somewhere. If we go with that, it's hardcoded so we can't do that.
Currently, Pose2
requires RoME to use, so I don't think it would make a difference. It makes it easier as you can just serialize the name, ie "Pose2" and not any fields. So getSofttype(::DFGVariable{T}) where T = T()
will still work.
Breadcrumb for function name: issingletontype
@dehann, is this the way it's added to factors:
So variableuserdata = softtype
And small data also? although I can't see where its copied in.
And related to performance #383 and the "Vector of abstract".
We can make it more type-stable by variableuserdata::Vector{T} where T<: InferenceVariable
or something like that.
Perhaps we can split it completely as a new abstract type? (But you know me, I like abstracting everything)
struct MyTimeVaribleUserData <: AbstractVariableUserSolverData
utime::Int
end
#and have
variableuserdata::Vector{T} where T<: AbstractVariableUserSolverData
If the options are a Dictionary (like small data) or a Struct, my vote is for struct since this will be called a lot and we want it in the stack.
Perhaps, we can copy the user data to a NamedTuple
(if it's immutable) for use in the calculations
PS. I'm just writing a bunch of options as a brainstorming exercise when I think of them.
@dehann, why is utime
not a measurement in the factor?
So I think in an ideal world we should remove FactorMetadata
and head in this direction:
function (vp2vp2::VelPoint2VelPoint2{D})(
res::Array{Float64},
variables::Vector{DFGVariable},
idx::Int,
meas::Tuple,
Xi::Array{Float64,2},
Xj::Array{Float64,2} ) where D
#
z = meas[1][:,idx]
xi, xj = Xi[:,idx], Xj[:,idx]
# change in time from microseconds with DynPoint2(ut=1_000_000) to seconds
dt = (variables[2].timestamp - variables[1].timestamp)*1e-6 # roughly the intended use of userdata
...
DF EDIT, FactorMetadata
has to remain for other reasons. This problem has been resolved by changing from softtype
to a hard type VariableType
From the user perspective (DFG and IIF will have to do this internally somehow)
struct FactorMetadata_NEW{USER}
cacheData::Vector{USER}
fullVariable::Vector{DFGVariable}
variablelist::Vector{Symbol} # obsolete
solvefor::Symbol
factoruserdata
dbg::Bool
end
FactorMetadata_NEW(x...;kw...) = FactorMetadata_NEW{Nothing}(nothing, x...;kw...)
# so naive replacement for timestamp is to use
function (::AwesomeFactor)(..., fmd::FactorMetadata, ...)
t1 = getTimestamp(fmd.fullVariable[1])
t2 = getTimestamp(fmd.fullVariable[2])
# do math
end
# so intermediate user replacement for timestamp is to use
struct MyDynPoseCache
timestamp::Int64
end
## not callback, rather dispatch
# this must happen either before each solve or before each convolution
# default in IIF
# based on USER, cacheData::Vector{USER}
prepCache(::Type{Nothing}, ::DFGVariable) = nothing
# user must do this, and IIF will call as standard procedure before solving
import IncrementalInference: prepCache # need better name
prepCache(::Type{MyDynPoseCache}, ::DFGVariable) = getTimestamp(fullVariable)
function (::AwesomeFactor)(..., fmd::FactorMetadata, ...)
t1 = fmd.cacheData[1].timestamp
t2 = fmd.cacheData[2].timestamp
# do math
end
## What the heck, let me do it myself...
struct MyDynPose2Factor
# .. do it all inthe factor
end
EDIT, DF I missed this before, sorry:
So I think in an ideal world we should remove FactorMetadata and head in this direction:
We do need other information like which variable is being solved for etc. I think we have a workable solution now, where the variables are included in the FactorMetadata{T} structure, alonside a Templated type T for user upgrade option for high performance hard-type approach -- i.e. an optimization after getting it working with direct hack on some variable accessor (possibly even data blob retrieval in the computation hot-loop ... yucky but helpful for quick testing). The optimization using T would allow the user to cache say a blob retrieval once, and then be fast.
Ground rule though, the factor operations on variable metadata should be totally immutable. The only way for the hot-loop to update the variables is to modify the belief numerical parameters passed in, i.e.
meas::Tuple,
Xi::Array{Float64,2},
Xj::Array{Float64,2}
...
the prepCache
function (we still need a better name for it) will likely be called around here (im like 70% sure):
see FactorMetadata
: https://github.com/JuliaRobotics/IncrementalInference.jl/issues/783#issuecomment-661899977
Is it ok to have continuous scalar only represent Euclid, ie ContinuousScalar() = new(1, (:Euclid,))
struct ContinuousScalar <: InferenceVariable
dims::Int
manifolds::Tuple{Symbol}
ContinuousScalar(;manifolds::Tuple{Symbol}=(:Euclid,)) = new(1, manifolds)
end
Since we have Sphere1 for (:Circular,)
struct Sphere1 <: InferenceVariable
dims::Int
manifolds::Tuple{Symbol}
Sphere1() = new(1, (:Circular,))
end
We can also do ContinuousMultivariate as all Euclidian
julia> struct ContinuousMultivariateEuclid{N} <: InferenceVariable end
julia> function Base.getproperty(::ContinuousMultivariateEuclid{N}, f::Symbol) where N
if f == :dims
return N
elseif f == :manifolds
return ntuple(i -> :Euclid, N)
end
end
julia> a = ContinuousMultivariateEuclid{4}()
julia> a.dims
4
julia> a.manifolds
(:Euclid, :Euclid, :Euclid, :Euclid)
Hi, yes I think thats fine thanks -- we can later add things like ContinuousCircular
?
i like the ::ContinuousMultivariateEuclid{N}
since that is similar to ::NTuple{N}
and ::Array{N}
Oh, just a correction :Euclid
not :euclid
. This will all become part of the JuliaRobotics/RoME.jl#244 Manifold.jl consolidation story.
I wrote down a trait-based example we can look at using the softtypes for other things such as visualization.
abstract type VisualizerTrait end
struct SE{N} <: VisualizerTrait end
struct Cartesian{N} <: VisualizerTrait end
VisualizerTrait(::T) where T = error("Type $T doesn't implement the VisualizerTrait")
VisualizerTrait(::Pose2) = SE{2}()
VisualizerTrait(::Pose3) = SE{3}()
VisualizerTrait(::Point2) = Cartesian{2}()
VisualizerTrait(::Point3) = Cartesian{3}()
visualize(x) = visualize(VisualizerTrait(x), x)
visualize(::SE{N}, x) where N = @info "Visualizing SE$N..."
visualize(::Cartesian{N}, x) where N = @info "Visualizing Cartesian $N..."
Xs = [Pose2(), Pose3(), Point2(), Point3()]
julia> visualize.(Xs)
[ Info: Visualizing SE2...
[ Info: Visualizing SE3...
[ Info: Visualizing Cartesian 2...
[ Info: Visualizing Cartesian 3...
i like those traits!
Added advantage, micro performance improvement. It might be negligible but this way the constant propagation can be used:
#the 2 versions of:
@code_lowered getDims(p3)
# old
CodeInfo(
1 ─ %1 = Base.getproperty(p, :dims)
└── return %1
)
# new
CodeInfo(
1 ─ return 6
)
nice!
Decision for #784 made, consolidating work effort in this issue.
Partially addressed by #823 - with full variables. Deprecation stared to change softtypes to singletons
see JuliaRobotics/DistributedFactorGraphs.jl#603
Stared the deprecation process in #841.
Too big a change to also include in v0.17.0, punting down one to v0.18.0 since this will require a breaking change upstream from DFG v0.11.0 which is not available yet.
xref #825
Issue Summary
History:
variableType
was added to DFGVariable DataLevel2 to solve two problems, then DFGVariableSummary (DataLevel1) came along and is exposing two competing needs which are being wrestled out assofttype
softtypename
.Requirements
FactorMetadata
that exposes the softtype and other information to the solver.Smalldata
is an ongoing discussion for which the design is still in flux, and might induce serious performance problems owing to risk of user adding Unstable Types into the deep-inner-computation-loops.DynPose2
need to store microsecond time to allow velocity calculation, which was only added as full timestamp much later -- however, the situation can repeat with some other user data that cannot be designed in advance.Related Issues
softtypename
: JuliaRobotics/DistributedFactorGraphs.jl#237825
Plan of Action
Can split these into more issues once a design decision is made.
softtype::T
should have stateful information or where to offload that requirement?use "inheritance"/"interface"/"traits" from a singleton type like Pose2 that you "inherit" and add own data to
getSofttype
should work.FactorMetadata
softtype
to onlyVariableType
see JuliaRobotics/DistributedFactorGraphs.jl#603Original Question
We need to fix all softtypes to have a default constructors, and take out all state information.
Types with issues:
DF EDIT, a related issue is how to make it easy for the user to add their own data types to the default factors. Softtype with state solved that problem, but making it a manifest or hardtype removes that option. SC: I suggest we add smallData to factors.