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