The OpenD Programming Language

1 /++
2 $(H2 Linear Interpolation)
3 
4 See_also: $(REF_ALTTEXT $(TT interp1), interp1, mir, interpolate)
5 
6 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache-2.0)
7 Copyright: 2020 Ilia Ki, Kaleidic Associates Advisory Limited, Symmetry Investments
8 Authors: Ilia Ki
9 
10 Macros:
11 SUBREF = $(REF_ALTTEXT $(TT $2), $2, mir, interpolate, $1)$(NBSP)
12 T2=$(TR $(TDNW $(LREF $1)) $(TD $+))
13 +/
14 module mir.interpolate.linear;
15 
16 import core.lifetime: move; 
17 import mir.functional;
18 import mir.internal.utility;
19 import mir.interpolate;
20 import mir.math.common: fmamath;
21 import mir.ndslice.slice;
22 import mir.primitives;
23 import mir.rc.array;
24 import mir.utility: min, max;
25 import std.meta: AliasSeq, staticMap;
26 import std.traits;
27 
28 ///
29 public import mir.interpolate: atInterval;
30 
31 enum  msg_min =  "linear interpolant: minimal allowed length for the grid equals 2.";
32 enum  msg_eq =  "linear interpolant: X and Y values length should be equal.";
33 version(D_Exceptions)
34 {
35     static immutable exc_min = new Exception(msg_min);
36     static immutable exc_eq = new Exception(msg_eq);
37 }
38 
39 @fmamath:
40 
41 /++
42 Constructs multivariate linear interpolant with nodes on rectilinear grid.
43 
44 Params:
45     grid = `x` values for interpolant
46     values = `f(x)` values for interpolant
47 
48 Constraints:
49     `grid`, `values` must have the same length >= 2
50 
51 Returns: $(LREF Linear)
52 +/
53 Linear!(F, N, X) linear(F, size_t N = 1, X = F)
54     (Repeat!(N, Slice!(RCI!(immutable X))) grid, Slice!(RCI!(const F), N) values)
55 {
56     return typeof(return)(forward!grid, values.move);
57 }
58 
59 /// R -> R: Linear interpolation
60 version(mir_test)
61 @safe pure @nogc unittest
62 {
63     import mir.algorithm.iteration;
64     import mir.ndslice;
65     import mir.math.common: approxEqual;
66 
67     static immutable x = [0, 1, 2, 3, 5.00274, 7.00274, 10.0055, 20.0137, 30.0192];
68     static immutable y = [0.0011, 0.0011, 0.0030, 0.0064, 0.0144, 0.0207, 0.0261, 0.0329, 0.0356,];
69     static immutable xs = [1, 2, 3, 4.00274, 5.00274, 6.00274, 7.00274, 8.00548, 9.00548, 10.0055, 11.0055, 12.0082, 13.0082, 14.0082, 15.0082, 16.011, 17.011, 18.011, 19.011, 20.0137, 21.0137, 22.0137, 23.0137, 24.0164, 25.0164, 26.0164, 27.0164, 28.0192, 29.0192, 30.0192];
70 
71     auto interpolant = linear!double(x.rcslice!(immutable double), y.rcslice!(const double));
72 
73     static immutable data = [0.0011, 0.0030, 0.0064, 0.0104, 0.0144, 0.0176, 0.0207, 0.0225, 0.0243, 0.0261, 0.0268, 0.0274, 0.0281, 0.0288, 0.0295, 0.0302, 0.0309, 0.0316, 0.0322, 0.0329, 0.0332, 0.0335, 0.0337, 0.0340, 0.0342, 0.0345, 0.0348, 0.0350, 0.0353, 0.0356];
74 
75     assert(xs.sliced.vmap(interpolant).all!((a, b) => approxEqual(a, b, 1e-4, 1e-4))(data));
76 
77     auto d = interpolant.withDerivative(9.0);
78     auto de = interpolant.opCall!2(9.0);
79     assert(de[0 .. 2] == d);
80     assert(de[2] == 0);
81 }
82 
83 /// R^2 -> R: Bilinear interpolation
84 version(mir_test)
85 @safe pure @nogc unittest
86 {
87     import mir.math.common: approxEqual;
88     import mir.ndslice;
89     alias appreq = (a, b) => approxEqual(a, b, 10e-10, 10e-10);
90 
91     //// set test function ////
92     enum y_x0 = 2;
93     enum y_x1 = -7;
94     enum y_x0x1 = 3;
95 
96     // this function should be approximated very well
97     alias f = (x0, x1) => y_x0 * x0 + y_x1 * x1 + y_x0x1 * x0 * x1 - 11;
98 
99     ///// set interpolant ////
100     static immutable x0 = [-1.0, 2, 8, 15];
101     static immutable x1 = [-4.0, 2, 5, 10, 13];
102 
103     auto grid = cartesian(x0, x1)
104         .map!f
105         .rcslice
106         .lightConst;
107 
108     auto interpolant = 
109         linear!(double, 2)(
110             x0.rcslice!(immutable double),
111             x1.rcslice!(immutable double),
112             grid
113         );
114 
115     ///// compute test data ////
116     auto test_grid = cartesian(x0.sliced + 1.23, x1.sliced + 3.23);
117     auto real_data = test_grid.map!f;
118     auto interp_data = test_grid.vmap(interpolant);
119     ///// verify result ////
120     assert(all!appreq(interp_data, real_data));
121 
122     //// check derivatives ////
123     auto z0 = 1.23;
124     auto z1 = 3.21;
125     auto d = interpolant.withDerivative(z0, z1);
126     assert(appreq(interpolant(z0, z1), f(z0, z1)));
127     assert(appreq(d[0][0], f(z0, z1)));
128     assert(appreq(d[1][0], y_x0 + y_x0x1 * z1));
129     assert(appreq(d[0][1], y_x1 + y_x0x1 * z0));
130     assert(appreq(d[1][1], y_x0x1));
131 }
132 
133 /// R^3 -> R: Trilinear interpolation
134 version(mir_test)
135 @safe pure unittest
136 {
137     import mir.math.common: approxEqual;
138     import mir.ndslice;
139     alias appreq = (a, b) => approxEqual(a, b, 10e-10, 10e-10);
140 
141     ///// set test function ////
142     enum y_x0 = 2;
143     enum y_x1 = -7;
144     enum y_x2 = 5;
145     enum y_x0x1 = 10;
146     enum y_x0x1x2 = 3;
147 
148     // this function should be approximated very well
149     static auto f(double x0, double x1, double x2)
150     {
151         return y_x0 * x0 + y_x1 * x1 + y_x2 * x2 + y_x0x1 * x0 * x1 + y_x0x1x2 * x0 * x1 * x2 - 11;
152     }
153 
154     ///// set interpolant ////
155     static immutable x0 = [-1.0, 2, 8, 15];
156     static immutable x1 = [-4.0, 2, 5, 10, 13];
157     static immutable x2 = [3, 3.7, 5];
158     auto grid = cartesian(x0, x1, x2)
159         .map!f
160         .as!(const double)
161         .rcslice;
162 
163     auto interpolant = linear!(double, 3)(
164             x0.rcslice!(immutable double),
165             x1.rcslice!(immutable double),
166             x2.rcslice!(immutable double),
167             grid);
168 
169     ///// compute test data ////
170     auto test_grid = cartesian(x0.sliced + 1.23, x1.sliced + 3.23, x2.sliced - 3);
171     auto real_data = test_grid.map!f;
172     auto interp_data = test_grid.vmap(interpolant);
173     ///// verify result ////
174     assert(all!appreq(interp_data, real_data));
175 
176     //// check derivatives ////
177     auto z0 = 1.23;
178     auto z1 = 3.21;
179     auto z2 = 4;
180     auto d = interpolant.withDerivative(z0, z1, z2);
181     assert(appreq(interpolant(z0, z1, z2), f(z0, z1, z2)));
182     assert(appreq(d[0][0][0], f(z0, z1, z2)));
183     assert(appreq(d[1][0][0], y_x0 + y_x0x1 * z1 + y_x0x1x2 * z1 * z2));
184     assert(appreq(d[0][1][0], y_x1 + y_x0x1 * z0 + y_x0x1x2 * z0 * z2));
185     assert(appreq(d[1][1][0], y_x0x1 + y_x0x1x2 * z2));
186     assert(appreq(d[1][1][1], y_x0x1x2));
187 }
188 
189 /++
190 Multivariate linear interpolant with nodes on rectilinear grid.
191 +/
192 struct Linear(F, size_t N = 1, X = F)
193     if (N && N <= 6)
194 {
195     /// Aligned buffer allocated with `mir.internal.memory`. $(RED For internal use.)
196     Slice!(RCI!(const F), N) _data;
197     /// Grid iterators. $(RED For internal use.)
198     Repeat!(N, RCI!(immutable X)) _grid;
199 
200 @fmamath extern(D):
201 
202     bool opEquals()(auto ref scope const typeof(this) rhs) scope const @trusted pure nothrow @nogc
203     {
204         if (rhs._data != this._data)
205             return false;
206         static foreach (d; 0 .. N)
207             if (gridScopeView!d != rhs.gridScopeView!d)
208                 return false;
209         return true;
210     }
211 
212     /++
213     +/
214     this(Repeat!(N, Slice!(RCI!(immutable X))) grid, Slice!(RCI!(const F), N) data) @safe @nogc
215     {
216         foreach(i, ref x; grid)
217         {
218             if (x.length < 2)
219             {
220                 version(D_Exceptions) { import mir.exception : toMutable; throw exc_min.toMutable; }
221                 else assert(0, msg_min);
222             }
223             if (x.length != data._lengths[i])
224             {
225                 version(D_Exceptions) { import mir.exception : toMutable; throw exc_eq.toMutable; }
226                 else assert(0, msg_eq);
227             }
228             _grid[i] = x._iterator.move;
229         }
230         _data = data.move;
231     }
232 
233 @trusted:
234 
235     ///
236     Linear lightConst()() const @property { return *cast(Linear*)&this; }
237 
238     ///
239     Slice!(RCI!(immutable X)) grid(size_t dimension = 0)() return scope const @property
240         if (dimension < N)
241     {
242         return _grid[dimension].lightConst.sliced(_data._lengths[dimension]);
243     }
244 
245     ///
246     immutable(X)[] gridScopeView(size_t dimension = 0)() return scope const @property @trusted
247         if (dimension < N)
248     {
249         return _grid[dimension]._iterator[0 .. _data._lengths[dimension]];
250     }
251 
252     /++
253     Returns: intervals count.
254     +/
255     size_t intervalCount(size_t dimension = 0)() scope const @property
256     {
257         assert(_data._lengths[dimension] > 1);
258         return _data._lengths[dimension] - 1;
259     }
260 
261     ///
262     size_t[N] gridShape()() scope const @property
263     {
264         return _data.shape;
265     }
266 
267     ///
268     enum uint derivativeOrder = 1;
269 
270     ///
271     enum uint dimensionCount = N;
272 
273     ///
274     template opCall(uint derivative = 0)
275     {
276         /++
277         `(x)` operator.
278         Complexity:
279             `O(log(grid.length))`
280         +/
281         auto opCall(X...)(const X xs) scope const @trusted
282             if (X.length == N)
283         {
284             static if (derivative > derivativeOrder)
285             {
286                 auto res = this.opCall!derivativeOrder(xs);
287                 typeof(res[0])[derivative + 1] ret = 0;
288                 ret[0 .. derivativeOrder + 1] = res;
289                 return ret;
290             }
291             else
292             {
293                 import mir.functional: AliasCall;
294                 import mir.ndslice.topology: iota;
295                 alias Kernel = AliasCall!(LinearKernel!F, "opCall", derivative);
296 
297                 size_t[N] indices;
298                 Kernel[N] kernels;
299 
300                 enum rp2d = derivative;
301 
302                 foreach(i; Iota!N)
303                 {
304                     static if (isInterval!(typeof(xs[i])))
305                     {
306                         indices[i] = xs[i][1];
307                         auto x = xs[i][0];
308                     }
309                     else
310                     { 
311                         alias x = xs[i];
312                         indices[i] = this.findInterval!i(x);
313                     }
314                     kernels[i] = LinearKernel!F(_grid[i][indices[i]], _grid[i][indices[i] + 1], x);
315                 }
316 
317                 align(64) F[2 ^^ N][derivative + 1] local;
318                 immutable strides = _data._lengths.iota.strides;
319 
320                 void load(sizediff_t i)(F* from, F* to)
321                 {
322                     version(LDC) pragma(inline, true);
323                     static if (i == -1)
324                     {
325                         *to = *from;
326                     }
327                     else
328                     {
329                         from += strides[i] * indices[i];
330                         load!(i - 1)(from, to);
331                         from += strides[i];
332                         enum s = 2 ^^ (N - 1 - i);
333                         to += s;
334                         load!(i - 1)(from, to);
335                     }
336                 }
337 
338                 load!(N - 1)(cast(F*) _data.ptr, cast(F*)local[0].ptr);
339 
340                 foreach(i; Iota!N)
341                 {
342                     enum P = 2 ^^ (N - 1 - i);
343                     enum L = 2 ^^ (N - i * (1 - rp2d)) / 2;
344                     vectorize(kernels[i], local[0][0 * L .. 1 * L], local[0][1 * L .. 2 * L], *cast(F[L][2 ^^ rp2d]*)local[rp2d].ptr);
345                     static if (rp2d == 1)
346                         shuffle3!1(local[1][0 .. L], local[1][L .. 2 * L], local[0][0 .. L], local[0][L .. 2 * L]);
347                     static if (i + 1 == N)
348                         return *cast(SplineReturnType!(F, N, 2 ^^ rp2d)*) local[0].ptr;
349                 }
350             }
351         }
352     }
353 
354     ///
355     alias withDerivative = opCall!1;
356 }
357 
358 ///
359 struct LinearKernel(X)
360 {
361     X step = 0;
362     X w0 = 0;
363     X w1 = 0;
364 
365 @fmamath:
366 
367     ///
368     auto lightConst()() const @property
369     {
370         return LinearKernel!X(step, w0, w1);
371     }
372 
373     ///
374     auto lightImmutable()() immutable @property
375     {
376         return LinearKernel!X(step, w0, w1);
377     }
378 
379     ///
380     this()(X x0, X x1, X x)
381     {
382         step = x1 - x0;
383         auto c0 = x - x0;
384         auto c1 = x1 - x;
385         w0 = c0 / step;
386         w1 = c1 / step;
387     }
388 
389     ///
390     template opCall(uint derivative = 0)
391         // if (derivative <= 1)
392     {
393         ///
394         auto opCall(Y)(const Y y0, const Y y1)
395             if (__traits(isFloating, Y))
396         {
397             auto r0 = y0 * w1;
398             auto r1 = y1 * w0;
399             auto y = r0 + r1;
400             static if (derivative)
401             {
402                 auto diff = y1 - y0;
403                 Y[derivative + 1] ret = 0;
404                 ret[0] = y;
405                 ret[1] = diff / step;
406                 return ret;
407             }
408             else
409             {
410                 return y;
411             }
412         }
413 
414         static if (derivative)
415         auto opCall(Y, size_t N)(scope ref const Y[N] y0, scope ref const Y[N] y1)
416         {
417             Y[N][derivative + 1] ret = void;
418             Y[derivative + 1][N] temp = void;
419 
420             static foreach(i; 0 .. N)
421                 temp[i] = this.opCall!derivative(y0[i], y1[i]);
422             static foreach(i; 0 .. N)
423             static foreach(d; 0 .. derivative + 1)
424                 ret[d][i] = temp[i][d];
425             return ret;
426         }
427     }
428 
429     ///
430     alias withDerivative = opCall!1;
431 }
432 
433 /++
434 Interpolator used for non-rectiliner trapezoid-like greeds.
435 Params:
436     grid = rc-array of interpolation grid
437     data = rc-array of interpolator-like structures
438 +/
439 auto metaLinear(X, T)(RCArray!(immutable X) grid, RCArray!(const T) data)
440 {
441     import core.lifetime: move;
442     return MetaLinear!(T, X)(grid.move, data.move);
443 }
444 
445 /// ditto
446 struct MetaLinear(T, X)
447     // if (T.derivativeOrder >= 1)
448 {
449     import mir.interpolate.utility: DeepType;
450     // alias ElementInterpolator = Linear!(F, N, X);
451 
452     ///
453     RCArray!(immutable X) grid;
454     ///
455     RCArray!(const T) data;
456 
457     ///
458     this(RCArray!(immutable X) grid, RCArray!(const T) data)
459     {
460         import core.lifetime: move;
461 
462         if (grid.length < 2)
463         {
464             version(D_Exceptions) { import mir.exception : toMutable; throw exc_min.toMutable; }
465             else assert(0, msg_min);
466         }
467         if (grid.length != data.length)
468         {
469             version(D_Exceptions) { import mir.exception : toMutable; throw exc_eq.toMutable; }
470             else assert(0, msg_eq);
471         }
472 
473         this.grid = grid.move;
474         this.data = data.move;
475     }
476 
477     ///
478     MetaLinear lightConst()() const @property { return *cast(MetaLinear*)&this; }
479 
480     ///
481     immutable(X)[] gridScopeView(size_t dimension = 0)() return scope const @property @trusted
482         if (dimension == 0)
483     {
484         return grid[];
485     }
486 
487     /++
488     Returns: intervals count.
489     +/
490     size_t intervalCount(size_t dimension = 0)() scope const @property
491         if (dimension == 0)
492     {
493         assert(data.length > 1);
494         return data.length - 1;
495     }
496 
497     ///
498     enum uint derivativeOrder = 1;
499 
500     static if (__traits(compiles, {enum N = T.dimensionCount;}))
501     ///
502     enum uint dimensionCount = T.dimensionCount + 1;
503 
504     ///
505     template opCall(uint derivative = 0)
506     {
507         /++
508         `(x)` operator.
509         Complexity:
510             `O(log(grid.length))`
511         +/
512         auto opCall(X...)(const X xs) scope const @trusted
513             // if (X.length == dimensionCount)
514         {
515             static if (isInterval!(typeof(xs[0])))
516             {
517                 size_t index = xs[0][1];
518                 auto x = xs[0][0];
519             }
520             else
521             { 
522                 alias x = xs[0];
523                 size_t index = this.findInterval(x);
524             }
525             auto lhs = data[index + 0].opCall!derivative(xs[1 .. $]);
526             auto rhs = data[index + 1].opCall!derivative(xs[1 .. $]);
527             alias E = typeof(lhs);
528             alias F = DeepType!E;
529             auto kernel = LinearKernel!F(grid[index], grid[index + 1], x);
530             return kernel.opCall!derivative(lhs, rhs);
531         }
532     }
533 
534     ///
535     alias withDerivative = opCall!1;
536 
537     ///
538     alias withTwoDerivatives = opCall!2;
539 }
540 
541 /// 2D trapezoid-like (not rectilinear) linear interpolation
542 version(mir_test)
543 unittest
544 {
545     auto x = [
546         [0.0, 1, 2, 3, 5],
547         [-4.0, 3, 4],
548         [0.0, 10],
549     ];
550     auto y = [
551         [4.0, 0, 9, 23, 40],
552         [9.0, 0, 3],
553         [4.0, 40],
554     ];
555 
556     auto g = [7.0, 10, 15];
557 
558     import mir.rc.array: RCArray;
559     import mir.ndslice.allocation: rcslice;
560 
561     auto d = RCArray!(Linear!double)(3);
562 
563     foreach (i; 0 .. x.length)
564         d[i] = linear!double(x[i].rcslice!(immutable double), y[i].rcslice!(const double));
565 
566     auto trapezoidInterpolator = metaLinear(g.rcarray!(immutable double), d.lightConst);
567 
568     auto val = trapezoidInterpolator(9.0, 1.8);
569     auto valWithDerivative = trapezoidInterpolator.withDerivative(9.0, 1.8);
570 }
571 
572 version(mir_test)
573 unittest
574 {
575     import mir.math.common: approxEqual;
576     import mir.ndslice;
577     alias appreq = (a, b) => approxEqual(a, b, 10e-10, 10e-10);
578 
579     //// set test function ////
580     enum y_x0 = 2;
581     enum y_x1 = -7;
582     enum y_x0x1 = 3;
583 
584     // this function should be approximated very well
585     alias f = (x0, x1) => y_x0 * x0 + y_x1 * x1 + y_x0x1 * x0 * x1 - 11;
586 
587     ///// set interpolant ////
588     static immutable x0 = [-1.0, 2, 8, 15];
589     static immutable x1 = [-4.0, 2, 5, 10, 13];
590 
591     auto grid = cartesian(x0, x1)
592         .map!f
593         .rcslice
594         .lightConst;
595     
596     auto int0 = linear!double(x1.rcslice!(immutable double), grid[0]);
597     auto int1 = linear!double(x1.rcslice!(immutable double), grid[1]);
598     auto int2 = linear!double(x1.rcslice!(immutable double), grid[2]);
599     auto int3 = linear!double(x1.rcslice!(immutable double), grid[3]);
600 
601     auto interpolant = metaLinear(x0.rcarray!(immutable double), rcarray(int0, int1, int2, int3).lightConst);
602 
603     ///// compute test data ////
604     auto test_grid = cartesian(x0.sliced + 1.23, x1.sliced + 3.23);
605     auto real_data = test_grid.map!f;
606     auto interp_data = test_grid.vmap(interpolant);
607     ///// verify result ////
608     assert(all!appreq(interp_data, real_data));
609 
610     //// check derivatives ////
611     auto z0 = 1.23;
612     auto z1 = 3.21;
613     auto d = interpolant.withDerivative(z0, z1);
614     assert(appreq(interpolant(z0, z1), f(z0, z1)));
615     assert(appreq(d[0][0], f(z0, z1)));
616     assert(appreq(d[1][0], y_x0 + y_x0x1 * z1));
617     assert(appreq(d[0][1], y_x1 + y_x0x1 * z0));
618     assert(appreq(d[1][1], y_x0x1));
619 }