|
| 1 | +# Developer documentation |
| 2 | + |
| 3 | +Conceptually, `Interpolations.jl` supports two operations: construction and usage of interpolants. |
| 4 | + |
| 5 | +## Interpolant construction |
| 6 | + |
| 7 | +Construction creates the interpolant object. In some situations this is relatively trivial: |
| 8 | +for example, when using only `NoInterp`, `Constant`, or `Linear` interpolation schemes, |
| 9 | +construction essentially corresponds to recording the array of values and the "settings" (the interpolation scheme) specified at the time of |
| 10 | +construction. This case is simple because interpolated values may be efficiently computed directly from the on-grid values supplied at construction time: ``(1-\Delta x) a_i + \Delta x a_{i+1}`` reconstructs ``a_i`` when ``\Delta x = 0``. |
| 11 | + |
| 12 | +For `Quadratic` and higher orders, efficient computation requires that the array of values be *prefiltered*. |
| 13 | +This essentially corresponds to "inverting" the computation that will be performed during interpolation, so as to approximately reconstruct the original values at on-grid points. Generally speaking this corresponds to solving a nearly-tridiagonal system of equations, inverting an underlying interpolation |
| 14 | +scheme such as ``p(\Delta x) \tilde a_{i-1} + q(\Delta x) \tilde a_i + p(1-\Delta x) \tilde a_{i+1}`` for some functions ``p`` and ``q`` (see [`Quadratic`](@ref) for further details). |
| 15 | +Here ``\tilde a`` is the pre-filtered version of `a`, designed so that |
| 16 | +substituting ``\Delta x = 0`` (for which one may *not* get 0 and 1 for the ``p`` and ``q`` calls, respectively) approximately recapitulates ``a_i``. |
| 17 | + |
| 18 | +The exact system of equations to be solved depends on the interpolation order and boundary conditions. |
| 19 | +Boundary conditions often introduce deviations from perfect tridiagonality; these "extras" are handled efficiently by the `WoodburyMatrices` package. |
| 20 | +These computations are implemented independently along each axis using the `AxisAlgorithms` package. |
| 21 | + |
| 22 | +In the `doc` directory there are some old files that give some of the mathematical details. |
| 23 | +A useful reference is: |
| 24 | + |
| 25 | +Thévenaz, Philippe, Thierry Blu, and Michael Unser. "Interpolation revisited." IEEE Transactions on Medical Imaging 19.7 (2000): 739-758. |
| 26 | + |
| 27 | +!!! note |
| 28 | + As an application of these concepts, note that supporting quadratic or cubic interpolation for `Gridded` would |
| 29 | + only require that someone implement prefiltering schemes for non-uniform grids; |
| 30 | + it's just a question of working out a little bit of math. |
| 31 | + |
| 32 | +## Interpolant usage |
| 33 | + |
| 34 | +Usage occurs when evaluating `itp(x, y...)`, or `Interpolations.gradient(itp, x, y...)`, etc. |
| 35 | +Usage itself involves two sub-steps: computation of the *weights* and then performing the *interpolation*. |
| 36 | + |
| 37 | +### Weight computation |
| 38 | + |
| 39 | +Weights depend on the interpolation scheme and the location `x, y...` but *not* the coefficients of the array we are interpolating. |
| 40 | +Consequently there are many circumstances where one might want to reuse previously-computed weights, and `Interpolations.jl` has been carefully designed |
| 41 | +with that kind of reuse in mind. |
| 42 | + |
| 43 | +The key concept here is the [`Interpolations.WeightedIndex`](@ref), and there is |
| 44 | +no point repeating its detailed docstring here. |
| 45 | +It suffices to add that `WeightedIndex` is actually an abstract type, with two |
| 46 | +concrete subtypes: |
| 47 | + |
| 48 | +- `WeightedAdjIndex` is for indexes that will address *adjacent* points of the coefficient array (ones where the index increments by 1 along the corresponding dimension). These are used when prefiltering produces padding that can be used even at the edges, or for schemes like `Linear` interpolation which require no padding. |
| 49 | +- `WeightedArbIndex` stores both the weight and index associated with each accessed grid point, and can therefore encode grid access patterns. These are used in specific circumstances--a prime example being periodic boundary conditions--where the coefficients array may be accessed at something other than adjacent locations. |
| 50 | + |
| 51 | +`WeightedIndex` computation reflects the interpolation scheme (e.g., `Linear` or `Quadratic`) and also whether one is computing values, `gradient`s, or `hessian`s. The handling of derivatives will be described further below. |
| 52 | + |
| 53 | +### Interpolation |
| 54 | + |
| 55 | +General `AbstractArray`s may be indexed with `WeightedIndex` indices, |
| 56 | +and the result produces the interpolated value. In other words, the end result |
| 57 | +is just `itp.coefs[wis...]`, where `wis` is a tuple of `WeightedIndex` indices. |
| 58 | + |
| 59 | +Derivatives along a particular axis can be computed just by substituting a component of `wis` for one that has been designed to compute derivatives rather than values. |
| 60 | + |
| 61 | +As a demonstration, let's see how the following computation occurs: |
| 62 | + |
| 63 | +```jldoctest derivs; setup = :(using Interpolations) |
| 64 | +julia> A = reshape(1:27, 3, 3, 3) |
| 65 | +3×3×3 reshape(::UnitRange{Int64}, 3, 3, 3) with eltype Int64: |
| 66 | +[:, :, 1] = |
| 67 | + 1 4 7 |
| 68 | + 2 5 8 |
| 69 | + 3 6 9 |
| 70 | +
|
| 71 | +[:, :, 2] = |
| 72 | + 10 13 16 |
| 73 | + 11 14 17 |
| 74 | + 12 15 18 |
| 75 | +
|
| 76 | +[:, :, 3] = |
| 77 | + 19 22 25 |
| 78 | + 20 23 26 |
| 79 | + 21 24 27 |
| 80 | +
|
| 81 | +julia> itp = interpolate(A, BSpline(Linear())); |
| 82 | +
|
| 83 | +julia> x = (1.2, 1.4, 1.7) |
| 84 | +(1.2, 1.4, 1.7) |
| 85 | +
|
| 86 | +julia> itp(x...) |
| 87 | +8.7 |
| 88 | +``` |
| 89 | + |
| 90 | +!!! note |
| 91 | + By using the debugging facilities of an IDE like Juno or VSCode, |
| 92 | + or using Debugger.jl from the REPL, you can easily step in to |
| 93 | + the call above and follow along with the description below. |
| 94 | + |
| 95 | +Aside from details such as bounds-checking, the key call is to [`Interpolations.weightedindexes`](@ref): |
| 96 | + |
| 97 | +```jldoctest derivs |
| 98 | +julia> wis = Interpolations.weightedindexes((Interpolations.value_weights,), Interpolations.itpinfo(itp)..., x) |
| 99 | +(Interpolations.WeightedAdjIndex{2,Float64}(1, (0.8, 0.19999999999999996)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.6000000000000001, 0.3999999999999999)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.30000000000000004, 0.7))) |
| 100 | +
|
| 101 | +julia> wis[1] |
| 102 | +Interpolations.WeightedAdjIndex{2,Float64}(1, (0.8, 0.19999999999999996)) |
| 103 | +
|
| 104 | +julia> wis[2] |
| 105 | +Interpolations.WeightedAdjIndex{2,Float64}(1, (0.6000000000000001, 0.3999999999999999)) |
| 106 | +
|
| 107 | +julia> wis[3] |
| 108 | +Interpolations.WeightedAdjIndex{2,Float64}(1, (0.30000000000000004, 0.7)) |
| 109 | +
|
| 110 | +julia> A[wis...] |
| 111 | +8.7 |
| 112 | +``` |
| 113 | + |
| 114 | +You can see that each of `wis` corresponds to a specific position: 1.2, 1.4, and 1.7 respectively. We can index `A` at `wis`, and it returns the value of `itp(x...)`, which here is just |
| 115 | + |
| 116 | + 0.8 * A[1, wis[2], wis[3]] + 0.2 * A[2, wis[2], wis[3]] |
| 117 | + = 0.6 * (0.8 * A[1, 1, wis[3]] + 0.2 * A[2, 1, wis[3]]) + |
| 118 | + 0.4 * (0.8 * A[1, 2, wis[3]] + 0.2 * A[2, 2, wis[3]]) |
| 119 | + = 0.3 * (0.6 * (0.8 * A[1, 1, 1] + 0.2 * A[2, 1, 1]) + |
| 120 | + 0.4 * (0.8 * A[1, 2, 1] + 0.2 * A[2, 2, 1]) ) + |
| 121 | + 0.7 * (0.6 * (0.8 * A[1, 1, 2] + 0.2 * A[2, 1, 2]) + |
| 122 | + 0.4 * (0.8 * A[1, 2, 2] + 0.2 * A[2, 2, 2]) ) |
| 123 | + |
| 124 | +This computed the value of `itp` at `x...` because we called `weightedindexes` with just a single function, [`Interpolations.value_weights`](@ref) (meaning, "the weights needed to compute the value"). |
| 125 | + |
| 126 | +!!! note |
| 127 | + Remember that prefiltering is not used for `Linear` interpolation. |
| 128 | + In a case where prefiltering is used, we would substitute `itp.coefs[wis...]` for `A[wis...]` above. |
| 129 | + |
| 130 | +To compute derivatives, we *also* pass additional functions like [`Interpolations.gradient_weights`](@ref): |
| 131 | + |
| 132 | +``` |
| 133 | +julia> wis = Interpolations.weightedindexes((Interpolations.value_weights, Interpolations.gradient_weights), Interpolations.itpinfo(itp)..., x) |
| 134 | +((Interpolations.WeightedAdjIndex{2,Float64}(1, (-1.0, 1.0)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.6000000000000001, 0.3999999999999999)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.30000000000000004, 0.7))), (Interpolations.WeightedAdjIndex{2,Float64}(1, (0.8, 0.19999999999999996)), Interpolations.WeightedAdjIndex{2,Float64}(1, (-1.0, 1.0)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.30000000000000004, 0.7))), (Interpolations.WeightedAdjIndex{2,Float64}(1, (0.8, 0.19999999999999996)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.6000000000000001, 0.3999999999999999)), Interpolations.WeightedAdjIndex{2,Float64}(1, (-1.0, 1.0)))) |
| 135 | +
|
| 136 | +julia> wis[1] |
| 137 | +(Interpolations.WeightedAdjIndex{2,Float64}(1, (-1.0, 1.0)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.6000000000000001, 0.3999999999999999)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.30000000000000004, 0.7))) |
| 138 | +
|
| 139 | +julia> wis[2] |
| 140 | +(Interpolations.WeightedAdjIndex{2,Float64}(1, (0.8, 0.19999999999999996)), Interpolations.WeightedAdjIndex{2,Float64}(1, (-1.0, 1.0)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.30000000000000004, 0.7))) |
| 141 | +
|
| 142 | +julia> wis[3] |
| 143 | +(Interpolations.WeightedAdjIndex{2,Float64}(1, (0.8, 0.19999999999999996)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.6000000000000001, 0.3999999999999999)), Interpolations.WeightedAdjIndex{2,Float64}(1, (-1.0, 1.0))) |
| 144 | +
|
| 145 | +julia> A[wis[1]...] |
| 146 | +1.0 |
| 147 | +
|
| 148 | +julia> A[wis[2]...] |
| 149 | +3.000000000000001 |
| 150 | +
|
| 151 | +julia> A[wis[3]...] |
| 152 | +9.0 |
| 153 | +``` |
| 154 | +In this case you can see that `wis` is a 3-tuple-of-3-tuples. `A[wis[i]...]` can be used to compute the `i`th component of the gradient. |
| 155 | + |
| 156 | +If you look carefully at each of the entries in `wis`, you'll see that |
| 157 | +each "inner" 3-tuple copies two of the three elements in the `wis` we |
| 158 | +obtained when we called `weightedindexes` with just `value_weights` above. |
| 159 | +`wis[1]` replaces the first entry with |
| 160 | +a weighted index having weights `(-1.0, 1.0)`, which corresponds to computing the *slope* along this dimension. |
| 161 | +Likewise `wis[2]` and `wis[3]` replace the second and third value-index, respectively, with the same slope computation. |
| 162 | + |
| 163 | +Hessian computation is quite similar, with the difference that one sometimes needs to replace two different indices or the same index with a set of weights corresponding to a second derivative. |
| 164 | + |
| 165 | +Consequently derivatives along particular directions are computed simply by "weight replacement" along the corresponding dimensions. |
| 166 | + |
| 167 | +The code to do this replacement is a bit complicated due to the need to support arbitrary dimensionality in a manner that allows Julia's type-inference to succeed. |
| 168 | +It makes good use of *tuple* manipulations, sometimes called "lispy tuple programming." |
| 169 | +You can search Julia's discourse forum for more tips about how to program this way. |
| 170 | +It could alternatively be done using generated functions, but this would increase compile time considerably and can lead to world-age problems. |
0 commit comments