1 /++
2 $(H2 Lagrange Barycentric 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.polynomial;
15 
16 public import mir.interpolate: atInterval;
17 import core.lifetime: move;
18 import mir.functional: RefTuple;
19 import mir.internal.utility : isFloatingPoint, Iota;
20 import mir.interpolate: findInterval;
21 import mir.math.common;
22 import mir.math.numeric;
23 import mir.math.sum;
24 import mir.ndslice.slice;
25 import mir.ndslice.topology: diff, map, member, iota, triplets;
26 import mir.rc.array;
27 import mir.utility: min, swap;
28 import std.traits: Unqual;
29 
30 @optmath:
31 
32 ///
33 version(mir_test)
34 @safe pure unittest
35 {
36     import mir.algorithm.iteration: all;
37     import mir.math;
38     import mir.ndslice;
39 
40     auto f(double x) { return (x-2) * (x-5) * (x-9); }
41     auto fd(double x) { return (x-5) * (x-9) + (x-2) * (x-5) + (x-2) * (x-9); }
42     auto fd2(double x) { return (x-5) + (x-9) + (x-2) + (x-5) + (x-2) + (x-9); }
43     double[2] fWithDerivative(double x) { return [f(x), fd(x)]; }
44     double[3] fWithTwoDerivatives(double x) { return [f(x), fd(x), fd2(x)]; }
45 
46     // 
47     auto x = [0.0, 3, 5, 10];
48     auto y = x.map!f.slice.field;
49     // `!2` adds first two derivatives: f, f', and f''
50     // default parameter is 0
51     auto l = x.lagrange!2(y);
52 
53     foreach(test; x ~ [2.0, 5, 9] ~ [double(PI), double(E), 10, 100, 1e3])
54     foreach(scale; [-1, +1, 1 + double.epsilon, 1 + double.epsilon.sqrt])
55     foreach(shift; [0, double.min_normal/2, double.min_normal*2, double.epsilon, double.epsilon.sqrt])
56     {
57         auto testX = test * scale + shift;
58 
59         auto lx = l(testX);
60         auto l_ldx = l.withDerivative(testX);
61         auto l_ld_ld2x = l.withTwoDerivatives(testX);
62 
63         assert(lx.approxEqual(f(testX)));
64         assert(l_ldx[].all!approxEqual(fWithDerivative(testX)[]));
65         assert(l_ld_ld2x[].all!approxEqual(fWithTwoDerivatives(testX)[]));
66     }
67 }
68 
69 /++
70 Constructs barycentric lagrange interpolant.
71 +/
72 Lagrange!(T, maxDerivative) lagrange(uint maxDerivative = 0, T, X)(scope const X[] x, scope const T[] y)
73     if (maxDerivative < 16)
74 {
75     import mir.ndslice.allocation: rcslice;
76     return x.rcslice!(immutable X).lagrange!maxDerivative(y.sliced);
77 }
78 
79 /// ditto
80 Lagrange!(Unqual!(Slice!(Iterator, 1, kind).DeepElement), maxDerivative, X)
81     lagrange(uint maxDerivative = 0, X, Iterator, SliceKind kind)(Slice!(RCI!(immutable X)) x, Slice!(Iterator, 1, kind) y) @trusted
82     if (maxDerivative < 16)
83 {
84     alias T = Unqual!(Slice!(Iterator, 1, kind).DeepElement);
85     return Lagrange!(T, maxDerivative)(x.move, rcarray!T(y));
86 }
87 
88 /++
89 +/
90 struct Lagrange(T, uint maxAdditionalFunctions = 0, X = T)
91     if (isFloatingPoint!T && maxAdditionalFunctions < 16)
92 {
93     /// $(RED for internal use only.)
94     Slice!(RCI!(immutable X)) _grid;
95     /// $(RED for internal use only.)
96     RCArray!(immutable T) _inversedBarycentricWeights;
97     /// $(RED for internal use only.)
98     RCArray!T[maxAdditionalFunctions + 1] _normalizedValues;
99     /// $(RED for internal use only.)
100     T[maxAdditionalFunctions + 1] _asums;
101 
102 @optmath @safe pure @nogc extern(D):
103 
104     ///
105     enum uint derivativeOrder = maxAdditionalFunctions;
106 
107     /++
108     Complexity: `O(N)`
109     +/
110     pragma(inline, false)
111     this(Slice!(RCI!(immutable X)) grid, RCArray!T values, RCArray!(immutable T) inversedBarycentricWeights)
112     {
113         import mir.algorithm.iteration: all;
114         assert(grid.length > 1);
115         assert(grid.length == values.length);
116         assert(grid.length == inversedBarycentricWeights.length);
117         enum msg = "Lagrange: grid points are too close or have not finite values.";
118         version(D_Exceptions)
119         {
120             enum T md = T.min_normal / T.epsilon;
121             static immutable exc = new Exception(msg);
122             if (!grid.lightScope.diff.all!(a => md <= a && a <= T.max))
123                 throw exc;
124         }
125         swap(_grid, grid);
126         swap(_inversedBarycentricWeights, inversedBarycentricWeights);
127         swap(_normalizedValues[0], values);
128         auto w = _inversedBarycentricWeights[].sliced;
129         foreach (_; Iota!maxAdditionalFunctions)
130         {
131             auto y = _normalizedValues[_][].sliced;
132             static if (_ == 0)
133                 _asums[_] = y.asumNorm;
134             _normalizedValues[_ + 1] = RCArray!T(_grid.length, true, false);
135             auto d = _normalizedValues[_ + 1][].sliced;
136             polynomialDerivativeValues(d, _grid.lightScope, y, w);
137             _asums[_ + 1] = d.asumNorm * _asums[_];
138         }
139     }
140 
141     /++
142     Complexity: `O(N^^2)`
143     +/
144     this(Slice!(RCI!(immutable X)) grid, RCArray!T values)
145     {
146         import core.lifetime: move; 
147         auto weights = grid.lightScope.inversedBarycentricWeights;
148         this(grid.move, values.move, weights.move);
149     }
150 
151 scope const:
152 
153     ///
154     Lagrange lightConst()() const @property @trusted { return *cast(Lagrange*)&this; }
155     ///
156     Lagrange lightImmutable()() immutable @property @trusted { return *cast(Lagrange*)&this; }
157 
158     @property
159     {
160         ///
161         ref const(Slice!(RCI!(immutable X))) grid() { return _grid; }
162         ///
163         immutable(X)[] gridScopeView() scope return const @property @trusted { return _grid.lightScope.field; }
164         ///
165         ref const(RCArray!(immutable T)) inversedBarycentricWeights() { return _inversedBarycentricWeights; }
166         ///
167         ref const(RCArray!T)[maxAdditionalFunctions + 1] normalizedValues() { return _normalizedValues; }
168         ///
169         ref const(T)[maxAdditionalFunctions + 1] asums() { return _asums; }
170         ///
171         size_t intervalCount(size_t dimension = 0)()
172         {
173             assert(_grid.length > 1);
174             return _grid.length - 1;
175         }
176     }
177 
178     template opCall(uint derivative = 0)
179         if (derivative <= maxAdditionalFunctions)
180     {
181         ///
182         pragma(inline, false)
183         auto opCall(T x)
184         {
185             return opCall!derivative(RefTuple!(size_t, T)(this.findInterval(x), x));
186         }
187 
188         ///
189         pragma(inline, false)
190         auto opCall(RefTuple!(size_t, T) tuple)
191         {
192 
193             const x = tuple[1];
194             const idx = tuple[0];
195             const inversedBarycentricWeights = _inversedBarycentricWeights[].sliced;
196             Slice!(const(T)*)[derivative + 1] values;
197             foreach (i; Iota!(derivative + 1))
198                 values[i] = _normalizedValues[i][].sliced;
199             const T[2] pd = [
200                 T(x - _grid[idx + 0]).fabs,
201                 T(x - _grid[idx + 1]).fabs,
202             ];
203             ptrdiff_t fastIndex =
204                 pd[0] < T.min_normal ? idx + 0 :
205                 pd[1] < T.min_normal ? idx + 1 :
206                 -1;
207             if (fastIndex >= 0)
208             {
209                 static if (derivative == 0)
210                 {
211                     return values[0][fastIndex] * _asums[0];
212                 }
213                 else
214                 {
215                     T[derivative + 1] ret = 0;
216                     foreach (_; Iota!(derivative + 1))
217                         ret[_] = values[_][fastIndex] * _asums[_];
218                     return ret;
219                 }
220             }
221             T d = 0;
222             T[derivative + 1] n = 0;
223             foreach (i; 0 .. _grid.length)
224             {
225                 T w = 1 / (inversedBarycentricWeights[i] * (x - _grid[i]));
226                 d += w;
227                 foreach (_; Iota!(derivative + 1))
228                     n[_] += w * values[_][i];
229             }
230             foreach (_; Iota!(derivative + 1))
231             {
232                 n[_] /= d;
233                 n[_] *= asums[_];
234             }
235             static if (derivative == 0)
236                 return n[0];
237             else
238                 return n;
239         }
240     }
241 
242     static if (maxAdditionalFunctions)
243     {
244         ///
245         alias withDerivative = opCall!1;
246 
247         static if (maxAdditionalFunctions > 1)
248         {
249             ///
250             alias withTwoDerivatives = opCall!2;
251         }
252     }
253 }
254 
255 
256 /++
257 +/
258 pragma(inline, false)
259 @nogc
260 RCArray!(immutable T) inversedBarycentricWeights(T)(Slice!(const(T)*) x)
261     if (isFloatingPoint!T)
262 {
263     
264     const n = x.length;
265     scope prodsa = RCArray!(ProdAccumulator!T)(n);
266     scope p = prodsa[].sliced;
267     foreach (triplet; n.iota.triplets) with(triplet)
268     {
269         foreach (l; left)
270         {
271             auto e = prod!T(x[center] - x[l]);
272             p[l] *= -e;
273             p[center] *= e;
274         }
275     }
276     import mir.algorithm.iteration: reduce;
277     const minExp = long.max.reduce!min(p.member!"exp");
278     foreach (ref e; p)
279         e = e.ldexp(1 - minExp);
280     auto ret = rcarray!(immutable T)(p.member!"prod");
281     return ret;
282 }
283 
284 /++
285 Computes derivative values in the same points
286 Params:
287     d = derivative values (output)
288     y = values
289     x = grid
290     w = inversed barycentric weights
291 Returns:
292     Derivative values in the same points
293 +/
294 @nogc
295 Slice!(T*) polynomialDerivativeValues(T)(return Slice!(T*) d,  Slice!(const(T)*) x,  Slice!(const(T)*) y,  Slice!(const(T)*) w)
296     if (isFloatingPoint!T)
297 {
298     pragma(inline, false);
299     import mir.math.sum;
300 
301     const n = x.length;
302     assert(n == w.length);
303     assert(n == y.length);
304 
305     d.fillCollapseSums!(Summation.fast,
306         (c, s1, s2) => w[c] * s1 + y[c] * s2,
307         (c, _) => y[_] / (w[_] * (x[c] - x[_])),
308         (c, _) => map!"1 / a"(x[c] - x[_]));
309     return d;
310 }
311 
312 ///
313 @nogc
314 Slice!(T*) polynomialDerivativeValues(T)(return Slice!(T*) d,  Slice!(const(T)*) x,  Slice!(const(T)*) y)
315     if (isFloatingPoint!T)
316 {
317     return .polynomialDerivativeValues(d, x, y, x.inversedBarycentricWeights[].sliced);
318 }
319 
320 private T asumNorm(T)(Slice!(T*) v)
321 {
322     pragma(inline, false);
323     auto n = v.map!fabs.sum!"fast";
324     v[] /= n;
325     return n;
326 }