0

What is the best way to constrain any value from -pi to pi ?

I currently have:

if (fAngle > XM_PI) { fAngle = fAngle - XM_2PI; } else if (fAngle < -XM_PI) { fAngle = fAngle - -XM_2PI; } 

However, I fear those if's should instead be while's

For reference, under the Exploit Symmetrical Functions section:

https://developer.arm.com/solutions/graphics-and-gaming/developer-guides/learn-the-basics/understanding-numerical-precision/mitigating-loss-of-precision

Extra bit of precision!

10
  • 3
    If there might be need for while consider using fmod(). Unsure how that would work in practice for the negative value though. Commented Feb 27, 2021 at 17:17
  • hmmm...fmod()?... as in fAngle = fmod(fAngle, XM_2PI); ... not sure whether you have to tweak the expression for negative values. Commented Feb 27, 2021 at 17:17
  • 1
    Your current variant with if will work if the original angle is in the range [−3·π, +3·π]. If you know where the original angle comes from and can be sure that this condition is met, you can get away with just if. (Most "arc" functions return such valid angles. Adding up to three "constrained" angles in the range [−π, +π] will yield a valid angle, too. User input may not.) Commented Feb 27, 2021 at 17:32
  • Well if lets say you did: sin(iTime) where iTime could be any value in floating point seconds. Commented Feb 27, 2021 at 17:37
  • 1
    MSVC says of sin() The sin functions return the sine of x. If x is greater than or equal to 263, or less than or equal to -263, a loss of significance in the result occurs. As that is radians, you don't need to worry about small excess of the ±pi constraint. Commented Feb 27, 2021 at 17:50

3 Answers 3

2

Adding or subtracting XM_2PI cannot restore any accuracy that has been lost. In fact, it adds noise, generally losing more accuracy, because XM_2PI is necessarily only an approximation of 2π. It has some error itself, so adding or subtracting it adds or subtracts the error in the approximation.

What it can do is keep you from losing more accuracy by ensuring that future results remain low in magnitude, thus remaining in a region where the floating-point format has more precision than if the number grew beyond 4, 8, 16, or other points where the exponent changes and the absolute precision becomes worse.

If you already have some value x outside [−π, π] and want its sine or cosine, you should get the best result by using sin(x) or cos(x) directly. Good implementations of sin and cos will reduce the argument using a high-precision value for 2π, so you will get a better result than using sin(x-XM_PI) or cos(x-XM_PI) (unless, by chance, the various errors in these happen to cancel).

So your task with trigonometric functions is not to reduce values you already have but to design your algorithms to keep values from growing. Adding or subtracting 2π is a reasonable way to do this. However, when you do it, add or subtract an extended-precision version of 2π, not just XM_2PI. You can do this by representing 2π as XM_2PI (which should be the value representable in floating-point that is closest to 2π) plus some residue r. r should be the value representable in floating-point that is closest to 2π−XM_2PI. You can calculate that with extended-precision software such as GMP or Maple and can likely find it online. (I do not have it handy or I would paste it here; anybody else is welcome to edit it in.) Then you would update your angle with fAngle = fAngle - XM_2PI - r; or fAngle = fAngle + XM_2PI + r;.

An exception is if you have the angle measured in some unit that you can represent or reduce exactly, such as in degrees (which you can reduce by 360º with no error as long as the number of degrees itself is represented with no error) or in time (such as number of seconds for some function with a period of a day or other rational number of seconds, so you can again reduce with no error). In that case, you can let the angle grow as long as you can represent it exactly, and you would reduce it modulo the period prior to converting it to radians.

Sign up to request clarification or add additional context in comments.

10 Comments

Yes; but the precision loss can be far worse as it also depends on the magnitudes (e.g. for very large values of x, x - XM_2PI == x).
Ok, so what about ARM's webpage I linked to, in exploit symmetrical functions paragraph?
Keeping the values within the pi range keeps the precision loss lower for any program duration vs. not wrapping the angle by the pi range. Then keeping your input values to sincos becomes simpler. For an accumulating iTime for example, where the value could get quite large, say in multiple hours.
XM_PI is suitably defined with 7 digits of precision in the fractional part of the float it represents. The entire DirectXMath library uses XM_PI where applicable. I don't think using a define with more digits of precision will matter as it will be limited when used as an immediate value (float).
@J.Tully: The fact that XM_PI or XM_2PI is “suitably defined” can only mean that it is as accurate as can be in the floating-point format. It does not mean that subtracting it alone is the best we can do. For illustration, consider a floating-point format with three decimal digits. If we have 7.00 and want to reduce it modulo 2π, the mathematical result would be 7-2π = .71681469… The XM_2PI in this format would be 6.28, because that is as close as the format can represent to 2π, 6.2831853… Then 7 - XM_2PI would yield .72…
|
1

The simplest coding way is to use the math library function remainder, as in

fAngle = remainder( fangle, XM_2PI); 

1 Comment

If this prevents continous precision loss like with sin(iTime) where iTime is a fairly large value being accumulated I believe its a great solution. Keeping the values within the pi range keeps the precision loss lower for any program duration vs. not wrapping the angle by the pi range.
0
STATIC_INLINE_PURE float const __vectorcall constrain(float const fAngle) { static constexpr double const dPI(std::numbers::pi), d2PI(2.0 * std::numbers::pi), dResidue(-1.74845553146951715461909770965576171875e-07); // abs difference between d2PI(double precision) and XM_2PI(float precision) double dAngle(fAngle); dAngle = std::remainder(dAngle, d2PI); if (dAngle > dPI) { dAngle = dAngle - d2PI - dResidue; } else if (dAngle < -dPI) { dAngle = dAngle + d2PI + dResidue; } return((float)dAngle); } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.