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