Mathf.Approximately is not always working!

When I set very simple float values like these representing rounded degrees that do not even change at all: 90.0f, 180.0f & 270.0f. Sometimes the function doesn’t even work. Is this a bug in v5.5.2f1? Because I’m pretty sure that if I use Mathf.Approximately(180.0f, 180.0f) it should say true at all times right o.0?! I fixed it by using my own solution for float imprecision, but that was not what I wanted… I hope this bug is known and fixed, but if it is then please tell me. I assume it isn’t fixed yet in the latest version because this is a difficult thing to estimate I suppose? I got a computer of high quality so that should’nt be the cause of the problem XD

1 Like

Show some actual code that exhibits this not working. There’s probably some other mistake there.

3 Likes

That is exactly why I’m so confused, I’m literally just using my own rounded values. So like I said it comes down to comparing the same values. Showing any of my code doesn’t help at all. I’m 100% positive that the problem is with Unity, atleast with the version I use. However I have heard from someone on reddit that when the float becomes bigger, that the imprecision will also be bigger. So perhaps the epsilon isn’t enough for values like 270.0f?

Someone said this:
I created the following class based on info I got from the book “Essential Mathematics for Games and Interactive Applications” by James Van Verth and Lars Bishop. The gist of it is that using epsilon by itself isn’t always sufficient because the magnitude of the floating point number can significantly change the size of the floating-point error. So you scale the epsilon “fudge-factor” accordingly. Also, values less than 1 are a special-case scenario, hence the + 1f involved:

class Floater
{
    public static bool Equal( float v, float k )
    {
        float leftSide = Math.Abs( v - k );
        float rightSide = (Math.Abs( v ) + Math.Abs( k ) + 1f) * float.Epsilon;

        return leftSide <= rightSide;
    }

    public static bool Equal( double v, double k )
    {
        double leftSide = Math.Abs( v - k );
        double rightSide = (Math.Abs( v ) + Math.Abs( k ) + 1f) * float.Epsilon;

        return leftSide <= rightSide;
    }

    public static bool LessThan( float a, float b )
    {
        if ( !Equal( a, b ) )
            return a < b;
        else
            return false;
    }

    public static bool LessThan( double a, double b )
    {
        if ( !Equal( a, b ) )
            return a < b;
        else
            return false;
    }

    public static bool GreaterThan( float a, float b )
    {
        if ( !Equal( a, b ) )
            return a > b;
        else
            return false;
    }

    public static bool GreaterThan( double a, double b )
    {
        if ( !Equal( a, b ) )
            return a > b;
        else
            return false;
    }

    public static bool LessThanOrEqual( float a, float b )
    {
        if ( Equal( a, b ) )
            return true;
        else
            return a < b;
    }

    public static bool LessThanOrEqual( double a, double b )
    {
        if ( Equal( a, b ) )
            return true;
        else
            return a < b;
    }

    public static bool GreaterThanOrEqual( float a, float b )
    {
        if ( Equal( a, b ) )
            return true;
        else
            return a > b;
    }

    public static bool GreaterThanOrEqual( double a, double b )
    {
        if ( Equal( a, b ) )
            return true;
        else
            return a > b;
    }
}

His code however does not make use of Mathf.Epsilon <.< but maybe it’s still relevant

Here’s the thing. Mathf.Approximately is a fairly straight forward function and Unity wouldn’t change it all that frequently. So its behaviour likely wouldn’t change from version to version.

Here is the code for it decompiled from 2019.2.0f1:

    /// <summary>
    ///   <para>Compares two floating point values and returns true if they are similar.</para>
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    public static bool Approximately(float a, float b)
    {
      return (double) Mathf.Abs(b - a) < (double) Mathf.Max(1E-06f * Mathf.Max(Mathf.Abs(a), Mathf.Abs(b)), Mathf.Epsilon * 8f);
    }

2018.4.1f1

    /// <summary>
    ///   <para>Compares two floating point values and returns true if they are similar.</para>
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    public static bool Approximately(float a, float b)
    {
      return (double) Mathf.Abs(b - a) < (double) Mathf.Max(1E-06f * Mathf.Max(Mathf.Abs(a), Mathf.Abs(b)), Mathf.Epsilon * 8f);
    }

And here from 5.5.2f1.

    /// <summary>
    ///   <para>Compares two floating point values if they are similar.</para>
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    public static bool Approximately(float a, float b)
    {
      return (double) Mathf.Abs(b - a) < (double) Mathf.Max(1E-06f * Mathf.Max(Mathf.Abs(a), Mathf.Abs(b)), Mathf.Epsilon * 8f);
    }

As you can see, not a whole lot of change.

If a math operation is failing, it’s relatively trivial to catch during some unit test, and would heavily impact the user base. If a math operation is failing… it’s reasonable to assume you’re the loose link rather than the API used by thousands of people and hammered on repeatedly.

This isn’t to say math operations are therefore 100% bug free. It just means odds are that for a chunk of code that has existed through years of in the wild usage… bugs would be caught. Math bugs aren’t subtle…

(I will point out the code does attempt to resolve for the scale of the numbers compared by multiplying 0.000001 by the scale of the largest of the 2 numbers passed in)

So you even can demonstrate how it may be the way you used it that is the problem… this is sort of an argument as to why we’d want to see your specific code. We may pick up on some nuance like this.

You’re on 5.5.2, that’s from February 2017… there’s a really easy way to see if it’s been fixed. Use a newer version!

TLDR;

How about you show us the code where Mathf.Approximately is failing for you.

Otherwise, there’s not a whole lot we can say to you aside from “works for us”.

If you can reproduce it repeatedly in a demo project on a more recent version of Unity. Well, wrap that project up and submit it as a bug report to Unity:
https://support.unity3d.com/hc/en-us/articles/206336985-How-do-I-submit-a-bug-report-

Heck, they’ve given me vouchers to spend in the unity store in the past for pointing out valid bugs.

3 Likes

As in all these cases, the chances that there’s a bug in a very simple standard library that thousands of people use every day are lower than there being a bug in your code.

Btw, that Floater class you posted breaks on infinity.

2 Likes

haha rly XD what did you do with infinity exactly? perhaps because it uses float.Epsilon instead of Mathf.Epsilon…?? idk, if you replace it with the epsilon value from Unity does it also not work?^^

Because: inf - inf = NaN

It’s the first operation of your Equals method.

Your return statement is going to be NaN <= Inf. Which is false.

With that said, Unity’s version also fails on infinity as well.

2 Likes

Even if you are correct that this is a bug in Unity, seeing your code would help to narrow down the conditions under which the bug occurs, so you should still post code demonstrating the bug.

But the odds that the bug is in Unity rather than in your code are very low, because Unity’s code has been used by many more people and therefore has been subject to much greater scrutiny.

Don’t use Mathf.Approximately. Use your own threshold for “equal” that you find acceptable.

Since the implementation scales the threshold by the input, you can get weird results unless all your input has approximately the same size, which is not a given.

2 Likes

It actually seems to work fine according to this test:

public static void Main()
    {
        Console.WriteLine(Equal(10, 9));                                                    // False
        Console.WriteLine(Equal(float.PositiveInfinity, 3));                                // True
        Console.WriteLine(Equal(float.PositiveInfinity, float.PositiveInfinity));            // False
        Console.WriteLine(Approximately(10, 9));                                            // False
        Console.WriteLine(Approximately(float.PositiveInfinity, 3));                        // False
        Console.WriteLine(Approximately(float.PositiveInfinity, float.NegativeInfinity));    // False
    }
   
    public static bool Equal( float v, float k )
    {
        float leftSide = Math.Abs( v - k );
        float rightSide = (Math.Abs( v ) + Math.Abs( k ) + 1f) * float.Epsilon;

        return leftSide <= rightSide;
    }
   
    public static bool Approximately(float a, float b)
    {
        return (double) Math.Abs(b - a) < (double) Math.Max(1E-06f * Math.Max(Math.Abs(a), Math.Abs(b)), float.Epsilon * 8f);
    }

I was referring to equality between inf and inf… which both return false (though float.PositiveInfinity == float.PositiveInfinity is true in C#. Though an argument could be made inf should not equal inf mathematically, but that’s not how C#/ieee 754 say they are).

I am seeing you’re referring to inf vs another number. Which yeah, I didn’t look into that. And yeah, I can see how that happens because inf - x = inf, and inf + x + 1 * epsilon = inf. So it thinks they’re equal.

We were talking about 2 different things.

1 Like

doesn’t matter how “big” the number is, what matters is how many digits it has and how far apart they are (the none zero values), you can store 0.000000123 with the same precision as 123000000.0, you can’t store 123000000.000000123 with good precision, 32 bit floats can handle up to 9 digits apart correctly, iirc.

Like @Baste said I also never use that function, I just write the check in the if statement unless there’s some edge cases to consider.

Also, show your code or go home, maybe you’re not sending it the values you think your sending it?
“unity has a bug guys, trust me, my code is good” is pure BS.

2 Likes