-
Notifications
You must be signed in to change notification settings - Fork 105
Make member variables of LowPassFilter class generic #351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: ros2-master
Are you sure you want to change the base?
Make member variables of LowPassFilter class generic #351
Conversation
@pedroazeredo04, all pull requests must be targeted towards the |
Signed-off-by: Pedro Nogueira <[email protected]>
This pull request is in conflict. Could you fix it @pedroazeredo04? |
e817778
to
d21cfcc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pedroazeredo04 can you fix the pre-commit?. Just install pre-commit and run pre-commit run --all-files
|
||
// Define the storage type based on T | ||
using StorageType = typename std::conditional< | ||
std::is_same<T, geometry_msgs::msg::WrenchStamped>::value, Eigen::Matrix<double, 6, 1>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
std::is_same<T, geometry_msgs::msg::WrenchStamped>::value, Eigen::Matrix<double, 6, 1>, | |
std::is_same<T, geometry_msgs::msg::WrenchStamped>::value, Eigen::Vector6d, |
May be we could use this directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might come from
https://github.com/ros/geometry/blob/noetic-devel/eigen_conversions/include/eigen_conversions/eigen_msg.h#L100
according to the comment
// TODO(destogl): use wrenchMsgToEigen
But I don't find these conversion wrappers for ROS 2, https://docs.ros.org/en/rolling/p/tf2_eigen/generated/index.html#tf2-eigen
Thanks a lot for your contribution.
You are right about that, I made a proposal in the issue: what do you think? I still think that the algorithms in the filters would be more clean, and we easily could apply that to all the other filters and add for example Pose messages (in a future PR). |
@christophfroehlich That sounds nice! I didn't know we could use this logic in other filters. I will leave this implementation for a future PR, but in which filters could we also use this? Also, do you think I should create a new file (for instance |
Signed-off-by: Pedro Nogueira <[email protected]>
for now, the exponential filter would be the only other candidate. But more might come in the future.
yes, exactly. |
Signed-off-by: Pedro Nogueira <[email protected]>
@christophfroehlich I commited your suggestion of having a That is because the override of this function for the type if (filtered_value_.empty())
{
if (std::any_of(data_in.begin(), data_in.end(), [](double val) { return !std::isfinite(val); }))
{
return false;
}
filtered_value_ = data_in;
filtered_old_value_ = data_in;
old_value_ = data_in;
}
else
{
assert(
data_in.size() == filtered_value_.size() &&
"Internal data and the data_in doesn't hold the same size");
assert(data_out.size() == data_in.size() && "data_in and data_out doesn't hold same size");
} is very distinct from the other overrides, so it is hard to generalize this for every data type. Also, about the other methods of the
I would only have to say , in order to do an operator overload for // undefined behavior
template<typename T>
std::vector<T> operator+(const std::vector<T>& lhs, const std::vector<T>& rhs) {
std::vector<T> result(lhs.size());
for (size_t i = 0; i < lhs.size(); ++i) {
result[i] = lhs[i] + rhs[i];
}
return result;
} We would probably have to do // Wrapper around std::vector<double> to enable element-wise + and * without touching std::
struct VecD {
std::vector<double> v;
VecD() = default;
VecD(const std::vector<double>& data) : v(data) {}
bool empty() const { return v.empty(); }
void resize(size_t n) { v.resize(n); }
double& operator[](size_t i) { return v[i]; }
const double& operator[](size_t i) const { return v[i]; }
};
// element-wise add
inline VecD operator+(const VecD& a, const VecD& b) {
VecD out(a.v.size());
for (size_t i = 0; i < a.v.size(); ++i)
out.v[i] = a.v[i] + b.v[i];
return out;
} |
Thanks for your work on this. My suggestion was only a draft, I'm not really experienced in writing templated classes, so I might be running in a dead end. Please shout out if this is the case ;)
The if branch could be implemented with the already mentionend isempty(), isnan(), and the else branch could be solved by a // helper: default false
template<typename T>
struct is_std_vector : std::false_type {};
// specialization for std::vector
template<typename U, typename Alloc>
struct is_std_vector<std::vector<U, Alloc>> : std::true_type {}; and use
This is a very good hint (haven't thought about that), but we aren't overloading this to |
Signed-off-by: Pedro Nogueira <[email protected]>
Hi @christophfroehlich ! No need to thank my work on this, I'm having fun messing around with templates :) I pushed a whip code that tries to make the update() function trully generic.
That is a good idea! I did something similar, but checking for the types inside of the function itself.
In my understanding, the problem is that filtered_value_ = old_value_ * b1_ + filtered_old_value_ * a1_;
filtered_old_value_ = filtered_value_; work for every possible datatype, I had to add in a |
Signed-off-by: Pedro Nogueira <[email protected]>
Signed-off-by: Pedro Nogueira <[email protected]>
Signed-off-by: Pedro Nogueira <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the direction of the simplification of the filter itself. The filter_traits look a bit verbose, but the API is clean and understandable IMHO.
The tests are failing now, apart from that only some minor comments from my side are left.
data_out[i] = b1_ * old_value[i] + a1_ * filtered_old_value[i]; | ||
filtered_old_value[i] = data_out[i]; | ||
if (std::isfinite(data_in[i])) | ||
if constexpr (std::is_same_v<T, std::vector<double>>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my suggestion of is_std_vector
would have been working with any std::vector<T>
, but not sure if any other datatype than double will ever make sense here 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think I should change that to generalize the type of the vector? If I understood correctly, I believe the previous code only supported std::vector<double
also
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
have you tried my code snippet above? I thought this could work.
we could also make another method for "validate_input" or similar? could be useful for other message types also, like checking if the frame_id the same or whatever.
Signed-off-by: Pedro Nogueira <[email protected]>
Signed-off-by: Pedro Nogueira <[email protected]>
Signed-off-by: Pedro Nogueira <[email protected]>
Thanks for the review @christophfroehlich !! I commited some the small fixes that you suggested, and I will try to fix the failing tests later. |
Signed-off-by: Pedro Nogueira <[email protected]>
Signed-off-by: Pedro Nogueira <[email protected]>
Signed-off-by: Pedro Nogueira <[email protected]>
Signed-off-by: Pedro Nogueira <[email protected]>
Hi @christophfroehlich. I had some time today and I added the generic method |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## ros2-master #351 +/- ##
===============================================
+ Coverage 77.24% 78.17% +0.93%
===============================================
Files 29 30 +1
Lines 1336 1338 +2
Branches 93 89 -4
===============================================
+ Hits 1032 1046 +14
+ Misses 252 245 -7
+ Partials 52 47 -5
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, 2 minor comments
|
||
// specialization for std::vector | ||
template <typename U, typename Alloc> | ||
struct is_std_vector<std::vector<U, Alloc>> : std::true_type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this used now?
}; | ||
|
||
template <> | ||
struct FilterTraits<std::vector<double>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this be generalized for all types inside a std::vector?
Signed-off-by: Pedro Nogueira <[email protected]>
Signed-off-by: Pedro Nogueira <[email protected]>
static void validate_input(const T & data_in, const StorageType & filtered_value, T & data_out) | ||
{ | ||
(void)data_in; | ||
(void)filtered_value; | ||
(void)data_out; // Suppress unused warnings | ||
} | ||
|
||
static void add_metadata(StorageType & storage, const StorageType & data_in) | ||
{ | ||
(void)storage; | ||
(void)data_in; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we make it pure virtual?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can't do virtual templates (I say this based on this discussion here). But the discussion does mention type erasure as an alternative, which might be applicable in our case. What do you think?
template <typename U, typename Alloc> | ||
struct FilterTraits<std::vector<U, Alloc>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we specialising with allocator here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand it correctly this function will only be used by "default" vectors, so I believe we could just drop the Alloc
, what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! You are right. That's why I asked about the alloc template argument
Signed-off-by: Pedro Nogueira <[email protected]>
Sorry @christophfroehlich, I'm not sure what I did but for some reason I dismissed your review 😢 |
This happens if you push a new commit after a review, no worries |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Just a minor comment
|
||
static void initialize(StorageType & storage) | ||
{ | ||
storage.data = std::vector<U>{std::numeric_limits<U>::quiet_NaN()}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking about this one. I'm not sure if we have to initialize it or not, because in the first loop it will have to fill in the data right?. Do we want to have it pre-filled with the data?
I'm just saying because we are not sure about the size at the initialization time
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true, I think it would be better to leave it empty.
In the low_pass_filter.hpp
, we already check for emptiness to check if it is a first call. So it would make sense to leave the std::vector
empty, because, as you said, we don't know its size at the initialization time.
But after your question I thought: does it even make sense to have a configure()
method to make the filter variables be NaN? I think it would be simpler if we removed the configure()
method for all overloads, not only the std::vector
.
And we could check if it is the first time that the update()
method is being called by a member bool first_run_
or something like that. So instead of having:
bool LowPassFilter<T>::update(const T & data_in, T & data_out)
{
if (!configured_)
{
throw std::runtime_error("Filter is not configured");
}
// If this is the first call to update initialize the filter at the current state
// so that we dont apply an impulse to the data.
if (Traits::is_nan(filtered_value_) || Traits::is_empty(filtered_value_))
{
// rest of the code
}
}
We could have:
bool LowPassFilter<T>::update(const T & data_in, T & data_out)
{
if (!configured_)
{
throw std::runtime_error("Filter is not configured");
}
// If this is the first call to update initialize the filter at the current state
// so that we dont apply an impulse to the data.
if (first_run_)
{
// rest of the code
first_run_ = false
}
}
And remove the configure()
method entirely. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends on the type of filter; it might make sense to have the configure method.
I would say just change it to be empty for now and that should do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
storage.data = std::vector<U>{std::numeric_limits<U>::quiet_NaN()}; | |
(void)storage; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. let me run the CI
Signed-off-by: Pedro Nogueira <[email protected]>
This PR aims to tackle #345.
In order to do so, I followed the advice from @christophfroehlich, using
type_traits
to identify which type was passed to the template of theLowPassFilter
and configure theStorageType
accordingly, all of that in compile time.Since there is now only one set of three variables instead of two sets, I also corrected all the other occurrences of the variables were removed.
Also, @christophfroehlich said
I thought this was a very beautiful idea, but I had some difficulties making this actually happen, because the function
update()
has some particularities for each data type. At the end, I was basically re-implementing theupdate()
function three times within the traits struct. So I thought it was simpler and more readable to leave the overload the way it is. However, I am open to suggestions!!One last thing: I had to change the base branch to
jazzy
, because theros2-master
branch was not compiling in my machine. Would you like for me to target the branchros2-master
instead ofjazzy
? EDIT: The bot told me to do it, so I did it.Please let me know anything that would be nice to change