1

I am working on an project to simulate physics/gravity in space and one part is to predict orbits and display certain information, one of many are periapsis and apopasis (closest and most remote point of an orbit). The easiest way to achieve this is to find the point where the approaching velocity of the object in orbit relative to the stationary object is 0 but because the amount of calculations per second is limited it almost never happens that the value is exact 0 - so my thought was to compare every loop if the sign of the current value and the value the loop before has changed.

The code looks similar to this:

ArrayList orbit = new ArrayList();    
orbit.Add(newValue);

//In case the sign changes - to +

if(
   orbit.Count >= 2 &&
   physics.getVelocity(orbit[orbit.Count-1], otherObject) == Math.Abs(physics.getVelocity(orbit[orbit.Count-1], otherObject) &&
   physics.getVelocity(orbit[orbit.Count-2], otherObject) != Math.Abs(physics.getVelocity(orbit[orbit.Count-2], otherObject)
)

//In case the sign changes + to -

if(
   orbit.Count >= 2 &&
   physics.getVelocity(orbit[orbit.Count-1], otherObject) != Math.Abs(physics.getVelocity(orbit[orbit.Count-1], otherObject) &&
   physics.getVelocity(orbit[orbit.Count-2], otherObject) == Math.Abs(physics.getVelocity(orbit[orbit.Count-2], otherObject)
)

But sometimes it does not seem to work so I debuged the variable and could not find any mistakes there were always patterns like:

Value (1): -0.4523452
Value (2): 1.2435235

or

Value (1): 0.4523452
Value (2): -1.2435235

But the algorithm ignored some of the events but to my observation only if the numbers were close to 0 (like above) but if it looks like this:

Value (1): 64.904534
Value (2): -7.3456800

it is fine and I could not figure out why. I hope someone can help me - if there are any questions left, please ask :)

Ganymed_
  • 13
  • 4
  • Why are you using an ArrayList? – It'sNotALie. Jun 14 '13 at 18:22
  • 5
    You cannot reliably compare floating point values for *exact* equality. They are not accurate enough. They float. Subtract the two numbers, take the absolute and compare to a small number. – Hans Passant Jun 14 '13 at 18:24
  • Moreover Math.Abs is the _absolute value_, you seem to be willing to compare the _sign_... why don't you use Math.Sign? – jods Jun 14 '13 at 18:27
  • @HansPassant: Shouldn't we always use `Double.Epsilon` as our small number? – myermian Jun 14 '13 at 18:36
  • 3
    @m-y: no, certainly not. Double.Epsilon is the smallest positive number that a double can hold. But the bigger your number absolute value, the bigger your error will be. Epsilon is pretty much guaranteed to be too small. – jods Jun 14 '13 at 18:38
  • 1
    There is no loss of precision taking an absolute value, it is a simple bit flip. But passing the value to a function that takes absolute value may lose precision (spill from FPU register and coercion to the parameter type of the function) – Ben Voigt Jun 14 '13 at 18:55

3 Answers3

5

This type of detection, to detect when one value is negative and the other positive (order is irrelevant), you should use the Math.Sign method:

if (Math.Sign(a) != Math.Sign(b)) { ... }

The answer by @supercat, however, is correct for your current code, floating point values are finicky that way.

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • Hey, thank you, this should have been obivous, but i did not thought of it :) But right now I am facing another, very similar problem: Math.Sign only works for me as long as I round the double to 0 decimal places. Thats really weird because when i debug the Math. Sign values i get something like -1 and 1 it should work regardles if i round the values or not. – Ganymed_ Jun 14 '13 at 23:09
  • Casting to integers helps for some reason! – Ganymed_ Jun 14 '13 at 23:17
4

When using floating-point numbers, one should generally assume that every computation could have some small but arbitrary fudge factor added to it, and that even in cases where it would seem that two sequences of computations should be performed identically and accumulate the same fudge factors, they won't always do so. It's entirely possible that if one tests Math.Abs(x+y+z) == x+y+z, the computation of the argument to Math.Abs will get performed slightly differently from the computation to the right of the equality operator sign.

If you want to test whether a number is less than zero, don't compare it against its absolute value. Check whether it's less than zero. Storing the result of a computation in a variable, passing that variable to Math.Abs, and comparing for equality would probably be more reliable than repeating a computation on both sides of the equality operator (whatever fudge factors result from the computation should get stored in that variable), but why bother?

supercat
  • 77,689
  • 9
  • 166
  • 211
  • A much better approach is to use Math.Sign. – Lasse V. Karlsen Jun 14 '13 at 18:40
  • About the last paragraph: storing the result in a local variable x, then comparing Math.Abs(x) == x. Given the nature of abs on floating point representation, one can think that this should work but I would still not use it. Because the compiler may not dump x into memory before doing abs on the fpu. And intermediate results on the fpu may have extended precision, so I worry that abs(x) and x may round differently when dumped to memory. – jods Jun 14 '13 at 18:47
  • @LasseV.Karlsen: If one wants to test whether something is negative, which is what the original poster seemed to be doing, checking whether it's less than zero is probably better than `Math.Sign`. Using `Math.Sign` would let the code detect transitions between positive and zero, or between zero and positive; that could be an advantage in some cases, but code might have to worry that a transition from negative to zero and then from zero to positive would get counted as two "changes". – supercat Jun 14 '13 at 19:03
  • @jods: I said "probably more reliable", rather than saying it would work, because I don't know what is or is not guaranteed. Certainly if code did `someFloat = (float)someCalculation(); if (someFloat > whatever)...` the compiler would be required to lop off any extra precision; I would think such a thing would be required even when assigning an internal type to a variable of type `double`, but I'm not sure. – supercat Jun 14 '13 at 19:04
  • @supercat The last sentence of his question says "... to compare every loop if the sign of the current value and the value the loop before has changed." – Lasse V. Karlsen Jun 14 '13 at 20:31
0

You could try just testing if the value is within a certain value of 0.

Let epsilon be the tolerance. Values less than this distance from a certain number will be treated as equal to that number.

Then you can write an IsApproximately() predicate:

public static bool IsApproximately(double number1, double number2, double epsilon)
{
    return Math.Abs(number1 - number2) <= epsilon;
}

It makes it a bit more readable then when you want to compare numbers, e.g.:

if (IsApproximately(velocity, 0, 0.0001))
{
    // Treat as zero
}
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276