ros2 / rosidl_python

rosidl support for Python
Apache License 2.0
19 stars 45 forks source link

Allow NaN values to pass floating point bounds check. #167

Closed oysstu closed 1 year ago

oysstu commented 2 years ago

This pull request resolves the issue #162 by explicitly adding NaN values to the floating point bounds check.

Signed-off-by: Oystein Sture os@skarvtech.com

Fixes #169 Fixes #162

oysstu commented 2 years ago

Math should only be imported for messages with floating point fields in order for it to pass the linter

oysstu commented 2 years ago

Updated the pull request. Inverted the bounds check instead of calling math.isnan. This works since NaN always evaluates to false in comparisons.

oysstu commented 2 years ago

In that case, the bounds check for float64 can be removed, as all numbers pass through it, unless I'm misunderstanding something about the representation of the values. Assigning a larger number than 1.7976931348623157e+308 to a regular python float or numpy.float64 automatically turns it into inf.

The bounds check may still make sense for float32, since there is no python builtin type, and may exceed the C/C++ float type. Question is if it should be converted to -inf/inf instead of throwing a bounds error, since this is the behavior for float64.

connoranderson commented 2 years ago

Question is if it should be converted to -inf/inf instead of throwing a bounds error, since this is the behavior for float64.

Just my opinion here, I think silent overflow to inf makes sense for float32, if we do not have type-checking to validate that the user is actually providing a np.float32.

oysstu commented 2 years ago

Question is if it should be converted to -inf/inf instead of throwing a bounds error, since this is the behavior for float64.

Just my opinion here, I think silent overflow to inf makes sense for float32, if we do not have type-checking to validate that the user is actually providing a np.float32.

I agree. And that seems to be the behavior in IEEE 754 as far as I can tell. Honestly, I'm in favor of removing the entire bounds check for floating point numbers. I don't see what problem it solves.

connoranderson commented 2 years ago

Yes it looks like IEEE 754 specifies this behavior

Maybe @clalancette could weigh in on why the original PR was merged and then we can figure out whether the bounds check should be kept?

sloretz commented 2 years ago

The bounds check may still make sense for float32, since there is no python builtin type, and may exceed the C/C++ float type. Question is if it should be converted to -inf/inf instead of throwing a bounds error, since this is the behavior for float64.

Honestly, I'm in favor of removing the entire bounds check for floating point numbers. I don't see what problem it solves.

The idea behind the checks using @property on the python classes was to make it easier to debug fields being set with invalid values. It raises an error at the point in code that a message was given an invalid value rather than getting an error when the message can't be serialized. The downside is these checks can be pretty expensive when setting a lot of fields. The checks can be skipped by passing the -O option to cPython. Checking the bounds of float32 here seems like it matches that intent.

Letting the conversion to inf happen means a message serialized and then deserialized might not be equal to itself, which feels non-intuitive to me.

from rclpy.serialization import serialize_message, deserialize_message

from test_msgs.msg import BasicTypes

def float32_value(self):
   return self._float32_value

def float32_value_setter(self, value):
   self._float32_value = value

BasicTypes.float32_value = property(float32_value, float32_value_setter)

if __name__ == '__main__':

    b = BasicTypes()
    b.float32_value = -3.402823e+40
    b_prime = deserialize_message(serialize_message(b), BasicTypes)

    print('b == b_prime?', b == b_prime)  # prints False because b_prime.float32_value is inf

As for float64, I agree the bounds check isn't needed as long as sys.float_info.max and sys.float_info.min are the same as the bounds we'd be checking. If we get rid of the bounds check for float64 I'd still recommend a check that sys.float_info matches what we expect when the class is constructed.

oysstu commented 2 years ago

Letting the conversion to inf happen means a message serialized and then deserialized might not be equal to itself, which feels non-intuitive to me.

I agree that this is undesirable. If the corresponding numpy type or python.ctypes type had been used in python, this would not be the case.

As for float64, I agree the bounds check isn't needed as long as sys.float_info.max and sys.float_info.min are the same as the bounds we'd be checking. If we get rid of the bounds check for float64 I'd still recommend a check that sys.float_info matches what we expect when the class is constructed.

That would mean that python is running on hardware that is not IEEE 754 compliant, or compiled with flags that disable conformance mode, but yes it can happen.

What if the floating point bounds checks are kept, but variables explicitly set to Inf/NaN are allowed through? How does that sound?

sloretz commented 2 years ago

What if the floating point bounds checks are kept, but variables explicitly set to Inf/NaN are allowed through? How does that sound?

That sounds great to me

oysstu commented 2 years ago

I think the tests are failing because the assigned value gets rounded to inf and passes the bounds check, but I'll need to have a closer look at why it's happening. I won't have time to look at this again for a few days

oysstu commented 1 year ago

I've updated the pull request, and it seems to pass the checks now. The float64 bounds check test is only executed if sys.float_info.max exceeds 1.7976931348623157e+308. Otherwise, the python float is converted silently to inf, which passes the bounds check without triggering the assertion error.

The out or range tests are performed in test_arrays(), test_bounded_sequences(), and test_unbounded_sequences(). I've added the new tests for all of them. Perhaps these can be cleaned up in a separate PR.

sloretz commented 1 year ago

CI (repos file build: --packages-above-and-dependencies rosidl_generator_py test: --packages-above rosidl_generator_py)

sloretz commented 1 year ago

CI LGTM!

Thank you for the PR and for iterating!

mmatthebi commented 1 year ago

Hi, is there any plan when and if this PR will be merged into current Humble release?

sloretz commented 1 year ago

Hi, is there any plan when and if this PR will be merged into current Humble release?

I'm cautious about backporting a behavior change to a low level package like this. If it is backported, then https://github.com/ros2/rosidl_python/pull/182 must be backported with it.

mmatthebi commented 1 year ago

Hi, is there any plan when and if this PR will be merged into current Humble release?

I'm cautious about backporting a behavior change to a low level package like this. If it is backported, then #182 must be backported with it.

thanks for the quick answer. In my eyes, this behaviour in Humble is a regression from Galactic, hence worth porting back into Humble. I actually wonder why this has not impeded more users. Our system relies on setting some message contents to nan to indicate that the value is empty and this is actually also commonly used in built-in messages (like BatteryState). With the present behaviour we cannot move our setup to Humble LTS (which we would rather prefer).

markcutler commented 1 year ago

Hi, is there any plan when and if this PR will be merged into current Humble release?

I'm cautious about backporting a behavior change to a low level package like this. If it is backported, then #182 must be backported with it.

thanks for the quick answer. In my eyes, this behaviour in Humble is a regression from Galactic, hence worth porting back into Humble. I actually wonder why this has not impeded more users. Our system relies on setting some message contents to nan to indicate that the value is empty and this is actually also commonly used in built-in messages (like BatteryState). With the present behaviour we cannot move our setup to Humble LTS (which we would rather prefer).

+1 This is affecting our system as well. We would like to upgrade to Humble but need this PR backported first. Thanks.

fujitatomoya commented 1 year ago

@mergify backport humble

mergify[bot] commented 1 year ago

backport humble

❌ Command disallowed due to command restrictions in the Mergify configuration.

- [ ] `sender-permission>=write`
fujitatomoya commented 1 year ago

https://github.com/Mergifyio backport humble

fujitatomoya commented 1 year ago

@Mergifyio backport humble

mergify[bot] commented 1 year ago

backport humble

❌ Command disallowed due to command restrictions in the Mergify configuration.

- [ ] `sender-permission>=write`
mergify[bot] commented 1 year ago

backport humble

❌ Command disallowed due to command restrictions in the Mergify configuration.

- [ ] `sender-permission>=write`