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 }