Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# Floating point ranges are experimental and may be deprecated

I'm trying to make a little function to interpolate between two values with a given increment.

[ 1.0 .. 0.5 .. 20.0 ]

The compiler tells me that this is deprecated, and suggests using ints then casting to float. But this seems a bit long-winded if I have a fractional increment - do I have to divide my start and end values by my increment, then multiple again afterwards? (yeuch!).

I saw something somewhere once about using sequence comprehensions to do this, but I can't remember how.

Help, please.

like image 916
Benjol Avatar asked Dec 06 '22 07:12

Benjol


2 Answers

TL;DR: F# PowerPack's BigRational type is the way to go.


What's Wrong with Floating-point Loops

As many have pointed out, float values are not suitable for looping:

  • They do have Round Off Error, just like with 1/3 in decimal, we inevitably lose all digits starting at a certain exponent;
  • They do experience Catastrophic Cancellation (when subtracting two almost equal numbers, the result is rounded to zero);
  • They always have non-zero Machine epsilon, so the error is increased with every math operation (unless we are adding different numbers many times so that errors mutually cancel out -- but this is not the case for the loops);
  • They do have different accuracy across the range: the number of unique values in a range [0.0000001 .. 0.0000002] is equivalent to the number of unique values in [1000000 .. 2000000];

Solution

What can instantly solve the above problems, is switching back to integer logic.

With F# PowerPack, you may use BigRational type:

open Microsoft.FSharp.Math
// [1 .. 1/3 .. 20]
[1N .. 1N/3N .. 20N]
|> List.map float
|> List.iter (printf "%f; ")

Note, I took my liberty to set the step to 1/3 because 0.5 from your question actually has an exact binary representation 0.1b and is represented as +1.00000000000000000000000 * 2-1; hence it does not produce any cumulative summation error.

Outputs:

1.000000; 1.333333; 1.666667; 2.000000; 2.333333; 2.666667; 3.000000; (skipped) 18.000000; 18.333333; 18.666667; 19.000000; 19.333333; 19.666667; 20.000000;

// [0.2 .. 0.1 .. 3]
[1N/5N .. 1N/10N .. 3N]
|> List.map float
|> List.iter (printf "%f; ")

Outputs:

0.200000; 0.300000; 0.400000; 0.500000; (skipped) 2.800000; 2.900000; 3.000000;

Conclusion

  • BigRational uses integer computations, which are not slower than for floating-points;
  • The round-off occurs only once for each value (upon conversion to a float, but not within the loop);
  • BigRational acts as if the machine epsilon were zero;

There is an obvious limitation: you can't use irrational numbers like pi or sqrt(2) as they have no exact representation as a fraction. It does not seem to be a very big problem because usually, we are not looping over both rational and irrational numbers, e.g. [1 .. pi/2 .. 42]. If we do (like for geometry computations), there's usually a way to reduce the irrational part, e.g. switching from radians to degrees.


Further reading:

  • What Every Computer Scientist Should Know About Floating-Point Arithmetic
  • Numeric types in PowerPack
like image 145
bytebuster Avatar answered May 23 '23 13:05

bytebuster


Interestingly, float ranges don't appear to be deprecated anymore. And I remember seeing a question recently (sorry, couldn't track it down) talking about the inherent issues which manifest with float ranges, e.g.

> let xl = [0.2 .. 0.1 .. 3.0];;

val xl : float list =
  [0.2; 0.3; 0.4; 0.5; 0.6; 0.7; 0.8; 0.9; 1.0; 1.1; 1.2; 1.3; 1.4; 1.5; 1.6;
   1.7; 1.8; 1.9; 2.0; 2.1; 2.2; 2.3; 2.4; 2.5; 2.6; 2.7; 2.8; 2.9]

I just wanted to point out that you can use ranges on decimal types with a lot less of these kind of rounding issues, e.g.

> [0.2m .. 0.1m .. 3.0m];;
val it : decimal list =
  [0.2M; 0.3M; 0.4M; 0.5M; 0.6M; 0.7M; 0.8M; 0.9M; 1.0M; 1.1M; 1.2M; 1.3M;
   1.4M; 1.5M; 1.6M; 1.7M; 1.8M; 1.9M; 2.0M; 2.1M; 2.2M; 2.3M; 2.4M; 2.5M;
   2.6M; 2.7M; 2.8M; 2.9M; 3.0M]

And if you really do need floats in the end, then you can do something like

> {0.2m .. 0.1m .. 3.0m} |> Seq.map float |> Seq.toList;;
val it : float list =
  [0.2; 0.3; 0.4; 0.5; 0.6; 0.7; 0.8; 0.9; 1.0; 1.1; 1.2; 1.3; 1.4; 1.5; 1.6;
   1.7; 1.8; 1.9; 2.0; 2.1; 2.2; 2.3; 2.4; 2.5; 2.6; 2.7; 2.8; 2.9; 3.0]
like image 37
Stephen Swensen Avatar answered May 23 '23 14:05

Stephen Swensen