1 /** 2 Flex module that allows to sample from arbitrary random distributions. 3 4 License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 5 6 Authors: Sebastian Wilzbach, Ilya Yaroshenko 7 8 $(RED This module is available in the extended configuration.) 9 10 The Transformed Density Rejection with Inflection Points (Flex) algorithm 11 can sample from arbitrary distributions given (1) its log-density function f, 12 (2) its first two derivatives and (3) a partitioning into intervals 13 with at most one inflection point. 14 15 These can be easily found by plotting `f''`. 16 $(B Inflection points) can be identified by observing at which points `f''` is 0 17 and an inflection interval which is defined by two inflection points can either be: 18 19 $(UL 20 $(LI $(F_TILDE) is entirely concave (`f''` is entirely negative)) 21 $(LI $(F_TILDE) is entirely convex (`f''` is entirely positive)) 22 $(LI $(F_TILDE) contains one inflection point (`f''` intersects the x-axis once)) 23 ) 24 25 It is not important to identify the exact inflection points, but the user input requires: 26 27 $(UL 28 $(LI Continuous density function $(F_TILDE).) 29 $(LI Continuous differentiability of $(F_TILDE) except in a finite number of 30 points which need to have a one-sided derivative.) 31 $(LI $(B Doubled) continuous differentiability of $(F_TILDE) except in a finite number of 32 points which need to be inflection points.) 33 $(LI At most one inflection point per interval) 34 ) 35 36 Internally the Flex algorithm transforms the distribution with a special 37 transformation function and constructs for every interval a linear `hat` function 38 that majorizes the `pdf` and a linear `squeeze` function that is majorized by 39 the `pdf` from the user-defined, mutually-exclusive partitioning. 40 41 $(H3 Efficiency `rho`) 42 43 In further steps the algorithm splits those intervals until a chosen efficiency 44 `rho` between the ratio of the sum of all hat areas to the sum of 45 all squeeze areas is reached. 46 For example an efficiency of 1.1 means that `10 / 110` of all 47 drawn uniform numbers don't match the target distribution and need be resampled. 48 A higher efficiency constructs more intervals, and thus requires more iterations 49 and a longer setup phase, but increases the speed of sampling. 50 51 $(H3 Unbounded intervals) 52 53 In each unbounded interval the transformation and thus the density must be 54 concave and strictly monotone. 55 56 $(H3 Transformation function (T_c)) $(A_NAME t_c_family) 57 58 The Flex algorithm uses a family of T_c transformations: 59 60 $(UL 61 $(LI `T_0(x) = log(x)) 62 $(LI `T_c(x) = sign(c) * x^c) 63 ) 64 65 Thus `c` has the following properties 66 67 $(UL 68 $(LI Decreasing `c` may decrease the number of inflection points) 69 $(LI For unbounded domains, `c > -1` is required) 70 $(LI For unbounded densities, `c` must be sufficiently small, but should 71 be great than -1. A common choice is `-0.5`) 72 $(LI `c=0` is the pure `log` transformation and thus decreases the 73 vulnerability for under- and overflows) 74 ) 75 76 References: 77 Botts, Carsten, Wolfgang Hörmann, and Josef Leydold. 78 "$(LINK2 http://epub.wu-wien.ac.at/3158/1/techreport-110.pdf, 79 Transformed density rejection with inflection points.)" 80 Statistics and Computing 23.2 (2013): 251-260. 81 82 Macros: 83 F_TILDE=$(D g(x)) 84 A_NAME=<a name="$1"></a> 85 */ 86 module mir.random.flex; 87 88 static if (is(typeof({ import mir.ndslice.slice; }))) 89 { 90 91 import mir.random.flex.internal.types; 92 import mir.random.variable : DiscreteVariable, discreteVar, RandomVariable; 93 import mir.random; 94 import std.traits : isCallable, isFloatingPoint, ReturnType; 95 96 version(Flex_logging) 97 { 98 import std.experimental.logger; 99 } 100 101 import mir.math.common; 102 103 /** 104 The Transformed Density Rejection with Inflection Points (Flex) algorithm 105 can sample from arbitrary distributions given its density function f, its 106 first two derivatives and a partitioning into intervals with at most one inflection 107 point. The partitioning needs to be mutually exclusive and sorted. 108 109 Params: 110 pdf = probability density function of the distribution 111 f0 = logarithmic pdf 112 f1 = first derivative of logarithmic pdf 113 f2 = second derivative of logarithmic pdf 114 c = $(LINK2 #t_c_family, T_c family) value 115 cs = $(LINK2 #t_c_family, T_c family) array 116 points = non-overlapping partitioning with at most one inflection point per interval 117 rho = efficiency of the Flex algorithm 118 maxApproxPoints = maximal number of points to use for the hat/squeeze approximation 119 120 Returns: 121 Flex Generator. 122 */ 123 auto flex(S, F0, F1, F2) 124 (in F0 f0, in F1 f1, in F2 f2, 125 S c, S[] points, S rho = 1.1, int maxApproxPoints = 1_000) 126 if (isFloatingPoint!S) 127 { 128 S[] cs = new S[points.length - 1]; 129 foreach (ref d; cs) 130 d = c; 131 return flex(f0, f1, f2, cs, points, rho, maxApproxPoints); 132 } 133 134 /// ditto 135 auto flex(S, Pdf, F0, F1, F2) 136 (in Pdf pdf, in F0 f0, in F1 f1, in F2 f2, 137 S c, S[] points, S rho = 1.1, int maxApproxPoints = 1_000) 138 if (isFloatingPoint!S) 139 { 140 S[] cs = new S[points.length - 1]; 141 foreach (ref d; cs) 142 d = c; 143 return flex(pdf, f0, f1, f2, cs, points, rho, maxApproxPoints); 144 } 145 146 /// ditto 147 auto flex(S, F0, F1, F2) 148 (in F0 f0, in F1 f1, in F2 f2, 149 S[] cs, S[] points, S rho = 1.1, int maxApproxPoints = 1_000) 150 if (isFloatingPoint!S) 151 { 152 auto pdf = (S x) => exp(f0(x)); 153 return flex(pdf, flexIntervals(f0, f1, f2, cs, points, rho, maxApproxPoints)); 154 } 155 156 /// ditto 157 auto flex(S, Pdf, F0, F1, F2) 158 (in Pdf pdf, in F0 f0, in F1 f1, in F2 f2, 159 S[] cs, S[] points, S rho = 1.1, int maxApproxPoints = 1_000) 160 if (isFloatingPoint!S) 161 { 162 return flex(pdf, flexIntervals(f0, f1, f2, cs, points, rho, maxApproxPoints)); 163 } 164 165 /// ditto 166 auto flex(S, Pdf)(in Pdf pdf, in FlexInterval!S[] intervals) 167 if (isFloatingPoint!S) 168 { 169 return Flex!(S, typeof(pdf))(pdf, intervals); 170 } 171 172 /++ 173 Data body of the Flex algorithm. 174 Can be used to sample from the distribution. 175 +/ 176 @RandomVariable struct Flex(S, Pdf) 177 if (isFloatingPoint!S) 178 { 179 // density function 180 private const Pdf _pdf; 181 182 // generated partition points 183 private const FlexInterval!S[] _intervals; 184 185 // discrete density sampler 186 private DiscreteVariable!S ds; 187 188 package this(in Pdf pdf, in FlexInterval!S[] intervals) 189 { 190 import mir.math.sum: Summator, Summation; 191 import mir.ndslice.topology: member; 192 193 _pdf = pdf; 194 _intervals = intervals; 195 196 // pre-calculate normalized probs 197 auto cdPoints = new S[intervals.length]; 198 Summator!(S, Summation.precise) total = 0; 199 200 foreach (el; intervals.member!`hatArea`) 201 total += el; 202 203 S totalSum = total.sum; 204 205 foreach (i, ref cd; cdPoints) 206 cd = intervals[i].hatArea / totalSum; 207 208 this.ds = discreteVar!S(cdPoints); 209 } 210 211 /// Generated partition points 212 const(FlexInterval!S[]) intervals() @property const 213 { 214 return _intervals; 215 } 216 217 /** 218 Sample a value from the distribution. 219 Params: 220 rng = random number generator to use 221 Returns: 222 Array of length `n` with the samples 223 */ 224 S opCall(RNG)(ref RNG rng) 225 if (isRandomEngine!RNG) 226 { 227 return flexImpl(_pdf, _intervals, ds, rng); 228 } 229 } 230 231 /// 232 version(mir_random_test) unittest 233 { 234 import mir.math : approxEqual; 235 import std.meta : AliasSeq; 236 import mir.random.engine.xorshift : Xorshift; 237 auto gen = Xorshift(42); 238 alias S = double; 239 auto f0 = (S x) => -x^^4 + 5 * x^^2 - 4; 240 auto f1 = (S x) => 10 * x - 4 * x^^3; 241 auto f2 = (S x) => 10 - 12 * x^^2; 242 S[] points = [-3, -1.5, 0, 1.5, 3]; 243 244 auto tf = flex(f0, f1, f2, 1.5, points, 1.1); 245 auto value = tf(gen); 246 } 247 248 version(X86_64) version(mir_random_test) unittest 249 { 250 import std.meta : AliasSeq; 251 import mir.math : approxEqual; 252 import mir.random.engine.xorshift : Xorshift; 253 import mir.math : exp, sqrt, PI; 254 foreach (S; AliasSeq!(float, double, real)) 255 { 256 S sqrt2PI = sqrt(2 * PI); 257 auto f0 = (S x) => 1 / (exp(x * x / 2) * sqrt2PI); 258 auto f1 = (S x) => -(x/(exp(x * x / 2) * sqrt2PI)); 259 auto f2 = (S x) => (-1 + x * x) / (exp(x * x / 2) * sqrt2PI); 260 auto pdf = (S x) => exp(f0(x)); 261 S[] points = [-3.0, 0, 3]; 262 alias F = FlexInterval!S; 263 alias LF = LinearFun!S; 264 265 // generated from 266 auto intervalsGen = flex(f0, f1, f2, [1.5, 1.5], points, S(1.1)).intervals; 267 268 auto intervals = [F(-3, -0.720759, 1.5, LF(0.254392, -0.720759, 1.58649), 269 LF(0.0200763, -3, 1.00667), 2.70507, 2.32388), 270 F(-0.720759, 0, 1.5, LF(-0, 0, 1.81923), 271 LF(0.322909, 0, 1.81923), 1.07411, 1.02762), 272 F(0, 0.720759, 1.5, LF(-0, 0, 1.81923), 273 LF(-0.322909, 0, 1.81923), 1.07411, 1.02762), 274 F(0.720759, 1.36003, 1.5, LF(-0.498434, 0.720759, 1.58649), 275 LF(-0.409229, 1.36003, 1.26786), 0.80997, 0.799256), 276 F(1.36003, 3, 1.5, LF(-0.159263, 1.36003, 1.26786), 277 LF(-0.0200763, 3, 1.00667), 1.78593, 1.66515) 278 ]; 279 280 foreach (i; 0..intervals.length) 281 { 282 assert(intervals[i].lx.approxEqual(intervalsGen[i].lx, 1e-5, 1e-5)); 283 assert(intervals[i].rx.approxEqual(intervalsGen[i].rx, 1e-5, 1e-5)); 284 assert(intervals[i].hatArea.approxEqual(intervalsGen[i].hatArea, 1e-5, 1e-5)); 285 assert(intervals[i].squeezeArea.approxEqual(intervalsGen[i].squeezeArea, 1e-5, 1e-5)); 286 } 287 288 auto tf = flex(pdf, intervals); 289 auto gen = Xorshift(42); 290 291 S[] res = [-1.27001, -1.56078, 0.112434, -1.86799, -0.2875, 1.12576, 292 -0.78079, 2.89136, -1.51572, 1.04432]; 293 294 //foreach (i; 0..res.length) 295 // assert(tf(gen).approxEqual(res[i])); 296 } 297 } 298 299 version(X86_64) version(mir_random_test) unittest 300 { 301 import mir.math.common; 302 import mir.math : approxEqual; 303 import std.meta : AliasSeq; 304 import mir.random.engine.xorshift : Xorshift; 305 foreach (S; AliasSeq!(float, double, real)) 306 { 307 auto gen = Xorshift(42); 308 auto f0 = (S x) => -pow(x, 4) + 5 * x * x - 4; 309 auto f1 = (S x) => 10 * x - 4 * pow(x, 3); 310 auto f2 = (S x) => 10 - 12 * x * x; 311 S[] points = [-3, -1.5, 0, 1.5, 3]; 312 313 auto tf = flex(f0, f1, f2, 1.5, points, 1.1); 314 S[] res = [-1.64677, 1.56697, -1.48606, -1.68103, -1.09229, 1.46837, 315 -1.61755, 1.73641, -1.66105, 1.10856]; 316 317 //foreach (i; 0..10) 318 // assert(tf(gen).approxEqual(res[i])); 319 } 320 } 321 322 /** 323 Sample from the distribution with generated, non-overlapping hat and squeeze functions. 324 Uses acceptance-rejection algorithm. Based on Table 4 from Botts et al. (2013). 325 326 Params: 327 pdf = probability density function of the distribution 328 intervals = calculated inflection points 329 ds = discrete distribution sampler for hat areas 330 rng = random number generator to use 331 See_Also: 332 $(LINK2 https://en.wikipedia.org/wiki/Rejection_sampling, 333 Acceptance-rejection sampling) 334 */ 335 private S flexImpl(S, Pdf, RNG) 336 (in Pdf pdf, in FlexInterval!S[] intervals, 337 DiscreteVariable!S ds, ref RNG rng) 338 if (isRandomEngine!RNG) 339 { 340 import mir.math.common: exp, fabs, log; 341 import mir.random.flex.internal.transformations : antiderivative, inverseAntiderivative; 342 343 S X = void; 344 enum S one_div_3 = 1 / S(3); 345 346 /** 347 The following performs a acceptance-rejection sampling loop based on these steps 348 1) Pick an interval randomly according to their density (=hatArea) 349 2) Sample a point p on the definite integral of the hat function of the selected interval 350 3) Transform the point p to an X value using the inverse function of the integral of hat(x) 351 4) Transform x back to the target space 352 5) Generate a second random variable and check whether y * X is below our density 353 at the point X 354 355 To prevent numerical errors, some approximations need to be applied. 356 */ 357 358 for (;;) 359 { 360 // sample an interval with density proportional to their hatArea 361 immutable index = ds(rng); 362 assert(index < intervals.length); 363 immutable interval = intervals[index]; 364 365 S u = rng.rand!S.fabs * interval.hatArea; 366 367 /** 368 Generate X with density proportional to the selected interval 369 In essence this is 370 371 hat^{-1}(F_T^{-1}(u * hat.slope + T_{-1}(hat(iv.lx)))) 372 373 but we need to apply some approximations here. 374 */ 375 with(interval) 376 { 377 immutable hatLx = hat(lx); 378 if (c == 0) 379 { 380 auto eXInv = exp(-hatLx); 381 auto z = u * hat.slope * eXInv; 382 if (fabs(z) < S(1e-6)) 383 { 384 X = lx + u * eXInv * (1 - z * S(0.5) + z * z * one_div_3); 385 goto finish; 386 } 387 } 388 else 389 { 390 if (c == S(-0.5)) 391 { 392 auto z = u * hat.slope * hatLx; 393 if (fabs(z) < S(1e-6)) 394 { 395 X = lx + u * hatLx * hatLx * (1 + z + z * z); 396 goto finish; 397 } 398 } 399 else if (c == 1) 400 { 401 auto k = hatLx; 402 auto z = u * hat.slope / (k * k); 403 if (fabs(z) < S(1e-6)) 404 { 405 X = lx + u * k * (1 - z * S(0.5) + z * z * S(0.5)); 406 goto finish; 407 } 408 } 409 else 410 { 411 if (fabs(hat.slope) < S(1e-10)) 412 { 413 // reset to uniform number 414 u /= hatArea; 415 // pick a point on the straight line between lx and rx 416 X = (1 - u) * lx + u * rx; 417 goto finish; 418 } 419 } 420 } 421 // general reverse operation 422 X = hat.inverse(inverseAntiderivative(u * hat.slope + antiderivative(hatLx, c), c)); 423 424 finish: 425 426 immutable hatX = hat(X); 427 immutable squeezeX = squeeze(X); 428 429 /** 430 We have sampled a point X which is a point on the inverse CDF of the 431 transformed hat function. However all this happened in the transformed 432 spaces, hence we need to transform back to our origin space using 433 the flex inverse transformation. 434 */ 435 436 auto invHatX = flexInverse(hatX, c); 437 auto invSqueezeX = squeezeArea > 0 ? flexInverse(squeezeX, c) : 0; 438 439 /** 440 We sample a second point - this is the y coordinate (aka height) 441 of the sampling area. 442 */ 443 444 S u2 = rng.rand!S.fabs; 445 immutable t = u2 * invHatX; 446 447 /** 448 With rejection sampling we need to check whether our point X 449 is below the target density. By definition the squeeze function 450 is always below f(x). As calculating the squeeze function will be cheaper 451 than evaluating f(x) (it's a linear function!), we first check the 452 squeeze function. 453 */ 454 455 // u * h(c) < s(X) "squeeze evaluation" 456 if (t <= invSqueezeX) 457 break; 458 459 // u * h(c) < f(X) "density evaluation" 460 if (t <= pdf(X)) 461 break; 462 } 463 } 464 return X; 465 } 466 467 version(X86_64) version(DigitalMars) 468 version(mir_random_test) unittest 469 { 470 import mir.math.common; 471 import mir.math : approxEqual; 472 import std.meta : AliasSeq; 473 import mir.random.engine.xorshift : Xorshift; 474 //foreach (S; AliasSeq!(double, real)) 475 // mir.random.discrete will pick different sections depending on the FP accuracy 476 // a solution for this without FP pragmas is to modify the alias sampling as follows: 477 // if (lg.prob < 1 || lg.prob.approxEqual(1, 1e-13, 1e-13)) 478 foreach (S; AliasSeq!(double)) 479 { 480 auto f0 = (S x) => cast(S) log(1 - pow(x, 4)); 481 auto f1 = (S x) => -4 * pow(x, 3) / (1 - pow(x, 4)); 482 auto f2 = (S x) => -(4 * pow(x, 6) + 12 * x * x) / (pow(x, 8) - 2 * pow(x, 4) + 1); 483 484 S[][] res = [ 485 [-0.543025, -0.134003, 0.298425, -0.422003, -0.270376, -0.585262, 0.802257, -0.0150451, 0.469276, -0.191259], // -2 486 [-0.542987, -0.134003, 0.298425, -0.422003, -0.270376, -0.585195, 0.802077, -0.0150451, 0.469276, -0.191259], // -1.5 487 [-0.101729, -0.134003, 0.298425, 0.0779973, -0.270376, -0.199442, 0.801872, -0.0150451, 0.469276, -0.191259], // -1 488 [-0.101729, -0.134003, 0.298425, 0.0779973, -0.270376, -0.199442, 0.801828, -0.0150451, 0.469276, -0.191259], // -0.9 489 [-0.101729, -0.134003, 0.298425, 0.0779973, -0.270376, -0.199442, 0.801638, -0.0150451, 0.469276, -0.191259], // -0.5 490 [-0.101729, -0.134003, 0.298425, 0.0779973, -0.270376, -0.199442, 0.80148, -0.0150451, 0.469276, -0.191259], // -0.2 491 [-0.101729, -0.134003, 0.298425, 0.0779973, -0.270376, -0.199442, 0.801366, -0.0150451, 0.469276, -0.191259], // 0 492 [-0.101729, -0.134003, 0.298425, 0.0779973, -0.270376, -0.199442, 0.801306, -0.0150451, 0.469276, -0.191259], // 0.1 493 //[-0.101729, -0.134003, 0.298425, -0.422003, -0.270376, -0.584882, 0.640603, -0.0150451, 0.469276, -0.191259], // 0.5 494 [0.398271, -0.134003, 0.298425, -0.422003, -0.270376, -0.584882, 0.640603, -0.0150451, 0.469276, -0.191259], // 0.5 495 [-0.101729, -0.556534, 0.298425, -0.422003, -0.84412, -0.199442, 0.640494, -0.726792, 0.469276, -0.581233], // 1 496 [-0.101729, -0.556464, 0.298425, -0.422003, -0.836564, -0.199442, 0.640377, -0.726436, 0.469276, -0.581138] // 1.5 497 ]; 498 499 foreach (i, c; [-2, -1.5, -1, -0.9, -0.5, -0.2, 0, 0.1, 0.5, 1, 1.5]) 500 { 501 auto tf = flex(f0, f1, f2, c, [-1, -0.5, 0.5, 1], 1.1); 502 auto gen = Xorshift(42); 503 foreach (j; 0..10) 504 { 505 S val = tf(gen); 506 //version(Flex_logging) 507 //scope(failure) logf("%d, %d: %5g vs. %g (%s)", i, j, res[i][j], val, S.stringof); 508 //assert(res[i][j].approxEqual(val)); 509 } 510 } 511 } 512 } 513 514 version(X86_64) version(DigitalMars) 515 version(mir_random_test) unittest 516 { 517 import mir.math.common; 518 import mir.math : approxEqual; 519 import std.meta : AliasSeq; 520 import mir.random.engine.xorshift : Xorshift; 521 522 // mir.random.discrete will pick different sections depending on the FP accuracy 523 foreach (S; AliasSeq!(double, real)) 524 { 525 auto f0 = (S x) => -2 * pow(x, 4) + 4 * x * x; 526 auto f1 = (S x) => -8 * pow(x, 3) + 8 * x; 527 auto f2 = (S x) => -24 * x * x + 8; 528 529 S[][] res = [ 530 [0.887424, -0.284572, -0.871759, -0.832144, 0.653578, 0.982841, -0.827574, 0.626698, -0.466767, 0.219059], // -2 531 [0.887424, -0.268444, -0.871759, -0.832144, 0.654373, 0.982841, -0.827574, 0.627678, -0.463709, 0.237372], // -1.5 532 [0.887424, -0.240707, -0.871759, -0.832144, 0.655323, 0.982841, -0.827574, 0.628869, -0.458046, 0.262425], // -1 533 [0.887424, -0.232643, -0.871759, -0.832144, 0.655536, 0.982841, -0.827574, 0.629139, -0.456287, 0.268522], // -0.9 534 [0.407301, 0.645498, -0.922003, 0.290245, 0.902514, 0.982841, -0.691259, 0.867293, -0.988469, 0.648578], // -0.5 535 //[-0.890789, 0.939852, -0.694033, -0.931482, -0.362742, 0.657945, -0.855293, 0.843667, -0.972872, -0.907874], // 0 536 [-0.890789, 0.939852, 0.543857, -0.931482, -0.362742, 0.657945, -0.855293, 0.843667, -0.972872, -0.907874], // 0 537 //[-0.890789, 0.939852, -0.693757, -0.931482, -0.362747, 0.657317, -0.855293, 0.843654, -0.972872, -0.907874], // -0.2 538 [-0.890789, 0.939852, 0.543857, -0.931482, -0.362747, 0.657317, -0.855293, 0.843654, -0.972872, -0.907874], // -0.2 539 //[-0.890789, 0.939852, -0.694181, -0.931482, -0.362729, 0.658285, -0.855293, 0.843674, -0.972872, -0.907874], // 0.1 540 [-0.890789, 0.939852, 0.543857, -0.931482, -0.362729, 0.658285, -0.855293, 0.843674, -0.972872, -0.907874], // 0.1 541 [-0.890789, 0.939852, -0.694238, -0.931482, -0.362598, 0.658584, -0.855293, 0.843702, -0.972872, -0.907874], // 0.5 542 //[5.71265, -0.693876, -0.362273, 0.658054, 0.843743, 0.39468, -0.231144, 0.150332, -0.84545, 3.83513], // 1 543 [5.71265, 0.546109, -0.362273, 0.658054, 0.843743, 0.39468, -0.231144, 0.150332, -0.84545, 3.83513], // 1 544 [-0.0524661, -0.574867, 0.411518, -0.976726, -0.931482, -0.36179, 0.947914, -0.855293, 0.843789, -0.972872] // 1.5 545 ]; 546 547 S[] cs = [-2, -1.5, -1, -0.9, -0.5, -0.2, 0, 0.1, 0.5, 1, 1.5]; 548 foreach (i, c; cs) 549 { 550 auto tf = flex(f0, f1, f2, c, [-1, -0.5, 0.5, 1], 1.1); 551 auto gen = Xorshift(42); 552 553 foreach (j; 0..10) 554 { 555 S val = tf(gen); 556 //version(Flex_logging) 557 //scope(failure) logf("%d, %d: %5g vs. %g, type: %s", i, j, res[i][j], val, S.stringof); 558 //assert(res[i][j].approxEqual(val)); 559 } 560 } 561 } 562 } 563 564 /** 565 Reduced version of $(LREF Interval). Contains only the necessary information 566 needed in the generation phase. 567 */ 568 struct FlexInterval(S) 569 if (isFloatingPoint!S) 570 { 571 /// Left position of the interval 572 S lx; 573 574 /// Right position of the interval 575 S rx; 576 577 /// T_c family of the interval 578 S c; 579 580 /// The majorizing linear hat function 581 LinearFun!S hat; 582 583 /// The linear squeeze function which is majorized by log(f(x)) 584 LinearFun!S squeeze; 585 586 /// The total area that is spanned by the hat function in this interval 587 S hatArea; 588 589 /// The total area that is spanned by the squeeze function in this interval 590 S squeezeArea; 591 } 592 593 /** 594 Calculate the intervals for the Flex algorithm for a T_c family given its 595 density function, the first two derivatives and a valid start partitioning. 596 The Flex algorithm will try to split the intervals until a chosen efficiency 597 rho is reached. 598 599 Params: 600 f0 = probability density function of the distribution 601 f1 = first derivative of f0 602 f1 = second derivative of f0 603 cs = T_c family (single value or array) 604 points = non-overlapping partitioning with at most one inflection point per interval 605 rho = efficiency of the Flex algorithm 606 maxApproxPoints = maximal number of splitting points before Flex is aborted 607 608 Returns: Array of $(LREF FlexInterval)'s. 609 */ 610 FlexInterval!S[] flexIntervals(S, F0, F1, F2) 611 (in F0 f0, in F1 f1, in F2 f2, 612 in S[] cs, in S[] points, in S rho = 1.1, 613 in int maxApproxPoints = 1_000) 614 in 615 { 616 import mir.algorithm.iteration : all; 617 import std.math : isFinite, isInfinity; 618 619 // check efficiency rho 620 assert(rho.isFinite, "rho must be a valid value"); 621 assert(rho > 1, "rho must be > 1"); 622 623 // check points 624 assert(points.length >= 2, "two or more splitting points are required"); 625 assert(points[1..$-1].all!isFinite, "intermediate interval can't be indefinite"); 626 627 // check cs 628 assert(cs.length == points.length - 1, "cs must have length equal to |points| - 1"); 629 630 // check first c 631 if (points[0].isInfinity) 632 assert(cs[0] > - 1, "c must be > -1 for unbounded domains"); 633 634 // check last c 635 if (points[$ - 1].isInfinity) 636 assert(cs[$ - 1] > - 1, "cs must be > -1 for unbounded domains"); 637 } 638 do 639 { 640 import mir.random.flex.internal.area: calcInterval; 641 import mir.random.flex.internal.calc: arcmean; 642 import mir.random.flex.internal.transformations : transform, transformInterval; 643 import mir.random.flex.internal.types: Interval; 644 import mir.math.sum: Summator, Summation; 645 import mir.ndslice.sorting : sort; 646 import std.container.array: Array; 647 import std.container.binaryheap: BinaryHeap; 648 import mir.math: nextDown; 649 650 alias Sum = Summator!(S, Summation.precise); 651 652 Sum totalHatAreaSummator = 0; 653 Sum totalSqueezeAreaSummator = 0; 654 655 auto nrIntervals = cs.length; 656 657 // binary heap can't be extended with normal arrays, see 658 // https://github.com/dlang/phobos/pull/4359 659 auto arr = Array!(Interval!S)(); 660 auto ips = BinaryHeap!(typeof(arr), (Interval!S a, Interval!S b){ 661 S aVal = a.hatArea - a.squeezeArea; 662 S bVal = b.hatArea - b.squeezeArea; 663 // Explicit order is needed for LDC (undefined otherwise) 664 if (!(aVal == bVal)) 665 return aVal < bVal; 666 else 667 return a.lx < b.lx; 668 })(arr); 669 670 S l = points[0]; 671 S l0 = f0(points[0]); 672 S l1 = f1(points[0]); 673 S l2 = f2(points[0]); 674 675 version(Flex_logging) 676 { 677 log("starting flex with p=", points, ", cs=", cs, " rho=", rho); 678 version(Flex_logging_hex) 679 { 680 logf("points= %(%a, %)", points); 681 logf("cs= %(%a, %)", points); 682 } 683 } 684 685 // initialize with user given splitting points 686 foreach (i, r; points[1..$]) 687 { 688 auto iv = Interval!S(l, r, cs[i], l0, l1, l2, f0(r), f1(r), f2(r)); 689 l = r; 690 l0 = iv.rtx; 691 l1 = iv.rt1x; 692 l2 = iv.rt2x; 693 transformInterval(iv); 694 695 calcInterval(iv); 696 totalHatAreaSummator += iv.hatArea; 697 totalSqueezeAreaSummator += iv.squeezeArea; 698 699 ips.insert(iv); 700 } 701 702 version(Windows) {} else 703 version(Flex_logging) 704 { 705 import mir.ndslice.topology: member; 706 auto ipsD = ips.dup; 707 log("----"); 708 709 logf("Interval: %(%f, %)", ipsD.member!`lx`); 710 version(Flex_logging_hex) 711 logf("Interval: %(%a, %)", ipsD.member!`lx`); 712 713 logf("hatArea: %(%f, %)", ipsD.member!`hatArea`); 714 version(Flex_logging_hex) 715 logf("hatArea: %(%a, %)", ipsD.member!`hatArea`); 716 717 logf("squeezeArea %(%f, %)", ipsD.member!`squeezeArea`); 718 version(Flex_logging_hex) 719 logf("squeezeArea %(%a, %)", ipsD.member!`squeezeArea`); 720 721 log("----"); 722 } 723 724 // Flex is not guaranteed to converge 725 for (; nrIntervals < maxApproxPoints; nrIntervals++) 726 { 727 immutable totalHatArea = totalHatAreaSummator.sum; 728 immutable totalSqueezeArea = totalSqueezeAreaSummator.sum; 729 730 // Flex aims for a user defined efficiency 731 if (totalHatArea / totalSqueezeArea <= rho) 732 break; 733 734 version(Windows) {} else 735 version(Flex_logging) 736 { 737 tracef("iteration %d: totalHat: %.3f, totalSqueeze: %.3f, rho: %.3f", 738 nrIntervals, totalHatArea, totalSqueezeArea, totalHatArea / totalSqueezeArea); 739 version(Flex_logging_hex) 740 tracef("iteration %d: totalHat: %a, totalSqueeze: %a, rho: %a", 741 nrIntervals, totalHatArea, totalSqueezeArea, totalHatArea / totalSqueezeArea); 742 version(Flex_logging_hex) 743 logf("to be split: %s", ips.front.logHex); 744 } 745 746 immutable avgArea = nextDown(totalHatArea - totalSqueezeArea) / nrIntervals; 747 748 // remove the first element and split it 749 auto curEl = ips.front; 750 ips.removeFront; 751 752 // prepare total areas for update 753 totalHatAreaSummator -= curEl.hatArea; 754 totalSqueezeAreaSummator -= curEl.squeezeArea; 755 756 // split the interval at the arcmean into two parts 757 auto mid = arcmean!S(curEl); 758 759 // cache 760 immutable c = curEl.c; 761 762 // calculate new values 763 S mx0 = f0(mid); 764 S mx1 = f1(mid); 765 S mx2 = f2(mid); 766 767 Interval!S midIP = Interval!S(mid, curEl.rx, c, 768 mx0, mx1, mx2, 769 curEl.rtx, curEl.rt1x, curEl.rt2x); 770 771 // apply transformation to right side (for c=0 no transformations are applied) 772 if (c) 773 mixin(transform!("midIP.ltx", "midIP.lt1x", "midIP.lt2x", "c")); 774 775 // left interval: update right values 776 curEl.rx = mid; 777 curEl.rtx = midIP.ltx; 778 curEl.rt1x = midIP.lt1x; 779 curEl.rt2x = midIP.lt2x; 780 781 // recalculate intervals 782 calcInterval(curEl); 783 calcInterval(midIP); 784 785 version(Flex_logging) 786 { 787 log("--split ", nrIntervals, " between ", curEl.lx, " - ", curEl.rx); 788 log("interval to be split: ", curEl); 789 log("new middle interval created: ", midIP); 790 version(Flex_logging_hex) 791 { 792 logf("left: %s", curEl.logHex); 793 logf("right: %s", midIP.logHex); 794 } 795 796 log("update left: ", curEl); 797 log("update mid: ", midIP); 798 } 799 800 // update total areas 801 totalHatAreaSummator += curEl.hatArea; 802 totalHatAreaSummator += midIP.hatArea; 803 totalSqueezeAreaSummator += curEl.squeezeArea; 804 totalSqueezeAreaSummator += midIP.squeezeArea; 805 806 // insert new middle part into linked list 807 ips.insert(curEl); 808 ips.insert(midIP); 809 } 810 811 // for sampling only a subset of the attributes is needed 812 auto intervals = new FlexInterval!S[nrIntervals]; 813 size_t i = 0; 814 foreach (ref ip; ips) 815 intervals[i++] = FlexInterval!S(ip.lx, ip.rx, ip.c, ip.hat, 816 ip.squeeze, ip.hatArea, ip.squeezeArea); 817 818 // intervals have been sorted after hatArea 819 intervals.sort!`a.lx < b.lx`(); 820 version(Flex_logging) 821 { 822 import mir.ndslice.topology: member; 823 log("----"); 824 log("Intervals generated: ", intervals.length); 825 log("Interval: ", intervals.member!`lx`); 826 log("hatArea", intervals.member!`hatArea`); 827 log("squeezeArea", intervals.member!`squeezeArea`); 828 log("rho: ", totalHatAreaSummator.sum / totalSqueezeAreaSummator.sum); 829 log("----"); 830 } 831 832 return intervals; 833 } 834 835 // default flex with c=1.5 836 version(mir_random_test) unittest 837 { 838 import mir.ndslice.topology: member; 839 import mir.math : approxEqual; 840 import std.meta : AliasSeq; 841 842 static immutable hats = [1.79547e-05, 0.00271776, 0.0629733, 0.0912821, 843 0.18815, 0.754863, 1.13845, 1.10943, 0.788248, 844 0.547819, 0.619752, 0.254661, 0.105682, 0.0790885, 845 0.0212445, 0.0252439, 0.0252439, 0.0212445, 846 0.0790885, 0.105682, 0.254661, 0.619752, 0.547819, 847 0.788248, 1.10943, 0.566373, 0.523965, 0.754863, 848 0.18815, 0.0912821, 0.0629733, 0.00271776, 1.79547e-05]; 849 850 static immutable sqs = [2.36004e-18, 3.89553e-05, 0.00970061, 0.0704188, 851 0.165753, 0.674084, 1.05251, 1.04208, 0.769742, 852 0.539798, 0.53902, 0.215078, 0.090285, 0.0522061, 853 0.0163806, 0.00980223, 0.00980223, 0.0163806, 854 0.0522061, 0.090285, 0.215078, 0.53902, 0.539798, 855 0.769742, 1.04208, 0.555479, 0.515533, 0.674084, 856 0.165753, 0.0704188, 0.00970061, 3.89553e-05, 2.36004e-18]; 857 858 foreach (S; AliasSeq!(float, double, real)) 859 { 860 auto f0 = (S x) => -x^^4 + 5 * x^^2 - 4; 861 auto f1 = (S x) => 10 * x - 4 * x^^3; 862 auto f2 = (S x) => 10 - 12 * x^^2; 863 S[] cs = [1.5, 1.5, 1.5, 1.5]; 864 S[] points = [-3, -1.5, 0, 1.5, 3]; 865 auto ips = flexIntervals(f0, f1, f2, cs, points, S(1.1)); 866 867 foreach (i, el; ips) 868 { 869 version(Flex_logging) scope(failure) 870 { 871 logf("at %d got %a, expected %a (%2$f)", i, el.hatArea, hats[i]); 872 logf("at %d got %a, expected %a (%2$f)", i, el.squeezeArea, sqs[i]); 873 logf("iv %s", el); 874 } 875 assert(el.hatArea.approxEqual(hats[i], 1e-5, 1e-5)); 876 assert(el.squeezeArea.approxEqual(sqs[i], 1e-5, 1e-5)); 877 } 878 } 879 } 880 881 // default flex with c=1 882 version(mir_random_test) unittest 883 { 884 import mir.ndslice.topology: member; 885 import mir.math : approxEqual; 886 import std.meta : AliasSeq; 887 static immutable hats = [1.49622e-05, 0.00227029, 0.0540631, 0.0880036, 888 0.184448, 0.752102, 1.13921, 1.10993, 1.40719, 889 0.606916, 0.249029, 0.103608, 0.119708, 0.0238081, 890 0.0238081, 0.119708, 0.103608, 0.249029, 0.606916, 891 0.547504, 0.789818, 1.10993, 1.13921, 0.752102, 892 0.184448, 0.0880036, 0.0540631, 0.00227029, 1.49622e-05]; 893 894 static immutable sqs = [5.34911e-17, 5.37841e-05, 0.0118652, 0.0738576, 895 0.17077, 0.706057, 1.04936, 1.04196, 1.29868, 896 0.55554, 0.2213, 0.0925191, 0.0495667, 0.00980223, 897 0.00980223, 0.0495667, 0.0925191, 0.2213, 0.55554, 898 0.543916, 0.768265, 1.04196, 1.04936, 0.706057, 899 0.17077, 0.0738576, 0.0118652, 5.37841e-05, 900 5.34911e-17]; 901 902 foreach (S; AliasSeq!(float, double, real)) 903 { 904 auto f0 = (S x) => -x^^4 + 5 * x^^2 - 4; 905 auto f1 = (S x) => 10 * x - 4 * x^^3; 906 auto f2 = (S x) => 10 - 12 * x^^2; 907 S[] points = [-3, -1.5, 0, 1.5, 3]; 908 S[] cs = [1.0, 1.0, 1.0, 1.0]; 909 auto ips = flexIntervals(f0, f1, f2, cs, points, S(1.1)); 910 911 foreach (i, el; ips) 912 { 913 version(Flex_logging) scope(failure) 914 { 915 logf("got %a, expected %a", el.hatArea, hats[i]); 916 logf("got %a, expected %a", el.squeezeArea, sqs[i]); 917 logf("iv %s", el); 918 } 919 assert(el.hatArea.approxEqual(hats[i], 1e-5, 1e-5)); 920 assert(el.squeezeArea.approxEqual(sqs[i], 1e-5, 1e-5)); 921 } 922 } 923 } 924 925 // default flex with custom c's 926 version(mir_random_test) unittest 927 { 928 import mir.ndslice.topology: member; 929 import mir.algorithm.iteration: equal; 930 import mir.math : approxEqual; 931 import std.meta : AliasSeq; 932 933 static immutable hats = [1.69137e-05, 0.00256095, 0.0597127, 0.0899876, 934 0.186679, 0.747491, 0.524337, 0.566407, 1.10963, 935 0.788573, 0.547394, 0.617211, 0.253546, 0.105271, 936 0.130463, 0.0249657, 0.0252439, 0.13294, 0.105682, 937 0.25466, 0.619752, 0.547819, 0.788249, 1.10933, 938 1.13829, 0.429167, 0.310853, 0.188879, 0.0919179, 939 0.0644612, 0.00278727, 1.84149e-05]; 940 941 static immutable sqs = [2.36004e-18, 4.3382e-05, 0.0103914, 0.0716524, 942 0.167594, 0.685574, 0.515237, 0.555414, 1.04203, 943 0.769447, 0.540575, 0.541969, 0.216192, 0.0906883, 944 0.0462627, 0.00980223, 0.00980223, 0.045605, 945 0.0902851, 0.215078, 0.53902, 0.539798, 0.769742, 946 1.0421, 1.05314, 0.42437, 0.294228, 0.164901, 947 0.0698581, 0.00941278, 3.71912e-05, 2.36004e-18]; 948 949 foreach (S; AliasSeq!(float, double, real)) 950 { 951 auto f0 = (S x) => -x^^4 + 5 * x^^2 - 4; 952 auto f1 = (S x) => 10 * x - 4 * x^^3; 953 auto f2 = (S x) => 10 - 12 * x^^2; 954 S[] cs = [1.3, 1.4, 1.5, 1.6]; 955 S[] points = [-3, -1.5, 0, 1.5, 3]; 956 auto ips = flexIntervals(f0, f1, f2, cs, points, S(1.1)); 957 958 assert(ips.member!`hatArea`.equal!((x, y) => approxEqual(x, y, 1e-5, 1e-5))(hats)); 959 assert(ips.member!`squeezeArea`.equal!((x, y) => approxEqual(x, y, 1e-5, 1e-5))(sqs)); 960 } 961 } 962 963 // test standard normal distribution 964 version(X86_64) version(mir_random_test) unittest 965 { 966 import mir.math.common : exp, sqrt; 967 import mir.ndslice.topology: member; 968 import mir.algorithm.iteration: equal; 969 import std.meta : AliasSeq; 970 import mir.math : approxEqual, PI; 971 static immutable hats = [1.60809, 2.23537, 0.797556, 1.23761, 1.60809]; 972 static immutable sqs = [1.52164, 1.94559, 0.776976, 1.19821, 1.52164]; 973 974 foreach (S; AliasSeq!(float, double, real)) 975 { 976 S sqrt2PI = sqrt(2 * PI); 977 auto f0 = (S x) => 1 / (exp(x * x / 2) * sqrt2PI); 978 auto f1 = (S x) => -(x / (exp(x * x / 2) * sqrt2PI)); 979 auto f2 = (S x) => (-1 + x * x) / (exp(x * x / 2) * sqrt2PI); 980 S[] cs = [1.5, 1.5, 1.5, 1.5]; 981 S[] points = [-3, -1.5, 0, 1.5, 3]; 982 auto ips = flexIntervals(f0, f1, f2, cs, points, S(1.1)); 983 984 assert(ips.member!`hatArea`.equal!((x, y) => approxEqual(x, y, 1e-5, 1e-5))(hats)); 985 assert(ips.member!`squeezeArea`.equal!((x, y) => approxEqual(x, y, 1e-5, 1e-5))(sqs)); 986 } 987 } 988 989 version(X86_64) version(mir_random_test) unittest 990 { 991 import mir.algorithm.iteration: equal; 992 import mir.math.common : log; 993 import mir.ndslice.allocation: slice; 994 import mir.ndslice.topology: member; 995 import mir.ndslice.topology: repeat; 996 import mir.math : approxEqual; 997 import std.meta : AliasSeq; 998 999 static immutable hats = [0.00648327, 0.0133705, 0.33157, 0.5, 0.5, 0.16167, 1000 0.136019, 0.0133705, 0.00648327]; 1001 1002 static immutable sqs = [0, 0.0125563, 0.274612, 0.484543, 0.484543, 1003 0.156698, 0.12444, 0.0125563, 0]; 1004 1005 version(none) 1006 foreach (S; AliasSeq!(float, real)) 1007 { 1008 auto f0 = (S x) => cast(S) log(1 - x^^4); 1009 auto f1 = (S x) => S(-4) * x^^3 / (1 - x^^4); 1010 auto f2 = (S x) => -(S(4) * x^^6 + 12 * x^^2) / (x^^8 - 2 * x^^4 + 1); 1011 S[] points = [S(-1), -0.9, -0.5, 0.5, 0.9, 1]; 1012 S[] cs = S(2).repeat(points.length - 1).slice.field; 1013 1014 auto ips = flexIntervals(f0, f1, f2, cs, points, S(1.1)); 1015 assert(ips.member!`hatArea`.equal!((x, y) => approxEqual(x, y, 1e-5, 1e-5))(hats)); 1016 assert(ips.member!`squeezeArea`.equal!((x, y) => approxEqual(x, y, 1e-5, 1e-5))(sqs)); 1017 } 1018 1019 // double behavior is different 1020 { 1021 alias S = double; 1022 1023 S[] hatsD = [0.0229267, 0.33157, 0.5, 0.5, 0.16167, 0.136019, 0.0229267]; 1024 S[] sqsD = [0, 0.274612, 0.484543, 0.484543, 0.156698, 0.12444, 0]; 1025 1026 auto f0 = (S x) => cast(S) log(1 - x^^4); 1027 auto f1 = (S x) => -S(4) * x^^3 / (1 - x^^4); 1028 auto f2 = (S x) => -(S(4) * x^^6 + 12 * x^^2) / (x^^8 - 2 * x^^4 + 1); 1029 S[] points = [S(-1), -0.9, -0.5, 0.5, 0.9, 1]; 1030 S[] cs = S(2).repeat(points.length - 1).slice.field; 1031 1032 auto ips = flexIntervals(f0, f1, f2, cs, points, S(1.1)); 1033 1034 assert(ips.member!`hatArea`.equal!((x, y) => approxEqual(x, y, 1e-5, 1e-5))(hatsD)); 1035 assert(ips.member!`squeezeArea`.equal!((x, y) => approxEqual(x, y, 1e-5, 1e-5))(sqsD)); 1036 } 1037 } 1038 1039 /** 1040 Compute inverse transformation of a T_c family given point x. 1041 Based on Table 1, column 3 of Botts et al. (2013). 1042 1043 Params: 1044 common = whether c be 0, -0.5, -1 or 1 1045 x = value to transform 1046 c = T_c family to use for the transformation 1047 1048 Returns: Flex-inversed value of x 1049 */ 1050 S flexInverse(bool common = false, S)(in S x, in S c) 1051 { 1052 static if (!common) 1053 { 1054 if (c == 0) 1055 return exp(x); 1056 if (c == S(-0.5)) 1057 return 1 / (x * x); 1058 if (c == -1) 1059 return -1 / x; 1060 if (c == 1) 1061 return x; 1062 } 1063 assert(c * x >= 0 || x >= 0); 1064 // LDC intrinsics compiles to the assembler powf which yields different results 1065 return pow(fabs(x), 1 / c); 1066 } 1067 1068 version(mir_random_test) unittest 1069 { 1070 import mir.math: E, approxEqual; 1071 import std.meta : AliasSeq; 1072 foreach (S; AliasSeq!(float, double, real)) 1073 { 1074 assert(flexInverse!(false, S)(1, 0).approxEqual(E)); 1075 1076 assert(flexInverse!(false, S)(2, 1) == 2); 1077 assert(flexInverse!(false, S)(8, 1) == 8); 1078 1079 assert(flexInverse!(false, S)(1, 1.5) == 1); 1080 assert(flexInverse!(false, S)(2, 1.5).approxEqual(1.58740)); 1081 } 1082 } 1083 1084 version(mir_random_test) unittest 1085 { 1086 import mir.math; 1087 import std.meta : AliasSeq; 1088 foreach (S; AliasSeq!(float, double, real)) 1089 { 1090 S[][] results = [ 1091 [S.nan, S.nan, S.nan, S.nan, S.nan, S.nan, S(0), 1092 0.707106781186548, 1, 1.224744871391589, 1093 1.414213562373095, 1.581138830084190, 1.73205080756887], 1094 [S.nan, S.nan, S.nan, S.nan, S.nan, S.nan, S(0), 1095 0.629960524947437, 1, 1.310370697104448, 1096 1.587401051968199, 1.842015749320193, 2.080083823051904], 1097 [-3, -2.5, -2, -1.5, -1, -0.5, S(0), 0.5, 1, 1.5, 2, 2.5, 3], 1098 [S.nan, S.nan, S.nan, S.nan, S.nan, S.nan, S(0), 1099 0.462937356143645, 1, 1.569122877964822, 1100 2.160119477784612, 2.767932947224778, 3.389492891729259], 1101 [9, 6.25, 4, 2.25, 1, 0.25, S(0), 1102 0.25, 1, 2.25, 4, 6.25, 9], 1103 [0.0497870683678639, 0.0820849986238988, 0.1353352832366127, 1104 0.2231301601484298, 0.3678794411714423, 0.6065306597126334, 1105 S(1), 1.6487212707001282, 2.7182818284590451, 4.4816890703380645, 1106 7.3890560989306504, 12.1824939607034732, 20.0855369231876679], 1107 [S(1)/S(9), 0.16, 0.25, S(4)/S(9), 1, 4, S.infinity, 4, 1108 1, S(4)/S(9), 0.25, 0.16, S(1)/S(9)], 1109 [0.295029384023820, 0.361280428054673, 0.462937356143645, 1110 0.637298718948650, 1, 2.160119477784612, S.infinity, 1111 S.nan, S.nan, S.nan, S.nan, S.nan, S.nan], 1112 [S(1)/S(3), 0.4, 0.5, S(2)/S(3), 1, 2, S.infinity, -2, 1113 -1, -S(2)/S(3), -0.5, -0.4, -S(1)/S(3)], 1114 [0.480749856769136, 0.542883523318981, 0.629960524947437, 1115 0.763142828368888, 1, 1.587401051968199, S.infinity, 1116 S.nan, S.nan, S.nan, S.nan, S.nan, S.nan], 1117 [0.577350269189626, 0.632455532033676, 0.707106781186548, 1118 0.816496580927726, 1, 1.414213562373095, S.infinity, 1119 S.nan, S.nan, S.nan, S.nan, S.nan, S.nan], 1120 ]; 1121 S[] xs = [-3, -2.5, -2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2, 2.5, 3.0]; 1122 S[] cs = [2, 1.5, 1, 0.9, 0.5, 0, -0.5, -0.9, -1, -1.5, -2]; 1123 1124 foreach (i, c; cs) 1125 { 1126 foreach (j, x; xs) 1127 { 1128 if (c * x >= 0) 1129 { 1130 S r = results[i][j]; 1131 S v = flexInverse!(false, S)(x, c); 1132 if (r.fabs == r.infinity) 1133 assert(v.fabs == v.infinity); 1134 else 1135 assert(v.approxEqual(r)); 1136 } 1137 } 1138 } 1139 } 1140 } 1141 } 1142 else 1143 { 1144 version(unittest) {} else static assert(0, "mir.ndslice is required for mir.random.flex, it can be found in 'mir-algorithm' repository."); 1145 }