Skip to main content
GameDev.net gamedev.net
🔒 Locked

Expecting calculation to be 0

Started by Mybowlcut Nov 6, 2011 at 7:31 AM 19 replies 5.1k views
Original Post
Mybowlcut
Mybowlcut
If I pass in amount=25 rotation=90 to the method below, the result is a weird -1.5308086E-15 instead of (what I thought) should be 0. Why is this?

/**
* @param amount The distance to move. Will always be positive.
* @param rotation
* @return The distance resulting from moving amount in the direction of rotation.
*/
public static float getYDistanceMovedBy(float amount, float rotation) {
return Math.abs(amount) * (float) -Math.cos(Math.toRadians(rotation));
}


public void testGetYDistanceMovedBy2() {
float amount = 25.0f;
float rotation = 90.0f;
float distance = Geometry.getYDistanceMovedBy(amount, rotation);
assertEquals(0.0f, distance);
}



The same code for the x axis works as expected, with the result being 0:
/**
* @param amount The distance to move. Will always be positive.
* @param rotation
* @return The distance resulting from moving amount in the direction of rotation.
*/
public static float getXDistanceMovedBy(float amount, float rotation) {
return Math.abs(amount) * (float) Math.sin(Math.toRadians(rotation));
}


public void testGetXDistanceMovedBy2() {
float amount = 25.0f;
float rotation = 0.0f;
float distance = Geometry.getXDistanceMovedBy(amount, rotation);
assertEquals(0.0f, distance);
}


Cheers.

Mybowlcut
Mybowlcut

Without looking over much of your code, it's because floats can easily have rounding errors, you will notice your incorrect value is really close to 0 anyway (Hopefully close enough):

http://www.cprogramm...ting_point.html

Yeah I thought it might be something to do with that, but I don't know how to account for it without affecting legitimate values.

alvaro
alvaro
It is also worth noting that the vast majority of the time, you can get away with doing only "less than" or "more than" comparisons, in such a way that in borderline cases it doesn't matter much whether those test as true or false. Algorithms with this feature tend to be robust.
haegarr
haegarr

If I pass in amount=25 rotation=90 to the method below, the result is a weird -1.5308086E-15 instead of (what I thought) should be 0. Why is this?
...



I'm roughly familiar with epsilon comparison, but I was under the impression that it was a far smaller threshold than -1.5 (the value in question)?

The value in question seems me -1.5*10[sup]-15[/sup] (notice the E-15 in -1.5308086E-15, see e.g. this in wikipedia), what is -0.0000000000000015 (if I've counted correctly).
Pthalicus
Pthalicus

I'm roughly familiar with epsilon comparison, but I was under the impression that it was a far smaller threshold than -1.5 (the value in question)? I'm also a bit confused as to how comparison relates to my problem when there are none taking place. If it was a smaller number (ie < 1), I'd understand...


The value [color=#1C2837][size=2]-1.5308086E-15 actually represents something like -0.0000000000000153 - which is rather close to 0.

[color="#1C2837"]

You can think of the "E-15" as a shift of the decimal point. Its a short hand notation for extremely large or small numbers.


[color="#1C2837"]

The previous posters have linked some nice resources, especially rip-off's post.

Saving the world, one semi-colon at a time.
Mybowlcut
Mybowlcut

[quote name='Mybowlcut' timestamp='1320564695' post='4881003']
If I pass in amount=25 rotation=90 to the method below, the result is a weird -1.5308086E-15 instead of (what I thought) should be 0. Why is this?
...



I'm roughly familiar with epsilon comparison, but I was under the impression that it was a far smaller threshold than -1.5 (the value in question)?


The value in question seems me -1.5*10[sup]-15[/sup], what is -0.0000000000000015 (if I've counted correctly). That is somewhat close to 0, and definitely less than 1 ;)
[/quote]
Why is it that the one time someone responds quickly on GameDev, I make a really stupid post? I tried deleting it, but you're just too quick. Haha.

Ok, so basically clamp to 0 if it's an acceptable margin of error?

Mybowlcut
Mybowlcut

[quote name='Mybowlcut' timestamp='1320651463' post='4881301']
I'm roughly familiar with epsilon comparison, but I was under the impression that it was a far smaller threshold than -1.5 (the value in question)? I'm also a bit confused as to how comparison relates to my problem when there are none taking place. If it was a smaller number (ie < 1), I'd understand...


The value [color="#1C2837"]-1.5308086E-15 actually represents something like -0.0000000000000153 - which is rather close to 0.

[color="#1C2837"]

You can think of the "E-15" as a shift of the decimal point. Its a short hand notation for extremely large or small numbers.


[color="#1C2837"]

The previous posters have linked some nice resources, especially rip-off's post.


[/quote]
Thank you for taking the time to explain it without being condescending, unlike certain people in other threads I've made... very helpful.

haegarr
haegarr

...Ok, so basically clamp to 0 if it's an acceptable margin of error?

It depends on the circumstances:

The value you're computing is a distance. If it is measured in meters and express the distance between 2 atoms then you shouldn't clamp it. But if it is a distance in meters between 2 people meeting at the streets a resolution of 0.001 is good enough.

You actually do a comparison by invoking assertEquals(0.0f, distance). For this you should use an Epsilon expression. For sub-sequent uses as transformation parameter you'll usually need not clamp the distance but use it as is.

Notice that small errors may become a problem when being accumulated. E.g. concatenating many rotations will yield in a non-uniform scaling deformation because the small errors summed up will leave you with a non-pure rotation. If you have a situation like that you need to be up against it. Another situation is where precision plays a role, e.g. at marginal cases in point in polygon tests. In such cases you need to use precise data types and functions.
Mybowlcut
Mybowlcut

[quote name='Mybowlcut' timestamp='1320652023' post='4881304']
...Ok, so basically clamp to 0 if it's an acceptable margin of error?

It depends on the circumstances:

The value you're computing is a distance. If it is measured in meters and express the distance between 2 atoms then you shouldn't clamp it. But if it is a distance in meters between 2 people meeting at the streets a resolution of 0.001 is good enough.

You actually do a comparison by invoking assertEquals(0.0f, distance). For this you should use an Epsilon expression. For sub-sequent uses as transformation parameter you'll usually need not clamp the distance but use it as is.

Notice that small errors may become a problem when being accumulated. E.g. concatenating many rotations will yield in a non-uniform scaling deformation because the small errors summed up will leave you with a non-pure rotation. If you have a situation like that you need to be up against it. Another situation is where precision plays a role, e.g. at marginal cases in point in polygon tests. In such cases you need to use precise data types and functions.
[/quote]
Hmm. I've been doing a bit of reading (I appreciate the links given, but I'm not mathematically minded and so opted for something with simpler examples) and it seems that my idea of using a fixed value for the epsilon wouldn't be a good idea. I'd like to use something like AlmostEqual2sComplement, but am not sure about two things:

  1. How to do the same thing in Java (in terms of the dereferencing etc.)
  2. What to use for maxUlps

Here is the [font="Courier New"]AlmostEqual2sComplement[/font] mentioned in the article:
bool AlmostEqual2sComplement(float A, float B, int maxUlps)
{
// Make sure maxUlps is non-negative and small enough that the
// default NAN won't compare as equal to anything.
assert(maxUlps > 0 && maxUlps < 4 * 1024 * 1024);
int aInt = *(int*)&A
// Make aInt lexicographically ordered as a twos-complement int
if (aInt < 0)
aInt = 0x80000000 - aInt;
// Make bInt lexicographically ordered as a twos-complement int
int bInt = *(int*)&B
if (bInt < 0)
bInt = 0x80000000 - bInt;
int intDiff = abs(aInt - bInt);
if (intDiff <= maxUlps)
return true;
return false;
}

haegarr
haegarr
Java has a build-in function to get the bit pattern of floats: Float.floatToIntBits(float), so that
int aInt = *(int*)&A
will become
int aInt = Float.floatToIntBits(A);
Mybowlcut
Mybowlcut

Java has a build-in function to get the bit pattern of floats: Float.floatToIntBits(float), so that
int aInt = *(int*)&A
will become
int aInt = Float.floatToIntBits(A);

Ah, thanks for that. Now I've got:

public static float getYDistanceMovedBy(float amount, float rotation) {
return Math.abs(amount) * (float) -Math.cos(Math.toRadians(rotation));
}

public void testGetYDistanceMovedBy2() {
float amount = 25.0f;
float rotation = 90.0f;
float distance = Geometry.getYDistanceMovedBy(amount, rotation);
int maxUlps = 10;

assertTrue(almostEqual2sComplement(0.0f, distance, maxUlps));
}

boolean almostEqual2sComplement(float a, float b, int maxUlps) {
// Make sure maxUlps is non-negative and small enough that the
// default NAN won't compare as equal to anything.
assert(maxUlps > 0 && maxUlps < 4 * 1024 * 1024);
int aInt = Float.floatToIntBits(a);
// Make aInt lexicographically ordered as a twos-complement int
if (aInt < 0)
aInt = 0x80000000 - aInt;
// Make bInt lexicographically ordered as a twos-complement int
int bInt = Float.floatToIntBits(b);
if (bInt < 0)
bInt = 0x80000000 - bInt;
int intDiff = Math.abs(aInt - bInt);
if (intDiff <= maxUlps)
return true;
return false;
}


That article says:
maxUlps cannot be arbitrarily large. If maxUlps is four million or greater then there is a risk of finding large negative floats equal to NANs. If maxUlps is sixteen million or greater then the largest positive floats will compare as equal to the largest negative floats.

As a practical matter such large maxUlps values should not be needed. A maxUlps of sixteen million means that numbers 100% larger and 50% smaller should count as equal. A maxUlps of four million means that numbers 25% larger and 12.5% smaller should count as equal. If these large maxUlps values are needed then separate checking for wrap-around above infinity to NANs or numbers of the opposite sign will be needed. To prevent accidental usage of huge maxUlps values the comparison routines assert that maxUlps is in a safe range.[/quote]

Is the maxUlps going to be variable or can I use a fixed value? How do I determine it in either scenario?

haegarr
haegarr

...
Is the maxUlps going to be variable or can I use a fixed value? How do I determine it in either scenario?

I'm not quite sure if I understand how the algorithm works. maxUlps is said to be an error measure defined as the amount of float bit-patterns that are allowed. You're working close to 0. The smallest positive value close to 0 is FLOAT.MIN_VALUE.

But then IMHO you have a problem: MIN_VALUE is 2[sup]-149[/sup] or approx. 1.4*10[sup]-45[/sup]. To fit the given error of 1.5*10[sup]-15[/sup] into this, you need a big maxUlps. Perhaps I'm missing something... Someone else with an insight?
alvaro
alvaro
Looking at the bit pattern makes sense if you are checking something like a relative error. If you want to compare to zero, checking if the absolute value is less than epsilon is actually much more reasonable.

What is it you are trying to do that requires checking if the number is 0?
Mybowlcut
Mybowlcut

Looking at the bit pattern makes sense if you are checking something like a relative error. If you want to compare to zero, checking if the absolute value is less than epsilon is actually much more reasonable.

What is it you are trying to do that requires checking if the number is 0?

If you look at the original post, I want to make sure that the method returns 0 and not [color="#1C2837"]-1.5308086E-15 for the y axis when an angle of 90 degrees is passed in. Everyone is saying it involves floating point comparison, so that's why we're discussing how to go about it.

[color="#1C2837"]Edit: A similar value results from an angle of 270 degrees ([color="#1c2837"]4.5924254E-15). What I want is for values directly along the x axis to return 0 during a calculation only involving movement along the y axis, as seen in my original post.

alvaro
alvaro
Values directly along the x axis do return 0 if you measure along the y axis. The problem is that you cannot represent pi/4 exactly as a floating-point value, and you don't get something that is directly along an axis if you take another axis and rotate it by an amount that is close to pi/4 but not quite pi/4.

In other words, your expectation of not losing precision in that process is unreasonable. Now, if the rest of your code is robust enough, whether you get 0 of -1.5308086E-15 shouldn't make much of a difference.


Mybowlcut
Mybowlcut

Values directly along the x axis do return 0 if you measure along the y axis. The problem is that you cannot represent pi/4 exactly as a floating-point value, and you don't get something that is directly along an axis if you take another axis and rotate it by an amount that is close to pi/4 but not quite pi/4.

Oh, ok. Probably had a lot to do with the fact that I thought the value I was getting was larger than it was, as well. :s


In other words, your expectation of not losing precision in that process is unreasonable. Now, if the rest of your code is robust enough, whether you get 0 of -1.5308086E-15 shouldn't make much of a difference.

How would I ensure that it is? I'm assuming it's something to do with what haegarr said:

[color="#1C2837"]Notice that small errors may become a problem when being accumulated. E.g. concatenating many rotations will yield in a non-uniform scaling deformation because the small errors summed up will leave you with a non-pure rotation. If you have a situation like that you need to be up against it. Another situation is where precision plays a role, e.g. at marginal cases in point in polygon tests. In such cases you need to use precise data types and functions. [/quote]

alvaro
alvaro

[quote name='alvaro' timestamp='1320699318' post='4881523']
In other words, your expectation of not losing precision in that process is unreasonable. Now, if the rest of your code is robust enough, whether you get 0 of -1.5308086E-15 shouldn't make much of a difference.

How would I ensure that it is? I'm assuming it's something to do with what haegarr said:

[color="#1C2837"]Notice that small errors may become a problem when being accumulated. E.g. concatenating many rotations will yield in a non-uniform scaling deformation because the small errors summed up will leave you with a non-pure rotation. If you have a situation like that you need to be up against it. Another situation is where precision plays a role, e.g. at marginal cases in point in polygon tests. In such cases you need to use precise data types and functions. [/quote]
[/quote]

In the case of a rotation matrix that ends up not being orthogonal, you can re-orthogonalize it every few frames. If you use a quaternion it's actually easier to do.

Point-in-polygon tests are tricky. Ideally it should be the case that for a point on the border of the polygon it doesn't matter much whether you classify it as inside or outside.

Anyway, you didn't say what you will do with the result, so it's hard to tell if you'll run into trouble. You should educate yourself about how floating-point numbers work. There's just no way around it.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.