diff --git a/.travis.yml b/.travis.yml
index 2becf377..873fd3f4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,12 +1,9 @@
language: julia
sudo: false
julia:
- - 0.6
- 0.7
+ - 1.0
- nightly
-matrix:
- allow_failures:
- - julia: nightly
after_success:
# push coverage results to Coveralls
- julia -e 'cd(Pkg.dir("Interpolations")); Pkg.add("Coverage"); using Coverage; Coveralls.submit(Coveralls.process_folder())'
diff --git a/README.md b/README.md
index 70a79ff1..482de3f6 100644
--- a/README.md
+++ b/README.md
@@ -5,17 +5,16 @@
[![Interpolations](http://pkg.julialang.org/badges/Interpolations_0.5.svg)](http://pkg.julialang.org/?pkg=Interpolations)
This package implements a variety of interpolation schemes for the
-Julia langauge. It has the goals of ease-of-use, broad algorithmic
+Julia language. It has the goals of ease-of-use, broad algorithmic
support, and exceptional performance.
-This package is still relatively new. Currently its support is best
+Currently this package's support is best
for [B-splines](https://en.wikipedia.org/wiki/B-spline) and also
supports irregular grids. However, the API has been designed with
intent to support more options. Pull-requests are more than welcome!
It should be noted that the API may continue to evolve over time.
Other interpolation packages for Julia include:
-- [Grid.jl](https://github.com/timholy/Grid.jl) (the predecessor of this package)
- [Dierckx.jl](https://github.com/kbarbary/Dierckx.jl)
- [GridInterpolations.jl](https://github.com/sisl/GridInterpolations.jl)
- [ApproXD.jl](https://github.com/floswald/ApproXD.jl)
@@ -42,7 +41,7 @@ from the Julia REPL.
Note: the current version of `Interpolations` supports interpolation evaluation using index calls `[]`, but this feature will be deprecated in future. We highly recommend function calls with `()` as follows.
Given an `AbstractArray` `A`, construct an "interpolation object" `itp` as
-```jl
+```julia
itp = interpolate(A, options...)
```
where `options...` (discussed below) controls the type of
@@ -50,30 +49,30 @@ interpolation you want to perform. This syntax assumes that the
samples in `A` are equally-spaced.
To evaluate the interpolation at position `(x, y, ...)`, simply do
-```jl
+```julia
v = itp(x, y, ...)
```
Some interpolation objects support computation of the gradient, which
can be obtained as
-```jl
+```julia
g = gradient(itp, x, y, ...)
```
or, if you're evaluating the gradient repeatedly, a somewhat more
efficient option is
-```jl
+```julia
gradient!(g, itp, x, y, ...)
```
where `g` is a pre-allocated vector.
Some interpolation objects support computation of the hessian, which
can be obtained as
-```jl
+```julia
h = hessian(itp, x, y, ...)
```
or, if you're evaluating the hessian repeatedly, a somewhat more
efficient option is
-```jl
+```julia
hessian!(h, itp, x, y, ...)
```
where `h` is a pre-allocated matrix.
@@ -85,25 +84,28 @@ and `Rational`, but also multi-valued types like `RGB` color vectors.
Positions `(x, y, ...)` are n-tuples of numbers. Typically these will
be real-valued (not necessarily integer-valued), but can also be of types
such as [DualNumbers](https://github.com/JuliaDiff/DualNumbers.jl) if
-you want to verify the computed value of gradients. You can also use
+you want to verify the computed value of gradients.
+(Alternatively, verify gradients using [ForwardDiff](https://github.com/JuliaDiff/ForwardDiff.jl).)
+You can also use
Julia's iterator objects, e.g.,
-```jl
+```julia
function ongrid!(dest, itp)
- for I in CartesianRange(size(itp))
+ for I in CartesianIndices(itp)
dest[I] = itp(I)
end
end
```
would store the on-grid value at each grid point of `itp` in the output `dest`.
Finally, courtesy of Julia's indexing rules, you can also use
-```jl
-fine = itp(linspace(1,10,1001), linspace(1,15,201))
+```julia
+fine = itp(range(1,stop=10,length=1001), range(1,stop=15,length=201))
```
### Quickstart guide
+
For linear and cubic spline interpolations, `LinearInterpolation` and `CubicSplineInterpolation` can be used to create interpolation objects handily:
-```jl
+```julia
f(x) = log(x)
xs = 1:0.2:5
A = [f(x) for x in xs]
@@ -119,7 +121,7 @@ interp_cubic(3) # exactly log(3)
interp_cubic(3.1) # approximately log(3.1)
```
which support multidimensional data as well:
-```jl
+```julia
f(x,y) = log(x+y)
xs = 1:0.2:5
ys = 2:0.1:5
@@ -137,19 +139,19 @@ interp_cubic(3.1, 2.1) # approximately log(3.1 + 2.1)
```
For extrapolation, i.e., when interpolation objects are evaluated in coordinates outside of range provided in constructors, the default option for a boundary condition is `Throw` so that they will return an error.
Interested users can specify boundary conditions by providing an extra parameter for `extrapolation_bc`:
-```jl
+```julia
f(x) = log(x)
xs = 1:0.2:5
A = [f(x) for x in xs]
# extrapolation with linear boundary conditions
-extrap = LinearInterpolation(xs, A, extrapolation_bc = Interpolations.Linear())
+extrap = LinearInterpolation(xs, A, extrapolation_bc = Line())
@test extrap(1 - 0.2) # ≈ f(1) - (f(1.2) - f(1))
@test extrap(5 + 0.2) # ≈ f(5) + (f(5) - f(4.8))
```
Irregular grids are supported as well; note that presently only `LinearInterpolation` supports irregular grids.
-```jl
+```julia
xs = [x^2 for x = 1:0.2:5]
A = [f(x) for x in xs]
@@ -163,34 +165,34 @@ interp_linear(1.05) # approximately log(1.05)
### BSplines
-The interpolation type is described in terms of *degree*, *grid behavior* and, if necessary, *boundary conditions*. There are currently three degrees available: `Constant`, `Linear`, `Quadratic`, and `Cubic` corresponding to B-splines of degree 0, 1, 2, and 3 respectively.
-
-You also have to specify what *grid representation* you want. There are currently two choices: `OnGrid`, in which the supplied data points are assumed to lie *on* the boundaries of the interpolation interval, and `OnCell` in which the data points are assumed to lie on half-intervals between cell boundaries.
+The interpolation type is described in terms of *degree* and, if necessary, *boundary conditions*. There are currently three degrees available: `Constant`, `Linear`, `Quadratic`, and `Cubic` corresponding to B-splines of degree 0, 1, 2, and 3 respectively.
B-splines of quadratic or higher degree require solving an equation system to obtain the interpolation coefficients, and for that you must specify a *boundary condition* that is applied to close the system. The following boundary conditions are implemented: `Flat`, `Line` (alternatively, `Natural`), `Free`, `Periodic` and `Reflect`; their mathematical implications are described in detail in the pdf document under `/doc/latex`.
+When specifying these boundary conditions you also have to specify whether they apply at the edge grid point (`OnGrid()`)
+or beyond the edge point halfway to the next (fictitious) grid point (`OnCell()`).
Some examples:
-```jl
+```julia
# Nearest-neighbor interpolation
-itp = interpolate(a, BSpline(Constant()), OnCell())
+itp = interpolate(a, BSpline(Constant()))
v = itp(5.4) # returns a[5]
# (Multi)linear interpolation
-itp = interpolate(A, BSpline(Linear()), OnGrid())
+itp = interpolate(A, BSpline(Linear()))
v = itp(3.2, 4.1) # returns 0.9*(0.8*A[3,4]+0.2*A[4,4]) + 0.1*(0.8*A[3,5]+0.2*A[4,5])
# Quadratic interpolation with reflecting boundary conditions
# Quadratic is the lowest order that has continuous gradient
-itp = interpolate(A, BSpline(Quadratic(Reflect())), OnCell())
+itp = interpolate(A, BSpline(Quadratic(Reflect(OnCell()))))
# Linear interpolation in the first dimension, and no interpolation (just lookup) in the second
-itp = interpolate(A, (BSpline(Linear()), NoInterp()), OnGrid())
+itp = interpolate(A, (BSpline(Linear()), NoInterp()))
v = itp(3.65, 5) # returns 0.35*A[3,5] + 0.65*A[4,5]
```
There are more options available, for example:
-```jl
+```julia
# In-place interpolation
-itp = interpolate!(A, BSpline(Quadratic(InPlace())), OnCell())
+itp = interpolate!(A, BSpline(Quadratic(InPlace(OnCell()))))
```
which destroys the input `A` but also does not need to allocate as much memory.
@@ -199,22 +201,22 @@ which destroys the input `A` but also does not need to allocate as much memory.
BSplines assume your data is uniformly spaced on the grid `1:N`, or its multidimensional equivalent. If you have data of the form `[f(x) for x in A]`, you need to tell Interpolations about the grid `A`. If `A` is not uniformly spaced, you must use gridded interpolation described below. However, if `A` is a collection of ranges or linspaces, you can use scaled BSplines. This is more efficient because the gridded algorithm does not exploit the uniform spacing. Scaled BSplines can also be used with any spline degree available for BSplines, while gridded interpolation does not currently support quadratic or cubic splines.
Some examples,
-```jl
+```julia
A_x = 1.:2.:40.
A = [log(x) for x in A_x]
-itp = interpolate(A, BSpline(Cubic(Line())), OnGrid())
+itp = interpolate(A, BSpline(Cubic(Line(OnGrid()))))
sitp = scale(itp, A_x)
sitp(3.) # exactly log(3.)
sitp(3.5) # approximately log(3.5)
```
For multidimensional uniformly spaced grids
-```jl
+```julia
A_x1 = 1:.1:10
A_x2 = 1:.5:20
f(x1, x2) = log(x1+x2)
A = [f(x1,x2) for x1 in A_x1, x2 in A_x2]
-itp = interpolate(A, BSpline(Cubic(Line())), OnGrid())
+itp = interpolate(A, BSpline(Cubic(Line(OnGrid()))))
sitp = scale(itp, A_x1, A_x2)
sitp(5., 10.) # exactly log(5 + 10)
sitp(5.6, 7.1) # approximately log(5.6 + 7.1)
@@ -227,7 +229,7 @@ are all `OnGrid`). As such one must specify a set of coordinate arrays
defining the knots of the array.
In 1D
-```jl
+```julia
A = rand(20)
A_x = collect(1.0:2.0:40.0)
knots = (A_x,)
@@ -236,34 +238,34 @@ itp(2.0)
```
The spacing between adjacent samples need not be constant, you can use the syntax
-```jl
+```julia
itp = interpolate(knots, A, options...)
```
where `knots = (xknots, yknots, ...)` to specify the positions along
each axis at which the array `A` is sampled for arbitrary ("rectangular") samplings.
For example:
-```jl
+```julia
A = rand(8,20)
knots = ([x^2 for x = 1:8], [0.2y for y = 1:20])
itp = interpolate(knots, A, Gridded(Linear()))
itp(4,1.2) # approximately A[2,6]
```
One may also mix modes, by specifying a mode vector in the form of an explicit tuple:
-```jl
+```julia
itp = interpolate(knots, A, (Gridded(Linear()),Gridded(Constant())))
```
Presently there are only three modes for gridded:
-```jl
+```julia
Gridded(Linear())
```
whereby a linear interpolation is applied between knots,
-```jl
+```julia
Gridded(Constant())
```
whereby nearest neighbor interpolation is used on the applied axis,
-```jl
+```julia
NoInterp
```
whereby the coordinate of the selected input vector MUST be located on a grid point. Requests for off grid
@@ -281,7 +283,7 @@ x = sin.(2π*t)
y = cos.(2π*t)
A = hcat(x,y)
-itp = scale(interpolate(A, (BSpline(Cubic(Natural())), NoInterp()), OnGrid()), t, 1:2)
+itp = scale(interpolate(A, (BSpline(Cubic(Natural(OnGrid()))), NoInterp())), t, 1:2)
tfine = 0:.01:1
xs, ys = [itp(t,1) for t in tfine], [itp(t,2) for t in tfine]
@@ -366,37 +368,6 @@ they ran more than 20 seconds (far longer than any other test). Both
performed much better in 2d, interestingly. You can see that
Interpolations wins in every case, sometimes by a very large margin.
-## Transitioning from Grid.jl
-
-Instead of using
-```julia
-yi = InterpGrid(y, BCreflect, InterpQuadratic)
-```
-you should use
-```julia
-yi = interpolate(y, BSpline(Quadratic(Reflect())), OnCell())
-```
-
-In general, here are the closest mappings:
-
-| Grid | Interpolations |
-|:-----------------:|:------------------------------------------:|
-| `InterpNearest` | `Constant` |
-| `InterpLinear` | `Linear` |
-| `InterpQuadratic` | `Quadratic` |
-| `InterpCubic` | `Cubic` |
-| | |
-| `BCnil` | `extrapolate(itp, Interpolations.Throw())` |
-| `BCnan` | `extrapolate(itp, NaN)` |
-| `BCna` | `extrapolate(itp, NaN)` |
-| `BCreflect` | `interpolate` with `Reflect()` |
-| `BCperiodic` | `interpolate` with `Periodic()` |
-| `BCnearest` | `interpolate` with `Flat()` |
-| `BCfill` | `extrapolate` with value |
-| | |
-| odd orders | `OnGrid()` |
-| even orders | `OnCell()` |
-
## Contributing
diff --git a/REQUIRE b/REQUIRE
index 91a313b1..056c8db9 100644
--- a/REQUIRE
+++ b/REQUIRE
@@ -1,8 +1,7 @@
-julia 0.6
+julia 0.7
-ShowItLikeYouBuildIt
WoodburyMatrices 0.1.5
Ratios
AxisAlgorithms 0.3.0
OffsetArrays
-Compat 0.59
+StaticArrays
diff --git a/doc/Interpolations.jl.ipynb b/doc/Interpolations.jl.ipynb
index 58bf31d8..e175fae7 100644
--- a/doc/Interpolations.jl.ipynb
+++ b/doc/Interpolations.jl.ipynb
@@ -11,7 +11,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "This is a brief exposé of the rewrite of Tim Holy's `Grid.jl` that started as a metaprogramming experiment, and is now becoming good enough to start working seriously toward feature parity. `Interpolations.jl` implements [(cardinal) B-splines](http://en.wikipedia.org/wiki/B-spline#Cardinal_B-spline), i.e. interpolating polynomial functions which are fit to the data and to each other."
+ "`Interpolations.jl` implements [(cardinal) B-splines](http://en.wikipedia.org/wiki/B-spline#Cardinal_B-spline), i.e. interpolating polynomial functions which are fit to the data and to each other."
]
},
{
@@ -23,7 +23,7 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
@@ -41,33 +41,33 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "An interpolation object is basically an array that supports indexing with any real nubmers. Currently, only a few different variants are implemented, but my hope is to extend this close to feature parity with `Grid.jl` quite soon.\n",
+ "An interpolation object is basically an array that supports evaluation with any real numbers.\n",
"\n",
- "To create an interpolation object, simply call the constructor `Interpolation`, providing the array and some configuration for the interpolation:"
+ "To create an interpolation object, simply call the constructor `interpolate`, providing the array and some configuration for the interpolation:"
]
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "10-element interpolate(::Array{Float64,1}, BSpline(Linear()), OnGrid()) with element type Float64:\n",
- " 0.0 \n",
- " 0.642788 \n",
- " 0.984808 \n",
- " 0.866025 \n",
- " 0.34202 \n",
- " -0.34202 \n",
- " -0.866025 \n",
- " -0.984808 \n",
- " -0.642788 \n",
- " -2.44929e-16"
+ "10-element interpolate(::Array{Float64,1}, BSpline(Linear())) with element type Float64:\n",
+ " 0.0 \n",
+ " 0.6427876096865393 \n",
+ " 0.984807753012208 \n",
+ " 0.8660254037844387 \n",
+ " 0.3420201433256689 \n",
+ " -0.34202014332566866 \n",
+ " -0.8660254037844385 \n",
+ " -0.9848077530122081 \n",
+ " -0.6427876096865396 \n",
+ " -2.4492935982947064e-16"
]
},
- "execution_count": 4,
+ "execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
@@ -75,14 +75,14 @@
"source": [
"using Interpolations\n",
"\n",
- "yitp = interpolate(ycoarse, BSpline(Linear()), OnGrid())"
+ "yitp = interpolate(ycoarse, BSpline(Linear()))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "There are a couple of noteworthy points here. First, note the two extra arguments to `interpolate`: `BSpline(Linear)` and `OnGrid`. These options determine the behavior of the interpolating function inside the domain. `Linear` tells us that it will be a linear interpolation (duh...) and `OnGrid` tells us that the data points are located at *cell boundaries*. In other words, for `yitp[x]` interpolation will be performed like this:\n",
+ "There are a couple of noteworthy points here. First, note the extra argument to `interpolate`, `BSpline(Linear())`. This determines the behavior of the interpolating function inside the domain. `Linear` tells us that it will be a linear interpolation (duh...); in other words, for `yitp(x)` interpolation will be performed like this:\n",
"\n",
" begin\n",
" ix = ifloor(x)\n",
@@ -92,36 +92,32 @@
"\n",
"A B-spline interpolation is basically a piecewise polynomial function, where each grid cell is associated with its own polynomial. The coefficients are chosen such that the interpolating function passes through all the data points, while the transitions between polynomials are as smooth as possible given their degree. For example, each grid cell in a linear interpolation (such as `yitp`) is associated with a straight line, and the interpolation is continuous (with discontinuous derivative) over the entire domain.\n",
"\n",
- "`OnGrid`, as used when creating `yitp`, signifies that the data points are located on *cell boundaries*; in other words, the interpolating polynomial will be the same for the entire interval between two data points (and, in fact, will be exactly the straight line connecting the dots). Other interpolation types in `Interpolations.jl` are `OnCell`, signifying that the data points are at the *center* of each grid cell. In such an interpolation, an interval such as $(2.5,3.5)$ will be represented by a single polynomial, rather than transitioning at $x=3$.\n",
- "\n",
"Let's take a look at how the different interpolation degrees behave:"
]
},
{
"cell_type": "code",
- "execution_count": 5,
- "metadata": {
- "collapsed": true
- },
+ "execution_count": 4,
+ "metadata": {},
"outputs": [],
"source": [
- "yitp_const = interpolate(ycoarse, BSpline(Constant()), OnCell())\n",
- "yconst = [yitp_const[x] for x in xfine]\n",
+ "yitp_const = interpolate(ycoarse, BSpline(Constant()))\n",
+ "yconst = [yitp_const(x) for x in xfine]\n",
"\n",
- "yitp_linear = interpolate(ycoarse, BSpline(Linear()), OnGrid())\n",
- "ylinear = [yitp_linear[x] for x in xfine]\n",
+ "yitp_linear = interpolate(ycoarse, BSpline(Linear()))\n",
+ "ylinear = [yitp_linear(x) for x in xfine]\n",
"\n",
- "yitp_quadratic = interpolate(ycoarse, BSpline(Quadratic(Line())), OnCell())\n",
- "yquadratic = [yitp_quadratic[x] for x in xfine];"
+ "yitp_quadratic = interpolate(ycoarse, BSpline(Quadratic(Line(OnCell()))))\n",
+ "yquadratic = [yitp_quadratic(x) for x in xfine];"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "You'll notice that when creating the quadratic interpolation, we had to give another input parameter to the interpolation type: a `ExtendInner` instance.\n",
+ "You'll notice that when creating the quadratic interpolation, we had to give another input parameter to the interpolation type: the `Line(OnCell())` argument which specifies the boundary condition. The first term (`Line`) specifies the behavior of the boundary condition, the second (`OnCell`) specifies where it applies. In this case `yitp_quadratic` will become linear for `x < 0.5` or `x > 10.5`. The alternative, `OnGrid`, would apply the boundary condition for `x < 1` or `x > 10`.\n",
"\n",
- "All interpolations of quadratic degree or higher require a prefiltering step, which entails solving a tridiagonal system of equations (details can be found for example in [this paper](http://dx.doi.org/10.1109/42.875199)), in order to make the interpolating function pass through the data points. `Interpolations.jl` takes care of solving this system for you, but in order to close the system a boundary condition is requred. `ExtendInner` simply means that the outermost well-defined polynomial will be extended all the way to the end. Quadratic interpolation is `OnCell`, so each piece is centered around a datapoint, and the polynomial coefficients are dependent on three data points; thus, the outermost well-defined polynomials are the ones centered on $x=2$ and $x=9$ (i.e. with domains $[1.5,2.5]$ and $[8.5,9.5]$, respectively). With `ExtendInner`, these polynomials are used all the way to the edge, i.e. on the domains $[1,2.5]$ and $[8.5,10]$ respectively. Other boundary conditions (yet to be implemented) are `Flat` (with $f' = 0$ at the edges), `Line` (with $f'' = 0$ at the edges), as well as `Reflect` and `Periodic` (which will both need to be combined with corresponding extrapolation behaviors, also yet to be implemented, in order to make sense...)."
+ "All interpolations of quadratic degree or higher require a prefiltering step, which entails solving a tridiagonal system of equations (details can be found for example in [this paper](http://dx.doi.org/10.1109/42.875199)), in order to make the interpolating function pass through the data points. `Interpolations.jl` takes care of solving this system for you, but in order to close the system a boundary condition is requred."
]
},
{
@@ -133,1901 +129,62 @@
},
{
"cell_type": "code",
- "execution_count": 8,
+ "execution_count": 6,
"metadata": {},
"outputs": [
{
- "data": {
- "image/png": "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",
- "image/svg+xml": [
- "\n",
- "\n"
- ],
- "text/html": [
- "\n",
- "\n"
- ],
- "text/plain": [
- "Plot(...)"
- ]
- },
- "execution_count": 8,
- "metadata": {},
- "output_type": "execute_result"
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "┌ Info: Precompiling Gadfly [c91e804a-d5a3-530f-b6f0-dfbca275c004]\n",
+ "└ @ Base loading.jl:1186\n",
+ "ERROR: LoadError: syntax: try without catch or finally\n",
+ "Stacktrace:\n",
+ " [1] include at ./boot.jl:317 [inlined]\n",
+ " [2] include_relative(::Module, ::String) at ./loading.jl:1038\n",
+ " [3] include(::Module, ::String) at ./sysimg.jl:29\n",
+ " [4] top-level scope at none:2\n",
+ " [5] eval at ./boot.jl:319 [inlined]\n",
+ " [6] eval(::Expr) at ./client.jl:389\n",
+ " [7] top-level scope at ./none:3\n",
+ "in expression starting at /home/tim/.julia/packages/Compose/y7cU7/src/Compose.jl:207\n",
+ "ERROR: LoadError: Failed to precompile Compose [a81c6b42-2e10-5240-aca2-a61377ecd94b] to /home/tim/.julia/compiled/v1.0/Compose/sbiEw.ji.\n",
+ "Stacktrace:\n",
+ " [1] error(::String) at ./error.jl:33\n",
+ " [2] macro expansion at ./logging.jl:313 [inlined]\n",
+ " [3] compilecache(::Base.PkgId, ::String) at ./loading.jl:1184\n",
+ " [4] _require(::Base.PkgId) at ./logging.jl:311\n",
+ " [5] require(::Base.PkgId) at ./loading.jl:852\n",
+ " [6] macro expansion at ./logging.jl:311 [inlined]\n",
+ " [7] require(::Module, ::Symbol) at ./loading.jl:834\n",
+ " [8] include at ./boot.jl:317 [inlined]\n",
+ " [9] include_relative(::Module, ::String) at ./loading.jl:1038\n",
+ " [10] include(::Module, ::String) at ./sysimg.jl:29\n",
+ " [11] top-level scope at none:2\n",
+ " [12] eval at ./boot.jl:319 [inlined]\n",
+ " [13] eval(::Expr) at ./client.jl:389\n",
+ " [14] top-level scope at ./none:3\n",
+ "in expression starting at /home/tim/.julia/packages/Gadfly/p8TXc/src/Gadfly.jl:7\n"
+ ]
+ },
+ {
+ "ename": "ErrorException",
+ "evalue": "Failed to precompile Gadfly [c91e804a-d5a3-530f-b6f0-dfbca275c004] to /home/tim/.julia/compiled/v1.0/Gadfly/DvECm.ji.",
+ "output_type": "error",
+ "traceback": [
+ "Failed to precompile Gadfly [c91e804a-d5a3-530f-b6f0-dfbca275c004] to /home/tim/.julia/compiled/v1.0/Gadfly/DvECm.ji.",
+ "",
+ "Stacktrace:",
+ " [1] error(::String) at ./error.jl:33",
+ " [2] macro expansion at ./logging.jl:313 [inlined]",
+ " [3] compilecache(::Base.PkgId, ::String) at ./loading.jl:1184",
+ " [4] macro expansion at ./logging.jl:311 [inlined]",
+ " [5] _require(::Base.PkgId) at ./loading.jl:941",
+ " [6] require(::Base.PkgId) at ./loading.jl:852",
+ " [7] macro expansion at ./logging.jl:311 [inlined]",
+ " [8] require(::Module, ::Symbol) at ./loading.jl:834",
+ " [9] top-level scope at In[6]:1"
+ ]
}
],
"source": [
@@ -2057,3956 +214,19 @@
},
{
"cell_type": "code",
- "execution_count": 9,
+ "execution_count": 7,
"metadata": {},
"outputs": [
{
- "data": {
- "image/png": "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",
- "image/svg+xml": [
- "\n",
- "\n"
- ],
- "text/html": [
- "\n",
- "\n"
- ],
- "text/plain": [
- "Plot(...)"
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "image/png": "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",
- "image/svg+xml": [
- "\n",
- "\n"
- ],
- "text/html": [
- "\n",
- "\n"
- ],
- "text/plain": [
- "Plot(...)"
- ]
- },
- "metadata": {},
- "output_type": "display_data"
+ "ename": "UndefVarError",
+ "evalue": "UndefVarError: plot not defined",
+ "output_type": "error",
+ "traceback": [
+ "UndefVarError: plot not defined",
+ "",
+ "Stacktrace:",
+ " [1] top-level scope at In[7]:7"
+ ]
}
],
"source": [
@@ -6014,10 +234,10 @@
"ys = 1:8\n",
"g = Float64[x^2 * sin(y) for x in xs, y in ys]\n",
"\n",
- "gitp_quad2d = interpolate(g, BSpline(Quadratic(Line())), OnCell())\n",
+ "gitp_quad2d = interpolate(g, BSpline(Quadratic(Line(OnCell()))))\n",
"\n",
"display(plot(x=xs,y=ys,z=g,Geom.contour))\n",
- "display(plot(x=1:.1:5, y=1:.1:8, z=[gitp_quad2d[x,y] for x in 1:.1:5, y in 1:.1:8], Geom.contour))"
+ "display(plot(x=1:.1:5, y=1:.1:8, z=[gitp_quad2d(x,y) for x in 1:.1:5, y in 1:.1:8], Geom.contour))"
]
},
{
@@ -6044,15 +264,15 @@
],
"metadata": {
"kernelspec": {
- "display_name": "Julia 0.6.1-pre",
+ "display_name": "Julia 1.0.0",
"language": "julia",
- "name": "julia-0.6"
+ "name": "julia-1.0"
},
"language_info": {
"file_extension": ".jl",
"mimetype": "application/julia",
"name": "julia",
- "version": "0.6.1"
+ "version": "1.0.1"
}
},
"nbformat": 4,
diff --git a/doc/Math.md b/doc/Math.md
index 27e0be67..7b6a707c 100644
--- a/doc/Math.md
+++ b/doc/Math.md
@@ -25,9 +25,7 @@ For higher interpolation degrees (specifically, from quadratic interpolation and
For quadratic interpolation, for example, a common boundary condition is to assume that the function is flat at the edges (i.e. the derivative there is 0). This lets us introduce an extra equation at each edge, through a finite approximation of the derivative, which closes the system. Another common way of terminating the interpolation is to extend the second-to-outermost all the way to the edge of the data set.
-## 3. Mid-point and on-grid interpolation
-
-In any discrete data representation, there is a *cell* associated with each data point. Depending on the application, it may make sense to consider the data points to represent either the *center* of the cells, or the *edges*. `Interpolations.jl` will support both of these, using the term *midpoint interpolation* for data sets where the data points are in the middle of the cells, and *on-grid interpolation* when the data points and cell boundaries coincide.
+One subtlety concerns the location at which the boundary conditions are applied: at the edge grid point (`OnGrid()`) or at the halfway mark to the first beyond-the-edge index (`OnCell()`). `Interpolations.jl` supports both of these for interpolation schemes affected by boundary conditions (quadratic and cubic).
## Interlude: the `Interpolation` type hierarchy
diff --git a/doc/Plotting examples.ipynb b/doc/Plotting examples.ipynb
index 196749d7..ca9533be 100644
--- a/doc/Plotting examples.ipynb
+++ b/doc/Plotting examples.ipynb
@@ -100120,7 +100120,7 @@
],
"metadata": {
"kernelspec": {
- "display_name": "Julia 0.6.1-pre",
+ "display_name": "Julia 0.6.4-pre",
"language": "julia",
"name": "julia-0.6"
},
@@ -100128,7 +100128,7 @@
"file_extension": ".jl",
"mimetype": "application/julia",
"name": "julia",
- "version": "0.6.1"
+ "version": "0.6.4"
}
},
"nbformat": 4,
diff --git a/doc/devdocs.md b/doc/devdocs.md
deleted file mode 100644
index c6bf8dd3..00000000
--- a/doc/devdocs.md
+++ /dev/null
@@ -1,123 +0,0 @@
-# Developer documentation
-
-Interpolations provides flexibility without compromising on performance by exploiting metaprogramming to
-generate streamlined code. However, for people new to metaprogramming this can can be a barrier.
-Fortunately, with a few tips a lot of the mystique goes away.
-
-## Looking under the hood
-
-First let's create an interpolation object:
-
- julia> using Interpolations
-
- julia> A = rand(5)
- 5-element Array{Float64,1}:
- 0.74838
- 0.995383
- 0.978916
- 0.134746
- 0.430876
-
- julia> yitp = interpolate(A, BSpline(Linear()), OnGrid())
- 5-element Interpolations.BSplineInterpolation{Float64,1,Float64,Interpolations.BSpline{Interpolations.Linear},Interpolations.OnGrid}:
- 0.74838
- 0.995383
- 0.978916
- 0.134746
- 0.430876
-
-We can use this object to learn a lot about how Interpolations works.
-For example, the key functionality provided by `yitp` is `getindex`, i.e., `itp[3.2]`.
-Where is this implemented?
-
- julia> @which yitp[3.2]
- getindex{T,N}(itp::Interpolations.BSplineInterpolation{T,N,TCoefs,IT<:Interpolations.BSpline{D<:Interpolations.Degree{N}},GT<:Interpolations.GridType},xs::Real) at /home/tim/.julia/v0.4/Interpolations/src/b-splines/indexing.jl:42
-
-Your specific output (and especially the line number) may differ, but the point is that you've now found out where this is implemented.
-If you take a look at that function definition, you might see something like this:
-
- @generated function getindex{T,N}(itp::BSplineInterpolation{T,N}, xs::Real)
- if N > 1
- error("Linear indexing is not supported for interpolation objects")
- end
- getindex_impl(itp)
- end
-
-This is a [generated function](http://docs.julialang.org/en/latest/manual/metaprogramming/#generated-functions), and you'll need to familiarize yourself with how these work.
-The "interesting" part of the function is the call to `getindex_impl`; we can see the code that gets generated like this:
-
- julia> Interpolations.getindex_impl(typeof(yitp))
- quote # /home/tim/.julia/v0.4/Interpolations/src/b-splines/indexing.jl, line 7:
- @nexprs 1 (d->begin # /home/tim/.julia/v0.4/Interpolations/src/b-splines/indexing.jl, line 7:
- x_d = xs[d]
- end) # line 11:
- begin # /home/tim/.julia/v0.4/Interpolations/src/b-splines/linear.jl, line 5:
- @nexprs 1 (d->begin # /home/tim/.julia/v0.4/Interpolations/src/b-splines/linear.jl, line 5:
- begin # /home/tim/.julia/v0.4/Interpolations/src/b-splines/linear.jl, line 6:
- ix_d = clamp(floor(Int,real(x_d)),1,size(itp,d) - 1) # line 7:
- ixp_d = ix_d + 1 # line 8:
- fx_d = x_d - ix_d
- end
- end)
- end # line 14:
- @nexprs 1 (d->begin # /home/tim/.julia/v0.4/Interpolations/src/b-splines/linear.jl, line 14:
- begin # /home/tim/.julia/v0.4/Interpolations/src/b-splines/linear.jl, line 20:
- c_d = 1 - fx_d # line 21:
- cp_d = fx_d
- end
- end) # line 17:
- @inbounds ret = c_1 * itp.coefs[ix_1] + cp_1 * itp.coefs[ixp_1] # line 18:
- ret
- end
-
-You can see that this code makes use of [Base.Cartesian](http://docs.julialang.org/en/latest/devdocs/cartesian/), which you may also need to study.
-However, the impact of these macros can be gleaned through `macroexpand`:
-
- julia> using Base.Cartesian
-
- julia> macroexpand(Interpolations.getindex_impl(typeof(yitp)))
- quote # /home/tim/.julia/v0.4/Interpolations/src/b-splines/indexing.jl, line 7:
- begin
- x_1 = xs[1]
- end # line 11:
- begin # /home/tim/.julia/v0.4/Interpolations/src/b-splines/linear.jl, line 5:
- begin
- begin # /home/tim/.julia/v0.4/Interpolations/src/b-splines/linear.jl, line 6:
- ix_1 = clamp(floor(Int,real(x_1)),1,size(itp,1) - 1) # line 7:
- ixp_1 = ix_1 + 1 # line 8:
- fx_1 = x_1 - ix_1
- end
- end
- end # line 14:
- begin
- begin # /home/tim/.julia/v0.4/Interpolations/src/b-splines/linear.jl, line 20:
- c_1 = 1 - fx_1 # line 21:
- cp_1 = fx_1
- end
- end # line 17:
- begin
- $(Expr(:boundscheck, false))
- begin
- ret = c_1 * itp.coefs[ix_1] + cp_1 * itp.coefs[ixp_1]
- $(Expr(:boundscheck, :(Base.pop)))
- end
- end # line 18:
- ret
- end
-
-This is probably starting to look like something you can read. Briefly, what's happening is:
-
-- `floor(Int,x_1)` gets clamped to the range `1:size(itp,1)-1` and assigned to `ix_1`; this is the lower-bound integer grid point for the *first dimension* (this is a one-dimensional problem, but in two or higher dimensions you'd have `ix_2`, etc.)
-- `ixp_1` is defined as `ix_1+1`; this is the upper-bound integer grid point. In Interpolations, `m` and `p` often mean "minus" and "plus", meaning the lower or upper grid point.
-- The fractional part is stored in `fx_1`
-- Position-coefficients `c_1` and `cp_1` associated with the lower and upper grid point are computed from `fx_1`
-- The interpolation is performed using the position-coefficients, grid points, and data-coefficients and stored in `ret`, which is returned.
-
-As useful exercises:
-
-- Try creating a 2-dimensional linear interpolation object and examine the created code
-- Create a `Quadratic` interpolation object and do the same
-
-Once you've gotten this far, you probably understand quite a lot about how Interpolations works.
-At this point, your best bet is to start looking into the helper functions used by `getindex_impl`;
-once you learn how to define these, you should be able to extend Interpolations to support new algorithms.
diff --git a/perf/benchmarks.jl b/perf/benchmarks.jl
index 0cba81e9..71e61814 100644
--- a/perf/benchmarks.jl
+++ b/perf/benchmarks.jl
@@ -15,27 +15,27 @@ end
@nexprs $N d->inds_d = inds[d]
s = zero(eltype(itp))
@inbounds @nloops $N i d->inds_d begin
- s += @nref($N, itp, i)
+ s += @ncall($N, itp, i)
end
s
end
end
function sumvalues_indices(itp)
- inds = indices(itp)
+ inds = axes(itp)
n = Int(round(10^(3/ndims(itp))))
- ntuple(d->collect(linspace(first(inds[d])+0.001, last(inds[d])-0.001, n)), ndims(itp))
+ ntuple(d->collect(range(first(inds[d])+0.001, stop=last(inds[d])-0.001, length=n)), ndims(itp))
end
-strip_prefix(str) = replace(str, "Interpolations.", "")
+strip_prefix(str::AbstractString) = replace(str, "Interpolations."=>"")
benchstr(::Type{T}) where {T<:Interpolations.GridType} = strip_prefix(string(T))
benchstr(::Type{Constant}) = "Constant()"
benchstr(::Type{Linear}) = "Linear()"
-benchstr(::Type{Quadratic{BC}}) where {BC<:Interpolations.Flag} =
- string("Quadratic(", strip_prefix(string(BC)), "())")
-benchstr(::Type{Cubic{BC}}) where {BC<:Interpolations.Flag} =
- string("Quadratic(", strip_prefix(string(BC)), "())")
+benchstr(::Type{Quadratic{BC}}, ::Type{GT}) where {BC<:Interpolations.BoundaryCondition,GT<:Interpolations.GridType} =
+ string("Quadratic(", strip_prefix(string(BC)), "(", strip_prefix(string(GT)), "()))")
+benchstr(::Type{Cubic{BC}}, ::Type{GT}) where {BC<:Interpolations.BoundaryCondition,GT<:Interpolations.GridType} =
+ string("Cubic(", strip_prefix(string(BC)), "(", strip_prefix(string(GT)), "()))")
groupstr(::Type{Constant}) = "constant"
groupstr(::Type{Linear}) = "linear"
@@ -49,35 +49,33 @@ for A in (collect(Float64, 1:3),
# Constant & Linear
for D in (Constant, Linear)
gstr = groupstr(D)
- for GT in (OnGrid, OnCell)
- Ac = copy(A)
- idstr = string(ndims(A), "d_", benchstr(D), '_', benchstr(GT))
- suite["bsplines"][gstr][string(idstr, "_construct")] =
- @benchmarkable interpolate($Ac, BSpline($D()), $GT())
- itp = interpolate(copy(A), BSpline(D()), GT())
- inds = sumvalues_indices(itp)
- suite["bsplines"][gstr][string(idstr, "_use")] =
- @benchmarkable sumvalues($itp, $inds)
- end
+ Ac = copy(A)
+ idstr = string(ndims(A), "d_", benchstr(D), '_', benchstr(OnGrid))
+ suite["bsplines"][gstr][string(idstr, "_construct")] =
+ @benchmarkable interpolate($Ac, BSpline($D()))
+ itp = interpolate(copy(A), BSpline(D()))
+ inds = sumvalues_indices(itp)
+ suite["bsplines"][gstr][string(idstr, "_use")] =
+ @benchmarkable sumvalues($itp, $inds)
end
# Quadratic
gstr = groupstr(Quadratic)
for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
Ac = copy(A)
- idstr = string(ndims(A), "d_", benchstr(Quadratic{BC}), '_', benchstr(GT))
+ idstr = string(ndims(A), "d_", benchstr(Quadratic{BC}, GT))
suite["bsplines"][gstr][string(idstr, "_construct")] =
- @benchmarkable interpolate($Ac, BSpline(Quadratic($BC())), $GT())
- itp = interpolate(copy(A), BSpline(Quadratic(BC())), GT())
+ @benchmarkable interpolate($Ac, BSpline(Quadratic($BC($GT()))))
+ itp = interpolate(copy(A), BSpline(Quadratic(BC(GT()))))
inds = sumvalues_indices(itp)
suite["bsplines"][gstr][string(idstr, "_use")] =
@benchmarkable sumvalues($itp, $inds)
end
for BC in (InPlace,InPlaceQ)
Ac = copy(A)
- idstr = string(ndims(A), "d_", benchstr(Quadratic{BC}), '_', benchstr(OnCell))
+ idstr = string(ndims(A), "d_", benchstr(Quadratic{BC}, OnCell))
suite["bsplines"][gstr][string(idstr, "_construct")] =
- @benchmarkable interpolate!($Ac, BSpline(Quadratic($BC())), OnCell())
- itp = interpolate!(copy(A), BSpline(Quadratic(BC())), OnCell())
+ @benchmarkable interpolate!($Ac, BSpline(Quadratic($BC(OnCell()))))
+ itp = interpolate!(copy(A), BSpline(Quadratic(BC(OnCell()))))
inds = sumvalues_indices(itp)
suite["bsplines"][gstr][string(idstr, "_use")] =
@benchmarkable sumvalues($itp, $inds)
@@ -86,10 +84,10 @@ for A in (collect(Float64, 1:3),
gstr = groupstr(Cubic)
for BC in (Flat,Line,Free,Periodic), GT in (OnGrid, OnCell)
Ac = copy(A)
- idstr = string(ndims(A), "d_", benchstr(Cubic{BC}), '_', benchstr(GT))
+ idstr = string(ndims(A), "d_", benchstr(Cubic{BC}, GT))
suite["bsplines"][gstr][string(idstr, "_construct")] =
- @benchmarkable interpolate($Ac, BSpline(Cubic($BC())), $GT())
- itp = interpolate(copy(A), BSpline(Cubic(BC())), GT())
+ @benchmarkable interpolate($Ac, BSpline(Cubic($BC($GT()))))
+ itp = interpolate(copy(A), BSpline(Cubic(BC(GT()))))
inds = sumvalues_indices(itp)
suite["bsplines"][gstr][string(idstr, "_use")] =
@benchmarkable sumvalues($itp, $inds)
@@ -101,7 +99,11 @@ paramspath = joinpath(dirname(@__FILE__), "params.json")
if isfile(paramspath)
loadparams!(suite, BenchmarkTools.load(paramspath)[1], :evals);
else
- info("Tuning suite (this may take a while)")
+ @info "Tuning suite (this may take a while)"
tune!(suite)
BenchmarkTools.save(paramspath, params(suite));
end
+
+# To run the benchmarks:
+# results = run(suite, verbose = true, seconds = 1)
+# BenchmarkTools.save(filename, results)
diff --git a/perf/compare_benchmarks.jl b/perf/compare_benchmarks.jl
new file mode 100644
index 00000000..929e373d
--- /dev/null
+++ b/perf/compare_benchmarks.jl
@@ -0,0 +1,60 @@
+# NOTE: create symlinks called "results_old.json" and "results_new.json" in this directory
+# to the results files you want to compare
+
+using BenchmarkTools, PyPlot, Unitful
+
+const tref = 1000
+
+function xycmp(results_old, results_new, filterstrs...)
+ x, y = Float64[], Float64[]
+ for (k, v) in results_new
+ passes = true
+ for str in filterstrs
+ passes &= occursin(str, k)
+ end
+ passes || continue
+ if haskey(results_old, k)
+ push!(x, minimum(results_old[k]).time/tref)
+ push!(y, minimum(v).time/tref)
+ end
+ end
+ x, y
+end
+
+function plotcmp(results_old, results_new, keys, filterstrs...)
+ for key in keys
+ results_old = results_old[key]
+ results_new = results_new[key]
+ end
+ x, y = xycmp(results_old, results_new, filterstrs...)
+ maxxy = max(maximum(x), maximum(y))
+ scatter(x, y)
+ plot([0, maxxy], [0, maxxy], "--")
+ # title(string(keys, filterstrs))
+ title(string(keys))
+end
+
+function plotcmppanels(results_old, results_new, keys, filterstrs...)
+ nrows = ceil(Int, sqrt(length(keys)))
+ ncols = ceil(Int, length(keys)/nrows)
+ figure()
+ for k = 1:length(keys)
+ subplot(nrows, ncols, k)
+ plotcmp(results_old, results_new, keys[k], filterstrs...)
+ end
+ suptitle(string(filterstrs))
+end
+
+results_old = BenchmarkTools.load("results_old.json")[1]
+results_new = BenchmarkTools.load("results_new.json")[1]
+
+results_old = results_old["bsplines"]
+results_new = results_new["bsplines"]
+
+for filt in (("use",), ("construct",))
+ # for keys in (("constant",), ("linear",), ("quadratic",), ("cubic",))
+ # plotcmp(results_old, results_new, keys, filt...)
+ # end
+ keys = (("constant",), ("linear",), ("quadratic",), ("cubic",))
+ plotcmppanels(results_old, results_new, keys, filt...)
+end
diff --git a/src/Interpolations.jl b/src/Interpolations.jl
index 5cb222bf..3bae69ed 100644
--- a/src/Interpolations.jl
+++ b/src/Interpolations.jl
@@ -1,5 +1,3 @@
-VERSION < v"0.7.0-beta2.199" && __precompile__()
-
module Interpolations
export
@@ -8,12 +6,6 @@ export
extrapolate,
scale,
- gradient!,
- gradient1,
- hessian!,
- hessian,
- hessian1,
-
AbstractInterpolation,
AbstractExtrapolation,
@@ -28,6 +20,7 @@ export
Natural,
InPlace,
InPlaceQ,
+ Throw,
LinearInterpolation,
CubicSplineInterpolation
@@ -37,20 +30,11 @@ export
# extrapolation/extrapolation.jl
# scaling/scaling.jl
-using Compat
-using Compat.LinearAlgebra, Compat.SparseArrays
-using WoodburyMatrices, Ratios, AxisAlgorithms, OffsetArrays
-
-import Base: convert, size, getindex, promote_rule,
- ndims, eltype, checkbounds
-
-@static if VERSION < v"0.7.0-DEV.3449"
- import Base: gradient
-else
- import LinearAlgebra: gradient
-end
+using LinearAlgebra, SparseArrays
+using StaticArrays, WoodburyMatrices, Ratios, AxisAlgorithms, OffsetArrays
-import Compat: axes
+using Base: @propagate_inbounds
+import Base: convert, size, axes, promote_rule, ndims, eltype, checkbounds, axes1
abstract type Flag end
abstract type InterpolationType <: Flag end
@@ -61,54 +45,338 @@ struct OnCell <: GridType end
const DimSpec{T} = Union{T,Tuple{Vararg{Union{T,NoInterp}}},NoInterp}
-abstract type AbstractInterpolation{T,N,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} <: AbstractArray{T,N} end
-abstract type AbstractInterpolationWrapper{T,N,ITPT,IT,GT} <: AbstractInterpolation{T,N,IT,GT} end
-abstract type AbstractExtrapolation{T,N,ITPT,IT,GT} <: AbstractInterpolationWrapper{T,N,ITPT,IT,GT} end
-
-struct Throw <: Flag end
-struct Flat <: Flag end
-struct Line <: Flag end
-struct Free <: Flag end
-struct Periodic <: Flag end
-struct Reflect <: Flag end
-struct InPlace <: Flag end
+abstract type AbstractInterpolation{T,N,IT<:DimSpec{InterpolationType}} <: AbstractArray{T,N} end
+abstract type AbstractInterpolationWrapper{T,N,ITPT,IT} <: AbstractInterpolation{T,N,IT} end
+abstract type AbstractExtrapolation{T,N,ITPT,IT} <: AbstractInterpolationWrapper{T,N,ITPT,IT} end
+
+"""
+ BoundaryCondition
+
+An abstract type with one of the following values (see the help for each for details):
+
+- `Throw(gt)`
+- `Flat(gt)`
+- `Line(gt)`
+- `Free(gt)`
+- `Periodic(gt)`
+- `Reflect(gt)`
+- `InPlace(gt)`
+- `InPlaceQ(gt)`
+
+where `gt` is the grid type, e.g., `OnGrid()` or `OnCell()`. `OnGrid` means that the boundary
+condition "activates" at the first and/or last integer location within the interpolation region,
+`OnCell` means the interpolation extends a half-integer beyond the edge before
+activating the boundary condition.
+"""
+abstract type BoundaryCondition <: Flag end
+# Put the gridtype into the boundary condition, since that's all it affects (see issue #228)
+# Nothing is used for extrapolation
+struct Throw{GT<:Union{GridType,Nothing}} <: BoundaryCondition gt::GT end
+struct Flat{GT<:Union{GridType,Nothing}} <: BoundaryCondition gt::GT end
+struct Line{GT<:Union{GridType,Nothing}} <: BoundaryCondition gt::GT end
+struct Free{GT<:Union{GridType,Nothing}} <: BoundaryCondition gt::GT end
+struct Periodic{GT<:Union{GridType,Nothing}} <: BoundaryCondition gt::GT end
+struct Reflect{GT<:Union{GridType,Nothing}} <: BoundaryCondition gt::GT end
+struct InPlace{GT<:Union{GridType,Nothing}} <: BoundaryCondition gt::GT end
# InPlaceQ is exact for an underlying quadratic. This is nice for ground-truth testing
# of in-place (unpadded) interpolation.
-struct InPlaceQ <: Flag end
+struct InPlaceQ{GT<:Union{GridType,Nothing}} <: BoundaryCondition gt::GT end
const Natural = Line
-@generated size(itp::AbstractInterpolation{T,N}) where {T, N} = Expr(:tuple, [:(size(itp, $i)) for i in 1:N]...)
-size(exp::AbstractExtrapolation, d) = size(exp.itp, d)
-bounds(itp::AbstractInterpolation{T,N}) where {T,N} = tuple(zip(lbounds(itp), ubounds(itp))...)
-bounds(itp::AbstractInterpolation{T,N}, d) where {T,N} = (lbound(itp,d),ubound(itp,d))
-@generated lbounds(itp::AbstractInterpolation{T,N}) where {T,N} = Expr(:tuple, [:(lbound(itp, $i)) for i in 1:N]...)
-@generated ubounds(itp::AbstractInterpolation{T,N}) where {T,N} = Expr(:tuple, [:(ubound(itp, $i)) for i in 1:N]...)
-lbound(itp::AbstractInterpolation{T,N}, d) where {T,N} = 1
-ubound(itp::AbstractInterpolation{T,N}, d) where {T,N} = size(itp, d)
-itptype(::Type{AbstractInterpolation{T,N,IT,GT}}) where {T,N,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} = IT
+(::Type{BC})() where BC<:BoundaryCondition = BC(nothing)
+function Base.show(io::IO, bc::BoundaryCondition)
+ print(io, nameof(typeof(bc)), '(')
+ bc.gt === nothing || show(io, bc.gt)
+ print(io, ')')
+end
+
+
+Base.IndexStyle(::Type{<:AbstractInterpolation}) = IndexCartesian()
+
+size(exp::AbstractExtrapolation) = size(exp.itp)
+axes(exp::AbstractExtrapolation) = axes(exp.itp)
+
+twotuple(r::AbstractUnitRange) = (first(r), last(r))
+twotuple(x, y) = (x, y)
+bounds(itp::AbstractInterpolation) = map(twotuple, lbounds(itp), ubounds(itp))
+bounds(itp::AbstractInterpolation, d) = bounds(itp)[d]
+
+itptype(::Type{AbstractInterpolation{T,N,IT}}) where {T,N,IT<:DimSpec{InterpolationType}} = IT
itptype(::Type{ITP}) where {ITP<:AbstractInterpolation} = itptype(supertype(ITP))
-itptype(itp::AbstractInterpolation ) = itptype(typeof(itp))
-gridtype(::Type{AbstractInterpolation{T,N,IT,GT}}) where {T,N,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} = GT
+itptype(itp::AbstractInterpolation) = itptype(typeof(itp))
+gridtype(::Type{AbstractInterpolation{T,N,IT}}) where {T,N,IT<:DimSpec{InterpolationType}} = GT
gridtype(::Type{ITP}) where {ITP<:AbstractInterpolation} = gridtype(supertype(ITP))
gridtype(itp::AbstractInterpolation) = gridtype(typeof(itp))
-ndims(::Type{AbstractInterpolation{T,N,IT,GT}}) where {T,N,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} = N
+ndims(::Type{AbstractInterpolation{T,N,IT}}) where {T,N,IT<:DimSpec{InterpolationType}} = N
ndims(::Type{ITP}) where {ITP<:AbstractInterpolation} = ndims(supertype(ITP))
ndims(itp::AbstractInterpolation) = ndims(typeof(itp))
-eltype(::Type{AbstractInterpolation{T,N,IT,GT}}) where {T,N,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} = T
+eltype(::Type{AbstractInterpolation{T,N,IT}}) where {T,N,IT<:DimSpec{InterpolationType}} = T
eltype(::Type{ITP}) where {ITP<:AbstractInterpolation} = eltype(supertype(ITP))
eltype(itp::AbstractInterpolation) = eltype(typeof(itp))
-count_interp_dims(::Type{T}, N) where {T<:AbstractInterpolation} = N
-# Generic indexing methods (fixes #183)
-Base.to_index(::AbstractInterpolation, i::Real) = i
+"""
+ n = count_interp_dims(ITP)
+
+Count the number of dimensions along which type `ITP` is interpolating.
+`NoInterp` dimensions do not contribute to the sum.
+"""
+count_interp_dims(::Type{ITP}) where {ITP<:AbstractInterpolation} = count_interp_dims(itptype(ITP), ndims(ITP))
+count_interp_dims(::Type{IT}, n) where {IT<:InterpolationType} = n * count_interp_dims(IT)
+count_interp_dims(it::Type{IT}, n) where IT<:Tuple{Vararg{InterpolationType,N}} where N =
+ _count_interp_dims(0, it...)
+@inline _count_interp_dims(c, ::IT1, args...) where IT1 =
+ _count_interp_dims(c + count_interp_dims(IT1), args...)
+_count_interp_dims(c) = c
+
+
+"""
+ wi = WeightedIndex(indexes, weights)
-@inline function Base._getindex(::IndexCartesian, A::AbstractInterpolation{T,N}, I::Vararg{Int,N}) where {T,N} # ambiguity resolution
- @inbounds r = getindex(A, I...)
- r
+Construct a weighted index `wi`, which can be thought of as a generalization of an
+ordinary array index to the context of interpolation.
+For an ordinary vector `a`, `a[i]` extracts the element at index `i`.
+When interpolating, one is typically interested in a range of indexes and the output is
+some weighted combination of array values at these indexes.
+For example, for linear interpolation between `i` and `i+1` we have
+
+ ret = (1-f)*a[i] + f*a[i]
+
+This can be represented `a[wi]`, where
+
+ wi = WeightedIndex(i:i+1, (1-f, f))
+
+i.e.,
+
+ ret = sum(a[indexes] .* weights)
+
+Linear interpolation thus constructs weighted indices using a 2-tuple for `weights` and
+a length-2 `indexes` range.
+Higher-order interpolation would involve more positions and weights (e.g., 3-tuples for
+quadratic interpolation, 4-tuples for cubic).
+
+In multiple dimensions, separable interpolation schemes are implemented in terms
+of multiple weighted indices, accessing `A[wi1, wi2, ...]` where each `wi` is the
+`WeightedIndex` along the corresponding dimension.
+
+For value interpolation, `weights` will typically sum to 1.
+However, for gradient and Hessian computation this will not necessarily be true.
+For example, the gradient of one-dimensional linear interpolation can be represented as
+
+ gwi = WeightedIndex(i:i+1, (-1, 1))
+ g1 = a[gwi]
+
+For a three-dimensional array `A`, one might compute `∂A/∂x₂` (the second component
+of the gradient) as `A[wi1, gwi2, wi3]`, where `wi1` and `wi3` are "value" weights
+and `gwi2` "gradient" weights.
+
+`indexes` may be supplied as a range or as a tuple of the same length as `weights`.
+The latter is applicable, e.g., for periodic boundary conditions.
+"""
+abstract type WeightedIndex{L,W} end
+
+# Type to use when array locations are adjacent. This may offer more opportunities
+# for compiler optimizations (e.g., SIMD).
+struct WeightedAdjIndex{L,W} <: WeightedIndex{L,W}
+ istart::Int
+ weights::NTuple{L,W}
+end
+# Type to use with non-adjacent locations. E.g., periodic boundary conditions.
+struct WeightedArbIndex{L,W} <: WeightedIndex{L,W}
+ indexes::NTuple{L,Int}
+ weights::NTuple{L,W}
end
-@inline function Base._getindex(::IndexCartesian, A::AbstractInterpolation{T,N}, I::Vararg{Real,N}) where {T,N}
- @inbounds r = getindex(A, I...)
- r
+
+function WeightedIndex(indexes::AbstractUnitRange{<:Integer}, weights::NTuple{L,Any}) where L
+ @noinline mismatch(indexes, weights) = throw(ArgumentError("the length of indexes must match weights, got $indexes vs $weights"))
+ length(indexes) == L || mismatch(indexes, weights)
+ WeightedAdjIndex(first(indexes), promote(weights...))
+end
+WeightedIndex(istart::Integer, weights::NTuple{L,Any}) where L =
+ WeightedAdjIndex(istart, promote(weights...))
+WeightedIndex(indexes::NTuple{L,Integer}, weights::NTuple{L,Any}) where L =
+ WeightedArbIndex(indexes, promote(weights...))
+
+weights(wi::WeightedIndex) = wi.weights
+indexes(wi::WeightedAdjIndex) = wi.istart
+indexes(wi::WeightedArbIndex) = wi.indexes
+
+# Make them iterable just like numbers are
+Base.iterate(x::WeightedIndex) = (x, nothing)
+Base.iterate(x::WeightedIndex, ::Any) = nothing
+Base.isempty(x::WeightedIndex) = false
+Base.length(x::WeightedIndex) = 1
+
+### Indexing with WeightedIndex
+
+# We inject indexing with `WeightedIndex` at a non-exported point in the dispatch heirarchy.
+# This is to avoid ambiguities with methods that specialize on the array type rather than
+# the index type.
+Base.to_indices(A, I::Tuple{Vararg{Union{Int,WeightedIndex}}}) = I
+@propagate_inbounds Base._getindex(::IndexLinear, A::AbstractVector, i::Int) = getindex(A, i) # ambiguity resolution
+@inline function Base._getindex(::IndexStyle, A::AbstractArray{T,N}, I::Vararg{Union{Int,WeightedIndex},N}) where {T,N}
+ interp_getindex(A, I, ntuple(d->0, Val(N))...)
+end
+
+# The non-generated version is currently disabled due to https://github.com/JuliaLang/julia/issues/29117
+# # This follows a "move processed indexes to the back" strategy, so J contains the yet-to-be-processed
+# # indexes and I all the processed indexes.
+# interp_getindex(A::AbstractArray{T,N}, J::Tuple{Int,Vararg{Any,L}}, I::Vararg{Int,M}) where {T,N,L,M} =
+# interp_getindex(A, Base.tail(J), I..., J[1])
+# function interp_getindex(A::AbstractArray{T,N}, J::Tuple{WeightedIndex,Vararg{Any,L}}, I::Vararg{Int,M}) where {T,N,L,M}
+# wi = J[1]
+# interp_getindex1(A, indexes(wi), weights(wi), Base.tail(J), I...)
+# end
+# interp_getindex(A::AbstractArray{T,N}, ::Tuple{}, I::Vararg{Int,N}) where {T,N} = # termination
+# @inbounds A[I...] # all bounds-checks have already happened
+#
+# ## Handle expansion of a single dimension
+# # version for WeightedAdjIndex
+# @inline interp_getindex1(A, i::Int, weights::NTuple{K,Any}, rest, I::Vararg{Int,M}) where {M,K} =
+# weights[1] * interp_getindex(A, rest, I..., i) + interp_getindex1(A, i+1, Base.tail(weights), rest, I...)
+# @inline interp_getindex1(A, i::Int, weights::Tuple{Any}, rest, I::Vararg{Int,M}) where M =
+# weights[1] * interp_getindex(A, rest, I..., i)
+# interp_getindex1(A, i::Int, weights::Tuple{}, rest, I::Vararg{Int,M}) where M =
+# error("exhausted the weights, this should never happen")
+#
+# # version for WeightedArbIndex
+# @inline interp_getindex1(A, indexes::NTuple{K,Int}, weights::NTuple{K,Any}, rest, I::Vararg{Int,M}) where {M,K} =
+# weights[1] * interp_getindex(A, rest, I..., indexes[1]) + interp_getindex1(A, Base.tail(indexes), Base.tail(weights), rest, I...)
+# @inline interp_getindex1(A, indexes::Tuple{Int}, weights::Tuple{Any}, rest, I::Vararg{Int,M}) where M =
+# weights[1] * interp_getindex(A, rest, I..., indexes[1])
+# interp_getindex1(A, indexes::Tuple{}, weights::Tuple{}, rest, I::Vararg{Int,M}) where M =
+# error("exhausted the weights and indexes, this should never happen")
+
+@inline interp_getindex(A::AbstractArray{T,N}, J::Tuple{Int,Vararg{Any,K}}, I::Vararg{Int,N}) where {T,N,K} =
+ interp_getindex(A, Base.tail(J), Base.tail(I)..., J[1])
+@generated function interp_getindex(A::AbstractArray{T,N}, J::Tuple{WeightedAdjIndex{L,W},Vararg{Any,K}}, I::Vararg{Int,N}) where {T,N,K,L,W}
+ ex = :(w[1]*interp_getindex(A, Jtail, Itail..., j))
+ for l = 2:L
+ ex = :(w[$l]*interp_getindex(A, Jtail, Itail..., j+$(l-1)) + $ex)
+ end
+ quote
+ $(Expr(:meta, :inline))
+ Jtail = Base.tail(J)
+ Itail = Base.tail(I)
+ j, w = J[1].istart, J[1].weights
+ $ex
+ end
+end
+@generated function interp_getindex(A::AbstractArray{T,N}, J::Tuple{WeightedArbIndex{L,W},Vararg{Any,K}}, I::Vararg{Int,N}) where {T,N,K,L,W}
+ ex = :(w[1]*interp_getindex(A, Jtail, Itail..., ij[1]))
+ for l = 2:L
+ ex = :(w[$l]*interp_getindex(A, Jtail, Itail..., ij[$l]) + $ex)
+ end
+ quote
+ $(Expr(:meta, :inline))
+ Jtail = Base.tail(J)
+ Itail = Base.tail(I)
+ ij, w = J[1].indexes, J[1].weights
+ $ex
+ end
+end
+@inline interp_getindex(A::AbstractArray{T,N}, ::Tuple{}, I::Vararg{Int,N}) where {T,N} = # termination
+ @inbounds A[I...] # all bounds-checks have already happened
+
+"""
+ w = value_weights(degree, δx)
+
+Compute the weights for interpolation of the value at an offset `δx` from the "base" position.
+`degree` describes the interpolation scheme.
+
+# Example
+
+```jldoctest
+julia> Interpolations.value_weights(Linear(), 0.2)
+(0.8, 0.2)
+```
+
+This corresponds to the fact that linear interpolation at `x + 0.2` is `0.8*y[x] + 0.2*y[x+1]`.
+"""
+function value_weights end
+
+"""
+ w = gradient_weights(degree, δx)
+
+Compute the weights for interpolation of the gradient at an offset `δx` from the "base" position.
+`degree` describes the interpolation scheme.
+
+# Example
+
+```jldoctest
+julia> Interpolations.gradient_weights(Linear(), 0.2)
+(-1.0, 1.0)
+```
+
+This defines the gradient of a linear interpolation at 3.2 as `y[4] - y[3]`.
+"""
+function gradient_weights end
+
+"""
+ w = hessian_weights(degree, δx)
+
+Compute the weights for interpolation of the hessian at an offset `δx` from the "base" position.
+`degree` describes the interpolation scheme.
+
+# Example
+
+```jldoctest
+julia> Interpolations.hessian_weights(Linear(), 0.2)
+(0.0, 0.0)
+```
+
+Linear interpolation uses straight line segments, so the second derivative is zero.
+"""
+function hessian_weights end
+
+
+gradient1(itp::AbstractInterpolation{T,1}, x) where {T} = gradient(itp, x)[1]
+hessian1(itp::AbstractInterpolation{T,1}, x) where {T} = hessian(itp, x)[1]
+
+### Supporting expansion of CartesianIndex
+
+const UnexpandedIndexTypes = Union{Number, AbstractVector, CartesianIndex}
+const ExpandedIndexTypes = Union{Number, AbstractVector}
+
+Base.to_index(::AbstractInterpolation, x::Number) = x
+
+# Commented out because you can't add methods to an abstract type.
+# @inline function (itp::AbstractInterpolation)(x::Vararg{UnexpandedIndexTypes})
+# itp(to_indices(itp, x)...)
+# end
+function gradient(itp::AbstractInterpolation, x::Vararg{UnexpandedIndexTypes})
+ xi = to_indices(itp, x)
+ xi == x && error("gradient of $itp not supported for position $x")
+ gradient(itp, xi...)
+end
+function gradient!(dest, itp::AbstractInterpolation, x::Vararg{UnexpandedIndexTypes})
+ gradient!(dest, itp, to_indices(itp, x)...)
+end
+function hessian(itp::AbstractInterpolation, x::Vararg{UnexpandedIndexTypes})
+ hessian(itp, to_indices(itp, x)...)
+end
+function hessian!(dest, itp::AbstractInterpolation, x::Vararg{UnexpandedIndexTypes})
+ hessian!(dest, itp, to_indices(itp, x)...)
+end
+
+# @inline function (itp::AbstractInterpolation)(x::Vararg{ExpandedIndexTypes})
+# itp.(Iterators.product(x...))
+# end
+# function gradient(itp::AbstractInterpolation, x::Vararg{ExpandedIndexTypes})
+# map(y->tgradient(itp, y), Iterators.product(x...))
+# end
+# function hessian(itp::AbstractInterpolation, x::Vararg{ExpandedIndexTypes})
+# map(y->thessian(itp, y), Iterators.product(x...))
+# end
+#
+# tgradient(itp, y) = gradient(itp, y...)
+# thessian(itp, y) = hessian(itp, y...)
+
+# getindex is supported only for Integer indices (deliberately)
+import Base: getindex
+@propagate_inbounds getindex(itp::AbstractInterpolation{T,N}, i::Vararg{Integer,N}) where {T,N} = itp(i...)
+@propagate_inbounds function getindex(itp::AbstractInterpolation{T,1}, i::Integer, j::Integer) where T
+ @boundscheck (j == 1 || Base.throw_boundserror(itp, (i, j)))
+ itp(i)
end
include("nointerp/nointerp.jl")
@@ -119,5 +387,6 @@ include("scaling/scaling.jl")
include("utils.jl")
include("io.jl")
include("convenience-constructors.jl")
+include("deprecations.jl")
end # module
diff --git a/src/b-splines/b-splines.jl b/src/b-splines/b-splines.jl
index 645e8cf5..0a6b2cbc 100644
--- a/src/b-splines/b-splines.jl
+++ b/src/b-splines/b-splines.jl
@@ -8,76 +8,119 @@ export
Cubic
abstract type Degree{N} <: Flag end
+abstract type DegreeBC{N} <: Degree{N} end # degree type supporting a BoundaryCondition
-struct BSpline{D<:Degree} <: InterpolationType end
-BSpline(::D) where {D<:Degree} = BSpline{D}()
+struct BSpline{D<:Degree} <: InterpolationType
+ degree::D
+end
bsplinetype(::Type{BSpline{D}}) where {D<:Degree} = D
+bsplinetype(::BS) where {BS<:BSpline} = bsplinetype(BS)
+
+degree(mode::BSpline) = mode.degree
+degree(::NoInterp) = NoInterp()
+
+iscomplete(mode::BSpline) = iscomplete(degree(mode))
+iscomplete(deg::DegreeBC) = _iscomplete(deg.bc.gt)
+iscomplete(deg::Degree) = true
+_iscomplete(::Nothing) = false
+_iscomplete(::GridType) = true
+
+function Base.show(io::IO, bs::BSpline)
+ print(io, "BSpline(")
+ show(io, degree(bs))
+ print(io, ')')
+end
+
+function Base.show(io::IO, deg::DegreeBC)
+ print(io, nameof(typeof(deg)), '(')
+ show(io, deg.bc)
+ print(io, ')')
+end
-struct BSplineInterpolation{T,N,TCoefs<:AbstractArray,IT<:DimSpec{BSpline},GT<:DimSpec{GridType},pad} <: AbstractInterpolation{T,N,IT,GT}
+struct BSplineInterpolation{T,N,TCoefs<:AbstractArray,IT<:DimSpec{BSpline},Axs<:Tuple{Vararg{AbstractUnitRange,N}}} <: AbstractInterpolation{T,N,IT}
coefs::TCoefs
+ parentaxes::Axs
+ it::IT
end
-function BSplineInterpolation(::Type{TWeights}, A::AbstractArray{Tel,N}, ::IT, ::GT, ::Val{pad}) where {N,Tel,TWeights<:Real,IT<:DimSpec{BSpline},GT<:DimSpec{GridType},pad}
- isconcretetype(IT) || error("The b-spline type must be a leaf type (was $IT)")
- isconcretetype(typeof(A)) || warn("For performance reasons, consider using an array of a concrete type (typeof(A) == $(typeof(A)))")
+function BSplineInterpolation(::Type{TWeights}, A::AbstractArray{Tel,N}, it::IT, axs) where {N,Tel,TWeights<:Real,IT<:DimSpec{BSpline}}
+ # String interpolation causes allocation, noinline avoids that unless they get called
+ @noinline err_concrete(IT) = error("The b-spline type must be a concrete type (was $IT)")
+ @noinline warn_concrete(A) = @warn("For performance reasons, consider using an array of a concrete type (typeof(A) == $(typeof(A)))")
+ @noinline err_incomplete(it) = error("OnGrid/OnCell is not supplied for some of the interpolation modes in $it")
- c = zero(TWeights)
- for _ in 2:N
- c *= c
- end
+ isconcretetype(IT) || err_concrete(IT)
+ isconcretetype(typeof(A)) || warn_concrete(A)
+ iscomplete(it) || err_incomplete(it)
+
+ # Compute the output element type when positions have type TWeights
if isempty(A)
- T = Base.promote_op(*, typeof(c), eltype(A))
+ T = Base.promote_op(*, TWeights, eltype(A))
else
- T = typeof(c * first(A))
+ T = typeof(zero(TWeights) * first(A))
end
- BSplineInterpolation{T,N,typeof(A),IT,GT,pad}(A)
+ BSplineInterpolation{T,N,typeof(A),IT,typeof(axs)}(A, fix_axis.(axs), it)
end
-# Utilities for working either with scalars or tuples/tuple-types
-iextract(::Type{T}, d) where {T<:BSpline} = T
-iextract(t, d) = t.parameters[d]
-padding(::Type{BSplineInterpolation{T,N,TCoefs,IT,GT,pad}}) where {T,N,TCoefs,IT,GT,pad} = pad
-padding(itp::AbstractInterpolation) = padding(typeof(itp))
-padextract(pad::Integer, d) = pad
-padextract(pad::Tuple{Vararg{Integer}}, d) = pad[d]
-
-lbound(itp::BSplineInterpolation{T,N,TCoefs,IT,OnGrid}, d::Integer) where {T,N,TCoefs,IT} =
- first(axes(itp, d))
-ubound(itp::BSplineInterpolation{T,N,TCoefs,IT,OnGrid}, d::Integer) where {T,N,TCoefs,IT} =
- last(axes(itp, d))
-lbound(itp::BSplineInterpolation{T,N,TCoefs,IT,OnCell}, d::Integer) where {T,N,TCoefs,IT} =
- first(axes(itp, d)) - 0.5
-ubound(itp::BSplineInterpolation{T,N,TCoefs,IT,OnCell}, d::Integer) where {T,N,TCoefs,IT} =
- last(axes(itp, d))+0.5
-
-lbound(itp::BSplineInterpolation{T,N,TCoefs,IT,OnGrid}, d, inds) where {T,N,TCoefs,IT} =
- first(inds)
-ubound(itp::BSplineInterpolation{T,N,TCoefs,IT,OnGrid}, d, inds) where {T,N,TCoefs,IT} =
- last(inds)
-lbound(itp::BSplineInterpolation{T,N,TCoefs,IT,OnCell}, d, inds) where {T,N,TCoefs,IT} =
- first(inds) - 0.5
-ubound(itp::BSplineInterpolation{T,N,TCoefs,IT,OnCell}, d, inds) where {T,N,TCoefs,IT} =
- last(inds)+0.5
-
-count_interp_dims(::Type{BSplineInterpolation{T,N,TCoefs,IT,GT,pad}}, n) where {T,N,TCoefs,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType},pad} = count_interp_dims(IT, n)
-
-function size(itp::BSplineInterpolation{T,N,TCoefs,IT,GT,pad}, d) where {T,N,TCoefs,IT,GT,pad}
- d <= N ? size(itp.coefs, d) - 2*padextract(pad, d) : 1
+iscomplete(its::Tuple) = all(iscomplete, its)
+
+coefficients(itp::BSplineInterpolation) = itp.coefs
+interpdegree(itp::BSplineInterpolation) = interpdegree(itpflag(itp))
+interpdegree(::BSpline{T}) where T = T()
+interpdegree(it::Tuple{Vararg{Union{BSpline,NoInterp},N}}) where N = interpdegree.(it)
+itpflag(itp::BSplineInterpolation) = itp.it
+
+size(itp::BSplineInterpolation) = map(length, itp.parentaxes)
+axes(itp::BSplineInterpolation) = itp.parentaxes
+
+lbounds(itp::BSplineInterpolation) = _lbounds(itp.parentaxes, itpflag(itp))
+ubounds(itp::BSplineInterpolation) = _ubounds(itp.parentaxes, itpflag(itp))
+_lbounds(axs, itp) = (lbound(axs[1], getfirst(itp)), _lbounds(Base.tail(axs), getrest(itp))...)
+_ubounds(axs, itp) = (ubound(axs[1], getfirst(itp)), _ubounds(Base.tail(axs), getrest(itp))...)
+_lbounds(::Tuple{}, itp) = ()
+_ubounds(::Tuple{}, itp) = ()
+
+lbound(ax::AbstractRange, bs::BSpline) = lbound(ax, degree(bs))
+lbound(ax::AbstractRange, deg::Degree) = first(ax)
+lbound(ax::AbstractRange, deg::DegreeBC) = lbound(ax, deg, deg.bc.gt)
+ubound(ax::AbstractRange, bs::BSpline) = ubound(ax, degree(bs))
+ubound(ax::AbstractRange, deg::Degree) = last(ax)
+ubound(ax::AbstractRange, deg::DegreeBC) = ubound(ax, deg, deg.bc.gt)
+
+lbound(ax::AbstractUnitRange, ::DegreeBC, ::OnCell) = first(ax) - 0.5
+ubound(ax::AbstractUnitRange, ::DegreeBC, ::OnCell) = last(ax) + 0.5
+lbound(ax::AbstractUnitRange, ::DegreeBC, ::OnGrid) = first(ax)
+ubound(ax::AbstractUnitRange, ::DegreeBC, ::OnGrid) = last(ax)
+
+fix_axis(r::Base.OneTo) = r
+fix_axis(r::Base.Slice) = r
+fix_axis(r::UnitRange) = Base.Slice(r)
+fix_axis(r::AbstractUnitRange) = fix_axis(UnitRange(r))
+
+count_interp_dims(::Type{BSI}, n) where BSI<:BSplineInterpolation = count_interp_dims(itptype(BSI), n)
+
+function interpolate(::Type{TWeights}, ::Type{TC}, A, it::IT) where {TWeights,TC,IT<:DimSpec{BSpline}}
+ Apad = prefilter(TWeights, TC, A, it)
+ BSplineInterpolation(TWeights, Apad, it, axes(A))
end
-@inline axes(itp::BSplineInterpolation{T,N,TCoefs,IT,GT,pad}) where {T,N,TCoefs,IT,GT,pad} =
- indices_removepad.(axes(itp.coefs), pad)
+"""
+ itp = interpolate(A, interpmode, gridstyle)
-function axes(itp::BSplineInterpolation{T,N,TCoefs,IT,GT,pad}, d) where {T,N,TCoefs,IT,GT,pad}
- d <= N ? indices_removepad(axes(itp.coefs, d), padextract(pad, d)) : axes(itp.coefs, d)
-end
+Interpolate an array `A` in the mode determined by `interpmode` and `gridstyle`.
+`interpmode` may be one of
-function interpolate(::Type{TWeights}, ::Type{TC}, A, it::IT, gt::GT) where {TWeights,TC,IT<:DimSpec{BSpline},GT<:DimSpec{GridType}}
- Apad, Pad = prefilter(TWeights, TC, A, IT, GT)
- BSplineInterpolation(TWeights, Apad, it, gt, Pad)
-end
-function interpolate(A::AbstractArray, it::IT, gt::GT) where {IT<:DimSpec{BSpline},GT<:DimSpec{GridType}}
- interpolate(tweight(A), tcoef(A), A, it, gt)
+- `BSpline(NoInterp())`
+- `BSpline(Linear())`
+- `BSpline(Quadratic(BC()))` (see [`BoundaryCondition`](@ref))
+- `BSpline(Cubic(BC()))`
+
+It may also be a tuple of such values, if you want to use different interpolation schemes along each axis.
+
+`gridstyle` should be one of `OnGrid()` or `OnCell()`.
+"""
+function interpolate(A::AbstractArray, it::IT) where {IT<:DimSpec{BSpline}}
+ interpolate(tweight(A), tcoef(A), A, it)
end
# We can't just return a tuple-of-types due to julia #12500
@@ -91,30 +134,18 @@ tcoef(A::AbstractArray{Float32}) = Float32
tcoef(A::AbstractArray{Rational{Int}}) = Rational{Int}
tcoef(A::AbstractArray{T}) where {T<:Integer} = typeof(float(zero(T)))
-interpolate!(::Type{TWeights}, A, it::IT, gt::GT) where {TWeights,IT<:DimSpec{BSpline},GT<:DimSpec{GridType}} = BSplineInterpolation(TWeights, prefilter!(TWeights, A, IT, GT), it, gt, Val{0}())
-function interpolate!(A::AbstractArray, it::IT, gt::GT) where {IT<:DimSpec{BSpline},GT<:DimSpec{GridType}}
- interpolate!(tweight(A), A, it, gt)
+function interpolate!(::Type{TWeights}, A::AbstractArray, it::IT) where {TWeights,IT<:DimSpec{BSpline}}
+ # Set the bounds of the interpolant inward, if necessary
+ axsA = axes(A)
+ axspad = padded_axes(axsA, it)
+ BSplineInterpolation(TWeights, prefilter!(TWeights, A, it), it, fix_axis.(padinset.(axsA, axspad)))
end
-
-offsetsym(off, d) = off == -1 ? Symbol("ixm_", d) :
- off == 0 ? Symbol("ix_", d) :
- off == 1 ? Symbol("ixp_", d) :
- off == 2 ? Symbol("ixpp_", d) : error("offset $off not recognized")
-
-# Ideally we might want to shift the indices symmetrically, but this
-# would introduce an inconsistency, so we just append on the right
-@inline indices_removepad(inds::Base.OneTo, pad) = Base.OneTo(length(inds) - 2*pad)
-@inline indices_removepad(inds, pad) = oftype(inds, first(inds):last(inds) - 2*pad)
-@inline indices_addpad(inds::Base.OneTo, pad) = Base.OneTo(length(inds) + 2*pad)
-@inline indices_addpad(inds, pad) = oftype(inds, first(inds):last(inds) + 2*pad)
-@inline indices_interior(inds, pad) = first(inds)+pad:last(inds)-pad
-
-@static if VERSION < v"0.7.0-DEV.3449"
- lut!(dl, d, du) = lufact!(Tridiagonal(dl, d, du), Val{false})
-else
- lut!(dl, d, du) = lu!(Tridiagonal(dl, d, du), Val(false))
+function interpolate!(A::AbstractArray, it::IT) where {IT<:DimSpec{BSpline}}
+ interpolate!(tweight(A), A, it)
end
+lut!(dl, d, du) = lu!(Tridiagonal(dl, d, du), Val(false))
+
include("constant.jl")
include("linear.jl")
include("quadratic.jl")
@@ -124,4 +155,5 @@ include("prefiltering.jl")
include("../filter1d.jl")
Base.parent(A::BSplineInterpolation{T,N,TCoefs,UT}) where {T,N,TCoefs,UT<:Union{BSpline{Linear},BSpline{Constant}}} = A.coefs
-Base.parent(A::BSplineInterpolation{T,N,TCoefs,UT}) where {T,N,TCoefs,UT} = throw(ArgumentError("The given BSplineInterpolation does not serve as a \"view\" for a parent array. This would only be true for Constant and Linear b-splines."))
+Base.parent(A::BSplineInterpolation{T,N,TCoefs,UT}) where {T,N,TCoefs,UT} =
+ throw(ArgumentError("The given BSplineInterpolation does not serve as a \"view\" for a parent array. This would only be true for Constant and Linear b-splines."))
diff --git a/src/b-splines/constant.jl b/src/b-splines/constant.jl
index 4ed4f657..037dbbcd 100644
--- a/src/b-splines/constant.jl
+++ b/src/b-splines/constant.jl
@@ -2,52 +2,18 @@ struct Constant <: Degree{0} end
"""
Constant b-splines are *nearest-neighbor* interpolations, and effectively
-return `A[round(Int,x)]` when interpolating
+return `A[round(Int,x)]` when interpolating.
"""
Constant
-"""
-`define_indices_d` for a constant b-spline calculates `ix_d = round(Int,x_d)`
-"""
-function define_indices_d(::Type{BSpline{Constant}}, d, pad)
- symix, symx = Symbol("ix_",d), Symbol("x_",d)
- :($symix = clamp(round(Int, $symx), first(inds_itp[$d]), last(inds_itp[$d])))
-end
-
-"""
-`coefficients` for a constant b-spline simply sets `c_d = 1` for compatibility
-with the general b-spline framework
-"""
-function coefficients(::Type{BSpline{Constant}}, N, d)
- sym, symx = Symbol("c_",d), Symbol("x_",d)
- :($sym = 1)
-end
-
-"""
-`gradient_coefficients` for a constant b-spline simply sets `c_d = 0` for
-compatibility with the general b-spline framework
-"""
-function gradient_coefficients(::Type{BSpline{Constant}}, d)
- sym, symx = Symbol("c_",d), Symbol("x_",d)
- :($sym = 0)
+function positions(::Constant, ax, x) # discontinuity occurs at half-integer locations
+ xm = roundbounds(x, ax)
+ δx = x - xm
+ fast_trunc(Int, xm), δx
end
-"""
-`hessian_coefficients` for a constant b-spline simply sets `c_d = 0` for
-compatibility with the general b-spline framework
-"""
-function hessian_coefficients(::Type{BSpline{Constant}}, d)
- sym = Symbol("c_",d)
- :($sym = 0)
-end
+value_weights(::Constant, δx) = (1,)
+gradient_weights(::Constant, δx) = (0,)
+hessian_weights(::Constant, δx) = (0,)
-function index_gen(::Type{BSpline{Constant}}, ::Type{IT}, N::Integer, offsets...) where IT<:DimSpec{BSpline}
- if (length(offsets) < N)
- d = length(offsets)+1
- sym = Symbol("c_", d)
- return :($sym * $(index_gen(IT, N, offsets..., 0)))
- else
- indices = [offsetsym(offsets[d], d) for d = 1:N]
- return :(itp.coefs[$(indices...)])
- end
-end
+padded_axis(ax::AbstractUnitRange, ::BSpline{Constant}) = ax
diff --git a/src/b-splines/cubic.jl b/src/b-splines/cubic.jl
index 847271d9..c6f152c2 100644
--- a/src/b-splines/cubic.jl
+++ b/src/b-splines/cubic.jl
@@ -1,5 +1,8 @@
-struct Cubic{BC<:Flag} <: Degree{3} end
-Cubic(::BC) where {BC<:Flag} = Cubic{BC}()
+struct Cubic{BC<:BoundaryCondition} <: DegreeBC{3}
+ bc::BC
+end
+
+(deg::Cubic)(gt::GridType) = Cubic(deg.bc(gt))
"""
Assuming uniform knots with spacing 1, the `i`th piece of cubic spline
@@ -24,145 +27,47 @@ When we derive boundary conditions we will use derivatives `y_0'(x)` and
"""
Cubic
-"""
-`define_indices_d` for a cubic b-spline calculates `ix_d = floor(x_d)` and
-`fx_d = x_d - ix_d` (corresponding to `i` `and `δx` in the docstring for
-`Cubic`), as well as auxiliary quantities `ixm_d`, `ixp_d` and `ixpp_d`
-"""
-function define_indices_d(::Type{BSpline{Cubic{BC}}}, d, pad) where BC
- symix, symixm, symixp = Symbol("ix_",d), Symbol("ixm_",d), Symbol("ixp_",d)
- symixpp, symx, symfx = Symbol("ixpp_",d), Symbol("x_",d), Symbol("fx_",d)
- quote
- # ensure that all of ix_d, ixm_d, ixp_d, and ixpp_d are in-bounds no
- # matter the value of pad
- $symix = clamp(floor(Int, $symx), first(inds_itp[$d]) + $(1-pad), last(inds_itp[$d]) + $(pad-2))
- $symfx = $symx - $symix
- $symix += $pad # padding for oob coefficient
- $symixm = $symix - 1
- $symixp = $symix + 1
- $symixpp = $symixp + 1
- end
+function positions(deg::Cubic, ax, x)
+ xf = floorbounds(x, ax)
+ xf -= ifelse(xf > last(ax)-1, oneunit(xf), zero(xf))
+ δx = x - xf
+ expand_index(deg, fast_trunc(Int, xf), ax, δx), δx
end
-"""
-`define_indices_d` for a cubic, periodic b-spline calculates `ix_d = floor(x_d)`
-and `fx_d = x_d - ix_d` (corresponding to `i` and `δx` in the docstring entry
-for `Cubic`), as well as auxiliary quantities `ixm_d`, `ixp_d` and `ixpp_d`.
+expand_index(::Cubic{BC}, xi::Number, ax::AbstractUnitRange, δx) where BC = xi-1
+expand_index(::Cubic{Periodic{GT}}, xi::Number, ax::AbstractUnitRange, δx) where GT<:GridType =
+ (modrange(xi-1, ax), modrange(xi, ax), modrange(xi+1, ax), modrange(xi+2, ax))
-If any `ixX_d` for `x ∈ {m, p, pp}` (note: not `c_d`) should fall outside of
-the data interval, they wrap around.
-"""
-function define_indices_d(::Type{BSpline{Cubic{Periodic}}}, d, pad)
- symix, symixm, symixp = Symbol("ix_",d), Symbol("ixm_",d), Symbol("ixp_",d)
- symixpp, symx, symfx = Symbol("ixpp_",d), Symbol("x_",d), Symbol("fx_",d)
- quote
- tmp = inds_itp[$d]
- $symix = clamp(floor(Int, $symx), first(tmp), last(tmp))
- $symfx = $symx - $symix
- $symixm = modrange($symix - 1, tmp)
- $symixp = modrange($symix + 1, tmp)
- $symixpp = modrange($symix + 2, tmp)
- end
+function value_weights(::Cubic, δx)
+ x3, xcomp3 = cub(δx), cub(1-δx)
+ (SimpleRatio(1,6) * xcomp3,
+ SimpleRatio(2,3) - sqr(δx) + SimpleRatio(1,2)*x3,
+ SimpleRatio(2,3) - sqr(1-δx) + SimpleRatio(1,2)*xcomp3,
+ SimpleRatio(1,6) * x3)
end
-padding(::Type{BSpline{Cubic{BC}}}) where {BC<:Flag} = Val{1}()
-padding(::Type{BSpline{Cubic{Periodic}}}) = Val{0}()
-
-"""
-In `coefficients` for a cubic b-spline we assume that `fx_d = x-ix_d`
-and we define `cX_d` for `X ⋹ {m, _, p, pp}` such that
-
- cm_d = p(fx_d)
- c_d = q(fx_d)
- cp_d = q(1-fx_d)
- cpp_d = p(1-fx_d)
-
-where `p` and `q` are defined in the docstring entry for `Cubic`, and
-`fx_d` in the docstring entry for `define_indices_d`.
-"""
-function coefficients(::Type{BSpline{C}}, N, d) where C<:Cubic
- symm, sym = Symbol("cm_",d), Symbol("c_",d)
- symp, sympp = Symbol("cp_",d) ,Symbol("cpp_",d)
- symfx = Symbol("fx_",d)
- symfx_cub = Symbol("fx_cub_", d)
- sym_1m_fx_cub = Symbol("one_m_fx_cub_", d)
- quote
- $symfx_cub = cub($symfx)
- $sym_1m_fx_cub = cub(1-$symfx)
- $symm = SimpleRatio(1,6)*$sym_1m_fx_cub
- $sym = SimpleRatio(2,3) - sqr($symfx) + SimpleRatio(1,2)*$symfx_cub
- $symp = SimpleRatio(2,3) - sqr(1-$symfx) + SimpleRatio(1,2)*$sym_1m_fx_cub
- $sympp = SimpleRatio(1,6)*$symfx_cub
- end
+function gradient_weights(::Cubic, δx)
+ x2, xcomp2 = sqr(δx), sqr(1-δx)
+ (-SimpleRatio(1,2) * xcomp2,
+ -2*δx + SimpleRatio(3,2)*x2,
+ +2*(1-δx) - SimpleRatio(3,2)*xcomp2,
+ SimpleRatio(1,2) * x2)
end
-"""
-In `gradient_coefficients` for a cubic b-spline we assume that `fx_d = x-ix_d`
-and we define `cX_d` for `X ⋹ {m, _, p, pp}` such that
+hessian_weights(::Cubic, δx) = (1-δx, 3*δx-2, 3*(1-δx)-2, δx)
- cm_d = p'(fx_d)
- c_d = q'(fx_d)
- cp_d = q'(1-fx_d)
- cpp_d = p'(1-fx_d)
-
-where `p` and `q` are defined in the docstring entry for `Cubic`, and
-`fx_d` in the docstring entry for `define_indices_d`.
-"""
-function gradient_coefficients(::Type{BSpline{C}}, d) where C<:Cubic
- symm, sym, symp, sympp = Symbol("cm_",d), Symbol("c_",d), Symbol("cp_",d), Symbol("cpp_",d)
- symfx = Symbol("fx_",d)
- symfx_sqr = Symbol("fx_sqr_", d)
- sym_1m_fx_sqr = Symbol("one_m_fx_sqr_", d)
- quote
- $symfx_sqr = sqr($symfx)
- $sym_1m_fx_sqr = sqr(1 - $symfx)
-
- $symm = -SimpleRatio(1,2) * $sym_1m_fx_sqr
- $sym = SimpleRatio(3,2) * $symfx_sqr - 2 * $symfx
- $symp = -SimpleRatio(3,2) * $sym_1m_fx_sqr + 2 * (1 - $symfx)
- $sympp = SimpleRatio(1,2) * $symfx_sqr
- end
-end
-
-"""
-In `hessian_coefficients` for a cubic b-spline we assume that `fx_d = x-ix_d`
-and we define `cX_d` for `X ⋹ {m, _, p, pp}` such that
-
- cm_d = p''(fx_d)
- c_d = q''(fx_d)
- cp_d = q''(1-fx_d)
- cpp_d = p''(1-fx_d)
-
-where `p` and `q` are defined in the docstring entry for `Cubic`, and
-`fx_d` in the docstring entry for `define_indices_d`.
-"""
-function hessian_coefficients(::Type{BSpline{C}}, d) where C<:Cubic
- symm, sym, symp, sympp = Symbol("cm_",d), Symbol("c_",d), Symbol("cp_",d), Symbol("cpp_",d)
- symfx = Symbol("fx_",d)
- quote
- $symm = 1 - $symfx
- $sym = 3 * $symfx - 2
- $symp = 1 - 3 * $symfx
- $sympp = $symfx
- end
-end
-
-function index_gen(::Type{BSpline{C}}, ::Type{IT}, N::Integer, offsets...) where {C<:Cubic,IT<:DimSpec{BSpline}}
- if length(offsets) < N
- d = length(offsets)+1
- symm, sym, symp, sympp = Symbol("cm_",d), Symbol("c_",d), Symbol("cp_",d), Symbol("cpp_",d)
- return :($symm * $(index_gen(IT, N, offsets...,-1)) + $sym * $(index_gen(IT, N, offsets..., 0)) +
- $symp * $(index_gen(IT, N, offsets..., 1)) + $sympp * $(index_gen(IT, N, offsets..., 2)))
- else
- indices = [offsetsym(offsets[d], d) for d = 1:N]
- return :(itp.coefs[$(indices...)])
- end
-end
# ------------ #
# Prefiltering #
# ------------ #
+padded_axis(ax::AbstractUnitRange, ::BSpline{<:Cubic}) = first(ax)-1:last(ax)+1
+padded_axis(ax::AbstractUnitRange, ::BSpline{Cubic{Periodic{GT}}}) where GT<:GridType = ax
+
+# # Due to padding we can extend the bounds
+# lbound(ax, ::BSpline{Cubic{BC}}, ::OnGrid) where BC = first(ax) - 0.5
+# ubound(ax, ::BSpline{Cubic{BC}}, ::OnGrid) where BC = last(ax) + 0.5
+
"""
`Cubic`: continuity in function value, first and second derivatives yields
@@ -171,7 +76,7 @@ end
1/6 2/3 1/6
⋱ ⋱ ⋱
"""
-function inner_system_diags(::Type{T}, n::Int, ::Type{C}) where {T,C<:Cubic}
+function inner_system_diags(::Type{T}, n::Int, ::Cubic) where {T}
du = fill(convert(T, SimpleRatio(1, 6)), n-1)
d = fill(convert(T, SimpleRatio(2, 3)), n)
dl = copy(du)
@@ -185,8 +90,8 @@ Applying this condition yields
-cm + cp = 0
"""
function prefiltering_system(::Type{T}, ::Type{TC}, n::Int,
- ::Type{Cubic{Flat}}, ::Type{OnGrid}) where {T,TC}
- dl, d, du = inner_system_diags(T, n, Cubic{Flat})
+ degree::Cubic{Flat{OnGrid}}) where {T,TC}
+ dl, d, du = inner_system_diags(T, n, degree)
d[1] = d[end] = -oneunit(T)
du[1] = dl[end] = zero(T)
@@ -211,8 +116,8 @@ were to use `y_0'(x)` we would have to introduce new coefficients, so that would
close the system. Instead, we extend the outermost polynomial for an extra half-cell.)
"""
function prefiltering_system(::Type{T}, ::Type{TC}, n::Int,
- ::Type{Cubic{Flat}}, ::Type{OnCell}) where {T,TC}
- dl, d, du = inner_system_diags(T,n,Cubic{Flat})
+ degree::Cubic{Flat{OnCell}}) where {T,TC}
+ dl, d, du = inner_system_diags(T,n,degree)
d[1] = d[end] = -9
du[1] = dl[end] = 11
@@ -240,8 +145,8 @@ were to use `y_0'(x)` we would have to introduce new coefficients, so that would
close the system. Instead, we extend the outermost polynomial for an extra half-cell.)
"""
function prefiltering_system(::Type{T}, ::Type{TC}, n::Int,
- ::Type{Cubic{Line}}, ::Type{OnCell}) where {T,TC}
- dl,d,du = inner_system_diags(T,n,Cubic{Line})
+ degree::Cubic{Line{OnCell}}) where {T,TC}
+ dl,d,du = inner_system_diags(T,n,degree)
d[1] = d[end] = 3
du[1] = dl[end] = -7
@@ -265,8 +170,8 @@ condition gives:
1 cm -2 c + 1 cp = 0
"""
function prefiltering_system(::Type{T}, ::Type{TC}, n::Int,
- ::Type{Cubic{Line}}, ::Type{OnGrid}) where {T,TC}
- dl,d,du = inner_system_diags(T,n,Cubic{Line})
+ degree::Cubic{Line{OnGrid}}) where {T,TC}
+ dl,d,du = inner_system_diags(T,n,degree)
d[1] = d[end] = 1
du[1] = dl[end] = -2
@@ -289,8 +194,8 @@ as periodic, yielding
where `N` is the number of data points.
"""
function prefiltering_system(::Type{T}, ::Type{TC}, n::Int,
- ::Type{Cubic{Periodic}}, ::Type{GT}) where {T,TC,GT<:GridType}
- dl, d, du = inner_system_diags(T,n,Cubic{Periodic})
+ degree::Cubic{<:Periodic}) where {T,TC}
+ dl, d, du = inner_system_diags(T,n,degree)
specs = WoodburyMatrices.sparse_factors(T, n,
(1, n, du[1]),
@@ -308,8 +213,8 @@ continuous derivative at the second-to-last cell boundary; this means
1 cm -3 c + 3 cp -1 cpp = 0
"""
function prefiltering_system(::Type{T}, ::Type{TC}, n::Int,
- ::Type{Cubic{Free}}, ::Type{GT}) where {T,TC,GT<:GridType}
- dl, d, du = inner_system_diags(T,n,Cubic{Periodic})
+ degree::Cubic{<:Free}) where {T,TC}
+ dl, d, du = inner_system_diags(T,n,degree)
specs = WoodburyMatrices.sparse_factors(T, n,
(1, n, du[1]),
diff --git a/src/b-splines/indexing.jl b/src/b-splines/indexing.jl
index a7be1ced..6348281c 100644
--- a/src/b-splines/indexing.jl
+++ b/src/b-splines/indexing.jl
@@ -1,181 +1,218 @@
-using Base.Cartesian
+### Primary evaluation entry points (itp(x...), gradient(itp, x...), and hessian(itp, x...))
-import Base.getindex
+itpinfo(itp) = (tcollect(itpflag, itp), axes(itp))
-Base.IndexStyle(::Type{<:AbstractInterpolation}) = IndexCartesian()
-
-define_indices(::Type{IT}, N, pad) where {IT} = Expr(:block, Expr[define_indices_d(iextract(IT, d), d, padextract(pad, d)) for d = 1:N]...)
-
-coefficients(::Type{IT}, N) where {IT} = Expr(:block, Expr[coefficients(iextract(IT, d), N, d) for d = 1:N]...)
-
-function gradient_coefficients(::Type{IT}, N, dim) where IT<:DimSpec{BSpline}
- exs = Expr[d==dim ? gradient_coefficients(iextract(IT, dim), d) :
- coefficients(iextract(IT, d), N, d) for d = 1:N]
- Expr(:block, exs...)
-end
-function hessian_coefficients(::Type{IT}, N, dim1, dim2) where IT<:DimSpec{BSpline}
- exs = if dim1 == dim2
- Expr[d==dim1==dim2 ? hessian_coefficients(iextract(IT, dim), d) :
- coefficients(iextract(IT, d), N, d) for d in 1:N]
- else
- Expr[d==dim1 || d==dim2 ? gradient_coefficients(iextract(IT, dim), d) :
- coefficients(iextract(IT, d), N, d) for d in 1:N]
- end
- Expr(:block, exs...)
+@inline function (itp::BSplineInterpolation{T,N})(x::Vararg{Number,N}) where {T,N}
+ @boundscheck (checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x))
+ wis = weightedindexes((value_weights,), itpinfo(itp)..., x)
+ itp.coefs[wis...]
+end
+@propagate_inbounds function (itp::BSplineInterpolation{T,N})(x::Vararg{Number,M}) where {T,M,N}
+ inds, trailing = split_trailing(itp, x)
+ @boundscheck (check1(trailing) || Base.throw_boundserror(itp, x))
+ @assert length(inds) == N
+ itp(inds...)
+end
+@inline function (itp::BSplineInterpolation{T,N})(x::Vararg{Union{Number,AbstractVector},N}) where {T,N}
+ @boundscheck (checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x))
+ itps = tcollect(itpflag, itp)
+ wis = dimension_wis(value_weights, itps, axes(itp), x)
+ coefs = coefficients(itp)
+ ret = [coefs[i...] for i in Iterators.product(wis...)]
+ reshape(ret, shape(wis...))
+end
+
+@inline function gradient(itp::BSplineInterpolation{T,N}, x::Vararg{Number,N}) where {T,N}
+ @boundscheck checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x)
+ wis = weightedindexes((value_weights, gradient_weights), itpinfo(itp)..., x)
+ SVector(map(inds->itp.coefs[inds...], wis))
+end
+@propagate_inbounds function gradient!(dest, itp::BSplineInterpolation{T,N}, x::Vararg{Number,N}) where {T,N}
+ dest .= gradient(itp, x...)
+end
+
+@inline function hessian(itp::BSplineInterpolation{T,N}, x::Vararg{Number,N}) where {T,N}
+ @boundscheck checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x)
+ wis = weightedindexes((value_weights, gradient_weights, hessian_weights), itpinfo(itp)..., x)
+ symmatrix(map(inds->itp.coefs[inds...], wis))
+end
+@propagate_inbounds function hessian!(dest, itp::BSplineInterpolation{T,N}, x::Vararg{Number,N}) where {T,N}
+ dest .= hessian(itp, x...)
+end
+
+checkbounds(::Type{Bool}, itp::AbstractInterpolation, x::Vararg{ExpandedIndexTypes,N}) where N =
+ checklubounds(lbounds(itp), ubounds(itp), x)
+
+checklubounds(ls, us, xs) = _checklubounds(true, ls, us, xs)
+_checklubounds(tf::Bool, ls, us, xs::Tuple{Number, Vararg{Any}}) =
+ _checklubounds(tf & (ls[1] <= xs[1] <= us[1]), Base.tail(ls), Base.tail(us), Base.tail(xs))
+_checklubounds(tf::Bool, ls, us, xs::Tuple{AbstractVector, Vararg{Any}}) =
+ _checklubounds(tf & all(ls[1] .<= xs[1] .<= us[1]), Base.tail(ls), Base.tail(us), Base.tail(xs))
+_checklubounds(tf::Bool, ::Tuple{}, ::Tuple{}, ::Tuple{}) = tf
+
+# Leftovers from AbstractInterpolation
+@inline function (itp::BSplineInterpolation)(x::Vararg{UnexpandedIndexTypes})
+ itp(to_indices(itp, x)...)
+end
+@inline function (itp::BSplineInterpolation)(x::Vararg{ExpandedIndexTypes})
+ itp.(Iterators.product(x...))
end
-function index_gen(::Type{IT}, N::Integer, offsets...) where {IT}
- idx = index_gen(iextract(IT, min(length(offsets)+1, N)), IT, N, offsets...)
- @static if v"0.7-" ≤ VERSION < v"0.7.0-beta2.119"
- # this is to avoid https://github.com/JuliaLang/julia/issues/27907
- return convert(Union{Expr,Symbol}, idx)
- end
- return idx
+
+@inline function weightedindexes(fs::F, itpflags::NTuple{N,Flag}, knots::NTuple{N,AbstractVector}, xs::NTuple{N,Number}) where {F,N}
+ parts = map((flag, knotvec, x)->weightedindex_parts(fs, flag, knotvec, x), itpflags, knots, xs)
+ weightedindexes(parts...)
end
-function getindex_impl(itp::Type{BSplineInterpolation{T,N,TCoefs,IT,GT,Pad}}) where {T,N,TCoefs,IT<:DimSpec{BSpline},GT<:DimSpec{GridType},Pad}
- meta = Expr(:meta, :inline)
- quote
- $meta
- @nexprs $N d->(x_d = xs[d])
- inds_itp = axes(itp)
+weightedindexes(i::Vararg{Int,N}) where N = i # the all-NoInterp case
- # Calculate the indices of all coefficients that will be used
- # and define fx = x - xi in each dimension
- $(define_indices(IT, N, Pad))
+const PositionCoefs{P,C} = NamedTuple{(:position,:coefs),Tuple{P,C}}
+const ValueParts{P,W} = PositionCoefs{P,Tuple{W}}
+@inline weightedindexes(parts::Vararg{Union{Int,ValueParts},N}) where N = maybe_weightedindex.(positions.(parts), valuecoefs.(parts))
+maybe_weightedindex(i::Integer, _::Integer) = Int(i)
+maybe_weightedindex(pos, coefs::Tuple) = WeightedIndex(pos, coefs)
- # Calculate coefficient weights based on fx
- $(coefficients(IT, N))
+positions(i::Int) = i
+valuecoefs(i::Int) = i
+gradcoefs(i::Int) = i
+hesscoefs(i::Int) = i
+positions(t::PositionCoefs) = t.position
+valuecoefs(t::PositionCoefs) = t.coefs[1]
+gradcoefs(t::PositionCoefs) = t.coefs[2]
+hesscoefs(t::PositionCoefs) = t.coefs[3]
- # Generate the indexing expression
- @inbounds ret = $(index_gen(IT, N))
- ret
- end
+const GradParts{P,W1,W2} = PositionCoefs{P,Tuple{W1,W2}}
+function weightedindexes(parts::Vararg{Union{Int,GradParts},N}) where N
+ # Create (wis1, wis2, ...) where wisn is used to evaluate the gradient along the nth *chosen* dimension
+ # Example: if itp is a 3d interpolation of form (Linear, NoInterp, Quadratic) then we will return
+ # (gwi1, i2, wi3), (wi1, i2, gwi3)
+ # where wik are value-coefficient WeightedIndexes along dimension k
+ # gwik are gradient-coefficient WeightedIndexes along dimension k
+ # i2 is the integer index along dimension 2
+ # These will result in a 2-vector gradient.
+ # TODO: check whether this is inferrable
+ slot_substitute(parts, positions.(parts), valuecoefs.(parts), gradcoefs.(parts))
end
-@generated function getindex(itp::BSplineInterpolation{T,N}, xs::Number...) where {T,N}
- getindex_impl(itp)
+# Skip over NoInterp dimensions
+slot_substitute(kind::Tuple{Int,Vararg{Any}}, p, v, g) = slot_substitute(Base.tail(kind), p, v, g)
+# Substitute the dth dimension's gradient coefs for the remaining coefs
+slot_substitute(kind, p, v, g) = (maybe_weightedindex.(p, substitute_ruled(v, kind, g)), slot_substitute(Base.tail(kind), p, v, g)...)
+# Termination
+slot_substitute(kind::Tuple{}, p, v, g) = ()
+
+const HessParts{P,W1,W2,W3} = PositionCoefs{P,Tuple{W1,W2,W3}}
+function weightedindexes(parts::Vararg{Union{Int,HessParts},N}) where N
+ # Create (wis1, wis2, ...) where wisn is used to evaluate the nth *chosen* hessian component
+ # Example: if itp is a 3d interpolation of form (Linear, NoInterp, Quadratic) then we will return
+ # (hwi1, i2, wi3), (gwi1, i2, gwi3), (wi1, i2, hwi3)
+ # where wik are value-coefficient WeightedIndexes along dimension k
+ # gwik are 1st-derivative WeightedIndexes along dimension k
+ # hwik are 2nd-derivative WeightedIndexes along dimension k
+ # i2 is just the index along dimension 2
+ # These will result in a 2x2 hessian [hc1 hc2; hc2 hc3] where
+ # hc1 = coefs[hwi1, i2, wi3]
+ # hc2 = coefs[gwi1, i2, gwi3]
+ # hc3 = coefs[wi1, i2, hwi3]
+ slot_substitute(parts, parts, positions.(parts), valuecoefs.(parts), gradcoefs.(parts), hesscoefs.(parts))
end
-function (itp::BSplineInterpolation{T,N,TCoefs,IT,GT,pad})(args...) where {T,N,TCoefs,IT,GT,pad}
- # support function calls
- itp[args...]
+# Skip over NoInterp dimensions
+function slot_substitute(kind1::Tuple{Int,Vararg{Any}}, kind2::Tuple{Int,Vararg{Any}}, p, v, g, h)
+ @assert(kind1 == kind2)
+ kind = Base.tail(kind1)
+ slot_substitute(kind, kind, p, v, g, h)
end
+function slot_substitute(kind1, kind2::Tuple{Int,Vararg{Any}}, p, v, g, h)
+ kind = Base.tail(kind1)
+ slot_substitute(kind, kind, p, v, g, h)
+end
+slot_substitute(kind1::Tuple{Int,Vararg{Any}}, kind2, p, v, g, h) = slot_substitute(Base.tail(kind1), kind2, p, v, g, h)
+# Substitute the dth dimension's gradient coefs for the remaining coefs
+function slot_substitute(kind1::K, kind2::K, p, v, g, h) where K
+ (maybe_weightedindex.(p, substitute_ruled(v, kind1, h)), slot_substitute(Base.tail(kind1), kind2, p, v, g, h)...)
+end
+function slot_substitute(kind1, kind2, p, v, g, h)
+ ss = substitute_ruled(substitute_ruled(v, kind1, g), kind2, g)
+ (maybe_weightedindex.(p, ss), slot_substitute(Base.tail(kind1), kind2, p, v, g, h)...)
+end
+# Termination
+slot_substitute(kind1::Tuple{}, kind2::Tuple{Int,Vararg{Any}}, p, v, g, h) = _slot_substitute(kind1::Tuple{}, kind2, p, v, g, h)
+slot_substitute(kind1::Tuple{}, kind2, p, v, g, h) = _slot_substitute(kind1::Tuple{}, kind2, p, v, g, h)
+function _slot_substitute(kind1::Tuple{}, kind2, p, v, g, h)
+ # finish "column" and continue on to the next "column"
+ kind = Base.tail(kind2)
+ slot_substitute(kind, kind, p, v, g, h)
+end
+slot_substitute(kind1::Tuple{}, kind2::Tuple{}, p, v, g, h) = ()
+
+
+weightedindex_parts(fs::F, itpflag::BSpline, ax, x) where F =
+ weightedindex_parts(fs, degree(itpflag), ax, x)
-function gradient_impl(itp::Type{BSplineInterpolation{T,N,TCoefs,IT,GT,Pad}}) where {T,N,TCoefs,IT<:DimSpec{BSpline},GT<:DimSpec{GridType},Pad}
- meta = Expr(:meta, :inline)
- # For each component of the gradient, alternately calculate
- # coefficients and set component
- n = count_interp_dims(IT, N)
- exs = Array{Expr, 1}(undef, 2n)
- cntr = 0
- for d = 1:N
- if count_interp_dims(iextract(IT, d), 1) > 0
- cntr += 1
- exs[2cntr-1] = gradient_coefficients(IT, N, d)
- exs[2cntr] = :(@inbounds g[$cntr] = $(index_gen(IT, N)))
- end
- end
- gradient_exprs = Expr(:block, exs...)
- quote
- $meta
- length(g) == $n || throw(ArgumentError(string("The length of the provided gradient vector (", length(g), ") did not match the number of interpolating dimensions (", n, ")")))
- @nexprs $N d->(x_d = xs[d])
- inds_itp = axes(itp)
-
- # Calculate the indices of all coefficients that will be used
- # and define fx = x - xi in each dimension
- $(define_indices(IT, N, Pad))
-
- $gradient_exprs
-
- g
- end
+function weightedindex_parts(fs::F, deg::Degree, ax::AbstractUnitRange{<:Integer}, x) where F
+ pos, δx = positions(deg, ax, x)
+ (position=pos, coefs=fmap(fs, deg, δx))
end
+
# there is a Heisenbug, when Base.promote_op is inlined into getindex_return_type
# thats why we use this @noinline fence
@noinline _promote_mul(a,b) = Base.promote_op(*, a, b)
-@noinline function getindex_return_type(::Type{BSplineInterpolation{T,N,TCoefs,IT,GT,Pad}}, argtypes::Tuple) where {T,N,TCoefs,IT<:DimSpec{BSpline},GT<:DimSpec{GridType},Pad}
+@noinline function getindex_return_type(::Type{BSplineInterpolation{T,N,TCoefs,IT,Axs}}, argtypes::Tuple) where {T,N,TCoefs,IT<:DimSpec{BSpline},Axs}
reduce(_promote_mul, eltype(TCoefs), argtypes)
end
-function getindex_return_type(::Type{BSplineInterpolation{T,N,TCoefs,IT,GT,Pad}}, ::Type{I}) where {T,N,TCoefs,IT<:DimSpec{BSpline},GT<:DimSpec{GridType},Pad,I}
+function getindex_return_type(::Type{BSplineInterpolation{T,N,TCoefs,IT,Axs}}, ::Type{I}) where {T,N,TCoefs,IT<:DimSpec{BSpline},Axs,I}
_promote_mul(eltype(TCoefs), I)
end
-@generated function gradient!(g::AbstractVector, itp::BSplineInterpolation{T,N}, xs::Number...) where {T,N}
- length(xs) == N || error("Can only be called with $N indexes")
- gradient_impl(itp)
-end
-
-@generated function gradient!(g::AbstractVector, itp::BSplineInterpolation{T,N}, index::CartesianIndex{N}) where {T,N}
- args = [:(index[$d]) for d = 1:N]
- :(gradient!(g, itp, $(args...)))
-end
-
-# @eval uglyness required for disambiguation with method in Base
-for R in [:Real, :Any]
- @eval @generated function gradient(itp::AbstractInterpolation{T,N}, xs::$R...) where {T,N}
- n = count_interp_dims(itp, N)
- xargs = [:(xs[$d]) for d in 1:length(xs)]
- quote
- Tg = $(Expr(:call, :promote_type, T, [x <: AbstractArray ? eltype(x) : x for x in xs]...))
- gradient!(Array{Tg, 1}(undef, $n), itp, $(xargs...))
- end
- end
-end
-
-gradient1(itp::AbstractInterpolation{T,1}, x) where {T} = gradient(itp, x)[1]
-
-function hessian_impl(itp::Type{BSplineInterpolation{T,N,TCoefs,IT,GT,Pad}}) where {T,N,TCoefs,IT<:DimSpec{BSpline},GT<:DimSpec{GridType},Pad}
- meta = Expr(:meta, :inline)
- # For each component of the hessian, alternately calculate
- # coefficients and set component
- n = count_interp_dims(IT, N)
- exs = Expr[]
- cntr = 0
- for d1 in 1:N, d2 in 1:N
- if count_interp_dims(iextract(IT,d1), 1) > 0 && count_interp_dims(iextract(IT,d2),1) > 0
- cntr += 1
- push!(exs, hessian_coefficients(IT, N, d1, d2))
- push!(exs, :(@inbounds H[$cntr] = $(index_gen(IT, N))))
+# This handles round-towards-the-middle for points on half-integer edges
+roundbounds(x::Integer, bounds::Tuple{Real,Real}) = x
+roundbounds(x::Integer, bounds::AbstractUnitRange) = x
+roundbounds(x::Number, bounds::Tuple{Real,Real}) = _roundbounds(x, bounds)
+roundbounds(x::Number, bounds::AbstractUnitRange) = _roundbounds(x, bounds)
+function _roundbounds(x::Number, bounds::Union{Tuple{Real,Real}, AbstractUnitRange})
+ l, u = first(bounds), last(bounds)
+ h = half(x)
+ xh = x+h
+ ifelse(x < u+half(u), floor(xh), ceil(xh)-1)
+end
+
+floorbounds(x::Integer, ax::Tuple{Real,Real}) = x
+floorbounds(x::Integer, ax::AbstractUnitRange) = x
+floorbounds(x, ax::Tuple{Real,Real}) = _floorbounds(x, ax)
+floorbounds(x, ax::AbstractUnitRange) = _floorbounds(x, ax)
+function _floorbounds(x, ax::Union{Tuple{Real,Real}, AbstractUnitRange})
+ l = first(ax)
+ h = half(x)
+ ifelse(x < l, floor(x+h), floor(x+zero(h)))
+end
+
+half(x) = oneunit(x)/2
+
+symmatrix(h::NTuple{1,Any}) = SMatrix{1,1}(h)
+symmatrix(h::NTuple{3,Any}) = SMatrix{2,2}((h[1], h[2], h[2], h[3]))
+symmatrix(h::NTuple{6,Any}) = SMatrix{3,3}((h[1], h[2], h[3], h[2], h[4], h[5], h[3], h[5], h[6]))
+function symmatrix(h::Tuple{L,Any}) where L
+ @noinline incommensurate(L) = error("$L must be equal to N*(N+1)/2 for integer N")
+ N = ceil(Int, sqrt(L))
+ (N*(N+1))÷2 == L || incommensurate(L)
+ l = Matrix{Int}(undef, N, N)
+ l[:,1] = 1:N
+ idx = N
+ for j = 2:N, i = 1:N
+ if i < j
+ l[i,j] = l[j,i]
+ else
+ l[i,j] = (idx+=1)
end
end
- hessian_exprs = Expr(:block, exs...)
-
- quote
- $meta
- size(H) == ($n,$n) || throw(ArgumentError(string("The size of the provided Hessian matrix wasn't a square matrix of size ", size(H))))
- @nexprs $N d->(x_d = xs[d])
- inds_itp = axes(itp)
-
- $(define_indices(IT, N, Pad))
-
- $hessian_exprs
-
- H
- end
-end
-
-@generated function hessian!(H::AbstractMatrix, itp::BSplineInterpolation{T,N}, xs::Number...) where {T,N}
- length(xs) == N || throw(ArgumentError("Can only be called with $N indexes"))
- hessian_impl(itp)
-end
-
-@generated function hessian!(H::AbstractMatrix, itp::BSplineInterpolation{T,N}, index::CartesianIndex{N}) where {T,N}
- args = [:(index[$d]) for d in 1:N]
- :(hessian!(H, itp, $(args...)))
-end
-
-@generated function hessian(itp::AbstractInterpolation{T,N}, xs...) where {T,N}
- n = count_interp_dims(itp,N)
- xargs = [:(xs[$d]) for d in 1:length(xs)]
- quote
- TH = $(Expr(:call, :promote_type, T, [x <: AbstractArray ? eltype(x) : x for x in xs]...))
- hessian!(Array{TH, 2}(undef, $n,$n), itp, $(xargs...))
+ if @generated
+ hexprs = [:(h[$i]) for i in vec(l)]
+ :(SMatrix{$N,$N}($(hexprs...,)))
+ else
+ SMatrix{N,N}([h[i] for i in vec(l)]...)
end
end
-
-hessian1(itp::AbstractInterpolation{T,1}, x) where {T} = hessian(itp, x)[1,1]
diff --git a/src/b-splines/linear.jl b/src/b-splines/linear.jl
index 0680c057..eb46b6fb 100644
--- a/src/b-splines/linear.jl
+++ b/src/b-splines/linear.jl
@@ -1,7 +1,7 @@
-struct Linear <: Degree{1} end
+struct Linear <: Degree{1} end # boundary conditions not supported
"""
-Assuming uniform knots with spacing 1, the `i`th peice of linear b-spline
+Assuming uniform knots with spacing 1, the `i`th piece of linear b-spline
implemented here is defined as follows.
y_i(x) = c p(x) + cp p(1-x)
@@ -21,83 +21,16 @@ a piecewise linear function connecting each pair of neighboring data points.
"""
Linear
-"""
-`define_indices_d` for a linear b-spline calculates `ix_d = floor(x_d)` and
-`fx_d = x_d - ix_d` (corresponding to `i` and `δx` in the docstring for
-`Linear`), as well as the auxiliary quantity `ixp_d`
-"""
-function define_indices_d(::Type{BSpline{Linear}}, d, pad)
- symix, symixp, symfx, symx = Symbol("ix_",d), Symbol("ixp_",d), Symbol("fx_",d), Symbol("x_",d)
- quote
- $symix = clamp(floor(Int, $symx), first(inds_itp[$d]), last(inds_itp[$d])-1)
- $symixp = $symix + 1
- $symfx = $symx - $symix
- end
-end
-
-"""
-In `coefficients` for a linear b-spline we assume that `fx_d = x-ix-d` and
-we define `cX_d` for `X ∈ {_, p}` such that
-
- c_d = p(fx_d)
- cp_d = p(1-fx_d)
-
-where `p` is defined in the docstring entry for `Linear` and `fx_d` in the
-docstring entry for `define_indices_d`.
-"""
-function coefficients(::Type{BSpline{Linear}}, N, d)
- sym, symp, symfx = Symbol("c_",d), Symbol("cp_",d), Symbol("fx_",d)
- quote
- $sym = 1 - $symfx
- $symp = $symfx
- end
-end
-
-"""
-In `gradient_coefficients` for a linear b-spline we assume that `fx_d = x-ix_d`
-and we define `cX_d` for `X ⋹ {_, p}` such that
-
- c_d = p'(fx_d)
- cp_d = p'(1-fx_d)
-
-where `p` is defined in the docstring entry for `Linear`, and `fx_d` in the
-docstring entry for `define_indices_d`.
-"""
-function gradient_coefficients(::Type{BSpline{Linear}}, d)
- sym, symp, symfx = Symbol("c_",d), Symbol("cp_",d), Symbol("fx_",d)
- quote
- $sym = -1
- $symp = 1
- end
+function positions(::Linear, ax::AbstractUnitRange{<:Integer}, x)
+ f = floor(x)
+ # When x == last(ax) we want to use the x-1, x pair
+ f = ifelse(x == last(ax), f - oneunit(f), f)
+ fi = fast_trunc(Int, f)
+ return fi, x-f
end
-"""
-In `hessian_coefficients` for a linear b-spline we assume that `fx_d = x-ix_d`
-and we define `cX_d` for `X ⋹ {_, p}` such that
-
- c_d = p''(fx_d)
- cp_d = p''(1-fx_d)
+value_weights(::Linear, δx) = (1-δx, δx)
+gradient_weights(::Linear, δx) = (-oneunit(δx), oneunit(δx))
+hessian_weights(::Linear, δx) = (zero(δx), zero(δx))
-where `p` is defined in the docstring entry for `Linear`, and `fx_d` in the
-docstring entry for `define_indices_d`. (These are both ≡ 0.)
-"""
-function hessian_coefficients(::Type{BSpline{Linear}}, d)
- sym, symp = Symbol("c_",d), Symbol("cp_",d)
- quote
- $sym = $symp = 0
- end
-end
-
-# This assumes fractional values 0 <= fx_d <= 1, integral values ix_d and ixp_d (typically ixp_d = ix_d+1,
-#except at boundaries), and an array itp.coefs
-function index_gen(::Type{BSpline{Linear}}, ::Type{IT}, N::Integer, offsets...) where IT<:DimSpec{BSpline}
- if length(offsets) < N
- d = length(offsets)+1
- sym = Symbol("c_", d)
- symp = Symbol("cp_", d)
- return :($sym * $(index_gen(IT, N, offsets..., 0)) + $symp * $(index_gen(IT, N, offsets..., 1)))
- else
- indices = [offsetsym(offsets[d], d) for d = 1:N]
- return :(itp.coefs[$(indices...)])
- end
-end
+padded_axis(ax::AbstractUnitRange, ::BSpline{Linear}) = ax
diff --git a/src/b-splines/prefiltering.jl b/src/b-splines/prefiltering.jl
index b485f496..cd6ad9e0 100644
--- a/src/b-splines/prefiltering.jl
+++ b/src/b-splines/prefiltering.jl
@@ -1,99 +1,80 @@
-deval(::Val{N}) where {N} = N
-padding(::Type{IT}) where {IT<:BSpline} = Val{0}()
-@generated function padding(t::Type{IT}) where IT
- pad = [deval(padding(IT.parameters[d])) for d = 1:length(IT.parameters)]
- t = tuple(pad...)
- :(Val{$t}())
-end
-
-@noinline function padded_index(indsA::NTuple{N,AbstractUnitRange{Int}}, ::Val{pad}) where {N,pad}
- @static if VERSION < v"0.7.0-DEV.843"
- indspad = ntuple(i->indices_addpad(indsA[i], padextract(pad,i)), Val{N})
- indscp = ntuple(i->indices_interior(indspad[i], padextract(pad,i)), Val{N})
- else
- indspad = ntuple(i->indices_addpad(indsA[i], padextract(pad,i)), Val(N))
- indscp = ntuple(i->indices_interior(indspad[i], padextract(pad,i)), Val(N))
- end
- indscp, indspad
-end
+padded_axes(axs, it::InterpolationType) = (ax->padded_axis(ax, it)).(axs)
+padded_axes(axs::NTuple{N,AbstractUnitRange}, it::NTuple{N,InterpolationType}) where N =
+ padded_axis.(axs, it)
padded_similar(::Type{TC}, inds::Tuple{Vararg{Base.OneTo{Int}}}) where TC = Array{TC}(undef, length.(inds))
padded_similar(::Type{TC}, inds) where TC = OffsetArray{TC}(undef, inds)
-# despite Compat, julia doesn't support 0.6 copy! with CartesianIndices argument
-@static if isdefined(Base, :CartesianIndices)
- ct!(coefs, indscp, A, indsA) = copyto!(coefs, CartesianIndices(indscp), A, CartesianIndices(indsA))
-else
- ct!(coefs, indscp, A, indsA) = copyto!(coefs, CartesianRange(indscp), A, CartesianRange(indsA))
+# Narrow ax by the amount that axpad is larger
+padinset(ax::AbstractUnitRange, axpad) = 2*first(ax)-first(axpad):2*last(ax)-last(axpad)
+function padinset(ax::Base.OneTo, axpad::Base.OneTo)
+ # We don't have any types that pad asymmetrically. Therefore if they both start at 1,
+ # they must be the same
+ @assert ax == axpad
+ return ax
end
-copy_with_padding(A, ::Type{IT}) where {IT} = copy_with_padding(eltype(A), A, IT)
-function copy_with_padding(::Type{TC}, A, ::Type{IT}) where {TC,IT<:DimSpec{InterpolationType}}
- Pad = padding(IT)
+ct!(coefs, indscp, A, indsA) = copyto!(coefs, CartesianIndices(indscp), A, CartesianIndices(indsA))
+
+copy_with_padding(A, it) = copy_with_padding(eltype(A), A, it)
+function copy_with_padding(::Type{TC}, A, it::DimSpec{InterpolationType}) where {TC}
indsA = axes(A)
- indscp, indspad = padded_index(indsA, Pad)
+ indspad = padded_axes(indsA, it)
coefs = padded_similar(TC, indspad)
if indspad == indsA
coefs = copyto!(coefs, A)
else
fill!(coefs, zero(TC))
- ct!(coefs, indscp, A, indsA)
+ ct!(coefs, indsA, A, indsA)
end
- coefs, Pad
+ coefs
end
-prefilter!(::Type{TWeights}, A, ::Type{IT}, ::Type{GT}) where {TWeights, IT<:BSpline, GT<:GridType} = A
-function prefilter(::Type{TWeights}, ::Type{TC}, A, ::Type{IT}, ::Type{GT}) where {TWeights, TC, IT<:BSpline, GT<:GridType}
- coefs = padded_similar(TC, axes(A))
- prefilter!(TWeights, copyto!(coefs, A), IT, GT), Val{0}()
-end
-
-function prefilter(
- ::Type{TWeights}, ::Type{TC}, A::AbstractArray, ::Type{BSpline{IT}}, ::Type{GT}
- ) where {TWeights,TC,IT<:Union{Cubic,Quadratic},GT<:GridType}
- ret, Pad = copy_with_padding(TC, A, BSpline{IT})
- prefilter!(TWeights, ret, BSpline{IT}, GT), Pad
-end
+prefilter!(::Type{TWeights}, A::AbstractArray, ::BSpline{D}, ::GridType) where {TWeights,D<:Union{Constant,Linear}} = A
function prefilter(
- ::Type{TWeights}, ::Type{TC}, A::AbstractArray, ::Type{IT}, ::Type{GT}
- ) where {TWeights,TC,IT<:Tuple{Vararg{Union{BSpline,NoInterp}}},GT<:DimSpec{GridType}}
- ret, Pad = copy_with_padding(TC, A, IT)
- prefilter!(TWeights, ret, IT, GT), Pad
+ ::Type{TWeights}, ::Type{TC}, A::AbstractArray,
+ it::Union{BSpline,Tuple{Vararg{Union{BSpline,NoInterp}}}}
+ ) where {TWeights,TC}
+ ret = copy_with_padding(TC, A, it)
+ prefilter!(TWeights, ret, it)
end
function prefilter!(
- ::Type{TWeights}, ret::TCoefs, ::Type{BSpline{IT}}, ::Type{GT}
- ) where {TWeights,TCoefs<:AbstractArray,IT<:Union{Quadratic,Cubic},GT<:GridType}
+ ::Type{TWeights}, ret::TCoefs, it::BSpline
+ ) where {TWeights,TCoefs<:AbstractArray}
local buf, shape, retrs
- sz = map(length, axes(ret))
+ sz = size(ret)
first = true
for dim in 1:ndims(ret)
- M, b = prefiltering_system(TWeights, eltype(TCoefs), sz[dim], IT, GT)
- A_ldiv_B_md!(ret, M, ret, dim, b)
+ M, b = prefiltering_system(TWeights, eltype(TCoefs), sz[dim], degree(it))
+ A_ldiv_B_md!(popwrapper(ret), M, popwrapper(ret), dim, b)
end
ret
end
function prefilter!(
- ::Type{TWeights}, ret::TCoefs, ::Type{IT}, ::Type{GT}
- ) where {TWeights,TCoefs<:AbstractArray,IT<:Tuple{Vararg{Union{BSpline,NoInterp}}},GT<:DimSpec{GridType}}
+ ::Type{TWeights}, ret::TCoefs, its::Tuple{Vararg{Union{BSpline,NoInterp}}}
+ ) where {TWeights,TCoefs<:AbstractArray}
local buf, shape, retrs
sz = size(ret)
first = true
for dim in 1:ndims(ret)
- it = iextract(IT, dim)
+ it = iextract(its, dim)
if it != NoInterp
- M, b = prefiltering_system(TWeights, eltype(TCoefs), sz[dim], bsplinetype(it), iextract(GT, dim))
+ M, b = prefiltering_system(TWeights, eltype(TCoefs), sz[dim], degree(it))
if M != nothing
- A_ldiv_B_md!(ret, M, ret, dim, b)
+ A_ldiv_B_md!(popwrapper(ret), M, popwrapper(ret), dim, b)
end
end
end
ret
end
-prefiltering_system(::Any, ::Any, ::Any, ::Any, ::Any) = nothing, nothing
+prefiltering_system(::Any, ::Any, ::Any, ::Any) = nothing, nothing
+
+popwrapper(A) = A
+popwrapper(A::OffsetArray) = A.parent
"""
M, b = prefiltering_system{T,TC,GT<:GridType,D<:Degree}m(::T, ::Type{TC}, n::Int, ::Type{D}, ::Type{GT})
diff --git a/src/b-splines/quadratic.jl b/src/b-splines/quadratic.jl
index b79adc5c..47bc60cd 100644
--- a/src/b-splines/quadratic.jl
+++ b/src/b-splines/quadratic.jl
@@ -1,5 +1,8 @@
-struct Quadratic{BC<:Flag} <: Degree{2} end
-Quadratic(::BC) where {BC<:Flag} = Quadratic{BC}()
+struct Quadratic{BC<:BoundaryCondition} <: DegreeBC{2}
+ bc::BC
+end
+(deg::Quadratic)(gt::GridType) = Quadratic(deg.bc(gt))
+
"""
Assuming uniform knots with spacing 1, the `i`th piece of quadratic spline
@@ -23,129 +26,39 @@ When we derive boundary conditions we will use derivatives `y_1'(x-1)` and
"""
Quadratic
-"""
-`define_indices_d` for a quadratic b-spline calculates `ix_d = floor(x_d)` and
-`fx_d = x_d - ix_d` (corresponding to `i` `and `δx` in the docstring for
-`Quadratic`), as well as auxiliary quantities `ixm_d` and `ixp_d`
-"""
-function define_indices_d(::Type{BSpline{Quadratic{BC}}}, d, pad) where BC
- symix, symixm, symixp = Symbol("ix_",d), Symbol("ixm_",d), Symbol("ixp_",d)
- symx, symfx = Symbol("x_",d), Symbol("fx_",d)
- quote
- # ensure that all three ix_d, ixm_d, and ixp_d are in-bounds no matter
- # the value of pad
- $symix = clamp(round(Int, $symx), first(inds_itp[$d])+1-$pad, last(inds_itp[$d])+$pad-1)
- $symfx = $symx - $symix
- $symix += $pad # padding for oob coefficient
- $symixp = $symix + 1
- $symixm = $symix - 1
- end
-end
-function define_indices_d(::Type{BSpline{Quadratic{Periodic}}}, d, pad)
- symix, symixm, symixp = Symbol("ix_",d), Symbol("ixm_",d), Symbol("ixp_",d)
- symx, symfx = Symbol("x_",d), Symbol("fx_",d)
- quote
- $symix = clamp(round(Int, $symx), first(inds_itp[$d]), last(inds_itp[$d]))
- $symfx = $symx - $symix
- $symixp = modrange($symix + 1, inds_itp[$d])
- $symixm = modrange($symix - 1, inds_itp[$d])
- end
-end
-function define_indices_d(::Type{BSpline{Quadratic{BC}}}, d, pad) where BC<:Union{InPlace,InPlaceQ}
- symix, symixm, symixp = Symbol("ix_",d), Symbol("ixm_",d), Symbol("ixp_",d)
- symx, symfx = Symbol("x_",d), Symbol("fx_",d)
- pad == 0 || error("Use $BC only with interpolate!")
- quote
- # ensure that all three ix_d, ixm_d, and ixp_d are in-bounds no matter
- # the value of pad
- $symix = clamp(round(Int, $symx), first(inds_itp[$d]), last(inds_itp[$d]))
- $symfx = $symx - $symix
- $symix += $pad # padding for oob coefficient
- $symixp = min(last(inds_itp[$d]), $symix + 1)
- $symixm = max(first(inds_itp[$d]), $symix - 1)
- end
+function positions(deg::Quadratic, ax, x)
+ xm = roundbounds(x, ax)
+ δx = x - xm
+ expand_index(deg, fast_trunc(Int, xm), ax, δx), δx
end
-"""
-In `coefficients` for a quadratic b-spline we assume that `fx_d = x-ix_d`
-and we define `cX_d` for `X ⋹ {m, _, p}` such that
+value_weights(::Quadratic, δx) = (
+ sqr(δx - SimpleRatio(1,2))/2,
+ SimpleRatio(3,4) - sqr(δx),
+ sqr(δx + SimpleRatio(1,2))/2)
- cm_d = p(fx_d)
- c_d = q(fx_d)
- cp_d = p(1-fx_d)
+gradient_weights(::Quadratic, δx) = (
+ δx - SimpleRatio(1,2),
+ -2 * δx,
+ δx + SimpleRatio(1,2))
-where `p` and `q` are defined in the docstring entry for `Quadratic`, and
-`fx_d` in the docstring entry for `define_indices_d`.
-"""
-function coefficients(::Type{BSpline{Q}}, N, d) where Q<:Quadratic
- symm, sym, symp = Symbol("cm_",d), Symbol("c_",d), Symbol("cp_",d)
- symfx = Symbol("fx_",d)
- quote
- $symm = sqr($symfx - SimpleRatio(1,2))/2
- $sym = SimpleRatio(3,4) - sqr($symfx)
- $symp = sqr($symfx + SimpleRatio(1,2))/2
- end
-end
-
-"""
-In `gradient_coefficients` for a quadratic b-spline we assume that `fx_d = x-ix_d`
-and we define `cX_d` for `X ⋹ {m, _, p}` such that
+hessian_weights(::Quadratic, δx) = (oneunit(δx), -2*oneunit(δx), oneunit(δx))
- cm_d = p'(fx_d)
- c_d = q'(fx_d)
- cp_d = p'(1-fx_d)
+expand_index(::Quadratic, xi::Number, ax::AbstractUnitRange, δx) = xi-1 # uses WeightedAdjIndex
+# Others use WeightedArbIndex
+expand_index(::Quadratic{<:Periodic}, xi::Number, ax::AbstractUnitRange, δx) =
+ (modrange(xi-1, ax), modrange(xi, ax), modrange(xi+1, ax))
+expand_index(::Quadratic{BC}, xi::Number, ax::AbstractUnitRange, δx) where BC<:Union{InPlace,InPlaceQ} =
+ (max(xi-1, first(ax)), xi, min(xi+1, last(ax)))
-where `p` and `q` are defined in the docstring entry for `Quadratic`, and
-`fx_d` in the docstring entry for `define_indices_d`.
-"""
-function gradient_coefficients(::Type{BSpline{Q}}, d) where Q<:Quadratic
- symm, sym, symp = Symbol("cm_",d), Symbol("c_",d), Symbol("cp_",d)
- symfx = Symbol("fx_",d)
- quote
- $symm = $symfx - SimpleRatio(1,2)
- $sym = -2 * $symfx
- $symp = $symfx + SimpleRatio(1,2)
- end
-end
-
-"""
-In `hessian_coefficients` for a quadratic b-spline we assume that `fx_d = x-ix_d`
-and we define `cX_d` for `X ⋹ {m, _, p}` such that
-
- cm_d = p''(fx_d)
- c_d = q''(fx_d)
- cp_d = p''(1-fx_d)
-
-where `p` and `q` are defined in the docstring entry for `Quadratic`, and
-`fx_d` in the docstring entry for `define_indices_d`.
-"""
-function hessian_coefficients(::Type{BSpline{Q}}, d) where Q<:Quadratic
- symm, sym, symp = Symbol("cm_",d), Symbol("c_",d), Symbol("cp_",d)
- quote
- $symm = 1
- $sym = -2
- $symp = 1
- end
-end
-
-# This assumes integral values ixm_d, ix_d, and ixp_d,
-# coefficients cm_d, c_d, and cp_d, and an array itp.coefs
-function index_gen(::Type{BSpline{Q}}, ::Type{IT}, N::Integer, offsets...) where {Q<:Quadratic,IT<:DimSpec{BSpline}}
- if length(offsets) < N
- d = length(offsets)+1
- symm, sym, symp = Symbol("cm_",d), Symbol("c_",d), Symbol("cp_",d)
- return :($symm * $(index_gen(IT, N, offsets...,-1)) + $sym * $(index_gen(IT, N, offsets..., 0)) +
- $symp * $(index_gen(IT, N, offsets..., 1)))
- else
- indices = [offsetsym(offsets[d], d) for d = 1:N]
- return :(itp.coefs[$(indices...)])
- end
-end
+padded_axis(ax::AbstractUnitRange, ::BSpline{<:Quadratic}) = first(ax)-1:last(ax)+1
+padded_axis(ax::AbstractUnitRange, ::BSpline{Quadratic{BC}}) where BC<:Union{Periodic,InPlace,InPlaceQ} = ax
-padding(::Type{BSpline{Quadratic{BC}}}) where {BC<:Flag} = Val{1}()
-padding(::Type{BSpline{Quadratic{Periodic}}}) = Val{0}()
+# # Due to padding we can extend the bounds
+# lbound(ax, ::BSpline{Quadratic{BC}}, ::OnGrid) where BC = first(ax) - 0.5
+# ubound(ax, ::BSpline{Quadratic{BC}}, ::OnGrid) where BC = last(ax) + 0.5
-function inner_system_diags(::Type{T}, n::Int, ::Type{Q}) where {T,Q<:Quadratic}
+function inner_system_diags(::Type{T}, n::Int, ::Quadratic) where {T}
du = fill(convert(T, SimpleRatio(1,8)), n-1)
d = fill(convert(T, SimpleRatio(3,4)), n)
dl = copy(du)
@@ -158,22 +71,22 @@ end
-cm + c = 0
"""
-function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, ::Type{Quadratic{BC}}, ::Type{OnCell}) where {T,TC,BC<:Union{Flat,Reflect}}
- dl,d,du = inner_system_diags(T,n,Quadratic{BC})
+function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, degree::Quadratic{BC}) where {T,TC,BC<:Union{Flat{OnCell},Reflect{OnCell}}}
+ dl,d,du = inner_system_diags(T,n,degree)
d[1] = d[end] = -1
du[1] = dl[end] = 1
lut!(dl, d, du), zeros(TC, n)
end
-function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, ::Type{Quadratic{InPlace}}, ::Type{OnCell}) where {T,TC}
- dl,d,du = inner_system_diags(T,n,Quadratic{InPlace})
+function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, degree::Quadratic{InPlace{OnCell}}) where {T,TC}
+ dl,d,du = inner_system_diags(T,n,degree)
d[1] = d[end] = convert(T, SimpleRatio(7,8))
lut!(dl, d, du), zeros(TC, n)
end
# InPlaceQ continues the quadratic at 2 all the way down to 1 (rather than 1.5)
-function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, ::Type{Quadratic{InPlaceQ}}, ::Type{OnCell}) where {T,TC}
- dl,d,du = inner_system_diags(T,n,Quadratic{InPlaceQ})
+function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, degree::Quadratic{InPlaceQ{OnCell}}) where {T,TC}
+ dl,d,du = inner_system_diags(T,n,degree)
d[1] = d[end] = SimpleRatio(9,8)
dl[end] = du[1] = SimpleRatio(-1,4)
# Woodbury correction to add 1/8 for row 1, col 3 and row n, col n-2
@@ -181,8 +94,8 @@ function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, ::Type{Quadratic{InP
colspec = spzeros(T, 2, n)
valspec = zeros(T, 2, 2)
valspec[1,1] = valspec[2,2] = SimpleRatio(1,8)
- rowspec[1,1] = rowspec[n,2] = 1
- colspec[1,3] = colspec[2,n-2] = 1
+ rowspec[1,1] = rowspec[end,2] = 1
+ colspec[1,3] = colspec[2,end-2] = 1
Woodbury(lut!(dl, d, du), rowspec, valspec, colspec), zeros(TC, n)
end
@@ -192,8 +105,8 @@ end
-cm + cp = 0
"""
-function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, ::Type{Quadratic{BC}}, ::Type{OnGrid}) where {T,TC,BC<:Union{Flat,Reflect}}
- dl,d,du = inner_system_diags(T,n,Quadratic{BC})
+function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, degree::Quadratic{BC}) where {T,TC,BC<:Union{Flat{OnGrid},Reflect{OnGrid}}}
+ dl,d,du = inner_system_diags(T,n,degree)
d[1] = d[end] = -1
du[1] = dl[end] = 0
@@ -212,8 +125,8 @@ of `x` for a quadratic b-spline, these both yield
1 cm -2 c + 1 cp = 0
"""
-function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, ::Type{Quadratic{Line}}, ::Type{GT}) where {T,TC,GT<:GridType}
- dl,d,du = inner_system_diags(T,n,Quadratic{Line})
+function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, degree::Quadratic{<:Line}) where {T,TC}
+ dl,d,du = inner_system_diags(T,n,degree)
d[1] = d[end] = 1
du[1] = dl[end] = -2
@@ -232,8 +145,8 @@ that `y_1''(3/2) = y_2''(3/2)`, yielding
1 cm -3 c + 3 cp - cpp = 0
"""
-function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, ::Type{Quadratic{Free}}, ::Type{GT}) where {T,TC,GT<:GridType}
- dl,d,du = inner_system_diags(T,n,Quadratic{Free})
+function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, degree::Quadratic{<:Free}) where {T,TC}
+ dl,d,du = inner_system_diags(T,n,degree)
d[1] = d[end] = 1
du[1] = dl[end] = -3
@@ -254,8 +167,8 @@ by looking at the coefficients themselves as periodic, yielding
where `N` is the number of data points.
"""
-function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, ::Type{Quadratic{Periodic}}, ::Type{GT}) where {T,TC,GT<:GridType}
- dl,d,du = inner_system_diags(T,n,Quadratic{Periodic})
+function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, degree::Quadratic{<:Periodic}) where {T,TC}
+ dl,d,du = inner_system_diags(T,n,degree)
specs = WoodburyMatrices.sparse_factors(T, n,
(1, n, du[1]),
diff --git a/src/convenience-constructors.jl b/src/convenience-constructors.jl
index 487023c1..61123911 100644
--- a/src/convenience-constructors.jl
+++ b/src/convenience-constructors.jl
@@ -1,10 +1,10 @@
# convenience copnstructors for linear / cubic spline interpolations
# 1D version
-LinearInterpolation(range::T, vs; extrapolation_bc = Interpolations.Throw()) where {T <: AbstractRange} = extrapolate(scale(interpolate(vs, BSpline(Linear()), OnGrid()), range), extrapolation_bc)
-LinearInterpolation(range::T, vs; extrapolation_bc = Interpolations.Throw()) where {T <: AbstractArray} = extrapolate(interpolate((range, ), vs, Gridded(Linear())), extrapolation_bc)
-CubicSplineInterpolation(range::T, vs; bc = Interpolations.Line(), extrapolation_bc = Interpolations.Throw()) where {T <: AbstractRange} = extrapolate(scale(interpolate(vs, BSpline(Cubic(bc)), OnGrid()), range), extrapolation_bc)
+LinearInterpolation(range::T, vs; extrapolation_bc = Throw()) where {T <: AbstractRange} = extrapolate(scale(interpolate(vs, BSpline(Linear())), range), extrapolation_bc)
+LinearInterpolation(range::T, vs; extrapolation_bc = Throw()) where {T <: AbstractArray} = extrapolate(interpolate((range, ), vs, Gridded(Linear())), extrapolation_bc)
+CubicSplineInterpolation(range::T, vs; bc = Line(OnGrid()), extrapolation_bc = Throw()) where {T <: AbstractRange} = extrapolate(scale(interpolate(vs, BSpline(Cubic(bc))), range), extrapolation_bc)
# multivariate versions
-LinearInterpolation(ranges::NTuple{N,T}, vs; extrapolation_bc = Interpolations.Throw()) where {N,T <: AbstractRange} = extrapolate(scale(interpolate(vs, BSpline(Linear()), OnGrid()), ranges...), extrapolation_bc)
-LinearInterpolation(ranges::NTuple{N,T}, vs; extrapolation_bc = Interpolations.Throw()) where {N,T <: AbstractArray} = extrapolate(interpolate(ranges, vs, Gridded(Linear())), extrapolation_bc)
-CubicSplineInterpolation(ranges::NTuple{N,T}, vs; bc = Interpolations.Line(), extrapolation_bc = Interpolations.Throw()) where {N,T <: AbstractRange} = extrapolate(scale(interpolate(vs, BSpline(Cubic(bc)), OnGrid()), ranges...), extrapolation_bc)
+LinearInterpolation(ranges::NTuple{N,T}, vs; extrapolation_bc = Throw()) where {N,T <: AbstractRange} = extrapolate(scale(interpolate(vs, BSpline(Linear())), ranges...), extrapolation_bc)
+LinearInterpolation(ranges::NTuple{N,T}, vs; extrapolation_bc = Throw()) where {N,T <: AbstractArray} = extrapolate(interpolate(ranges, vs, Gridded(Linear())), extrapolation_bc)
+CubicSplineInterpolation(ranges::NTuple{N,T}, vs; bc = Line(OnGrid()), extrapolation_bc = Throw()) where {N,T <: AbstractRange} = extrapolate(scale(interpolate(vs, BSpline(Cubic(bc))), ranges...), extrapolation_bc)
diff --git a/src/deprecations.jl b/src/deprecations.jl
new file mode 100644
index 00000000..658f1afb
--- /dev/null
+++ b/src/deprecations.jl
@@ -0,0 +1,64 @@
+# deprecate getindex for non-integer numeric indices
+@deprecate getindex(itp::AbstractInterpolation{T,N}, i::Vararg{Number,N}) where {T,N} itp(i...)
+@deprecate getindex(itp::AbstractInterpolation{T,N}, i::Vararg{ExpandedIndexTypes,N}) where {T,N} itp(i...)
+
+for T in (:Throw, :Flat, :Line, :Free, :Periodic, :Reflect, :InPlace, :InPlaceQ)
+ @eval begin
+ # Support changing the gridtype for an instance
+ (::$T)(gt::GridType) = $T(gt)
+ $T{GT}() where GT<:GridType = $T(GT())
+ end
+end
+
+@deprecate interpolate(A::AbstractArray, ::NoInterp, ::GridType) interpolate(A, NoInterp())
+function interpolate(::Type{TWeights}, ::Type{TC}, A, it::IT, gt::GT) where {TWeights,TC,IT<:DimSpec{BSpline},GT<:DimSpec{GridType}}
+ bcs = create_bcs(it, gt)
+ Base.depwarn("interpolate($TWeights, $TC, A, $it, $gt) is deprecated, use interpolate($TWeights, $TC, A, $bcs)", :interpolate)
+ interpolate(TWeights, TC, A, bcs)
+end
+function interpolate(A::AbstractArray, it::IT, gt::GT) where {IT<:DimSpec{BSpline},GT<:DimSpec{GridType}}
+ bcs = create_bcs(it, gt)
+ Base.depwarn("interpolate(A, $it, $gt) is deprecated, use interpolate(A, $bcs)", :interpolate)
+ interpolate(A, bcs)
+end
+
+function interpolate!(::Type{TWeights}, A, it::IT, gt::GT) where {TWeights,IT<:DimSpec{BSpline},GT<:DimSpec{GridType}}
+ bcs = create_bcs(it, gt)
+ Base.depwarn("interpolate!($TWeights, A, $it, $gt) is deprecated, use interpolate!($TWeights, A, $bcs)", :interpolate)
+ interpolate!(TWeights, A, bcs)
+end
+function interpolate!(A, it::IT, gt::GT) where {TWeights,IT<:DimSpec{BSpline},GT<:DimSpec{GridType}}
+ bcs = create_bcs(it, gt)
+ Base.depwarn("interpolate!(A, $it, $gt) is deprecated, use interpolate!(A, $bcs)", :interpolate)
+ interpolate!(A, bcs)
+end
+
+# extrapolate(A, Linear()) should probably have been extrapolate(A, Line())
+# (Line<:BoundaryCondition but Linear<:Degree)
+# @deprecate extrapolate(itp::AbstractInterpolation{T,N,IT}, ::Linear) where {T,N,IT} extrapolate(itp, Line())
+
+const OldExtrapDimSpec = Union{Flag,Tuple{Vararg{Union{Flag,NTuple{2,Flag}}}}}
+function extrapolate(itp::AbstractInterpolation{T,N,IT}, etpflag::OldExtrapDimSpec) where {T,N,IT}
+ replacement = replace_linear_line(etpflag)
+ io = IOBuffer()
+ show(io, etpflag)
+ etpstring = String(take!(io))
+ show(io, replacement)
+ repstring = String(take!(io))
+ Base.depwarn("extrapolate(itp, $etpstring) is deprecated, use extrapolate(itp, $repstring) instead", :extrapolate)
+ extrapolate(itp, replacement)
+end
+
+create_bcs(it::BSpline, gt::GridType) = BSpline(create_bcs(degree(it), gt))
+create_bcs(it::NoInterp, gt::GridType) = it
+create_bcs(it::Constant, gt::GridType) = it
+create_bcs(it::Linear, gt::GridType) = it
+create_bcs(it::Quadratic, gt::GridType) = it(gt)
+create_bcs(it::Cubic, gt::GridType) = it(gt)
+create_bcs(it::Tuple, gt::Tuple) = map((i,g)->i(g), it, gt)
+create_bcs(it::Tuple, gt::GridType) = map(t->create_bcs(t, gt), it)
+create_bcs(it::Flag, gt::Tuple) = map(t->it(t), gt)
+
+replace_linear_line(::Linear) = Line()
+replace_linear_line(bc::BoundaryCondition) = bc
+replace_linear_line(etpflag::Tuple) = replace_linear_line.(etpflag)
diff --git a/src/extrapolation/extrap_prep.jl b/src/extrapolation/extrap_prep.jl
deleted file mode 100644
index a2cb2fee..00000000
--- a/src/extrapolation/extrap_prep.jl
+++ /dev/null
@@ -1,91 +0,0 @@
-"""
-The `extrap_prep` function is used by `getindex_impl` to generate the body of
-the `getindex` function for extrapolation objects.
-
-The methods of `extrap_prep` work in "layers", iteratively working out the exact
-expression needed.
-
-The first layer takes a specification of the extrapolation scheme(s) to be used
-and a `Val` object that specifies the dimensionality of the extrapolation object:
-`extrap_prep{T,N}(::Type{T}, Val{N})`. These methods only dispatch to the second
-layer, and need not be extended for new schemes.
-
-The second layer also takes a `Val` object that specifies a single dimension on
-which to work: `extrap_prep{T,N,d}(::Type{T}, ::Val{N}, ::Val{d}). The methods
-with this signature in src/extrapolation/extrap_prep.jl simply expand into a
-block with sub-expressions for handling too-low and too-high values separately
-(the third layer), but specific interpolation schemes can provide more specific
-methods for this layer that handle both ends simultaneously. For example, the
-`Flat` scheme has a layer-2 method that uses `clamp` to restrict the coordinate
-when used in both directions, but uses `min` and `max` when handling each end
-separately.
-
-The third layer, to which the second dispatches if no scheme-specific method is
-found, adds a final `Val` object with a symbol `:lo` or `:hi`:
-`extrap_prep{T,N,d,l}(::Type{T}, ::Val{N}, ::Val{d}, ::Val{l})`. These methods
-must be specified for each extrapolation scheme. However, the general framework
-takes care of expanding all possible tuple combinations, so individual schemes
-need only care about e.g. `T==Flat`.
-
-In addition to these methods, there is a similar three-layer method hierarchy
-for gradient evaluation, in which a `Val{:gradient}` is prepended to the other
-arguments:
-`extrap_prep{T,N,d,l}(::Val{:gradient}`, ::Type{T}, ::Val{N}, ::Val{d}, ::Val{l})`
-If nothing else is specified for the individual schemes, these methods forward
-to the same methods without the `:gradient` argument, i.e. the same behavior as
-for value extrapolation. This works well with all schemes that are simple
-coordinate transformations, but for anything else methods for the low- and high-
-value cases need to be implemented for each scheme.
-""" extrap_prep
-
-extrap_prep(::Type{T}, n::Val{1}) where {T} = quote
- inds_etp = axes(etp)
- $(extrap_prep(T, n, Val{1}()))
-end
-extrap_prep(::Type{Tuple{T}}, n::Val{1}) where {T} = extrap_prep(T, n)
-extrap_prep(::Type{Tuple{T,T}}, n::Val{1}) where {T} = extrap_prep(T, n)
-extrap_prep(::Type{Tuple{Tuple{T,T}}}, n::Val{1}) where {T} = extrap_prep(T, n)
-function extrap_prep(::Type{Tuple{S,T}}, n::Val{1}) where {S,T}
- quote
- inds_etp = axes(etp)
- $(extrap_prep(S, n, Val{1}(), Val{:lo}()))
- $(extrap_prep(T, n, Val{1}(), Val{:hi}()))
- end
-end
-extrap_prep(::Type{Tuple{Tuple{S,T}}}, n::Val{1}) where {S,T} = extrap_prep(Tuple{S,T}, n)
-
-# needed for ambiguity resolution
-extrap_prep(::Type{T}, ::Val{1}) where {T<:Tuple} = :(throw(ArgumentError("The 1-dimensional extrap configuration $T is not supported")))
-
-function extrap_prep(::Type{T}, n::Val{N}) where {T,N}
- exprs = [:(inds_etp = axes(etp))]
- for d in 1:N
- push!(exprs, extrap_prep(T, n, Val{d}()))
- end
- return Expr(:block, exprs...)
-end
-function extrap_prep(::Type{T}, n::Val{N}) where {N,T<:Tuple}
- length(T.parameters) == N || return :(throw(ArgumentError("The $N-dimensional extrap configuration $T is not supported - must be a tuple of length $N (was length $(length(T.parameters)))")))
- exprs = [:(inds_etp = axes(etp))]
- for d in 1:N
- Tdim = T.parameters[d]
- if Tdim <: Tuple
- length(Tdim.parameters) == 2 || return :(throw(ArgumentError("The extrap configuration $Tdim for dimension $d is not supported - must be a tuple of length 2 or a simple configuration type")))
- if Tdim.parameters[1] != Tdim.parameters[2]
- push!(exprs, extrap_prep(Tdim, n, Val{d}()))
- else
- push!(exprs, extrap_prep(Tdim.parameters[1], n, Val{d}()))
- end
- else
- push!(exprs, extrap_prep(Tdim, n, Val{d}()))
- end
- end
- return Expr(:block, exprs...)
-end
-extrap_prep(::Type{T}, n::Val{N}, dim::Val{d}) where {T,N,d} = extrap_prep(Tuple{T,T}, n, dim)
-function extrap_prep(::Type{Tuple{S,T}}, n::Val{N}, dim::Val{d}) where {S,T,N,d}
- quote
- $(extrap_prep(S, n, dim, Val{:lo}()))
- $(extrap_prep(T, n, dim, Val{:hi}()))
- end
-end
diff --git a/src/extrapolation/extrap_prep_gradient.jl b/src/extrapolation/extrap_prep_gradient.jl
deleted file mode 100644
index a31e5364..00000000
--- a/src/extrapolation/extrap_prep_gradient.jl
+++ /dev/null
@@ -1,54 +0,0 @@
-# See ?extrap_prep for documentation for all these methods
-
-extrap_prep(g::Val{:gradient}, ::Type{T}, n::Val{1}) where {T} = quote
- inds_etp = axes(etp)
- $(extrap_prep(g, T, n, Val{1}()))
-end
-extrap_prep(g::Val{:gradient}, ::Type{Tuple{T}}, n::Val{1}) where {T} = extrap_prep(g, T, n)
-extrap_prep(g::Val{:gradient}, ::Type{Tuple{T,T}}, n::Val{1}) where {T} = extrap_prep(g, T, n)
-extrap_prep(g::Val{:gradient}, ::Type{Tuple{Tuple{T,T}}}, n::Val{1}) where {T} = extrap_prep(g, T, n)
-function extrap_prep(g::Val{:gradient}, ::Type{Tuple{S,T}}, ::Val{1}) where {S,T}
- quote
- inds_etp = axes(etp)
- $(extrap_prep(g, S, n, Val{1}(), Val{:lo}()))
- $(extrap_prep(g, T, n, Val{1}(), Val{:hi}()))
- end
-end
-extrap_prep(g::Val{:gradient}, ::Type{Tuple{Tuple{S,T}}}, n::Val{1}) where {S,T} = extrap_prep(g, Tuple{S,T}, n)
-# needed for ambiguity resolution
-extrap_prep(::Val{:gradient}, ::Type{T}, ::Val{1}) where {T<:Tuple} = :(throw(ArgumentError("The 1-dimensional extrap configuration $T is not supported")))
-
-
-function extrap_prep(g::Val{:gradient}, ::Type{T}, n::Val{N}) where {T,N}
- Expr(:block, :(inds_etp = axes(etp)), [extrap_prep(g, T, n, Val{d}()) for d in 1:N]...)
-end
-
-function extrap_prep(g::Val{:gradient}, ::Type{T}, n::Val{N}) where {T<:Tuple,N}
- length(T.parameters) == N || return :(throw(ArgumentError("The $N-dimensional extrap configuration $T is not supported")))
- exprs = [:(inds_etp = axes(etp))]
- for d in 1:N
- Tdim = T.parameters[d]
- if Tdim <: Tuple
- length(Tdim.parameters) == 2 || return :(throw(ArgumentError("The extrap configuration $Tdim for dimension $d is not supported - must be a tuple of length 2 or a simple configuration type"))
- )
- if Tdim.parameters[1] != Tdim.parameters[2]
- push!(exprs, extrap_prep(g, Tdim, n, Val{d}()))
- else
- push!(exprs, extrap_prep(g, Tdim.parameters[1], n, Val{d}()))
- end
- else
- push!(exprs, extrap_prep(g, Tdim, n, Val{d}()))
- end
- end
- return Expr(:block, exprs...)
-end
-
-function extrap_prep(g::Val{:gradient}, ::Type{Tuple{S,T}}, n::Val{N}, dim::Val{d}) where {S,T,N,d}
- quote
- $(extrap_prep(g, S, n, dim, Val{:lo}()))
- $(extrap_prep(g, T, n, dim, Val{:hi}()))
- end
-end
-
-extrap_prep(g::Val{:gradient}, ::Type{T}, n::Val{N}, dim::Val{d}) where {T,N,d} = extrap_prep(g, Tuple{T,T}, n, dim)
-extrap_prep(g::Val{:gradient}, args...) = extrap_prep(args...)
diff --git a/src/extrapolation/extrapolation.jl b/src/extrapolation/extrapolation.jl
index fb873242..1adf2a01 100644
--- a/src/extrapolation/extrapolation.jl
+++ b/src/extrapolation/extrapolation.jl
@@ -1,14 +1,19 @@
-mutable struct Extrapolation{T,N,ITPT,IT,GT,ET} <: AbstractExtrapolation{T,N,ITPT,IT,GT}
+struct Extrapolation{T,N,ITPT,IT,ET} <: AbstractExtrapolation{T,N,ITPT,IT}
itp::ITPT
+ et::ET
end
Base.parent(A::Extrapolation) = A.itp
+itpflag(etp::Extrapolation) = itpflag(etp.itp)
# DimSpec{Flag} is not enough for extrapolation dispatch, since we allow nested tuples
# However, no tuples should be nested deeper than this; the first level is for different
# schemes in different dimensions, and the second level is for different schemes in
# different directions.
-const ExtrapDimSpec = Union{Flag,Tuple{Vararg{Union{Flag,NTuple{2,Flag}}}}}
+const ExtrapDimSpec = Union{BoundaryCondition,Tuple{Vararg{Union{BoundaryCondition,NTuple{2,BoundaryCondition}}}}}
+
+etptype(::Extrapolation{T,N,ITPT,IT,ET}) where {T,N,ITPT,IT,ET} = ET
+etpflag(etp::Extrapolation{T,N,ITPT,IT,ET}) where {T,N,ITPT,IT,ET} = etp.et
"""
`extrapolate(itp, scheme)` adds extrapolation behavior to an interpolation object, according to the provided scheme.
@@ -17,79 +22,130 @@ The scheme can take any of these values:
* `Throw` - throws a BoundsError for out-of-bounds indices
* `Flat` - for constant extrapolation, taking the closest in-bounds value
-* `Linear` - linear extrapolation (the wrapped interpolation object must support gradient)
+* `Line - linear extrapolation (the wrapped interpolation object must support gradient)
* `Reflect` - reflecting extrapolation (indices must support `mod`)
* `Periodic` - periodic extrapolation (indices must support `mod`)
-You can also combine schemes in tuples. For example, the scheme `(Linear(), Flat())` will use linear extrapolation in the first dimension, and constant in the second.
+You can also combine schemes in tuples. For example, the scheme `(Line), Flat())` will use linear extrapolation in the first dimension, and constant in the second.
-Finally, you can specify different extrapolation behavior in different direction. `((Linear(),Flat()), Flat())` will extrapolate linearly in the first dimension if the index is too small, but use constant etrapolation if it is too large, and always use constant extrapolation in the second dimension.
+Finally, you can specify different extrapolation behavior in different direction. `((Line),Flat()), Flat())` will extrapolate linearly in the first dimension if the index is too small, but use constant etrapolation if it is too large, and always use constant extrapolation in the second dimension.
"""
-extrapolate(itp::AbstractInterpolation{T,N,IT,GT}, ::ET) where {T,N,IT,GT,ET<:ExtrapDimSpec} =
- Extrapolation{T,N,typeof(itp),IT,GT,ET}(itp)
+extrapolate(itp::AbstractInterpolation{T,N,IT}, et::ET) where {T,N,IT,ET<:ExtrapDimSpec} =
+ Extrapolation{T,N,typeof(itp),IT,ET}(itp, et)
count_interp_dims(::Type{<:Extrapolation{T,N,ITPT}}, n) where {T,N,ITPT} = count_interp_dims(ITPT, n)
-include("throw.jl")
-include("flat.jl")
-include("linear.jl")
-include("reflect.jl")
-include("periodic.jl")
+@inline function (etp::Extrapolation{T,N})(x::Vararg{Number,N}) where {T,N}
+ itp = parent(etp)
+ eflag = etpflag(etp)
+ xs = inbounds_position(eflag, bounds(itp), x, etp, x)
+ extrapolate_value(eflag, skip_flagged_nointerp(itp, x), skip_flagged_nointerp(itp, xs), Tuple(gradient(itp, xs...)), itp(xs...))
+end
+@inline function (etp::Extrapolation{T,N})(x::Vararg{Union{Number,AbstractVector},N}) where {T,N}
+ itp = parent(etp)
+ Tret = typeof(lispyprod(zero(T), x...))
+ ret = zeros(Tret, shape(x...))
+ for (i, y) in zip(eachindex(ret), Iterators.product(x...))
+ ret[i] = etp(y...)
+ end
+ return ret
+end
-include("extrap_prep.jl")
-include("extrap_prep_gradient.jl")
+@inline function gradient(etp::AbstractExtrapolation{T,N}, x::Vararg{Number,N}) where {T,N}
+ itp = parent(etp)
+ if checkbounds(Bool, itp, x...)
+ gradient(itp, x...)
+ else
+ eflag = tcollect(etpflag, etp)
+ xs = inbounds_position(eflag, bounds(itp), x, etp, x)
+ g = gradient(itp, xs...)
+ skipni = t->skip_flagged_nointerp(itp, t)
+ SVector(extrapolate_gradient.(skipni(eflag), skipni(x), skipni(xs), Tuple(g)))
+ end
+end
-"""
-`getindex_impl(::Type{E<:Extrapolation}, xs...)`
+checkbounds(::Bool, ::AbstractExtrapolation, I...) = true
-Generates an expression to be used
-as the function body of the getindex method for the given type of extrapolation
-and indices. The heavy lifting is done by the `extrap_prep` function; see
-`?extrap_prep` for details.
-"""
-function getindex_impl(etp::Type{Extrapolation{T,N,ITPT,IT,GT,ET}}, xs...) where {T,N,ITPT,IT,GT,ET}
- coords = [Symbol("xs_",d) for d in 1:N]
- quote
- $(Expr(:meta, :inline))
- @nexprs $N d->(xs_d = xs[d])
- $(extrap_prep(ET, Val{N}()))
- etp.itp[$(coords...)]
- end
+# The last two arguments are just for error-reporting
+function inbounds_position(eflag, bounds, x, etp, xN)
+ item = inbounds_index(getfirst(eflag), bounds[1], x[1], etp, xN)
+ (item, inbounds_position(getrest(eflag), Base.tail(bounds), Base.tail(x), etp, xN)...)
+end
+inbounds_position(::Any, ::Tuple{}, ::Tuple{}, etp, xN) = ()
+
+# By default, convert all calls to 2-sided calls
+inbounds_index(flag::Flag, bounds, x, etp, xN) = inbounds_index((flag, flag), bounds, x, etp, xN)
+# But some one-sided calls can be handled more efficiently that way
+function inbounds_index(::Throw, (l,u), x, etp, xN)
+ @boundscheck(l <= x <= u || Base.throw_boundserror(etp, xN))
+ x
+end
+inbounds_index(::Periodic, (l,u), x, etp, xN) = periodic(x, l, u)
+inbounds_index(::Reflect, (l,u), x, etp, xN) = reflect(x, l, u)
+
+# Left-then-right implementations
+function inbounds_index((flagl,flagu)::Tuple{Throw,Flag}, (l,u), x, etp, xN)
+ @boundscheck(l <= x || Base.throw_boundserror(etp, xN))
+ inbounds_index((nothing,flagu), (l,u), x, etp, xN)
+end
+function inbounds_index((flagl,flagu)::Tuple{Nothing,Throw}, (l,u), x, etp, xN)
+ @boundscheck(x <= u || Base.throw_boundserror(etp, xN))
+ x
end
-@generated function getindex(etp::Extrapolation{T,N,ITPT,IT,GT,ET}, xs::Number...) where {T,N,ITPT,IT,GT,ET}
- getindex_impl(etp, xs...)
+function inbounds_index((flagl,flagu)::Tuple{Union{Flat,Line},Flag}, (l,u), x, etp, xN)
+ inbounds_index((nothing,flagu), (l,u), maxp(x,l), etp, xN)
+end
+function inbounds_index((flagl,flagu)::Tuple{Nothing,Union{Flat,Line}}, (l,u), x, etp, xN)
+ minp(x,u)
end
-function (etp::Extrapolation{T,N,ITPT,IT,GT,ET})(args...) where {T,N,ITPT,IT,GT,ET}
- # support function calls
- etp[args...]
+function inbounds_index((flagl,flagu)::Tuple{Periodic,Flag}, (l,u), x, etp, xN)
+ inbounds_index((nothing,flagu), (l,u), periodic(x, l, u), etp, xN)
+end
+function inbounds_index((flagl,flagu)::Tuple{Nothing,Periodic}, (l,u), x, etp, xN)
+ periodic(x, l, u)
end
-checkbounds(::AbstractExtrapolation,I...) = nothing
+function inbounds_index((flagl,flagu)::Tuple{Reflect,Flag}, (l,u), x, etp, xN)
+ inbounds_index((nothing,flagu), (l,u), reflect(x, l, u), etp, xN)
+end
+function inbounds_index((flagl,flagu)::Tuple{Nothing,Reflect}, (l,u), x, etp, xN)
+ reflect(x, l, u)
+end
+minp(a::T, b::T) where T = min(a, b)
+minp(a, b) = min(promote(a, b)...)
+maxp(a::T, b::T) where T = max(a, b)
+maxp(a, b) = max(promote(a, b)...)
-function gradient!_impl(g, etp::Type{Extrapolation{T,N,ITPT,IT,GT,ET}}, xs...) where {T,N,ITPT,IT,GT,ET}
- coords = [Symbol("xs_", d) for d in 1:N]
- quote
- $(Expr(:meta, :inline))
- @nexprs $N d->(xs_d = xs[d])
- $(extrap_prep(Val{:gradient}(), ET, Val{N}()))
- gradient!(g, etp.itp, $(coords...))
- end
+function reflect(y, l, u)
+ yr = mod(y - l, 2(u-l)) + l
+ return ifelse(yr > u, 2u-yr, yr)
end
+periodic(y, l, u) = mod(y-l, u-l) + l
+
-@generated function gradient!(g::AbstractVector, etp::Extrapolation{T,N,ITPT,IT,GT,ET}, xs...) where {T,N,ITPT,IT,GT,ET}
- gradient!_impl(g, etp, xs...)
+function extrapolate_value(eflag, x, xs, g, val)
+ val = extrapolate_axis(getfirst(eflag), x[1], xs[1], g[1], val)
+ extrapolate_value(getrest(eflag), Base.tail(x), Base.tail(xs), Base.tail(g), val)
end
+extrapolate_value(::Any, ::Tuple{}, ::Tuple{}, ::Tuple{}, val) = val
+
+extrapolate_axis(::Flag, x, xs, g, val) = val
+extrapolate_axis(::Line, x, xs, g, val) = val + (x-xs)*g
+
+extrapolate_axis((flagl,flagu)::Tuple{Flag,Flag}, x, xs, g, val) =
+ extrapolate_axis((nothing,flagu), x, xs, g, val)
+extrapolate_axis((flagl,flagu)::Tuple{Nothing,Flag}, x, xs, g, val) = val
+
+extrapolate_axis((flagl,flagu)::Tuple{Line, Flag}, x, xs, g, val) =
+ extrapolate_axis((nothing,flagu), x, xs, g, ifelse(x < xs, val + (x-xs)*g, val))
+extrapolate_axis((flagl,flagu)::Tuple{Nothing, Line}, x, xs, g, val) =
+ ifelse(x > xs, val + (x-xs)*g, val)
-lbound(etp::Extrapolation, d) = lbound(etp.itp, d)
-ubound(etp::Extrapolation, d) = ubound(etp.itp, d)
-lbound(etp::Extrapolation, d, inds) = lbound(etp.itp, d, inds)
-ubound(etp::Extrapolation, d, inds) = ubound(etp.itp, d, inds)
-size(etp::Extrapolation, d) = size(etp.itp, d)
-@inline axes(etp::AbstractExtrapolation) = axes(etp.itp)
-axes(etp::AbstractExtrapolation, d) = axes(etp.itp, d)
+extrapolate_gradient(::Flat, x, xs, g) = ifelse(x==xs, g, zero(g))
+extrapolate_gradient(::Flag, x, xs, g) = g
include("filled.jl")
diff --git a/src/extrapolation/filled.jl b/src/extrapolation/filled.jl
index 7df84925..a49dff46 100644
--- a/src/extrapolation/filled.jl
+++ b/src/extrapolation/filled.jl
@@ -1,47 +1,54 @@
-nindexes(N::Int) = N == 1 ? "1 index" : "$N indexes"
-
-mutable struct FilledExtrapolation{T,N,ITP<:AbstractInterpolation,IT,GT,FT} <: AbstractExtrapolation{T,N,ITP,IT,GT}
+mutable struct FilledExtrapolation{T,N,ITP<:AbstractInterpolation,IT,FT} <: AbstractExtrapolation{T,N,ITP,IT}
itp::ITP
fillvalue::FT
end
-function FilledExtrapolation(itp::AbstractInterpolation{T,N,IT,GT}, fillvalue) where {T,N,IT,GT}
+function FilledExtrapolation(itp::AbstractInterpolation{T,N,IT}, fillvalue) where {T,N,IT}
Te = promote_type(T,typeof(fillvalue))
- FilledExtrapolation{Te,N,typeof(itp),IT,GT,typeof(fillvalue)}(itp, fillvalue)
+ FilledExtrapolation{Te,N,typeof(itp),IT,typeof(fillvalue)}(itp, fillvalue)
end
Base.parent(A::FilledExtrapolation) = A.itp
+etpflag(A::FilledExtrapolation) = A.fillvalue
+itpflag(A::FilledExtrapolation) = itpflag(A.itp)
"""
-`extrapolate(itp, fillvalue)` creates an extrapolation object that returns the `fillvalue` any time the indexes in `itp[x1,x2,...]` are out-of-bounds.
+`extrapolate(itp, fillvalue)` creates an extrapolation object that returns the `fillvalue` any time the indexes in `itp(x1,x2,...)` are out-of-bounds.
"""
-extrapolate(itp::AbstractInterpolation{T,N,IT,GT}, fillvalue) where {T,N,IT,GT} = FilledExtrapolation(itp, fillvalue)
-
-@inline function getindex(fitp::FilledExtrapolation{T,N,ITP,IT,GT,FT}, args::Vararg{Number,M}) where {T,N,ITP,IT,GT,FT,M}
- @static if VERSION < v"0.7.0-DEV.843"
- inds, trailing = Base.IteratorsMD.split(args, Val{N})
+extrapolate(itp::AbstractInterpolation{T,N,IT}, fillvalue) where {T,N,IT} = FilledExtrapolation(itp, fillvalue)
+
+@inline function (etp::FilledExtrapolation{T,N})(x::Vararg{Number,N}) where {T,N}
+ itp = parent(etp)
+ Tret = typeof(prod(x) * zero(T))
+ if checkbounds(Bool, itp, x...)
+ wis = weightedindexes((value_weights,), itpinfo(itp)..., x)
+ convert(Tret, itp.coefs[wis...])
else
- inds, trailing = Base.IteratorsMD.split(args, Val(N))
+ convert(Tret, etp.fillvalue)
end
- @boundscheck all(x->x==1, trailing) || Base.throw_boundserror(fitp, args)
- Tret = typeof(prod(inds) * zero(T))
- checkbounds(Bool, fitp, inds...) && return convert(Tret, fitp.itp[inds...])
- convert(Tret, fitp.fillvalue)
end
-
-function (fitp::FilledExtrapolation{T,N,ITP,IT,GT,FT})(args...) where {T,N,ITP,IT,GT,FT}
- # support function calls
- fitp[args...]
+@inline function (etp::FilledExtrapolation{T,N})(args::Vararg{Number,M}) where {T,M,N}
+ inds, trailing = Base.IteratorsMD.split(args, Val(N))
+ @boundscheck all(x->x==1, trailing) || Base.throw_boundserror(etp, args)
+ @assert length(inds) == N
+ etp(inds...)
+end
+@inline function (etp::FilledExtrapolation{T,N})(x::Vararg{Union{Number,AbstractVector},N}) where {T,N}
+ itp = parent(etp)
+ Tret = typeof(lispyprod(zero(T), x...))
+ ret = fill(convert(Tret, etp.fillvalue), shape(x...))
+ axsib = inbounds(itp, x...)
+ any(isempty, axsib) && return ret
+ xib = getindex.(x, axsib)
+ getindex!(view(ret, keepvectors(axsib...)...), itp, xib...)
+ return ret
end
-@inline Base.checkbounds(::Type{Bool}, A::FilledExtrapolation, I...) = _checkbounds(A, 1, axes(A), I)
-@inline _checkbounds(A, d::Int, IA::TT1, I::TT2) where {TT1,TT2} =
- (I[1] >= lbound(A, d, IA[1])) & (I[1] <= ubound(A, d, IA[1])) & _checkbounds(A, d+1, Base.tail(IA), Base.tail(I))
-_checkbounds(A, d::Int, ::Tuple{}, ::Tuple{}) = true
-
-getindex(fitp::FilledExtrapolation{T,1}, x::Number, y::Int) where {T} = y == 1 ? fitp[x] : throw(BoundsError())
+expand_index_resid_etp(deg, fillvalue, (l, u), x, etp::FilledExtrapolation, xN) =
+ (l <= x <= u || Base.throw_boundserror(etp, xN))
-lbound(etp::FilledExtrapolation, d) = lbound(etp.itp, d)
-ubound(etp::FilledExtrapolation, d) = ubound(etp.itp, d)
-lbound(etp::FilledExtrapolation, d, inds) = lbound(etp.itp, d, inds)
-ubound(etp::FilledExtrapolation, d, inds) = ubound(etp.itp, d, inds)
+# expand_etp_valueE(fv::FT, etp::FilledExtrapolation{T,N,ITP,IT,FT}, x) where {T,N,ITP,IT,FT} = fv
+# expand_etp_gradientE(fv::FT, etp::FilledExtrapolation{T,N,ITP,IT,FT}, x) where {T,N,ITP,IT,FT} =
+# zero(SVector{N,FT})
+# expand_etp_hessianE(fv::FT, etp::FilledExtrapolation{T,N,ITP,IT,FT}, x) where {T,N,ITP,IT,FT} =
+# zero(Matrix{N,N,FT})
diff --git a/src/extrapolation/flat.jl b/src/extrapolation/flat.jl
deleted file mode 100644
index c77cdbcd..00000000
--- a/src/extrapolation/flat.jl
+++ /dev/null
@@ -1,42 +0,0 @@
-function extrap_prep(::Type{Flat}, ::Val{N}, ::Val{d}) where {N,d}
- xs_d = Symbol("xs_", d)
- :($xs_d = clamp($xs_d, lbound(etp, $d, inds_etp[$d]), ubound(etp, $d, inds_etp[$d])))
-end
-
-function extrap_prep(::Type{Flat}, ::Val{N}, ::Val{d}, ::Val{:lo}) where {N,d}
- xs_d = Symbol("xs_", d)
- :($xs_d = max($xs_d, lbound(etp, $d, inds_etp[$d])))
-end
-
-function extrap_prep(::Type{Flat}, ::Val{N}, ::Val{d}, ::Val{:hi}) where {N,d}
- xs_d = Symbol("xs_", d)
- :($xs_d = min($xs_d, ubound(etp, $d, inds_etp[$d])))
-end
-
-function extrap_prep(::Val{:gradient}, ::Type{Flat}, ::Val{N}, ::Val{d}, ::Val{:lo}) where {N,d}
- coords = [Symbol("xs_", k) for k in 1:N]
- xs_d = coords[d]
-
- quote
- if $xs_d < lbound(etp, $d, inds_etp[$d])
- $xs_d = lbound(etp, $d, inds_etp[$d])
- gradient!(g, etp.itp, $(coords...))
- g[$d] = 0
- return g
- end
- end
-end
-
-function extrap_prep(::Val{:gradient}, ::Type{Flat}, ::Val{N}, ::Val{d}, ::Val{:hi}) where {N,d}
- coords = [Symbol("xs_", k) for k in 1:N]
- xs_d = coords[d]
-
- quote
- if $xs_d > ubound(etp, $d, inds_etp[$d])
- $xs_d = ubound(etp, $d, inds_etp[$d])
- gradient!(g, etp.itp, $(coords...))
- g[$d] = 0
- return g
- end
- end
-end
diff --git a/src/extrapolation/indexing.jl b/src/extrapolation/indexing.jl
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/extrapolation/linear.jl b/src/extrapolation/linear.jl
deleted file mode 100644
index cc4203a3..00000000
--- a/src/extrapolation/linear.jl
+++ /dev/null
@@ -1,26 +0,0 @@
-function extrap_prep(::Type{Linear}, ::Val{N}, ::Val{d}, ::Val{:lo}) where {N,d}
- coords = [Symbol("xs_", k) for k in 1:N]
- xs_d = coords[d]
- quote
- if $xs_d < lbound(etp.itp, $d, inds_etp[$d])
- $xs_d = lbound(etp.itp, $d, inds_etp[$d])
- return etp[$(coords...)] + gradient(etp, $(coords...))[$d] * (xs[$d] - $xs_d)
- end
- end
-end
-function extrap_prep(::Type{Linear}, ::Val{N}, ::Val{d}, ::Val{:hi}) where {N,d}
- coords = [Symbol("xs_", k) for k in 1:N]
- xs_d = coords[d]
- quote
- if $xs_d > ubound(etp, $d, inds_etp[$d])
- $xs_d = ubound(etp, $d, inds_etp[$d])
- return etp[$(coords...)] + gradient(etp, $(coords...))[$d] * (xs[$d] - $xs_d)
- end
- end
-end
-
-extrap_prep(::Val{:gradient}, ::Type{Linear}, n::Val{N}, dim::Val{d}, lohi::Val{l}) where {N,d,l} =
- extrap_prep(Flat, n, dim, lohi)
-
-extrap_prep(::Val{:gradient}, ::Type{Linear}, n::Val{N}, dim::Val{d}) where {N,d} =
- extrap_prep(Flat, n, dim)
diff --git a/src/extrapolation/periodic.jl b/src/extrapolation/periodic.jl
deleted file mode 100644
index 1721028c..00000000
--- a/src/extrapolation/periodic.jl
+++ /dev/null
@@ -1,27 +0,0 @@
-"""
-`extrap_prep_dim(d, ::Type{Periodic})`
-
-Translate x into the domain [lbound, ubound] my means of `mod()`
-"""
-function extrap_prep_dim(::Type{Periodic}, d)
- xs_d = Symbol("xs_", d)
- :($xs_d = mod(xs[$d] - lbound(etp.itp, $d, inds_etp[$d]), ubound(etp.itp, $d, inds_etp[$d]) - lbound(etp.itp, $d, inds_etp[$d])) + lbound(etp.itp, $d, inds_etp[$d]))
-end
-
-extrap_prep(::Type{Periodic}, ::Val{N}, ::Val{d}) where {N,d} = extrap_prep_dim(Periodic, d)
-function extrap_prep(::Type{Periodic}, ::Val{N}, ::Val{d}, ::Val{:lo}) where {N,d}
- xs_d = Symbol("xs_", d)
- quote
- if $xs_d < lbound(etp.itp, $d, inds_etp[$d])
- $(extrap_prep_dim(Periodic, d))
- end
- end
-end
-function extrap_prep(::Type{Periodic}, ::Val{N}, ::Val{d}, ::Val{:hi}) where {N,d}
- xs_d = Symbol("xs_", d)
- quote
- if $xs_d > ubound(etp.itp, d, inds_etp[$d])
- $(extrap_prep_dim(Periodic, d))
- end
- end
-end
diff --git a/src/extrapolation/reflect.jl b/src/extrapolation/reflect.jl
deleted file mode 100644
index ffc141ab..00000000
--- a/src/extrapolation/reflect.jl
+++ /dev/null
@@ -1,26 +0,0 @@
-"""
-`extrap_prep_dim(::Type{Reflect}, d)`
-
-First, translate x into the domain over [lbound, 2(ubound-lbound)) i.e. into twice the size of the domain.
-Next, if x is now in the upper part of this ''double-domain´´, reflect over the middle to obtain a new value x' for which f(x') == f(x), but where x' is inside the domain
-"""
-function extrap_prep_dim(::Type{Reflect}, d)
- xs_d = Symbol("xs_", d)
- quote
- start = lbound(etp.itp, $d, inds_etp[$d])
- width = ubound(etp.itp, $d, inds_etp[$d]) - start
-
- $xs_d = mod($xs_d - start, 2width) + start
- $xs_d > start + width && (xs_d = start + width - $xs_d)
- end
-end
-
-extrap_prep(::Type{Reflect}, ::Val{N}, ::Val{d}) where {N,d} = extrap_prep_dim(Reflect, d)
-function extrap_prep(::Type{Reflect}, ::Val{N}, ::Val{d}, ::Val{:lo}) where {N,d}
- xs_d = Symbol("xs_", d)
- :($xs_d < lbound(etp.itp, $d, inds_etp[$d]) && $(extrap_prep_dim(Reflect, d)))
-end
-function extrap_prep(::Type{Reflect}, ::Val{N}, ::Val{d}, ::Val{:hi}) where {N,d}
- xs_d = Symbol("xs_", d)
- :($xs_d > ubound(etp.itp, $d, inds_etp[$d]) && $(extrap_prep_dim(Reflect, d)))
-end
diff --git a/src/extrapolation/throw.jl b/src/extrapolation/throw.jl
deleted file mode 100644
index dd4f5da2..00000000
--- a/src/extrapolation/throw.jl
+++ /dev/null
@@ -1,14 +0,0 @@
-function extrap_prep(::Type{Throw}, ::Val{N}, ::Val{d}) where {N,d}
- xsym = Symbol("xs_", d)
- :(lbound(etp, $d, inds_etp[$d]) <= $xsym <= ubound(etp, $d, inds_etp[$d]) || throw(BoundsError()))
-end
-
-function extrap_prep(::Type{Throw}, ::Val{N}, ::Val{d}, ::Val{:lo}) where {N,d}
- xsym = Symbol("xs_", d)
- :(lbound(etp, $d, inds_etp[$d]) <= $xsym || throw(BoundsError()))
-end
-
-function extrap_prep(::Type{Throw}, ::Val{N}, ::Val{d}, ::Val{:hi}) where {N,d}
- xsym = Symbol("xs_", d)
- :($xsym <= ubound(etp, $d, inds_etp[$d]) || throw(BoundsError()))
-end
diff --git a/src/filter1d.jl b/src/filter1d.jl
index b276de9c..048b0917 100644
--- a/src/filter1d.jl
+++ b/src/filter1d.jl
@@ -3,6 +3,8 @@ import AxisAlgorithms: A_ldiv_B_md!, _A_ldiv_B_md!
### Tridiagonal inversion along a particular dimension, first offsetting the values by b
+A_ldiv_B_md!(dest, ::Nothing, src, dim::Integer, ::Nothing) = dest
+
function A_ldiv_B_md!(dest, F, src, dim::Integer, b::AbstractVector)
1 <= dim <= max(ndims(dest),ndims(src)) || throw(DimensionMismatch("The chosen dimension $dim is larger than $(ndims(src)) and $(ndims(dest))"))
n = size(F, 1)
diff --git a/src/gridded/constant.jl b/src/gridded/constant.jl
index 856b5732..91390e4f 100644
--- a/src/gridded/constant.jl
+++ b/src/gridded/constant.jl
@@ -1,3 +1,11 @@
+function base_rem(::Constant, knotv, ki, x)
+ l, u = knotv[ki], knotv[ki+1]
+
+ xm = roundbounds(x, bounds)
+ δx = x - xm
+ fast_trunc(Int, xm), δx
+end
+
function define_indices_d(::Type{Gridded{Constant}}, d, pad)
symix, symx = Symbol("ix_",d), Symbol("x_",d)
symk, symkix = Symbol("k_",d), Symbol("kix_",d)
diff --git a/src/gridded/gridded.jl b/src/gridded/gridded.jl
index cf9a3138..df82246c 100644
--- a/src/gridded/gridded.jl
+++ b/src/gridded/gridded.jl
@@ -1,74 +1,74 @@
export Gridded
-struct Gridded{D<:Degree} <: InterpolationType end
-Gridded(::D) where {D<:Degree} = Gridded{D}()
+struct Gridded{D<:Degree} <: InterpolationType
+ degree::D
+end
-griddedtype(::Type{Gridded{D}}) where {D<:Degree} = D
+function Base.show(io::IO, g::Gridded)
+ print(io, "Gridded(")
+ show(io, degree(g))
+ print(io, ')')
+end
const GridIndex{T} = Union{AbstractVector{T}, Tuple}
-# Because Ranges check bounds on getindex, it's actually faster to convert the
-# knots to Vectors. It's also good to take a copy, so it doesn't get modified later.
-struct GriddedInterpolation{T,N,TCoefs,IT<:DimSpec{Gridded},K<:Tuple{Vararg{Vector}},pad} <: AbstractInterpolation{T,N,IT,OnGrid}
+struct GriddedInterpolation{T,N,TCoefs,IT<:DimSpec{Gridded},K<:Tuple{Vararg{AbstractVector}}} <: AbstractInterpolation{T,N,IT}
knots::K
coefs::Array{TCoefs,N}
+ it::IT
end
-function GriddedInterpolation(::Type{TWeights}, knots::NTuple{N,GridIndex}, A::AbstractArray{TCoefs,N}, ::IT, ::Val{pad}) where {N,TCoefs,TWeights<:Real,IT<:DimSpec{Gridded},pad}
+function GriddedInterpolation(::Type{TWeights}, knots::NTuple{N,GridIndex}, A::AbstractArray{TCoefs,N}, it::IT) where {N,TCoefs,TWeights<:Real,IT<:DimSpec{Gridded},pad}
isconcretetype(IT) || error("The b-spline type must be a leaf type (was $IT)")
isconcretetype(TCoefs) || warn("For performance reasons, consider using an array of a concrete type (eltype(A) == $(eltype(A)))")
- knts = mapcollect(knots...)
- for (d,k) in enumerate(knts)
- length(k) == size(A, d) || throw(DimensionMismatch("knot vectors must have the same number of elements as the corresponding dimension of the array"))
- length(k) == 1 && error("dimensions of length 1 not yet supported") # FIXME
- issorted(k) || error("knot-vectors must be sorted in increasing order")
- iextract(IT, d) != NoInterp || k == collect(1:size(A, d)) || error("knot-vector should be the range 1:$(size(A,d)) for the method Gridded{NoInterp}")
- end
+ check_gridded(it, knots, axes(A))
c = zero(TWeights)
- for _ in 2:N
- c *= c
- end
if isempty(A)
T = Base.promote_op(*, typeof(c), eltype(A))
else
T = typeof(c * first(A))
end
- GriddedInterpolation{T,N,TCoefs,IT,typeof(knts),pad}(knts, A)
+ GriddedInterpolation{T,N,TCoefs,IT,typeof(knots)}(knots, A, it)
end
-Base.parent(A::GriddedInterpolation) = A.coefs
+@inline function check_gridded(itpflag, knots, axs)
+ flag, ax1, k1 = getfirst(itpflag), axs[1], knots[1]
+ if flag isa NoInterp
+ k1 == ax1 || error("for NoInterp knot vector should be $ax1, got $k1")
+ else
+ axes(k1, 1) == ax1 || throw(DimensionMismatch("knot vectors must have the same axes as the corresponding dimension of the array"))
+ end
+ degree(flag) isa Union{NoInterp,Constant,Linear} || error("only Linear, Constant, and NoInterp supported, got $flag")
+ length(k1) == 1 && error("dimensions of length 1 not yet supported") # FIXME
+ issorted(k1) || error("knot-vectors must be sorted in increasing order")
+ check_gridded(getrest(itpflag), Base.tail(knots), Base.tail(axs))
+end
+check_gridded(::Any, ::Tuple{}, ::Tuple{}) = nothing
+degree(flag::Gridded) = flag.degree
-# A type-stable version of map(collect, knots)
-mapcollect() = ()
-@inline mapcollect(k::AbstractVector) = (collect(k),)
-@inline mapcollect(k1::AbstractVector, k2::AbstractVector...) = (collect(k1), mapcollect(k2...)...)
+Base.parent(A::GriddedInterpolation) = A.coefs
+coefficients(A::GriddedInterpolation) = A.coefs
-# Utilities for working either with scalars or tuples/tuple-types
-iextract(::Type{T}, d) where {T<:Gridded} = T
-iextract(::Type{T}, d) where {T<:GridType} = T
+size(A::GriddedInterpolation) = size(A.coefs)
+axes(A::GriddedInterpolation) = axes(A.coefs)
-@generated function size(itp::GriddedInterpolation{T,N,TCoefs,IT,K,pad}, d) where {T,N,TCoefs,IT,K,pad}
- quote
- d <= $N ? size(itp.coefs, d) - 2*padextract($pad, d) : 1
- end
-end
+itpflag(A::GriddedInterpolation) = A.it
function interpolate(::Type{TWeights}, ::Type{TCoefs}, knots::NTuple{N,GridIndex}, A::AbstractArray{Tel,N}, it::IT) where {TWeights,TCoefs,Tel,N,IT<:DimSpec{Gridded}}
- GriddedInterpolation(TWeights, knots, A, it, Val{0}())
+ GriddedInterpolation(TWeights, knots, A, it)
end
function interpolate(knots::NTuple{N,GridIndex}, A::AbstractArray{Tel,N}, it::IT) where {Tel,N,IT<:DimSpec{Gridded}}
interpolate(tweight(A), tcoef(A), knots, A, it)
end
-interpolate!(::Type{TWeights}, knots::NTuple{N,GridIndex}, A::AbstractArray{Tel,N}, it::IT) where {TWeights,Tel,N,IT<:DimSpec{Gridded}} = GriddedInterpolation(TWeights, knots, A, it, Val{0}())
+interpolate!(::Type{TWeights}, knots::NTuple{N,GridIndex}, A::AbstractArray{Tel,N}, it::IT) where {TWeights,Tel,N,IT<:DimSpec{Gridded}} =
+ GriddedInterpolation(TWeights, knots, A, it)
function interpolate!(knots::NTuple{N,GridIndex}, A::AbstractArray{Tel,N}, it::IT) where {Tel,N,IT<:DimSpec{Gridded}}
interpolate!(tweight(A), tcoef(A), knots, A, it)
end
-lbound(itp::GriddedInterpolation, d) = itp.knots[d][1]
-ubound(itp::GriddedInterpolation, d) = itp.knots[d][end]
-lbound(itp::GriddedInterpolation, d, inds) = itp.knots[d][1]
-ubound(itp::GriddedInterpolation, d, inds) = itp.knots[d][end]
+lbounds(itp::GriddedInterpolation) = first.(itp.knots)
+ubounds(itp::GriddedInterpolation) = last.(itp.knots)
include("constant.jl")
include("linear.jl")
diff --git a/src/gridded/indexing.jl b/src/gridded/indexing.jl
index a5ab952c..ff8606d2 100644
--- a/src/gridded/indexing.jl
+++ b/src/gridded/indexing.jl
@@ -1,154 +1,96 @@
-using Base.Cartesian
-
-import Base.getindex
+# Indexing at a point
+@inline function (itp::GriddedInterpolation{T,N})(x::Vararg{Number,N}) where {T,N}
+ @boundscheck (checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x))
+ wis = weightedindexes((value_weights,), itpinfo(itp)..., x)
+ coefficients(itp)[wis...]
+end
+@inline function (itp::GriddedInterpolation)(x::Vararg{UnexpandedIndexTypes})
+ itp(to_indices(itp, x)...)
+end
-function gradient_coefficients(::Type{Gridded{Linear}}, N, dim)
- exs = Expr[d==dim ? gradient_coefficients(iextract(Gridded{Linear}, dim), d) :
- coefficients(iextract(Gridded{Linear}, d), N, d) for d = 1:N]
- Expr(:block, exs...)
+@inline function gradient(itp::GriddedInterpolation{T,N}, x::Vararg{Number,N}) where {T,N}
+ @boundscheck (checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x))
+ wis = weightedindexes((value_weights, gradient_weights), itpinfo(itp)..., x)
+ SVector(map(inds->coefficients(itp)[inds...], wis))
+end
+@propagate_inbounds function gradient!(dest, itp::GriddedInterpolation{T,N}, x::Vararg{Number,N}) where {T,N}
+ dest .= gradient(itp, x...)
end
+itpinfo(itp::GriddedInterpolation) = (tcollect(itpflag, itp), itp.knots)
-# Indexing at a point
-function getindex_impl(itp::Type{GriddedInterpolation{T,N,TCoefs,IT,K,P}}) where {T,N,TCoefs,IT<:DimSpec{Gridded},K,P}
- meta = Expr(:meta, :inline)
- quote
- $meta
- @nexprs $N d->begin
- x_d = x[d]
- k_d = itp.knots[d]
- ix_d = searchsortedfirst(k_d, x_d, 1, length(k_d), Base.Order.ForwardOrdering()) - 1
- end
- $(define_indices(IT, N, P))
- $(coefficients(IT, N))
- @inbounds ret = $(index_gen(IT, N))
- ret
- end
-end
+weightedindex_parts(fs::F, itpflag::Gridded, ax, x) where F =
+ weightedindex_parts(fs, degree(itpflag), ax, x)
-@generated function getindex(itp::GriddedInterpolation{T,N}, x::Number...) where {T,N}
- getindex_impl(itp)
+roundbounds(x::Integer, knotvec::AbstractVector) = gridded_roundbounds(x, knotvec)
+roundbounds(x::Number, knotvec::AbstractVector) = gridded_roundbounds(x, knotvec)
+function gridded_roundbounds(x, knotvec::AbstractVector)
+ i = find_knot_index(knotvec, x)
+ iclamp = max(i, first(axes1(knotvec)))
+ inext = min(iclamp+1, last(axes1(knotvec)))
+ ifelse(i < iclamp, i+1, ifelse(x - knotvec[iclamp] < knotvec[inext] - x, i, inext))
end
-# Because of the "vectorized" definition below, we need a definition for CartesianIndex
-@generated function getindex(itp::GriddedInterpolation{T,N}, index::CartesianIndex{N}) where {T,N}
- args = [:(index[$d]) for d = 1:N]
- :(getindex(itp, $(args...)))
+floorbounds(x::Integer, knotvec::AbstractVector) = gridded_floorbounds(x, knotvec)
+floorbounds(x, knotvec::AbstractVector) = gridded_floorbounds(x, knotvec)
+function gridded_floorbounds(x, knotvec::AbstractVector)
+ i = find_knot_index(knotvec, x)
+ max(i, first(axes1(knotvec)))
end
-function (itp::GriddedInterpolation{T,N,TCoefs,IT,K,pad})(args...) where {T,N,TCoefs,IT,K,pad}
- # support function calls
- itp[args...]
-end
+@inline find_knot_index(knotv, x) = searchsortedfirst(knotv, x, first(axes1(knotv)), length(knotv), Base.Order.ForwardOrdering()) - 1
-# Indexing with vector inputs. Here, it pays to pre-process the input indexes,
-# because N*n is much smaller than n^N.
-# TODO: special-case N=1, because there is no reason to separately cache the indexes.
-@generated function getindex!(dest, itp::GriddedInterpolation{T,N,TCoefs,IT,K,P}, xv...) where {T,N,TCoefs,IT<:DimSpec{Gridded},K,P}
- length(xv) == N || error("Can only be called with $N indexes")
- indexes_exprs = Expr[define_indices_d(iextract(IT, d), d, P) for d = 1:N]
- coefficient_exprs = Expr[coefficients(iextract(IT, d), N, d) for d = 1:N]
- # A manual @nloops (the interaction of d with the two exprs above is tricky...)
- ex = :(@nref($N,dest,i) = $(index_gen(IT, N)))
- for d = 1:N
- isym, xsym, xvsym, ixsym, ixvsym = Symbol("i_",d), Symbol("x_",d), Symbol("xv_",d), Symbol("ix_",d), Symbol("ixv_",d)
- ex = quote
- for $isym = 1:length($xvsym)
- $xsym = $xvsym[$isym]
- $ixsym = $ixvsym[$isym]
- $(indexes_exprs[d])
- $(coefficient_exprs[d])
- $ex
- end
- end
- end
- quote
- @inbounds begin
- @nexprs $N d->begin
- xv_d = xv[d]
- k_d = itp.knots[d]
- ixv_d = Array{Int}(undef, length(xv_d)) # ixv_d[i] is the smallest value such that k_d[ixv_d[i]] <= x_d[i]
- # If x_d is sorted and has quite a few entries, it's better to match
- # entries of x_d and k_d by iterating through them both in unison.
- l_d = length(k_d) # FIXME: check l_d == 1 someday, see FIXME above
- # estimate the time required for searchsortedfirst vs. linear traversal
- den = 5*log(l_d) - 1 # 5 is arbitrary, for now (it's the coefficient of ssf compared to the while loop below)
- ascending = den*length(xv_d) > l_d # if this is (or becomes) false, use searchsortedfirst
- i = 2 # this clamps ixv_d .>= 1
- knext = k_d[i]
- xjold = xv_d[1]
- for j = 1:length(xv_d)
- xj = xv_d[j]
- ascending = ascending & (xj >= xjold)
- if ascending
- while i < length(k_d) && knext < xj
- knext = k_d[i+=1]
- end
- ixv_d[j] = i-1
- xjold = xj
- else
- ixv_d[j] = searchsortedfirst(k_d, xj, 1, l_d, Base.Order.ForwardOrdering()) - 1
- end
- end
- end
- $ex
- end
- dest
- end
+function weightedindex_parts(fs::F, deg::Degree, knotvec::AbstractVector, x) where F
+ i = find_knot_index(knotvec, x)
+ ax1 = axes1(knotvec)
+ iclamp = clamp(i, first(ax1), last(ax1)-1)
+ weightedindex(fs, deg, knotvec, x, iclamp)
end
-function getindex(itp::GriddedInterpolation{T,N,TCoefs,IT,K,P}, x...) where {T,N,TCoefs,IT<:DimSpec{Gridded},K,P}
- dest = Array{T}(undef, map(length, x))::Array{T,N}
- getindex!(dest, itp, x...)
+function weightedindex(fs::F, deg::Constant, knotvec, x, iclamp) where F
+ pos, δx = positions(deg, knotvec, x)
+ (position=pos, coefs=fmap(fs, deg, δx))
+end
+function weightedindex(fs::F, deg::Degree, knotvec, x, iclamp) where F
+ @inbounds l, u = knotvec[iclamp], knotvec[iclamp+1]
+ δx = (x - l)/(u - l)
+ (position=iclamp, coefs=rescale_gridded(fs, fmap(fs, deg, δx), u-l))
end
-function gradient_impl(itp::Type{GriddedInterpolation{T,N,TCoefs,IT,K,P}}) where {T,N,TCoefs,IT<:DimSpec{Gridded},K,P}
- meta = Expr(:meta, :inline)
- # For each component of the gradient, alternately calculate
- # coefficients and set component
- n = count_interp_dims(IT, N)
- exs = Array{Expr}(undef, 2n)
- cntr = 0
- for d = 1:N
- if count_interp_dims(iextract(IT, d), 1) > 0
- cntr += 1
- exs[2cntr-1] = gradient_coefficients(IT, N, d)
- exs[2cntr] = :(@inbounds g[$cntr] = $(index_gen(IT, N)))
- end
- end
- gradient_exprs = Expr(:block, exs...)
- quote
- $meta
- length(g) == $n || throw(ArgumentError(string("The length of the provided gradient vector (", length(g), ") did not match the number of interpolating dimensions (", n, ")")))
- @nexprs $N d->begin
- x_d = x[d]
- k_d = itp.knots[d]
- ix_d = searchsortedfirst(k_d, x_d, 1, length(k_d), Base.Order.ForwardOrdering()) - 1
- end
- # Calculate the indices of all coefficients that will be used
- # and define fx = x - xi in each dimension
- $(define_indices(IT, N, P))
+rescale_gridded(fs::F, coefs, Δx) where F =
+ (rescale_gridded(fs[1], coefs[1], Δx), rescale_gridded(Base.tail(fs), Base.tail(coefs), Δx)...)
+rescale_gridded(::Tuple{}, ::Tuple{}, Δx) = ()
+rescale_gridded(::typeof(value_weights), coefs, Δx) = coefs
+rescale_gridded(::typeof(gradient_weights), coefs, Δx) = coefs./Δx
+rescale_gridded(::typeof(hessian_weights), coefs, Δx) = coefs./Δx.^2
- $gradient_exprs
+@inline function (itp::GriddedInterpolation{T,N})(x::Vararg{Union{Number,AbstractVector},N}) where {T,N}
+ @boundscheck (checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x))
+ itps = tcollect(itpflag, itp)
+ wis = dimension_wis(value_weights, itps, itp.knots, x)
+ coefs = coefficients(itp)
+ ret = [coefs[i...] for i in Iterators.product(wis...)]
+ reshape(ret, shape(wis...))
+end
- g
+function dimension_wis(f::F, itps, knots, xs) where F
+ itpflag, knotvec, x = itps[1], knots[1], xs[1]
+ function makewi(y)
+ pos, coefs = weightedindex_parts((f,), itpflag, knotvec, y)
+ maybe_weightedindex(pos, coefs[1])
end
+ (makewi.(x), dimension_wis(f, Base.tail(itps), Base.tail(knots), Base.tail(xs))...)
end
-
-@generated function gradient!(g::AbstractVector, itp::GriddedInterpolation{T,N}, x::Number...) where {T,N}
- length(x) == N || error("Can only be called with $N indexes")
- gradient_impl(itp)
+function dimension_wis(f::F, itps::Tuple{NoInterp,Vararg{Any}}, knots, xs) where F
+ (Int.(xs[1]), dimension_wis(f, Base.tail(itps), Base.tail(knots), Base.tail(xs))...)
end
+dimension_wis(f, ::Tuple{}, ::Tuple{}, ::Tuple{}) = ()
-@generated function gradient!(g::AbstractVector, itp::GriddedInterpolation{T,N}, index::CartesianIndex{N}) where {T,N}
- args = [:(index[$d]) for d = 1:N]
- :(gradient!(g, itp, $(args...)))
-end
-function getindex_return_type(::Type{GriddedInterpolation{T,N,TCoefs,IT,K,P}}, argtypes) where {T,N,TCoefs,IT<:DimSpec{Gridded},K,P}
+function getindex_return_type(::Type{GriddedInterpolation{T,N,TCoefs,IT,K}}, argtypes) where {T,N,TCoefs,IT<:DimSpec{Gridded},K}
Tret = TCoefs
for a in argtypes
- Tret = Base.promote_op(*, Tret, a) # the macro is used to support julia 0.4
+ Tret = Base.promote_op(*, Tret, a)
end
Tret
end
diff --git a/src/io.jl b/src/io.jl
index 232489e7..da0bdffb 100644
--- a/src/io.jl
+++ b/src/io.jl
@@ -1,83 +1,65 @@
-using ShowItLikeYouBuildIt
-
-Base.summary(A::AbstractInterpolation) = summary_build(A)
-
-function ShowItLikeYouBuildIt.showarg(io::IO, A::BSplineInterpolation{T,N,TW,ST,GT}) where {T,N,TW,ST,GT}
+function Base.showarg(io::IO, A::BSplineInterpolation{T,N,TW,ST}, toplevel) where {T,N,TW,ST}
print(io, "interpolate(")
- showarg(io, A.coefs)
- print(io, ", ")
- _showtypeparam(io, ST)
+ Base.showarg(io, A.coefs, false)
print(io, ", ")
- _showtypeparam(io, GT)
- print(io, ')')
+ show(io, itpflag(A))
+ if toplevel
+ print(io, ") with element type ",T)
+ else
+ print(io, ')')
+ end
end
-function ShowItLikeYouBuildIt.showarg(io::IO, A::GriddedInterpolation{T,N,TC,ST,K}) where {T,N,TC,ST,K}
+function Base.showarg(io::IO, A::GriddedInterpolation{T,N,TC,ST,K}, toplevel) where {T,N,TC,ST,K}
print(io, "interpolate(")
_showknots(io, A.knots)
print(io, ", ")
- showarg(io, A.coefs)
+ Base.showarg(io, A.coefs, false)
print(io, ", ")
- _showtypeparam(io, ST)
- print(io, ')')
+ show(io, itpflag(A))
+ if toplevel
+ print(io, ") with element type ",T)
+ else
+ print(io, ')')
+ end
end
-_showknots(io, A) = showarg(io, A)
+_showknots(io, A) = Base.showarg(io, A, false)
function _showknots(io, tup::NTuple{N,Any}) where N
print(io, '(')
for (i, A) in enumerate(tup)
- showarg(io, A)
+ Base.showarg(io, A, false)
i < N && print(io, ',')
end
N == 1 && print(io, ',')
print(io, ')')
end
-function ShowItLikeYouBuildIt.showarg(io::IO, A::ScaledInterpolation)
+function Base.showarg(io::IO, A::ScaledInterpolation{T}, toplevel) where {T}
print(io, "scale(")
- showarg(io, A.itp)
+ Base.showarg(io, A.itp, false)
print(io, ", ", A.ranges, ')')
+ if toplevel
+ print(io, " with element type ",T)
+ end
end
-function ShowItLikeYouBuildIt.showarg(io::IO, A::Extrapolation{T,N,TI,IT,GT,ET}) where {T,N,TI,IT,GT,ET}
+function Base.showarg(io::IO, A::Extrapolation{T,N,TI,IT,ET}, toplevel) where {T,N,TI,IT,ET}
print(io, "extrapolate(")
- showarg(io, A.itp)
+ Base.showarg(io, A.itp, false)
print(io, ", ")
- _showtypeparam(io, ET)
+ show(io, etpflag(A))
print(io, ')')
+ if toplevel
+ print(io, " with element type ",T)
+ end
end
-function ShowItLikeYouBuildIt.showarg(io::IO, A::FilledExtrapolation{T,N,TI,IT,GT}) where {T,N,TI,IT,GT}
+function Base.showarg(io::IO, A::FilledExtrapolation{T,N,TI,IT}, toplevel) where {T,N,TI,IT}
print(io, "extrapolate(")
- showarg(io, A.itp)
+ Base.showarg(io, A.itp, false)
print(io, ", ", A.fillvalue, ')')
-end
-
-_showtypeparam(io, ::Type{T}) where {T} =
- print(io, T.name.name, "()")
-_showtypeparam(io, ::Type{Quadratic{T}}) where {T} =
- print(io, "Quadratic(", T.name.name, "())")
-_showtypeparam(io, ::Type{Cubic{T}}) where {T} =
- print(io, "Cubic(", T.name.name, "())")
-
-function _showtypeparam(io, ::Type{BSpline{T}}) where T
- print(io, "BSpline(")
- _showtypeparam(io, T)
- print(io, ')')
-end
-
-function _showtypeparam(io, ::Type{Gridded{T}}) where T
- print(io, "Gridded(")
- _showtypeparam(io, T)
- print(io, ')')
-end
-
-function _showtypeparam(io, types::Type{TTup}) where TTup<:Tuple
- print(io, '(')
- N = length(types.types)
- for (i, T) in enumerate(types.types)
- _showtypeparam(io, T)
- i < N && print(io, ", ")
+ if toplevel
+ print(io, " with element type ",T)
end
- print(io, ')')
end
diff --git a/src/nointerp/nointerp.jl b/src/nointerp/nointerp.jl
index 17490cd8..c46d0191 100644
--- a/src/nointerp/nointerp.jl
+++ b/src/nointerp/nointerp.jl
@@ -1,38 +1,25 @@
-function interpolate(A::AbstractArray, ::NoInterp, gt::GT) where {GT<:DimSpec{GridType}}
- interpolate(Int, eltype(A), A, NoInterp(), gt)
+function interpolate(A::AbstractArray, ::NoInterp)
+ interpolate(Int, eltype(A), A, NoInterp())
end
-iextract(::Type{NoInterp}, d) = NoInterp
+# How many non-NoInterp dimensions are there?
+count_interp_dims(::Type{NoInterp}) = 0
-function define_indices_d(::Type{NoInterp}, d, pad)
- symix, symx = Symbol("ix_",d), Symbol("x_",d)
- :($symix = convert(Int, $symx))
-end
+interpdegree(::NoInterp) = NoInterp()
-function coefficients(::Type{NoInterp}, N, d)
- :()
-end
+iscomplete(::NoInterp) = true
-function index_gen(::Type{NoInterp}, ::Type{IT}, N::Integer, offsets...) where IT<:DimSpec
- if (length(offsets) < N)
- return :($(index_gen(IT, N, offsets..., 0)))
- else
- indices = [offsetsym(offsets[d], d) for d = 1:N]
- return :(itp.coefs[$(indices...)])
- end
-end
+prefilter(::Type{TWeights}, ::Type{TC}, A::AbstractArray, ::NoInterp) where {TWeights, TC} = A
-padding(::Type{NoInterp}) = Val{0}()
+lbound(ax, ::NoInterp) = first(ax)
+ubound(ax, ::NoInterp) = last(ax)
-# How many non-NoInterp dimensions are there?
-count_interp_dims(::Type{NoInterp}, N) = 0
-count_interp_dims(::Type{IT}, N) where {IT<:InterpolationType} = N
-function count_interp_dims(it::Type{IT}, N) where IT<:Tuple{Vararg{InterpolationType}}
- n = 0
- for p in it.parameters
- n += count_interp_dims(p, 1)
- end
- n
-end
+weightedindex_parts(fs, ::NoInterp, ax, x::Number) = Int(x)
+
+# positions(::NoInterp, ax, x) = (Int(x),), 0
+
+# value_weights(::NoInterp, δx) = (oneunit(δx),)
+# gradient_weights(::NoInterp, δx) = (NoInterp(),)
+# hessian_weights(::NoInterp, δx) = (NoInterp(),)
-prefilter(::Type{TWeights}, ::Type{TC}, A, ::Type{IT},::Type{GT}) where {TWeights, TC, IT<:NoInterp, GT<:GridType} = A, Val{0}()
+padded_axis(ax::AbstractUnitRange, ::NoInterp) = ax
diff --git a/src/rewrite.jl b/src/rewrite.jl
new file mode 100644
index 00000000..001664d8
--- /dev/null
+++ b/src/rewrite.jl
@@ -0,0 +1,15 @@
+struct MyInterp{T,N,A<:AbstractArray{T,N}}
+ data::A
+end
+
+@inline (itp::MyInterp{T,N})(i::Vararg{<:Number,N}) where {T,N} = expand(itp, i)
+
+@inline function expand(itp::MyInterp, ipre::Tuple{Vararg{Number,L}}, ipost::Vararg{Integer,M}) where {L,M} # force specialization
+ ifront, ilast = Base.front(ipre), ipre[end]
+ im, ip = floor(ilast), ceil(ilast)
+ return (ip - ilast)*expand(itp, ifront, unsafe_trunc(Int, im), ipost...) +
+ (ilast - im)*expand(itp, ifront, unsafe_trunc(Int, ip), ipost...)
+end
+
+@inline expand(itp::MyInterp, ::Tuple{}, ipost::Vararg{Integer,N}) where N =
+ @inbounds itp.data[CartesianIndex(ipost)]
diff --git a/src/scaling/scaling.jl b/src/scaling/scaling.jl
index 7153f673..65c9acf9 100644
--- a/src/scaling/scaling.jl
+++ b/src/scaling/scaling.jl
@@ -1,22 +1,11 @@
export ScaledInterpolation, eachvalue
-@static if VERSION < v"0.7.0-DEV.5126"
- import Base: done, next, start
-else
- import Base: iterate
-end
+import Base: iterate
-struct ScaledInterpolation{T,N,ITPT,IT,GT,RT} <: AbstractInterpolationWrapper{T,N,ITPT,IT,GT}
+struct ScaledInterpolation{T,N,ITPT,IT,RT} <: AbstractInterpolationWrapper{T,N,ITPT,IT}
itp::ITPT
ranges::RT
end
-@generated function ScaledInterpolation(itp::ITPT, ranges::RT) where {ITPT,RT}
- T = eltype(itp)
- N = ndims(itp)
- IT = itptype(itp)
- GT = gridtype(itp)
- :(ScaledInterpolation{$T,$N,$ITPT,$IT,$GT,$RT}(itp, ranges))
-end
Base.parent(A::ScaledInterpolation) = A.itp
count_interp_dims(::Type{<:ScaledInterpolation{T,N,ITPT}}, n) where {T,N,ITPT} = count_interp_dims(ITPT, n)
@@ -28,96 +17,100 @@ The parameters `xs` etc must be either ranges or linspaces, and there must be on
For every `NoInterp` dimension of the interpolation object, the range must be exactly `1:size(itp, d)`.
"""
-function scale(itp::AbstractInterpolation{T,N,IT,GT}, ranges::AbstractRange...) where {T,N,IT,GT}
- length(ranges) == N || throw(ArgumentError("Must scale $N-dimensional interpolation object with exactly $N ranges (you used $(length(ranges)))"))
- for d in 1:N
- if iextract(IT,d) != NoInterp
- length(ranges[d]) == size(itp,d) || throw(ArgumentError("The length of the range in dimension $d ($(length(ranges[d]))) did not equal the size of the interpolation object in that direction ($(size(itp,d)))"))
- elseif ranges[d] != 1:size(itp,d)
- throw(ArgumentError("NoInterp dimension $d must be scaled with unit range 1:$(size(itp,d))"))
- end
- end
-
- ScaledInterpolation(itp, ranges)
+function scale(itp::AbstractInterpolation{T,N,IT}, ranges::Vararg{AbstractRange,N}) where {T,N,IT}
+ check_ranges(itpflag(itp), axes(itp), ranges)
+ ScaledInterpolation{T,N,typeof(itp),IT,typeof(ranges)}(itp, ranges)
end
-@generated function getindex(sitp::ScaledInterpolation{T,N,ITPT,IT}, xs::Number...) where {T,N,ITPT,IT<:DimSpec}
- length(xs) == N || throw(ArgumentError("Must index into $N-dimensional scaled interpolation object with exactly $N indices (you used $(length(xs)))"))
- interp_indices = map(i -> iextract(IT, i) != NoInterp ? :(coordlookup(sitp.ranges[$i], xs[$i])) : :(xs[$i]), 1:N)
- return :($(Expr(:meta,:inline)); getindex(sitp.itp, $(interp_indices...)))
+function check_ranges(flags, axs, ranges)
+ check_range(getfirst(flags), axs[1], ranges[1])
+ check_ranges(getrest(flags), Base.tail(axs), Base.tail(ranges))
end
+check_ranges(::Any, ::Tuple{}, ::Tuple{}) = nothing
+
+check_range(::NoInterp, ax, r) = ax == r || throw(ArgumentError("The range $r did not equal the corresponding axis of the interpolation object $ax"))
+check_range(::Any, ax, r) = length(ax) == length(r) || throw(ArgumentError("The range $r is incommensurate with the corresponding axis $ax"))
-getindex(sitp::ScaledInterpolation{T,1}, x::Number, y::Int) where {T} = y == 1 ? sitp[x] : throw(BoundsError())
+# With regards to size and [], ScaledInterpolation behaves like the underlying interpolation object
+size(sitp::ScaledInterpolation) = size(sitp.itp)
+axes(sitp::ScaledInterpolation) = axes(sitp.itp)
-function (sitp::ScaledInterpolation{T,N,ITPT,IT})(args...) where {T,N,ITPT,IT<:DimSpec}
- sitp[args...]
+itpflag(sitp::ScaledInterpolation) = itpflag(sitp.itp)
+
+@propagate_inbounds function Base.getindex(sitp::ScaledInterpolation{T,N}, i::Vararg{Int,N}) where {T,N}
+ sitp.itp[i...]
end
-size(sitp::ScaledInterpolation, d) = size(sitp.itp, d)
-lbound(sitp::ScaledInterpolation{T,N,ITPT,IT,OnGrid}, d) where {T,N,ITPT,IT} = 1 <= d <= N ? sitp.ranges[d][1] : throw(BoundsError())
-lbound(sitp::ScaledInterpolation{T,N,ITPT,IT,OnCell}, d) where {T,N,ITPT,IT} = 1 <= d <= N ? sitp.ranges[d][1] - boundstep(sitp.ranges[d]) : throw(BoundsError())
-ubound(sitp::ScaledInterpolation{T,N,ITPT,IT,OnGrid}, d) where {T,N,ITPT,IT} = 1 <= d <= N ? sitp.ranges[d][end] : throw(BoundsError())
-ubound(sitp::ScaledInterpolation{T,N,ITPT,IT,OnCell}, d) where {T,N,ITPT,IT} = 1 <= d <= N ? sitp.ranges[d][end] + boundstep(sitp.ranges[d]) : throw(BoundsError())
-
-lbound(sitp::ScaledInterpolation{T,N,ITPT,IT,OnGrid}, d, inds) where {T,N,ITPT,IT} =
- sitp.ranges[d][1]
-lbound(sitp::ScaledInterpolation{T,N,ITPT,IT,OnCell}, d, inds) where {T,N,ITPT,IT} =
- sitp.ranges[d][1] - boundstep(sitp.ranges[d])
-ubound(sitp::ScaledInterpolation{T,N,ITPT,IT,OnGrid}, d, inds) where {T,N,ITPT,IT} =
- sitp.ranges[d][end]
-ubound(sitp::ScaledInterpolation{T,N,ITPT,IT,OnCell}, d, inds) where {T,N,ITPT,IT} =
- sitp.ranges[d][end] + boundstep(sitp.ranges[d])
+lbounds(sitp::ScaledInterpolation) = _lbounds(sitp.ranges, itpflag(sitp.itp))
+ubounds(sitp::ScaledInterpolation) = _ubounds(sitp.ranges, itpflag(sitp.itp))
boundstep(r::StepRange) = r.step / 2
boundstep(r::UnitRange) = 1//2
-
"""
Returns *half* the width of one step of the range.
This function is used to calculate the upper and lower bounds of `OnCell` interpolation objects.
""" boundstep
+lbound(ax::AbstractRange, ::DegreeBC, ::OnCell) = first(ax) - boundstep(ax)
+ubound(ax::AbstractRange, ::DegreeBC, ::OnCell) = last(ax) + boundstep(ax)
+lbound(ax::AbstractRange, ::DegreeBC, ::OnGrid) = first(ax)
+ubound(ax::AbstractRange, ::DegreeBC, ::OnGrid) = last(ax)
+
+# For (), we scale the evaluation point
+function (sitp::ScaledInterpolation{T,N})(xs::Vararg{Number,N}) where {T,N}
+ xl = coordslookup(itpflag(sitp.itp), sitp.ranges, xs)
+ sitp.itp(xl...)
+end
+@inline function (sitp::ScaledInterpolation)(x::Vararg{UnexpandedIndexTypes})
+ xis = to_indices(sitp, x)
+ xis == x && error("evaluation not supported for ScaledInterpolation at positions $x")
+ sitp(xis...)
+end
+
+(sitp::ScaledInterpolation{T,1}, x::Number, y::Int) where {T} = y == 1 ? sitp(x) : Base.throw_boundserror(sitp, (x, y))
+
+@inline function (itp::ScaledInterpolation{T,N})(x::Vararg{Union{Number,AbstractVector},N}) where {T,N}
+ # @boundscheck (checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x))
+ [itp(i...) for i in Iterators.product(x...)]
+end
+
+@inline function coordslookup(flags, ranges, xs)
+ item = coordlookup(getfirst(flags), ranges[1], xs[1])
+ (item, coordslookup(getrest(flags), Base.tail(ranges), Base.tail(xs))...)
+end
+coordslookup(::Any, ::Tuple{}, ::Tuple{}) = ()
+
+coordlookup(::NoInterp, r, i) = i
+coordlookup(::Flag, r, x) = coordlookup(r, x)
+
coordlookup(r::UnitRange, x) = x - r.start + oneunit(eltype(r))
-coordlookup(i::Bool, r::AbstractRange, x) = i ? coordlookup(r, x) : convert(typeof(coordlookup(r,x)), x)
+# coordlookup(i::Bool, r::AbstractRange, x) = i ? coordlookup(r, x) : convert(typeof(coordlookup(r,x)), x)
coordlookup(r::StepRange, x) = (x - r.start) / r.step + oneunit(eltype(r))
-@static if isdefined(Base, :StepRangeLen)
- coordlookup(r::StepRangeLen, x) = (x - first(r)) / step(r) + oneunit(eltype(r))
- boundstep(r::StepRangeLen) = 0.5*step(r)
- rescale_gradient(r::StepRangeLen, g) = g / step(r)
-end
+coordlookup(r::StepRangeLen, x) = (x - first(r)) / step(r) + oneunit(eltype(r))
+boundstep(r::StepRangeLen) = 0.5*step(r)
+rescale_gradient(r::StepRangeLen, g) = g / step(r)
-basetype(::Type{ScaledInterpolation{T,N,ITPT,IT,GT,RT}}) where {T,N,ITPT,IT,GT,RT} = ITPT
+basetype(::Type{ScaledInterpolation{T,N,ITPT,IT,RT}}) where {T,N,ITPT,IT,RT} = ITPT
basetype(sitp::ScaledInterpolation) = basetype(typeof(sitp))
-# @eval uglyness required for disambiguation with method in b-splies/indexing.jl
-# also, GT is only specified to avoid disambiguation warnings on julia 0.4
-gradient(sitp::ScaledInterpolation{T,N,ITPT,IT,GT}, xs::Real...) where {T,N,ITPT,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} =
- gradient!(Array{T}(undef, count_interp_dims(IT,N)), sitp, xs...)
-gradient(sitp::ScaledInterpolation{T,N,ITPT,IT,GT}, xs...) where {T,N,ITPT,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} =
- gradient!(Array{T}(undef, count_interp_dims(IT,N)), sitp, xs...)
-@generated function gradient!(g, sitp::ScaledInterpolation{T,N,ITPT,IT}, xs::Number...) where {T,N,ITPT,IT}
- ndims(g) == 1 || throw(DimensionMismatch("g must be a vector (but had $(ndims(g)) dimensions)"))
- length(xs) == N || throw(DimensionMismatch("Must index into $N-dimensional scaled interpolation object with exactly $N indices (you used $(length(xs)))"))
-
- interp_types = length(IT.parameters) == N ? IT.parameters : tuple([IT.parameters[1] for _ in 1:N]...)
- interp_dimens = map(it -> interp_types[it] != NoInterp, 1:N)
- interp_indices = map(i -> interp_dimens[i] ? :(coordlookup(sitp.ranges[$i], xs[$i])) : :(xs[$i]), 1:N)
-
- quote
- length(g) == $(count_interp_dims(IT, N)) || throw(ArgumentError(string("The length of the provided gradient vector (", length(g), ") did not match the number of interpolating dimensions (", $(count_interp_dims(IT, N)), ")")))
- gradient!(g, sitp.itp, $(interp_indices...))
- cntr = 0
- for i = 1:N
- if $(interp_dimens)[i]
- cntr += 1
- g[cntr] = rescale_gradient(sitp.ranges[i], g[cntr])
- end
- end
- g
- end
+
+function gradient(sitp::ScaledInterpolation{T,N}, xs::Vararg{Number,N}) where {T,N}
+ xl = coordslookup(itpflag(sitp.itp), sitp.ranges, xs)
+ g = gradient(sitp.itp, xl...)
+ SVector(rescale_gradient_components(itpflag(sitp.itp), sitp.ranges, Tuple(g)))
end
+function rescale_gradient_components(flags, ranges, g)
+ if getfirst(flags) isa NoInterp
+ return rescale_gradient_components(getrest(flags), Base.tail(ranges), g) # don't consume a coordinate of g
+ else
+ item = rescale_gradient(ranges[1], g[1])
+ return (item, rescale_gradient_components(getrest(flags), Base.tail(ranges), Base.tail(g))...)
+ end
+end
+rescale_gradient_components(flags, ::Tuple{}, ::Tuple{}) = ()
rescale_gradient(r::StepRange, g) = g / r.step
rescale_gradient(r::UnitRange, g) = g
@@ -128,182 +121,85 @@ rescale_gradient(r::UnitRange, g) = g
Implements the chain rule dy/dx = dy/du * du/dx for use when calculating gradients with scaled interpolation objects.
""" rescale_gradient
-
### Iteration
-mutable struct ScaledIterator{CR<:CartesianIndices,SITPT,X1,Deg,T}
- rng::CR
- sitp::SITPT
- dx_1::X1
- nremaining::Int
- fx_1::X1
- itp_tail::NTuple{Deg,T}
-end
-nelements(::Union{Type{NoInterp},Type{Constant}}) = 1
-nelements(::Type{Linear}) = 2
-nelements(::Type{Q}) where {Q<:Quadratic} = 3
-
-eachvalue_zero(::Type{R}, ::Type{BT}) where {R,BT<:Union{Type{NoInterp},Type{Constant}}} =
- (zero(R),)
-eachvalue_zero(::Type{R}, ::Type{Linear}) where {R} = (zero(R),zero(R))
-eachvalue_zero(::Type{R}, ::Type{Q}) where {R,Q<:Quadratic} = (zero(R),zero(R),zero(R))
-
-"""
-`eachvalue(sitp)` constructs an iterator for efficiently visiting each
-grid point of a ScaledInterpolation object in which a small grid is
-being "scaled up" to a larger one. For example, suppose you have a
-core `BSpline` object defined on a 5x7x4 grid, and you are scaling it
-to a 100x120x20 grid (via `linspace(1,5,100), linspace(1,7,120),
-linspace(1,4,20)`). You can perform interpolation at each of these
-grid points via
-
-```
- function foo!(dest, sitp)
- i = 0
- for s in eachvalue(sitp)
- dest[i+=1] = s
- end
- dest
- end
-```
-
-which should be more efficient than
-
-```
- function bar!(dest, sitp)
- for I in CartesianIndices(size(dest))
- dest[I] = sitp[I]
- end
- dest
- end
-```
-"""
-function eachvalue(sitp::ScaledInterpolation{T,N}) where {T,N}
- ITPT = basetype(sitp)
- IT = itptype(ITPT)
- R = getindex_return_type(ITPT, Int)
- BT = bsplinetype(iextract(IT, 1))
- itp_tail = eachvalue_zero(R, BT)
- dx_1 = coordlookup(sitp.ranges[1], 2) - coordlookup(sitp.ranges[1], 1)
- ScaledIterator(CartesianIndices(ssize(sitp)), sitp, dx_1, 0, zero(dx_1), itp_tail)
+struct ScaledIterator{SITPT,CI,WIS}
+ sitp::SITPT # ScaledInterpolation object
+ ci::CI # the CartesianIndices object
+ wis::WIS # WeightedIndex vectors
+ breaks1::Vector{Int} # breaks along dimension 1 where new evaluations must occur
end
-@static if VERSION < v"0.7.0-DEV.5126"
- @inline start(iter::ScaledIterator) = start(iter.rng)
- @inline done(iter::ScaledIterator, state) = done(iter.rng, state)
-end
+Base.IteratorSize(::Type{ScaledIterator{SITPT,CI,WIS}}) where {SITPT,CI<:CartesianIndices{N},WIS} where N = Base.HasShape{N}()
+Base.axes(iter::ScaledIterator) = axes(iter.ci)
+Base.size(iter::ScaledIterator) = size(iter.ci)
-function index_gen1(::Union{Type{NoInterp}, Type{BSpline{Constant}}})
- quote
- value = iter.itp_tail[1]
- end
+struct ScaledIterState{N,V}
+ cistate::CartesianIndex{N}
+ ibreak::Int
+ cached_evaluations::NTuple{N,V}
end
-function index_gen1(::Type{BSpline{Linear}})
- quote
- p = iter.itp_tail
- value = c_1*p[1] + cp_1*p[2]
+function eachvalue(sitp::ScaledInterpolation{T,N}) where {T,N}
+ itps = tcollect(itpflag, sitp.itp)
+ newaxes = map(r->Base.Slice(ceil(Int, first(r)):floor(Int, last(r))), sitp.ranges)
+ wis = dimension_wis(value_weights, itps, axes(sitp.itp), newaxes, sitp.ranges)
+ wis1 = wis[1]
+ i1 = first(axes(wis1, 1))
+ breaks1 = [i1]
+ for i in Iterators.drop(axes(wis1, 1), 1)
+ if indexes(wis1[i]) != indexes(wis1[i-1])
+ push!(breaks1, i)
+ end
end
+ push!(breaks1, last(axes(wis1, 1))+1)
+ ScaledIterator(sitp, CartesianIndices(newaxes), wis, breaks1)
end
-function index_gen1(::Type{BSpline{Q}}) where Q<:Quadratic
- quote
- p = iter.itp_tail
- value = cm_1*p[1] + c_1*p[2] + cp_1*p[3]
+function dimension_wis(f::F, itps, axs, newaxes, ranges) where F
+ itpflag, ax, nax, r = itps[1], axs[1], newaxes[1], ranges[1]
+ function makewi(x)
+ pos, coefs = weightedindex_parts((f,), itpflag, ax, coordlookup(r, x))
+ maybe_weightedindex(pos, coefs[1])
end
-end
-function index_gen_tail(B::Union{Type{NoInterp}, Type{BSpline{Constant}}}, ::Type{IT}, N) where IT
- [index_gen(B, IT, N, 0)]
-end
-
-function index_gen_tail(::Type{BSpline{Linear}}, ::Type{IT}, N) where IT
- [index_gen(BS1, IT, N, i) for i = 0:1]
-end
-
-function index_gen_tail(::Type{BSpline{Q}}, ::Type{IT}, N) where {IT,Q<:Quadratic}
- [index_gen(BSpline{Q}, IT, N, i) for i = -1:1]
-end
-function nremaining_gen(::Union{Type{BSpline{Constant}}, Type{BSpline{Q}}}) where Q<:Quadratic
- quote
- EPS = 0.001*iter.dx_1
- floor(Int, iter.dx_1 >= 0 ?
- (min(length(range1)+EPS, round(Int,x_1) + 0.5) - x_1)/iter.dx_1 :
- (max(1-EPS, round(Int,x_1) - 0.5) - x_1)/iter.dx_1)
+ (makewi.(nax), dimension_wis(f, Base.tail(itps), Base.tail(axs), Base.tail(newaxes), Base.tail(ranges))...)
+end
+dimension_wis(f, ::Tuple{}, ::Tuple{}, ::Tuple{}, ::Tuple{}) = ()
+
+function Base.iterate(iter::ScaledIterator)
+ ret = iterate(iter.ci)
+ ret === nothing && return nothing
+ item, cistate = ret
+ wis = getindex.(iter.wis, Tuple(item))
+ ces = cache_evaluations(iter.sitp.itp.coefs, indexes(wis[1]), weights(wis[1]), Base.tail(wis))
+ return _reduce(+, weights(wis[1]).*ces), ScaledIterState(cistate, first(iter.breaks1), ces)
+end
+
+function Base.iterate(iter::ScaledIterator, state)
+ ret = iterate(iter.ci, state.cistate)
+ ret === nothing && return nothing
+ item, cistate = ret
+ i1 = item[1]
+ isnext1 = i1 == state.cistate[1]+1
+ if isnext1 && i1 < iter.breaks1[state.ibreak+1]
+ # We can use the previously cached values
+ wis1 = iter.wis[1][i1]
+ return _reduce(+, weights(wis1).*state.cached_evaluations), ScaledIterState(cistate, state.ibreak, state.cached_evaluations)
end
+ # Re-evaluate. We're being a bit lazy here: in some cases, some of the cached values could be reused
+ wis = getindex.(iter.wis, Tuple(item))
+ ces = cache_evaluations(iter.sitp.itp.coefs, indexes(wis[1]), weights(wis[1]), Base.tail(wis))
+ return _reduce(+, weights(wis[1]).*ces), ScaledIterState(cistate, isnext1 ? state.ibreak+1 : first(iter.breaks1), ces)
end
-function nremaining_gen(::Type{BSpline{Linear}})
- quote
- EPS = 0.001*iter.dx_1
- floor(Int, iter.dx_1 >= 0 ?
- (min(length(range1)+EPS, floor(Int,x_1) + 1) - x_1)/iter.dx_1 :
- (max(1-EPS, floor(Int,x_1)) - x_1)/iter.dx_1)
- end
-end
-function next_gen(::Type{ScaledIterator{CR,SITPT,X1,Deg,T}}) where {CR,SITPT,X1,Deg,T}
- N = ndims(CR)
- ITPT = basetype(SITPT)
- IT = itptype(ITPT)
- BS1 = iextract(IT, 1)
- BS1 == NoInterp && error("eachvalue is not implemented (and does not make sense) for NoInterp along the first dimension")
- pad = padding(ITPT)
- x_syms = [Symbol("x_", i) for i = 1:N]
- interp_index(IT, i) = iextract(IT, i) != NoInterp ?
- :($(x_syms[i]) = coordlookup(sitp.ranges[$i], state[$i])) :
- :($(x_syms[i]) = state[$i])
- # Calculations for the first dimension
- interp_index1 = interp_index(IT, 1)
- indices1 = define_indices_d(BS1, 1, padextract(pad, 1))
- coefexprs1 = coefficients(BS1, N, 1)
- nremaining_expr = nremaining_gen(BS1)
- # Calculations for the rest of the dimensions
- interp_indices_tail = map(i -> interp_index(IT, i), 2:N)
- indices_tail = [define_indices_d(iextract(IT, i), i, padextract(pad, i)) for i = 2:N]
- coefexprs_tail = [coefficients(iextract(IT, i), N, i) for i = 2:N]
- value_exprs_tail = index_gen_tail(BS1, IT, N)
- quote
- sitp = iter.sitp
- itp = sitp.itp
- inds_itp = axes(itp)
- if iter.nremaining > 0
- iter.nremaining -= 1
- iter.fx_1 += iter.dx_1
- else
- range1 = sitp.ranges[1]
- $interp_index1
- $indices1
- iter.nremaining = $nremaining_expr
- iter.fx_1 = fx_1
- $(interp_indices_tail...)
- $(indices_tail...)
- $(coefexprs_tail...)
- @inbounds iter.itp_tail = ($(value_exprs_tail...),)
- end
- fx_1 = iter.fx_1
- $coefexprs1
- $(index_gen1(BS1))
- end
-end
+_reduce(op, list) = op(list[1], _reduce(op, Base.tail(list)))
+_reduce(op, list::Tuple{Number}) = list[1]
+_reduce(op, list::Tuple{}) = error("cannot reduce an empty list")
-@static if VERSION < v"0.7.0-DEV.5126"
- @generated function next(iter::ScaledIterator{CR,ITPT}, state::CartesianIndex{N}) where {CR,ITPT,N}
- value_expr = next_gen(iter)
- quote
- $value_expr
- (value, next(iter.rng, state)[2])
- end
- end
-else
- @generated function iterate(iter::ScaledIterator{CR,ITPT}, state::Union{Nothing,CartesianIndex{N}} = nothing) where {CR,ITPT,N}
- value_expr = next_gen(iter)
- quote
- rng_next = state ≡ nothing ? iterate(iter.rng) : iterate(iter.rng, state)
- rng_next ≡ nothing && return nothing
- state = rng_next[2]
- $value_expr
- (value, state)
- end
- end
-end
+# We use weights only as a ruler to determine when we are done
+cache_evaluations(coefs, i::Int, weights, rest) = (coefs[i, rest...], cache_evaluations(coefs, i+1, Base.tail(weights), rest)...)
+cache_evaluations(coefs, indexes, weights, rest) = (coefs[indexes[1], rest...], cache_evaluations(coefs, Base.tail(indexes), Base.tail(weights), rest)...)
+cache_evaluations(coefs, ::Int, ::Tuple{}, rest) = ()
+cache_evaluations(coefs, ::Any, ::Tuple{}, rest) = ()
ssize(sitp::ScaledInterpolation{T,N}) where {T,N} = map(r->round(Int, last(r)-first(r)+1), sitp.ranges)::NTuple{N,Int}
diff --git a/src/utils.jl b/src/utils.jl
index c740b1bd..f0b025a6 100644
--- a/src/utils.jl
+++ b/src/utils.jl
@@ -2,3 +2,95 @@
@inline cub(x) = x*x*x
modrange(x, r::AbstractUnitRange) = mod(x-first(r), length(r)) + first(r)
+modrange(x, (l, u)::Tuple{Real,Real}) = mod(x-l, u-l+1) + l
+
+fmap(fs, x...) = _fmap(x, fs...)
+@inline _fmap(x, f, fs...) = (f(x...), _fmap(x, fs...)...)
+@inline _fmap(x) = ()
+
+split_flag(f::Flag) = f, f
+split_flag(t::Tuple) = t[1], Base.tail(t)
+
+getfirst(f::Flag) = f
+getfirst(t::Tuple) = t[1]
+getrest(f::Flag) = f
+getrest(t::Tuple) = Base.tail(t)
+
+tcollect(f, itp::AbstractInterpolation{T,N}) where {T,N} = _tcollect(ntuple(d->true, Val(N)), f(itp))
+@inline _tcollect(ruler, prop) = (getfirst(prop), _tcollect(Base.tail(ruler), getrest(prop))...)
+_tcollect(::Tuple{}, prop) = ()
+
+split_trailing(::AbstractArray{T,N}, x) where {T,N} = Base.IteratorsMD.split(x, Val(N))
+check1(args) = _check1(true, args...)
+@inline _check1(tf, a, args...) = _check1(tf & (a == 1), args...)
+_check1(tf) = tf
+
+# These are not inferrable for mixed-type tuples, so when that's important use `getfirst`
+# and `getrest` instead.
+iextract(f::Flag, d) = f
+iextract(t::Tuple, d) = t[d]
+
+splitgrouped(prs::Tuple{Vararg{NTuple{2,Any}}}) = first.(prs), last.(prs)
+splitgrouped(prs::Tuple{Vararg{NTuple{3,Any}}}) = first.(prs), middle.(prs), last.(prs)
+middle(t::Tuple{Any,Any,Any}) = t[2]
+
+fast_trunc(::Type{Int}, x) = unsafe_trunc(Int, x)
+fast_trunc(::Type{Int}, x::Rational) = x.num ÷ x.den
+
+# Slot-substitution guided by a `ruler` tuple. Substitution occurs when `default` has the same
+# length as `ruler`.
+@inline substitute_ruled(default, ruler, subst) = (default[1], substitute_ruled(Base.tail(default), ruler, Base.tail(subst))...)
+@inline substitute_ruled(default::NTuple{N,Any}, ruler::NTuple{N,Any}, subst) where N =
+ (subst[1], substitute_ruled(Base.tail(default), ruler, Base.tail(subst))...)
+substitute_ruled(default::Tuple{}, ruler::NTuple{N,Any}, subst) where N = ()
+
+@inline skip_nointerp(x, rest...) = (x, skip_nointerp(rest...)...)
+@inline skip_nointerp(::NoInterp, rest...) = skip_nointerp(rest...)
+skip_nointerp() = ()
+
+skip_flagged_nointerp(itp::AbstractInterpolation, xs) = skip_flagged_nointerp(tcollect(itpflag, itp), xs)
+skip_flagged_nointerp(itpflags::Tuple{NoInterp,Vararg{Any}}, xs) = skip_flagged_nointerp(Base.tail(itpflags), Base.tail(xs))
+skip_flagged_nointerp(itpflags::Tuple, xs) = (xs[1], skip_flagged_nointerp(Base.tail(itpflags), Base.tail(xs))...)
+skip_flagged_nointerp(::Tuple{}, ::Tuple{}) = ()
+
+@inline sumvals(val, δval, args...) = sumvals(val+δval, args...)
+@inline sumvals(val, ::Nothing, args...) = sumvals(val, args...)
+sumvals(val) = val
+
+@inline promote_typeof(a, b, args...) = _promote_typeof(promote_type(typeof(a), typeof(b)), args...)
+@inline _promote_typeof(::Type{T}, a, args...) where T = _promote_typeof(promote_type(T, typeof(a)), args...)
+_promote_typeof(::Type{T}) where T = T
+
+## Vector indexing utilities
+# Drop dimensions associated with "scalar" WeightedIndexes
+shape(i::WeightedIndex, rest...) = shape(rest...)
+shape(i::Number, rest...) = shape(rest...)
+shape(v::AbstractVector, rest...) = (axes1(v), shape(rest...)...)
+shape() = ()
+
+@inline keepvectors(v::Vector{Int}, rest...) = (v, keepvectors(rest...)...)
+@inline keepvectors(x, rest...) = keepvectors(rest...)
+keepvectors() = ()
+
+@inline lispyprod(p, v::Vector{T}, rest...) where T = lispyprod(p*zero(T), rest...)
+@inline lispyprod(p, x::Number, rest...) = lispyprod(p*x, rest...)
+lispyprod(p) = p
+
+const onevec = 1:1
+const emptyvec = 1:0
+@inline inbounds(itp::AbstractInterpolation, x...) = _inbounds.(bounds(itp), x)
+_inbounds((l,u)::Tuple{Number,Number}, x::Number) = ifelse(l <= x <= u, onevec, emptyvec)
+function _inbounds((l,u)::Tuple{Number,Number}, x::AbstractVector)
+ ret = Int[]
+ for i in eachindex(x)
+ l <= x[i] <= u && push!(ret, i)
+ end
+ ret
+end
+
+function getindex!(dest, itp, xs...)
+ for (i, x) in zip(eachindex(dest), Iterators.product(xs...))
+ dest[i] = itp(x...)
+ end
+ return dest
+end
diff --git a/test/InterpolationTestUtils.jl b/test/InterpolationTestUtils.jl
new file mode 100644
index 00000000..8232cd3d
--- /dev/null
+++ b/test/InterpolationTestUtils.jl
@@ -0,0 +1,164 @@
+module InterpolationTestUtils
+
+using Test, Interpolations, ForwardDiff, StaticArrays
+using Interpolations: degree, itpflag, bounds, lbounds, ubounds
+
+export check_axes, check_inbounds_values, check_oob, can_eval_near_boundaries,
+ check_gradient, check_hessian
+export MyPair
+
+const failstore = Ref{Any}(nothing) # stash the inputs to failing tests here
+
+## Property accessors
+coefs(itp) = itp.coefs
+
+boundaryconditions(itp::AbstractInterpolation) = boundaryconditions(itpflag(itp))
+boundaryconditions(bs::BSpline) = degree(bs)
+boundaryconditions(ni::NoInterp) = ni
+
+ndaccessor(x, d) = x
+ndaccessor(x::Tuple, d) = x[d]
+
+haspadding(itp) = haspadding(boundaryconditions(itp))
+haspadding(::Union{Constant,Linear,NoInterp}) = false
+haspadding(::Quadratic{BC}) where BC = haspadding(BC())
+haspadding(::Cubic{BC}) where BC = haspadding(BC())
+haspadding(::BC) where BC<:Interpolations.BoundaryCondition = !(BC <: Union{Periodic,InPlace,InPlaceQ})
+haspadding(bcs::Tuple) = map(haspadding, bcs)
+haspadding(::NoInterp) = false
+
+getindexib(itp, i...) = @inbounds itp[i...]
+callib(itp, i...) = @inbounds itp(i...)
+
+⊂(r1::AbstractRange, r2::AbstractRange) = first(r2) < first(r1) < last(r2) && first(r2) < last(r1) < last(r2)
+
+function check_axes(itp, A, isinplace=false)
+ @test ndims(itp) == ndims(A)
+ axsi, axsA = @inferred(axes(itp)), axes(A)
+ szi, szA = @inferred(size(itp)), size(A)
+ haspad = haspadding(itp)
+ for d = 1:ndims(A)
+ if isinplace && ndaccessor(haspad, d)
+ @test axsi[d] != axsA[d] && axsi[d] ⊂ axsA[d]
+ @test szi[d] < szA[d]
+ else
+ @test axsi[d] == axsA[d]
+ @test szi[d] == szA[d]
+ end
+ end
+ nothing
+end
+
+function check_inbounds_values(itp, A)
+ i = first(eachindex(itp))
+ @test A[i] ≈ @inferred(itp[i]) == @inferred(itp[Tuple(i)...]) ≈ @inferred(itp(i)) ≈ @inferred(itp(float.(Tuple(i))...))
+ for i in eachindex(itp)
+ @test A[i] ≈ itp[i] == itp[Tuple(i)...] ≈ itp(i) ≈ itp(float.(Tuple(i))...)
+ end
+ cb = Base.JLOptions().check_bounds == 1
+ if ndims(itp) == 1
+ for i in eachindex(itp)
+ @test itp[i,1] ≈ A[i] # used in the AbstractArray display infrastructure
+ @test_throws BoundsError itp[i,2]
+ # if cb
+ # @test_throws BoundsError getindexib(itp, i, 2)
+ # @test_throws BoundsError callib(itp, i, 2)
+ # else
+ # @test getindexib(itp, i, 2) ≈ A[i]
+ # @test callib(itp, i, 2) ≈ A[i]
+ # end
+ end
+ end
+ nothing
+end
+
+function check_oob(itp)
+ widen(r) = first(r)-1:last(r)+1
+ indsi = axes(itp)
+ indsci = CartesianIndices(indsi)
+ for i in CartesianIndices(widen.(indsi))
+ i ∈ indsci && continue
+ @test_throws BoundsError itp[i]
+ end
+ nothing
+end
+
+function can_eval_near_boundaries(itp::AbstractInterpolation{T,1}) where T
+ l, u = bounds(itp, 1)
+ @test isfinite(itp(l+0.1))
+ @test isfinite(itp(u-0.1))
+ @test_throws BoundsError itp(l-0.1)
+ @test_throws BoundsError itp(u+0.1)
+end
+
+function can_eval_near_boundaries(itp::AbstractInterpolation)
+ l, u = Float64.(lbounds(itp)), Float64.(ubounds(itp))
+ for d = 1:ndims(itp)
+ nearl = substitute(l, d, l[d]+0.1)
+ # @show summary(itp) nearl
+ @test isfinite(itp(nearl...))
+ outl = substitute(l, d, l[d]-0.1)
+ @test_throws BoundsError itp(outl...)
+ nearu = substitute(u, d, u[d]-0.1)
+ # @show nearu
+ @test isfinite(itp(nearu...))
+ outu = substitute(u, d, u[d]+0.1)
+ @test_throws BoundsError itp(outu...)
+ end
+end
+
+function substitute(default::NTuple{N,T}, d::Integer, val::T) where {T,N}
+ ntuple(i->ifelse(i==d, val, default[i]), Val(N))
+end
+
+
+# Generate a grid of points [1.0, 1.3333, 1.6667, 2.0, 2.3333, ...] along each coordinate
+thirds(axs) = Iterators.product(_thirds(axs...)...)
+
+_thirds(a, axs...) =
+ (sort(Float64[a; (first(a):last(a)-1) .+ 1/3; (first(a)+1:last(a)) .- 1/3]), _thirds(axs...)...)
+_thirds() = ()
+
+function check_gradient(itp::AbstractInterpolation, gtmp)
+ val(x) = itp(Tuple(x)...)
+ g!(gstore, x) = ForwardDiff.gradient!(gstore, val, x)
+ gtmp2 = similar(gtmp)
+ i = first(thirds(axes(itp)))
+ @inferred(Interpolations.gradient(itp, i...))
+ for i in thirds(axes(itp))
+ @test Interpolations.gradient(itp, i...) ≈ g!(gtmp, SVector(i))
+ @test Interpolations.gradient!(gtmp2, itp, i...) ≈ gtmp
+ end
+end
+
+function check_hessian(itp::AbstractInterpolation, htmp)
+ val(x) = itp(Tuple(x)...)
+ h!(hstore, x) = ForwardDiff.hessian!(hstore, val, x)
+ htmp2 = similar(htmp)
+ i = first(thirds(axes(itp)))
+ @inferred(Interpolations.hessian(itp, i...))
+ for i in thirds(axes(itp))
+ @test Interpolations.hessian(itp, i...) ≈ h!(htmp, SVector(i))
+ @test Interpolations.hessian!(htmp2, itp, i...) ≈ htmp
+ end
+end
+
+## A type used for multi-valued tests
+import Base: +, -, *, /, ≈
+
+struct MyPair{T}
+ first::T
+ second::T
+end
+
+# Here's the interface your type must define
+(+)(p1::MyPair, p2::MyPair) = MyPair(p1.first+p2.first, p1.second+p2.second)
+(-)(p1::MyPair, p2::MyPair) = MyPair(p1.first-p2.first, p1.second-p2.second)
+(*)(n::Number, p::MyPair) = MyPair(n*p.first, n*p.second)
+(*)(p::MyPair, n::Number) = n*p
+(/)(p::MyPair, n::Number) = MyPair(p.first/n, p.second/n)
+Base.zero(::Type{MyPair{T}}) where {T} = MyPair(zero(T),zero(T))
+Base.promote_rule(::Type{MyPair{T1}}, ::Type{T2}) where {T1,T2<:Number} = MyPair{promote_type(T1,T2)}
+≈(p1::MyPair, p2::MyPair) = (p1.first ≈ p2.first) & (p1.second ≈ p2.second)
+
+end
diff --git a/test/REQUIRE b/test/REQUIRE
index b9f1e8af..db94cc84 100644
--- a/test/REQUIRE
+++ b/test/REQUIRE
@@ -1,2 +1,3 @@
OffsetArrays
DualNumbers
+ForwardDiff
diff --git a/test/b-splines/constant.jl b/test/b-splines/constant.jl
index 6daecbb9..504be388 100644
--- a/test/b-splines/constant.jl
+++ b/test/b-splines/constant.jl
@@ -1,63 +1,39 @@
-module ConstantTests
-
-using Interpolations
-using Compat.Test
-
-# Instantiation
-N1 = 10
-A1 = rand(Float64, N1) * 100
-A2 = rand(Float64, N1, N1) * 100
-A3 = rand(Float64, N1, N1, N1) * 100
-
-for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy))
- itp1c = @inferred(constructor(copier(A1), BSpline(Constant()), OnCell()))
- itp1g = @inferred(constructor(copier(A1), BSpline(Constant()), OnGrid()))
- itp2c = @inferred(constructor(copier(A2), BSpline(Constant()), OnCell()))
- itp2g = @inferred(constructor(copier(A2), BSpline(Constant()), OnGrid()))
- itp3c = @inferred(constructor(copier(A3), BSpline(Constant()), OnCell()))
- itp3g = @inferred(constructor(copier(A3), BSpline(Constant()), OnGrid()))
-
- @test parent(itp1c) === itp1c.coefs
-
- # Evaluation on provided data points
- # 1D
- for i in 1:length(A1)
- @test A1[i] == itp1c[i] == itp1g[i]
- @test A1[i] == itp1c[convert(Float64,i)] == itp1g[convert(Float64,i)]
- end
- @test @inferred(size(itp1c)) == size(A1)
- @test @inferred(size(itp1g)) == size(A1)
- # 2D
- for i in 1:N1, j in 1:N1
- @test A2[i,j] == itp2c[i,j] == itp2g[i,j]
- @test A2[i,j] == itp2c[convert(Float64,i),convert(Float64,j)] == itp2g[convert(Float64,i),convert(Float64,j)]
- end
- @test @inferred(size(itp2c)) == size(A2)
- @test @inferred(size(itp2g)) == size(A2)
- # 3D
- for i in 1:N1, j in 1:N1, k in 1:N1
- @test A3[i,j,k] == itp3c[i,j,k] == itp3g[i,j,k]
- @test A3[i,j,k] == itp3c[convert(Float64,i),convert(Float64,j),convert(Float64,k)] == itp3g[convert(Float64,i),convert(Float64,j),convert(Float64,k)]
+@testset "Constant" begin
+ # Instantiation
+ N1 = 10
+ A1 = rand(Float64, N1) * 100
+ A2 = rand(Float64, N1, N1) * 100
+ A3 = rand(Float64, N1, N1, N1) * 100
+
+ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy))
+ isinplace = constructor == interpolate!
+ itp1 = @inferred(constructor(copier(A1), BSpline(Constant())))
+ itp2 = @inferred(constructor(copier(A2), BSpline(Constant())))
+ itp3 = @inferred(constructor(copier(A3), BSpline(Constant())))
+
+ @test parent(itp1) === itp1.coefs
+ @test Interpolations.lbounds(itp1) == (1,)
+ @test Interpolations.ubounds(itp1) == (N1,)
+
+ # Evaluation on provided data points
+ for (itp, A) in ((itp1, A1), (itp2, A2), (itp3, A3))
+ check_axes(itp, A, isinplace)
+ check_inbounds_values(itp, A)
+ check_oob(itp)
+ can_eval_near_boundaries(itp)
+ end
+
+ # Evaluation between data points (tests constancy)
+ for i in 2:N1-1
+ @test A1[i] == itp1(i+.3) == itp1(i+.3) == itp1(i-.3) == itp1(i-.3)
+ end
+ # 2D
+ for i in 2:N1-1, j in 2:N1-1
+ @test A2[i,j] == itp2(i+.4,j-.3) == itp2(i+.4,j-.3)
+ end
+ # 3D
+ for i in 2:N1-1, j in 2:N1-1, k in 2:N1-1
+ @test A3[i,j,k] == itp3(i+.4,j-.3,k+.1) == itp3(i+.4,j-.3,k+.2)
+ end
end
- @test @inferred(size(itp3c)) == size(A3)
- @test @inferred(size(itp3g)) == size(A3)
-
- # Evaluation between data points
- for i in 2:N1-1
- @test A1[i] == itp1c[i+.3] == itp1g[i+.3] == itp1c[i-.3] == itp1g[i-.3]
- end
- # 2D
- for i in 2:N1-1, j in 2:N1-1
- @test A2[i,j] == itp2c[i+.4,j-.3] == itp2g[i+.4,j-.3]
- end
- # 3D
- for i in 2:N1-1, j in 2:N1-1, k in 2:N1-1
- @test A3[i,j,k] == itp3c[i+.4,j-.3,k+.1] == itp3g[i+.4,j-.3,k+.2]
- end
-
- # Edge behavior
- @test A1[1] == itp1c[.7]
- @test A1[N1] == itp1c[N1+.3]
-end
-
end
diff --git a/test/b-splines/cubic.jl b/test/b-splines/cubic.jl
index e2f2b086..2ab6ff36 100644
--- a/test/b-splines/cubic.jl
+++ b/test/b-splines/cubic.jl
@@ -1,86 +1,71 @@
-module CubicTests
-
-using Compat.Test
-using Interpolations
-
-for (constructor, copier) in ((interpolate, identity), (interpolate!, copy))
- f0(x) = sin((x-3)*2pi/9 - 1)
- f1(x) = 1.0 + 0.1*x + 0.01*x^2 + 0.001*x^3
-
- xmax = 10
- A0 = Float64[f0(x) for x in 1:xmax]
- A1 = Float64[f1(x) for x in 1:xmax]
-
- f2(x, y) = sin(x/10)*cos(y/6)
- xmax2, ymax2 = 30, 10
- A2 = Float64[f2(x, y) for x in 1:xmax2, y in 1:ymax2]
-
- for BC in (Line, Flat, Free, Periodic), GT in (OnGrid, OnCell)
- for (A, f) in ((A0, f0), (A1, f1))
- itp1 = @inferred(constructor(copier(A), BSpline(Cubic(BC())), GT()))
- @test @inferred(size(itp1)) == size(A)
- @test_throws ArgumentError parent(itp1)
-
- # test that inner region is close to data
- for x in 3.1:.2:8.1
- @test ≈(f(x),itp1[x],atol=abs(0.1 * f(x)))
+@testset "Cubic" begin
+ for (constructor, copier) in ((interpolate, identity), (interpolate!, copy))
+ isinplace = constructor == interpolate!
+ f0(x) = sin((x-3)*2pi/9 - 1)
+ f1(x) = 1.0 + 0.1*x + 0.01*x^2 + 0.001*x^3
+
+ xmax = 10
+ A0 = Float64[f0(x) for x in 1:xmax]
+ A1 = Float64[f1(x) for x in 1:xmax]
+
+ f2(x, y) = sin(x/10)*cos(y/6)
+ xmax2, ymax2 = 30, 10
+ A2 = Float64[f2(x, y) for x in 1:xmax2, y in 1:ymax2]
+
+ for BC in (Line, Flat, Free, Periodic), GT in (OnGrid, OnCell)
+ for (A, f) in ((A0, f0), (A1, f1))
+ itp1 = @inferred(constructor(copier(A), BSpline(Cubic(BC(GT())))))
+ ax1 = axes(itp1)[1]
+ @test Interpolations.lbounds(itp1) == (GT == OnGrid ? (first(ax1),) : (first(ax1) - 0.5,))
+ @test Interpolations.ubounds(itp1) == (GT == OnGrid ? (last(ax1),) : (last(ax1) + 0.5,))
+ @test_throws ArgumentError parent(itp1)
+ check_axes(itp1, A, isinplace)
+ check_inbounds_values(itp1, A)
+ check_oob(itp1)
+ can_eval_near_boundaries(itp1)
+
+ # test that inner region is close to data
+ for x in 3.1:.2:8.1
+ @test f(x) ≈ itp1(x) atol=abs(0.1 * f(x))
+ end
end
- # test that we can evaluate close to, and at, boundaries
- if GT == OnGrid
- itp1[1.]
- itp1[1.0]
- itp1[1.2]
- itp1[9.8]
- itp1[10.]
- itp1[10]
- else
- itp1[0.5]
- itp1[0.6]
- itp1[10.4]
- itp1[10.5]
- end
- end
-
- itp2 = @inferred(constructor(copier(A2), BSpline(Cubic(BC())), GT()))
- @test @inferred(size(itp2)) == size(A2)
+ itp2 = @inferred(constructor(copier(A2), BSpline(Cubic(BC(GT())))))
+ @test_throws ArgumentError parent(itp2)
+ check_axes(itp2, A2, isinplace)
+ check_inbounds_values(itp2, A2)
+ check_oob(itp2)
+ can_eval_near_boundaries(itp2)
- for x in 3.1:.2:xmax2-3, y in 3.1:2:ymax2-3
- @test ≈(f2(x,y),itp2[x,y],atol=abs(0.1 * f2(x,y)))
+ for x in 3.1:.2:xmax2-3, y in 3.1:2:ymax2-3
+ @test f2(x,y) ≈ itp2(x,y) atol=abs(0.1 * f2(x,y))
+ end
end
end
-end
-
-end
-
-module CubicGradientTests
-using Interpolations, Compat.Test, Compat.LinearAlgebra
-using Compat: range
+ ix = 1:15
+ k = length(ix) - 1
+ f(x) = cos((x-1)*2pi/k)
+ g(x) = -2pi/k * sin((x-1)*2pi/k)
-ix = 1:15
-f(x) = cos((x-1)*2pi/(length(ix)-1))
-g(x) = -2pi/14 * sin((x-1)*2pi/(length(ix)-1))
+ A = map(f, ix)
-A = map(f, ix)
+ for (constructor, copier) in ((interpolate, identity), (interpolate!, copy))
-for (constructor, copier) in ((interpolate, identity), (interpolate!, copy))
+ for BC in (Line, Flat, Free, Periodic), GT in (OnGrid,OnCell)
- for BC in (Line, Flat, Free, Periodic), GT in (OnGrid,OnCell)
-
- itp = constructor(copier(A), BSpline(Cubic(BC())), GT())
- # test that inner region is close to data
- for x in range(ix[5], stop=ix[end-4], length=100)
- @test ≈(g(x),(gradient(itp,x))[1],atol=cbrt(cbrt(eps(g(x)))))
+ itp = constructor(copier(A), BSpline(Cubic(BC(GT()))))
+ # test that inner region is close to data
+ for x in range(ix[5], stop=ix[end-4], length=100)
+ @test g(x) ≈ Interpolations.gradient1(itp,x) atol=cbrt(cbrt(eps(g(x))))
+ end
end
end
-end
-itp_flat_g = interpolate(A, BSpline(Cubic(Flat())), OnGrid())
-@test ≈((gradient(itp_flat_g,1))[1],0,atol=eps())
-@test ≈((gradient(itp_flat_g,ix[end]))[1],0,atol=eps())
-
-itp_flat_c = interpolate(A, BSpline(Cubic(Flat())), OnCell())
-@test ≈((gradient(itp_flat_c,0.5))[1],0,atol=eps())
-@test ≈((gradient(itp_flat_c,ix[end] + 0.5))[1],0,atol=eps())
+ itp_flat_g = interpolate(A, BSpline(Cubic(Flat(OnGrid()))))
+ @test Interpolations.gradient(itp_flat_g,1)[1] ≈ 0 atol=eps()
+ @test Interpolations.gradient(itp_flat_g,ix[end])[1] ≈ 0 atol=eps()
+ itp_flat_c = interpolate(A, BSpline(Cubic(Flat(OnCell()))))
+ @test Interpolations.gradient(itp_flat_c,0.5)[1] ≈ 0 atol=eps()
+ @test Interpolations.gradient(itp_flat_c,ix[end] + 0.5)[1] ≈ 0 atol=eps()
end
diff --git a/test/b-splines/function-call-syntax.jl b/test/b-splines/function-call-syntax.jl
deleted file mode 100644
index 5f553b7f..00000000
--- a/test/b-splines/function-call-syntax.jl
+++ /dev/null
@@ -1,25 +0,0 @@
-module ExtrapFunctionCallSyntax
-
-using Compat.Test, Interpolations, DualNumbers
-using Compat: range
-
-# Test if b-spline interpolation by function syntax yields identical results
-f(x) = sin((x-3)*2pi/9 - 1)
-xmax = 10
-A = Float64[f(x) for x in 1:xmax]
-itpg = interpolate(A, BSpline(Linear()), OnGrid())
-schemes = (Flat,Line,Free)
-
-for T in (Cubic, Quadratic), GC in (OnGrid, OnCell)
- for etp in map(S -> @inferred(interpolate(A, BSpline(T(S())), GC())), schemes),
- x in range(1, stop=xmax, length=100)
- @test (getindex(etp, x)) == etp(x)
- end
-end
-
-for T in (Constant, Linear), GC in (OnGrid, OnCell), x in range(1, stop=xmax, length=100)
- etp = interpolate(A, BSpline(T()), GC())
- @test (getindex(etp, x)) == etp(x)
-end
-
-end
diff --git a/test/b-splines/linear.jl b/test/b-splines/linear.jl
index f1e1b5ec..35e21523 100644
--- a/test/b-splines/linear.jl
+++ b/test/b-splines/linear.jl
@@ -1,50 +1,55 @@
-module LinearTests
-
-using Interpolations
-using Compat.Test
-
-xmax = 10
-g1(x) = sin((x-3)*2pi/(xmax-1)-1)
-f(x) = g1(x)
-A1 = Float64[f(x) for x in 1:xmax]
-fr(x) = (x^2) // 40 + 2
-
-ymax = 10
-g2(y) = cos(y/6)
-f(x,y) = g1(x)*g2(y)
-A2 = Float64[f(x,y) for x in 1:xmax, y in 1:ymax]
-
-for (constructor, copier) in ((interpolate, identity), (interpolate!, copy))
- itp1c = @inferred(constructor(copier(A1), BSpline(Linear()), OnCell()))
-
- @test parent(itp1c) === itp1c.coefs
-
- # Just interpolation
- for x in 1:.2:xmax
- @test ≈(f(x),itp1c[x],atol=abs(0.1 * f(x)))
+@testset "Linear" begin
+ xmax = 10
+ g1(x) = sin((x-3)*2pi/(xmax-1)-1)
+ f(x) = g1(x)
+ A1 = Float64[f(x) for x in 1:xmax]
+ fr(x) = (x^2) // 40 + 2
+
+ ymax = 10
+ g2(y) = cos(y/6)
+ f(x,y) = g1(x)*g2(y)
+ A2 = Float64[f(x,y) for x in 1:xmax, y in 1:ymax]
+
+ for (constructor, copier) in ((interpolate, identity), (interpolate!, copy))
+ isinplace = constructor == interpolate!
+ itp1 = @inferred(constructor(copier(A1), BSpline(Linear())))
+ itp2 = @inferred(constructor(copier(A2), BSpline(Linear())))
+
+ @test parent(itp1) === itp1.coefs
+ @test Interpolations.lbounds(itp1) == (1,)
+ @test Interpolations.ubounds(itp1) == (xmax,)
+
+ for (itp, A) in ((itp1, A1), (itp2, A2))
+ check_axes(itp, A, isinplace)
+ check_inbounds_values(itp, A)
+ check_oob(itp)
+ can_eval_near_boundaries(itp)
+ I = first(eachindex(itp))
+ @test itp(I) == itp(Tuple(I)...)
+ end
+
+ # Just interpolation
+ for x in 1:.2:xmax
+ @test f(x) ≈ itp1(x) atol=abs(0.1 * f(x))
+ end
+
+ # 2D
+ for x in 2.1:.2:xmax-1, y in 1.9:.2:ymax-.9
+ @test ≈(f(x,y),itp2(x,y),atol=abs(0.25 * f(x,y)))
+ end
+
+ # Rational element types
+ A1R = Rational{Int}[fr(x) for x in 1:10]
+ itp1r = @inferred(constructor(copier(A1R), BSpline(Linear())))
+ @test @inferred(size(itp1r)) == size(A1R)
+ @test itp1r(23 // 10) ≈ fr(23 // 10) atol=abs(0.1 * fr(23 // 10))
+ @test typeof(itp1r(23//10)) == Rational{Int}
+ @test eltype(itp1r) == Rational{Int}
end
- # Rational element types
- A1R = Rational{Int}[fr(x) for x in 1:10]
- itp1r = @inferred(constructor(copier(A1R), BSpline(Linear()), OnGrid()))
- @test @inferred(size(itp1r)) == size(A1R)
- @test ≈(itp1r[23 // 10],fr(23 // 10),atol=abs(0.1 * fr(23 // 10)))
- @test typeof(itp1r[23//10]) == Rational{Int}
- @test eltype(itp1r) == Rational{Int}
-
- # 2D
- itp2 = @inferred(constructor(copier(A2), BSpline(Linear()), OnGrid()))
- @test @inferred(size(itp2)) == size(A2)
-
- for x in 2.1:.2:xmax-1, y in 1.9:.2:ymax-.9
- @test ≈(f(x,y),itp2[x,y],atol=abs(0.25 * f(x,y)))
- end
-end
-
-# Issue #183
-x = rand(3,3,3)
-itp = interpolate(x, BSpline(Linear()), OnGrid())
-@test itp[1.5, CartesianIndex((2, 3))] === itp[1.5, 2, 3]
-@test itp[CartesianIndex((1, 2)), 1.5] == itp[1, 2, 1.5]
-
+ # Issue #183
+ x = rand(3,3,3)
+ itp = interpolate(x, BSpline(Linear()))
+ @test itp(1.5, CartesianIndex((2, 3))) === itp(1.5, 2, 3)
+ @test itp(CartesianIndex((1, 2)), 1.5) === itp(1, 2, 1.5)
end
diff --git a/test/b-splines/mixed.jl b/test/b-splines/mixed.jl
index 2e9a3325..aea46fe0 100644
--- a/test/b-splines/mixed.jl
+++ b/test/b-splines/mixed.jl
@@ -1,65 +1,83 @@
-module MixedTests
+@testset "Mixed" begin
+ N = 10
-using Interpolations, Compat, Compat.Test, Compat.SharedArrays, Compat.Random
+ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy))
+ A2 = rand(Float64, N, N) * 100
+ for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
+ itp_a = @inferred(constructor(copier(A2), (BSpline(Linear()), BSpline(Quadratic(BC(GT()))))))
+ itp_b = @inferred(constructor(copier(A2), (BSpline(Quadratic(BC(GT()))), BSpline(Linear()))))
+ isfullsize = constructor == interpolate || BC==Periodic
+ if isfullsize
+ @test @inferred(size(itp_a)) == size(A2)
+ @test @inferred(size(itp_b)) == size(A2)
+ @test @inferred(axes(itp_a)) == axes(A2)
+ @test @inferred(axes(itp_b)) == axes(A2)
+ else
+ @test @inferred(size(itp_a)) == (N, N-2)
+ @test @inferred(size(itp_b)) == (N-2, N)
+ @test @inferred(axes(itp_a)) == (1:N, 2:N-1)
+ @test @inferred(axes(itp_b)) == (2:N-1, 1:N)
+ end
+ @test_throws ArgumentError parent(itp_a)
+ @test_throws ArgumentError parent(itp_b)
-N = 10
+ for i in eachindex(itp_a)
+ @test itp_a[i] ≈ A2[i] atol=sqrt(eps(A2[i]))
+ end
+ for i in eachindex(itp_b)
+ @test itp_b[i] ≈ A2[i] atol=sqrt(eps(A2[i]))
+ end
-for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy))
- A2 = rand(Float64, N, N) * 100
- for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
- itp_a = @inferred(constructor(copier(A2), (BSpline(Linear()), BSpline(Quadratic(BC()))), GT()))
- itp_b = @inferred(constructor(copier(A2), (BSpline(Quadratic(BC())), BSpline(Linear())), GT()))
- @test @inferred(size(itp_a)) == size(A2)
- @test @inferred(size(itp_b)) == size(A2)
- @test_throws ArgumentError parent(itp_a)
- @test_throws ArgumentError parent(itp_b)
-
- for j = 2:N-1, i = 2:N-1
- @test ≈(itp_a[i,j],A2[i,j],atol=sqrt(eps(A2[i,j])))
- @test ≈(itp_b[i,j],A2[i,j],atol=sqrt(eps(A2[i,j])))
- end
-
- for i = 1:10
- dx, dy = rand(), rand()
- @test itp_a[2 + dx,2] ≈ (1 - dx) * A2[2,2] + dx * A2[3,2]
- @test itp_b[2,2 + dy] ≈ (1 - dy) * A2[2,2] + dy * A2[2,3]
+ for i = 1:10
+ dx, dy = rand(), rand()
+ @test itp_a(2 + dx,2) ≈ (1 - dx) * A2[2,2] + dx * A2[3,2]
+ @test itp_b(2,2 + dy) ≈ (1 - dy) * A2[2,2] + dy * A2[2,3]
+ end
end
end
-end
-
-# AbstractArrays
-makesharedarray(::Type{T}, dims; kwargs...) where {T} = SharedArray{T}(dims; kwargs...)
-function copyshared(A)
- B = makesharedarray(eltype(A), size(A))
- copyto!(B, A)
-end
-for (constructor, copier) in ((interpolate, x->x), (interpolate!, copyshared))
- A2 = makesharedarray(Float64, (N,N), init=A->rand!(A))
- for i = 1:length(A2)
- A2[i] *= 100
+ # AbstractArrays
+ makesharedarray(::Type{T}, dims; kwargs...) where {T} = SharedArray{T}(dims; kwargs...)
+ function copyshared(A)
+ B = makesharedarray(eltype(A), size(A))
+ copyto!(B, A)
end
- for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
- itp_a = @inferred(constructor(copier(A2), (BSpline(Linear()), BSpline(Quadratic(BC()))), GT()))
- itp_b = @inferred(constructor(copier(A2), (BSpline(Quadratic(BC())), BSpline(Linear())), GT()))
- if constructor == interpolate!
- @test isa(itp_a.coefs, SharedArray)
- @test isa(itp_b.coefs, SharedArray)
- end
- @test @inferred(size(itp_a)) == size(A2)
- @test @inferred(size(itp_b)) == size(A2)
- for j = 2:N-1, i = 2:N-1
- @test ≈(itp_a[i,j],A2[i,j],atol=sqrt(eps(A2[i,j])))
- @test ≈(itp_b[i,j],A2[i,j],atol=sqrt(eps(A2[i,j])))
+ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copyshared))
+ A2 = makesharedarray(Float64, (N,N), init=A->rand!(A))
+ for i = 1:length(A2)
+ A2[i] *= 100
end
+ for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
+ itp_a = @inferred(constructor(copier(A2), (BSpline(Linear()), BSpline(Quadratic(BC(GT()))))))
+ itp_b = @inferred(constructor(copier(A2), (BSpline(Quadratic(BC(GT()))), BSpline(Linear()))))
+ if constructor == interpolate!
+ @test isa(itp_a.coefs, SharedArray)
+ @test isa(itp_b.coefs, SharedArray)
+ end
+ isfullsize = constructor == interpolate || BC==Periodic
+ if isfullsize
+ @test @inferred(size(itp_a)) == size(A2)
+ @test @inferred(size(itp_b)) == size(A2)
+ @test @inferred(axes(itp_a)) == axes(A2)
+ @test @inferred(axes(itp_b)) == axes(A2)
+ else
+ @test @inferred(size(itp_a)) == (N, N-2)
+ @test @inferred(size(itp_b)) == (N-2, N)
+ @test @inferred(axes(itp_a)) == (1:N, 2:N-1)
+ @test @inferred(axes(itp_b)) == (2:N-1, 1:N)
+ end
+
+ for j = 2:N-1, i = 2:N-1
+ @test ≈(itp_a[i,j],A2[i,j],atol=sqrt(eps(A2[i,j])))
+ @test ≈(itp_b[i,j],A2[i,j],atol=sqrt(eps(A2[i,j])))
+ end
- for i = 1:10
- dx, dy = rand(), rand()
- @test itp_a[2 + dx,2] ≈ (1 - dx) * A2[2,2] + dx * A2[3,2]
- @test itp_b[2,2 + dy] ≈ (1 - dy) * A2[2,2] + dy * A2[2,3]
+ for i = 1:10
+ dx, dy = rand(), rand()
+ @test itp_a(2 + dx,2) ≈ (1 - dx) * A2[2,2] + dx * A2[3,2]
+ @test itp_b(2,2 + dy) ≈ (1 - dy) * A2[2,2] + dy * A2[2,3]
+ end
end
end
end
-
-end
diff --git a/test/b-splines/multivalued.jl b/test/b-splines/multivalued.jl
index 55400472..9182c39a 100644
--- a/test/b-splines/multivalued.jl
+++ b/test/b-splines/multivalued.jl
@@ -1,44 +1,25 @@
-module NonNumeric
-
-# Test interpolation with a multi-valued type
-
-using Interpolations
-using Compat
-
-import Base: +, -, *, /
-
-struct MyPair{T}
- first::T
- second::T
-end
-
-# Here's the interface your type must define
-(+)(p1::MyPair, p2::MyPair) = MyPair(p1.first+p2.first, p1.second+p2.second)
-(-)(p1::MyPair, p2::MyPair) = MyPair(p1.first-p2.first, p1.second-p2.second)
-(*)(n::Number, p::MyPair) = MyPair(n*p.first, n*p.second)
-(*)(p::MyPair, n::Number) = n*p
-(/)(p::MyPair, n::Number) = MyPair(p.first/n, p.second/n)
-Base.zero(::Type{MyPair{T}}) where {T} = MyPair(zero(T),zero(T))
-Base.promote_rule(::Type{MyPair{T1}}, ::Type{T2}) where {T1,T2<:Number} = MyPair{promote_type(T1,T2)}
-Base.promote_op(::typeof(*), ::Type{MyPair{T1}}, ::Type{T2}) where {T1,T2<:Number} = MyPair{promote_type(T1,T2)}
-Base.promote_op(::typeof(*), ::Type{T1}, ::Type{MyPair{T2}}) where {T1<:Number,T2} = MyPair{promote_type(T1,T2)}
-
-# 1d
-A = reinterpret(MyPair{Float64}, rand(20))
-itp = interpolate(A, BSpline(Constant()), OnGrid())
-itp[3.2]
-itp = interpolate(A, BSpline(Linear()), OnGrid())
-itp[3.2]
-itp = interpolate(A, BSpline(Quadratic(Flat())), OnGrid())
-itp[3.2]
-
-# 2d
-A = reshape(reinterpret(MyPair{Float64}, rand(100)), (10,5))
-itp = interpolate(A, BSpline(Constant()), OnGrid())
-itp[3.2,1.8]
-itp = interpolate(A, BSpline(Linear()), OnGrid())
-itp[3.2,1.8]
-itp = interpolate(A, BSpline(Quadratic(Flat())), OnGrid())
-itp[3.2,1.8]
-
+@testset "Multivalued" begin
+ # 1d
+ A0 = rand(20)
+ A = reinterpret(MyPair{Float64}, A0)
+ a1, a2 = A0[1:2:end], A0[2:2:end]
+ @test length(A) == 10
+ itp = interpolate(A, BSpline(Constant()))
+ @test itp(3.2) ≈ MyPair(A0[5],A0[6])
+ itp = interpolate(A, BSpline(Linear()))
+ @test itp(3.2) ≈ 0.8*MyPair(A0[5],A0[6]) + 0.2*MyPair(A0[7],A0[8])
+ it = BSpline(Quadratic(Flat(OnGrid())))
+ itp = interpolate(A, it)
+ @test itp(3.2) ≈ MyPair(interpolate(a1, it)(3.2), interpolate(a2, it)(3.2))
+
+ # 2d
+ A0 = rand(100)
+ A = reshape(reinterpret(MyPair{Float64}, A0), (10,5))
+ a1, a2 = reshape(A0[1:2:end], (10,5)), reshape(A0[2:2:end], (10,5))
+ for it in (BSpline(Constant()),
+ BSpline(Linear()),
+ BSpline(Quadratic(Flat(OnGrid()))))
+ itp = interpolate(A, it)
+ @test itp(3.2,1.8) ≈ MyPair(interpolate(a1, it)(3.2,1.8), interpolate(a2, it)(3.2,1.8))
+ end
end
diff --git a/test/b-splines/non1.jl b/test/b-splines/non1.jl
index 410bc6c9..82be4c29 100644
--- a/test/b-splines/non1.jl
+++ b/test/b-splines/non1.jl
@@ -1,94 +1,86 @@
-module Non1Tests
-
-using Interpolations, OffsetArrays, AxisAlgorithms, Compat.Test
-using Compat: axes
-
-# At present, for a particular type of non-1 array you need to specialize this function
-function AxisAlgorithms.A_ldiv_B_md!(dest::OffsetArray, F, src::OffsetArray, dim::Integer, b::AbstractVector)
- indsdim = axes(parent(src), dim)
- indsF = axes(F)[2]
- if indsF == indsdim
- return A_ldiv_B_md!(parent(dest), F, parent(src), dim, b)
- end
- throw(DimensionMismatch("indices $(axes(parent(src))) do not match $(axes(F))"))
-end
-
-for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy))
- f1(x) = sin((x-3)*2pi/9 - 1)
- inds = -3:6
- A1 = OffsetArray(Float64[f1(x) for x in inds], inds)
-
- f2(x,y) = sin(x/10)*cos(y/6) + 0.1
- xinds, yinds = -2:28,0:9
- A2 = OffsetArray(Float64[f2(x,y) for x in xinds, y in yinds], xinds, yinds)
-
- for GT in (OnGrid, OnCell), O in (Constant, Linear)
- itp1 = @inferred(constructor(copier(A1), BSpline(O()), GT()))
- @test @inferred(axes(itp1)) === axes(A1)
-
- # test that we reproduce the values at on-grid points
- for x = inds
- @test itp1[x] ≈ f1(x)
- end
-
- itp2 = @inferred(constructor(copier(A2), BSpline(O()), GT()))
- @test @inferred(axes(itp2)) === axes(A2)
- for j = yinds, i = xinds
- @test itp2[i,j] ≈ A2[i,j]
+using AxisAlgorithms, OffsetArrays
+
+@testset "Unconventional axes" begin
+ # At present, for a particular type of non-1 array you need to specialize this function
+ function AxisAlgorithms.A_ldiv_B_md!(dest::OffsetArray, F, src::OffsetArray, dim::Integer, b::AbstractVector)
+ indsdim = axes(parent(src), dim)
+ indsF = axes(F)[2]
+ if indsF == indsdim
+ return A_ldiv_B_md!(parent(dest), F, parent(src), dim, b)
end
+ throw(DimensionMismatch("indices $(axes(parent(src))) do not match $(axes(F))"))
end
- for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
- itp1 = @inferred(constructor(copier(A1), BSpline(Quadratic(BC())), GT()))
- @test @inferred(axes(itp1)) === axes(A1)
-
- # test that we reproduce the values at on-grid points
- inset = constructor == interpolate!
- for x = first(inds)+inset:last(inds)-inset
- @test itp1[x] ≈ f1(x)
- end
-
- itp2 = @inferred(constructor(copier(A2), BSpline(Quadratic(BC())), GT()))
- @test @inferred(axes(itp2)) === axes(A2)
- for j = first(yinds)+inset:last(yinds)-inset, i = first(xinds)+inset:last(xinds)-inset
- @test itp2[i,j] ≈ A2[i,j]
+ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy))
+ isinplace = constructor == interpolate!
+ f1(x) = sin((x-3)*2pi/9 - 1)
+ inds = -3:6
+ A1 = OffsetArray(Float64[f1(x) for x in inds], inds)
+
+ f2(x,y) = sin(x/10)*cos(y/6) + 0.1
+ xinds, yinds = -2:28,0:9
+ A2 = OffsetArray(Float64[f2(x,y) for x in xinds, y in yinds], xinds, yinds)
+
+ for O in (Constant, Linear)
+ itp1 = @inferred(constructor(copier(A1), BSpline(O())))
+ check_axes(itp1, A1, isinplace)
+ check_inbounds_values(itp1, A1)
+ check_oob(itp1)
+ can_eval_near_boundaries(itp1)
+
+ itp2 = @inferred(constructor(copier(A2), BSpline(O())))
+ check_axes(itp2, A2, isinplace)
+ check_inbounds_values(itp2, A2)
+ check_oob(itp2)
+ can_eval_near_boundaries(itp2)
end
- end
-
- for BC in (Flat,Line,Free,Periodic), GT in (OnGrid, OnCell)
- itp1 = @inferred(constructor(copier(A1), BSpline(Cubic(BC())), GT()))
- @test @inferred(axes(itp1)) === axes(A1)
- # test that we reproduce the values at on-grid points
- inset = constructor == interpolate!
- for x = first(inds)+inset:last(inds)-inset
- @test itp1[x] ≈ f1(x)
+ for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
+ itp1 = @inferred(constructor(copier(A1), BSpline(Quadratic(BC(GT())))))
+ check_axes(itp1, A1, isinplace)
+ check_inbounds_values(itp1, A1)
+ check_oob(itp1)
+ can_eval_near_boundaries(itp1)
+
+ itp2 = @inferred(constructor(copier(A2), BSpline(Quadratic(BC(GT())))))
+ check_axes(itp2, A2, isinplace)
+ check_inbounds_values(itp2, A2)
+ check_oob(itp2)
+ can_eval_near_boundaries(itp2)
end
- itp2 = @inferred(constructor(copier(A2), BSpline(Cubic(BC())), GT()))
- @test @inferred(axes(itp2)) === axes(A2)
- for j = first(yinds)+inset:last(yinds)-inset, i = first(xinds)+inset:last(xinds)-inset
- @test itp2[i,j] ≈ A2[i,j]
+ for BC in (Flat,Line,Free,Periodic), GT in (OnGrid, OnCell)
+ itp1 = @inferred(constructor(copier(A1), BSpline(Cubic(BC(GT())))))
+ check_axes(itp1, A1, isinplace)
+ check_inbounds_values(itp1, A1)
+ check_oob(itp1)
+ can_eval_near_boundaries(itp1)
+
+ itp2 = @inferred(constructor(copier(A2), BSpline(Cubic(BC(GT())))))
+ check_axes(itp2, A2, isinplace)
+ check_inbounds_values(itp2, A2)
+ check_oob(itp2)
+ can_eval_near_boundaries(itp2)
end
end
-end
-
-let
- f(x) = sin((x-3)*2pi/9 - 1)
- inds = -7:2
- A = OffsetArray(Float64[f(x) for x in inds], inds)
- itp1 = interpolate!(copy(A), BSpline(Quadratic(InPlace())), OnCell())
- for i in inds
- @test itp1[i] ≈ A[i]
- end
- f(x,y) = sin(x/10)*cos(y/6) + 0.1
- xinds, yinds = -2:28,0:9
- A2 = OffsetArray(Float64[f(x,y) for x in xinds, y in yinds], xinds, yinds)
- itp2 = interpolate!(copy(A2), BSpline(Quadratic(InPlace())), OnCell())
- for j = yinds, i = xinds
- @test itp2[i,j] ≈ A2[i,j]
+ let
+ f(x) = sin((x-3)*2pi/9 - 1)
+ inds = -7:2
+ A = OffsetArray(Float64[f(x) for x in inds], inds)
+ itp1 = interpolate!(copy(A), BSpline(Quadratic(InPlace(OnCell()))))
+ check_axes(itp1, A)
+ check_inbounds_values(itp1, A)
+ check_oob(itp1)
+ can_eval_near_boundaries(itp1)
+
+ f(x,y) = sin(x/10)*cos(y/6) + 0.1
+ xinds, yinds = -2:28,0:9
+ A2 = OffsetArray(Float64[f(x,y) for x in xinds, y in yinds], xinds, yinds)
+ itp2 = interpolate!(copy(A2), BSpline(Quadratic(InPlace(OnCell()))))
+ check_axes(itp2, A2)
+ check_inbounds_values(itp2, A2)
+ check_oob(itp2)
+ can_eval_near_boundaries(itp2)
end
end
-
-end
diff --git a/test/b-splines/quadratic.jl b/test/b-splines/quadratic.jl
index 70214bf3..582c3c61 100644
--- a/test/b-splines/quadratic.jl
+++ b/test/b-splines/quadratic.jl
@@ -1,68 +1,58 @@
-module QuadraticTests
-
-using Interpolations, Compat.Test
-
-for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy))
- f(x) = sin((x-3)*2pi/9 - 1)
- xmax = 10
- A = Float64[f(x) for x in 1:xmax]
- for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
- itp1 = @inferred(constructor(copier(A), BSpline(Quadratic(BC())), GT()))
- @test @inferred(size(itp1)) == size(A)
- @test_throws ArgumentError parent(itp1)
-
- # test that inner region is close to data
- for x in 3.1:.2:8.1
- @test ≈(f(x),itp1[x],atol=abs(0.1 * f(x)))
+@testset "Quadratic" begin
+ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy))
+ isinplace = constructor == interpolate!
+ f(x) = sin((x-3)*2pi/9 - 1)
+ xmax = 10
+ A = Float64[f(x) for x in 1:xmax]
+ for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
+ itp1 = @inferred(constructor(copier(A), BSpline(Quadratic(BC(GT())))))
+ ax1 = axes(itp1)[1]
+ @test Interpolations.lbounds(itp1) == (GT == OnGrid ? (first(ax1),) : (first(ax1) - 0.5,))
+ @test Interpolations.ubounds(itp1) == (GT == OnGrid ? (last(ax1),) : (last(ax1) + 0.5,))
+ @test_throws ArgumentError parent(itp1)
+ check_axes(itp1, A, isinplace)
+ check_inbounds_values(itp1, A)
+ check_oob(itp1)
+ can_eval_near_boundaries(itp1)
+
+ # test that inner region is close to data
+ for x in 3.1:.2:8.1
+ @test f(x) ≈ itp1(x) atol=abs(0.1 * f(x))
+ end
end
- # test that we can evaluate close to, and at, boundaries
- if GT == OnGrid
- itp1[1.]
- itp1[1.0]
- itp1[1.2]
- itp1[9.8]
- itp1[10.]
- itp1[10]
- else
- itp1[0.5]
- itp1[0.6]
- itp1[10.4]
- itp1[10.5]
- end
- end
-
- f(x,y) = sin(x/10)*cos(y/6)
- xmax, ymax = 30,10
- A = Float64[f(x,y) for x in 1:xmax, y in 1:ymax]
+ f(x,y) = sin(x/10)*cos(y/6)
+ xmax, ymax = 30,10
+ A = Float64[f(x,y) for x in 1:xmax, y in 1:ymax]
- # test that inner region is close to data
- for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
- itp2 = @inferred(constructor(copier(A), BSpline(Quadratic(BC())), GT()))
- @test @inferred(size(itp2)) == size(A)
-
- for x in 3.1:.2:xmax-3, y in 3.1:2:ymax-3
- @test ≈(f(x,y),itp2[x,y],atol=abs(0.1 * f(x,y)))
+ # test that inner region is close to data
+ for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
+ itp2 = @inferred(constructor(copier(A), BSpline(Quadratic(BC(GT())))))
+ check_axes(itp2, A, isinplace)
+ check_inbounds_values(itp2, A)
+ check_oob(itp2)
+ can_eval_near_boundaries(itp2)
+
+ for x in 3.1:.2:xmax-3, y in 3.1:2:ymax-3
+ @test f(x,y) ≈ itp2(x,y) atol=abs(0.1 * f(x,y))
+ end
end
end
-end
-let
- f(x) = sin((x-3)*2pi/9 - 1)
- xmax = 10
- A = Float64[f(x) for x in 1:xmax]
- itp1 = interpolate!(copy(A), BSpline(Quadratic(InPlace())), OnCell())
- for i = 1:xmax
- @test itp1[i] ≈ A[i]
+ # InPlace
+ let
+ f(x) = sin((x-3)*2pi/9 - 1)
+ xmax = 10
+ A = Float64[f(x) for x in 1:xmax]
+ itp1 = interpolate!(copy(A), BSpline(Quadratic(InPlace(OnCell()))))
+ @test axes(itp1) == axes(A)
+ check_inbounds_values(itp1, A)
+
+ f(x,y) = sin(x/10)*cos(y/6)
+ xmax, ymax = 30,10
+ A = Float64[f(x,y) for x in 1:xmax, y in 1:ymax]
+ itp2 = interpolate!(copy(A), BSpline(Quadratic(InPlace(OnCell()))))
+ @test axes(itp2) == axes(A)
+ check_inbounds_values(itp2, A)
end
-
- f(x,y) = sin(x/10)*cos(y/6)
- xmax, ymax = 30,10
- A = Float64[f(x,y) for x in 1:xmax, y in 1:ymax]
- itp2 = interpolate!(copy(A), BSpline(Quadratic(InPlace())), OnCell())
- for j = 1:ymax, i = 1:xmax
- @test itp2[i,j] ≈ A[i,j]
- end
-end
-
end
diff --git a/test/b-splines/runtests.jl b/test/b-splines/runtests.jl
index 981a6c9e..8b3a620e 100644
--- a/test/b-splines/runtests.jl
+++ b/test/b-splines/runtests.jl
@@ -1,12 +1,9 @@
-module BSplineTests
-
-include("constant.jl")
-include("linear.jl")
-include("quadratic.jl")
-include("cubic.jl")
-include("mixed.jl")
-include("multivalued.jl")
-include("non1.jl")
-include("function-call-syntax.jl")
-
+@testset "BSpline" begin
+ include("constant.jl")
+ include("linear.jl")
+ include("quadratic.jl")
+ include("cubic.jl")
+ include("mixed.jl")
+ include("multivalued.jl")
+ include("non1.jl")
end
diff --git a/test/convenience-constructors.jl b/test/convenience-constructors.jl
index b7774c60..70a30107 100644
--- a/test/convenience-constructors.jl
+++ b/test/convenience-constructors.jl
@@ -1,7 +1,7 @@
module ConvenienceConstructorTests
using Interpolations
-using Compat.Test
+using Test
using Base.Cartesian
# unit test setup
@@ -20,7 +20,7 @@ YLEN = convert(Integer, floor((YMAX - YMIN)/ΔY) + 1)
f(x) = log(x)
A = [f(x) for x in xs]
interp = LinearInterpolation(xs, A) # using convenience constructor
- interp_full = extrapolate(scale(interpolate(A, BSpline(Linear()), OnGrid()), xs), Interpolations.Throw()) # using full constructor
+ interp_full = extrapolate(scale(interpolate(A, BSpline(Linear())), xs), Throw()) # using full constructor
@test typeof(interp) == typeof(interp_full)
@test interp(XMIN) ≈ f(XMIN)
@@ -37,7 +37,7 @@ YLEN = convert(Integer, floor((YMAX - YMIN)/ΔY) + 1)
f(x) = log(x)
A = [f(x) for x in xs]
interp = CubicSplineInterpolation(xs, A)
- interp_full = extrapolate(scale(interpolate(A, BSpline(Cubic(Line())), OnGrid()), xs), Interpolations.Throw())
+ interp_full = extrapolate(scale(interpolate(A, BSpline(Cubic(Line(OnGrid())))), xs), Throw())
@test typeof(interp) == typeof(interp_full)
@test interp(XMIN) ≈ f(XMIN)
@@ -56,7 +56,7 @@ YLEN = convert(Integer, floor((YMAX - YMIN)/ΔY) + 1)
f(x) = log(x)
A = [f(x) for x in xs]
interp = LinearInterpolation(xs, A)
- interp_full = extrapolate(interpolate((xs, ), A, Gridded(Linear())), Interpolations.Throw())
+ interp_full = extrapolate(interpolate((xs, ), A, Gridded(Linear())), Throw())
@test typeof(interp) == typeof(interp_full)
@test interp(xmin) ≈ f(xmin)
@@ -76,8 +76,8 @@ YLEN = convert(Integer, floor((YMAX - YMIN)/ΔY) + 1)
x_lower = XMIN - ΔX
x_higher = XMAX + ΔX
- extrap = LinearInterpolation(xs, A, extrapolation_bc = Interpolations.Linear())
- extrap_full = extrapolate(scale(interpolate(A, BSpline(Linear()), OnGrid()), xs), Interpolations.Linear())
+ extrap = LinearInterpolation(xs, A, extrapolation_bc = Line())
+ extrap_full = extrapolate(scale(interpolate(A, BSpline(Linear())), xs), Line())
@test typeof(extrap) == typeof(extrap_full)
@test extrap(x_lower) ≈ A[1] - ΔA_l
@@ -92,7 +92,7 @@ end
f(x, y) = log(x+y)
A = [f(x,y) for x in xs, y in ys]
interp = LinearInterpolation((xs, ys), A)
- interp_full = extrapolate(scale(interpolate(A, BSpline(Linear()), OnGrid()), xs, ys), Interpolations.Throw())
+ interp_full = extrapolate(scale(interpolate(A, BSpline(Linear())), xs, ys), Throw())
@test typeof(interp) == typeof(interp_full)
@test interp(XMIN,YMIN) ≈ f(XMIN,YMIN)
@@ -115,7 +115,7 @@ end
f(x, y) = log(x+y)
A = [f(x,y) for x in xs, y in ys]
interp = CubicSplineInterpolation((xs, ys), A)
- interp_full = extrapolate(scale(interpolate(A, BSpline(Cubic(Line())), OnGrid()), xs, ys), Interpolations.Throw())
+ interp_full = extrapolate(scale(interpolate(A, BSpline(Cubic(Line(OnGrid())))), xs, ys), Throw())
@test typeof(interp) == typeof(interp_full)
@test interp(XMIN,YMIN) ≈ f(XMIN,YMIN)
@@ -142,7 +142,7 @@ end
f(x, y) = log(x+y)
A = [f(x,y) for x in xs, y in ys]
interp = LinearInterpolation((xs, ys), A)
- interp_full = extrapolate(interpolate((xs, ys), A, Gridded(Linear())), Interpolations.Throw())
+ interp_full = extrapolate(interpolate((xs, ys), A, Gridded(Linear())), Throw())
@test typeof(interp) == typeof(interp_full)
@test interp(xmin,ymin) ≈ f(xmin,ymin)
@@ -171,8 +171,8 @@ end
y_lower = YMIN - ΔY
y_higher = YMAX + ΔY
- extrap = LinearInterpolation((xs, ys), A, extrapolation_bc = (Interpolations.Linear(), Interpolations.Flat()))
- extrap_full = extrapolate(scale(interpolate(A, BSpline(Linear()), OnGrid()), xs, ys), (Interpolations.Linear(), Interpolations.Flat()))
+ extrap = LinearInterpolation((xs, ys), A, extrapolation_bc = (Line(), Flat()))
+ extrap_full = extrapolate(scale(interpolate(A, BSpline(Linear())), xs, ys), (Line(), Flat()))
@test typeof(extrap) == typeof(extrap_full)
@test extrap(x_lower, y_lower) ≈ A[1, 1] - ΔA_l
diff --git a/test/core.jl b/test/core.jl
new file mode 100644
index 00000000..344afb69
--- /dev/null
+++ b/test/core.jl
@@ -0,0 +1,15 @@
+@testset "Core" begin
+ A = reshape([0], 1, 1, 1, 1, 1)
+ wis = ntuple(d->Interpolations.WeightedAdjIndex(1, (1,)), ndims(A))
+ @test @inferred(A[wis...]) === 0
+ wis = ntuple(d->Interpolations.WeightedAdjIndex(1, (1.0,)), ndims(A))
+ @test @inferred(A[wis...]) === 0.0
+ wis = ntuple(d->Interpolations.WeightedArbIndex((1,), (1,)), ndims(A))
+ @test @inferred(A[wis...]) === 0
+ wis = ntuple(d->Interpolations.WeightedArbIndex((1,), (1.0,)), ndims(A))
+ @test @inferred(A[wis...]) === 0.0
+ wis = ntuple(d->Interpolations.WeightedArbIndex((1,1), (1,0)), ndims(A))
+ @test @inferred(A[wis...]) === 0
+ wis = ntuple(d->Interpolations.WeightedArbIndex((1,1), (1.0,0.0)), ndims(A))
+ @test @inferred(A[wis...]) === 0.0
+end
diff --git a/test/extrapolation/function-call-syntax.jl b/test/extrapolation/function-call-syntax.jl
deleted file mode 100644
index 58810c39..00000000
--- a/test/extrapolation/function-call-syntax.jl
+++ /dev/null
@@ -1,104 +0,0 @@
-module ExtrapFunctionCallSyntax
-
-using Compat.Test, Interpolations, DualNumbers
-
-# Test if extrapolation by function syntax yields identical results
-f(x) = sin((x-3)*2pi/9 - 1)
-xmax = 10
-A = Float64[f(x) for x in 1:xmax]
-itpg = interpolate(A, BSpline(Linear()), OnGrid())
-
-schemes = (
- Flat,
- Linear,
- Reflect,
- Periodic
-)
-
-for etp in map(E -> @inferred(extrapolate(itpg, E())), schemes),
- x in [
- # In-bounds evaluation
- 3.4, 3, dual(3.1),
- # Out-of-bounds evaluation
- -3.4, -3, dual(-3,1),
- 13.4, 13, dual(13,1)
- ]
- @test (getindex(etp, x)) == etp(x)
-end
-
-etpg = extrapolate(itpg, Flat())
-@test typeof(etpg) <: AbstractExtrapolation
-
-@test etpg(-3) == etpg(-4.5) == etpg(0.9) == etpg(1.0) == A[1]
-@test etpg(10.1) == etpg(11) == etpg(148.298452) == A[end]
-
-etpf = @inferred(extrapolate(itpg, NaN))
-@test typeof(etpf) <: Interpolations.FilledExtrapolation
-@test parent(etpf) === itpg
-
-@test @inferred(size(etpf)) == (xmax,)
-@test isnan(@inferred(etpf(-2.5)))
-@test isnan(etpf(0.999))
-@test @inferred(etpf(1)) ≈ f(1)
-@test etpf(10) ≈ f(10)
-@test isnan(@inferred(etpf(10.001)))
-
-@test etpf(2.5,1) == etpf(2.5) # for show method
-@test_throws BoundsError etpf(2.5,2)
-@test_throws BoundsError etpf(2.5,2,1)
-
-x = @inferred(etpf(dual(-2.5,1)))
-@test isa(x, Dual)
-
-etpl = extrapolate(itpg, Linear())
-k_lo = A[2] - A[1]
-x_lo = -3.2
-@test etpl(x_lo) ≈ A[1] + k_lo * (x_lo - 1)
-k_hi = A[end] - A[end-1]
-x_hi = xmax + 5.7
-@test etpl(x_hi) ≈ A[end] + k_hi * (x_hi - xmax)
-
-xmax, ymax = 8,8
-g(x, y) = (x^2 + 3x - 8) * (-2y^2 + y + 1)
-
-itp2g = interpolate(Float64[g(x,y) for x in 1:xmax, y in 1:ymax], (BSpline(Quadratic(Free())), BSpline(Linear())), OnGrid())
-etp2g = extrapolate(itp2g, (Linear(), Flat()))
-
-@test @inferred(etp2g(-0.5,4)) ≈ itp2g(1,4) - 1.5 * epsilon(etp2g(dual(1,1),4))
-@test @inferred(etp2g(5,100)) ≈ itp2g(5,ymax)
-
-etp2ud = extrapolate(itp2g, ((Linear(), Flat()), Flat()))
-@test @inferred(etp2ud(-0.5,4)) ≈ itp2g(1,4) - 1.5 * epsilon(etp2g(dual(1,1),4))
-@test @inferred(etp2ud(5, -4)) == etp2ud(5,1)
-@test @inferred(etp2ud(100, 4)) == etp2ud(8,4)
-@test @inferred(etp2ud(-.5, 100)) == itp2g(1,8) - 1.5 * epsilon(etp2g(dual(1,1),8))
-
-etp2ll = extrapolate(itp2g, Linear())
-@test @inferred(etp2ll(-0.5,100)) ≈ (itp2g(1,8) - 1.5 * epsilon(etp2ll(dual(1,1),8))) + (100 - 8) * epsilon(etp2ll(1,dual(8,1)))
-
-# Allow element types that don't support conversion to Int (#87):
-etp87g = extrapolate(interpolate([1.0im, 2.0im, 3.0im], BSpline(Linear()), OnGrid()), 0.0im)
-@test @inferred(etp87g(1)) == 1.0im
-@test @inferred(etp87g(1.5)) == 1.5im
-@test @inferred(etp87g(0.75)) == 0.0im
-@test @inferred(etp87g(3.25)) == 0.0im
-
-etp87c = extrapolate(interpolate([1.0im, 2.0im, 3.0im], BSpline(Linear()), OnCell()), 0.0im)
-@test @inferred(etp87c(1)) == 1.0im
-@test @inferred(etp87c(1.5)) == 1.5im
-@test @inferred(etp87c(0.75)) == 0.75im
-@test @inferred(etp87c(3.25)) == 3.25im
-@test @inferred(etp87g(0)) == 0.0im
-@test @inferred(etp87g(3.7)) == 0.0im
-
-# Make sure it works with Gridded too
-etp100g = extrapolate(interpolate(([10;20],),[100;110], Gridded(Linear())), Flat())
-@test @inferred(etp100g(5)) == 100
-@test @inferred(etp100g(15)) == 105
-@test @inferred(etp100g(25)) == 110
-# issue #178
-a = randn(10,10) + im*rand(10,10)
-etp = @inferred(extrapolate(interpolate((1:10, 1:10), a, Gridded(Linear())), 0.0))
-@test @inferred(etp(-1,0)) === 0.0+0.0im
-
-end
diff --git a/test/extrapolation/non1.jl b/test/extrapolation/non1.jl
index 79fe7968..ef7959f0 100644
--- a/test/extrapolation/non1.jl
+++ b/test/extrapolation/non1.jl
@@ -1,15 +1,15 @@
module ExtrapNon1
-using Compat.Test, Interpolations, OffsetArrays
+using Test, Interpolations, OffsetArrays
f(x) = sin((x-3)*2pi/9 - 1)
xinds = -3:6
A = OffsetArray(Float64[f(x) for x in xinds], xinds)
-itpg = interpolate(A, BSpline(Linear()), OnGrid())
+itpg = interpolate(A, BSpline(Linear()))
schemes = (
Flat,
- Linear,
+ Line,
Reflect,
Periodic
)
@@ -22,7 +22,7 @@ end
g(y) = (y/100)^3
yinds = 2:5
A = OffsetArray(Float64[f(x)*g(y) for x in xinds, y in yinds], xinds, yinds)
-itp2 = interpolate(A, BSpline(Linear()), OnGrid())
+itp2 = interpolate(A, BSpline(Linear()))
for (etp2,E) in map(E -> (extrapolate(itp2, E()), E), schemes)
@test parent(etp2) === itp2
diff --git a/test/extrapolation/runtests.jl b/test/extrapolation/runtests.jl
index 6526bf0d..e103c2f6 100644
--- a/test/extrapolation/runtests.jl
+++ b/test/extrapolation/runtests.jl
@@ -1,106 +1,96 @@
-module ExtrapTests
-
-using Compat.Test, DualNumbers
+using DualNumbers
using Interpolations
-
-
-f(x) = sin((x-3)*2pi/9 - 1)
-xmax = 10
-A = Float64[f(x) for x in 1:xmax]
-
-itpg = interpolate(A, BSpline(Linear()), OnGrid())
-
-etpg = extrapolate(itpg, Flat())
-@test typeof(etpg) <: AbstractExtrapolation
-
-@test etpg[-3] == etpg[-4.5] == etpg[0.9] == etpg[1.0] == A[1]
-@test etpg[10.1] == etpg[11] == etpg[148.298452] == A[end]
-
-etpf = @inferred(extrapolate(itpg, NaN))
-@test typeof(etpf) <: Interpolations.FilledExtrapolation
-@test parent(etpf) === itpg
-
-@test @inferred(size(etpf)) == (xmax,)
-@test isnan(@inferred(getindex(etpf, -2.5)))
-@test isnan(etpf[0.999])
-@test @inferred(getindex(etpf,1)) ≈ f(1)
-@test etpf[10] ≈ f(10)
-@test isnan(@inferred(getindex(etpf,10.001)))
-
-@test etpf[2.5,1] == etpf[2.5] # for show method
-@test_throws BoundsError etpf[2.5,2]
-@test_throws BoundsError etpf[2.5,2,1]
-
-x = @inferred(getindex(etpf, dual(-2.5,1)))
-@test isa(x, Dual)
-
-etpl = extrapolate(itpg, Linear())
-k_lo = A[2] - A[1]
-x_lo = -3.2
-@test etpl[x_lo] ≈ A[1] + k_lo * (x_lo - 1)
-k_hi = A[end] - A[end-1]
-x_hi = xmax + 5.7
-@test etpl[x_hi] ≈ A[end] + k_hi * (x_hi - xmax)
-
-
-xmax, ymax = 8,8
-g(x, y) = (x^2 + 3x - 8) * (-2y^2 + y + 1)
-
-itp2g = interpolate(Float64[g(x,y) for x in 1:xmax, y in 1:ymax], (BSpline(Quadratic(Free())), BSpline(Linear())), OnGrid())
-etp2g = extrapolate(itp2g, (Linear(), Flat()))
-
-@test @inferred(getindex(etp2g,-0.5,4)) ≈ itp2g[1,4] - 1.5 * epsilon(etp2g[dual(1,1),4])
-@test @inferred(getindex(etp2g,5,100)) ≈ itp2g[5,ymax]
-
-etp2ud = extrapolate(itp2g, ((Linear(), Flat()), Flat()))
-@test @inferred(getindex(etp2ud,-0.5,4)) ≈ itp2g[1,4] - 1.5 * epsilon(etp2g[dual(1,1),4])
-@test @inferred(getindex(etp2ud, 5, -4)) == etp2ud[5,1]
-@test @inferred(getindex(etp2ud, 100, 4)) == etp2ud[8,4]
-@test @inferred(getindex(etp2ud, -.5, 100)) == itp2g[1,8] - 1.5 * epsilon(etp2g[dual(1,1),8])
-
-etp2ll = extrapolate(itp2g, Linear())
-@test @inferred(getindex(etp2ll,-0.5,100)) ≈ (itp2g[1,8] - 1.5 * epsilon(etp2ll[dual(1,1),8])) + (100 - 8) * epsilon(etp2ll[1,dual(8,1)])
-
-# Allow element types that don't support conversion to Int (#87):
-etp87g = extrapolate(interpolate([1.0im, 2.0im, 3.0im], BSpline(Linear()), OnGrid()), 0.0im)
-@test @inferred(getindex(etp87g, 1)) == 1.0im
-@test @inferred(getindex(etp87g, 1.5)) == 1.5im
-@test @inferred(getindex(etp87g, 0.75)) == 0.0im
-@test @inferred(getindex(etp87g, 3.25)) == 0.0im
-
-etp87c = extrapolate(interpolate([1.0im, 2.0im, 3.0im], BSpline(Linear()), OnCell()), 0.0im)
-@test @inferred(getindex(etp87c, 1)) == 1.0im
-@test @inferred(getindex(etp87c, 1.5)) == 1.5im
-@test @inferred(getindex(etp87c, 0.75)) == 0.75im
-@test @inferred(getindex(etp87c, 3.25)) == 3.25im
-@test @inferred(getindex(etp87g, 0)) == 0.0im
-@test @inferred(getindex(etp87g, 3.7)) == 0.0im
-
-# Make sure it works with Gridded too
-etp100g = extrapolate(interpolate(([10;20],),[100;110], Gridded(Linear())), Flat())
-@test @inferred(getindex(etp100g, 5)) == 100
-@test @inferred(getindex(etp100g, 15)) == 105
-@test @inferred(getindex(etp100g, 25)) == 110
-# issue #178
-a = randn(10,10) + im*rand(10,10)
-etp = @inferred(extrapolate(interpolate((1:10, 1:10), a, Gridded(Linear())), 0.0))
-@test @inferred(etp[-1,0]) === 0.0+0.0im
-
-# check all extrapolations work with vectorized indexing
-for E in [0,Flat(),Linear(),Periodic(),Reflect()]
- @test (@inferred(getindex(extrapolate(interpolate([0,0],BSpline(Linear()),OnGrid()),E),[1.2, 1.8, 3.1]))) == [0,0,0]
+using Test
+
+@testset "Extrapolation" begin
+
+ f(x) = sin((x-3)*2pi/9 - 1)
+ xmax = 10
+ A = Float64[f(x) for x in 1:xmax]
+
+ itpg = interpolate(A, BSpline(Linear()))
+
+ etpg = extrapolate(itpg, Flat())
+ @test typeof(etpg) <: AbstractExtrapolation
+
+ @test etpg(-3) == etpg(-4.5) == etpg(0.9) == etpg(1.0) == A[1]
+ @test etpg(10.1) == etpg(11) == etpg(148.298452) == A[end]
+
+ etpf = @inferred(extrapolate(itpg, NaN))
+ @test typeof(etpf) <: Interpolations.FilledExtrapolation
+ @test parent(etpf) === itpg
+
+ @test @inferred(size(etpf)) == (xmax,)
+ @test isnan(@inferred(etpf(-2.5)))
+ @test isnan(etpf(0.999))
+ @test @inferred(etpf(1)) ≈ f(1)
+ @test etpf(10) ≈ f(10)
+ @test isnan(@inferred(etpf(10.001)))
+
+ @test etpf(2.5,1) == etpf(2.5) # for show method
+ @test_throws BoundsError etpf(2.5,2)
+ @test_throws BoundsError etpf(2.5,2,1)
+
+ x = @inferred(etpf(dual(-2.5,1)))
+ @test isa(x, Dual)
+
+ etpl = extrapolate(itpg, Line())
+ k_lo = A[2] - A[1]
+ x_lo = -3.2
+ @test etpl(x_lo) ≈ A[1] + k_lo * (x_lo - 1)
+ k_hi = A[end] - A[end-1]
+ x_hi = xmax + 5.7
+ @test etpl(x_hi) ≈ A[end] + k_hi * (x_hi - xmax)
+
+
+ xmax, ymax = 8,8
+ g(x, y) = (x^2 + 3x - 8) * (-2y^2 + y + 1)
+
+ itp2g = interpolate(Float64[g(x,y) for x in 1:xmax, y in 1:ymax], (BSpline(Quadratic(Free(OnGrid()))), BSpline(Linear())))
+ etp2g = extrapolate(itp2g, (Line(), Flat()))
+
+ @test @inferred(etp2g(-0.5,4)) ≈ itp2g(1,4) - 1.5 * epsilon(etp2g(dual(1,1),4))
+ @test @inferred(etp2g(5,100)) ≈ itp2g(5,ymax)
+
+ etp2ud = extrapolate(itp2g, ((Line(), Flat()), Flat()))
+ @test @inferred(etp2ud(-0.5,4)) ≈ itp2g(1,4) - 1.5 * epsilon(etp2g(dual(1,1),4))
+ @test @inferred(etp2ud(5, -4)) == etp2ud(5,1)
+ @test @inferred(etp2ud(100, 4)) == etp2ud(8,4)
+ @test @inferred(etp2ud(-.5, 100)) == itp2g(1,8) - 1.5 * epsilon(etp2g(dual(1,1),8))
+
+ etp2ll = extrapolate(itp2g, Line())
+ @test @inferred(etp2ll(-0.5,100)) ≈ (itp2g(1,8) - 1.5 * epsilon(etp2ll(dual(1,1),8))) + (100 - 8) * epsilon(etp2ll(1,dual(8,1)))
+
+ # Allow element types that don't support conversion to Int (#87):
+ etp87g = extrapolate(interpolate([1.0im, 2.0im, 3.0im], BSpline(Linear())), 0.0im)
+ @test @inferred(etp87g(1)) == 1.0im
+ @test @inferred(etp87g(1.5)) == 1.5im
+ @test @inferred(etp87g(0.75)) == 0.0im
+ @test @inferred(etp87g(3.25)) == 0.0im
+
+ # Make sure it works with Gridded too
+ etp100g = extrapolate(interpolate(([10;20],),[100;110], Gridded(Linear())), Flat())
+ @test @inferred(etp100g(5)) == 100
+ @test @inferred(etp100g(15)) == 105
+ @test @inferred(etp100g(25)) == 110
+ # issue #178
+ a = randn(10,10) + im*rand(10,10)
+ etp = @inferred(extrapolate(interpolate((1:10, 1:10), a, Gridded(Linear())), 0.0))
+ @test @inferred(etp(-1,0)) === 0.0+0.0im
+
+ # check all extrapolations work with vectorized indexing
+ for E in [0,Flat(),Line(),Periodic(),Reflect()]
+ @test (@inferred(extrapolate(interpolate([0,0],BSpline(Linear())),E))([1.2, 1.8, 3.1])) == [0,0,0]
+ end
+
+ # Issue #156
+ F = *(collect(1.0:10.0), collect(1:4)')
+ itp = interpolate(F, (BSpline(Linear()), NoInterp()));
+ itps = scale(itp, 1:10, 1:4)
+ itpe = extrapolate(itps, (Line(), Throw()))
+ @test itpe(10.1, 1) ≈ 10.1
+ @test_throws BoundsError itpe(9.9, 0)
+
+ include("type-stability.jl")
+ include("non1.jl")
end
-
-# Issue #156
-F = *(collect(1.0:10.0), collect(1:4)')
-itp = interpolate(F, (BSpline(Linear()), NoInterp()), OnGrid());
-itps = scale(itp, 1:10, 1:4)
-itpe = extrapolate(itps, (Linear(), Interpolations.Throw()))
-@test itpe[10.1, 1] ≈ 10.1
-@test_throws BoundsError itpe[9.9, 0]
-
-end
-
-include("type-stability.jl")
-include("non1.jl")
-include("function-call-syntax.jl")
diff --git a/test/extrapolation/type-stability.jl b/test/extrapolation/type-stability.jl
index 3345dd99..57b3d59d 100644
--- a/test/extrapolation/type-stability.jl
+++ b/test/extrapolation/type-stability.jl
@@ -1,36 +1,36 @@
module ExtrapTypeStability
-using Compat.Test, Interpolations, DualNumbers
+using Test, Interpolations, DualNumbers
# Test type-stability of 1-dimensional extrapolation
f(x) = sin((x-3)*2pi/9 - 1)
xmax = 10
A = Float64[f(x) for x in 1:xmax]
-itpg = interpolate(A, BSpline(Linear()), OnGrid())
+itpg = interpolate(A, BSpline(Linear()))
schemes = (
Flat,
- Linear,
+ Line,
Reflect,
Periodic
)
for etp in map(E -> @inferred(extrapolate(itpg, E())), schemes),
- x in [
+ x in (
# In-bounds evaluation
3.4, 3, dual(3.1),
# Out-of-bounds evaluation
-3.4, -3, dual(-3,1),
13.4, 13, dual(13,1)
- ]
- @inferred(getindex(etp, x))
+ )
+ @inferred(etp(x))
end
# Test type-stability of 2-dimensional extrapolation with homogeneous scheme
g(y) = (y/100)^3
ymax = 4
A = Float64[f(x)*g(y) for x in 1:xmax, y in 1:ymax]
-itp2 = interpolate(A, BSpline(Linear()), OnGrid())
+itp2 = interpolate(A, BSpline(Linear()))
for (etp2,E) in map(E -> (extrapolate(itp2, E()), E), schemes),
x in (
@@ -47,16 +47,16 @@ for (etp2,E) in map(E -> (extrapolate(itp2, E()), E), schemes),
-2.1, -2, dual(-2.3, 1),
12.1, 12, dual(12.1, 1)
)
- @inferred(getindex(etp2, x, y))
+ @inferred(etp2(x, y))
end
A = [1 2; 3 4]
Af = Float64.(A)
for B in (A, Af)
- itpg2 = interpolate(B, BSpline(Linear()), OnGrid())
+ itpg2 = interpolate(B, BSpline(Linear()))
etp = extrapolate(itpg2, NaN)
- @test typeof(@inferred(getindex(etp, dual(1.5,1), dual(1.5,1)))) ==
- typeof(@inferred(getindex(etp, dual(6.5,1), dual(3.5,1))))
+ @test typeof(@inferred(etp(dual(1.5,1), dual(1.5,1)))) ==
+ typeof(@inferred(etp(dual(6.5,1), dual(3.5,1))))
end
end
diff --git a/test/gradient.jl b/test/gradient.jl
index a22d2b8c..e91ec703 100644
--- a/test/gradient.jl
+++ b/test/gradient.jl
@@ -1,140 +1,166 @@
-module GradientTests
-
-using Compat, Compat.Test, Interpolations, DualNumbers, Compat.LinearAlgebra
-
-nx = 10
-f1(x) = sin((x-3)*2pi/(nx-1) - 1)
-g1(x) = 2pi/(nx-1) * cos((x-3)*2pi/(nx-1) - 1)
+using Test, Interpolations, DualNumbers, LinearAlgebra
+
+@testset "Gradients" begin
+ nx = 10
+ f1(x) = sin((x-3)*2pi/(nx-1) - 1)
+ g1gt(x) = 2pi/(nx-1) * cos((x-3)*2pi/(nx-1) - 1)
+ A1 = Float64[f1(x) for x in 1:nx]
+ g1 = Array{Float64}(undef, 1)
+ A2 = rand(Float64, nx, nx) * 100
+ g2 = Array{Float64}(undef, 2)
+
+ for (A, g) in ((A1, g1), (A2, g2))
+ # Gradient of Constant should always be 0
+ itp = interpolate(A, BSpline(Constant()))
+ for x in InterpolationTestUtils.thirds(axes(A))
+ @test all(iszero, @inferred(Interpolations.gradient(itp, x...)))
+ @test all(iszero, @inferred(Interpolations.gradient!(g, itp, x...)))
+ end
+
+ itp = interpolate(A, BSpline(Linear()))
+ check_gradient(itp, g)
+ i = first(eachindex(itp))
+ @test Interpolations.gradient(itp, i) == Interpolations.gradient(itp, Tuple(i)...)
+
+ for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
+ itp = interpolate(A, BSpline(Quadratic(BC(GT()))))
+ check_gradient(itp, g)
+ i = first(eachindex(itp))
+ @test Interpolations.gradient(itp, i) == Interpolations.gradient(itp, Tuple(i)...)
+ end
+
+ for BC in (Line, Flat, Free, Periodic), GT in (OnGrid, OnCell)
+ itp = interpolate(A, BSpline(Cubic(BC(GT()))))
+ check_gradient(itp, g)
+ I = first(eachindex(itp))
+ @test Interpolations.gradient(itp, I) == Interpolations.gradient(itp, Tuple(I)...)
+ end
+ end
-# Gradient of Constant should always be 0
-itp1 = interpolate(Float64[f1(x) for x in 1:nx-1],
- BSpline(Constant()), OnGrid())
+ # Since Linear is OnGrid in the domain, check the gradients between grid points
+ itp1 = interpolate(Float64[f1(x) for x in 1:nx],
+ BSpline(Linear()))
+ itp2 = interpolate((1:nx-1,), Float64[f1(x) for x in 1:nx-1],
+ Gridded(Linear()))
+ for itp in (itp1, itp2)
+ for x in 2.5:nx-1.5
+ @test ≈(g1gt(x),(Interpolations.gradient(itp,x))[1],atol=abs(0.1 * g1gt(x)))
+ @test ≈(g1gt(x),(Interpolations.gradient!(g1,itp,x))[1],atol=abs(0.1 * g1gt(x)))
+ @test ≈(g1gt(x),g1[1],atol=abs(0.1 * g1gt(x)))
+ end
+
+ for i = 1:10
+ x = rand()*(nx-2)+1.5
+ checkbounds(Bool, itp, x) || continue
+ gtmp = Interpolations.gradient(itp, x)[1]
+ xd = dual(x, 1)
+ @test epsilon(itp(xd)) ≈ gtmp
+ end
+ end
-g = Array{Float64}(undef, 1)
+ # test gridded on a non-uniform grid
+ knots = (1.0:0.3:nx-1,)
+ itp_grid = interpolate(knots, Float64[f1(x) for x in knots[1]],
+ Gridded(Linear()))
-for x in 1:nx
- @test gradient(itp1, x)[1] == 0
- @test gradient!(g, itp1, x)[1] == 0
- @test g[1] == 0
-end
+ for x in 1.5:0.5:nx-1.5
+ @test ≈(g1gt(x),(Interpolations.gradient(itp_grid,x))[1],atol=abs(0.5 * g1gt(x)))
+ @test ≈(g1gt(x),(Interpolations.gradient!(g1,itp_grid,x))[1],atol=abs(0.5 * g1gt(x)))
+ @test ≈(g1gt(x),g1[1],atol=abs(0.5 * g1gt(x)))
+ end
-# Since Linear is OnGrid in the domain, check the gradients between grid points
-itp1 = interpolate(Float64[f1(x) for x in 1:nx-1],
- BSpline(Linear()), OnGrid())
-itp2 = interpolate((1:nx-1,), Float64[f1(x) for x in 1:nx-1],
- Gridded(Linear()))
-for itp in (itp1, itp2)
- for x in 2.5:nx-1.5
- @test ≈(g1(x),(gradient(itp,x))[1],atol=abs(0.1 * g1(x)))
- @test ≈(g1(x),(gradient!(g,itp,x))[1],atol=abs(0.1 * g1(x)))
- @test ≈(g1(x),g[1],atol=abs(0.1 * g1(x)))
+ # Since Quadratic is OnCell in the domain, check gradients at grid points
+ itp1 = interpolate(Float64[f1(x) for x in 1:nx-1],
+ BSpline(Quadratic(Periodic(OnCell()))))
+ for x in 2:nx-1
+ @test ≈(g1gt(x),(Interpolations.gradient(itp1,x))[1],atol=abs(0.05 * g1gt(x)))
+ @test ≈(g1gt(x),(Interpolations.gradient!(g1,itp1,x))[1],atol=abs(0.05 * g1gt(x)))
+ @test ≈(g1gt(x),g1[1],atol=abs(0.1 * g1gt(x)))
end
for i = 1:10
x = rand()*(nx-2)+1.5
- gtmp = gradient(itp, x)[1]
+ gtmp = Interpolations.gradient(itp1, x)[1]
xd = dual(x, 1)
- @test epsilon(itp[xd]) ≈ gtmp
+ @test epsilon(itp1(xd)) ≈ gtmp
end
-end
-
-# test gridded on a non-uniform grid
-knots = (1.0:0.3:nx-1,)
-itp_grid = interpolate(knots, Float64[f1(x) for x in knots[1]],
- Gridded(Linear()))
-
-for x in 1.5:0.5:nx-1.5
- @test ≈(g1(x),(gradient(itp_grid,x))[1],atol=abs(0.5 * g1(x)))
- @test ≈(g1(x),(gradient!(g,itp_grid,x))[1],atol=abs(0.5 * g1(x)))
- @test ≈(g1(x),g[1],atol=abs(0.5 * g1(x)))
-end
-# Since Quadratic is OnCell in the domain, check gradients at grid points
-itp1 = interpolate(Float64[f1(x) for x in 1:nx-1],
- BSpline(Quadratic(Periodic())), OnCell())
-for x in 2:nx-1
- @test ≈(g1(x),(gradient(itp1,x))[1],atol=abs(0.05 * g1(x)))
- @test ≈(g1(x),(gradient!(g,itp1,x))[1],atol=abs(0.05 * g1(x)))
- @test ≈(g1(x),g[1],atol=abs(0.1 * g1(x)))
-end
-
-for i = 1:10
- x = rand()*(nx-2)+1.5
- gtmp = gradient(itp1, x)[1]
- xd = dual(x, 1)
- @test epsilon(itp1[xd]) ≈ gtmp
-end
-
-# For a quadratic function and quadratic interpolation, we expect an
-# "exact" answer
-# 1d
-c = 2.3
-a = 8.1
-o = 1.6
-qfunc = x -> a*(x .- c).^2 .+ o
-dqfunc = x -> 2*a*(x .- c)
-xg = Float64[1:5;]
-y = qfunc(xg)
-
-iq = interpolate(y, BSpline(Quadratic(Free())), OnCell())
-x = 1.8
-@test iq[x] ≈ qfunc(x)
-@test (gradient(iq,x))[1] ≈ dqfunc(x)
-
-# 2d (biquadratic)
-p = [(x-1.75)^2 for x = 1:7]
-A = p*p'
-iq = interpolate(A, BSpline(Quadratic(Free())), OnCell())
-@test iq[4,4] ≈ (4 - 1.75) ^ 4
-@test iq[4,3] ≈ (4 - 1.75) ^ 2 * (3 - 1.75) ^ 2
-g = gradient(iq, 4, 3)
-@test g[1] ≈ 2 * (4 - 1.75) * (3 - 1.75) ^ 2
-@test g[2] ≈ 2 * (4 - 1.75) ^ 2 * (3 - 1.75)
-
-iq = interpolate!(copy(A), BSpline(Quadratic(InPlace())), OnCell())
-@test iq[4,4] ≈ (4 - 1.75) ^ 4
-@test iq[4,3] ≈ (4 - 1.75) ^ 2 * (3 - 1.75) ^ 2
-g = gradient(iq, 4, 3)
-@test ≈(g[1],2 * (4 - 1.75) * (3 - 1.75) ^ 2,atol=0.03)
-@test ≈(g[2],2 * (4 - 1.75) ^ 2 * (3 - 1.75),atol=0.2)
-
-# InPlaceQ is exact for an underlying quadratic
-iq = interpolate!(copy(A), BSpline(Quadratic(InPlaceQ())), OnCell())
-@test iq[4,4] ≈ (4 - 1.75) ^ 4
-@test iq[4,3] ≈ (4 - 1.75) ^ 2 * (3 - 1.75) ^ 2
-g = gradient(iq, 4, 3)
-@test g[1] ≈ 2 * (4 - 1.75) * (3 - 1.75) ^ 2
-@test g[2] ≈ 2 * (4 - 1.75) ^ 2 * (3 - 1.75)
-
-A2 = rand(Float64, nx, nx) * 100
-for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
- itp_a = interpolate(A2, (BSpline(Linear()), BSpline(Quadratic(BC()))), GT())
- itp_b = interpolate(A2, (BSpline(Quadratic(BC())), BSpline(Linear())), GT())
- itp_c = interpolate(A2, (NoInterp(), BSpline(Quadratic(BC()))), GT())
- itp_d = interpolate(A2, (BSpline(Quadratic(BC())), NoInterp()), GT())
-
- for i = 1:10
- global x, y
- x = rand()*(nx-2)+1.5
- y = rand()*(nx-2)+1.5
- xd = dual(x, 1)
- yd = dual(y, 1)
- gtmp = gradient(itp_a, x, y)
- @test length(gtmp) == 2
- @test epsilon(itp_a[xd,y]) ≈ gtmp[1]
- @test epsilon(itp_a[x,yd]) ≈ gtmp[2]
- gtmp = gradient(itp_b, x, y)
- @test length(gtmp) == 2
- @test epsilon(itp_b[xd,y]) ≈ gtmp[1]
- @test epsilon(itp_b[x,yd]) ≈ gtmp[2]
- ix, iy = round(Int, x), round(Int, y)
- gtmp = gradient(itp_c, ix, y)
- @test length(gtmp) == 1
- @test epsilon(itp_c[ix,yd]) ≈ gtmp[1]
- gtmp = gradient(itp_d, x, iy)
- @test length(gtmp) == 1
- @test epsilon(itp_d[xd,iy]) ≈ gtmp[1]
+ # For a quadratic function and quadratic interpolation, we expect an
+ # "exact" answer
+ # 1d
+ c = 2.3
+ a = 8.1
+ o = 1.6
+ qfunc = x -> a*(x .- c).^2 .+ o
+ dqfunc = x -> 2*a*(x .- c)
+ xg = Float64[1:5;]
+ y = qfunc(xg)
+
+ iq = interpolate(y, BSpline(Quadratic(Free(OnCell()))))
+ x = 1.8
+ @test iq(x) ≈ qfunc(x)
+ @test (Interpolations.gradient(iq,x))[1] ≈ dqfunc(x)
+
+ # 2d (biquadratic)
+ p = [(x-1.75)^2 for x = 1:7]
+ A = p*p'
+ iq = interpolate(A, BSpline(Quadratic(Free(OnCell()))))
+ @test iq[4,4] ≈ (4 - 1.75) ^ 4
+ @test iq[4,3] ≈ (4 - 1.75) ^ 2 * (3 - 1.75) ^ 2
+ g = Interpolations.gradient(iq, 4, 3)
+ @test g[1] ≈ 2 * (4 - 1.75) * (3 - 1.75) ^ 2
+ @test g[2] ≈ 2 * (4 - 1.75) ^ 2 * (3 - 1.75)
+
+ iq = interpolate!(copy(A), BSpline(Quadratic(InPlace(OnCell()))))
+ @test iq[4,4] ≈ (4 - 1.75) ^ 4
+ @test iq[4,3] ≈ (4 - 1.75) ^ 2 * (3 - 1.75) ^ 2
+ g = Interpolations.gradient(iq, 4, 3)
+ @test ≈(g[1],2 * (4 - 1.75) * (3 - 1.75) ^ 2,atol=0.03)
+ @test ≈(g[2],2 * (4 - 1.75) ^ 2 * (3 - 1.75),atol=0.2)
+
+ # InPlaceQ is exact for an underlying quadratic
+ iq = interpolate!(copy(A), BSpline(Quadratic(InPlaceQ(OnCell()))))
+ @test iq[4,4] ≈ (4 - 1.75) ^ 4
+ @test iq[4,3] ≈ (4 - 1.75) ^ 2 * (3 - 1.75) ^ 2
+ g = Interpolations.gradient(iq, 4, 3)
+ @test g[1] ≈ 2 * (4 - 1.75) * (3 - 1.75) ^ 2
+ @test g[2] ≈ 2 * (4 - 1.75) ^ 2 * (3 - 1.75)
+
+ A2 = rand(Float64, nx, nx) * 100
+ gni = [1.0]
+ for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
+ itp_a = interpolate(A2, (BSpline(Linear()), BSpline(Quadratic(BC(GT())))))
+ itp_b = interpolate(A2, (BSpline(Quadratic(BC(GT()))), BSpline(Linear())))
+ itp_c = interpolate(A2, (NoInterp(), BSpline(Quadratic(BC(GT())))))
+ itp_d = interpolate(A2, (BSpline(Quadratic(BC(GT()))), NoInterp()))
+
+ for i = 1:10
+ x = rand()*(nx-2)+1.5
+ y = rand()*(nx-2)+1.5
+ xd = dual(x, 1)
+ yd = dual(y, 1)
+ gtmp = Interpolations.gradient(itp_a, x, y)
+ @test length(gtmp) == 2
+ @test epsilon(itp_a(xd,y)) ≈ gtmp[1]
+ @test epsilon(itp_a(x,yd)) ≈ gtmp[2]
+ gtmp = Interpolations.gradient(itp_b, x, y)
+ @test length(gtmp) == 2
+ @test epsilon(itp_b(xd,y)) ≈ gtmp[1]
+ @test epsilon(itp_b(x,yd)) ≈ gtmp[2]
+ ix, iy = round(Int, x), round(Int, y)
+ gtmp = Interpolations.gradient(itp_c, ix, y)
+ @test length(gtmp) == 1
+ @test epsilon(itp_c(ix,yd)) ≈ gtmp[1]
+ gni[1] = NaN
+ Interpolations.gradient!(gni, itp_c, ix, y)
+ @test gni[1] ≈ gtmp[1]
+ gtmp = Interpolations.gradient(itp_d, x, iy)
+ @test length(gtmp) == 1
+ @test epsilon(itp_d(xd,iy)) ≈ gtmp[1]
+ gni[1] = NaN
+ Interpolations.gradient!(gni, itp_d, x, iy)
+ @test gni[1] ≈ gtmp[1]
+ end
end
-end
end
diff --git a/test/grid.jl b/test/grid.jl
deleted file mode 100644
index 22af0969..00000000
--- a/test/grid.jl
+++ /dev/null
@@ -1,54 +0,0 @@
-module GridTests
-
-using Interpolations, Compat.Test
-
-# On-grid values
-A = randn(4,10)
-const EPS = sqrt(eps())
-for it in (Constant(OnCell()), Linear(OnGrid()), Quadratic(Free(),OnCell()))
- for eb in (ExtrapNaN(), ExtrapError())
- itp = Interpolation(A, it, eb)
- for i = 1:size(A,1)
- for j = 1:size(A,2)
- @test ≈(itp[i,j],A[i,j],atol=EPS)
- end
- end
- # v = itp[1:size(A,1), 1:size(A,2)]
- # @assert all(abs(v - A) .< EPS)
- end
-end
-
-A = randn(4,5,4)
-for it in (Constant(OnCell()), Linear(OnGrid()), Quadratic(Free(),OnCell()))
- for eb in (ExtrapNaN(), ExtrapError())
- itp = Interpolation(A, it, eb)
- for k = 1:size(A,3), j = 1:size(A,2), i = 1:size(A,1)
- @test ≈(itp[i,j,k],A[i,j,k],atol=EPS)
- end
- # v = itp[1:size(A,1), 1:size(A,2), 1:size(A,3)]
- # @assert all(abs(v - A) .< EPS)
- end
-end
-A = randn(4,5,4,3)
-for it in (Constant(OnCell()), Linear(OnGrid()), Quadratic(Free(),OnCell()))
- for eb in (ExtrapNaN(), ExtrapError())
- itp = Interpolation(A, it, eb)
- for i = 1:size(A,1), j = 1:size(A,2), k = 1:size(A,3), l = 1:size(A,4)
- @test ≈(itp[i,j,k,l],A[i,j,k,l],atol=EPS)
- end
- # v = itp[1:size(A,1), 1:size(A,2), 1:size(A,3), 1:size(A,4)]
- # @assert all(abs(v - A) .< EPS)
- end
-end
-
-A = float([1:4])
-for it in (Constant(OnCell()), Linear(OnGrid()), Quadratic(Free(),OnCell()))
- itp = Interpolation(A, it, ExtrapError())
- @test_throws BoundsError itp[-0.8]
-end
-for it in (Constant(OnCell()), Linear(OnGrid()), Quadratic(Free(),OnCell()))
- itp = Interpolation(A, it, ExtrapNaN())
- @test isnan(itp[-0.8])
-end
-
-end
diff --git a/test/gridded/function-call-syntax.jl b/test/gridded/function-call-syntax.jl
deleted file mode 100644
index 5cb36b1f..00000000
--- a/test/gridded/function-call-syntax.jl
+++ /dev/null
@@ -1,75 +0,0 @@
-module GriddedFunctionCallSyntax
-
-using Interpolations, Compat.Test
-using Compat: range
-
-for D in (Constant, Linear), G in (OnCell, OnGrid)
- ## 1D
- a = rand(5)
- knots = (collect(range(1, stop=length(a), length=length(a))),)
- itp = @inferred(interpolate(knots, a, Gridded(D())))
- @inferred(getindex(itp, 2))
- @inferred(getindex(itp, CartesianIndex((2,))))
- for i = 2:length(a)-1
- @test itp(i) ≈ a[i]
- @test itp(CartesianIndex((i,))) ≈ a[i]
- end
- @inferred(getindex(itp, knots...))
- @test itp[knots...] ≈ a
- # compare scalar indexing and vector indexing
- x = knots[1] .+ 0.1
- v = itp(x)
- for i = 1:length(x)
- @test v[i] ≈ itp(x[i])
- end
- # check the fallback vector indexing
- x = [2.3,2.2] # non-increasing order
- v = itp[x]
- for i = 1:length(x)
- @test v[i] ≈ itp(x[i])
- end
- # compare against BSpline
- itpb = @inferred(interpolate(a, BSpline(D()), G()))
- for x in range(1.1, stop=4.9, length=101)
- @test itp(x) ≈ itpb(x)
- end
-
- ## 2D
- A = rand(6,5)
- knots = (collect(range(1, stop=size(A,1), length=size(A,1))),collect(range(1, stop=size(A,2), length=size(A,2))))
- itp = @inferred(interpolate(knots, A, Gridded(D())))
- @test parent(itp) === A
- @inferred(getindex(itp, 2, 2))
- @inferred(getindex(itp, CartesianIndex((2,2))))
- for j = 2:size(A,2)-1, i = 2:size(A,1)-1
- @test itp(i,j) ≈ A[i,j]
- @test itp(CartesianIndex((i,j))) ≈ A[i,j]
- end
- @test itp[knots...] ≈ A
- @inferred(getindex(itp, knots...))
- # compare scalar indexing and vector indexing
- x, y = knots[1] .+ 0.1, knots[2] .+ 0.6
- v = itp(x,y)
- for j = 1:length(y), i = 1:length(x)
- @test v[i,j] ≈ itp(x[i],y[j])
- end
- # check the fallback vector indexing
- x = [2.3,2.2] # non-increasing order
- y = [3.5,2.8]
- v = itp[x,y]
- for j = 1:length(y), i = 1:length(x)
- @test v[i,j] ≈ itp(x[i],y[j])
- end
- # compare against BSpline
- itpb = @inferred(interpolate(A, BSpline(D()), G()))
- for y in range(1.1, stop=5.9, length=101), x in range(1.1, stop=4.9, length=101)
- @test itp(x,y) ≈ itpb(x,y)
- end
-
- A = rand(8,20)
- knots = ([x^2 for x = 1:8], [0.2y for y = 1:20])
- itp = interpolate(knots, A, Gridded(D()))
- @test itp(4,1.2) ≈ A[2,6]
-end
-
-end
diff --git a/test/gridded/gridded.jl b/test/gridded/gridded.jl
index 8d1d4c05..19c74902 100644
--- a/test/gridded/gridded.jl
+++ b/test/gridded/gridded.jl
@@ -1,75 +1,79 @@
-module LinearTests
+using Interpolations, Test
-using Interpolations, Compat.Test
-using Compat: range
+@testset "LinearTests" begin
+ front(r::AbstractUnitRange) = first(r):last(r)-1
+ front(r::AbstractRange) = range(first(r), step=step(r), length=length(r)-1)
-for D in (Constant, Linear), G in (OnCell, OnGrid)
- ## 1D
- a = rand(5)
- knots = (collect(range(1, stop=length(a), length=length(a))),)
- itp = @inferred(interpolate(knots, a, Gridded(D())))
- @inferred(getindex(itp, 2))
- @inferred(getindex(itp, CartesianIndex((2,))))
- for i = 2:length(a)-1
- @test itp[i] ≈ a[i]
- @test itp[CartesianIndex((i,))] ≈ a[i]
- end
- @inferred(getindex(itp, knots...))
- @test itp[knots...] ≈ a
- # compare scalar indexing and vector indexing
- x = knots[1] .+ 0.1
- v = itp[x]
- for i = 1:length(x)
- @test v[i] ≈ itp[x[i]]
- end
- # check the fallback vector indexing
- x = [2.3,2.2] # non-increasing order
- v = itp[x]
- for i = 1:length(x)
- @test v[i] ≈ itp[x[i]]
- end
- # compare against BSpline
- itpb = @inferred(interpolate(a, BSpline(D()), G()))
- for x in range(1.1, stop=4.9, length=101)
- @test itp[x] ≈ itpb[x]
- end
+ for D in (Constant, Linear)
+ ## 1D
+ a = rand(5)
+ knots = (range(1, stop=length(a), length=length(a)),)
+ itp = @inferred(interpolate(knots, a, Gridded(D())))
+ @inferred(itp(2))
+ @inferred(itp(CartesianIndex(2)))
+ for i = 1:length(a)
+ @test itp(i) ≈ a[i]
+ @test itp(CartesianIndex(i)) ≈ a[i]
+ end
+ @inferred(itp(knots...))
+ @test itp(knots...) ≈ a
+ # compare scalar indexing and vector indexing
+ x = front(knots[1] .+ 0.1)
+ v = itp(x)
+ for i = 1:length(x)
+ @test v[i] ≈ itp(x[i])
+ end
+ x = [2.3,2.2] # non-increasing order
+ v = itp(x)
+ for i = 1:length(x)
+ @test v[i] ≈ itp(x[i])
+ end
+ # compare against BSpline
+ itpb = @inferred(interpolate(a, BSpline(D())))
+ for x in range(1.1, stop=4.9, length=101)
+ @test itp(x) ≈ itpb(x)
+ end
- ## 2D
- A = rand(6,5)
- knots = (collect(range(1, stop=size(A,1), length=size(A,1))),collect(range(1, stop=size(A,2), length=size(A,2))))
- itp = @inferred(interpolate(knots, A, Gridded(D())))
- @test parent(itp) === A
- @inferred(getindex(itp, 2, 2))
- @inferred(getindex(itp, CartesianIndex((2,2))))
- for j = 2:size(A,2)-1, i = 2:size(A,1)-1
- @test itp[i,j] ≈ A[i,j]
- @test itp[CartesianIndex((i,j))] ≈ A[i,j]
- end
- @test itp[knots...] ≈ A
- @inferred(getindex(itp, knots...))
- # compare scalar indexing and vector indexing
- x, y = knots[1] .+ 0.1, knots[2] .+ 0.6
- v = itp[x,y]
- for j = 1:length(y), i = 1:length(x)
- @test v[i,j] ≈ itp[x[i],y[j]]
- end
- # check the fallback vector indexing
- x = [2.3,2.2] # non-increasing order
- y = [3.5,2.8]
- v = itp[x,y]
- for j = 1:length(y), i = 1:length(x)
- @test v[i,j] ≈ itp[x[i],y[j]]
- end
- # compare against BSpline
- itpb = @inferred(interpolate(A, BSpline(D()), G()))
- for y in range(1.1, stop=5.9, length=101), x in range(1.1, stop=4.9, length=101)
- @test itp[x,y] ≈ itpb[x,y]
- end
+ knots = (range(0.0, stop=1.0, length=length(a)),)
+ itp = @inferred(interpolate(knots, a, Gridded(D())))
+ @test itp([0.1, 0.2, 0.3]) == [itp(0.1), itp(0.2), itp(0.3)]
- A = rand(8,20)
- knots = ([x^2 for x = 1:8], [0.2y for y = 1:20])
- itp = interpolate(knots, A, Gridded(D()))
- @test itp[4,1.2] ≈ A[2,6]
-end
+ ## 2D
+ A = rand(6,5)
+ knots = (range(1, stop=size(A,1), length=size(A,1)), range(1, stop=size(A,2), length=size(A,2)))
+ itp = @inferred(interpolate(knots, A, Gridded(D())))
+ @test parent(itp) === A
+ @inferred(itp(2, 2))
+ @inferred(itp(CartesianIndex((2,2))))
+ for j = 2:size(A,2)-1, i = 2:size(A,1)-1
+ @test itp(i,j) ≈ A[i,j]
+ @test itp(CartesianIndex((i,j))) ≈ A[i,j]
+ end
+ @test itp(knots...) ≈ A
+ @inferred(itp(knots...))
+ # compare scalar indexing and vector indexing
+ x, y = front(knots[1] .+ 0.1), front(knots[2] .+ 0.6)
+ v = itp(x,y)
+ for j = 1:length(y), i = 1:length(x)
+ @test v[i,j] ≈ itp(x[i],y[j])
+ end
+ # check the fallback vector indexing
+ x = [2.3,2.2] # non-increasing order
+ y = [3.5,2.8]
+ v = itp(x,y)
+ for j = 1:length(y), i = 1:length(x)
+ @test v[i,j] ≈ itp(x[i],y[j])
+ end
+ # compare against BSpline
+ itpb = @inferred(interpolate(A, BSpline(D())))
+ for x in range(1.1, stop=5.9, length=101), y in range(1.1, stop=4.9, length=101)
+ @test itp(x,y) ≈ itpb(x,y)
+ end
+
+ A = rand(8,20)
+ knots = ([x^2 for x = 1:8], [0.2y for y = 1:20])
+ itp = interpolate(knots, A, Gridded(D()))
+ @test itp(4,1.2) ≈ A[2,6]
+ end
end
diff --git a/test/gridded/mixed.jl b/test/gridded/mixed.jl
index c24047c0..789720f2 100644
--- a/test/gridded/mixed.jl
+++ b/test/gridded/mixed.jl
@@ -1,24 +1,21 @@
-module MixedTests
+using Interpolations, Test
-using Interpolations, Compat.Test
-using Compat: range
+@testset "MixedTests" begin
+ A = rand(6,5)
+ knots = (range(1, stop=size(A,1), length=size(A,1)), range(1, stop=size(A,2), length=size(A,2)))
+ itp = @inferred(interpolate(knots, A, (Gridded(Linear()),NoInterp())))
+ @inferred(itp(2, 2))
+ @inferred(itp(CartesianIndex((2,2))))
+ for j = 2:size(A,2)-1, i = 2:size(A,1)-1
+ @test itp(i,j) ≈ A[i,j]
+ @test itp(CartesianIndex(i,j)) ≈ A[i,j]
+ end
+ @inferred(itp(knots...)) ≈ A
-A = rand(6,5)
-knots = (collect(range(1, stop=size(A,1), length=size(A,1))),collect(range(1, stop=size(A,2), length=size(A,2))))
-itp = @inferred(interpolate(knots, A, (Gridded(Linear()),NoInterp())))
-@inferred(getindex(itp, 2, 2))
-@inferred(getindex(itp, CartesianIndex((2,2))))
-for j = 2:size(A,2)-1, i = 2:size(A,1)-1
- @test itp[i,j] ≈ A[i,j]
- @test itp[CartesianIndex((i,j))] ≈ A[i,j]
-end
-@test itp[knots...] ≈ A
-@inferred(getindex(itp, knots...))
-
-A = rand(8,20)
-knots = ([x^2 for x = 1:8], [0.2y for y = 1:20])
-itp = interpolate(knots, A, Gridded(Linear()))
-@test itp[4,1.2] ≈ A[2,6]
+ A = rand(8,20)
+ knots = ([x^2 for x = 1:8], [0.2y for y = 1:20])
+ itp = interpolate(knots, A, Gridded(Linear()))
+ @test itp(4,1.2) ≈ A[2,6]
-@test_throws ErrorException interpolate(knots, A, (Gridded(Linear()),NoInterp()))
+ @test_throws ErrorException interpolate(knots, A, (Gridded(Linear()),NoInterp()))
end
diff --git a/test/gridded/runtests.jl b/test/gridded/runtests.jl
index 4491d388..31ef641b 100644
--- a/test/gridded/runtests.jl
+++ b/test/gridded/runtests.jl
@@ -2,6 +2,5 @@ module GriddedTests
include("gridded.jl")
include("mixed.jl")
-include("function-call-syntax.jl")
end
diff --git a/test/hessian.jl b/test/hessian.jl
new file mode 100644
index 00000000..2af7574d
--- /dev/null
+++ b/test/hessian.jl
@@ -0,0 +1,45 @@
+using Test, Interpolations, LinearAlgebra
+
+@testset "Hessians" begin
+ nx = 5
+ k = 2pi/(nx-1)
+ f1(x) = sin(k*(x-3) - 1)
+ A1 = Float64[f1(x) for x in 1:nx]
+ h1 = Array{Float64}(undef, 1, 1)
+ A2 = rand(Float64, nx, nx) * 100
+ h2 = Array{Float64}(undef, 2, 2)
+
+ for (A, h) in ((A1, h1), (A2, h2))
+ for itp in (interpolate(A, BSpline(Constant())),
+ interpolate(A, BSpline(Linear())))
+ if ndims(A) == 1
+ # Hessian of Constant and Linear should always be 0 in 1d
+ for x in InterpolationTestUtils.thirds(axes(A))
+ @test all(iszero, Interpolations.hessian(itp, x...))
+ @test all(iszero, Interpolations.hessian!(h, itp, x...))
+ end
+ else
+ for x in InterpolationTestUtils.thirds(axes(A))
+ check_hessian(itp, h)
+ end
+ end
+ end
+
+ for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell)
+ itp = interpolate(A, BSpline(Quadratic(BC(GT()))))
+ check_hessian(itp, h)
+ I = first(eachindex(itp))
+ @test Interpolations.hessian(itp, I) == Interpolations.hessian(itp, Tuple(I)...)
+ end
+
+ for BC in (Line, Flat, Free, Periodic), GT in (OnGrid, OnCell)
+ itp = interpolate(A, BSpline(Cubic(BC(GT()))))
+ check_hessian(itp, h)
+ end
+ end
+
+ itp = interpolate(A2, (BSpline(Quadratic(Flat(OnCell()))), NoInterp()))
+ v = A2[:, 2]
+ itpcol = interpolate(v, BSpline(Quadratic(Flat(OnCell()))))
+ @test Interpolations.hessian(itp, 3.2, 2) == Interpolations.hessian(itpcol, 3.2)
+end
diff --git a/test/io.jl b/test/io.jl
index 66fba08b..6027c13e 100644
--- a/test/io.jl
+++ b/test/io.jl
@@ -1,73 +1,70 @@
-module IOTests
-
-using Compat.Test
using Interpolations
-
-SPACE = " "
-
-@testset "BSpline" begin
- A = rand(8,20)
-
- itp = interpolate(A, BSpline(Constant()), OnCell())
- @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Constant()), OnCell()) with element type Float64"
-
- itp = interpolate(A, BSpline(Constant()), OnGrid())
- @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Constant()), OnGrid()) with element type Float64"
-
- itp = interpolate(A, BSpline(Linear()), OnGrid())
- @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Linear()), OnGrid()) with element type Float64"
-
- itp = interpolate(A, BSpline(Quadratic(Reflect())), OnCell())
- @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Quadratic(Reflect())), OnCell()) with element type Float64"
-
- itp = interpolate(A, (BSpline(Linear()), NoInterp()), OnGrid())
- @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, (BSpline(Linear()), NoInterp()), OnGrid()) with element type Float64"
-
- itp = interpolate!(copy(A), BSpline(Quadratic(InPlace())), OnCell())
- @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Quadratic(InPlace())), OnCell()) with element type Float64"
-end
-
-@testset "Gridded" begin
- A = rand(20)
- A_x = collect(1.0:2.0:40.0)
- knots = (A_x,)
- itp = interpolate(knots, A, Gridded(Linear()))
- @test summary(itp) == "20-element interpolate((::Array{Float64,1},), ::Array{Float64,1}, Gridded(Linear())) with element type Float64"
-
- A = rand(8,20)
- knots = ([x^2 for x = 1:8], [0.2y for y = 1:20])
- itp = interpolate(knots, A, Gridded(Linear()))
- @test summary(itp) == "8×20 interpolate((::Array{Int64,1},::Array{Float64,1}), ::Array{Float64,2}, Gridded(Linear())) with element type Float64"
-
- itp = interpolate(knots, A, (Gridded(Linear()),Gridded(Constant())))
- @test summary(itp) == "8×20 interpolate((::Array{Int64,1},::Array{Float64,1}), ::Array{Float64,2}, (Gridded(Linear()), Gridded(Constant()))) with element type Float64"
-end
-
-@testset "scaled" begin
- itp = interpolate(1:1.0:10, BSpline(Linear()), OnGrid())
- sitp = scale(itp, -3:.5:1.5)
- @test summary(sitp) == "10-element scale(interpolate(::Array{Float64,1}, BSpline(Linear()), OnGrid()), (-3.0:0.5:1.5,)) with element type Float64"
-
- gauss(phi, mu, sigma) = exp(-(phi-mu)^2 / (2sigma)^2)
- testfunction(x,y) = gauss(x, 0.5, 4) * gauss(y, -.5, 2)
- xs = -5:.5:5
- ys = -4:.2:4
- zs = Float64[testfunction(x,y) for x in xs, y in ys]
- itp2 = interpolate(zs, BSpline(Quadratic(Flat())), OnGrid())
- sitp2 = scale(itp2, xs, ys)
- @test summary(sitp2) == "21×41 scale(interpolate(::Array{Float64,2}, BSpline(Quadratic(Flat())), OnGrid()), (-5.0:0.5:5.0,$SPACE-4.0:0.2:4.0)) with element type Float64"
-end
-
-@testset "Extrapolation" begin
- A = rand(8,20)
-
- itpg = interpolate(A, BSpline(Linear()), OnGrid())
- etpg = extrapolate(itpg, Flat())
- @test summary(etpg) == "8×20 extrapolate(interpolate(::Array{Float64,2}, BSpline(Linear()), OnGrid()), Flat()) with element type Float64"
-
- etpf = extrapolate(itpg, NaN)
- @test summary(etpf) == "8×20 extrapolate(interpolate(::Array{Float64,2}, BSpline(Linear()), OnGrid()), NaN) with element type Float64"
-end
-
-
+using Test
+
+@testset "IO" begin
+ SPACE = " "
+
+ @testset "BSpline" begin
+ A = rand(8,20)
+
+ itp = interpolate(A, BSpline(Constant()))
+ @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Constant())) with element type Float64"
+
+ itp = interpolate(A, BSpline(Constant()))
+ @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Constant())) with element type Float64"
+
+ itp = interpolate(A, BSpline(Linear()))
+ @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Linear())) with element type Float64"
+
+ itp = interpolate(A, BSpline(Quadratic(Reflect(OnCell()))))
+ @test summary(itp) == "8×20 interpolate(OffsetArray(::Array{Float64,2}, 0:9, 0:21), BSpline(Quadratic(Reflect(OnCell())))) with element type Float64"
+
+ itp = interpolate(A, (BSpline(Linear()), NoInterp()))
+ @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, (BSpline(Linear()), NoInterp())) with element type Float64"
+
+ itp = interpolate!(copy(A), BSpline(Quadratic(InPlace(OnCell()))))
+ @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Quadratic(InPlace(OnCell())))) with element type Float64"
+ end
+
+ @testset "Gridded" begin
+ A = rand(20)
+ A_x = collect(1.0:2.0:40.0)
+ knots = (A_x,)
+ itp = interpolate(knots, A, Gridded(Linear()))
+ @test summary(itp) == "20-element interpolate((::Array{Float64,1},), ::Array{Float64,1}, Gridded(Linear())) with element type Float64"
+
+ A = rand(8,20)
+ knots = ([x^2 for x = 1:8], [0.2y for y = 1:20])
+ itp = interpolate(knots, A, Gridded(Linear()))
+ @test summary(itp) == "8×20 interpolate((::Array{Int64,1},::Array{Float64,1}), ::Array{Float64,2}, Gridded(Linear())) with element type Float64"
+
+ itp = interpolate(knots, A, (Gridded(Linear()),Gridded(Constant())))
+ @test summary(itp) == "8×20 interpolate((::Array{Int64,1},::Array{Float64,1}), ::Array{Float64,2}, (Gridded(Linear()), Gridded(Constant()))) with element type Float64"
+ end
+
+ @testset "scaled" begin
+ itp = interpolate(1:1.0:10, BSpline(Linear()))
+ sitp = scale(itp, -3:.5:1.5)
+ @test summary(sitp) == "10-element scale(interpolate(::Array{Float64,1}, BSpline(Linear())), (-3.0:0.5:1.5,)) with element type Float64"
+
+ gauss(phi, mu, sigma) = exp(-(phi-mu)^2 / (2sigma)^2)
+ testfunction(x,y) = gauss(x, 0.5, 4) * gauss(y, -.5, 2)
+ xs = -5:.5:5
+ ys = -4:.2:4
+ zs = Float64[testfunction(x,y) for x in xs, y in ys]
+ itp2 = interpolate(zs, BSpline(Quadratic(Flat(OnGrid()))))
+ sitp2 = scale(itp2, xs, ys)
+ @test summary(sitp2) == "21×41 scale(interpolate(OffsetArray(::Array{Float64,2}, 0:22, 0:42), BSpline(Quadratic(Flat(OnGrid())))), (-5.0:0.5:5.0,$SPACE-4.0:0.2:4.0)) with element type Float64"
+ end
+
+ @testset "Extrapolation" begin
+ A = rand(8,20)
+
+ itpg = interpolate(A, BSpline(Linear()))
+ etpg = extrapolate(itpg, Flat())
+ @test summary(etpg) == "8×20 extrapolate(interpolate(::Array{Float64,2}, BSpline(Linear())), Flat()) with element type Float64"
+
+ etpf = extrapolate(itpg, NaN)
+ @test summary(etpf) == "8×20 extrapolate(interpolate(::Array{Float64,2}, BSpline(Linear())), NaN) with element type Float64"
+ end
end # Module
diff --git a/test/issues/runtests.jl b/test/issues/runtests.jl
index 7d7857fa..2fe7ba5b 100644
--- a/test/issues/runtests.jl
+++ b/test/issues/runtests.jl
@@ -1,14 +1,133 @@
-module Issue34
+using Interpolations, Test, ForwardDiff
-using Interpolations, Compat.Test
+@testset "Issues" begin
+ @testset "issue 34" begin
+ A = rand(1:20, 100, 100)
-A = rand(1:20, 100, 100)
+ # In #34, this incantation throws
+ itp = interpolate(A, BSpline(Quadratic(Flat(OnCell()))))
+ # Sanity check that not only don't throw, but actually interpolate
+ for i in 1:size(A,1), j in 1:size(A,2)
+ @test itp[i,j] ≈ A[i,j]
+ end
+ end
-# In #34, this incantation throws
-itp = interpolate(A, BSpline(Quadratic(Flat())), OnCell())
-# Sanity check that not only don't throw, but actually interpolate
-for i in 1:size(A,1), j in 1:size(A,2)
- @test itp[i,j] ≈ A[i,j]
-end
+ @testset "issue 129" begin
+ xy = [3z+2y for z in range(0.0,stop=1,length=10),y in 0:0.2:1]
+ itp = interpolate((1:10,1:6),xy,(Gridded(Linear()),NoInterp()))
+ @test itp(3.3,1:6) == [itp(3.3,i) for i = 1:6]
+ sitp = scale(itp ,range(0.0,stop=1.0,length=10),1:6)
+ @test sitp(0.8,1:6) == [sitp(0.8,i) for i = 1:6]
+ end
+
+ @testset "issue 151" begin
+ V = zeros(10,10,10,10)
+ interpV = interpolate(V, BSpline(Cubic(Line(OnGrid()))))
+ @test ndims(interpV) == 4
+ end
+
+ @testset "issue 158" begin
+ A_x = 1.:2.:40.
+ A = [log(x) for x in A_x]
+ itp = interpolate(A, BSpline(Cubic(Line(OnGrid()))))
+ sitp = scale(itp, A_x)
+ @test_throws BoundsError sitp(42.0)
+ @test_throws BoundsError sitp([3.0,42.0])
+ end
+
+ @testset "issue 165" begin
+ V0 = Array{Float64, 2}(undef, 5, 6)
+ yGrid = range(-1.0,stop= 1.0,length= 5)
+ bGrid = range(2.0, stop=3.0, length=6)
+ iV = scale(interpolate(V0, BSpline(Cubic(Line(OnCell())))), yGrid, bGrid)
+ @test iV isa Interpolations.AbstractInterpolation
+ end
+
+ @testset "issue 191" begin
+ function eff(p, thing)
+ out = p[1] * p[2]
+ return out
+ end
+ function GradientWrapper(func, p, thing)
+ f(x) = func(x, thing)
+ return ForwardDiff.gradient(f,p)
+ end
+ io = IOBuffer()
+ println(io, "Start.")
+ n = 40
+ z = [i * j for i in 1:n, j in 1:n]
+ itp = interpolate(z, BSpline(Cubic(Natural(OnGrid()))))
+ p = [5.0, 6.0]
+ test = [itp, itp]
+ println(io, "Using type $(typeof(test))")
+ println(io, "Calling the derivative directly:")
+ f(x) = eff(x,test)
+ println(io, ForwardDiff.gradient(f, p))
+ println(io, "Calling the wrapper:")
+ println(io, GradientWrapper(eff, p, test))
+ test = Array{Any}(undef, 2)
+ test[1] = itp
+ test[2] = itp
+ println(io, "Using type $(typeof(test))")
+ println(io, "Calling the derivative directly:")
+ g(x) = eff(x, test)
+ println(io, ForwardDiff.gradient(g, p))
+ println(io, "Calling the wrapper:")
+ println(io, GradientWrapper(eff, p, test))
+ test = Array{AbstractInterpolation}(undef, 2)
+ test[1] = itp
+ test[2] = itp
+ println(io, "Using type $(typeof(test))")
+ println(io, "Calling the derivative directly:")
+ h(x) = eff(x, test)
+ println(io, ForwardDiff.gradient(h, p))
+ println(io, "Calling the wrapper -- this fails:")
+ println(io, GradientWrapper(eff, p, test))
+ println(io, "Done.")
+ str = String(take!(io))
+ @show str
+ @test endswith(str, "Done.\n")
+ end
+
+ @testset "issue 200" begin
+ grid = range(0, stop=2.0, length=10)
+ vals = [-exp(-x) for x in grid]
+ itp = interpolate(vals, BSpline(Cubic(Line(OnGrid()))))
+ sitp = scale(itp, grid)
+ s = sitp(0.0)
+ @test sitp([0.0, 0.0]) == [s, s]
+ end
+
+ @testset "issue 202" begin
+ xs = 1:10
+ itp = scale(interpolate(zeros(10), BSpline(Cubic(Periodic(OnGrid())))), xs)
+ extp = extrapolate(itp, Periodic())
+ @test Interpolations.gradient(extp, 2.) == [0.0]
+ @test_throws ErrorException Interpolations.gradient(extp, 2.0, 2.0)
+
+ # The issue title says 3d, so let's try a 3d case
+ A = (0.2.*(1:3)) .* (0.3.*(1:3))' .* reshape(0.4.*(1:3), 1, 1, 3)
+ itp = interpolate(A, BSpline(Linear()))
+ etp = extrapolate(itp, Line())
+ @test Interpolations.gradient(etp, 2, 2, 2) ≈ [0.2*0.6*0.8, 0.4*0.3*0.8, 0.4*0.6*0.4]
+ @test Interpolations.gradient(etp, 10, 2, 2) ≈ [0.2*0.6*0.8, 0.6*0.3*0.8, 0.6*0.6*0.4]
+ @test Interpolations.gradient(etp, 2, 10, 2) ≈ [0.2*0.9*0.8, 0.4*0.3*0.8, 0.4*0.9*0.4]
+ @test Interpolations.gradient(etp, 2, 2, 10) ≈ [0.2*0.6*1.2, 0.4*0.3*1.2, 0.4*0.6*0.4]
+ end
+
+ @testset "issue 213" begin
+ function build_itp()
+ A_x = 1.0:2.0:40.0
+ A = A_x.^2
+ knots = (A_x,)
+ interpolate(knots, A, Gridded(Linear()))
+ end
+ function itp_test(x::AbstractVector)
+ itp = build_itp()
+ return itp(x...)
+ end
+ j = ForwardDiff.gradient(itp_test, [2.0])
+ @test j ≈ Interpolations.gradient(build_itp(), 2.0)
+ end
end
diff --git a/test/linear.jl b/test/linear.jl
index 1ea64310..859e888b 100644
--- a/test/linear.jl
+++ b/test/linear.jl
@@ -1,6 +1,6 @@
module Linear1DTests
println("Testing Linear interpolation in 1D...")
-using Interpolations, Compat.Test
+using Interpolations, Test
f(x) = sin((x-3)*2pi/9 - 1)
xmax = 10
diff --git a/test/nointerp.jl b/test/nointerp.jl
index 6b9985bf..76b7843d 100644
--- a/test/nointerp.jl
+++ b/test/nointerp.jl
@@ -1,19 +1,16 @@
-module NoInterpTests
-println("Testing NoInterp...")
-using Interpolations, Compat.Test
-
-a = reshape(1:12, 3, 4)
-ai = interpolate(a, NoInterp(), OnGrid())
-@test eltype(ai) == Int
-@test ai[1,1] == 1
-@test ai[3, 3] == 9
-@test_throws InexactError ai[2.2, 2]
-@test_throws InexactError ai[2, 2.2]
-
-ae = extrapolate(ai, NaN)
-@test eltype(ae) == Float64
-@test ae[1,1] === 1.0
-@test ae[0,1] === NaN
-@test_throws InexactError ae[1.5,2]
+@testset "NoInterp" begin
+ a = reshape(1:12, 3, 4)
+ ai = interpolate(a, NoInterp())
+ @test eltype(ai) == Int
+ check_axes(ai, a)
+ check_inbounds_values(ai, a)
+ check_oob(ai)
+ @test_throws InexactError ai(2.2, 2)
+ @test_throws InexactError ai(2, 2.2)
+ # ae = extrapolate(ai, NaN)
+ # @test eltype(ae) == Float64
+ # @test ae[1,1] === 1.0
+ # @test ae[0,1] === NaN
+ # @test_throws InexactError ae(1.5,2)
end
diff --git a/test/on-grid.jl b/test/on-grid.jl
index 1195d672..e8ea07db 100644
--- a/test/on-grid.jl
+++ b/test/on-grid.jl
@@ -1,6 +1,6 @@
module OnGridTests
-using Interpolations, Compat.Test
+using Interpolations, Test
nx, ny, nz = 10, 8, 9
xg, yg, zg = 1:nx, 1:ny, 1:nz
diff --git a/test/readme-examples.jl b/test/readme-examples.jl
index 4ff6fe69..872ae77e 100644
--- a/test/readme-examples.jl
+++ b/test/readme-examples.jl
@@ -1,53 +1,54 @@
-module ReadmeExampleTests
# verify examples from README.md run
-using Interpolations, Compat.Test
-
-## Bsplines
-a = randn(5)
-A = randn(5, 5)
-
-# Nearest-neighbor interpolation
-itp = interpolate(a, BSpline(Constant()), OnCell())
-v = itp(5.4) # returns a[5]
-@test v ≈ a[5]
-
-# (Multi)linear interpolation
-itp = interpolate(A, BSpline(Linear()), OnGrid())
-v = itp(3.2, 4.1) # returns 0.9*(0.8*A[3,4]+0.2*A[4,4]) + 0.1*(0.8*A[3,5]+0.2*A[4,5])
-@test v ≈ (0.9*(0.8*A[3,4]+0.2*A[4,4]) + 0.1*(0.8*A[3,5]+0.2*A[4,5]))
-
-# Quadratic interpolation with reflecting boundary conditions
-# Quadratic is the lowest order that has continuous gradien
-itp = interpolate(A, BSpline(Quadratic(Reflect())), OnCell())
-
-# Linear interpolation in the first dimension, and no interpolation (just lookup) in the second
-itp = interpolate(A, (BSpline(Linear()), NoInterp()), OnGrid())
-v = itp(3.65, 5) # returns 0.35*A[3,5] + 0.65*A[4,5]
-@test v ≈ (0.35*A[3,5] + 0.65*A[4,5])
-
-
-## Scaled Bsplines
-A_x = 1.:2.:40.
-A = [log(x) for x in A_x]
-itp = interpolate(A, BSpline(Cubic(Line())), OnGrid())
-sitp = scale(itp, A_x)
-@test sitp(3.) ≈ log(3.) # exactly log(3.)
-@test sitp(3.5) ≈ log(3.5) atol=.1 # approximately log(3.5)
-
-# For multidimensional uniformly spaced grids
-A_x1 = 1:.1:10
-A_x2 = 1:.5:20
-f(x1, x2) = log(x1+x2)
-A = [f(x1,x2) for x1 in A_x1, x2 in A_x2]
-itp = interpolate(A, BSpline(Cubic(Line())), OnGrid())
-sitp = scale(itp, A_x1, A_x2)
-@test sitp(5., 10.) ≈ log(5 + 10) # exactly log(5 + 10)
-@test sitp(5.6, 7.1) ≈ log(5.6 + 7.1) atol=.1 # approximately log(5.6 + 7.1)
-
-## Gridded interpolation
-A = rand(8,20)
-knots = ([x^2 for x = 1:8], [0.2y for y = 1:20])
-itp = interpolate(knots, A, Gridded(Linear()))
-@test itp[4,1.2] ≈ A[2,6] atol=.1 # approximately A[2,6]
+using Interpolations, Test
+
+@testset "Readme Examples" begin
+
+ ## Bsplines
+ a = randn(6)
+ A = randn(5, 5)
+
+ # Nearest-neighbor interpolation
+ itp = interpolate(a, BSpline(Constant()))
+ v = itp(5.4) # returns a[5]
+ @test v ≈ a[5]
+
+ # (Multi)linear interpolation
+ itp = interpolate(A, BSpline(Linear()))
+ v = itp(3.2, 4.1) # returns 0.9*(0.8*A[3,4]+0.2*A[4,4]) + 0.1*(0.8*A[3,5]+0.2*A[4,5])
+ @test v ≈ (0.9*(0.8*A[3,4]+0.2*A[4,4]) + 0.1*(0.8*A[3,5]+0.2*A[4,5]))
+
+ # Quadratic interpolation with reflecting boundary conditions
+ # Quadratic is the lowest order that has continuous gradient
+ itp = interpolate(A, BSpline(Quadratic(Reflect(OnCell()))))
+
+ # Linear interpolation in the first dimension, and no interpolation (just lookup) in the second
+ itp = interpolate(A, (BSpline(Linear()), NoInterp()))
+ v = itp(3.65, 5) # returns 0.35*A[3,5] + 0.65*A[4,5]
+ @test v ≈ (0.35*A[3,5] + 0.65*A[4,5])
+
+
+ ## Scaled Bsplines
+ A_x = 1.:2.:40.
+ A = [log(x) for x in A_x]
+ itp = interpolate(A, BSpline(Cubic(Line(OnGrid()))))
+ sitp = scale(itp, A_x)
+ @test sitp(3.) ≈ log(3.) # exactly log(3.)
+ @test sitp(3.5) ≈ log(3.5) atol=.1 # approximately log(3.5)
+
+ # For multidimensional uniformly spaced grids
+ A_x1 = 1:.1:10
+ A_x2 = 1:.5:20
+ f(x1, x2) = log(x1+x2)
+ A = [f(x1,x2) for x1 in A_x1, x2 in A_x2]
+ itp = interpolate(A, BSpline(Cubic(Line(OnGrid()))))
+ sitp = scale(itp, A_x1, A_x2)
+ @test sitp(5., 10.) ≈ log(5 + 10) # exactly log(5 + 10)
+ @test sitp(5.6, 7.1) ≈ log(5.6 + 7.1) atol=.1 # approximately log(5.6 + 7.1)
+
+ ## Gridded interpolation
+ A = rand(8,20)
+ knots = ([x^2 for x = 1:8], [0.2y for y = 1:20])
+ itp = interpolate(knots, A, Gridded(Linear()))
+ @test itp(4,1.2) ≈ A[2,6] atol=.1 # approximately A[2,6]
end
diff --git a/test/runtests.jl b/test/runtests.jl
index 3f3859f8..c31533d9 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -1,36 +1,47 @@
-module RunTests
+if !isdefined(Main, :InterpolationTestUtils)
+ include("InterpolationTestUtils.jl")
+ @eval using Main.InterpolationTestUtils
+end
+
+using Test, SharedArrays, Random
+using StaticArrays, WoodburyMatrices
+ambs = detect_ambiguities(StaticArrays, WoodburyMatrices, Base, Core)
-using Compat.Test
using Interpolations
+@test isempty(setdiff(detect_ambiguities(Interpolations, Base, Core), ambs))
+const isci = get(ENV, "CI", "") in ("true", "True")
-# extrapolation tests
-include("extrapolation/runtests.jl")
-# b-spline interpolation tests
-include("b-splines/runtests.jl")
-include("nointerp.jl")
+@testset "Interpolations" begin
+ include("core.jl")
-# scaling tests
-include("scaling/runtests.jl")
+ # b-spline interpolation tests
+ include("b-splines/runtests.jl")
+ isci && println("finished b-spline")
+ include("nointerp.jl")
+ # extrapolation tests
+ include("extrapolation/runtests.jl")
-# # test gradient evaluation
-include("gradient.jl")
+ # scaling tests
+ include("scaling/runtests.jl")
-# gridded interpolation tests
-include("gridded/runtests.jl")
+ # test gradient evaluation
+ include("gradient.jl")
+ isci && println("finished gradient")
+ # test hessian evaluation
+ include("hessian.jl")
+ isci && println("finished hessian")
-# test interpolation with specific types
-include("typing.jl")
+ # gridded interpolation tests
+ include("gridded/runtests.jl")
-# Tests copied from Grid.jl's old test suite
-# include("grid.jl")
+ # test interpolation with specific types
+ include("typing.jl")
-include("issues/runtests.jl")
+ include("issues/runtests.jl")
-# include("io.jl")
-include("convenience-constructors.jl")
-include("readme-examples.jl")
+ include("io.jl")
+ include("convenience-constructors.jl")
+ include("readme-examples.jl")
end
-
-nothing
diff --git a/test/scaling/dimspecs.jl b/test/scaling/dimspecs.jl
index 2e8ab9b6..009771fa 100644
--- a/test/scaling/dimspecs.jl
+++ b/test/scaling/dimspecs.jl
@@ -1,23 +1,21 @@
-module ScalingDimspecTests
+using Interpolations, DualNumbers, Test, LinearAlgebra
-using Interpolations, DualNumbers, Compat.Test, Compat.LinearAlgebra
+@testset "ScalingDimspecTests" begin
+ xs = -pi:(2pi/10):pi-2pi/10
+ ys = -2:.1:2
+ f(x,y) = sin(x) * y^2
-xs = -pi:(2pi/10):pi-2pi/10
-ys = -2:.1:2
-f(x,y) = sin(x) * y^2
+ itp = interpolate(Float64[f(x,y) for x in xs, y in ys], (BSpline(Quadratic(Periodic(OnGrid()))), BSpline(Linear())))
+ sitp = scale(itp, xs, ys)
-itp = interpolate(Float64[f(x,y) for x in xs, y in ys], (BSpline(Quadratic(Periodic())), BSpline(Linear())), OnGrid())
-sitp = scale(itp, xs, ys)
+ for (ix,x) in enumerate(xs), (iy,y) in enumerate(ys)
+ @test ≈(sitp(x,y),f(x,y),atol=sqrt(eps(1.0)))
-for (ix,x) in enumerate(xs), (iy,y) in enumerate(ys)
- @test ≈(sitp[x,y],f(x,y),atol=sqrt(eps(1.0)))
-
- g = gradient(sitp, x, y)
- fx = epsilon(sitp[dual(x,1), dual(y,0)])
- fy = epsilon(sitp[dual(x,0), dual(y,1)])
-
- @test ≈(g[1],fx,atol=sqrt(eps(1.0)))
- @test ≈(g[2],fy,atol=sqrt(eps(1.0)))
-end
+ g = Interpolations.gradient(sitp, x, y)
+ fx = epsilon(sitp(dual(x,1), dual(y,0)))
+ fy = epsilon(sitp(dual(x,0), dual(y,1)))
+ @test ≈(g[1],fx,atol=sqrt(eps(1.0)))
+ @test ≈(g[2],fy,atol=sqrt(eps(1.0)))
+ end
end
diff --git a/test/scaling/function-call-syntax.jl b/test/scaling/function-call-syntax.jl
deleted file mode 100644
index c533c41f..00000000
--- a/test/scaling/function-call-syntax.jl
+++ /dev/null
@@ -1,118 +0,0 @@
-module ScalingFunctionCallTests
-
-using Interpolations, Compat, Compat.Test
-using Compat: range
-
-# Model linear interpolation of y = -3 + .5x by interpolating y=x
-# and then scaling to the new x range
-
-itp = interpolate(1:1.0:10, BSpline(Linear()), OnGrid())
-
-sitp = @inferred(scale(itp, -3:.5:1.5))
-@test typeof(sitp) <: Interpolations.ScaledInterpolation
-@test parent(sitp) === itp
-
-for (x,y) in zip(-3:.05:1.5, 1:.1:10)
- @test sitp(x) ≈ y
-end
-
-# Verify that it works in >1D, with different types of ranges
-
-gauss(phi, mu, sigma) = exp(-(phi-mu)^2 / (2sigma)^2)
-testfunction(x,y) = gauss(x, 0.5, 4) * gauss(y, -.5, 2)
-
-xs = -5:.5:5
-ys = -4:.2:4
-zs = Float64[testfunction(x,y) for x in xs, y in ys]
-
-itp2 = interpolate(zs, BSpline(Quadratic(Flat())), OnGrid())
-sitp2 = @inferred scale(itp2, xs, ys)
-
-for x in xs, y in ys
- @test testfunction(x,y) ≈ sitp2(x,y)
-end
-
-# Iteration
-itp = interpolate(rand(3,3,3), BSpline(Quadratic(Flat())), OnCell())
-knots = map(d->1:10:21, 1:3)
-sitp = @inferred scale(itp, knots...)
-
-iter = @inferred(eachvalue(sitp))
-
-@static if VERSION < v"0.7.0-DEV.5126"
- state = @inferred(start(iter))
- @test !(@inferred(done(iter, state)))
- val, state = @inferred(next(iter, state))
-else
- iter_next = iterate(iter)
- @test iter_next isa Tuple
- @test iter_next[1] isa Float64
- state = iter_next[2]
- inferred_next = Base.return_types(iterate, (typeof(iter),))
- @test length(inferred_next) == 1
- @test inferred_next[1] == Union{Nothing,Tuple{Float64,typeof(state)}}
- iter_next = iterate(iter, state)
- @test iter_next isa Tuple
- @test iter_next[1] isa Float64
- inferred_next = Base.return_types(iterate, (typeof(iter),typeof(state)))
- state = iter_next[2]
- @test length(inferred_next) == 1
- @test inferred_next[1] == Union{Nothing,Tuple{Float64,typeof(state)}}
-end
-
-function foo!(dest, sitp)
- i = 0
- for s in eachvalue(sitp)
- dest[i+=1] = s
- end
- dest
-end
-function bar!(dest, sitp)
- for I in CartesianIndices(size(dest))
- dest[I] = sitp(I)
- end
- dest
-end
-rfoo = Array{Float64}(undef, Interpolations.ssize(sitp))
-rbar = similar(rfoo)
-foo!(rfoo, sitp)
-bar!(rbar, sitp)
-@test rfoo ≈ rbar
-
-# with extrapolation
-END = 10
-xs = range(-5, stop=5, length=END)
-ys = map(sin, xs)
-
-function run_tests(sut::Interpolations.AbstractInterpolation{T,N,IT,OnGrid}, itp) where {T,N,IT}
- for x in xs
- @test ≈(sut[x],sin(x),atol=sqrt(eps(sin(x))))
- end
- @test sut(-5) == sut(-5.1) == sut(-15.8) == sut(-Inf) == itp(1)
- @test sut(5) == sut(5.1) == sut(15.8) == sut(Inf) == itp(END)
-end
-
-function run_tests(sut::Interpolations.AbstractInterpolation{T,N,IT,OnCell}, itp) where {T,N,IT}
- halfcell = (xs[2] - xs[1]) / 2
-
- for x in (5 + halfcell, 5 + 1.1halfcell, 15.8, Inf)
- @test sut(-x) == itp(.5)
- @test sut(x) == itp(END+.5)
- end
-end
-
-for GT in (OnGrid, OnCell)
- itp3 = interpolate(ys, BSpline(Quadratic(Flat())), GT())
-
- # Test extrapolating, then scaling
- eitp = extrapolate(itp3, Flat())
- seitp = scale(eitp, xs)
- run_tests(seitp, itp3)
-
- # Test scaling, then extrapolating
- sitp3 = scale(itp3, xs)
- esitp = extrapolate(sitp3, Flat())
- run_tests(esitp, itp3)
-end
-
-end
diff --git a/test/scaling/nointerp.jl b/test/scaling/nointerp.jl
index db3e2cef..2128990b 100644
--- a/test/scaling/nointerp.jl
+++ b/test/scaling/nointerp.jl
@@ -1,39 +1,37 @@
-module ScalingNoInterpTests
+using Interpolations, Test, LinearAlgebra, Random
-using Interpolations, Compat.Test, Compat.LinearAlgebra, Compat.Random
-using Compat: range
+@testset "ScalingNoInterpTests" begin
+ xs = -pi:2pi/10:pi
+ f1(x) = sin(x)
+ f2(x) = cos(x)
+ f3(x) = sin(x) .* cos(x)
+ f(x,y) = y == 1 ? f1(x) : (y == 2 ? f2(x) : (y == 3 ? f3(x) : error("invalid value for y (must be 1, 2 or 3, you used $y)")))
+ ys = 1:3
-xs = -pi:2pi/10:pi
-f1(x) = sin(x)
-f2(x) = cos(x)
-f3(x) = sin(x) .* cos(x)
-f(x,y) = y == 1 ? f1(x) : (y == 2 ? f2(x) : (y == 3 ? f3(x) : error("invalid value for y (must be 1, 2 or 3, you used $y)")))
-ys = 1:3
+ A = hcat(map(f1, xs), map(f2, xs), map(f3, xs))
-A = hcat(map(f1, xs), map(f2, xs), map(f3, xs))
+ itp = interpolate(A, (BSpline(Quadratic(Periodic(OnGrid()))), NoInterp()))
+ sitp = scale(itp, xs, ys)
-itp = interpolate(A, (BSpline(Quadratic(Periodic())), NoInterp()), OnGrid())
-sitp = scale(itp, xs, ys)
+ for (ix,x0) in enumerate(xs[1:end-1]), y0 in ys
+ x,y = x0, y0
+ @test ≈(sitp(x,y),f(x,y),atol=0.05)
+ end
-for (ix,x0) in enumerate(xs[1:end-1]), y0 in ys
- x,y = x0, y0
- @test ≈(sitp[x,y],f(x,y),atol=0.05)
-end
-
-@test length(gradient(sitp, pi/3, 2)) == 1
+ @test length(Interpolations.gradient(sitp, pi/3, 2)) == 1
-# check for case where initial/middle indices are NoInterp but later ones are <:BSpline
-isdefined(Random, :seed!) ? Random.seed!(1234) : srand(1234) # `srand` was renamed to `seed!`
-z0 = rand(10,10)
-za = copy(z0)
-zb = copy(z0')
+ # check for case where initial/middle indices are NoInterp but later ones are <:BSpline
+ isdefined(Random, :seed!) ? Random.seed!(1234) : srand(1234) # `srand` was renamed to `seed!`
+ z0 = rand(10,10)
+ za = copy(z0)
+ zb = copy(z0')
-itpa = interpolate(za, (BSpline(Linear()), NoInterp()), OnGrid())
-itpb = interpolate(zb, (NoInterp(), BSpline(Linear())), OnGrid())
+ itpa = interpolate(za, (BSpline(Linear()), NoInterp()))
+ itpb = interpolate(zb, (NoInterp(), BSpline(Linear())))
-rng = range(1.0, stop=19.0, length=10)
-sitpa = scale(itpa, rng, 1:10)
-sitpb = scale(itpb, 1:10, rng)
-@test gradient(sitpa, 3.0, 3) == gradient(sitpb, 3, 3.0)
+ rng = range(1.0, stop=19.0, length=10)
+ sitpa = scale(itpa, rng, 1:10)
+ sitpb = scale(itpb, 1:10, rng)
+ @test Interpolations.gradient(sitpa, 3.0, 3) == Interpolations.gradient(sitpb, 3, 3.0)
end
diff --git a/test/scaling/runtests.jl b/test/scaling/runtests.jl
index 512c294d..24235d1a 100644
--- a/test/scaling/runtests.jl
+++ b/test/scaling/runtests.jl
@@ -2,4 +2,3 @@ include("scaling.jl")
include("dimspecs.jl")
include("nointerp.jl")
include("withextrap.jl")
-include("function-call-syntax.jl")
diff --git a/test/scaling/scaling.jl b/test/scaling/scaling.jl
index ed4488fc..ace48ec6 100644
--- a/test/scaling/scaling.jl
+++ b/test/scaling/scaling.jl
@@ -1,101 +1,78 @@
-module ScalingTests
+using Interpolations
+using Test, LinearAlgebra
-using Interpolations, Compat
-using Compat.Test, Compat.LinearAlgebra
+@testset "Scaling" begin
+ # Model linear interpolation of y = -3 + .5x by interpolating y=x
+ # and then scaling to the new x range
-# Model linear interpolation of y = -3 + .5x by interpolating y=x
-# and then scaling to the new x range
+ itp = interpolate(1:1.0:10, BSpline(Linear()))
-itp = interpolate(1:1.0:10, BSpline(Linear()), OnGrid())
+ sitp = @inferred(scale(itp, -3:.5:1.5))
+ @test typeof(sitp) <: Interpolations.ScaledInterpolation
+ @test parent(sitp) === itp
-sitp = @inferred(scale(itp, -3:.5:1.5))
-@test typeof(sitp) <: Interpolations.ScaledInterpolation
-@test parent(sitp) === itp
+ for (x,y) in zip(-3:.05:1.5, 1:.1:10)
+ @test sitp(x) ≈ y
+ end
-for (x,y) in zip(-3:.05:1.5, 1:.1:10)
- @test sitp[x] ≈ y
-end
+ # Verify that it works in >1D, with different types of ranges
-# Verify that it works in >1D, with different types of ranges
+ gauss(phi, mu, sigma) = exp(-(phi-mu)^2 / (2sigma)^2)
+ testfunction(x,y) = gauss(x, 0.5, 4) * gauss(y, -.5, 2)
-gauss(phi, mu, sigma) = exp(-(phi-mu)^2 / (2sigma)^2)
-testfunction(x,y) = gauss(x, 0.5, 4) * gauss(y, -.5, 2)
+ xs = -5:.5:5
+ ys = -4:.2:4
+ zs = Float64[testfunction(x,y) for x in xs, y in ys]
-xs = -5:.5:5
-ys = -4:.2:4
-zs = Float64[testfunction(x,y) for x in xs, y in ys]
+ itp2 = interpolate(zs, BSpline(Quadratic(Flat(OnGrid()))))
+ sitp2 = @inferred scale(itp2, xs, ys)
-itp2 = interpolate(zs, BSpline(Quadratic(Flat())), OnGrid())
-sitp2 = @inferred scale(itp2, xs, ys)
+ for x in xs, y in ys
+ @test testfunction(x,y) ≈ sitp2(x,y)
+ end
-for x in xs, y in ys
- @test testfunction(x,y) ≈ sitp2[x,y]
-end
+ # Test gradients of scaled grids
+ xs = -pi:.1:pi
+ ys = map(sin, xs)
+ itp = interpolate(ys, BSpline(Linear()))
+ sitp = @inferred scale(itp, xs)
-# Test gradients of scaled grids
-xs = -pi:.1:pi
-ys = map(sin, xs)
-itp = interpolate(ys, BSpline(Linear()), OnGrid())
-sitp = @inferred scale(itp, xs)
+ for x in -pi:.1:pi
+ g = @inferred(Interpolations.gradient(sitp, x))[1]
+ @test ≈(cos(x),g,atol=0.05)
+ end
-for x in -pi:.1:pi
- g = @inferred(gradient(sitp, x))[1]
- @test ≈(cos(x),g,atol=0.05)
-end
+ # Verify that return types are reasonable
+ @inferred(sitp2(-3.4, 1.2))
+ @inferred(sitp2(-3, 1))
+ @inferred(sitp2(-3.4, 1))
-# Verify that return types are reasonable
-@inferred(getindex(sitp2, -3.4, 1.2))
-@inferred(getindex(sitp2, -3, 1))
-@inferred(getindex(sitp2, -3.4, 1))
-
-sitp32 = @inferred scale(interpolate(Float32[testfunction(x,y) for x in -5:.5:5, y in -4:.2:4], BSpline(Quadratic(Flat())), OnGrid()), -5f0:.5f0:5f0, -4f0:.2f0:4f0)
-@test typeof(@inferred(getindex(sitp32, -3.4f0, 1.2f0))) == Float32
-
-# Iteration
-itp = interpolate(rand(3,3,3), BSpline(Quadratic(Flat())), OnCell())
-knots = map(d->1:10:21, 1:3)
-sitp = @inferred scale(itp, knots...)
-
-iter = @inferred(eachvalue(sitp))
-
-@static if VERSION < v"0.7.0-DEV.5126"
- state = @inferred(start(iter))
- @test !(@inferred(done(iter, state)))
- val, state = @inferred(next(iter, state))
-else
- iter_next = iterate(iter)
- @test iter_next isa Tuple
- @test iter_next[1] isa Float64
- state = iter_next[2]
- inferred_next = Base.return_types(iterate, (typeof(iter),))
- @test length(inferred_next) == 1
- @test inferred_next[1] == Union{Nothing,Tuple{Float64,typeof(state)}}
- iter_next = iterate(iter, state)
- @test iter_next isa Tuple
- @test iter_next[1] isa Float64
- inferred_next = Base.return_types(iterate, (typeof(iter),typeof(state)))
- state = iter_next[2]
- @test length(inferred_next) == 1
- @test inferred_next[1] == Union{Nothing,Tuple{Float64,typeof(state)}}
-end
+ sitp32 = @inferred scale(interpolate(Float32[testfunction(x,y) for x in -5:.5:5, y in -4:.2:4], BSpline(Quadratic(Flat(OnGrid())))), -5f0:.5f0:5f0, -4f0:.2f0:4f0)
+ @test typeof(@inferred(sitp32(-3.4f0, 1.2f0))) == Float32
+
+ # Iteration
+ itp = interpolate(rand(3,3,3), BSpline(Quadratic(Flat(OnCell()))))
+ knots = map(d->1:10:21, 1:3)
+ sitp = @inferred scale(itp, knots...)
+
+ iter = @inferred(eachvalue(sitp))
-function foo!(dest, sitp)
- i = 0
- for s in eachvalue(sitp)
- dest[i+=1] = s
+ function foo!(dest, sitp)
+ i = 0
+ for s in eachvalue(sitp)
+ dest[i+=1] = s
+ end
+ dest
end
- dest
-end
-function bar!(dest, sitp)
- for I in CartesianIndices(size(dest))
- dest[I] = sitp[I]
+ function bar!(dest, sitp)
+ for I in CartesianIndices(size(dest))
+ dest[I] = sitp(I)
+ end
+ dest
end
- dest
-end
-rfoo = Array{Float64}(undef, Interpolations.ssize(sitp))
-rbar = similar(rfoo)
-foo!(rfoo, sitp)
-bar!(rbar, sitp)
-@test rfoo ≈ rbar
-
+ rfoo = Array{Float64}(undef, Interpolations.ssize(sitp))
+ rbar = similar(rfoo)
+ foo!(rfoo, sitp)
+ bar!(rbar, sitp)
+ @test rfoo ≈ rbar
end
diff --git a/test/scaling/withextrap.jl b/test/scaling/withextrap.jl
index 86a78ec6..77b880bd 100644
--- a/test/scaling/withextrap.jl
+++ b/test/scaling/withextrap.jl
@@ -1,40 +1,39 @@
-module ScalingWithExtrapTests
+using Interpolations, Test
-using Interpolations, Compat.Test
-using Compat: range
+@testset "ScalingWithExtrapTests" begin
-xs = range(-5, stop=5, length=10)
-ys = map(sin, xs)
+ xs = range(-5, stop=5, length=10)
+ ys = map(sin, xs)
-function run_tests(sut::Interpolations.AbstractInterpolation{T,N,IT,OnGrid}, itp) where {T,N,IT}
- for x in xs
- @test ≈(sut[x],sin(x),atol=sqrt(eps(sin(x))))
+ function run_tests(sut::Interpolations.AbstractInterpolation{T,N,IT}, itp) where {T,N,IT}
+ for x in xs
+ @test ≈(sut(x),sin(x),atol=sqrt(eps(sin(x))))
+ end
+ @test sut(-5) == sut(-5.1) == sut(-15.8) == sut(-Inf) == itp(1)
+ @test sut(5) == sut(5.1) == sut(15.8) == sut(Inf) == itp[end]
end
- @test sut[-5] == sut[-5.1] == sut[-15.8] == sut[-Inf] == itp[1]
- @test sut[5] == sut[5.1] == sut[15.8] == sut[Inf] == itp[end]
-end
-
-function run_tests(sut::Interpolations.AbstractInterpolation{T,N,IT,OnCell}, itp) where {T,N,IT}
- halfcell = (xs[2] - xs[1]) / 2
- for x in (5 + halfcell, 5 + 1.1halfcell, 15.8, Inf)
- @test sut[-x] == itp[.5]
- @test sut[x] == itp[end+.5]
+ function run_tests(sut::Interpolations.AbstractInterpolation{T,N,IT}, itp) where {T,N,IT}
+ halfcell = (xs[2] - xs[1]) / 2
+ itps, axs = Interpolations.itpinfo(itp)
+ for x in (5 + halfcell, 5 + 1.1halfcell, 15.8, Inf)
+ @test sut(-x) == itp(Interpolations.lbound(axs[1], itps[1]))
+ @test sut(x) == itp(Interpolations.ubound(axs[1], itps[1]))
+ end
end
-end
-for GT in (OnGrid, OnCell)
- itp = interpolate(ys, BSpline(Quadratic(Flat())), GT())
+ for GT in (OnGrid, OnCell)
+ itp = interpolate(ys, BSpline(Quadratic(Flat(GT()))))
- # Test extrapolating, then scaling
- eitp = extrapolate(itp, Flat())
- seitp = scale(eitp, xs)
- run_tests(seitp, itp)
+ # Test extrapolating, then scaling
+ eitp = extrapolate(itp, Flat())
+ seitp = scale(eitp, xs)
+ run_tests(seitp, itp)
- # Test scaling, then extrapolating
- sitp = scale(itp, xs)
- esitp = extrapolate(sitp, Flat())
- run_tests(esitp, itp)
-end
+ # Test scaling, then extrapolating
+ sitp = scale(itp, xs)
+ esitp = extrapolate(sitp, Flat())
+ run_tests(esitp, itp)
+ end
end
diff --git a/test/type-instantiation.jl b/test/type-instantiation.jl
index 5343c8d7..2c7cae55 100644
--- a/test/type-instantiation.jl
+++ b/test/type-instantiation.jl
@@ -1,6 +1,6 @@
module TypeInstantiationTests
-using Interpolations, Compat.Test
+using Interpolations, Test
# NO DIMSPECS
# tests that we forward types correctly to the instance constructors
diff --git a/test/typing.jl b/test/typing.jl
index 31dcba5e..9cf02eaa 100644
--- a/test/typing.jl
+++ b/test/typing.jl
@@ -1,38 +1,35 @@
-module TypingTests
+using Interpolations, Test, LinearAlgebra
-using Interpolations, Compat.Test, Compat.LinearAlgebra
+@testset "Typing" begin
+ nx = 10
+ f(x) = convert(Float32, x^3/(nx-1))
+ g(x) = convert(Float32, 3x^2/(nx-1))
-nx = 10
-f(x) = convert(Float32, x^3/(nx-1))
-g(x) = convert(Float32, 3x^2/(nx-1))
+ A = Float32[f(x) for x in 1:nx]
-A = Float32[f(x) for x in 1:nx]
+ itp = interpolate(A, BSpline(Quadratic(Flat(OnCell()))))
-itp = interpolate(A, BSpline(Quadratic(Flat())), OnCell())
+ # display(plot(
+ # layer(x=1:nx,y=[f(x) for x in 1:1//1:nx],Geom.point),
+ # layer(x=1:.1:nx,y=[itp[x] for x in 1:1//10:nx],Geom.path),
+ # ))
-# display(plot(
-# layer(x=1:nx,y=[f(x) for x in 1:1//1:nx],Geom.point),
-# layer(x=1:.1:nx,y=[itp[x] for x in 1:1//10:nx],Geom.path),
-# ))
+ for x in 3.1:.2:4.3
+ @test ≈(float(f(x)),float(itp(x)),atol=abs(0.1 * f(x)))
+ end
-for x in 3.1:.2:4.3
- @test ≈(float(f(x)),float(itp[x]),atol=abs(0.1 * f(x)))
-end
-
-@test typeof(itp[3.5f0]) == Float32
-
-for x in 3.1:.2:4.3
- @test ≈([g(x)], gradient(itp,x),atol=abs(0.1 * g(x)))
-end
+ @test typeof(itp(3.5f0)) == Float32
-@test typeof(gradient(itp, 3.5f0)[1]) == Float32
+ for x in 3.1:.2:4.3
+ @test ≈([g(x)], Interpolations.gradient(itp,x),atol=abs(0.1 * g(x)))
+ end
-# Rational element types
-R = Rational{Int}[x^2//10 for x in 1:10]
-itp = interpolate(R, BSpline(Quadratic(Free())), OnCell())
-itp[11//10]
+ @test typeof(Interpolations.gradient(itp, 3.5f0)[1]) == Float32
-@test typeof(itp[11//10]) == Rational{Int}
-@test itp[11//10] == (11//10)^2//10
+ # Rational element types
+ R = Rational{Int}[x^2//10 for x in 1:10]
+ itp = interpolate(R, BSpline(Quadratic(Free(OnCell()))))
+ @test typeof(itp(11//10)) == Rational{Int}
+ @test itp(11//10) == (11//10)^2//10
end
diff --git a/test/visual.jl b/test/visual.jl
index 5d4d4289..f103b2c2 100644
--- a/test/visual.jl
+++ b/test/visual.jl
@@ -14,22 +14,22 @@ p = plot()
if true
Btypes = (Periodic, Flat, Line, Free, Reflect)
-Itypes = (
- Constant, Linear,
- [Quadratic{T} for T in Btypes]...,
- [Cubic{T} for T in Btypes[1:end-1]]..., # no Reflect for Cubic
-)
-Etypes = (Flat, Linear, Reflect, Periodic)
Gtypes = (OnCell, OnGrid)
+degrees = (
+ Constant(), Linear(),
+ [Quadratic(T(G())) for T in Btypes, G in Gtypes]...,
+ [Cubic(T(G())) for T in Btypes[1:end-1], G in Gtypes]..., # no Reflect for Cubic
+)
+Etypes = (Flat, Line, Reflect, Periodic)
-for IT in Itypes, GT in Gtypes, ET in Etypes
- itp = extrapolate(interpolate(y1, BSpline(IT()), GT()), ET())
+for deg in degrees, ET in Etypes
+ itp = extrapolate(interpolate(y1, BSpline(deg)), ET())
stuff = Any[]
push!(stuff, layer(x=xg,y=y1,Geom.point,Theme(default_color=colorant"green")))
push!(stuff, layer(x=xf,y=[itp[x] for x in xf],Geom.path,Theme(point_size=2px)))
- title = "$(IT.name.name){$(join(map(t -> t.name.name, IT.parameters), ','))}, $(ET.name.name)"
+ title = "$deg, $(ET.name.name)"
push!(stuff, Guide.title(title))
display(plot(stuff...))
end
@@ -45,11 +45,11 @@ zg = f2.(xg, yg')
xf = -1:.1:nx+1
yf = -1:.1:ny+1
-itp2 = extrapolate(interpolate(zg, BSpline(Quadratic{Flat}()), OnCell()), Linear())
+itp2 = extrapolate(interpolate(zg, BSpline(Quadratic(Flat(OnCell()))), Line())
display(plot(
layer(x=xf,y=yf,z=[itp2[x,y] for x in xf, y in yf], Geom.contour),
- Guide.title("Quadratic{Flat}, Oncell, Linear")
+ Guide.title("Quadratic(Flat(OnCell)), Line")
))
end