What operations can make floats leave a [0, 1] range?

Viewed 162

I frequently work with float or double types which are in a range of [0, 1]. I am aware that floating point operations are imprecise, so I typically clamp my values so that they are guaranteed to be in this range before/after operations.

In some cases I rely on floats being not even slightly negative and being exactly <= 1, making this necessary.

For example is it necessary in any of these functions:

// x and y are guaranteed to be in [0, 1]
float avg(float x, float y) {
    // the average of [0, 1] values should always be in [0, 1]
    return std::clamp<float>((x + y) / 2, 0, 1);
}

float mul(float x, float y) {
    // the product of [0, 1] values should always be in [0, 1]
    return std::clamp<float>(x * y, 0, 1);
}

float pow(float x, unsigned y) {
    // raising an [0, 1] value to any unsigned power should also result in an [0, 1] value
    return std::clamp<float>(std::pow(x, y), 0, 1);
}

Are there any consistent rules for when arithmetic operations can make floats leave the [0, 1] range?

2 Answers

Restricting this answer to IEEE754 if I may.

0, 1, and 2 can all be represented exactly as a float. The arithmetic operators are required to return the best floating point value possible. Since x and y are neither greater than 1, their sum cannot be greater than 2 since then otherwise there would exist a better float for the sum. Put another way, the sum of two floats a little less than 1 cannot be greater than 2.

The same applies to the product.

The third one requires a clamp since there are no guarantees that std::pow(x, y) returns the best possible float.

Corner issue

float pow(float x, unsigned y) {
    // raising an [0, 1] value to any unsigned power should also result in an [0, 1] value
    return std::clamp<float>(std::pow(x, y), 0, 1);
}

With std::pow(±0, 0) may lead to outside [0..1] range (or cause a domain error) as that result is not specified by std::pow() nor mathematically resolved: Zero to the power of zero.

If following IEEE, the result is 1 yet a compliant library is not required to follow that.

If not following IEEE, and std::pow(0,0) returns NAN, I'd expect std::clamp(NAN,0,1) to also return NAN and break the system.

Candidate alternative code

return (y==0) ? 1 : x;

Note a result of the functions could be -0.0, yet that value is still in the [0...1] range.

I see no concerns with mul(), avg() regarding -0.0, intermediate calculation done at higher precision and variant rounding modes.

Related