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 }