1 /// This module is about comparison of floating point arithmetics.
2 /// Supported by this very informative article:
3 /// https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
4 module gfx.math.approx;
5 
6 import std.traits : isFloatingPoint;
7 
8 /// Different methods to check if two floating point values are close to each other.
9 enum ApproxMethod
10 {
11     /// Unit of Least Precision.
12     ulp,
13     /// Relative error
14     rel,
15     /// ULP, with absolute error
16     ulpAndAbs,
17     /// Relative and absolute error
18     relAndAbs,
19 }
20 
21 /// Check with method given at runtime with default parameters
22 /// a and b can be any data sets supported by the approx template
23 bool approx(T)(ApproxMethod method, in T a, in T b)
24 {
25     final switch(method)
26     {
27         case ApproxMethod.ulp:
28             return approxUlp(a, b);
29         case ApproxMethod.rel:
30             return approxRel(a, b);
31         case ApproxMethod.ulpAndAbs:
32             return approxUlpAndAbs(a, b);
33         case ApproxMethod.relAndAbs:
34             return approxRelAndAbs(a, b);
35     }
36 }
37 
38 /// Compare two data sets with ApproxMethod.ulp.
39 /// That is call scalarApproxUlp on each couple of the data sets.
40 alias approxUlp = approx!(ApproxMethod.ulp);
41 /// Compare two data sets with ApproxMethod.rel.
42 /// That is call scalarApproxRel on each couple of the data sets.
43 alias approxRel = approx!(ApproxMethod.rel);
44 /// Compare two data sets with ApproxMethod.ulpAndAbs.
45 /// That is call scalarApproxUlpAndAbs on each couple of the data sets.
46 alias approxUlpAndAbs = approx!(ApproxMethod.ulpAndAbs);
47 /// Compare two data sets with ApproxMethod.relAndAbs.
48 /// That is call scalarApproxRelAndAbs on each couple of the data sets.
49 alias approxRelAndAbs = approx!(ApproxMethod.relAndAbs);
50 
51 
52 /// Compute the ULP difference between two floating point numbers
53 /// Negative result indicates that b has higher ULP value than a.
54 template ulpDiff(T) if (isFloatingPoint!T)
55 {
56     long ulpDiff(in T a, in T b)
57     {
58         immutable fnA = FloatNum!T(a);
59         immutable fnB = FloatNum!T(b);
60 
61         return (fnA - fnB);
62     }
63 }
64 
65 /// Determines if two floating point scalars are maxUlps close to each other.
66 template scalarApproxUlp(T) if (isFloatingPoint!T)
67 {
68     bool scalarApproxUlp(in T a, in T b, in ulong maxUlps=4)
69     {
70         import std.math : abs;
71 
72         immutable fnA = FloatNum!T(a);
73         immutable fnB = FloatNum!T(b);
74 
75         if (fnA.negative != fnB.negative)
76         {
77             return a == b; // check for +0 / -0
78         }
79 
80         return (abs(fnA - fnB) <= maxUlps);
81     }
82 }
83 
84 /// Check whether the relative error between a and b is smaller than maxEps
85 template scalarApproxRel(T) if (isFloatingPoint!T)
86 {
87     bool scalarApproxRel (in T a, in T b, in T maxEps=4*T.epsilon)
88     {
89         import std.algorithm : max;
90         import std.math : abs;
91         const diff = abs(b-a);
92         const absA = abs(a);
93         const absB = abs(b);
94         const largest = max(absA, absB);
95         return diff <= maxEps*largest;
96     }
97 }
98 
99 /// Determines if two floating point scalars are maxUlps close to each other.
100 /// If the absolute error is less than maxAbs, the test succeeds however.
101 /// This is useful when comparing against zero the result of a subtraction.
102 template scalarApproxUlpAndAbs(T) if (isFloatingPoint!T)
103 {
104     bool scalarApproxUlpAndAbs(in T a, in T b, in T maxAbs=0.0001, in ulong maxUlps=4)
105     {
106         import std.math : abs;
107         if (abs(b-a) <= maxAbs) return true;
108         return scalarApproxUlp(a, b, maxUlps);
109     }
110 }
111 
112 /// Check whether the relative error between a and b is smaller than maxEps.
113 /// If the absolute error is less than maxAbs, the test succeeds however.
114 /// This is useful when comparing against zero the result of a subtraction.
115 template scalarApproxRelAndAbs(T) if (isFloatingPoint!T)
116 {
117     bool scalarApproxRelAndAbs(in T a, in T b, in T maxAbs=0.0001, in T maxEps=4*T.epsilon)
118     {
119         import std.math : abs;
120         if (abs(b-a) <= maxAbs) return true;
121         return scalarApproxRel(a, b, maxEps);
122     }
123 }
124 
125 /// Generic template to check approx method on different sets of data.
126 template approx(ApproxMethod method)
127 {
128     import gfx.math.mat : Mat;
129     import gfx.math.vec : Vec;
130 
131     /// Apply method on scalar
132     bool approx(T, Args...)(in T a, in T b, Args args)
133     if (isFloatingPoint!T)
134     {
135         static if (method == ApproxMethod.ulp)
136         {
137             return scalarApproxUlp(a, b, args);
138         }
139         else static if (method == ApproxMethod.rel)
140         {
141             return scalarApproxRel(a, b, args);
142         }
143         else static if (method == ApproxMethod.ulpAndAbs)
144         {
145             return scalarApproxUlpAndAbs(a, b, args);
146         }
147         else static if (method == ApproxMethod.relAndAbs)
148         {
149             return scalarApproxRelAndAbs(a, b, args);
150         }
151         else
152         {
153             static assert(false, "unimplemented");
154         }
155     }
156     /// Apply method check on vectors
157     bool approx(T, size_t N, Args...)(in T[N] v1, in T[N] v2, Args args)
158     if (isFloatingPoint!T)
159     {
160         static foreach (i; 0 .. N) {
161             if (!approx(v1[i], v2[i], args)) return false;
162         }
163         return true;
164     }
165     /// ditto
166     bool approx(T, size_t N, Args...)(in Vec!(T, N) v1, in Vec!(T, N) v2, Args args)
167     if (isFloatingPoint!T)
168     {
169         return approx(v1.data, v2.data, args);
170     }
171     /// Apply method check on arrays
172     bool approx(T, Args...)(in T[] arr1, in T[] arr2, Args args)
173     if (isFloatingPoint!T)
174     {
175         if (arr1.length != arr2.length) return false;
176         foreach (i; 0 .. arr1.length) {
177             if (!approx(arr1[i], arr2[i], args)) return false;
178         }
179         return true;
180     }
181     /// Apply method check on matrices
182     bool approx(T, size_t R, size_t C, Args...)(in T[C][R] m1, in T[C][R] m2, Args args)
183     if (isFloatingPoint!T)
184     {
185         static foreach (r; 0 .. R) {
186             if (!approx(m1[r], m2[r], args)) return false;
187         }
188         return true;
189     }
190     // /// ditto
191     bool approx(T, size_t R, size_t C, Args...)(in Mat!(T, R, C) m1, in Mat!(T, R, C) m2, Args args)
192     if (isFloatingPoint!T)
193     {
194         static foreach (r; 0 .. R) {
195             if (!approx(m1[r], m2[r], args)) return false;
196         }
197         return true;
198     }
199 }
200 
201 
202 private:
203 
204 template FloatTraits(T)
205 if (isFloatingPoint!T)
206 {
207     static if (is(T == float))
208     {
209         alias IntType = int;
210         enum exponentMask = 0x7f80_0000;
211     }
212     else static if (is(T == double) || (is(T == real) && T.sizeof == 8))
213     {
214         alias IntType = long;
215         enum exponentMask = 0x7ff0_0000_0000_0000;
216     }
217     else static if (is(T == real) && T.sizeof > 8)
218     {
219         static assert(false, "FloatTraits unsupported for real.");
220     }
221 }
222 
223 union FloatNum(T)
224 if (is(T == float) || is(T == double) || is(T == real) && T.sizeof == 8)
225 {
226     alias F = FloatTraits!T;
227 
228     alias FloatType = T;
229     alias IntType = F.IntType;
230 
231     this (FloatType f)
232     {
233         this.f = f;
234     }
235 
236     FloatType f;
237     IntType i;
238 
239     long opBinary(string op)(FloatNum rhs) const
240     if (op == "-")
241     {
242         return i - rhs.i;
243     }
244 
245     @property bool negative() const
246     {
247         return i < 0;
248     }
249 
250     debug @property IntType mantissa() const
251     {
252         enum IntType one = 1;
253         return i & ((one << T.mant_dig) - one);
254     }
255 
256     debug @property IntType exponent() const
257     {
258         return ((i & F.exponentMask) >> T.mant_dig);
259     }
260 }
261 
262 
263 union FloatNum(T)
264 if (is(T == real) && T.sizeof > 8)
265 {
266     static assert(T.sizeof == 16 || T.sizeof == 12 || T.sizeof == 10, "Unexpected real size.");
267 
268     static if (T.sizeof == 10)
269     {
270         struct Int
271         {
272             long l;
273             ushort h;
274         }
275     }
276     static if (T.sizeof == 12)
277     {
278         struct Int
279         {
280             long l;
281             uint h;
282         }
283     }
284     static if (T.sizeof == 16)
285     {
286         struct Int
287         {
288             long l;
289             ulong h;
290         }
291     }
292 
293     real f;
294     Int i;
295 
296     this (real f)
297     {
298         this.f = f;
299     }
300 
301     long opBinary(string op)(FloatNum rhs) const
302     if (op == "-")
303     {
304         immutable lo = i.l - rhs.i.l;
305         // do not check for overflow of the hi long
306         return lo;
307     }
308 
309     @property bool negative() const
310     {
311         return f < 0;
312     }
313 }
314 
315 unittest
316 {
317     immutable fn1 = FloatNum!real(1);
318     immutable fn2 = FloatNum!real(-1);
319 
320     assert(! fn1.negative);
321     assert(  fn2.negative);
322 }
323 
324 unittest
325 {
326     import std.math : nextUp;
327     immutable real r1 = 0;
328     immutable real r2 = nextUp(r1);
329 
330     immutable fn1 = FloatNum!real(r1);
331     immutable fn2 = FloatNum!real(r2);
332 
333     assert(fn2 - fn1 == 1);
334 }