The OpenD Programming Language

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 }