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 Ilya Yaroshenko, Kaleidic Associates Advisory Limited, Symmetry Investments
8 Authors: Ilya Yaroshenko
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: optmath;
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 public import mir.interpolate: atInterval;
28 
29 @optmath:
30 
31 /++
32 Constructs multivariate linear interpolant with nodes on rectilinear grid.
33 
34 Params:
35     grid = `x` values for interpolant
36     values = `f(x)` values for interpolant
37 
38 Constraints:
39     `grid`, `values` must have the same length >= 2
40 
41 Returns: $(LREF Linear)
42 +/
43 Linear!(F, N, X) linear(F, size_t N = 1, X = F)
44     (Repeat!(N, Slice!(RCI!(immutable X))) grid, Slice!(RCI!(const F), N) values)
45 {
46     return typeof(return)(forward!grid, values.move);
47 }
48 
49 /// R -> R: Linear interpolation
50 version(mir_test)
51 @safe pure @nogc unittest
52 {
53     import mir.algorithm.iteration;
54     import mir.ndslice;
55     import mir.math.common: approxEqual;
56 
57     static immutable x = [0, 1, 2, 3, 5.00274, 7.00274, 10.0055, 20.0137, 30.0192];
58     static immutable y = [0.0011, 0.0011, 0.0030, 0.0064, 0.0144, 0.0207, 0.0261, 0.0329, 0.0356,];
59     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];
60 
61     auto interpolant = linear!double(x.rcslice!(immutable double), y.rcslice!(const double));
62 
63     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];
64 
65     assert(xs.sliced.vmap(interpolant).all!((a, b) => approxEqual(a, b, 1e-4, 1e-4))(data));
66 }
67 
68 /// R^2 -> R: Bilinear interpolation
69 version(mir_test)
70 @safe pure @nogc unittest
71 {
72     import mir.math.common: approxEqual;
73     import mir.ndslice;
74     alias appreq = (a, b) => approxEqual(a, b, 10e-10, 10e-10);
75 
76     //// set test function ////
77     enum y_x0 = 2;
78     enum y_x1 = -7;
79     enum y_x0x1 = 3;
80 
81     // this function should be approximated very well
82     alias f = (x0, x1) => y_x0 * x0 + y_x1 * x1 + y_x0x1 * x0 * x1 - 11;
83 
84     ///// set interpolant ////
85     static immutable x0 = [-1.0, 2, 8, 15];
86     static immutable x1 = [-4.0, 2, 5, 10, 13];
87 
88     auto grid = cartesian(x0, x1)
89         .map!f
90         .rcslice
91         .lightConst;
92 
93     auto interpolant = 
94         linear!(double, 2)(
95             x0.rcslice!(immutable double),
96             x1.rcslice!(immutable double),
97             grid
98         );
99 
100     ///// compute test data ////
101     auto test_grid = cartesian(x0.sliced + 1.23, x1.sliced + 3.23);
102     auto real_data = test_grid.map!f;
103     auto interp_data = test_grid.vmap(interpolant);
104     ///// verify result ////
105     assert(all!appreq(interp_data, real_data));
106 
107     //// check derivatives ////
108     auto z0 = 1.23;
109     auto z1 = 3.21;
110     auto d = interpolant.withDerivative(z0, z1);
111     assert(appreq(interpolant(z0, z1), f(z0, z1)));
112     assert(appreq(d[0][0], f(z0, z1)));
113     assert(appreq(d[1][0], y_x0 + y_x0x1 * z1));
114     assert(appreq(d[0][1], y_x1 + y_x0x1 * z0));
115     assert(appreq(d[1][1], y_x0x1));
116 }
117 
118 /// R^3 -> R: Trilinear interpolation
119 version(mir_test)
120 @safe pure unittest
121 {
122     import mir.math.common: approxEqual;
123     import mir.ndslice;
124     alias appreq = (a, b) => approxEqual(a, b, 10e-10, 10e-10);
125 
126     ///// set test function ////
127     enum y_x0 = 2;
128     enum y_x1 = -7;
129     enum y_x2 = 5;
130     enum y_x0x1 = 10;
131     enum y_x0x1x2 = 3;
132 
133     // this function should be approximated very well
134     static auto f(double x0, double x1, double x2)
135     {
136         return y_x0 * x0 + y_x1 * x1 + y_x2 * x2 + y_x0x1 * x0 * x1 + y_x0x1x2 * x0 * x1 * x2 - 11;
137     }
138 
139     ///// set interpolant ////
140     static immutable x0 = [-1.0, 2, 8, 15];
141     static immutable x1 = [-4.0, 2, 5, 10, 13];
142     static immutable x2 = [3, 3.7, 5];
143     auto grid = cartesian(x0, x1, x2)
144         .map!f
145         .as!(const double)
146         .rcslice;
147 
148     auto interpolant = linear!(double, 3)(
149             x0.rcslice!(immutable double),
150             x1.rcslice!(immutable double),
151             x2.rcslice!(immutable double),
152             grid);
153 
154     ///// compute test data ////
155     auto test_grid = cartesian(x0.sliced + 1.23, x1.sliced + 3.23, x2.sliced - 3);
156     auto real_data = test_grid.map!f;
157     auto interp_data = test_grid.vmap(interpolant);
158     ///// verify result ////
159     assert(all!appreq(interp_data, real_data));
160 
161     //// check derivatives ////
162     auto z0 = 1.23;
163     auto z1 = 3.21;
164     auto z2 = 4;
165     auto d = interpolant.withDerivative(z0, z1, z2);
166     assert(appreq(interpolant(z0, z1, z2), f(z0, z1, z2)));
167     assert(appreq(d[0][0][0], f(z0, z1, z2)));
168     assert(appreq(d[1][0][0], y_x0 + y_x0x1 * z1 + y_x0x1x2 * z1 * z2));
169     assert(appreq(d[0][1][0], y_x1 + y_x0x1 * z0 + y_x0x1x2 * z0 * z2));
170     assert(appreq(d[1][1][0], y_x0x1 + y_x0x1x2 * z2));
171     assert(appreq(d[1][1][1], y_x0x1x2));
172 }
173 
174 /++
175 Multivariate linear interpolant with nodes on rectilinear grid.
176 +/
177 struct Linear(F, size_t N = 1, X = F)
178     if (N && N <= 6)
179 {
180     /// Aligned buffer allocated with `mir.internal.memory`. $(RED For internal use.)
181     Slice!(RCI!(const F), N) _data;
182     /// Grid iterators. $(RED For internal use.)
183     Repeat!(N, RCI!(immutable X)) _grid;
184 
185 @optmath extern(D):
186 
187     /++
188     +/
189     this(Repeat!(N, Slice!(RCI!(immutable X))) grid, Slice!(RCI!(const F), N) data) @safe @nogc
190     {
191         enum  msg_min =  "linear interpolant: minimal allowed length for the grid equals 2.";
192         enum  msg_eq =  "linear interpolant: X and Y values length should be equal.";
193         version(D_Exceptions)
194         {
195             static immutable exc_min = new Exception(msg_min);
196             static immutable exc_eq = new Exception(msg_eq);
197         }
198         foreach(i, ref x; grid)
199         {
200             if (x.length < 2)
201             {
202                 version(D_Exceptions) throw exc_min;
203                 else assert(0, msg_min);
204             }
205             if (x.length != data._lengths[i])
206             {
207                 version(D_Exceptions) throw exc_eq;
208                 else assert(0, msg_eq);
209             }
210             _grid[i] = x._iterator.move;
211         }
212         _data = data.move;
213     }
214 
215 @trusted:
216 
217     ///
218     Linear lightConst()() const @property { return *cast(Linear*)&this; }
219 
220     ///
221     Slice!(RCI!(immutable X)) grid(size_t dimension = 0)() scope return const @property
222         if (dimension < N)
223     {
224         return _grid[dimension].lightConst.sliced(_data._lengths[dimension]);
225     }
226 
227     ///
228     immutable(X)[] gridScopeView(size_t dimension = 0)() scope return const @property @trusted
229         if (dimension < N)
230     {
231         return _grid[dimension]._iterator[0 .. _data._lengths[dimension]];
232     }
233 
234     /++
235     Returns: intervals count.
236     +/
237     size_t intervalCount(size_t dimension = 0)() scope const @property
238     {
239         assert(_data._lengths[dimension] > 1);
240         return _data._lengths[dimension] - 1;
241     }
242 
243     ///
244     size_t[N] gridShape()() scope const @property
245     {
246         return _data.shape;
247     }
248 
249     ///
250     enum uint derivativeOrder = 1;
251 
252     ///
253     template opCall(uint derivative = 0)
254         if (derivative <= derivativeOrder)
255     {
256         /++
257         `(x)` operator.
258         Complexity:
259             `O(log(grid.length))`
260         +/
261         auto opCall(X...)(in X xs) scope const @trusted
262             if (X.length == N)
263         {
264             import mir.functional: AliasCall;
265             import mir.ndslice.topology: iota;
266             alias Kernel = AliasCall!(LinearKernel!F, "opCall", derivative);
267 
268             size_t[N] indices;
269             Kernel[N] kernels;
270 
271             enum rp2d = derivative;
272 
273             foreach(i; Iota!N)
274             {
275                 static if (isInterval!(typeof(xs[i])))
276                 {
277                     indices[i] = xs[i][1];
278                     auto x = xs[i][0];
279                 }
280                 else
281                 { 
282                     alias x = xs[i];
283                     indices[i] = this.findInterval!i(x);
284                 }
285                 kernels[i] = LinearKernel!F(_grid[i][indices[i]], _grid[i][indices[i] + 1], x);
286             }
287 
288             align(64) F[2 ^^ N][derivative + 1] local;
289             immutable strides = _data._lengths.iota.strides;
290 
291             void load(sizediff_t i)(F* from, F* to)
292             {
293                 version(LDC) pragma(inline, true);
294                 static if (i == -1)
295                 {
296                     *to = *from;
297                 }
298                 else
299                 {
300                     from += strides[i] * indices[i];
301                     load!(i - 1)(from, to);
302                     from += strides[i];
303                     enum s = 2 ^^ (N - 1 - i);
304                     to += s;
305                     load!(i - 1)(from, to);
306                 }
307             }
308 
309             load!(N - 1)(cast(F*) _data.ptr, cast(F*)local[0].ptr);
310 
311             foreach(i; Iota!N)
312             {
313                 enum P = 2 ^^ (N - 1 - i);
314                 enum L = 2 ^^ (N - i * (1 - rp2d)) / 2;
315                 vectorize(kernels[i], local[0][0 * L .. 1 * L], local[0][1 * L .. 2 * L], *cast(F[L][2 ^^ rp2d]*)local[rp2d].ptr);
316                 static if (rp2d == 1)
317                     shuffle3!1(local[1][0 .. L], local[1][L .. 2 * L], local[0][0 .. L], local[0][L .. 2 * L]);
318                 static if (i + 1 == N)
319                     return *cast(SplineReturnType!(F, N, 2 ^^ rp2d)*) local[0].ptr;
320             }
321         }
322     }
323 
324     ///
325     alias withDerivative = opCall!1;
326 }
327 
328 ///
329 struct LinearKernel(X)
330 {
331     X step = 0;
332     X w0 = 0;
333     X w1 = 0;
334 
335     ///
336     auto lightConst()() const @property
337     {
338         return LinearKernel!X(step, w0, w1);
339     }
340 
341     ///
342     auto lightImmutable()() immutable @property
343     {
344         return LinearKernel!X(step, w0, w1);
345     }
346 
347     ///
348     this()(X x0, X x1, X x)
349     {
350         step = x1 - x0;
351         auto c0 = x - x0;
352         auto c1 = x1 - x;
353         w0 = c0 / step;
354         w1 = c1 / step;
355     }
356 
357     ///
358     template opCall(uint derivative = 0)
359         if (derivative <= 1)
360     {
361         ///
362         auto opCall(Y)(in Y y0, in Y y1)
363         {
364             auto r0 = y0 * w1;
365             auto r1 = y1 * w0;
366             auto y = r0 + r1;
367             static if (derivative)
368             {
369                 auto diff = y1 - y0;
370                 Y[derivative + 1] ret = 0;
371                 ret[0] = y;
372                 ret[1] = diff / step;
373                 return ret;
374             }
375             else
376             {
377                 return y;
378             }
379         }
380     }
381 
382     ///
383     alias withDerivative = opCall!1;
384 }